Greasy Fork is available in English.

Google Searching Tags Box

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

// ==UserScript==
// @name         Google Searching Tags Box
// @version      2.0.4
// @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/*
// @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';
}

// 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;
  item.draggable = true;

  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);

  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(){
    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();

});