Make your searches easier by adding tags to your search queries with one click
// ==UserScript== // @name Google Searching Tags Box // @version 2.1.0 // @description Make your searches easier by adding tags to your search queries with one click // @author OpenDec // @match https://www.google.com/* // @match https://www.google.co.jp/* // @match https://www.google.co.uk/* // @match https://www.google.es/* // @match https://www.google.ca/* // @match https://www.google.de/* // @match https://www.google.it/* // @match https://www.google.fr/* // @match https://www.google.com.au/* // @match https://www.google.com.tw/* // @match https://www.google.nl/* // @match https://www.google.com.br/* // @match https://www.google.com.tr/* // @match https://www.google.be/* // @match https://www.google.com.gr/* // @match https://www.google.co.in/* // @match https://www.google.com.mx/* // @match https://www.google.dk/* // @match https://www.google.com.ar/* // @match https://www.google.ch/* // @match https://www.google.cl/* // @match https://www.google.at/* // @match https://www.google.co.kr/* // @match https://www.google.ie/* // @match https://www.google.com.co/* // @match https://www.google.pl/* // @match https://www.google.pt/* // @match https://www.google.com.pk/* // @include https://www.google.tld/* // @icon https://www.google.com/s2/favicons?sz=64&domain=google.com // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @run-at document-start // @namespace https://greasyfork.org/users/873547 // @license MIT // ==/UserScript== /* jshint esversion: 9 */ window.addEventListener('DOMContentLoaded', function stageReady(){ 'use strict'; // -------------------------------------------------- // --- INIT --- // -------------------------------------------------- addGlobalStyle(css(getColorMode(isDarkRGB(RGBCSS2Obj(window.getComputedStyle(document.body).backgroundColor)) ? 'dark' : 'light'))); const _input = document.querySelector('input.gLFyf, textarea.gLFyf, #REsRA'); const _container = document.querySelector('div[jsname=RNNXgb]'); const _tagsBoxWrapper = document.createElement('div'); const _tagsBox = document.createElement('div'); const _deletingZone = document.createElement('div'); const _contextMenu = document.createElement('div'); const _inputFile = document.createElement('input'); const _arrTags = []; const _actions = {}; const _settings = {}; const _defaultSettings = {tagsWidth: 'S', labelsCase: 'a'}; const _paramsKeys = {S: 'tagsWidth', L: 'tagsWidth', A: 'tagsWidth', a: 'labelsCase', c: 'labelsCase', C: 'labelsCase'}; /* _paramsKeys values: S: Small tags width L: Large tags width A: Auto tags width a: Labels with letter-case as typed by the user c: Lowercase labels C: Uppercase labels */ let _tagIdCounter = 0; let _draggedItem = null; let _draggedData = null; let _dragenterBoxCounter = 0; let _history; // -------------------------------------------------- // --- PAGE DRAWING --- // -------------------------------------------------- _tagsBoxWrapper.id = 'od-tagsbox-wrapper'; _tagsBox.id = 'od-tagsbox'; _tagsBoxWrapper.appendChild(_tagsBox); _container.parentNode.insertBefore(_tagsBoxWrapper, _container.nextSibling); function updatePage(str, options = {}){ const res = updateData(str, options); applyParam('tagsWidth'); applyParam('labelsCase'); redrawBox(options); saveData(); if (res.error){ fxGlowErr(_tagsBox); modal(res.error); } else if (options.glow) fxGlow(_tagsBox); return res; } function redrawBox(options = {}){ let delay = 0; let index = 0; const arrRemoved = []; const items = [..._arrTags]; const plus = document.getElementById('od-addtag'); if (plus) index = getItemIndex(plus); else { items.splice(options.plusIndex || 0, 0, {action: 'add', id: 'od-addtag', color: options.plusColor}); } items.forEach(tag=>{ if (tag.action === 'remove'){ arrRemoved.push(tag); } else if (tag.action === 'update'){ fxGlow(setItem(tag)); } else if (tag.action === 'add'){ if (options.noFxIn) addItem(tag, ++index); else { (options.noSlideIn ? fxFadein : fxSlideFadein)(addItem(tag, ++index), 400, delay); delay += 30; } } delete tag.action; }); arrRemoved.forEach(tag=>{ removeItem(tag); }); } function applyParam(param, key){ if (key) setParam(param, key); else key = _settings[param] || _defaultSettings[param]; // Remove the class with the specific prefix from the BOX and reapply it with the new key _tagsBox.className = _tagsBox.className.replace(new RegExp('(^| )' + param + '-[^ ]($| )'), ' '); _tagsBox.classList.add(param + '-' + key); // Select context menu item const old = _contextMenu.querySelector('li[data-group="' + param + '"].od-checked'); if (old) old.classList.remove('od-checked'); _contextMenu.querySelector('li[data-group="' + param + '"][data-key="' + key + '"]').classList.add('od-checked'); } // -------------------------------------------------- // --- DRAG-AND-DROP SETTINGS --- // -------------------------------------------------- // BOX HANDLERS _tagsBox.addEventListener('dragenter', function (e){ e.preventDefault(); e.dataTransfer.dropEffect = _draggedItem ? 'move' : _draggedData ? 'copy' : 'none' ; }); _tagsBox.addEventListener('dragover', function (e){ e.preventDefault(); e.dataTransfer.dropEffect = _draggedItem ? 'move' : _draggedData ? 'copy' : 'none' ; }); // ITEMS HANDLERS function itemDragstart (e){ if (!e.target.matches('.od-item:not(.od-edit-tag)')){ e.preventDefault(); return false; } e.dataTransfer.effectAllowed = "move"; _deletingZone.classList.add('od-dragging'); _tagsBox.classList.add('od-dragging-item'); _draggedItem = e.target; _draggedItem.classList.add('od-draggeditem'); _draggedItem.dataset.startingIndex = getItemIndex(_draggedItem); } function itemDragend (e){ const startingIndex = +_draggedItem.dataset.startingIndex; const currentIndex = getItemIndex(_draggedItem); const belowitem = _tagsBox.querySelector('.od-belowitem'); if (currentIndex !== startingIndex){ if (e.dataTransfer.dropEffect === 'none'){ // If ESC was pressed or the drop target is invalid, cancel the move _tagsBox.insertBefore(_draggedItem, _tagsBox.children[+(currentIndex < startingIndex) + startingIndex]); } else if (_draggedItem.id === 'od-addtag'){ _history.add(); } else if (belowitem !== null){ // Reorder and save the data _arrTags.length = 0; [..._tagsBox.children].forEach(function(tag){ if (!tag.dataset.text) return; _arrTags.push({ label: tag.dataset.label, text: tag.dataset.text, color: tag.dataset.color, id: tag.id }); }); saveData(); } } if (belowitem) belowitem.classList.remove('od-belowitem'); delete _draggedItem.dataset.startingIndex; _draggedItem.classList.remove('od-draggeditem'); _draggedItem = null; _deletingZone.classList.remove('od-dragging', 'od-dragging-hover'); _tagsBox.classList.remove('od-dragging-item'); } function itemDragenter (e){ e.preventDefault(); if (_draggedItem === null && _draggedData === null) e.dataTransfer.effectAllowed = "none"; if (_draggedItem === null) return false; let swapItem = e.target; swapItem.classList.add('od-belowitem'); swapItem = swapItem === _draggedItem.nextSibling ? swapItem.nextSibling : swapItem; _tagsBox.insertBefore(_draggedItem, swapItem); } function itemDragleave (e){ e.target.classList.remove('od-belowitem'); } function itemDragover (e){ e.preventDefault(); e.dataTransfer.dropEffect = _draggedItem === null && _draggedData === null ? 'none' : 'move'; } function setDraggable(item, b=true){ item.draggable = b; } // TAG DELETING ZONE _deletingZone.id = 'od-deletingZone'; _tagsBoxWrapper.appendChild(_deletingZone); _deletingZone.addEventListener('dragenter', function (e){ e.preventDefault(); if (_draggedItem.id !== 'od-addtag') _deletingZone.classList.add('od-dragging-hover'); }); _deletingZone.addEventListener('dragleave', function (e){ e.preventDefault(); _deletingZone.classList.remove('od-dragging-hover'); }); _deletingZone.addEventListener('dragover', function (e){ e.preventDefault(); e.dataTransfer.dropEffect = _draggedItem.id === 'od-addtag' ? 'none' : 'move'; }); _deletingZone.addEventListener('drop', function (e){ e.preventDefault(); if (_draggedItem.id !== 'od-addtag'){ removeItem(getTagById(_draggedItem.id)); saveData(); } }); // -------------------------------------------------- // --- INPUT FILE --- // -------------------------------------------------- _inputFile.id = 'od-inputFile'; _inputFile.type = 'file'; _inputFile.style = 'display:none'; _inputFile.accept = '.txt'; _inputFile.addEventListener('change', function (){ importData(this.files); }); _tagsBoxWrapper.appendChild(_inputFile); // -------------------------------------------------- // --- CONTEXT MENU --- // -------------------------------------------------- _contextMenu.id = 'od-contextMenu'; _contextMenu.innerHTML = `<ul> <li><span><i>🔠</i> Tag properties</span> <ul> <li data-action="setText" class="od-over-tag"><span><i>✏️</i> Tag text<kbd>Shift + Click</kbd></span> <li data-action="setLabel" class="od-over-tag"><span><i>🏷️</i> Custom label <kbd>Alt + Click</kbd></span> <li data-action="setColor" class="od-over-tag od-over-plus"><span><i id="od-setcolor"></i> Color <kbd>Ctrl + Click</kbd></span> </ul> <li></li> <li><span><i>🧰</i> Edit</span> <ul> <li data-action="undo" id="od-contextMenu-undo"><span><i>↶</i> Undo <kbd>Ctrl + Z</kbd></span> <li data-action="redo" id="od-contextMenu-redo"><span><i>↷</i> Redo <kbd>Ctrl + Y</kbd></span> <li></li> <li data-action="copyTags"><span><i>📋</i> Copy Tags <kbd>Ctrl + C</kbd></span> <li data-action="pasteTags"><span><i>📌</i> Paste Tags <kbd>Ctrl + V</kbd></span> <li></li> <li data-action="clearBox"><span><i>🗑️</i> Clear the Tags Box</span> </ul> <li></li> <li data-action="importTags"><span><i>📂</i> Import Tags from txt</span> <li data-action="exportTags"><span><i>💾</i> Export Tags as txt</span> <li></li> <li><span><i>📐</i> Tags width</span> <ul> <li class="od-checkable" data-group="tagsWidth" data-key="S"><i>◻</i> Small Tags width <li class="od-checkable" data-group="tagsWidth" data-key="L"><i>▭</i> Large Tags width <li class="od-checkable" data-group="tagsWidth" data-key="A"><i>⇿</i> Auto Tags width </ul> <li><span><i>Aa</i> Label case</span> <ul> <li class="od-checkable" data-group="labelsCase" data-key="a"><i>Aa</i> As it is <li class="od-checkable" data-group="labelsCase" data-key="c"><i>aa</i> Lowercase <li class="od-checkable" data-group="labelsCase" data-key="C"><i>AA</i> Uppercase </ul> </ul>`; _tagsBox.addEventListener('contextmenu', contextMenuOpen); onoffListeners(_contextMenu, 'mousedown contextmenu wheel', function(e){ e.preventDefault(); e.stopPropagation(); }, true); _contextMenu.querySelector('ul').addEventListener('mouseup', contextMenuClick); _tagsBoxWrapper.appendChild(_contextMenu); function contextMenuOpen(e){ const item = e.target; if (item.tagName.toLowerCase() === 'input') return; e.preventDefault(); const isOverItem = item.classList.contains('od-item'); const isOverPlus = item.id === 'od-addtag'; // Toggle functions for the active BOX item _contextMenu.querySelectorAll('li.od-over-tag').forEach(function(li){ li.classList.toggle('od-disabled', !isOverItem || isOverPlus && !li.classList.contains('od-over-plus')); }); if (isOverItem){ activateItem(item); document.getElementById('od-setcolor').style.color = '#' + item.dataset.color; } // Toggle undo/redo functions document.getElementById('od-contextMenu-undo').classList.toggle('od-disabled', _history.done.length <= 1); document.getElementById('od-contextMenu-redo').classList.toggle('od-disabled', _history.reverted.length === 0); keepBoxOpen(); // Init position const x = e.clientX - 1; const y = e.clientY - 1; _contextMenu.style = 'top: ' + y + 'px; left: ' + x + 'px'; _contextMenu.classList.add('open'); // Fix position to prevent overflow const rect = _contextMenu.getBoundingClientRect(); const fixX = Math.max(0, Math.round(x - Math.max(0, rect.right - window.innerWidth))); const fixY = rect.bottom > window.innerHeight ? Math.max(0, Math.round(rect.top - _contextMenu.offsetHeight)) : y; _contextMenu.style = 'top: ' + fixY + 'px; left: ' + fixX + 'px'; _contextMenu.querySelectorAll(':scope > ul > li > ul').forEach(function(sub){ const item = sub.parentElement; item.classList.remove('od-sub-left'); const rect = sub.getBoundingClientRect(); if (rect.right > window.innerWidth) item.classList.add('od-sub-left'); }); // Enable closing listeners setTimeout(function(){ onoffListeners(window, 'wheel resize blur mousedown contextmenu', contextMenuClose, true); }, 1); _contextMenu.addEventListener('keydown', contextMenuEsc); } function contextMenuEsc(e){ if (e.keyCode === 27) contextMenuClose(); } function contextMenuClose(){ unlockBoxOpen(); deactivateItem(); setTimeout(function(){ _contextMenu.classList.remove('open'); _contextMenu.removeAttribute('style'); onoffListeners(window, 'wheel resize blur mousedown contextmenu', contextMenuClose, false); }, 1); _contextMenu.removeEventListener('keydown', contextMenuEsc); } function contextMenuClick(e){ e.preventDefault(); e.stopPropagation(); if (_contextMenu.querySelector('ul').contains(e.target)){ const menuItem = e.target.closest('li[data-action], li.od-checkable'); if (!menuItem) return; if (menuItem.classList.contains('od-checkable')) _actions.checkItem(menuItem); if (menuItem.dataset.action) _actions[menuItem.dataset.action](); contextMenuClose(); } } // -------------------------------------------------- // --- CONTEXT MENU ACTIONS --- // -------------------------------------------------- _actions.setText = function(){ editTagText(getActiveItem()); }; _actions.setLabel = function(){ editTagLabel(getActiveItem()); }; _actions.setColor = function(){ openColorPicker(getActiveItem()); }; _actions.undo = function(){ _history.undo(); }; _actions.redo = function(){ _history.redo(); }; _actions.copyTags = function(){ // Exit if no data to copy if (_arrTags.length === 0) return; const str = encodeData(); clipboardCopy(str) .then(function(){ fxGlow(_tagsBox); }) .catch(function(){ // Cannot write on clipboard modal(50); // Allow to copy the data from the search field _input.value = str; }) ; }; _actions.pasteTags = function(){ clipboardPaste() .then(function(str){ updatePage(str, {glow: true, from: 'paste'}); }) .catch(function(){ // Cannot read clipboard data modal(60); }) ; }; _actions.importTags = function(){ _inputFile.value = null; _inputFile.click(); }; _actions.exportTags = function(){ exportData(encodeData()); }; _actions.clearBox = function(){ const addtag = document.getElementById('od-addtag'); _arrTags.length = 0; _tagsBox.innerHTML = ''; _tagsBox.append(addtag); saveData(); fxGlow(_tagsBox); }; _actions.checkItem = function(menuItem){ if (menuItem.dataset.group){ // If group, select this item applyParam(menuItem.dataset.group, menuItem.dataset.key); saveData(); } else { // If single item, toggle check menuItem.classList.toggle('od-checked'); } }; // -------------------------------------------------- // --- GENERIC FUNCTIONS --- // -------------------------------------------------- function isNothingFocused(denyIfTextFieldsFocused){ // Returns TRUE if nothing is selected on the page const actEl = document.activeElement; return ( ( !(// check if there are no focused fields denyIfTextFieldsFocused && actEl && ( actEl.tagName.toLowerCase() === 'input' && actEl.type == 'text' || actEl.tagName.toLowerCase() === 'textarea' ) ) && (actEl.selectionStart === actEl.selectionEnd) ) && ['none', 'caret'].includes(window.getSelection().type.toLowerCase()) ); } function onoffListeners(element, events, listener, flag){ const ev = events.trim().split(/ +/); for (let i = 0; i < ev.length; i++){ element[(flag ? 'add' : 'remove') + 'EventListener'](ev[i], listener); } } // -------------------------------------------------- // --- DATA MANAGEMENT --- // -------------------------------------------------- function encodeData(settings = _settings, tags = _arrTags){ let strParams = ''; Object.keys(settings).forEach(function(k){ if (settings[k] != _defaultSettings[k]) strParams += settings[k]; }); return ':tags' + (strParams ? '['+ strParams +']' : '')+ ':' + tags.map(function(e){ return (e.label ? e.label + '::' : '') + e.text + '#' + e.color; }).join(''); } function decodeData(str){ const res = {params: null, tags: [], error: null, buttonColor: ''}; let arrTags = []; if (str == null) return res; str = str.trim().replace(/ +/g, ' '); if (str === ''){ // Empty data res.error = 11; return res; } else if (isTagsPacket(str)){ // If the :tags: prefix is found (in the first line), retrieve parameters and TAGs const matches = str.match(/^\s*:tags(\[(.*)])?:(.*)(?:\r?\n|$)/); if (matches[1] != null){ // If params block found res.params = {}; const keys = matches[2]; let i = keys.length; let k; while (i--){ k = getParamByKey(keys[i]); if (k) res.params[k] = keys[i]; } } arrTags = matches[3] ? matches[3].split('') : []; } else { // If plain text, each line of the string is taken as a TAG arrTags = str.split(/\r?\n/); } res.tags = arrTags.reduce(function(a, b){ const matches = b.match(/^(?:\s*(.*?)\s*::)?\s*((?:^\s*[0-9a-f]{6})|.*?)\s*(?:#?([0-9a-f]{6}))?$/); if (matches){ // Return color for ADD button if (!matches[1] && !matches[2] && arrTags.length === 1) res.buttonColor = matches[3]; // Include valid TAGs else a.push({label: matches[1], text: matches[2], color: matches[3]}); } return a; }, []); // If no valid data was found, report "unknoun data format" error if (res.tags.length === 0 && res.params == null && res.buttonColor === '') res.error = 10; return res; } // Update all TAGs through the specified command string function updateData(str, options = {}){ const data = decodeData(str); const plus = document.getElementById('od-addtag'); const res = { newTags: [], error: data.error, buttonColor: data.buttonColor, keepButtonColor: options.from === 'add-button' ? (data.tags.length === 1 && !!data.tags[0].color) : _arrTags.length > 0 }; // Update settings if BOX is empty or no TAG to add if (data.params !== null && _arrTags.length === 0 || data.tags.length === 0){ Object.keys(_defaultSettings).forEach(function(param){ setParam(param, data.params ? data.params[param] : _settings[param]); }); } // Merge the new data with the existing ones if (data.tags.length){ const newTags = []; let badTagCounter = 0; data.tags.forEach(tag=>{ let exist = getTags(tag.label, tag.text); if (exist){ // Mark duplicate TAGs as to be removed if (exist.withLabel && exist.withText) exist.withText.action = 'remove'; // Mark existing TAGs as to be updated exist = exist.withLabel || exist.withText; exist.action = 'update'; if (tag.label !== undefined && (exist.label || false) !== (tag.label || false)){ exist.label = tag.label || undefined; res.keepButtonColor = true; } if (tag.text && tag.text !== exist.text){ exist.text = tag.text; res.keepButtonColor = true; } exist.color = options.from === 'add-button' ? tag.color || (data.tags.length === 1 && options.color) || exist.color : exist.color; } else if (tag.text !== ''){ // Mark new TAGs as to be added tag.action = 'add'; tag.color = tag.color || options.color || randomColor(); tag.id = 'od-tagref-' + _tagIdCounter++; newTags.push(tag); } else { ++badTagCounter; } }); if (badTagCounter === data.tags.length) { // If no valid TAGs are found, return the "unknown data format" error. res.error = 10; } else if (newTags.length){ res.newTags = newTags; // Consider the position of the ADD button as the index to insert new TAGs const index = plus ? getItemIndex(plus) : 0; // Insert new TAGs _arrTags.splice(index, 0, ...newTags); } } return res; } // Updates the specific TAG. Other involved TAGs can be edited or removed function updateTag(tag, label, text){ // Purge values to avoid format conflicts label = label.trim().replace(/ +/g, ' '); if (label) label = decodeData(label + '::foo').tags[0].label; text = (decodeData(text).tags[0] || {text: ''}).text; // Remove TAG if text is empty if (text === ''){ tag.action = 'remove'; return; } let exist = getTags(label, text); if (exist){ if (exist.withLabel){ exist.withLabel.label = ''; exist.withLabel.action = 'update'; } if (exist.withText) exist.withText.action = 'remove'; } tag.label = label; tag.text = text; tag.action = 'update'; } function getTagById(id){ return _arrTags.find(tag=>tag.id === id); } // Returns an object of existing TAGs by label and text function getTags(label, text){ let withLabel, withText; if (label) withLabel = _arrTags.find(tag=>tag.label && tag.label === label); if (text) withText = _arrTags.find(tag=>tag.text && tag.text.toLowerCase() === text.toLowerCase()); return (withLabel || withText) ? {withLabel: withLabel, withText: withLabel && withLabel === withText ? null : withText} : null; } // Stores data via GM APIs and keeps it backed up with Web Storage Objects async function saveData(){ const str = encodeData(); if (str === ':tags:'){ localStorage.removeItem('odtagsbox'); if (!!GM) await GM.deleteValue('odtagsbox'); } else { _history.add(str); localStorage.setItem('odtagsbox', str); if (!!GM) await GM.setValue('odtagsbox', str); } } function importData(files){ if (window.FileReader){ const file = files[0]; const reader = new FileReader(); reader.addEventListener('load', function (){ updatePage(reader.r###lt, {glow: true, from: 'import'}); }); reader.addEventListener('error', function (e){ // Cannot read this file if (e.target.error.name == 'NotReadableError') modal(21); }); reader.readAsText(file, 'utf-8'); } else { // Cannot open the file reader modal(20); } } function exportData(str){ const name = 'tags_packet.txt'; const blob = new Blob(['\ufeff' + str], { type: 'text/plain;charset=utf-8' }); const objUrl = window.URL.createObjectURL(blob, { type: 'text/plain' }); const a = document.createElement('a'); a.href = objUrl; a.download = name; _tagsBoxWrapper.appendChild(a); a.click(); setTimeout(function (){ window.URL.revokeObjectURL(objUrl); _tagsBoxWrapper.removeChild(a); }, 100); } function isTagsPacket(str){ return /^\s*:tags(?:\[.*])?:/.test(str); } function getParamByKey(k){ return _paramsKeys[k]; } function setParam(param, key){ _settings[param] = key || _defaultSettings[param]; } function clipboardCopy(txt){ // Returns a promise if (navigator.clipboard){ return navigator.clipboard.writeText(txt); } else if (document.queryCommandSupported && document.queryCommandSupported('copy')){ const textarea = document.createElement('textarea'); textarea.value = txt; textarea.style.position = 'fixed'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); return new Promise(function(ok, ko){ if (document.execCommand('copy')) ok(); else ko(); document.body.removeChild(textarea); }); } } function clipboardPaste(){ // Returns a promise if (navigator.clipboard){ return navigator.clipboard.readText(); } else if (document.queryCommandSupported && document.queryCommandSupported('paste')){ return new Promise(function(ok, ko){ if (document.execCommand('paste')) ok(); else ko(); }); } } // Undo/redo functions _history = { done: [], reverted: [], limit: 30, get: function(){ return JSON.stringify([_history.done, _history.reverted]); }, set: function(json){ const data = JSON.parse(json); _history.done = data[0]; _history.reverted = data[1]; _history.restore(_history.done.slice(-1)[0], {noSlideIn: true, noFxIn: false, glow: false}); }, add: function(str = encodeData()){ if (_history.skipAdd){ delete _history.skipAdd; return; } const plus = document.getElementById('od-addtag'); const item = plus.dataset.color + getItemIndex(plus) + str; if (item === _history.done.slice(-1)[0]) return; if (_history.done.length >= _history.limit) _history.done.shift(); _history.done.push(item); _history.reverted.length = 0; }, undo: function() { if (_history.done.length <= 1){ return; } const item = _history.done.pop(); if (item){ _history.reverted.push(item); _history.restore(_history.done.slice(-1)[0]); } }, redo: function() { const item = _history.reverted.pop(); if (item){ _history.done.push(item); _history.restore(item); } }, restore: function(item, options = {noFxIn: true, glow: true}){ const data = item.match(/^([^:]+)(.+)$/); const plusColor = data[1].slice(0, 6); const plusIndex = data[1].slice(6); const str = data[2]; _arrTags.length = 0; _tagsBox.innerHTML = ''; _history.skipAdd = true; updatePage(str, {plusColor: plusColor, plusIndex: plusIndex, noSlideIn: true, noFxIn: options.noFxIn, glow: options.glow, from: 'restore'}); }, keyboardShortcuts: function(e){ if (!e.ctrlKey || !isNothingFocused(true)) return; if ((e.keyCode === 89 && _history.reverted.length > 0) || (e.keyCode === 90 && _history.done.length > 1)){ e.preventDefault(); _history[{89:'redo', 90:'undo'}[e.keyCode]](); } } }; window.addEventListener('keydown', _history.keyboardShortcuts); window.addEventListener('beforeunload', e=>{ sessionStorage.setItem('odtagsbox_history', _history.get()); }); // -------------------------------------------------- // --- DATA TRANSFER --- // -------------------------------------------------- // COPY-PASTE KEYBOARD SHORTCUTS window.addEventListener('copy', function(e){ if (_arrTags.length && isNothingFocused()){ // Put the tags data on the clipboard e.clipboardData.setData('text/plain', encodeData()); e.preventDefault(); fxGlow(_tagsBox); } }); window.addEventListener('paste', function(e){ const str = (e.clipboardData || window.clipboardData).getData('text'); if (isNothingFocused(true)){ updatePage(str, {glow: true, from: 'paste'}); e.preventDefault(); } }); // DRAG-AND-DROP STRING OR EXTERNAL TXT FILE function isValidDraggedDataType(data){ // Accept only TEXT in external data type for (let i = 0; i < data.length; i++){ if (data[i].type.match('^text/plain')){ return true; } } return false; } _tagsBox.addEventListener('dragenter', function (e){ _dragenterBoxCounter++; const data = e.dataTransfer.items; if (_draggedData === null && isValidDraggedDataType(data)){ _draggedData = data[0]; _tagsBox.classList.add('od-dragging-external-data'); } }); _tagsBox.addEventListener('dragleave', function (){ _dragenterBoxCounter--; // Counter needed to prevent bubbling effect if (_dragenterBoxCounter === 0){ if (_draggedData === null) return; _draggedData = null; _tagsBox.classList.remove('od-dragging-external-data'); } }); _tagsBox.addEventListener('drop', function (e){ e.preventDefault(); _draggedData = null; _dragenterBoxCounter = 0; _tagsBox.classList.remove('od-dragging-external-data'); const data = e.dataTransfer.items; // Exit if not TEXT data type if (!isValidDraggedDataType(data)) return false; if (data[0].kind === 'string'){ // If string updatePage(e.dataTransfer.getData('Text'), {glow: true, from: 'drop'}); } else if (data[0].kind === 'file'){ // If file importData(e.dataTransfer.files); } }); // -------------------------------------------------- // --- ITEMS FUNCTIONS --- // -------------------------------------------------- // Add and set a item in the BOX function addItem(o, index){ const item = document.createElement('div'); const label = document.createElement('i'); item.appendChild(label); item.classList.add('od-item'); item.id = o.id; if (index < _tagsBox.childElementCount) _tagsBox.insertBefore(item, _tagsBox.children[index]); else _tagsBox.appendChild(item); setItem(o); // Drag-and-drop item.addEventListener('dragstart', itemDragstart); item.addEventListener('dragend', itemDragend); item.addEventListener('dragenter', itemDragenter); item.addEventListener('dragleave', itemDragleave); item.addEventListener('dragover', itemDragover); return item; } function setItem(o){ const item = document.getElementById(o.id); const label = item.querySelector('i'); const itemText = o.text || ''; const itemLabel = (o.label && o.label !== o.text) ? o.label : ''; const itemColor = o.color ? o.color : randomColor(); setItemColor(item, itemColor); label.dataset.value = itemLabel || itemText; item.title = itemText || 'Add TAG'; item.dataset.text = itemText; if (itemLabel) item.dataset.label = itemLabel; else delete item.dataset.label; setDraggable(item); return item; } // Remove a TAG item function removeItem(tag){ let item = document.getElementById(tag.id); if (item){ item.classList.add('od-removed'); setTimeout(()=>{_tagsBox.removeChild(item);}, 310); } let index = _arrTags.indexOf(tag); if (index !== -1) _arrTags.splice(index, 1); } function setItemColor(item, color){ const label = item.querySelector('i'); label.style.backgroundColor = '#' + color; item.dataset.color = color; // Dark text if the fill is light item.classList.toggle('od-darktext', !isDarkRGB(hex2RGB(color), 170)); } function openColorPicker(item){ keepActiveItem(item); const colorPicker = new ColorPicker({ color: item.dataset.color, target: item, parent: _tagsBoxWrapper, onChange: function(){ setItemColor(item, colorPicker.hex); }, onClose: function(){ boxReset(); if (colorPicker.hex === colorPicker.initHex) return; if (item.id === 'od-addtag'){ _history.add(); return; } _arrTags.find(tag=>tag.id === item.id).color = colorPicker.hex; saveData(); } }); } function editTagText(item){ inputOnTag({ item: item, property: 'text', placeholder: '- text -' }); } function editTagLabel(item){ inputOnTag({ item: item, property: 'label', placeholder: '- label -' }); } function inputOnTag(o){ const item = o.item; const property = o.property; const placeholder = o.placeholder; keepActiveItem(item); const initVal = { label: item.dataset.label || '', text: item.dataset.text }; const label = item.querySelector(':scope > i'); const input = document.createElement('input'); // Get width values let wa = item.offsetWidth; item.classList.add('od-edit-tag'); input.value = label.dataset.value = initVal[property]; let wb = Math.max(60, Math.min(180, item.offsetWidth)); widthTransition(wa, wb); input.placeholder = placeholder; input.spellcheck = false; item.appendChild(input); input.style.opacity = '0'; setTimeout(()=>{input.style.removeProperty('opacity');}, 1); setDraggable(item, false); // FIX: FF unable to interact with mouse on input field when parent is draggable input.focus(); input.addEventListener('input', function(){ label.dataset.value = this.value; }); input.addEventListener('keydown', function(e){ if (e.keyCode === 27) { e.preventDefault(); esc(); } else if (e.keyCode === 13) { e.preventDefault(); done(); } }); input.addEventListener('blur', done); function widthTransition(a, b, callback){ if (widthTransition.running) clearTimeout(widthTransition.timeout); item.style.width = item.style.minWidth = item.style.maxWidth = a + 'px'; if (b != null) setTimeout(widthTransition, 1, b, null, callback); else { item.classList.add('od-edit-tag-transition'); widthTransition.running = true; widthTransition.timeout = setTimeout(()=>{ delete widthTransition.running; widthTransition.end(callback); }, 350); } } widthTransition.end = function(callback){ item.style.removeProperty('width'); item.style.removeProperty('min-width'); item.style.removeProperty('max-width'); item.classList.remove('od-edit-tag-transition'); if (callback) callback(); }; function esc(){ wa = item.offsetWidth; widthTransition.end(); label.dataset.value = initVal.label || initVal.text; close(); } function done(){ wa = item.offsetWidth; widthTransition.end(); if (input.value !== initVal[property]){ const tag = getTagById(item.id); updateTag( tag, property === 'label' ? input.value : initVal.label, property === 'text' ? input.value : initVal.text ); redrawBox(); saveData(); label.dataset.value = tag.label || tag.text; } else label.dataset.value = initVal.label || initVal.text; close(); } function close(){ setDraggable(item, true); input.removeEventListener('blur', done); // Get final width item.style.transition = '0s'; item.classList.remove('od-edit-tag'); wb = item.offsetWidth; item.style.removeProperty('transition'); item.classList.add('od-edit-tag'); input.style.opacity = '0'; widthTransition(wa, wb, ()=>{ item.removeChild(input); item.classList.remove('od-edit-tag'); }); boxReset(); } } // Get the index of the item in the BOX function getItemIndex(item){ return [..._tagsBox.querySelectorAll(':scope > :not(.od-removed)')].indexOf(item); } function activateItem(item){ deactivateItem(); item.classList.add('od-active', 'od-highlight'); } function deactivateItem(){ const activeItem = getActiveItem(); if (activeItem) activeItem.classList.remove('od-active', 'od-highlight'); return activeItem; } function getActiveItem(){ return _tagsBox.querySelector(':scope .od-item.od-active'); } function keepActiveItem(item = getActiveItem()){ setTimeout( function(){ keepBoxOpen(); activateItem(item); }, 1); } function keepBoxOpen(){ _tagsBox.classList.add('od-keep-open'); } function unlockBoxOpen(){ _tagsBox.classList.remove('od-keep-open'); } function boxReset(){ unlockBoxOpen(); setTimeout(deactivateItem, 1); } // -------------------------------------------------- // --- CLICK ITEMS --- // -------------------------------------------------- _tagsBox.addEventListener('click', function (e){ const item = e.target; if (!item.classList.contains('od-item')) return; const query = _input.value; const label = item.querySelector(':scope > i'); if (item.id === 'od-addtag'){ // PLUS BUTTON (+) - Adds in the BOX new TAGs based on the search field query or highlighted text const singleTag = !isTagsPacket(query); const labelFormat = singleTag && /^.*::/.test(query); const str = ((labelFormat || _input.selectionStart === _input.selectionEnd) ? query : query.substring(_input.selectionStart, _input.selectionEnd)).trim(); let res = {}; if (e.ctrlKey){ // If CTRL was pressed, edit color openColorPicker(item); return; } else if (!str) _input.focus(); else { res = updatePage((singleTag ? ':tags:' : '') + str, {from: 'add-button', color: item.dataset.color}); if (labelFormat && res.newTags.length === 1){ _input.value = res.newTags[0].text + ' '; _input.focus(); } } // Set the button color if (!res.keepButtonColor){ const newColor = res.buttonColor || randomColor(); setItemColor(item, newColor); if (res.buttonColor) fxGlow(item); _history.add(); } } else if (!item.classList.contains('od-edit-tag')){ // TAG ELEMENT - Enters the text of the TAG in the search field or edits its properties const itemText = item.dataset.text; if (e.shiftKey){ // If SHIFT was pressed, edit text editTagText(item); } else if (e.altKey){ // If ALT was pressed, edit label editTagLabel(item); } else if (e.ctrlKey){ // If CTRL was pressed, edit color openColorPicker(item); } else if (_input.selectionStart !== undefined){ // If there is a selection, the TAG text will be inserted relative to it let startPos = _input.selectionStart; let endPos = _input.selectionEnd; const text = (startPos > 0 ? ' ' : '') + itemText + ' '; if (startPos > 0 && query[startPos-1] === ' ') startPos--; if (endPos < query.length && query[endPos] === ' ') endPos++; _input.value = query.slice(0, startPos) + text + query.slice(endPos); _input.focus(); const pos = startPos + text.length; _input.setSelectionRange(pos, pos); } else { // Append the TAG text _input.value = query.trim() + ' ' + itemText + ' '; _input.focus(); _input.click(); } } }); // -------------------------------------------------- // --- COLOR PROCESSING --- // -------------------------------------------------- function hex2HSV(hex){ const [r, g, b] = hex.match(/../g).map(c=>parseInt(c, 16) / 255); const v = Math.max(r, g, b), c = v - Math.min(r, g, b); const h = c && ((v === r) ? (g - b) / c : ((v === g) ? 2 + (b - r) / c : 4 + (r - g) / c)); return {h: (h < 0 ? h + 6 : h) / 6, s: v && c / v, v: v}; } function HSV2Hex(hsv){ 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); return f(5) + f(3) + f(1); } function hex2RGB(hex){ return hex.match(/../g).reduce((a, v, i)=>({ ...a, ['rgb'[i]]: parseInt(v, 16)}), {}); } function RGBCSS2Obj(str){ return str.slice(4, -1).split(',').reduce((a, v, i)=>({ ...a, ['rgb'[i]]: v}), {}); } function randomHSV(){ return {h: Math.random(), s: 0.3 + 0.4 * Math.random(), v: 0.5 + 0.2 * Math.random()}; } function randomColor(){ return HSV2Hex(randomHSV()); } function isDarkRGB(rgb, threshold = 155){ // threshold range [0, 255] return rgb.r * 0.2126 + rgb.g * 0.7152 + rgb.b * 0.0722 < threshold; } // -------------------------------------------------- // --- COLOR PICKER --- // -------------------------------------------------- class ColorPicker { constructor(o){ const me = this; me.hex = me.initHex = o.color || '000000'; me.hsv = hex2HSV(me.hex); me.parent = o.parent || document.body; me.picker = document.createElement('div'); me.block = document.createElement('div'); me.strip = document.createElement('div'); me.blockThumb = document.createElement('i'); me.stripThumb = document.createElement('i'); me.block.tabIndex = 0; me.strip.tabIndex = 0; me.operatedSlider = null; me.events = ['change', 'close', 'startSlide', 'endSlide'].reduce((a, b)=>({ ...a, [b]: o['on' + b[0].toUpperCase() + b.slice(1)]}), {}); me.init(); me.display(); me.position(o.target); } init(){ const me = this; me.picker.classList.add('od-colorpicker'); me.block.classList.add('od-colorpicker-block'); me.strip.classList.add('od-colorpicker-strip'); me.block.appendChild(me.blockThumb); me.strip.appendChild(me.stripThumb); me.picker.dataset.color = me.hex; me.picker.appendChild(me.block); me.picker.appendChild(me.strip); function sliding(e){ if (me.operatedSlider === me.block){ const rect = me.block.getBoundingClientRect(); me.hsv.s = Math.max(0, Math.min(1, 1 / me.block.offsetWidth * (e.clientX - rect.left))); me.hsv.v = Math.max(0, Math.min(1, 1 - (1 / me.block.offsetHeight * (e.clientY - rect.top)))); me.setBlock(); } else if (me.operatedSlider === me.strip){ const rect = me.strip.getBoundingClientRect(); me.hsv.h = Math.max(0, Math.min(1, 1 / me.strip.offsetWidth * (e.clientX - rect.left))); me.setStrip(); } const newHex = HSV2Hex(me.hsv); if (me.hex !== newHex){ me.hex = newHex; me.change(); } } function endSlide(){ window.removeEventListener('mouseup', endSlide); window.removeEventListener('mousemove', sliding); document.documentElement.classList.remove('od-colorpicker-sliding'); me.operatedSlider = null; me.handler('endSlide'); } me.picker.addEventListener('mousedown', function(e){ e.stopPropagation(); if (me.block.contains(e.target)) me.operatedSlider = me.block; else if (me.strip.contains(e.target)) me.operatedSlider = me.strip; else return; document.documentElement.classList.add('od-colorpicker-sliding'); me.handler('startSlide'); sliding(e); window.addEventListener('mousemove', sliding); window.addEventListener('mouseup', endSlide); }); onoffListeners(me.picker, 'contextmenu wheel', function(e){ e.preventDefault(); e.stopPropagation(); }, true); function beforeClosing(){ onoffListeners(window, 'wheel resize blur mousedown contextmenu', beforeClosing, false); me.close(); } onoffListeners(window, 'wheel resize blur mousedown contextmenu', beforeClosing, true); function esc(e){ if (e.keyCode === 27){ if (me.hex === me.initHex) beforeClosing(); else { me.hsv = hex2HSV(me.hex = me.initHex); me.setBlock(); me.setStrip(); me.change(); } } } me.picker.addEventListener('keydown', esc); } display(){ this.parent.appendChild(this.picker); this.setBlock(); this.setStrip(); } position(target){ let x = 0; let y = 0; if (target){ const rect = target.getBoundingClientRect(); x = (rect.left + this.picker.offsetWidth > window.innerWidth) ? Math.max(0, Math.round(rect.right - this.picker.offsetWidth)) : rect.left; y = (rect.bottom + this.picker.offsetHeight > window.innerHeight) ? Math.max(0, Math.round(rect.top - this.picker.offsetHeight)) : rect.bottom; } this.picker.style = 'top: ' + y + 'px; left: ' + x + 'px'; } setBlock(){ const x = Math.round(this.block.offsetWidth * this.hsv.s); const y = Math.round(this.block.offsetHeight * (1 - this.hsv.v)); this.blockThumb.style = 'top: ' + y + 'px; left: ' + x + 'px;'; } setStrip(){ const hue = 'hsl(' + Math.round(this.hsv.h * 360) + ',100%,50%)'; const x = Math.round(this.strip.offsetWidth * this.hsv.h); this.stripThumb.style = 'left: ' + x + 'px; color: ' + hue; this.block.style.color = hue; } change(){ this.handler('change'); } close(){ this.parent.removeChild(this.picker); this.handler('close'); } handler(event){ if (typeof this.events[event] === 'function') this.events[event](); } } // -------------------------------------------------- // --- EFFECTS --- // -------------------------------------------------- function fxGlow(el){ el.classList.add('od-highlight'); setTimeout(function(){el.classList.remove('od-highlight');}, 500); } function fxGlowErr(el){ el.classList.add('od-error'); setTimeout(function(){el.classList.remove('od-error');}, 800); } function fxFadein(el, duration, delay){ duration = duration == null ? 300 : +duration; delay = delay == null ? 0 : +delay; el.style.opacity = '0'; el.style.transition = duration + 'ms ' + delay + 'ms ease-in-out'; setTimeout(function(){ el.style.removeProperty('opacity'); setTimeout(function(){ el.style.removeProperty('transition'); }, duration + delay); }, 1); } function fxSlideFadein(el, duration, delay){ duration = duration == null ? 300 : +duration; delay = delay == null ? 0 : +delay; el.style.opacity = '0'; el.style.minWidth = '0'; el.style.maxWidth = '0'; el.style.transition = duration + 'ms ' + delay + 'ms ease-in-out'; setTimeout(function(){ el.style.removeProperty('opacity'); el.style.removeProperty('min-width'); el.style.removeProperty('max-width'); setTimeout(function(){ el.style.removeProperty('transition'); }, duration + delay); }, 1); } // -------------------------------------------------- // --- MODAL --- // -------------------------------------------------- function modal(msg, delay = 10){ if (typeof msg === 'number'){ msg = modal.msgList[msg]; } // Prevents freezing of hovered elements when the alert is shown _tagsBoxWrapper.classList.add('od-nohover'); setTimeout(function(){ alert(msg); _tagsBoxWrapper.classList.remove('od-nohover'); }, delay); } modal.msgList = { 10: '⚠️ Sorry!\nI don\'t understand the format of this data.\n\nNo TAGs have been added.', 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.', 20: '⚠️ Oops!\nI can\'t open the file reader.💡 But...\nyou can open it elsewhere, then try the copy-paste functions.', 21: '⚠️ Oops!\nI can\'t read this file.💡 Try picking it up and opening it again.', 50: '⚠️ Oops!\nUnable to copy data to clipboard.\n\n💡 But...\nyou can copy the string from the search field.', 60: '⚠️ Oops!\nI can\'t read data from the clipboard.\n\n💡 But... try with CTRL+V.\n– Close this modal first –', }; // -------------------------------------------------- // --- START --- // -------------------------------------------------- async function start(){ // If exist, use the history data stored in the local session const data = sessionStorage.getItem('odtagsbox_history'); if (data){ _history.set(data); return; } // Retrieve data via GM APIs or fall back to localStorage let str = !!GM && await GM.getValue('odtagsbox'); if (!str) str = localStorage.getItem('odtagsbox'); _tagsBox.innerHTML = ''; updatePage(str, {noSlideIn: true, from: 'start'}); setTimeout(function(){ _tagsBox.classList.remove('od-hidein');}, 2); } // -------------------------------------------------- // --- STYLE --- // -------------------------------------------------- function addGlobalStyle(strCSS){ const h = document.querySelector('head'); if (!h) return; const s = document.createElement('style'); s.type = 'text/css'; s.innerHTML = strCSS; h.appendChild(s); } function getColorMode(mode){ return {dark: mode === 'dark', light: mode !== 'dark'}; } function css (colorMode){ return ( ` /* RESET */ /* Google SERP - make space for the BOX */ #tsf, #sf { margin-top: 10px !important; transition: margin-top .8s ease-in-out } #searchform.minidiv #tsf, #kO001e.DU1Mzb #sf{ padding-top: 16px !important } #searchform > .sfbg { margin-top: 0 !important } #searchform.minidiv > .sfbg { padding-top: 2px } /* Google Images SERP - fix position */ #sf #od-tagsbox { margin: -5px 0 0 3px } #kO001e.DU1Mzb { padding: 10px 0 6px } .M3w8Nb #od-tagsbox-wrapper, .KZFCbe #od-tagsbox-wrapper { padding-left: 27px } /* Demote dropdowns/popups below the search field to avoid overlapping on the BOX */ .ea0Lbe, #tsf .UUbT9 { z-index: 984 !important } /* CONTAINERS */ #od-tagsbox-wrapper *, #od-tagsbox-wrapper *::before, #od-tagsbox-wrapper *::after { box-sizing: border-box; } #od-tagsbox-wrapper { height: 0; } #od-tagsbox { position: absolute; top: -29px; max-width: 100%; max-height: 32px; border: 1px solid; border-color: rgba(${ colorMode.dark ? '95,99,104' : '208,211,215' },0); border-radius: 16px; outline: 2px solid transparent; background: rgba(${ colorMode.dark ? '75,75,75' : '240,240,240' },0); box-shadow: 0 2px 5px 1px rgba(64,60,67,0); overflow: hidden; transition: all .4s .1s ease-in-out, z-index 0s, outline-style 0s .4s; z-index: 985; } #searchform #od-tagsbox { top: -34px; left: 30px; } #od-tagsbox-wrapper.od-nohover { pointer-events: none; } #od-tagsbox-wrapper.od-nohover > #od-tagsbox { transition: 0s; } #od-tagsbox-wrapper:not(.od-nohover) > #od-tagsbox:hover, #od-tagsbox.od-keep-open { max-height: 300px; border-color: rgba(${ colorMode.dark ? '95,99,104' : '208,211,215' },1); background: rgba(${ colorMode.dark ? '75,75,75' : '240,240,240' },.8); box-shadow: 0 2px 5px 1px rgba(64,60,67,.3); transition: all .2s, max-height .4s .1s ease-in-out, z-index 0s; } /* ITEM */ .od-item { position: relative; float: left; height: 30px; outline-color: transparent; font: normal 12px/20px Arial, sans-serif; text-align: center; cursor: pointer; transition: all .3s ease-out, opacity .3s .1s ease-out; } /* ITEM WIDTH PRESETS */ /* #od-tagsbox.tagsWidth-S > .od-item > i { min-width: 24px; max-width: 24px; } #od-tagsbox.tagsWidth-L > .od-item > i { min-width: 54px; max-width: 54px; } #od-tagsbox.tagsWidth-A > .od-item > i { min-width: 24px; max-width: 174px; } #od-tagsbox.tagsWidth-A > .od-item > i::before { text-overflow: ellipsis; } */ #od-tagsbox.tagsWidth-S > .od-item { min-width: 30px; max-width: 30px; } #od-tagsbox.tagsWidth-L > .od-item { min-width: 60px; max-width: 60px; } #od-tagsbox.tagsWidth-A > .od-item { min-width: 30px; max-width: 180px; } #od-tagsbox.tagsWidth-A > .od-item > i::before { text-overflow: ellipsis; } /* TAG LABEL */ .od-item > i { display: block; height: calc(100% - 6px); margin: 3px; padding: 0 3px; color: #fff; border: 2px solid rgba(0,0,0,.2); border-radius: 15px; outline: 1px solid transparent; font: inherit; white-space: nowrap; pointer-events: none; transition: all .3s ease-out, color .3s ease-out, background-color .3s ease-out, font-size 0s, font-weight 0s; } .od-item > i::before { content: attr(data-value); display: block; width: 100%; height: calc(100% - 2px); overflow: hidden; } .od-item.od-darktext > i { color: rgba(0, 0, 0, .7); } #od-addtag > i{ font-size: 18px; font-weight: bold; } #od-addtag > i::before { content: "+"; } /* LABEL CASE PRESETS */ #od-tagsbox.labelsCase-c > .od-item > i { text-transform: lowercase; } #od-tagsbox.labelsCase-C > .od-item > i { text-transform: uppercase; } /* USER-DEFINED LABELS */ .od-item[data-label] > i::before { border-bottom: 1px dashed currentcolor; transition: border-color .3s ease-out; } /* ITEM HOVER */ #od-tagsbox > .od-item:not(.od-draggeditem):hover > i { border-color: rgba(255,255,255,.4); outline-color: rgba(0,0,0,.4); transition-duration: 0s, .3s, .3s, 0s, 0s; } #od-tagsbox > .od-item.od-darktext:not(.od-draggeditem):hover > i { color: #000; } /* ACTIVE ITEM */ #od-tagsbox > .od-item.od-active > i { transition-duration: .3s, .1s, 0s, 0s, 0s; } /* ITEM REMOVED */ #od-tagsbox > .od-item.od-removed { max-width: 0; min-width: 0; } #od-tagsbox > .od-item.od-removed > i { opacity: 0; } /* EDIT TAG */ #od-tagsbox#od-tagsbox > .od-edit-tag { min-width: 60px; max-width: 180px; } #od-tagsbox#od-tagsbox > .od-edit-tag-transition { transition: .3s; } .od-edit-tag > i::before { /* Keep extra spaces while editing */ white-space: pre; } #od-tagsbox#od-tagsbox > .od-item.od-edit-tag:not(.od-edit-tag-transition) > i::before { visibility: hidden; } #od-tagsbox.tagsWidth-A > .od-item.od-edit-tag-transition > i::before { text-overflow: clip; } .od-edit-tag > input { position: absolute; top: 7px; left: 5px; height: calc(100% - 14px); width: calc(100% - 10px); margin: 0; padding: 0; color: #0a0905; font: inherit; text-align: inherit; border: solid rgba(0,0,0,.3); border-width: 1px 0; border-radius: 6px; background: rgba(255,255,255,.8); transition: opacity .3s; } .od-item:not(.od-edit-tag) > input { display: none; } .od-edit-tag > input:focus-visible { outline: none; } /* DRAG-AND-DROP */ .od-draggeditem > i { opacity: 0; } #od-tagsbox.od-dragging-item { z-index: 988; } #od-tagsbox.od-dragging-item > .od-item { opacity: .6; transition-delay: 0s; } .od-belowitem { } #od-deletingZone { position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(255,0,0,.2); opacity: 0; display: none; z-index: 987; transition: .3s, z-index 0s; } #od-deletingZone.od-dragging { display: block; } #od-deletingZone.od-dragging-hover { opacity: 1; } #od-tagsbox::before { content:""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: rgba(138,180,248,.34); border-radius: inherit; border: 1px dashed rgb(138,180,248); opacity: 0; transition: .3s; } #od-tagsbox.od-dragging-external-data { z-index: 998; } #od-tagsbox.od-dragging-external-data::before { opacity: 1; } #od-tagsbox.od-dragging-external-data > .od-item { transition: .3s; opacity: .5; pointer-events: none; } /* CONTEXT MENU */ /* Containers */ #od-contextMenu { position: fixed; z-index: 999; font: 400 12px/23px "Segoe UI", Calibri, Arial, sans-serif; color: #000; user-select: none; cursor: default; } #od-contextMenu:not(.open) { display: none; } #od-contextMenu ul { list-style-type: none; margin: 0; padding: 3px 0; border: 1px #dadce0 solid; background: #fff; box-shadow: 5px 5px 4px -4px rgba(0,0,0,.9); } /* Item */ #od-contextMenu ul > li { position: relative; margin: 0; padding: 0 22px 0 38px; line-height: 23px; white-space: nowrap; } /* Separator */ #od-contextMenu ul > li:empty { margin: 4px 1px; padding: 0; border-top: 1px #dadce0 solid; } /* Item content */ #od-contextMenu ul > li > span { display: flex; } /* Icon */ #od-contextMenu ul > li i:first-child { position: absolute; top: 0; left: 0; display: block; width: 35px; text-align: center; font-size: 1.3em; line-height: 23px; font-style: normal; } /* Shortcut */ #od-contextMenu ul > li kbd { margin-left: auto; padding-left: 10px; font: inherit; } #od-contextMenu ul > li:not(:hover) kbd { color: #5f6368; } /* Item hover */ #od-contextMenu ul > li:hover { color: #000; background: #e8e8e9; } /* Checkable item */ #od-contextMenu ul > li.od-checkable { padding-left: 48px; } #od-contextMenu ul > li.od-checkable.od-checked::before { content: "✓"; position: absolute; left: 32px; } /* Submenu */ #od-contextMenu ul > li > ul { display: block; position: absolute; top: 0; width: auto; min-width: 80px; white-space: nowrap; visibility: hidden; opacity: 0; transition: visibility 0s .3s, opacity .3s; } #od-contextMenu ul > li:not(.od-sub-left) ul { left: 100%; } #od-contextMenu ul > li.od-sub-left ul { right: 100%; } #od-contextMenu ul > li:hover > ul { visibility: visible; opacity: 1; z-index: 1; transition: visibility 0s, opacity .3s; } /* Arrow to open submenu */ #od-contextMenu ul > li > :first-child:not(:last-child)::after { content: "\\23F5"; position: absolute; right: 3px; font-size: .9em; line-height: inherit; opacity: .7; } /* Disabled item */ #od-contextMenu ul li.od-disabled { pointer-events: none; opacity: .55; filter: saturate(0); } /* Color setting */ #od-setcolor::before { content: ""; display: inline-block; width: 14px; height: 14px; border: 1px solid #000; outline: 1px solid #777; background: currentColor; } /* COLOR PICKER */ .od-colorpicker { position: fixed; z-index: 999; display: flex; flex-direction: column; align-items: center; width: 225px; padding: 4px; border: 1px solid #858585; color: #fff; background: ${colorMode.dark ? '#707578' : '#919395'}; box-shadow: 5px 5px 4px -4px rgba(0,0,0,.9); } .od-colorpicker > div { position: relative; cursor: pointer; } .od-colorpicker > div:focus-visible { outline: none; } .od-colorpicker > div > i { pointer-events: none; content: ''; position: absolute; transform: translate(-50%, -50%); display: block; box-shadow: none; border: 2px solid #fff; outline: 2px solid #0007; height: 16px; width: 16px; border-radius: 100%; color: transparent; background: currentColor; transition: outline-color .3s; } .od-colorpicker > div:active > i { outline-color: #75bfff; transition-duration: 0s; } .od-colorpicker-block { width: 100%; padding-bottom: 100%; color: inherit; background: linear-gradient(to right, #fff, currentColor); overflow: hidden; } .od-colorpicker-block::before { content: ''; position: absolute; top: 0; right: 0; bottom: 0; left: 0; background: linear-gradient(to bottom, transparent, #000); } .od-colorpicker-strip { width: calc(100% - 10px); height: 16px; margin: 5px 0 1px; } .od-colorpicker-strip::before{ content: ''; display: block; position: absolute; top: 0; right: -5px; bottom: 0; left: -5px; border: solid transparent; border-width: 3px 0; background: padding-box linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); } html.od-colorpicker-sliding { cursor: pointer; } html.od-colorpicker-sliding > body { user-select: none; pointer-events: none; } .od-colorpicker-block > i { will-change: left, top; } .od-colorpicker-strip > i { will-change: left; top: 50%; } /* EFFECTS */ /* Glow */ #od-tagsbox.od-highlight, .od-item.od-highlight::before { outline-color: #45bfff; transition: 0s; } #od-tagsbox.od-highlight { background: rgba(100,180,255,.6); } .od-item::before { content: ""; display: block; position: absolute; top: 3px; right: 3px; bottom: 3px; left: 3px; border-radius: 15px; outline: 2px solid transparent; transition: .4s ease-in-out; } /* Glow error */ #od-tagsbox.od-error { background-color: rgba(255,0,0,.6) !important; outline-color: #f00; transition: 0s; } /* COLOR SCHEME */ @media (prefers-color-scheme: dark) { /* Dark-mode applies to the context menu according to the system color scheme */ #od-contextMenu { color: #fff; font-weight: 100; } #od-contextMenu ul { background: #292a2d; border-color: #3c4043; } #od-contextMenu ul > li:empty { border-color: #3c4043; } #od-contextMenu ul > li:hover { color: #fff; background: #3f4042; } #od-contextMenu ul > li:not(:hover) kbd { color: #9aa0a6; } }` ); } // -------------------------------------------------- // --- WE CAN START! --- // -------------------------------------------------- start(); });