您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Set your own keyboard shortcuts for entity view page on Kanka.
当前为
// ==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); /******/ /******/ })() ;