Greasy Fork is available in English.

Google Searching Tags Box

Make your searches easier by adding tags to your search queries with one click

  1. // ==UserScript==
  2. // @name Google Searching Tags Box
  3. // @version 2.1.0
  4. // @description Make your searches easier by adding tags to your search queries with one click
  5. // @author OpenDec
  6. // @match https://www.google.com/*
  7. // @match https://www.google.co.jp/*
  8. // @match https://www.google.co.uk/*
  9. // @match https://www.google.es/*
  10. // @match https://www.google.ca/*
  11. // @match https://www.google.de/*
  12. // @match https://www.google.it/*
  13. // @match https://www.google.fr/*
  14. // @match https://www.google.com.au/*
  15. // @match https://www.google.com.tw/*
  16. // @match https://www.google.nl/*
  17. // @match https://www.google.com.br/*
  18. // @match https://www.google.com.tr/*
  19. // @match https://www.google.be/*
  20. // @match https://www.google.com.gr/*
  21. // @match https://www.google.co.in/*
  22. // @match https://www.google.com.mx/*
  23. // @match https://www.google.dk/*
  24. // @match https://www.google.com.ar/*
  25. // @match https://www.google.ch/*
  26. // @match https://www.google.cl/*
  27. // @match https://www.google.at/*
  28. // @match https://www.google.co.kr/*
  29. // @match https://www.google.ie/*
  30. // @match https://www.google.com.co/*
  31. // @match https://www.google.pl/*
  32. // @match https://www.google.pt/*
  33. // @match https://www.google.com.pk/*
  34. // @include https://www.google.tld/*
  35. // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com
  36. // @grant GM.getValue
  37. // @grant GM.setValue
  38. // @grant GM.deleteValue
  39. // @run-at document-start
  40. // @namespace https://greasyfork.org/users/873547
  41. // @license MIT
  42. // ==/UserScript==
  43.  
  44. /* jshint esversion: 9 */
  45.  
  46. window.addEventListener('DOMContentLoaded', function stageReady(){
  47. 'use strict';
  48.  
  49. // --------------------------------------------------
  50. // --- INIT ---
  51. // --------------------------------------------------
  52.  
  53. addGlobalStyle(css(getColorMode(isDarkRGB(RGBCSS2Obj(window.getComputedStyle(document.body).backgroundColor)) ? 'dark' : 'light')));
  54.  
  55. const _input = document.querySelector('input.gLFyf, textarea.gLFyf, #REsRA');
  56. const _container = document.querySelector('div[jsname=RNNXgb]');
  57. const _tagsBoxWrapper = document.createElement('div');
  58. const _tagsBox = document.createElement('div');
  59. const _deletingZone = document.createElement('div');
  60. const _contextMenu = document.createElement('div');
  61. const _inputFile = document.createElement('input');
  62. const _arrTags = [];
  63. const _actions = {};
  64. const _settings = {};
  65. const _defaultSettings = {tagsWidth: 'S', labelsCase: 'a'};
  66. const _paramsKeys = {S: 'tagsWidth', L: 'tagsWidth', A: 'tagsWidth', a: 'labelsCase', c: 'labelsCase', C: 'labelsCase'};
  67. /* _paramsKeys values:
  68. S: Small tags width
  69. L: Large tags width
  70. A: Auto tags width
  71. a: Labels with letter-case as typed by the user
  72. c: Lowercase labels
  73. C: Uppercase labels
  74. */
  75. let _tagIdCounter = 0;
  76. let _draggedItem = null;
  77. let _draggedData = null;
  78. let _dragenterBoxCounter = 0;
  79. let _history;
  80.  
  81. // --------------------------------------------------
  82. // --- PAGE DRAWING ---
  83. // --------------------------------------------------
  84.  
  85. _tagsBoxWrapper.id = 'od-tagsbox-wrapper';
  86. _tagsBox.id = 'od-tagsbox';
  87. _tagsBoxWrapper.appendChild(_tagsBox);
  88. _container.parentNode.insertBefore(_tagsBoxWrapper, _container.nextSibling);
  89.  
  90. function updatePage(str, options = {}){
  91. const res = updateData(str, options);
  92. applyParam('tagsWidth');
  93. applyParam('labelsCase');
  94. redrawBox(options);
  95. saveData();
  96. if (res.error){
  97. fxGlowErr(_tagsBox);
  98. modal(res.error);
  99. } else if (options.glow) fxGlow(_tagsBox);
  100. return res;
  101. }
  102. function redrawBox(options = {}){
  103. let delay = 0;
  104. let index = 0;
  105. const arrRemoved = [];
  106. const items = [..._arrTags];
  107.  
  108. const plus = document.getElementById('od-addtag');
  109. if (plus) index = getItemIndex(plus);
  110. else {
  111. items.splice(options.plusIndex || 0, 0, {action: 'add', id: 'od-addtag', color: options.plusColor});
  112. }
  113.  
  114. items.forEach(tag=>{
  115. if (tag.action === 'remove'){
  116. arrRemoved.push(tag);
  117. } else if (tag.action === 'update'){
  118. fxGlow(setItem(tag));
  119. } else if (tag.action === 'add'){
  120. if (options.noFxIn) addItem(tag, ++index);
  121. else {
  122. (options.noSlideIn ? fxFadein : fxSlideFadein)(addItem(tag, ++index), 400, delay);
  123. delay += 30;
  124. }
  125. }
  126. delete tag.action;
  127. });
  128. arrRemoved.forEach(tag=>{
  129. removeItem(tag);
  130. });
  131. }
  132. function applyParam(param, key){
  133. if (key) setParam(param, key);
  134. else key = _settings[param] || _defaultSettings[param];
  135. // Remove the class with the specific prefix from the BOX and reapply it with the new key
  136. _tagsBox.className = _tagsBox.className.replace(new RegExp('(^| )' + param + '-[^ ]($| )'), ' ');
  137. _tagsBox.classList.add(param + '-' + key);
  138. // Select context menu item
  139. const old = _contextMenu.querySelector('li[data-group="' + param + '"].od-checked');
  140. if (old) old.classList.remove('od-checked');
  141. _contextMenu.querySelector('li[data-group="' + param + '"][data-key="' + key + '"]').classList.add('od-checked');
  142. }
  143.  
  144. // --------------------------------------------------
  145. // --- DRAG-AND-DROP SETTINGS ---
  146. // --------------------------------------------------
  147.  
  148. // BOX HANDLERS
  149.  
  150. _tagsBox.addEventListener('dragenter', function (e){
  151. e.preventDefault();
  152. e.dataTransfer.dropEffect = _draggedItem ? 'move' : _draggedData ? 'copy' : 'none' ;
  153. });
  154. _tagsBox.addEventListener('dragover', function (e){
  155. e.preventDefault();
  156. e.dataTransfer.dropEffect = _draggedItem ? 'move' : _draggedData ? 'copy' : 'none' ;
  157. });
  158.  
  159. // ITEMS HANDLERS
  160.  
  161. function itemDragstart (e){
  162. if (!e.target.matches('.od-item:not(.od-edit-tag)')){
  163. e.preventDefault();
  164. return false;
  165. }
  166. e.dataTransfer.effectAllowed = "move";
  167. _deletingZone.classList.add('od-dragging');
  168. _tagsBox.classList.add('od-dragging-item');
  169. _draggedItem = e.target;
  170. _draggedItem.classList.add('od-draggeditem');
  171. _draggedItem.dataset.startingIndex = getItemIndex(_draggedItem);
  172. }
  173. function itemDragend (e){
  174. const startingIndex = +_draggedItem.dataset.startingIndex;
  175. const currentIndex = getItemIndex(_draggedItem);
  176. const belowitem = _tagsBox.querySelector('.od-belowitem');
  177. if (currentIndex !== startingIndex){
  178. if (e.dataTransfer.dropEffect === 'none'){
  179. // If ESC was pressed or the drop target is invalid, cancel the move
  180. _tagsBox.insertBefore(_draggedItem, _tagsBox.children[+(currentIndex < startingIndex) + startingIndex]);
  181. } else if (_draggedItem.id === 'od-addtag'){
  182. _history.add();
  183. } else if (belowitem !== null){
  184. // Reorder and save the data
  185. _arrTags.length = 0;
  186. [..._tagsBox.children].forEach(function(tag){
  187. if (!tag.dataset.text) return;
  188. _arrTags.push({
  189. label: tag.dataset.label,
  190. text: tag.dataset.text,
  191. color: tag.dataset.color,
  192. id: tag.id
  193. });
  194. });
  195. saveData();
  196. }
  197. }
  198. if (belowitem) belowitem.classList.remove('od-belowitem');
  199. delete _draggedItem.dataset.startingIndex;
  200. _draggedItem.classList.remove('od-draggeditem');
  201. _draggedItem = null;
  202. _deletingZone.classList.remove('od-dragging', 'od-dragging-hover');
  203. _tagsBox.classList.remove('od-dragging-item');
  204. }
  205. function itemDragenter (e){
  206. e.preventDefault();
  207. if (_draggedItem === null && _draggedData === null) e.dataTransfer.effectAllowed = "none";
  208. if (_draggedItem === null) return false;
  209. let swapItem = e.target;
  210. swapItem.classList.add('od-belowitem');
  211. swapItem = swapItem === _draggedItem.nextSibling ? swapItem.nextSibling : swapItem;
  212. _tagsBox.insertBefore(_draggedItem, swapItem);
  213. }
  214. function itemDragleave (e){
  215. e.target.classList.remove('od-belowitem');
  216. }
  217. function itemDragover (e){
  218. e.preventDefault();
  219. e.dataTransfer.dropEffect = _draggedItem === null && _draggedData === null ? 'none' : 'move';
  220. }
  221. function setDraggable(item, b=true){
  222. item.draggable = b;
  223. }
  224.  
  225. // TAG DELETING ZONE
  226.  
  227. _deletingZone.id = 'od-deletingZone';
  228. _tagsBoxWrapper.appendChild(_deletingZone);
  229.  
  230. _deletingZone.addEventListener('dragenter', function (e){
  231. e.preventDefault();
  232. if (_draggedItem.id !== 'od-addtag') _deletingZone.classList.add('od-dragging-hover');
  233. });
  234. _deletingZone.addEventListener('dragleave', function (e){
  235. e.preventDefault();
  236. _deletingZone.classList.remove('od-dragging-hover');
  237. });
  238. _deletingZone.addEventListener('dragover', function (e){
  239. e.preventDefault();
  240. e.dataTransfer.dropEffect = _draggedItem.id === 'od-addtag' ? 'none' : 'move';
  241. });
  242. _deletingZone.addEventListener('drop', function (e){
  243. e.preventDefault();
  244. if (_draggedItem.id !== 'od-addtag'){
  245. removeItem(getTagById(_draggedItem.id));
  246. saveData();
  247. }
  248. });
  249.  
  250. // --------------------------------------------------
  251. // --- INPUT FILE ---
  252. // --------------------------------------------------
  253.  
  254. _inputFile.id = 'od-inputFile';
  255. _inputFile.type = 'file';
  256. _inputFile.style = 'display:none';
  257. _inputFile.accept = '.txt';
  258. _inputFile.addEventListener('change', function (){ importData(this.files); });
  259. _tagsBoxWrapper.appendChild(_inputFile);
  260.  
  261. // --------------------------------------------------
  262. // --- CONTEXT MENU ---
  263. // --------------------------------------------------
  264.  
  265. _contextMenu.id = 'od-contextMenu';
  266. _contextMenu.innerHTML = `<ul>
  267. <li><span><i>🔠</i> Tag properties</span>
  268. <ul>
  269. <li data-action="setText" class="od-over-tag"><span><i>✏️</i> Tag text<kbd>Shift + Click</kbd></span>
  270. <li data-action="setLabel" class="od-over-tag"><span><i>🏷️</i> Custom label <kbd>Alt + Click</kbd></span>
  271. <li data-action="setColor" class="od-over-tag od-over-plus"><span><i id="od-setcolor"></i> Color <kbd>Ctrl + Click</kbd></span>
  272. </ul>
  273. <li></li>
  274. <li><span><i>🧰</i> Edit</span>
  275. <ul>
  276. <li data-action="undo" id="od-contextMenu-undo"><span><i>↶</i> Undo <kbd>Ctrl + Z</kbd></span>
  277. <li data-action="redo" id="od-contextMenu-redo"><span><i>↷</i> Redo <kbd>Ctrl + Y</kbd></span>
  278. <li></li>
  279. <li data-action="copyTags"><span><i>📋</i> Copy Tags <kbd>Ctrl + C</kbd></span>
  280. <li data-action="pasteTags"><span><i>📌</i> Paste Tags <kbd>Ctrl + V</kbd></span>
  281. <li></li>
  282. <li data-action="clearBox"><span><i>🗑️</i> Clear the Tags Box</span>
  283. </ul>
  284. <li></li>
  285. <li data-action="importTags"><span><i>📂</i> Import Tags from txt</span>
  286. <li data-action="exportTags"><span><i>💾</i> Export Tags as txt</span>
  287. <li></li>
  288. <li><span><i>📐</i> Tags width</span>
  289. <ul>
  290. <li class="od-checkable" data-group="tagsWidth" data-key="S"><i>◻</i> Small Tags width
  291. <li class="od-checkable" data-group="tagsWidth" data-key="L"><i>▭</i> Large Tags width
  292. <li class="od-checkable" data-group="tagsWidth" data-key="A"><i>⇿</i> Auto Tags width
  293. </ul>
  294. <li><span><i>Aa</i> Label case</span>
  295. <ul>
  296. <li class="od-checkable" data-group="labelsCase" data-key="a"><i>Aa</i> As it is
  297. <li class="od-checkable" data-group="labelsCase" data-key="c"><i>aa</i> Lowercase
  298. <li class="od-checkable" data-group="labelsCase" data-key="C"><i>AA</i> Uppercase
  299. </ul>
  300. </ul>`;
  301.  
  302. _tagsBox.addEventListener('contextmenu', contextMenuOpen);
  303. onoffListeners(_contextMenu, 'mousedown contextmenu wheel', function(e){
  304. e.preventDefault();
  305. e.stopPropagation();
  306. }, true);
  307.  
  308. _contextMenu.querySelector('ul').addEventListener('mouseup', contextMenuClick);
  309. _tagsBoxWrapper.appendChild(_contextMenu);
  310.  
  311. function contextMenuOpen(e){
  312. const item = e.target;
  313. if (item.tagName.toLowerCase() === 'input') return;
  314. e.preventDefault();
  315. const isOverItem = item.classList.contains('od-item');
  316. const isOverPlus = item.id === 'od-addtag';
  317. // Toggle functions for the active BOX item
  318. _contextMenu.querySelectorAll('li.od-over-tag').forEach(function(li){
  319. li.classList.toggle('od-disabled', !isOverItem || isOverPlus && !li.classList.contains('od-over-plus'));
  320. });
  321. if (isOverItem){
  322. activateItem(item);
  323. document.getElementById('od-setcolor').style.color = '#' + item.dataset.color;
  324. }
  325. // Toggle undo/redo functions
  326. document.getElementById('od-contextMenu-undo').classList.toggle('od-disabled', _history.done.length <= 1);
  327. document.getElementById('od-contextMenu-redo').classList.toggle('od-disabled', _history.reverted.length === 0);
  328.  
  329. keepBoxOpen();
  330.  
  331. // Init position
  332. const x = e.clientX - 1;
  333. const y = e.clientY - 1;
  334. _contextMenu.style = 'top: ' + y + 'px; left: ' + x + 'px';
  335. _contextMenu.classList.add('open');
  336.  
  337. // Fix position to prevent overflow
  338. const rect = _contextMenu.getBoundingClientRect();
  339. const fixX = Math.max(0, Math.round(x - Math.max(0, rect.right - window.innerWidth)));
  340. const fixY = rect.bottom > window.innerHeight ? Math.max(0, Math.round(rect.top - _contextMenu.offsetHeight)) : y;
  341. _contextMenu.style = 'top: ' + fixY + 'px; left: ' + fixX + 'px';
  342.  
  343. _contextMenu.querySelectorAll(':scope > ul > li > ul').forEach(function(sub){
  344. const item = sub.parentElement;
  345. item.classList.remove('od-sub-left');
  346. const rect = sub.getBoundingClientRect();
  347. if (rect.right > window.innerWidth) item.classList.add('od-sub-left');
  348. });
  349.  
  350. // Enable closing listeners
  351. setTimeout(function(){
  352. onoffListeners(window, 'wheel resize blur mousedown contextmenu', contextMenuClose, true);
  353. }, 1);
  354. _contextMenu.addEventListener('keydown', contextMenuEsc);
  355. }
  356. function contextMenuEsc(e){
  357. if (e.keyCode === 27) contextMenuClose();
  358. }
  359. function contextMenuClose(){
  360. unlockBoxOpen();
  361. deactivateItem();
  362.  
  363. setTimeout(function(){
  364. _contextMenu.classList.remove('open');
  365. _contextMenu.removeAttribute('style');
  366. onoffListeners(window, 'wheel resize blur mousedown contextmenu', contextMenuClose, false);
  367. }, 1);
  368. _contextMenu.removeEventListener('keydown', contextMenuEsc);
  369. }
  370. function contextMenuClick(e){
  371. e.preventDefault();
  372. e.stopPropagation();
  373. if (_contextMenu.querySelector('ul').contains(e.target)){
  374. const menuItem = e.target.closest('li[data-action], li.od-checkable');
  375. if (!menuItem) return;
  376. if (menuItem.classList.contains('od-checkable')) _actions.checkItem(menuItem);
  377. if (menuItem.dataset.action) _actions[menuItem.dataset.action]();
  378. contextMenuClose();
  379. }
  380. }
  381.  
  382. // --------------------------------------------------
  383. // --- CONTEXT MENU ACTIONS ---
  384. // --------------------------------------------------
  385.  
  386. _actions.setText = function(){
  387. editTagText(getActiveItem());
  388. };
  389. _actions.setLabel = function(){
  390. editTagLabel(getActiveItem());
  391. };
  392. _actions.setColor = function(){
  393. openColorPicker(getActiveItem());
  394. };
  395. _actions.undo = function(){
  396. _history.undo();
  397. };
  398. _actions.redo = function(){
  399. _history.redo();
  400. };
  401. _actions.copyTags = function(){
  402. // Exit if no data to copy
  403. if (_arrTags.length === 0) return;
  404.  
  405. const str = encodeData();
  406. clipboardCopy(str)
  407. .then(function(){
  408. fxGlow(_tagsBox);
  409. })
  410. .catch(function(){
  411. // Cannot write on clipboard
  412. modal(50);
  413. // Allow to copy the data from the search field
  414. _input.value = str;
  415. })
  416. ;
  417. };
  418. _actions.pasteTags = function(){
  419. clipboardPaste()
  420. .then(function(str){
  421. updatePage(str, {glow: true, from: 'paste'});
  422. })
  423. .catch(function(){
  424. // Cannot read clipboard data
  425. modal(60);
  426. })
  427. ;
  428. };
  429. _actions.importTags = function(){
  430. _inputFile.value = null;
  431. _inputFile.click();
  432. };
  433. _actions.exportTags = function(){
  434. exportData(encodeData());
  435. };
  436. _actions.clearBox = function(){
  437. const addtag = document.getElementById('od-addtag');
  438. _arrTags.length = 0;
  439. _tagsBox.innerHTML = '';
  440. _tagsBox.append(addtag);
  441. saveData();
  442. fxGlow(_tagsBox);
  443. };
  444. _actions.checkItem = function(menuItem){
  445. if (menuItem.dataset.group){
  446. // If group, select this item
  447. applyParam(menuItem.dataset.group, menuItem.dataset.key);
  448. saveData();
  449. } else {
  450. // If single item, toggle check
  451. menuItem.classList.toggle('od-checked');
  452. }
  453. };
  454.  
  455. // --------------------------------------------------
  456. // --- GENERIC FUNCTIONS ---
  457. // --------------------------------------------------
  458.  
  459. function isNothingFocused(denyIfTextFieldsFocused){
  460. // Returns TRUE if nothing is selected on the page
  461. const actEl = document.activeElement;
  462. return (
  463. (
  464. !(// check if there are no focused fields
  465. denyIfTextFieldsFocused &&
  466. actEl &&
  467. (
  468. actEl.tagName.toLowerCase() === 'input' &&
  469. actEl.type == 'text' ||
  470. actEl.tagName.toLowerCase() === 'textarea'
  471. )
  472. ) &&
  473. (actEl.selectionStart === actEl.selectionEnd)
  474. ) &&
  475. ['none', 'caret'].includes(window.getSelection().type.toLowerCase())
  476. );
  477. }
  478. function onoffListeners(element, events, listener, flag){
  479. const ev = events.trim().split(/ +/);
  480. for (let i = 0; i < ev.length; i++){
  481. element[(flag ? 'add' : 'remove') + 'EventListener'](ev[i], listener);
  482. }
  483. }
  484.  
  485. // --------------------------------------------------
  486. // --- DATA MANAGEMENT ---
  487. // --------------------------------------------------
  488.  
  489. function encodeData(settings = _settings, tags = _arrTags){
  490. let strParams = '';
  491. Object.keys(settings).forEach(function(k){
  492. if (settings[k] != _defaultSettings[k]) strParams += settings[k];
  493. });
  494. return ':tags' +
  495. (strParams ? '['+ strParams +']' : '')+
  496. ':' +
  497. tags.map(function(e){
  498. return (e.label ? e.label + '::' : '') + e.text + '#' + e.color;
  499. }).join('');
  500. }
  501. function decodeData(str){
  502. const res = {params: null, tags: [], error: null, buttonColor: ''};
  503. let arrTags = [];
  504. if (str == null) return res;
  505. str = str.trim().replace(/ +/g, ' ');
  506. if (str === ''){
  507. // Empty data
  508. res.error = 11;
  509. return res;
  510. } else if (isTagsPacket(str)){
  511. // If the :tags: prefix is found (in the first line), retrieve parameters and TAGs
  512. const matches = str.match(/^\s*:tags(\[(.*)])?:(.*)(?:\r?\n|$)/);
  513. if (matches[1] != null){
  514. // If params block found
  515. res.params = {};
  516. const keys = matches[2];
  517. let i = keys.length;
  518. let k;
  519. while (i--){
  520. k = getParamByKey(keys[i]);
  521. if (k) res.params[k] = keys[i];
  522. }
  523. }
  524. arrTags = matches[3] ? matches[3].split('') : [];
  525. } else {
  526. // If plain text, each line of the string is taken as a TAG
  527. arrTags = str.split(/\r?\n/);
  528. }
  529. res.tags = arrTags.reduce(function(a, b){
  530. const matches = b.match(/^(?:\s*(.*?)\s*::)?\s*((?:^\s*[0-9a-f]{6})|.*?)\s*(?:#?([0-9a-f]{6}))?$/);
  531. if (matches){
  532.  
  533. // Return color for ADD button
  534. if (!matches[1] && !matches[2] && arrTags.length === 1) res.buttonColor = matches[3];
  535. // Include valid TAGs
  536. else a.push({label: matches[1], text: matches[2], color: matches[3]});
  537. }
  538. return a;
  539. }, []);
  540. // If no valid data was found, report "unknoun data format" error
  541. if (res.tags.length === 0 && res.params == null && res.buttonColor === '') res.error = 10;
  542. return res;
  543. }
  544. // Update all TAGs through the specified command string
  545. function updateData(str, options = {}){
  546. const data = decodeData(str);
  547. const plus = document.getElementById('od-addtag');
  548. const res = {
  549. newTags: [],
  550. error: data.error,
  551. buttonColor: data.buttonColor,
  552. keepButtonColor: options.from === 'add-button' ? (data.tags.length === 1 && !!data.tags[0].color) : _arrTags.length > 0
  553. };
  554. // Update settings if BOX is empty or no TAG to add
  555. if (data.params !== null && _arrTags.length === 0 || data.tags.length === 0){
  556. Object.keys(_defaultSettings).forEach(function(param){
  557. setParam(param, data.params ? data.params[param] : _settings[param]);
  558. });
  559. }
  560. // Merge the new data with the existing ones
  561. if (data.tags.length){
  562. const newTags = [];
  563. let badTagCounter = 0;
  564. data.tags.forEach(tag=>{
  565. let exist = getTags(tag.label, tag.text);
  566. if (exist){
  567. // Mark duplicate TAGs as to be removed
  568. if (exist.withLabel && exist.withText) exist.withText.action = 'remove';
  569. // Mark existing TAGs as to be updated
  570. exist = exist.withLabel || exist.withText;
  571. exist.action = 'update';
  572. if (tag.label !== undefined && (exist.label || false) !== (tag.label || false)){
  573. exist.label = tag.label || undefined;
  574. res.keepButtonColor = true;
  575. }
  576. if (tag.text && tag.text !== exist.text){
  577. exist.text = tag.text;
  578. res.keepButtonColor = true;
  579. }
  580. exist.color = options.from === 'add-button' ? tag.color || (data.tags.length === 1 && options.color) || exist.color : exist.color;
  581. } else if (tag.text !== ''){
  582. // Mark new TAGs as to be added
  583. tag.action = 'add';
  584. tag.color = tag.color || options.color || randomColor();
  585. tag.id = 'od-tagref-' + _tagIdCounter++;
  586. newTags.push(tag);
  587. } else {
  588. ++badTagCounter;
  589. }
  590. });
  591. if (badTagCounter === data.tags.length) {
  592. // If no valid TAGs are found, return the "unknown data format" error.
  593. res.error = 10;
  594. } else if (newTags.length){
  595. res.newTags = newTags;
  596. // Consider the position of the ADD button as the index to insert new TAGs
  597. const index = plus ? getItemIndex(plus) : 0;
  598. // Insert new TAGs
  599. _arrTags.splice(index, 0, ...newTags);
  600. }
  601. }
  602. return res;
  603. }
  604. // Updates the specific TAG. Other involved TAGs can be edited or removed
  605. function updateTag(tag, label, text){
  606. // Purge values to avoid format conflicts
  607. label = label.trim().replace(/ +/g, ' ');
  608. if (label) label = decodeData(label + '::foo').tags[0].label;
  609. text = (decodeData(text).tags[0] || {text: ''}).text;
  610.  
  611. // Remove TAG if text is empty
  612. if (text === ''){
  613. tag.action = 'remove';
  614. return;
  615. }
  616.  
  617. let exist = getTags(label, text);
  618. if (exist){
  619. if (exist.withLabel){
  620. exist.withLabel.label = '';
  621. exist.withLabel.action = 'update';
  622. }
  623. if (exist.withText) exist.withText.action = 'remove';
  624. }
  625.  
  626. tag.label = label;
  627. tag.text = text;
  628. tag.action = 'update';
  629. }
  630. function getTagById(id){
  631. return _arrTags.find(tag=>tag.id === id);
  632. }
  633. // Returns an object of existing TAGs by label and text
  634. function getTags(label, text){
  635. let withLabel, withText;
  636. if (label) withLabel = _arrTags.find(tag=>tag.label && tag.label === label);
  637. if (text) withText = _arrTags.find(tag=>tag.text && tag.text.toLowerCase() === text.toLowerCase());
  638. return (withLabel || withText) ? {withLabel: withLabel, withText: withLabel && withLabel === withText ? null : withText} : null;
  639. }
  640. // Stores data via GM APIs and keeps it backed up with Web Storage Objects
  641. async function saveData(){
  642. const str = encodeData();
  643. if (str === ':tags:'){
  644. localStorage.removeItem('odtagsbox');
  645. if (!!GM) await GM.deleteValue('odtagsbox');
  646. } else {
  647. _history.add(str);
  648. localStorage.setItem('odtagsbox', str);
  649. if (!!GM) await GM.setValue('odtagsbox', str);
  650. }
  651. }
  652. function importData(files){
  653. if (window.FileReader){
  654. const file = files[0];
  655. const reader = new FileReader();
  656. reader.addEventListener('load', function (){
  657. updatePage(reader.result, {glow: true, from: 'import'});
  658. });
  659. reader.addEventListener('error', function (e){
  660. // Cannot read this file
  661. if (e.target.error.name == 'NotReadableError') modal(21);
  662. });
  663. reader.readAsText(file, 'utf-8');
  664. } else {
  665. // Cannot open the file reader
  666. modal(20);
  667. }
  668. }
  669. function exportData(str){
  670. const name = 'tags_packet.txt';
  671. const blob = new Blob(['\ufeff' + str], { type: 'text/plain;charset=utf-8' });
  672. const objUrl = window.URL.createObjectURL(blob, { type: 'text/plain' });
  673. const a = document.createElement('a');
  674. a.href = objUrl;
  675. a.download = name;
  676. _tagsBoxWrapper.appendChild(a);
  677. a.click();
  678. setTimeout(function (){
  679. window.URL.revokeObjectURL(objUrl);
  680. _tagsBoxWrapper.removeChild(a);
  681. }, 100);
  682. }
  683. function isTagsPacket(str){
  684. return /^\s*:tags(?:\[.*])?:/.test(str);
  685. }
  686. function getParamByKey(k){
  687. return _paramsKeys[k];
  688. }
  689. function setParam(param, key){
  690. _settings[param] = key || _defaultSettings[param];
  691. }
  692. function clipboardCopy(txt){
  693. // Returns a promise
  694. if (navigator.clipboard){
  695. return navigator.clipboard.writeText(txt);
  696. } else if (document.queryCommandSupported && document.queryCommandSupported('copy')){
  697. const textarea = document.createElement('textarea');
  698. textarea.value = txt;
  699. textarea.style.position = 'fixed';
  700. document.body.appendChild(textarea);
  701. textarea.focus();
  702. textarea.select();
  703. return new Promise(function(ok, ko){
  704. if (document.execCommand('copy')) ok();
  705. else ko();
  706. document.body.removeChild(textarea);
  707. });
  708. }
  709. }
  710. function clipboardPaste(){
  711. // Returns a promise
  712. if (navigator.clipboard){
  713. return navigator.clipboard.readText();
  714. } else if (document.queryCommandSupported && document.queryCommandSupported('paste')){
  715. return new Promise(function(ok, ko){
  716. if (document.execCommand('paste')) ok();
  717. else ko();
  718. });
  719. }
  720. }
  721. // Undo/redo functions
  722. _history = {
  723. done: [],
  724. reverted: [],
  725. limit: 30,
  726. get: function(){
  727. return JSON.stringify([_history.done, _history.reverted]);
  728. },
  729. set: function(json){
  730. const data = JSON.parse(json);
  731. _history.done = data[0];
  732. _history.reverted = data[1];
  733. _history.restore(_history.done.slice(-1)[0], {noSlideIn: true, noFxIn: false, glow: false});
  734. },
  735. add: function(str = encodeData()){
  736. if (_history.skipAdd){
  737. delete _history.skipAdd;
  738. return;
  739. }
  740. const plus = document.getElementById('od-addtag');
  741. const item = plus.dataset.color + getItemIndex(plus) + str;
  742. if (item === _history.done.slice(-1)[0]) return;
  743. if (_history.done.length >= _history.limit) _history.done.shift();
  744. _history.done.push(item);
  745. _history.reverted.length = 0;
  746. },
  747. undo: function() {
  748. if (_history.done.length <= 1){
  749. return;
  750. }
  751. const item = _history.done.pop();
  752. if (item){
  753. _history.reverted.push(item);
  754. _history.restore(_history.done.slice(-1)[0]);
  755. }
  756. },
  757. redo: function() {
  758. const item = _history.reverted.pop();
  759. if (item){
  760. _history.done.push(item);
  761. _history.restore(item);
  762. }
  763. },
  764. restore: function(item, options = {noFxIn: true, glow: true}){
  765. const data = item.match(/^([^:]+)(.+)$/);
  766. const plusColor = data[1].slice(0, 6);
  767. const plusIndex = data[1].slice(6);
  768. const str = data[2];
  769. _arrTags.length = 0;
  770. _tagsBox.innerHTML = '';
  771. _history.skipAdd = true;
  772. updatePage(str, {plusColor: plusColor, plusIndex: plusIndex, noSlideIn: true, noFxIn: options.noFxIn, glow: options.glow, from: 'restore'});
  773. },
  774. keyboardShortcuts: function(e){
  775. if (!e.ctrlKey || !isNothingFocused(true)) return;
  776. if ((e.keyCode === 89 && _history.reverted.length > 0) || (e.keyCode === 90 && _history.done.length > 1)){
  777. e.preventDefault();
  778. _history[{89:'redo', 90:'undo'}[e.keyCode]]();
  779. }
  780. }
  781. };
  782. window.addEventListener('keydown', _history.keyboardShortcuts);
  783. window.addEventListener('beforeunload', e=>{
  784. sessionStorage.setItem('odtagsbox_history', _history.get());
  785. });
  786.  
  787. // --------------------------------------------------
  788. // --- DATA TRANSFER ---
  789. // --------------------------------------------------
  790.  
  791. // COPY-PASTE KEYBOARD SHORTCUTS
  792.  
  793. window.addEventListener('copy', function(e){
  794. if (_arrTags.length && isNothingFocused()){
  795. // Put the tags data on the clipboard
  796. e.clipboardData.setData('text/plain', encodeData());
  797. e.preventDefault();
  798. fxGlow(_tagsBox);
  799. }
  800. });
  801. window.addEventListener('paste', function(e){
  802. const str = (e.clipboardData || window.clipboardData).getData('text');
  803. if (isNothingFocused(true)){
  804. updatePage(str, {glow: true, from: 'paste'});
  805. e.preventDefault();
  806. }
  807. });
  808.  
  809. // DRAG-AND-DROP STRING OR EXTERNAL TXT FILE
  810.  
  811. function isValidDraggedDataType(data){
  812. // Accept only TEXT in external data type
  813. for (let i = 0; i < data.length; i++){
  814. if (data[i].type.match('^text/plain')){
  815. return true;
  816. }
  817. }
  818. return false;
  819. }
  820. _tagsBox.addEventListener('dragenter', function (e){
  821. _dragenterBoxCounter++;
  822. const data = e.dataTransfer.items;
  823. if (_draggedData === null && isValidDraggedDataType(data)){
  824. _draggedData = data[0];
  825. _tagsBox.classList.add('od-dragging-external-data');
  826. }
  827. });
  828. _tagsBox.addEventListener('dragleave', function (){
  829. _dragenterBoxCounter--;
  830. // Counter needed to prevent bubbling effect
  831. if (_dragenterBoxCounter === 0){
  832. if (_draggedData === null) return;
  833. _draggedData = null;
  834. _tagsBox.classList.remove('od-dragging-external-data');
  835. }
  836. });
  837. _tagsBox.addEventListener('drop', function (e){
  838. e.preventDefault();
  839. _draggedData = null;
  840. _dragenterBoxCounter = 0;
  841. _tagsBox.classList.remove('od-dragging-external-data');
  842. const data = e.dataTransfer.items;
  843. // Exit if not TEXT data type
  844. if (!isValidDraggedDataType(data)) return false;
  845.  
  846. if (data[0].kind === 'string'){
  847. // If string
  848. updatePage(e.dataTransfer.getData('Text'), {glow: true, from: 'drop'});
  849. } else if (data[0].kind === 'file'){
  850. // If file
  851. importData(e.dataTransfer.files);
  852. }
  853. });
  854.  
  855. // --------------------------------------------------
  856. // --- ITEMS FUNCTIONS ---
  857. // --------------------------------------------------
  858.  
  859. // Add and set a item in the BOX
  860. function addItem(o, index){
  861. const item = document.createElement('div');
  862. const label = document.createElement('i');
  863.  
  864. item.appendChild(label);
  865. item.classList.add('od-item');
  866. item.id = o.id;
  867.  
  868. if (index < _tagsBox.childElementCount) _tagsBox.insertBefore(item, _tagsBox.children[index]);
  869. else _tagsBox.appendChild(item);
  870.  
  871. setItem(o);
  872.  
  873. // Drag-and-drop
  874. item.addEventListener('dragstart', itemDragstart);
  875. item.addEventListener('dragend', itemDragend);
  876. item.addEventListener('dragenter', itemDragenter);
  877. item.addEventListener('dragleave', itemDragleave);
  878. item.addEventListener('dragover', itemDragover);
  879.  
  880. return item;
  881. }
  882. function setItem(o){
  883. const item = document.getElementById(o.id);
  884. const label = item.querySelector('i');
  885. const itemText = o.text || '';
  886. const itemLabel = (o.label && o.label !== o.text) ? o.label : '';
  887. const itemColor = o.color ? o.color : randomColor();
  888.  
  889. setItemColor(item, itemColor);
  890. label.dataset.value = itemLabel || itemText;
  891. item.title = itemText || 'Add TAG';
  892. item.dataset.text = itemText;
  893. if (itemLabel) item.dataset.label = itemLabel;
  894. else delete item.dataset.label;
  895. setDraggable(item);
  896.  
  897. return item;
  898. }
  899. // Remove a TAG item
  900. function removeItem(tag){
  901. let item = document.getElementById(tag.id);
  902. if (item){
  903. item.classList.add('od-removed');
  904. setTimeout(()=>{_tagsBox.removeChild(item);}, 310);
  905. }
  906. let index = _arrTags.indexOf(tag);
  907. if (index !== -1) _arrTags.splice(index, 1);
  908. }
  909. function setItemColor(item, color){
  910. const label = item.querySelector('i');
  911. label.style.backgroundColor = '#' + color;
  912. item.dataset.color = color;
  913. // Dark text if the fill is light
  914. item.classList.toggle('od-darktext', !isDarkRGB(hex2RGB(color), 170));
  915. }
  916. function openColorPicker(item){
  917. keepActiveItem(item);
  918. const colorPicker = new ColorPicker({
  919. color: item.dataset.color,
  920. target: item,
  921. parent: _tagsBoxWrapper,
  922. onChange: function(){
  923. setItemColor(item, colorPicker.hex);
  924. },
  925. onClose: function(){
  926. boxReset();
  927. if (colorPicker.hex === colorPicker.initHex) return;
  928. if (item.id === 'od-addtag'){
  929. _history.add();
  930. return;
  931. }
  932. _arrTags.find(tag=>tag.id === item.id).color = colorPicker.hex;
  933. saveData();
  934. }
  935. });
  936. }
  937. function editTagText(item){
  938. inputOnTag({
  939. item: item,
  940. property: 'text',
  941. placeholder: '- text -'
  942. });
  943. }
  944. function editTagLabel(item){
  945. inputOnTag({
  946. item: item,
  947. property: 'label',
  948. placeholder: '- label -'
  949. });
  950. }
  951. function inputOnTag(o){
  952. const item = o.item;
  953. const property = o.property;
  954. const placeholder = o.placeholder;
  955. keepActiveItem(item);
  956. const initVal = {
  957. label: item.dataset.label || '',
  958. text: item.dataset.text
  959. };
  960. const label = item.querySelector(':scope > i');
  961. const input = document.createElement('input');
  962.  
  963. // Get width values
  964. let wa = item.offsetWidth;
  965. item.classList.add('od-edit-tag');
  966. input.value = label.dataset.value = initVal[property];
  967. let wb = Math.max(60, Math.min(180, item.offsetWidth));
  968.  
  969. widthTransition(wa, wb);
  970. input.placeholder = placeholder;
  971. input.spellcheck = false;
  972. item.appendChild(input);
  973. input.style.opacity = '0';
  974. setTimeout(()=>{input.style.removeProperty('opacity');}, 1);
  975.  
  976. setDraggable(item, false); // FIX: FF unable to interact with mouse on input field when parent is draggable
  977. input.focus();
  978. input.addEventListener('input', function(){
  979. label.dataset.value = this.value;
  980. });
  981. input.addEventListener('keydown', function(e){
  982. if (e.keyCode === 27) {
  983. e.preventDefault();
  984. esc();
  985. } else if (e.keyCode === 13) {
  986. e.preventDefault();
  987. done();
  988. }
  989. });
  990. input.addEventListener('blur', done);
  991.  
  992. function widthTransition(a, b, callback){
  993. if (widthTransition.running) clearTimeout(widthTransition.timeout);
  994. item.style.width = item.style.minWidth = item.style.maxWidth = a + 'px';
  995. if (b != null) setTimeout(widthTransition, 1, b, null, callback);
  996. else {
  997. item.classList.add('od-edit-tag-transition');
  998. widthTransition.running = true;
  999. widthTransition.timeout = setTimeout(()=>{
  1000. delete widthTransition.running;
  1001. widthTransition.end(callback);
  1002. }, 350);
  1003. }
  1004. }
  1005. widthTransition.end = function(callback){
  1006. item.style.removeProperty('width');
  1007. item.style.removeProperty('min-width');
  1008. item.style.removeProperty('max-width');
  1009. item.classList.remove('od-edit-tag-transition');
  1010. if (callback) callback();
  1011. };
  1012. function esc(){
  1013. wa = item.offsetWidth;
  1014. widthTransition.end();
  1015. label.dataset.value = initVal.label || initVal.text;
  1016. close();
  1017. }
  1018. function done(){
  1019. wa = item.offsetWidth;
  1020. widthTransition.end();
  1021. if (input.value !== initVal[property]){
  1022. const tag = getTagById(item.id);
  1023. updateTag(
  1024. tag,
  1025. property === 'label' ? input.value : initVal.label,
  1026. property === 'text' ? input.value : initVal.text
  1027. );
  1028. redrawBox();
  1029. saveData();
  1030. label.dataset.value = tag.label || tag.text;
  1031. } else label.dataset.value = initVal.label || initVal.text;
  1032. close();
  1033. }
  1034. function close(){
  1035. setDraggable(item, true);
  1036. input.removeEventListener('blur', done);
  1037.  
  1038. // Get final width
  1039. item.style.transition = '0s';
  1040. item.classList.remove('od-edit-tag');
  1041. wb = item.offsetWidth;
  1042. item.style.removeProperty('transition');
  1043. item.classList.add('od-edit-tag');
  1044.  
  1045. input.style.opacity = '0';
  1046. widthTransition(wa, wb, ()=>{
  1047. item.removeChild(input);
  1048. item.classList.remove('od-edit-tag');
  1049. });
  1050. boxReset();
  1051. }
  1052. }
  1053. // Get the index of the item in the BOX
  1054. function getItemIndex(item){
  1055. return [..._tagsBox.querySelectorAll(':scope > :not(.od-removed)')].indexOf(item);
  1056. }
  1057. function activateItem(item){
  1058. deactivateItem();
  1059. item.classList.add('od-active', 'od-highlight');
  1060. }
  1061. function deactivateItem(){
  1062. const activeItem = getActiveItem();
  1063. if (activeItem) activeItem.classList.remove('od-active', 'od-highlight');
  1064. return activeItem;
  1065. }
  1066. function getActiveItem(){
  1067. return _tagsBox.querySelector(':scope .od-item.od-active');
  1068. }
  1069. function keepActiveItem(item = getActiveItem()){
  1070. setTimeout( function(){
  1071. keepBoxOpen();
  1072. activateItem(item);
  1073. }, 1);
  1074. }
  1075. function keepBoxOpen(){
  1076. _tagsBox.classList.add('od-keep-open');
  1077. }
  1078. function unlockBoxOpen(){
  1079. _tagsBox.classList.remove('od-keep-open');
  1080. }
  1081. function boxReset(){
  1082. unlockBoxOpen();
  1083. setTimeout(deactivateItem, 1);
  1084. }
  1085.  
  1086. // --------------------------------------------------
  1087. // --- CLICK ITEMS ---
  1088. // --------------------------------------------------
  1089.  
  1090. _tagsBox.addEventListener('click', function (e){
  1091. const item = e.target;
  1092. if (!item.classList.contains('od-item')) return;
  1093. const query = _input.value;
  1094. const label = item.querySelector(':scope > i');
  1095. if (item.id === 'od-addtag'){
  1096.  
  1097. // PLUS BUTTON (+) - Adds in the BOX new TAGs based on the search field query or highlighted text
  1098.  
  1099. const singleTag = !isTagsPacket(query);
  1100. const labelFormat = singleTag && /^.*::/.test(query);
  1101. const str = ((labelFormat || _input.selectionStart === _input.selectionEnd) ? query : query.substring(_input.selectionStart, _input.selectionEnd)).trim();
  1102. let res = {};
  1103. if (e.ctrlKey){
  1104. // If CTRL was pressed, edit color
  1105. openColorPicker(item);
  1106. return;
  1107. } else if (!str) _input.focus();
  1108. else {
  1109. res = updatePage((singleTag ? ':tags:' : '') + str, {from: 'add-button', color: item.dataset.color});
  1110. if (labelFormat && res.newTags.length === 1){
  1111. _input.value = res.newTags[0].text + ' ';
  1112. _input.focus();
  1113. }
  1114. }
  1115. // Set the button color
  1116. if (!res.keepButtonColor){
  1117. const newColor = res.buttonColor || randomColor();
  1118. setItemColor(item, newColor);
  1119. if (res.buttonColor) fxGlow(item);
  1120. _history.add();
  1121. }
  1122. } else if (!item.classList.contains('od-edit-tag')){
  1123.  
  1124. // TAG ELEMENT - Enters the text of the TAG in the search field or edits its properties
  1125.  
  1126. const itemText = item.dataset.text;
  1127. if (e.shiftKey){
  1128. // If SHIFT was pressed, edit text
  1129. editTagText(item);
  1130. } else if (e.altKey){
  1131. // If ALT was pressed, edit label
  1132. editTagLabel(item);
  1133. } else if (e.ctrlKey){
  1134. // If CTRL was pressed, edit color
  1135. openColorPicker(item);
  1136. } else if (_input.selectionStart !== undefined){
  1137. // If there is a selection, the TAG text will be inserted relative to it
  1138. let startPos = _input.selectionStart;
  1139. let endPos = _input.selectionEnd;
  1140. const text = (startPos > 0 ? ' ' : '') + itemText + ' ';
  1141. if (startPos > 0 && query[startPos-1] === ' ') startPos--;
  1142. if (endPos < query.length && query[endPos] === ' ') endPos++;
  1143. _input.value = query.slice(0, startPos) + text + query.slice(endPos);
  1144. _input.focus();
  1145. const pos = startPos + text.length;
  1146. _input.setSelectionRange(pos, pos);
  1147. } else {
  1148. // Append the TAG text
  1149. _input.value = query.trim() + ' ' + itemText + ' ';
  1150. _input.focus();
  1151. _input.click();
  1152. }
  1153. }
  1154. });
  1155.  
  1156. // --------------------------------------------------
  1157. // --- COLOR PROCESSING ---
  1158. // --------------------------------------------------
  1159.  
  1160. function hex2HSV(hex){
  1161. const [r, g, b] = hex.match(/../g).map(c=>parseInt(c, 16) / 255);
  1162. const v = Math.max(r, g, b), c = v - Math.min(r, g, b);
  1163. const h = c && ((v === r) ? (g - b) / c : ((v === g) ? 2 + (b - r) / c : 4 + (r - g) / c));
  1164. return {h: (h < 0 ? h + 6 : h) / 6, s: v && c / v, v: v};
  1165. }
  1166. function HSV2Hex(hsv){
  1167. let f = (n, k = (n + hsv.h * 6) % 6)=>('0' + Math.round((hsv.v - hsv.v * hsv.s * Math.max(Math.min(k, 4 - k, 1), 0)) * 255).toString(16)).slice(-2);
  1168. return f(5) + f(3) + f(1);
  1169. }
  1170. function hex2RGB(hex){
  1171. return hex.match(/../g).reduce((a, v, i)=>({ ...a, ['rgb'[i]]: parseInt(v, 16)}), {});
  1172. }
  1173. function RGBCSS2Obj(str){
  1174. return str.slice(4, -1).split(',').reduce((a, v, i)=>({ ...a, ['rgb'[i]]: v}), {});
  1175. }
  1176. function randomHSV(){
  1177. return {h: Math.random(), s: 0.3 + 0.4 * Math.random(), v: 0.5 + 0.2 * Math.random()};
  1178. }
  1179. function randomColor(){
  1180. return HSV2Hex(randomHSV());
  1181. }
  1182. function isDarkRGB(rgb, threshold = 155){ // threshold range [0, 255]
  1183. return rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722 < threshold;
  1184. }
  1185.  
  1186. // --------------------------------------------------
  1187. // --- COLOR PICKER ---
  1188. // --------------------------------------------------
  1189.  
  1190. class ColorPicker {
  1191. constructor(o){
  1192. const me = this;
  1193. me.hex = me.initHex = o.color || '000000';
  1194. me.hsv = hex2HSV(me.hex);
  1195. me.parent = o.parent || document.body;
  1196. me.picker = document.createElement('div');
  1197. me.block = document.createElement('div');
  1198. me.strip = document.createElement('div');
  1199. me.blockThumb = document.createElement('i');
  1200. me.stripThumb = document.createElement('i');
  1201. me.block.tabIndex = 0;
  1202. me.strip.tabIndex = 0;
  1203. me.operatedSlider = null;
  1204. me.events = ['change', 'close', 'startSlide', 'endSlide'].reduce((a, b)=>({ ...a, [b]: o['on' + b[0].toUpperCase() + b.slice(1)]}), {});
  1205. me.init();
  1206. me.display();
  1207. me.position(o.target);
  1208. }
  1209. init(){
  1210. const me = this;
  1211. me.picker.classList.add('od-colorpicker');
  1212. me.block.classList.add('od-colorpicker-block');
  1213. me.strip.classList.add('od-colorpicker-strip');
  1214. me.block.appendChild(me.blockThumb);
  1215. me.strip.appendChild(me.stripThumb);
  1216. me.picker.dataset.color = me.hex;
  1217. me.picker.appendChild(me.block);
  1218. me.picker.appendChild(me.strip);
  1219.  
  1220. function sliding(e){
  1221. if (me.operatedSlider === me.block){
  1222. const rect = me.block.getBoundingClientRect();
  1223. me.hsv.s = Math.max(0, Math.min(1, 1 / me.block.offsetWidth * (e.clientX - rect.left)));
  1224. me.hsv.v = Math.max(0, Math.min(1, 1 - (1 / me.block.offsetHeight * (e.clientY - rect.top))));
  1225. me.setBlock();
  1226. } else if (me.operatedSlider === me.strip){
  1227. const rect = me.strip.getBoundingClientRect();
  1228. me.hsv.h = Math.max(0, Math.min(1, 1 / me.strip.offsetWidth * (e.clientX - rect.left)));
  1229. me.setStrip();
  1230. }
  1231. const newHex = HSV2Hex(me.hsv);
  1232. if (me.hex !== newHex){
  1233. me.hex = newHex;
  1234. me.change();
  1235. }
  1236. }
  1237. function endSlide(){
  1238. window.removeEventListener('mouseup', endSlide);
  1239. window.removeEventListener('mousemove', sliding);
  1240. document.documentElement.classList.remove('od-colorpicker-sliding');
  1241. me.operatedSlider = null;
  1242. me.handler('endSlide');
  1243. }
  1244. me.picker.addEventListener('mousedown', function(e){
  1245. e.stopPropagation();
  1246. if (me.block.contains(e.target)) me.operatedSlider = me.block;
  1247. else if (me.strip.contains(e.target)) me.operatedSlider = me.strip;
  1248. else return;
  1249. document.documentElement.classList.add('od-colorpicker-sliding');
  1250. me.handler('startSlide');
  1251. sliding(e);
  1252. window.addEventListener('mousemove', sliding);
  1253. window.addEventListener('mouseup', endSlide);
  1254. });
  1255. onoffListeners(me.picker, 'contextmenu wheel', function(e){
  1256. e.preventDefault();
  1257. e.stopPropagation();
  1258. }, true);
  1259. function beforeClosing(){
  1260. onoffListeners(window, 'wheel resize blur mousedown contextmenu', beforeClosing, false);
  1261. me.close();
  1262. }
  1263. onoffListeners(window, 'wheel resize blur mousedown contextmenu', beforeClosing, true);
  1264. function esc(e){
  1265. if (e.keyCode === 27){
  1266. if (me.hex === me.initHex) beforeClosing();
  1267. else {
  1268. me.hsv = hex2HSV(me.hex = me.initHex);
  1269. me.setBlock();
  1270. me.setStrip();
  1271. me.change();
  1272. }
  1273. }
  1274. }
  1275. me.picker.addEventListener('keydown', esc);
  1276. }
  1277. display(){
  1278. this.parent.appendChild(this.picker);
  1279. this.setBlock();
  1280. this.setStrip();
  1281. }
  1282. position(target){
  1283. let x = 0;
  1284. let y = 0;
  1285. if (target){
  1286. const rect = target.getBoundingClientRect();
  1287. x = (rect.left + this.picker.offsetWidth > window.innerWidth) ? Math.max(0, Math.round(rect.right - this.picker.offsetWidth)) : rect.left;
  1288. y = (rect.bottom + this.picker.offsetHeight > window.innerHeight) ? Math.max(0, Math.round(rect.top - this.picker.offsetHeight)) : rect.bottom;
  1289. }
  1290. this.picker.style = 'top: ' + y + 'px; left: ' + x + 'px';
  1291. }
  1292. setBlock(){
  1293. const x = Math.round(this.block.offsetWidth * this.hsv.s);
  1294. const y = Math.round(this.block.offsetHeight * (1 - this.hsv.v));
  1295. this.blockThumb.style = 'top: ' + y + 'px; left: ' + x + 'px;';
  1296. }
  1297. setStrip(){
  1298. const hue = 'hsl(' + Math.round(this.hsv.h * 360) + ',100%,50%)';
  1299. const x = Math.round(this.strip.offsetWidth * this.hsv.h);
  1300. this.stripThumb.style = 'left: ' + x + 'px; color: ' + hue;
  1301. this.block.style.color = hue;
  1302. }
  1303. change(){
  1304. this.handler('change');
  1305. }
  1306. close(){
  1307. this.parent.removeChild(this.picker);
  1308. this.handler('close');
  1309. }
  1310. handler(event){
  1311. if (typeof this.events[event] === 'function') this.events[event]();
  1312. }
  1313. }
  1314.  
  1315. // --------------------------------------------------
  1316. // --- EFFECTS ---
  1317. // --------------------------------------------------
  1318.  
  1319. function fxGlow(el){
  1320. el.classList.add('od-highlight');
  1321. setTimeout(function(){el.classList.remove('od-highlight');}, 500);
  1322. }
  1323. function fxGlowErr(el){
  1324. el.classList.add('od-error');
  1325. setTimeout(function(){el.classList.remove('od-error');}, 800);
  1326. }
  1327. function fxFadein(el, duration, delay){
  1328. duration = duration == null ? 300 : +duration;
  1329. delay = delay == null ? 0 : +delay;
  1330. el.style.opacity = '0';
  1331. el.style.transition = duration + 'ms ' + delay + 'ms ease-in-out';
  1332. setTimeout(function(){
  1333. el.style.removeProperty('opacity');
  1334. setTimeout(function(){
  1335. el.style.removeProperty('transition');
  1336. }, duration + delay);
  1337. }, 1);
  1338. }
  1339. function fxSlideFadein(el, duration, delay){
  1340. duration = duration == null ? 300 : +duration;
  1341. delay = delay == null ? 0 : +delay;
  1342. el.style.opacity = '0';
  1343. el.style.minWidth = '0';
  1344. el.style.maxWidth = '0';
  1345. el.style.transition = duration + 'ms ' + delay + 'ms ease-in-out';
  1346. setTimeout(function(){
  1347. el.style.removeProperty('opacity');
  1348. el.style.removeProperty('min-width');
  1349. el.style.removeProperty('max-width');
  1350. setTimeout(function(){
  1351. el.style.removeProperty('transition');
  1352. }, duration + delay);
  1353. }, 1);
  1354. }
  1355.  
  1356. // --------------------------------------------------
  1357. // --- MODAL ---
  1358. // --------------------------------------------------
  1359.  
  1360. function modal(msg, delay = 10){
  1361. if (typeof msg === 'number'){
  1362. msg = modal.msgList[msg];
  1363. }
  1364. // Prevents freezing of hovered elements when the alert is shown
  1365. _tagsBoxWrapper.classList.add('od-nohover');
  1366. setTimeout(function(){
  1367. alert(msg);
  1368. _tagsBoxWrapper.classList.remove('od-nohover');
  1369. }, delay);
  1370. }
  1371. modal.msgList = {
  1372. 10: '⚠️ Sorry!\nI don\'t understand the format of this data.\n\nNo TAGs have been added.',
  1373. 11: '⚠️ Hey!\nIt looks like you are trying to put something weird in the BOX. I don\'t see valid data here.\n\nNo TAGS have been added.',
  1374. 20: '⚠️ Oops!\nI can\'t open the file reader.💡 But...\nyou can open it elsewhere, then try the copy-paste functions.',
  1375. 21: '⚠️ Oops!\nI can\'t read this file.💡 Try picking it up and opening it again.',
  1376. 50: '⚠️ Oops!\nUnable to copy data to clipboard.\n\n💡 But...\nyou can copy the string from the search field.',
  1377. 60: '⚠️ Oops!\nI can\'t read data from the clipboard.\n\n💡 But... try with CTRL+V.\n– Close this modal first –',
  1378. };
  1379.  
  1380. // --------------------------------------------------
  1381. // --- START ---
  1382. // --------------------------------------------------
  1383.  
  1384. async function start(){
  1385. // If exist, use the history data stored in the local session
  1386. const data = sessionStorage.getItem('odtagsbox_history');
  1387. if (data){
  1388. _history.set(data);
  1389. return;
  1390. }
  1391. // Retrieve data via GM APIs or fall back to localStorage
  1392. let str = !!GM && await GM.getValue('odtagsbox');
  1393. if (!str) str = localStorage.getItem('odtagsbox');
  1394.  
  1395. _tagsBox.innerHTML = '';
  1396. updatePage(str, {noSlideIn: true, from: 'start'});
  1397. setTimeout(function(){ _tagsBox.classList.remove('od-hidein');}, 2);
  1398. }
  1399.  
  1400. // --------------------------------------------------
  1401. // --- STYLE ---
  1402. // --------------------------------------------------
  1403.  
  1404. function addGlobalStyle(strCSS){
  1405. const h = document.querySelector('head');
  1406. if (!h) return;
  1407. const s = document.createElement('style');
  1408. s.type = 'text/css';
  1409. s.innerHTML = strCSS;
  1410. h.appendChild(s);
  1411. }
  1412. function getColorMode(mode){
  1413. return {dark: mode === 'dark', light: mode !== 'dark'};
  1414. }
  1415. function css (colorMode){ return (
  1416. `
  1417. /* RESET */
  1418.  
  1419. /* Google SERP - make space for the BOX */
  1420. #tsf, #sf { margin-top: 10px !important; transition: margin-top .8s ease-in-out }
  1421. #searchform.minidiv #tsf, #kO001e.DU1Mzb #sf{ padding-top: 16px !important }
  1422. #searchform > .sfbg { margin-top: 0 !important }
  1423. #searchform.minidiv > .sfbg { padding-top: 2px }
  1424. /* Google Images SERP - fix position */
  1425. #sf #od-tagsbox { margin: -5px 0 0 3px }
  1426. #kO001e.DU1Mzb { padding: 10px 0 6px }
  1427. .M3w8Nb #od-tagsbox-wrapper, .KZFCbe #od-tagsbox-wrapper { padding-left: 27px }
  1428.  
  1429. /* Demote dropdowns/popups below the search field to avoid overlapping on the BOX */
  1430. .ea0Lbe, #tsf .UUbT9 { z-index: 984 !important }
  1431.  
  1432. /* CONTAINERS */
  1433.  
  1434. #od-tagsbox-wrapper *,
  1435. #od-tagsbox-wrapper *::before,
  1436. #od-tagsbox-wrapper *::after {
  1437. box-sizing: border-box;
  1438. }
  1439. #od-tagsbox-wrapper {
  1440. height: 0;
  1441. }
  1442. #od-tagsbox {
  1443. position: absolute;
  1444. top: -29px;
  1445. max-width: 100%;
  1446. max-height: 32px;
  1447. border: 1px solid;
  1448. border-color: rgba(${ colorMode.dark ? '95,99,104' : '208,211,215' },0);
  1449. border-radius: 16px;
  1450. outline: 2px solid transparent;
  1451. background: rgba(${ colorMode.dark ? '75,75,75' : '240,240,240' },0);
  1452. box-shadow: 0 2px 5px 1px rgba(64,60,67,0);
  1453. overflow: hidden;
  1454. transition: all .4s .1s ease-in-out, z-index 0s, outline-style 0s .4s;
  1455. z-index: 985;
  1456. }
  1457. #searchform #od-tagsbox {
  1458. top: -34px;
  1459. left: 30px;
  1460. }
  1461. #od-tagsbox-wrapper.od-nohover {
  1462. pointer-events: none;
  1463. }
  1464. #od-tagsbox-wrapper.od-nohover > #od-tagsbox {
  1465. transition: 0s;
  1466. }
  1467. #od-tagsbox-wrapper:not(.od-nohover) > #od-tagsbox:hover,
  1468. #od-tagsbox.od-keep-open {
  1469. max-height: 300px;
  1470. border-color: rgba(${ colorMode.dark ? '95,99,104' : '208,211,215' },1);
  1471. background: rgba(${ colorMode.dark ? '75,75,75' : '240,240,240' },.8);
  1472. box-shadow: 0 2px 5px 1px rgba(64,60,67,.3);
  1473. transition: all .2s, max-height .4s .1s ease-in-out, z-index 0s;
  1474. }
  1475.  
  1476. /* ITEM */
  1477.  
  1478. .od-item {
  1479. position: relative;
  1480. float: left;
  1481. height: 30px;
  1482. outline-color: transparent;
  1483. font: normal 12px/20px Arial, sans-serif;
  1484. text-align: center;
  1485. cursor: pointer;
  1486. transition: all .3s ease-out, opacity .3s .1s ease-out;
  1487. }
  1488.  
  1489. /* ITEM WIDTH PRESETS */
  1490. /*
  1491. #od-tagsbox.tagsWidth-S > .od-item > i { min-width: 24px; max-width: 24px; }
  1492. #od-tagsbox.tagsWidth-L > .od-item > i { min-width: 54px; max-width: 54px; }
  1493. #od-tagsbox.tagsWidth-A > .od-item > i { min-width: 24px; max-width: 174px; }
  1494. #od-tagsbox.tagsWidth-A > .od-item > i::before { text-overflow: ellipsis; }
  1495. */
  1496. #od-tagsbox.tagsWidth-S > .od-item { min-width: 30px; max-width: 30px; }
  1497. #od-tagsbox.tagsWidth-L > .od-item { min-width: 60px; max-width: 60px; }
  1498. #od-tagsbox.tagsWidth-A > .od-item { min-width: 30px; max-width: 180px; }
  1499. #od-tagsbox.tagsWidth-A > .od-item > i::before { text-overflow: ellipsis; }
  1500.  
  1501. /* TAG LABEL */
  1502.  
  1503. .od-item > i {
  1504. display: block;
  1505. height: calc(100% - 6px);
  1506. margin: 3px;
  1507. padding: 0 3px;
  1508. color: #fff;
  1509. border: 2px solid rgba(0,0,0,.2);
  1510. border-radius: 15px;
  1511. outline: 1px solid transparent;
  1512. font: inherit;
  1513. white-space: nowrap;
  1514. pointer-events: none;
  1515. transition: all .3s ease-out, color .3s ease-out, background-color .3s ease-out, font-size 0s, font-weight 0s;
  1516. }
  1517. .od-item > i::before {
  1518. content: attr(data-value);
  1519. display: block;
  1520. width: 100%;
  1521. height: calc(100% - 2px);
  1522. overflow: hidden;
  1523. }
  1524. .od-item.od-darktext > i {
  1525. color: rgba(0, 0, 0, .7);
  1526. }
  1527. #od-addtag > i{
  1528. font-size: 18px;
  1529. font-weight: bold;
  1530. }
  1531. #od-addtag > i::before {
  1532. content: "+";
  1533. }
  1534.  
  1535. /* LABEL CASE PRESETS */
  1536.  
  1537. #od-tagsbox.labelsCase-c > .od-item > i { text-transform: lowercase; }
  1538. #od-tagsbox.labelsCase-C > .od-item > i { text-transform: uppercase; }
  1539.  
  1540. /* USER-DEFINED LABELS */
  1541.  
  1542. .od-item[data-label] > i::before {
  1543. border-bottom: 1px dashed currentcolor;
  1544. transition: border-color .3s ease-out;
  1545. }
  1546.  
  1547. /* ITEM HOVER */
  1548.  
  1549. #od-tagsbox > .od-item:not(.od-draggeditem):hover > i {
  1550. border-color: rgba(255,255,255,.4);
  1551. outline-color: rgba(0,0,0,.4);
  1552. transition-duration: 0s, .3s, .3s, 0s, 0s;
  1553. }
  1554. #od-tagsbox > .od-item.od-darktext:not(.od-draggeditem):hover > i {
  1555. color: #000;
  1556. }
  1557.  
  1558. /* ACTIVE ITEM */
  1559.  
  1560. #od-tagsbox > .od-item.od-active > i {
  1561. transition-duration: .3s, .1s, 0s, 0s, 0s;
  1562. }
  1563.  
  1564. /* ITEM REMOVED */
  1565.  
  1566. #od-tagsbox > .od-item.od-removed {
  1567. max-width: 0;
  1568. min-width: 0;
  1569. }
  1570. #od-tagsbox > .od-item.od-removed > i {
  1571. opacity: 0;
  1572. }
  1573.  
  1574. /* EDIT TAG */
  1575.  
  1576. #od-tagsbox#od-tagsbox > .od-edit-tag {
  1577. min-width: 60px;
  1578. max-width: 180px;
  1579. }
  1580. #od-tagsbox#od-tagsbox > .od-edit-tag-transition {
  1581. transition: .3s;
  1582. }
  1583. .od-edit-tag > i::before {
  1584. /* Keep extra spaces while editing */
  1585. white-space: pre;
  1586. }
  1587. #od-tagsbox#od-tagsbox > .od-item.od-edit-tag:not(.od-edit-tag-transition) > i::before {
  1588. visibility: hidden;
  1589. }
  1590. #od-tagsbox.tagsWidth-A > .od-item.od-edit-tag-transition > i::before { text-overflow: clip; }
  1591.  
  1592. .od-edit-tag > input {
  1593. position: absolute;
  1594. top: 7px;
  1595. left: 5px;
  1596. height: calc(100% - 14px);
  1597. width: calc(100% - 10px);
  1598. margin: 0;
  1599. padding: 0;
  1600. color: #0a0905;
  1601. font: inherit;
  1602. text-align: inherit;
  1603. border: solid rgba(0,0,0,.3);
  1604. border-width: 1px 0;
  1605. border-radius: 6px;
  1606. background: rgba(255,255,255,.8);
  1607. transition: opacity .3s;
  1608. }
  1609. .od-item:not(.od-edit-tag) > input {
  1610. display: none;
  1611. }
  1612. .od-edit-tag > input:focus-visible {
  1613. outline: none;
  1614. }
  1615.  
  1616. /* DRAG-AND-DROP */
  1617.  
  1618. .od-draggeditem > i {
  1619. opacity: 0;
  1620. }
  1621. #od-tagsbox.od-dragging-item {
  1622. z-index: 988;
  1623. }
  1624. #od-tagsbox.od-dragging-item > .od-item {
  1625. opacity: .6;
  1626. transition-delay: 0s;
  1627. }
  1628. .od-belowitem {
  1629. }
  1630. #od-deletingZone {
  1631. position: fixed;
  1632. top: 0;
  1633. right: 0;
  1634. bottom: 0;
  1635. left: 0;
  1636. background: rgba(255,0,0,.2);
  1637. opacity: 0;
  1638. display: none;
  1639. z-index: 987;
  1640. transition: .3s, z-index 0s;
  1641. }
  1642. #od-deletingZone.od-dragging {
  1643. display: block;
  1644. }
  1645. #od-deletingZone.od-dragging-hover {
  1646. opacity: 1;
  1647. }
  1648. #od-tagsbox::before {
  1649. content:"";
  1650. position: absolute;
  1651. top: 0;
  1652. right: 0;
  1653. bottom: 0;
  1654. left: 0;
  1655. background: rgba(138,180,248,.34);
  1656. border-radius: inherit;
  1657. border: 1px dashed rgb(138,180,248);
  1658. opacity: 0;
  1659. transition: .3s;
  1660. }
  1661. #od-tagsbox.od-dragging-external-data {
  1662. z-index: 998;
  1663. }
  1664. #od-tagsbox.od-dragging-external-data::before {
  1665. opacity: 1;
  1666. }
  1667. #od-tagsbox.od-dragging-external-data > .od-item {
  1668. transition: .3s;
  1669. opacity: .5;
  1670. pointer-events: none;
  1671. }
  1672.  
  1673. /* CONTEXT MENU */
  1674.  
  1675. /* Containers */
  1676. #od-contextMenu {
  1677. position: fixed;
  1678. z-index: 999;
  1679. font: 400 12px/23px "Segoe UI", Calibri, Arial, sans-serif;
  1680. color: #000;
  1681. user-select: none;
  1682. cursor: default;
  1683. }
  1684. #od-contextMenu:not(.open) {
  1685. display: none;
  1686. }
  1687. #od-contextMenu ul {
  1688. list-style-type: none;
  1689. margin: 0;
  1690. padding: 3px 0;
  1691. border: 1px #dadce0 solid;
  1692. background: #fff;
  1693. box-shadow: 5px 5px 4px -4px rgba(0,0,0,.9);
  1694. }
  1695. /* Item */
  1696. #od-contextMenu ul > li {
  1697. position: relative;
  1698. margin: 0;
  1699. padding: 0 22px 0 38px;
  1700. line-height: 23px;
  1701. white-space: nowrap;
  1702. }
  1703. /* Separator */
  1704. #od-contextMenu ul > li:empty {
  1705. margin: 4px 1px;
  1706. padding: 0;
  1707. border-top: 1px #dadce0 solid;
  1708. }
  1709. /* Item content */
  1710. #od-contextMenu ul > li > span {
  1711. display: flex;
  1712. }
  1713. /* Icon */
  1714. #od-contextMenu ul > li i:first-child {
  1715. position: absolute;
  1716. top: 0;
  1717. left: 0;
  1718. display: block;
  1719. width: 35px;
  1720. text-align: center;
  1721. font-size: 1.3em;
  1722. line-height: 23px;
  1723. font-style: normal;
  1724. }
  1725. /* Shortcut */
  1726. #od-contextMenu ul > li kbd {
  1727. margin-left: auto;
  1728. padding-left: 10px;
  1729. font: inherit;
  1730. }
  1731. #od-contextMenu ul > li:not(:hover) kbd {
  1732. color: #5f6368;
  1733. }
  1734. /* Item hover */
  1735. #od-contextMenu ul > li:hover {
  1736. color: #000;
  1737. background: #e8e8e9;
  1738. }
  1739. /* Checkable item */
  1740. #od-contextMenu ul > li.od-checkable {
  1741. padding-left: 48px;
  1742. }
  1743. #od-contextMenu ul > li.od-checkable.od-checked::before {
  1744. content: "✓";
  1745. position: absolute;
  1746. left: 32px;
  1747. }
  1748. /* Submenu */
  1749. #od-contextMenu ul > li > ul {
  1750. display: block;
  1751. position: absolute;
  1752. top: 0;
  1753. width: auto;
  1754. min-width: 80px;
  1755. white-space: nowrap;
  1756. visibility: hidden;
  1757. opacity: 0;
  1758. transition: visibility 0s .3s, opacity .3s;
  1759. }
  1760. #od-contextMenu ul > li:not(.od-sub-left) ul {
  1761. left: 100%;
  1762. }
  1763. #od-contextMenu ul > li.od-sub-left ul {
  1764. right: 100%;
  1765. }
  1766. #od-contextMenu ul > li:hover > ul {
  1767. visibility: visible;
  1768. opacity: 1;
  1769. z-index: 1;
  1770. transition: visibility 0s, opacity .3s;
  1771. }
  1772. /* Arrow to open submenu */
  1773. #od-contextMenu ul > li > :first-child:not(:last-child)::after {
  1774. content: "\\23F5";
  1775. position: absolute;
  1776. right: 3px;
  1777. font-size: .9em;
  1778. line-height: inherit;
  1779. opacity: .7;
  1780. }
  1781. /* Disabled item */
  1782. #od-contextMenu ul li.od-disabled {
  1783. pointer-events: none;
  1784. opacity: .55;
  1785. filter: saturate(0);
  1786. }
  1787. /* Color setting */
  1788. #od-setcolor::before {
  1789. content: "";
  1790. display: inline-block;
  1791. width: 14px;
  1792. height: 14px;
  1793. border: 1px solid #000;
  1794. outline: 1px solid #777;
  1795. background: currentColor;
  1796. }
  1797.  
  1798. /* COLOR PICKER */
  1799.  
  1800. .od-colorpicker {
  1801. position: fixed;
  1802. z-index: 999;
  1803. display: flex;
  1804. flex-direction: column;
  1805. align-items: center;
  1806. width: 225px;
  1807. padding: 4px;
  1808. border: 1px solid #858585;
  1809. color: #fff;
  1810. background: ${colorMode.dark ? '#707578' : '#919395'};
  1811. box-shadow: 5px 5px 4px -4px rgba(0,0,0,.9);
  1812. }
  1813. .od-colorpicker > div {
  1814. position: relative;
  1815. cursor: pointer;
  1816. }
  1817. .od-colorpicker > div:focus-visible {
  1818. outline: none;
  1819. }
  1820. .od-colorpicker > div > i {
  1821. pointer-events: none;
  1822. content: '';
  1823. position: absolute;
  1824. transform: translate(-50%, -50%);
  1825. display: block;
  1826. box-shadow: none;
  1827. border: 2px solid #fff;
  1828. outline: 2px solid #0007;
  1829. height: 16px;
  1830. width: 16px;
  1831. border-radius: 100%;
  1832. color: transparent;
  1833. background: currentColor;
  1834. transition: outline-color .3s;
  1835. }
  1836. .od-colorpicker > div:active > i {
  1837. outline-color: #75bfff;
  1838. transition-duration: 0s;
  1839. }
  1840. .od-colorpicker-block {
  1841. width: 100%;
  1842. padding-bottom: 100%;
  1843. color: inherit;
  1844. background: linear-gradient(to right, #fff, currentColor);
  1845. overflow: hidden;
  1846. }
  1847. .od-colorpicker-block::before {
  1848. content: '';
  1849. position: absolute;
  1850. top: 0;
  1851. right: 0;
  1852. bottom: 0;
  1853. left: 0;
  1854. background: linear-gradient(to bottom, transparent, #000);
  1855. }
  1856. .od-colorpicker-strip {
  1857. width: calc(100% - 10px);
  1858. height: 16px;
  1859. margin: 5px 0 1px;
  1860. }
  1861. .od-colorpicker-strip::before{
  1862. content: '';
  1863. display: block;
  1864. position: absolute;
  1865. top: 0;
  1866. right: -5px;
  1867. bottom: 0;
  1868. left: -5px;
  1869. border: solid transparent;
  1870. border-width: 3px 0;
  1871. background: padding-box linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00);
  1872. }
  1873. html.od-colorpicker-sliding {
  1874. cursor: pointer;
  1875. }
  1876. html.od-colorpicker-sliding > body {
  1877. user-select: none;
  1878. pointer-events: none;
  1879. }
  1880. .od-colorpicker-block > i {
  1881. will-change: left, top;
  1882. }
  1883. .od-colorpicker-strip > i {
  1884. will-change: left;
  1885. top: 50%;
  1886. }
  1887.  
  1888. /* EFFECTS */
  1889.  
  1890. /* Glow */
  1891. #od-tagsbox.od-highlight,
  1892. .od-item.od-highlight::before {
  1893. outline-color: #45bfff;
  1894. transition: 0s;
  1895. }
  1896. #od-tagsbox.od-highlight {
  1897. background: rgba(100,180,255,.6);
  1898. }
  1899. .od-item::before {
  1900. content: "";
  1901. display: block;
  1902. position: absolute;
  1903. top: 3px;
  1904. right: 3px;
  1905. bottom: 3px;
  1906. left: 3px;
  1907. border-radius: 15px;
  1908. outline: 2px solid transparent;
  1909. transition: .4s ease-in-out;
  1910. }
  1911. /* Glow error */
  1912. #od-tagsbox.od-error {
  1913. background-color: rgba(255,0,0,.6) !important;
  1914. outline-color: #f00;
  1915. transition: 0s;
  1916. }
  1917.  
  1918. /* COLOR SCHEME */
  1919.  
  1920. @media (prefers-color-scheme: dark) {
  1921.  
  1922. /* Dark-mode applies to the context menu according to the system color scheme */
  1923. #od-contextMenu {
  1924. color: #fff;
  1925. font-weight: 100;
  1926. }
  1927. #od-contextMenu ul {
  1928. background: #292a2d;
  1929. border-color: #3c4043;
  1930. }
  1931. #od-contextMenu ul > li:empty {
  1932. border-color: #3c4043;
  1933. }
  1934. #od-contextMenu ul > li:hover {
  1935. color: #fff;
  1936. background: #3f4042;
  1937. }
  1938. #od-contextMenu ul > li:not(:hover) kbd {
  1939. color: #9aa0a6;
  1940. }
  1941. }`
  1942. );
  1943. }
  1944.  
  1945. // --------------------------------------------------
  1946. // --- WE CAN START! ---
  1947. // --------------------------------------------------
  1948.  
  1949. start();
  1950.  
  1951. });