// ==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.result, {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();
});