Kanka.io Keybinds

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

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

  1. // ==UserScript==
  2. // @name Kanka.io Keybinds
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.8.2
  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;
  28. Object.defineProperty(exports, "__esModule", ({ value: true }));
  29. /* ====================================
  30. You can change these keybinds
  31. ====================================
  32. */
  33. const keybinds = {
  34. LABEL: 'l',
  35. MOVE: 'm',
  36. HELP: '?',
  37. };
  38. /*
  39.  
  40. ## Combination of keys - generic mod helper sets cross platform shortcuts
  41. 'mod+s' => command+s / ctrl+s
  42.  
  43. ## Sequence of keys - keys separated by a space will be considered a sequence
  44. 'g i'
  45.  
  46. ## Shift key - handled magically
  47. '?' instead of 'shift+/'
  48.  
  49. ## Text fields - keyboard events will not fire in textarea, input, or select
  50. enable with [class='mousetrap']
  51.  
  52. */
  53. /* =======================================
  54. You probably shouldn't edit below
  55. ======================================= */
  56. const mousetrap_1 = __importDefault(__webpack_require__(802));
  57. function parseBodyClasses(body) {
  58. if (body instanceof HTMLElement == false) {
  59. body = body[0];
  60. }
  61. const classes = Array.from(body.classList);
  62. const entity = { id: '', entityType: 'default', type: '' };
  63. const tags = [];
  64. const regex = /^kanka-(\w+)-(\w+)$/;
  65. let tempTag = null;
  66. classes.forEach(className => {
  67. const match = className.match(regex);
  68. if (match) {
  69. const [, key, value] = match;
  70. const isValueNumeric = !isNaN(Number(value));
  71. switch (key) {
  72. case 'entity':
  73. entity[isValueNumeric ? 'id' : 'entityType'] = value;
  74. break;
  75. case 'type':
  76. entity.type = value;
  77. break;
  78. case 'tag':
  79. if (isValueNumeric) {
  80. tempTag = value;
  81. }
  82. else {
  83. tags.push({
  84. id: tempTag,
  85. entityType: value,
  86. });
  87. tempTag = null;
  88. }
  89. break;
  90. default:
  91. console.warn("what's this?", match);
  92. break;
  93. }
  94. }
  95. });
  96. return { tags, entity };
  97. }
  98. const route = window.location.pathname;
  99. const kanka = {
  100. csrfToken: (_a = document.head.querySelector('meta[name="csrf-token"]')) === null || _a === void 0 ? void 0 : _a.getAttribute('content'),
  101. route,
  102. campaignID: ((_b = route.match(/w\/(\d+)\//)) !== null && _b !== void 0 ? _b : [null, '0'])[1],
  103. entityID: ((_c = route.match(/w\/\d+\/entities\/(\d+)/)) !== null && _c !== void 0 ? _c : [null, '0'])[1],
  104. meta: parseBodyClasses(document.body),
  105. entityTypeHasLocation: ({
  106. default: {},
  107. character: { headerLink: true },
  108. location: { headerLink: true },
  109. map: { headerLink: true },
  110. organisation: { sidebarLink: true },
  111. family: { headerLink: true },
  112. creature: { sidebarLink: true, multiple: true },
  113. race: { sidebarLink: true, multiple: true },
  114. event: { sidebarLink: true },
  115. journal: { sidebarLink: true },
  116. item: { sidebarLink: true },
  117. tag: {},
  118. note: {},
  119. quest: {},
  120. }),
  121. };
  122. const handlers = {
  123. [keybinds.LABEL]: function (evt, combo) {
  124. initSelector(templates.TAG_SELECT, processTagSelection);
  125. },
  126. [keybinds.MOVE]: function (evt, combo) {
  127. initSelector(templates.LOCATION_SELECT, processLocationSelection);
  128. },
  129. [keybinds.HELP]: function (evt, combo) {
  130. // TODO show a modal describing the keybinds
  131. },
  132. };
  133. const templates = {
  134. SELECT_ELEMENT: (dataUrl, placeholder) => `
  135. <select class="form-tags select2"
  136. style="width: 100%"
  137. data-url="${dataUrl}"
  138. data-allow-new="false"
  139. data-allow-clear="true"
  140. data-placeholder="${placeholder}"
  141. data-dropdown-parent="#app"
  142. </select>`.trim(),
  143. SELECT_ITEM: (text, image) => {
  144. if (!!image) {
  145. return $(`
  146. <span class="flex gap-2 items-center text-left">
  147. <img src="${image}" class="rounded-full flex-none w-6 h-6" />
  148. <span class="grow">${text}</span>
  149. </span>`.trim());
  150. }
  151. return $(`<span>${text}</span>`);
  152. },
  153. TAG_SELECT: () => templates.SELECT_ELEMENT(`https://app.kanka.io/w/${kanka.campaignID}/search/tags`, 'Apply Tag'),
  154. TAG_URL: (tagID) => `https://app.kanka.io/w/${kanka.campaignID}/tags/${tagID}`,
  155. TAG_LINK: (tagID, text) => `
  156. <a href="${templates.TAG_URL(tagID)}" title="Refresh to get full tooltip functionality">
  157. <span class="badge color-tag rounded-sm px-2 py-1">${text}</span>
  158. </a>`.trim(),
  159. LOCATION_SELECT: () => templates.SELECT_ELEMENT(`https://app.kanka.io/w/${kanka.campaignID}/search/locations`, 'Move to...'),
  160. LOCATION_URL: (locationID) => `https://app.kanka.io/w/${kanka.campaignID}/entities/${locationID}`,
  161. LOCATION_LINK: (locationID, text) => `<a class="name" href="${templates.LOCATION_URL(locationID)}" title="Refresh to get full tooltip functionality">${text}</a>`,
  162. // TODO - get popper/tippy working to enable preview tooltips
  163. // data-toggle="tooltip-ajax" data-id="${locationID}" data-url="${templates.LOCATION_URL(locationID)}/tooltip">
  164. };
  165. /// making my own container for the select to avoid any interference
  166. function createFloatingElement(template) {
  167. let floatingDiv = document.getElementById('#infinite-select2');
  168. if (!floatingDiv) {
  169. floatingDiv = document.createElement('div');
  170. floatingDiv.id = 'infinite-select2';
  171. // Add styles to make it float and position it as needed
  172. floatingDiv.style.position = 'absolute';
  173. floatingDiv.style.top = '5%';
  174. floatingDiv.style.left = '41%';
  175. floatingDiv.style.minWidth = '200px';
  176. floatingDiv.style.width = '18%';
  177. floatingDiv.style.maxWidth = '400px';
  178. }
  179. floatingDiv.innerHTML = '';
  180. $(template()).appendTo(floatingDiv);
  181. document.body.appendChild(floatingDiv);
  182. return floatingDiv;
  183. }
  184. function createPostParams() {
  185. const params = new URLSearchParams();
  186. params.append('_token', kanka.csrfToken);
  187. params.append('datagrid-action', 'batch');
  188. params.append('entity', kanka.meta.entity.entityType);
  189. params.append('mode', 'table');
  190. params.append('models', kanka.meta.entity.id);
  191. params.append('undefined', '');
  192. return params;
  193. }
  194. function post(url, body) {
  195. return fetch(url, {
  196. method: 'POST',
  197. redirect: 'follow',
  198. headers: {
  199. 'Content-Type': 'application/x-www-form-urlencoded',
  200. },
  201. body,
  202. })
  203. .then((response) => {
  204. var _a;
  205. console.log('Success:', response);
  206. (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader().read().then(content => {
  207. const responseText = new TextDecoder().decode(content.value);
  208. const body = responseText.match(/\<body[^\>]*?\>/);
  209. if (body) {
  210. const newMeta = parseBodyClasses($(body[0]));
  211. console.log({ newMeta });
  212. }
  213. });
  214. return response.ok;
  215. })
  216. .catch((error) => {
  217. console.error('Error:', error);
  218. return error.ok;
  219. });
  220. }
  221. function processLocationSelection(event) {
  222. const { id: locationID, text } = event.params.data;
  223. const params = createPostParams();
  224. params.append('location_id', locationID);
  225. post(`/w/${kanka.campaignID}/bulk/process`, params)
  226. .then(() => {
  227. const thisEntityLocation = kanka.entityTypeHasLocation[kanka.meta.entity.entityType];
  228. if (thisEntityLocation.headerLink) {
  229. const headerLink_Location = $($('[title="Location"]').next().next());
  230. if (!!headerLink_Location) {
  231. headerLink_Location.replaceWith(templates.LOCATION_LINK(locationID, text));
  232. }
  233. }
  234. if (thisEntityLocation.sidebarLink) {
  235. const sidebar = $('#sidebar-profile-elements > div').first();
  236. let sidebar_Location = sidebar.find('.profile-location');
  237. if (!sidebar_Location) {
  238. sidebar_Location = $('<div class="element profile-location">');
  239. sidebar_Location.append($('<div class="title text-uppercase text-xs">Location</div>'));
  240. sidebar.prepend(sidebar_Location);
  241. }
  242. const link = templates.LOCATION_LINK(locationID, text);
  243. if (thisEntityLocation.multiple) {
  244. sidebar_Location.append(link);
  245. }
  246. else {
  247. sidebar_Location.find('a').replaceWith(link);
  248. }
  249. }
  250. });
  251. }
  252. function processTagSelection(event) {
  253. const { id: tagID, text } = event.params.data;
  254. const params = createPostParams();
  255. const header = $('.entity-header .entity-header-text');
  256. if (header.has('.entity-tags').length == 0) {
  257. $('<div class="entity-tags entity-header-line text-xs flex flex-wrap gap-2"></div>')
  258. .insertBefore(header.find('.header-buttons'));
  259. }
  260. const tagBar = header.find('.entity-tags');
  261. const hasTag = !!kanka.meta.tags.find(tag => tag.id == tagID);
  262. if (hasTag) {
  263. const existingTag = tagBar.children(`[href="${templates.TAG_URL(tagID)}"]`)[0];
  264. if (!!existingTag) {
  265. params.append('bulk-tagging', 'remove');
  266. params.append('tags[]', tagID);
  267. params.append('save-tags', '1');
  268. post(`/w/${kanka.campaignID}/bulk/process`, params)
  269. .then(() => {
  270. existingTag.remove();
  271. });
  272. return;
  273. }
  274. }
  275. params.append('entities[]', kanka.meta.entity.id);
  276. params.append('tag_id', tagID);
  277. post(`/w/${kanka.campaignID}/tags/${tagID}/entity-add/`, params)
  278. .then((ok) => {
  279. ok && tagBar.append($(templates.TAG_LINK(tagID, text)));
  280. });
  281. }
  282. function initSelector(template, processSelection) {
  283. const floatingDiv = createFloatingElement(template);
  284. $(floatingDiv).find('select.select2')
  285. .each(function () {
  286. const me = $(this);
  287. me.select2({
  288. tags: false,
  289. placeholder: me.data('placeholder'),
  290. allowClear: me.data('allowClear') || true,
  291. language: me.data('language'),
  292. minimumInputLength: 0,
  293. dropdownParent: $(me.data('dropdownParent')) || '',
  294. width: '100%',
  295. sorter: (data) => {
  296. const term = $('input.select2-search__field').val().toLowerCase();
  297. return data.sort(byMatchiness(term));
  298. },
  299. ajax: {
  300. delay: 500, // quiet ms
  301. url: me.data('url'),
  302. dataType: 'json',
  303. data: (params) => { var _a; return ({ q: (_a = params.term) === null || _a === void 0 ? void 0 : _a.trim() }); },
  304. processResults: (data) => ({ results: data }),
  305. error: function (jqXHR, textStatus, errorThrown) {
  306. if (textStatus === 'abort') {
  307. // it does this for the empty field, I think?
  308. return;
  309. }
  310. if (jqXHR.status === 503) {
  311. window.showToast(jqXHR.responseJSON.message, 'error');
  312. }
  313. console.log('error', jqXHR, textStatus, errorThrown);
  314. return { results: [] };
  315. },
  316. cache: true
  317. },
  318. templateResult: (item) => templates.SELECT_ITEM(item.text, item.image),
  319. })
  320. .on('select2:select', processSelection)
  321. .on('select2:close', () => {
  322. setTimeout(() => { $(floatingDiv).remove(); }, 100);
  323. });
  324. setTimeout(() => { me.select2('open'); }, 0);
  325. });
  326. }
  327. function byMatchiness(term) {
  328. return (a, b) => {
  329. const textA = a.text.toLowerCase();
  330. const textB = b.text.toLowerCase();
  331. // Assign a score based on how well the option matches the search term
  332. const scoreA = textA === term ? 3 : textA.startsWith(term) ? 2 : textA.includes(term) ? 1 : 0;
  333. const scoreB = textB === term ? 3 : textB.startsWith(term) ? 2 : textB.includes(term) ? 1 : 0;
  334. // Sort by score. If the scores are equal, sort alphabetically
  335. return scoreB - scoreA || textA.localeCompare(textB);
  336. };
  337. }
  338. (function () {
  339. if (!document.body.className.includes('kanka-entity-')) {
  340. return;
  341. }
  342. for (const key in handlers) {
  343. mousetrap_1.default.bind(key, handlers[key]);
  344. }
  345. console.log(kanka.meta);
  346. })();
  347.  
  348.  
  349. /***/ }),
  350.  
  351. /***/ 802:
  352. /***/ ((module) => {
  353.  
  354. module.exports = Mousetrap;
  355.  
  356. /***/ })
  357.  
  358. /******/ });
  359. /************************************************************************/
  360. /******/ // The module cache
  361. /******/ var __webpack_module_cache__ = {};
  362. /******/
  363. /******/ // The require function
  364. /******/ function __webpack_require__(moduleId) {
  365. /******/ // Check if module is in cache
  366. /******/ var cachedModule = __webpack_module_cache__[moduleId];
  367. /******/ if (cachedModule !== undefined) {
  368. /******/ return cachedModule.exports;
  369. /******/ }
  370. /******/ // Create a new module (and put it into the cache)
  371. /******/ var module = __webpack_module_cache__[moduleId] = {
  372. /******/ // no module.id needed
  373. /******/ // no module.loaded needed
  374. /******/ exports: {}
  375. /******/ };
  376. /******/
  377. /******/ // Execute the module function
  378. /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  379. /******/
  380. /******/ // Return the exports of the module
  381. /******/ return module.exports;
  382. /******/ }
  383. /******/
  384. /************************************************************************/
  385. /******/
  386. /******/ // startup
  387. /******/ // Load entry module and return exports
  388. /******/ // This entry module is referenced by other modules so it can't be inlined
  389. /******/ var __webpack_exports__ = __webpack_require__(519);
  390. /******/
  391. /******/ })()
  392. ;