Kanka.io Keybinds

Set your own keyboard shortcuts for entity view page on Kanka.

当前为 2024-02-10 提交的版本,查看 最新版本

// ==UserScript==
// @name         Kanka.io Keybinds
// @namespace    http://tampermonkey.net/
// @version      0.7a
// @description  Set your own keyboard shortcuts for entity view page on Kanka.
// @author       Infinite
// @license      MIT
// @match        https://app.kanka.io/w/*/entities/*
// @icon         https://www.google.com/s2/favicons?domain=kanka.io
// @run-at       document-idle
// @grant        none
// @require      https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js?a4098
// @require      https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js
// ==/UserScript==

/******/ (() => { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ 519:
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {


var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _a, _b, _c, _d, _e, _f, _g;
Object.defineProperty(exports, "__esModule", ({ value: true }));
/*  ====================================
        You can change these keybinds
    ==================================== */
const keybinds = {
    LABEL: 'l',
    MOVE: 'm',
    HELP: '?',
};
const mousetrap_1 = __importDefault(__webpack_require__(802));
const handlers = {
    [keybinds.LABEL]: function (evt, combo) {
        initSelector(templates.TAG_SELECT, processTagSelection);
    },
    [keybinds.MOVE]: function (evt, combo) {
        initSelector(templates.LOCATION_SELECT, processLocationSelection);
    },
};
const route = window.location.pathname;
const campaignID = ((_a = route.match(/w\/(\d+)\//)) !== null && _a !== void 0 ? _a : [null, '0'])[1];
const entityID = ((_b = route.match(/w\/\d+\/entities\/(\d+)/)) !== null && _b !== void 0 ? _b : [null, '0'])[1];
const entityBits = (_d = (_c = document.querySelector('.icon[data-title="Edit"]')) === null || _c === void 0 ? void 0 : _c.getAttribute('href')) === null || _d === void 0 ? void 0 : _d.match(/\/(?<type>[^/]+)\/(?<id>\d+)\/edit/);
const type = (_e = entityBits === null || entityBits === void 0 ? void 0 : entityBits.groups) === null || _e === void 0 ? void 0 : _e['type'];
const objectID = (_f = entityBits === null || entityBits === void 0 ? void 0 : entityBits.groups) === null || _f === void 0 ? void 0 : _f['id'];
const csrfToken = (_g = document.head.querySelector('meta[name="csrf-token"]')) === null || _g === void 0 ? void 0 : _g.getAttribute('content');
const templates = {
    SELECT_ITEM: (text, image) => {
        if (!!image) {
            return $(`<span class='flex gap-2 items-center text-left'>
    <img src='${image}' class='rounded-full flex-none w-6 h-6'/>
    <span class='grow'>${text}</span>
</span>`);
        }
        return $(`<span>${text}</span>`);
    },
    TAG_SELECT: () => `
<select class="form-tags select2"
    style="width: 100%"
    data-url="https://app.kanka.io/w/${campaignID}/search/tags"
    data-allow-new="false"
    data-allow-clear="true"
    data-placeholder="Apply Tag"
    data-dropdown-parent="#app"
</select>`,
    TAG_URL: (tagID) => `https://app.kanka.io/w/${campaignID}/tags/${tagID}`,
    TAG: (tagID, text) => `
<a href="${templates.TAG_URL(tagID)}" title="Refresh to get full tooltip functionality">
    <span class="badge color-tag rounded-sm px-2 py-1">${text}</span>
</a>`,
    LOCATION_SELECT: () => `
<select class="form-tags select2"
    style="width: 100%"
    data-url="https://app.kanka.io/w/${campaignID}/search/locations"
    data-allow-new="false"
    data-allow-clear="true"
    data-placeholder="Move to..."
    data-dropdown-parent="#app"
</select>`,
    LOCATION_URL: (locationID) => `https://app.kanka.io/w/${campaignID}/locations/${locationID}`,
    LOCATION: (locationID, text) => `
<a class="name" href="https://app.kanka.io/w/${campaignID}/entities/${locationID}" 
    title="Refresh to get full tooltip functionality">
    ${text}
</a>`,
};
function createFloatingElement(template) {
    let floatingDiv = document.getElementById('#infinite-select2');
    if (!floatingDiv) {
        floatingDiv = document.createElement('div');
        floatingDiv.id = 'infinite-select2';
        // Add styles to make it float and position it as needed
        floatingDiv.style.position = 'absolute';
        floatingDiv.style.top = '5%';
        floatingDiv.style.left = '41%';
        floatingDiv.style.minWidth = '200px';
        floatingDiv.style.width = '18%';
        floatingDiv.style.maxWidth = '400px';
    }
    floatingDiv.innerHTML = '';
    $(template()).appendTo(floatingDiv);
    document.body.appendChild(floatingDiv);
    return floatingDiv;
}
function createPostParams() {
    const params = new URLSearchParams();
    params.append('_token', csrfToken);
    params.append('datagrid-action', 'batch');
    params.append('entity', type);
    params.append('mode', 'table');
    params.append('models', objectID);
    params.append('undefined', '');
    return params;
}
function post(url, body) {
    return fetch(url, {
        method: 'POST',
        redirect: 'follow',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body,
    })
        .then((response) => {
        console.log('Success:', response);
    })
        .catch((error) => {
        console.error('Error:', error);
    });
}
function processLocationSelection(event) {
    const { id: locationID, text } = event.params.data;
    const params = createPostParams();
    params.append('location_id', locationID);
    post(`/w/${campaignID}/bulk/process`, params)
        .then(() => {
        const locationLink = $($('[title="Location"]').next().next());
        locationLink.replaceWith(templates.LOCATION(locationID, text));
    });
}
function processTagSelection(event) {
    const { id: tagID, text } = event.params.data;
    const tagBar = $('.entity-tags');
    const existingTag = tagBar.children(`[href="${templates.TAG_URL(tagID)}"]`)[0];
    const params = createPostParams();
    if (!!existingTag) {
        params.append('bulk-tagging', 'remove');
        params.append('tags[]', tagID);
        params.append('save-tags', '1');
        post(`/w/${campaignID}/bulk/process`, params)
            .then(() => {
            existingTag.remove();
        });
        return;
    }
    params.append('entities[]', entityID);
    params.append('tag_id', tagID);
    // not sure why the API has this twice...
    params.append('_token', csrfToken);
    post(`/w/${campaignID}/tags/${tagID}/entity-add/`, params)
        .then((response) => {
        tagBar.append($(templates.TAG(tagID, text)));
    });
}
function initSelector(template, processSelection) {
    const floatingDiv = createFloatingElement(template);
    $(floatingDiv).find('select.select2')
        .each(function () {
        const me = $(this);
        me.select2({
            tags: false,
            placeholder: me.data('placeholder'),
            allowClear: me.data('allowClear') || true,
            language: me.data('language'),
            minimumInputLength: 0,
            dropdownParent: $(me.data('dropdownParent')) || '',
            width: '100%',
            sorter: (data) => {
                const term = $('input.select2-search__field').val().toLowerCase();
                return data.sort(byMatchiness(term));
            },
            ajax: {
                delay: 500, // quiet ms
                url: me.data('url'),
                dataType: 'json',
                data: (params) => { var _a; return ({ q: (_a = params.term) === null || _a === void 0 ? void 0 : _a.trim() }); },
                processResults: (data) => ({ results: data }),
                error: function (jqXHR, textStatus, errorThrown) {
                    if (textStatus === 'abort') {
                        // it does this for the empty field, I think?
                        return;
                    }
                    if (jqXHR.status === 503) {
                        window.showToast(jqXHR.responseJSON.message, 'error');
                    }
                    console.log('error', jqXHR, textStatus, errorThrown);
                    return { results: [] };
                },
                cache: true
            },
            templateResult: (item) => templates.SELECT_ITEM(item.text, item.image),
        })
            .on('select2:select', processSelection)
            .on('select2:close', () => {
            setTimeout(() => { $(floatingDiv).remove(); }, 100);
        });
        setTimeout(() => { me.select2('open'); }, 0);
    });
}
function byMatchiness(term) {
    return (a, b) => {
        const textA = a.text.toLowerCase();
        const textB = b.text.toLowerCase();
        // Assign a score based on how well the option matches the search term
        const scoreA = textA === term ? 3 : textA.startsWith(term) ? 2 : textA.includes(term) ? 1 : 0;
        const scoreB = textB === term ? 3 : textB.startsWith(term) ? 2 : textB.includes(term) ? 1 : 0;
        // Sort by score. If the scores are equal, sort alphabetically
        return scoreB - scoreA || textA.localeCompare(textB);
    };
}
(function () {
    if (!document.body.className.includes('kanka-entity-')) {
        return;
    }
    for (const key in handlers) {
        mousetrap_1.default.bind(key, handlers[key]);
    }
})();


/***/ }),

/***/ 802:
/***/ ((module) => {

module.exports = Mousetrap;

/***/ })

/******/ 	});
/************************************************************************/
/******/ 	// The module cache
/******/ 	var __webpack_module_cache__ = {};
/******/ 	
/******/ 	// The require function
/******/ 	function __webpack_require__(moduleId) {
/******/ 		// Check if module is in cache
/******/ 		var cachedModule = __webpack_module_cache__[moduleId];
/******/ 		if (cachedModule !== undefined) {
/******/ 			return cachedModule.exports;
/******/ 		}
/******/ 		// Create a new module (and put it into the cache)
/******/ 		var module = __webpack_module_cache__[moduleId] = {
/******/ 			// no module.id needed
/******/ 			// no module.loaded needed
/******/ 			exports: {}
/******/ 		};
/******/ 	
/******/ 		// Execute the module function
/******/ 		__webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ 	
/******/ 		// Return the exports of the module
/******/ 		return module.exports;
/******/ 	}
/******/ 	
/************************************************************************/
/******/ 	
/******/ 	// startup
/******/ 	// Load entry module and return exports
/******/ 	// This entry module is referenced by other modules so it can't be inlined
/******/ 	var __webpack_exports__ = __webpack_require__(519);
/******/ 	
/******/ })()
;