Kanka.io Keybinds

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

目前為 2024-02-10 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Kanka.io Keybinds
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.7a
  5. // @description Set your own keyboard shortcuts for entity view page on Kanka.
  6. // @author Infinite
  7. // @license MIT
  8. // @match https://app.kanka.io/w/*/entities/*
  9. // @icon https://www.google.com/s2/favicons?domain=kanka.io
  10. // @run-at document-idle
  11. // @grant none
  12. // @require https://craig.global.ssl.fastly.net/js/mousetrap/mousetrap.min.js?a4098
  13. // @require https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.min.js
  14. // ==/UserScript==
  15.  
  16. /******/ (() => { // webpackBootstrap
  17. /******/ "use strict";
  18. /******/ var __webpack_modules__ = ({
  19.  
  20. /***/ 519:
  21. /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
  22.  
  23.  
  24. var __importDefault = (this && this.__importDefault) || function (mod) {
  25. return (mod && mod.__esModule) ? mod : { "default": mod };
  26. };
  27. var _a, _b, _c, _d, _e, _f, _g;
  28. Object.defineProperty(exports, "__esModule", ({ value: true }));
  29. /* ====================================
  30. You can change these keybinds
  31. ==================================== */
  32. const keybinds = {
  33. LABEL: 'l',
  34. MOVE: 'm',
  35. HELP: '?',
  36. };
  37. const mousetrap_1 = __importDefault(__webpack_require__(802));
  38. const handlers = {
  39. [keybinds.LABEL]: function (evt, combo) {
  40. initSelector(templates.TAG_SELECT, processTagSelection);
  41. },
  42. [keybinds.MOVE]: function (evt, combo) {
  43. initSelector(templates.LOCATION_SELECT, processLocationSelection);
  44. },
  45. };
  46. const route = window.location.pathname;
  47. const campaignID = ((_a = route.match(/w\/(\d+)\//)) !== null && _a !== void 0 ? _a : [null, '0'])[1];
  48. const entityID = ((_b = route.match(/w\/\d+\/entities\/(\d+)/)) !== null && _b !== void 0 ? _b : [null, '0'])[1];
  49. 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/);
  50. const type = (_e = entityBits === null || entityBits === void 0 ? void 0 : entityBits.groups) === null || _e === void 0 ? void 0 : _e['type'];
  51. const objectID = (_f = entityBits === null || entityBits === void 0 ? void 0 : entityBits.groups) === null || _f === void 0 ? void 0 : _f['id'];
  52. const csrfToken = (_g = document.head.querySelector('meta[name="csrf-token"]')) === null || _g === void 0 ? void 0 : _g.getAttribute('content');
  53. const templates = {
  54. SELECT_ITEM: (text, image) => {
  55. if (!!image) {
  56. return $(`<span class='flex gap-2 items-center text-left'>
  57. <img src='${image}' class='rounded-full flex-none w-6 h-6'/>
  58. <span class='grow'>${text}</span>
  59. </span>`);
  60. }
  61. return $(`<span>${text}</span>`);
  62. },
  63. TAG_SELECT: () => `
  64. <select class="form-tags select2"
  65. style="width: 100%"
  66. data-url="https://app.kanka.io/w/${campaignID}/search/tags"
  67. data-allow-new="false"
  68. data-allow-clear="true"
  69. data-placeholder="Apply Tag"
  70. data-dropdown-parent="#app"
  71. </select>`,
  72. TAG_URL: (tagID) => `https://app.kanka.io/w/${campaignID}/tags/${tagID}`,
  73. TAG: (tagID, text) => `
  74. <a href="${templates.TAG_URL(tagID)}" title="Refresh to get full tooltip functionality">
  75. <span class="badge color-tag rounded-sm px-2 py-1">${text}</span>
  76. </a>`,
  77. LOCATION_SELECT: () => `
  78. <select class="form-tags select2"
  79. style="width: 100%"
  80. data-url="https://app.kanka.io/w/${campaignID}/search/locations"
  81. data-allow-new="false"
  82. data-allow-clear="true"
  83. data-placeholder="Move to..."
  84. data-dropdown-parent="#app"
  85. </select>`,
  86. LOCATION_URL: (locationID) => `https://app.kanka.io/w/${campaignID}/locations/${locationID}`,
  87. LOCATION: (locationID, text) => `
  88. <a class="name" href="https://app.kanka.io/w/${campaignID}/entities/${locationID}"
  89. title="Refresh to get full tooltip functionality">
  90. ${text}
  91. </a>`,
  92. };
  93. function createFloatingElement(template) {
  94. let floatingDiv = document.getElementById('#infinite-select2');
  95. if (!floatingDiv) {
  96. floatingDiv = document.createElement('div');
  97. floatingDiv.id = 'infinite-select2';
  98. // Add styles to make it float and position it as needed
  99. floatingDiv.style.position = 'absolute';
  100. floatingDiv.style.top = '5%';
  101. floatingDiv.style.left = '41%';
  102. floatingDiv.style.minWidth = '200px';
  103. floatingDiv.style.width = '18%';
  104. floatingDiv.style.maxWidth = '400px';
  105. }
  106. floatingDiv.innerHTML = '';
  107. $(template()).appendTo(floatingDiv);
  108. document.body.appendChild(floatingDiv);
  109. return floatingDiv;
  110. }
  111. function createPostParams() {
  112. const params = new URLSearchParams();
  113. params.append('_token', csrfToken);
  114. params.append('datagrid-action', 'batch');
  115. params.append('entity', type);
  116. params.append('mode', 'table');
  117. params.append('models', objectID);
  118. params.append('undefined', '');
  119. return params;
  120. }
  121. function post(url, body) {
  122. return fetch(url, {
  123. method: 'POST',
  124. redirect: 'follow',
  125. headers: {
  126. 'Content-Type': 'application/x-www-form-urlencoded',
  127. },
  128. body,
  129. })
  130. .then((response) => {
  131. console.log('Success:', response);
  132. })
  133. .catch((error) => {
  134. console.error('Error:', error);
  135. });
  136. }
  137. function processLocationSelection(event) {
  138. const { id: locationID, text } = event.params.data;
  139. const params = createPostParams();
  140. params.append('location_id', locationID);
  141. post(`/w/${campaignID}/bulk/process`, params)
  142. .then(() => {
  143. const locationLink = $($('[title="Location"]').next().next());
  144. locationLink.replaceWith(templates.LOCATION(locationID, text));
  145. });
  146. }
  147. function processTagSelection(event) {
  148. const { id: tagID, text } = event.params.data;
  149. const tagBar = $('.entity-tags');
  150. const existingTag = tagBar.children(`[href="${templates.TAG_URL(tagID)}"]`)[0];
  151. const params = createPostParams();
  152. if (!!existingTag) {
  153. params.append('bulk-tagging', 'remove');
  154. params.append('tags[]', tagID);
  155. params.append('save-tags', '1');
  156. post(`/w/${campaignID}/bulk/process`, params)
  157. .then(() => {
  158. existingTag.remove();
  159. });
  160. return;
  161. }
  162. params.append('entities[]', entityID);
  163. params.append('tag_id', tagID);
  164. // not sure why the API has this twice...
  165. params.append('_token', csrfToken);
  166. post(`/w/${campaignID}/tags/${tagID}/entity-add/`, params)
  167. .then((response) => {
  168. tagBar.append($(templates.TAG(tagID, text)));
  169. });
  170. }
  171. function initSelector(template, processSelection) {
  172. const floatingDiv = createFloatingElement(template);
  173. $(floatingDiv).find('select.select2')
  174. .each(function () {
  175. const me = $(this);
  176. me.select2({
  177. tags: false,
  178. placeholder: me.data('placeholder'),
  179. allowClear: me.data('allowClear') || true,
  180. language: me.data('language'),
  181. minimumInputLength: 0,
  182. dropdownParent: $(me.data('dropdownParent')) || '',
  183. width: '100%',
  184. sorter: (data) => {
  185. const term = $('input.select2-search__field').val().toLowerCase();
  186. return data.sort(byMatchiness(term));
  187. },
  188. ajax: {
  189. delay: 500, // quiet ms
  190. url: me.data('url'),
  191. dataType: 'json',
  192. data: (params) => { var _a; return ({ q: (_a = params.term) === null || _a === void 0 ? void 0 : _a.trim() }); },
  193. processResults: (data) => ({ results: data }),
  194. error: function (jqXHR, textStatus, errorThrown) {
  195. if (textStatus === 'abort') {
  196. // it does this for the empty field, I think?
  197. return;
  198. }
  199. if (jqXHR.status === 503) {
  200. window.showToast(jqXHR.responseJSON.message, 'error');
  201. }
  202. console.log('error', jqXHR, textStatus, errorThrown);
  203. return { results: [] };
  204. },
  205. cache: true
  206. },
  207. templateResult: (item) => templates.SELECT_ITEM(item.text, item.image),
  208. })
  209. .on('select2:select', processSelection)
  210. .on('select2:close', () => {
  211. setTimeout(() => { $(floatingDiv).remove(); }, 100);
  212. });
  213. setTimeout(() => { me.select2('open'); }, 0);
  214. });
  215. }
  216. function byMatchiness(term) {
  217. return (a, b) => {
  218. const textA = a.text.toLowerCase();
  219. const textB = b.text.toLowerCase();
  220. // Assign a score based on how well the option matches the search term
  221. const scoreA = textA === term ? 3 : textA.startsWith(term) ? 2 : textA.includes(term) ? 1 : 0;
  222. const scoreB = textB === term ? 3 : textB.startsWith(term) ? 2 : textB.includes(term) ? 1 : 0;
  223. // Sort by score. If the scores are equal, sort alphabetically
  224. return scoreB - scoreA || textA.localeCompare(textB);
  225. };
  226. }
  227. (function () {
  228. if (!document.body.className.includes('kanka-entity-')) {
  229. return;
  230. }
  231. for (const key in handlers) {
  232. mousetrap_1.default.bind(key, handlers[key]);
  233. }
  234. })();
  235.  
  236.  
  237. /***/ }),
  238.  
  239. /***/ 802:
  240. /***/ ((module) => {
  241.  
  242. module.exports = Mousetrap;
  243.  
  244. /***/ })
  245.  
  246. /******/ });
  247. /************************************************************************/
  248. /******/ // The module cache
  249. /******/ var __webpack_module_cache__ = {};
  250. /******/
  251. /******/ // The require function
  252. /******/ function __webpack_require__(moduleId) {
  253. /******/ // Check if module is in cache
  254. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  255. /******/ if (cachedModule !== undefined) {
  256. /******/ return cachedModule.exports;
  257. /******/ }
  258. /******/ // Create a new module (and put it into the cache)
  259. /******/ var module = __webpack_module_cache__[moduleId] = {
  260. /******/ // no module.id needed
  261. /******/ // no module.loaded needed
  262. /******/ exports: {}
  263. /******/ };
  264. /******/
  265. /******/ // Execute the module function
  266. /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  267. /******/
  268. /******/ // Return the exports of the module
  269. /******/ return module.exports;
  270. /******/ }
  271. /******/
  272. /************************************************************************/
  273. /******/
  274. /******/ // startup
  275. /******/ // Load entry module and return exports
  276. /******/ // This entry module is referenced by other modules so it can't be inlined
  277. /******/ var __webpack_exports__ = __webpack_require__(519);
  278. /******/
  279. /******/ })()
  280. ;