Kanka.io Keybinds

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);
/******/ 	
/******/ })()
;