File Regexplorer

Implements local explorer - file manager and provide a means to sort the file list in custom order via regular expressions

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name              File Regexplorer
// @version           0.2.1
// @namespace         file_regexplorer
// @description       Implements local explorer - file manager and provide a means to sort the file list in custom order via regular expressions
// @description:en    Implements local explorer - file manager and provide a means to sort the file list in custom order via regular expressions
// @description:it    Implementa il file manager locale del browser e fornisce un sistema per ordinare l'elenco dei file in modo personalizzato tramite espressioni regolari
// @author            OpenDec
// @match             file:///*/
// @icon              data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBzdGFuZGFsb25lPSJubyI/Pg0KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4NCjwhLS1HZW5lcmF0b3I6IFhhcmEgRGVzaWduZXIgKHd3dy54YXJhLmNvbSksIFNWRyBmaWx0ZXIgdmVyc2lvbjogNi4wLjAuNC0tPg0KPHN2ZyBzdHJva2Utd2lkdGg9IjAuNTAxIiBzdHJva2UtbGluZWpvaW49ImJldmVsIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgb3ZlcmZsb3c9InZpc2libGUiIHdpZHRoPSIyODQuNzE4cHQiIGhlaWdodD0iMjQyLjk5N3B0IiB2aWV3Qm94PSIwIDAgMjg0LjcxOCAyNDIuOTk3Ij4NCiA8ZGVmcz4NCgk8L2RlZnM+DQogPGcgaWQ9IkRvY3VtZW50IiBmaWxsPSJub25lIiBzdHJva2U9ImJsYWNrIiBmb250LWZhbWlseT0iVGltZXMgTmV3IFJvbWFuIiBmb250LXNpemU9IjE2IiB0cmFuc2Zvcm09InNjYWxlKDEgLTEpIj4NCiAgPGcgaWQ9IlNwcmVhZCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCAtMjQyLjk5NykiPg0KICAgPGcgaWQ9IkxheWVyIDMiPg0KICAgIDxwYXRoIGQ9Ik0gMCwyMTUuMDczIEMgMCwyMzAuNDg3IDEyLjUxLDI0Mi45OTcgMjcuOTIzLDI0Mi45OTcgTCA2MC41NzgsMjQyLjk5NyBDIDc1Ljk5MSwyNDIuOTk3IDg4LjUwMSwyMzAuNDg3IDg4LjUwMSwyMTUuMDczIEwgODguNTAxLDIxMC43NDkgTCAyMjQuNzM4LDIxMC43NSBDIDI0MS4wMjgsMjEwLjc1IDI1My4wMzUsMTk3LjU4NCAyNTEuNTM4LDE4MS4zNjMgTCAyMzcuNTEsMjkuMzg4IEwgMC4wMDEsMjkuMTUxIiBmaWxsPSIjY2Y5YzBlIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSI3OS44NDAzMTkzNjEyNzc1Ii8+DQogICAgPHBhdGggZD0iTSA1Mi41LDE3NS4zMjQgTCA1Mi41LDczLjY3NSBMIDIzNi4yNTEsNzMuNjc1IEwgMjM2LjI1MSwxNzUuMzI0IEMgMjM2LjI1MSwxODMuMjg3IDIyOS43ODgsMTg5Ljc1IDIyMS44MjUsMTg5Ljc1IEwgNjYuOTI2LDE4OS43NSBDIDU4Ljk2MywxODkuNzUgNTIuNSwxODMuMjg3IDUyLjUsMTc1LjMyNCBaIiBmaWxsPSIjZmZmZmZmIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG1hcmtlci1zdGFydD0ibm9uZSIgbWFya2VyLWVuZD0ibm9uZSIgc3Ryb2tlLW1pdGVybGltaXQ9Ijc5Ljg0MDMxOTM2MTI3NzUiLz4NCiAgICA8cGF0aCBkPSJNIDIzLjQ3NSwxMjguNzQ0IEwgMS4wOSwzNy41MTEgQyAtMy45ODksMTYuODA1IDkuMTkxLDAgMzAuNTExLDAgTCAyMTMuNDE3LDAgQyAyMzQuNzM3LDAgMjU2LjE2NCwxNi44MDUgMjYxLjI0NCwzNy41MTEgTCAyODMuNjI5LDEyOC43NDQgQyAyODguNzA5LDE0OS40NDkgMjc1LjUyOSwxNjYuMjU0IDI1NC4yMDgsMTY2LjI1NCBMIDcxLjMwMywxNjYuMjU0IEMgNDkuOTgyLDE2Ni4yNTQgMjguNTU1LDE0OS40NDkgMjMuNDc1LDEyOC43NDQgWiIgZmlsbD0iI2ZlZDU1NyIgc3Ryb2tlPSJub25lIiBzdHJva2Utd2lkdGg9IjAuNSIgc3Ryb2tlLWxpbmVqb2luPSJtaXRlciIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBtYXJrZXItc3RhcnQ9Im5vbmUiIG1hcmtlci1lbmQ9Im5vbmUiIHN0cm9rZS1taXRlcmxpbWl0PSI3OS44NDAzMTkzNjEyNzc1Ii8+DQogICAgPHBhdGggZD0iTSAxNzIuNzI1LDg5LjI4NCBMIDE3Mi43MjQsMTY5Ljc5NyBDIDE3Mi43MjQsMTcyLjczNCAxNzUuMTA3LDE3NS4xMTYgMTc4LjA0MywxNzUuMTE2IEwgMjE3LjMzLDE3NS4xMTYgQyAyMjAuMjY1LDE3NS4xMTYgMjIyLjY0OCwxNzIuNzM0IDIyMi42NDgsMTY5Ljc5NyBMIDIyMi42NDUsODkuMjg0IEMgMjIyLjY0NSw4NC4xMjUgMjE4Ljg2NSw4MS43MzkgMjE0LjIwNyw4My45NTYgTCAxOTkuMzc4LDkxLjAyMSBDIDE5Ny42ODUsOTEuODA2IDE5Ny42ODUsOTEuODA2IDE5NS45OTIsOTEuMDIxIEwgMTgxLjE2NSw4My45NTYgQyAxNzYuNTA2LDgxLjczOSAxNzIuNzI1LDg0LjEyNSAxNzIuNzI1LDg5LjI4NCBaIiBmaWxsPSIjZDAwMDAwIiBzdHJva2U9Im5vbmUiIHN0cm9rZS13aWR0aD0iMC41IiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG1hcmtlci1zdGFydD0ibm9uZSIgbWFya2VyLWVuZD0ibm9uZSIgc3Ryb2tlLW1pdGVybGltaXQ9Ijc5Ljg0MDMxOTM2MTI3NzUiLz4NCiAgICA8cGF0aCBkPSJNIDYyLjQ5NywxMTIuNjk1IEMgNTAuMzc1LDEwMi42MDUgNDEuODQyLDg3LjkxMSA0MS44NDIsNzIuNzYgQyA0MS44NDIsNTcuNjA5IDUwLjM3Niw0Mi45MTUgNjIuNDk4LDMyLjgyNSIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2Utd2lkdGg9IjE1IiBzdHJva2U9IiM2MzRkMjkiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSI3OS44NDAzMTkzNjEyNzc1Ii8+DQogICAgPHBhdGggZD0iTSAxMzcuODQ0LDExMi42OTUgQyAxNDkuOTY2LDEwMi42MDUgMTU4LjQ5OSw4Ny45MTEgMTU4LjQ5OSw3Mi43NiBDIDE1OC40OTksNTcuNjA5IDE0OS45NjUsNDIuOTE1IDEzNy44NDMsMzIuODI1IiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iMTUiIHN0cm9rZT0iIzYzNGQyOSIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9Ijc5Ljg0MDMxOTM2MTI3NzUiLz4NCiAgICA8cGF0aCBkPSJNIDc5Ljk3Myw3MS4xNTggQyA4Ni40NTUsNzEuMTU4IDkxLjcxNyw2NS44OTYgOTEuNzE3LDU5LjQxNCBDIDkxLjcxNyw1Mi45MzIgODYuNDU1LDQ3LjY3IDc5Ljk3Myw0Ny42NyBDIDczLjQ5MSw0Ny42NyA2OC4yMjksNTIuOTMyIDY4LjIyOSw1OS40MTQgQyA2OC4yMjksNjUuODk2IDczLjQ5MSw3MS4xNTggNzkuOTczLDcxLjE1OCBaIiBzdHJva2Utd2lkdGg9IjAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgZmlsbD0iIzYzNGQyOSIgc3Ryb2tlPSJub25lIiBzdHJva2UtbGluZWpvaW49Im1pdGVyIiBtYXJrZXItc3RhcnQ9Im5vbmUiIG1hcmtlci1lbmQ9Im5vbmUiIHN0cm9rZS1taXRlcmxpbWl0PSI3OS44NDAzMTkzNjEyNzc1Ii8+DQogICAgPHBhdGggZD0iTSAxMDIuNzg5LDgwLjEzOSBMIDEwNy4xNjMsOTEuOTYgTCA5Ni42NzYsOTguOTUxIEwgMTA5LjI3MSw5OC40NDQgTCAxMTIuNjc4LDExMC41NzggTCAxMTYuMDg4LDk4LjQ0MyBMIDEyOC42ODIsOTguOTUxIEwgMTE4LjE5NCw5MS45NiBMIDEyMi41NjksODAuMTM5IEwgMTEyLjY3OCw4Ny45NTIgTCAxMDIuNzg5LDgwLjEzOSBaIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS13aWR0aD0iOSIgc3Ryb2tlPSIjNjM0ZDI5IiBmaWxsPSIjNjM0ZDI5IiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBtYXJrZXItc3RhcnQ9Im5vbmUiIG1hcmtlci1lbmQ9Im5vbmUiIHN0cm9rZS1taXRlcmxpbWl0PSI3OS44NDAzMTkzNjEyNzc1Ii8+DQogICA8L2c+DQogIDwvZz4NCiA8L2c+DQo8L3N2Zz4NCg==
// @grant             GM.getValue
// @grant             GM.setValue
// @grant             GM.deleteValue
// @run-at            document-start
// @license           MIT
// ==/UserScript==

/* jshint esversion: 11 */

// --------------------------------------------------
// ---                   START                    ---
// --------------------------------------------------

function start(){

'use strict';

// Exit if page is cached
const _html = document.documentElement;
if (_html.classList.contains('od_regexplorer')) return;
_html.classList.add('od_regexplorer');

const _name = GM.info.script.name;
const _vers = GM.info.script.version;
const _icon = GM.info.script.icon || GM.info.scriptMetaStr.match(/^\/\/ @icon +(.*)$/m)[1];
const _header = document.querySelector('#header, h1');
const _table = document.querySelector('table');
const _thead = _table.querySelector('#theader, thead > tr');
const _tbody = _table.querySelector('#tbody, tbody');
const _topbar = document.createElement('div');
const _form = document.createElement('div');
const _url = window.location.href;
const _urlHash = getHash(_url);
let _arrRows;
let _order;
let _orderBy;
let _userData;

// --------------------------------------------------
// ---                    DATA                    ---
// --------------------------------------------------

async function saveData(){
  const data = {};
  if (_inpPattern.value) data.pattern = _inpPattern.value;
  if (_ckbFlagI.checked) data.flagI = 1;
  if (_inpOutput.value) data.output = _inpOutput.value;
  if (_ckbFilteredView.checked) data.filteredView = 1;
  if (_orderBy) data.orderBy = _orderBy;
  if (_order === -1) data.order = -1;

  if (Object.keys(data).length === 0) GM.deleteValue(_urlHash);
  else GM.setValue(_urlHash, JSON.stringify(data));
}
async function loadData(){
  _html.removeAttribute('style');
  const dataJSON = await GM.getValue(_urlHash);
  if (!dataJSON) return;
  const data = JSON.parse(dataJSON);

  if (data.pattern) _inpPattern.value = data.pattern;
  if (data.flagI) _ckbFlagI.checked = true;
  if (data.output) _inpOutput.value = data.output;
  if (data.filteredView) _ckbFilteredView.checked = true;
  _orderBy = data.orderBy ? +data.orderBy : 0;
  _order = data.order ? -1 : 1;

  fillOutputColumn(_inpPattern.value, _inpOutput.value);
  setFilteredView(_ckbFilteredView.checked, {skipTransition: true});

  if (_orderBy || _order === -1){
    _table.dataset.order = _order;
    sortBy(_orderBy, true);
  }
}
function getHash(s){
  const hash = Array.from(s).reduce((hash, char) => 0 | (31 * hash + char.charCodeAt(0)), 0).toString(36);
  return '_' + (hash[0] === '-' ? hash.slice(1).toUpperCase() : hash);
}

loadData();

// --------------------------------------------------
// ---                    TOP                     ---
// --------------------------------------------------

_topbar.id = 'od_topbar';
_topbar.innerHTML = `<a href="https://greasyfork.org/it/scripts/467835-file-regexplorer">${_name} v${_vers}</a>`;
document.body.appendChild(_topbar);


// --------------------------------------------------
// ---                    FORM                    ---
// --------------------------------------------------

// Pattern fields
_form.id = 'od_form';
_form.innerHTML = `
<div class="od_container_field" id="od_container_pattern">
  <label class="od_lbl" id="od_lbl_pattern">Pattern (.*): <input class="od_inp" id="od_inp_pattern" value="" spellcheck="false"></label>
  <label class="od_lbl od_lbl_top" id="od_lbl_pattern_flag_i"><input type="checkbox" class="od_ckb od_ckb_top" id="od_ckb_pattern_flag_i"> Case Insensitive</label>
</div>
<div class="od_container_field" id="od_container_output">
  <label class="od_lbl" id="od_lbl_output">Output: <input class="od_inp" id="od_inp_output" value="" spellcheck="false"></label>
  <label class="od_lbl od_lbl_top" id="od_lbl_output_filtered_view"><input type="checkbox" class="od_ckb od_ckb_top" id="od_ckb_output_filtered_view"> Filtered view</label>
</div>
`;
_header.parentNode.insertBefore(_form, _header.nextSibling);
const _inpPattern = document.getElementById('od_inp_pattern');
const _ckbFlagI = document.getElementById('od_ckb_pattern_flag_i');
const _inpOutput = document.getElementById('od_inp_output');
const _ckbFilteredView = document.getElementById('od_ckb_output_filtered_view');

// Check regex pattern validity
_inpPattern.addEventListener('input', e => {
  const me = e.target;
  const pat = me.value;
  me.setCustomValidity('');
  try {
    new RegExp(pat);
  } catch(err){
    me.setCustomValidity('Invalid regex pattern');
  }
});

// Redraw output column based on form values
function submitForm(){
  fillOutputColumn(_inpPattern.value, _inpOutput.value);
  if (_table.dataset.orderBy === '1') sortBy(1, true);
  saveData();
}

// When press a key in the fields
function onKey(e){
  const me = e.target;
  if (e.keyCode === 13){
    if (me ===_inpPattern) _inpOutput.focus();
    else me.blur();
  } else if (e.keyCode === 27){
    me.value = me.dataset.currentValue;
    me.blur();
  }
}

// When field focus
function onFocus(e){
  const me = e.target;
  me.dataset.currentValue = me.value;
}

// Form fields listeners
[_inpPattern, _inpOutput].forEach(e => {
  e.addEventListener('change', submitForm);
  e.addEventListener('focus', onFocus);
  e.addEventListener('keydown', onKey);
});
_ckbFlagI.addEventListener('change', e => {
  submitForm();
});
_ckbFilteredView.addEventListener('change', e => {
  const me = e.target;
  saveData();
  setFilteredView(me.checked);
});


// --------------------------------------------------
// ---                   TABLE                    ---
// --------------------------------------------------

(()=>{
  const th = document.createElement('th');
  th.innerText = '(.*)';
  _thead.insertBefore(th, _thead.cells[1]);

  // Clean & set column headers
  setTimeout(()=>{
    Array.from(_thead.cells).forEach(th => {
      th.innerHTML = th.innerText;
      th.tabIndex = '0';
      th.setAttribute('role', 'button');
    });
  }, 100);

  Array.from(_tbody.rows).forEach(tr => {
    const td0 = tr.cells[0];
    if ('value' in td0.dataset){
      // On Chromium, prepend 1 or 2 for the value of folders or files respectively, to keep items separate when sorting, like Firefox does
      const tdVal = td0.dataset.value;
      if (tdVal){
        td0.dataset.value = (tdVal.slice(-1) === '/')
          ? 1 + (td0.firstChild.innerHTML = tdVal.slice(0,-1))
          : 2 + tdVal
        ;
      }
    } else if (td0.hasAttribute('sortable-data')){
      // FF - pass the values to the data attribute for each td
      Array.from(tr.cells).forEach(td => {
        td.dataset.value = td.getAttribute('sortable-data');
        td.removeAttribute('sortable-data');
      });
      // FF remove the nested table with the ellipsis from the file name cell
      td0.innerHTML = td0.querySelector('a').outerHTML;
    }

    // Wrap cell content
    Array.from(tr.cells).forEach((td, index) => {
      if (td.innerHTML === '') return;
      td.innerHTML = `<div>${td.innerHTML}</div>`;
    });

    // Add output column
    let td = document.createElement('td');
    tr.insertBefore(td, tr.cells[1]);
  });

  // Shadow thead sticky
  const shadow = document.createElement('div');
  shadow.id = 'od_shadow_thead_sticky';
  document.body.insertBefore(shadow, _table);

  // Change the sort order when you press on the headers
  _thead.addEventListener('click', e => {
    e.stopPropagation();
    sortBy(e.target.cellIndex);
    saveData();
  }, true);
  _thead.addEventListener('keydown', e => {
    if (e.key == 'Enter' || e.key == ' ') {
      e.stopPropagation();
      e.preventDefault();
      sortBy(e.target.cellIndex);
      saveData();
    }
  }, true);

  // Default array rows (sorted by filename ascending)
  _arrRows = Array.from(_tbody.rows);
  sortBy(0);
  _arrRows = Array.from(_tbody.rows);
})();

function fillOutputColumn(pat, out){
  let reg;
  try {
    reg = new RegExp(pat, _ckbFlagI.checked ? 'i' : '');
  } catch(err) {
    console.error('Invalid regex pattern');
    return;
  }
  const nomatches = [];

  _arrRows.forEach((tr, index) => {
    const valName = tr.cells[0].innerText;
    const matches = valName.match(reg);
    const td = tr.cells[1];
    if (matches === null){
      nomatches[index] = true;
      td.innerHTML = '<div></div>';
      // Prepend 2 for empty values, otherwise 1, to keep items separate when sorting
      td.dataset.value = 2;
    } else {
      const text = matches[0]?.replace(reg, out || '$&') || '';
      td.innerHTML = `<div>${text}</div>`;
      td.dataset.value = 1 + text;
    }
  });
  // Apply nomatch class
  applyNomatch(nomatches);

}
function applyNomatch(nomatches){
    _arrRows.forEach((tr, index) => {
      tr.classList.toggle('od_nomatch', nomatches[index] || false);
    });
}
function sortBy(column, keepOrder = false){
  const rows = Array.from(_arrRows);
  _order = keepOrder ? +_table.dataset.order : (_orderBy == column && _table.dataset.order === '1') ? -1 : 1;
  _table.dataset.orderBy = _orderBy = column;
  _table.dataset.order = _order;
  rows.sort((rowA, rowB) => {
    let a = rowA.cells[_orderBy].dataset.value || '';
    let b = rowB.cells[_orderBy].dataset.value || '';
    return _order * a.localeCompare(b, false, {numeric: true});
  });

  _tbody.innerHTML = '';
  for (let i = 0; i < rows.length; i++){
    _tbody.appendChild(rows[i]);
  }
}
function setFilteredView(b, o){
  if (o?.skipTransition){
    _table.classList.add('od_skip_transition');
    setTimeout(()=>{_table.classList.remove('od_skip_transition');}, 0)
  }
  _table.classList.toggle('od_filtered_view', b);
}

// --------------------------------------------------
// ---                   STYLE                    ---
// --------------------------------------------------

addGlobalStyle(`
/* -------------------------------------------------- */
/* ---                   RESET                    --- */
/* -------------------------------------------------- */

html *,
html *::before,
html *::after {box-sizing: border-box}
:root {
  padding-inline: 0;
}

/* -------------------------------------------------- */
/* ---                    MAIN                    --- */
/* -------------------------------------------------- */

body {
  position: relative;
  width: auto;
  min-width: 500px;
  margin: 4em auto;
  padding: 2em 1em;
  font: 12px "Segoe UI", "DejaVu Sans", "Bitstream Vera Sans", "Lucida Grande", Verdana, Tahoma, Arial, sans-serif;
  border: 1px solid;
  border-radius: 10px;
}
#header, h1 {
  margin: 0 0 60px;
  padding: 0;
  white-space: normal;
  word-break: break-word;
  font-size: 160%;
  font-weight: normal;
  border-bottom: 1px solid;
}
#header, h1 {
  margin: 0 0 60px;
  padding: 0;
  white-space: normal;
  word-break: break-word;
  font-size: 160%;
  font-weight: normal;
  border-bottom: 1px solid;
}

/* -------------------------------------------------- */
/* ---                   TOPBAR                   --- */
/* -------------------------------------------------- */

#od_topbar {
  display: inline-flex;
  align-items: center;
  position: absolute;
  top: -30px;
  left: 0;
}
#od_topbar a {
  display: inline-flex;
  align-items: center;
  text-decoration: none;
  padding-left: 40px;
  height: 30px;
  background: 8px/24px url(${_icon}) no-repeat;
}

/* -------------------------------------------------- */
/* ---                    FORM                    --- */
/* -------------------------------------------------- */

#od_form {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-columns: 1fr;
  grid-gap: 1rem;
  margin: -50px 0 10px;
  padding: 5px 10px;
}
input.od_inp {
  width: 100%;
  margin: 5px 0 0;
  padding: .5em;
  border: 1px solid;
}
input.od_inp:invalid {
  outline: 1px solid;
}
.od_container_field {
  position: relative;
}
.od_lbl {
  user-select: none;
}
.od_lbl_top {
  display: flex;
  align-items: center;
  position: absolute;
  top: 0;
  right: 0;
  padding-left: 18px;
  margin-right: 10px;
}
.od_ckb_top {
  position: absolute;
  left: 0;
  margin: 0;
}

/* -------------------------------------------------- */
/* ---                   TABLE                    --- */
/* -------------------------------------------------- */

body > table {
  min-width: 100%;
  margin: 0 auto;
  border-collapse: separate;
  border-spacing: 0;
}
thead {
  position: sticky;
  top: 0;
  z-index: 2;
}
#od_shadow_thead_sticky {
  clear: both;
  position: sticky;
  top: 0;
  width: 100%;
  height: 2.5em;
  margin-top: -2.5em;
  pointer-events: none;
}

/* FF fix. Collapse margin after floated elements */
#UI_goUp, #UI_showHidden {
  margin-bottom: -2.5em;
  position: relative;
  z-index: 1;
}

:root.od_regexplorer > body > table > * > tr > * {
  padding-block: 4px;
  padding-inline: 8px;
}
body > table > thead > tr > th {
  position: relative;
  border-width: 1px;
  border-style: solid;
  font-size: 15px;
  font-weight: normal;
  text-align: center;
  white-space: nowrap;
  cursor: pointer;
  user-select: none;
}
body > table > tbody > tr {
  outline-offset: -1px;
}
body > table > tbody > tr > td {
  border: solid transparent;
  border-width: 0 1px;
}
body > table > tbody > tr > td {
}
body > table > thead > tr > th:first-child {
  text-align: start;
}
body > table > tbody > tr > td:nth-child(-n+2) {
  text-align: start;
  overflow-wrap: anywhere;
}
body > table > tbody > tr > td:nth-child(3) {
  text-align: end;
}
body > table > tbody > tr > td:nth-child(4) {
  text-align: center;
}
body > table > tbody > tr {
  outline: 1px solid transparent;
}
body > table > tbody > tr > td:first-child a {
  display: inline-block;
  white-space: break-spaces;
}

/* Sorting icons - column headers */
table[data-order-by="0"] > thead > tr > th:nth-child(1)::after,
table[data-order-by="1"] > thead > tr > th:nth-child(2)::after,
table[data-order-by="2"] > thead > tr > th:nth-child(3)::after,
table[data-order-by="3"] > thead > tr > th:nth-child(4)::after {
  display: inline-block;
}
table[data-order] > thead > tr > th::after {
  display: none;
  position: absolute;
  top: -4px;
  left: 0;
  right: 0;
  width: fit-content;
  margin-inline: auto;
  font-size: 9px;
  text-align: center;
  opacity: .4;
  transform: scaleX(1.5);
}
table[data-order="1"] > thead > tr > th::after {
  content: "˄";
}
table[data-order="-1"] > thead > tr > th::after {
  content: "˅";
}

/* Unmatched items */
tr.od_nomatch td:nth-child(2) {
  position: relative;
}
tr td:nth-child(2) > div:empty {
  width: 0;
  left: 50%;
  transform: translateX(-50%);
}
tr.od_nomatch td:nth-child(2) > div {
  position: absolute;
  top: 50%;
  border-bottom: 1px solid #e5683e;
  width: 100%;
}

/* Filtered view - hide rows with unmatched items */
body > table > tbody > tr > td {
  opacity: 1;
  transition: .3s .2s;
}
body > table > tbody > tr > td > div {
  overflow: hidden;
  max-width: 75vw;
  max-height: 10em;
  transition: .3s .2s, max-width .2s;
}
body > table.od_filtered_view > tbody > tr.od_nomatch > td {
  padding-block: 0;
  opacity: 0;
  transition: .5s;
}
body > table.od_filtered_view > tbody > tr.od_nomatch > td > div {
  max-width: 0;
  max-height: 0;
  transition: .3s, max-width .3s .3s;
}
body > table.od_skip_transition > tbody > tr.od_nomatch > td,
body > table.od_skip_transition > tbody > tr.od_nomatch > td > div {
  transition: none;
}

@media (min-width: 600px) {
#od_form {grid-template-columns: 3fr 2fr;}
body {
  max-width: 800px;
  min-width: fit-content;
  padding: 3em;
}
}

/* -------------------------------------------------- */
/* ---                   COLORS                   --- */
/* -------------------------------------------------- */

thead > tr                            {box-shadow: -1px 0 0, 1px 0 0;}
:root                                 {background-color: #efefef;}
body                                  {background-color: #fff; border-color: #8888;}
#od_shadow_thead_sticky               {box-shadow: 0 .3em .8em .3em #fff;}
#od_topbar a                          {color: #555;}
#od_topbar a:hover                    {color: #111;}
#od_form                              {background: #8882;}
#header, h1                           {border-bottom-color: #8889;}
input.od_inp                          {background: #fff; border-color: #888;}
input.od_inp:invalid                  {background-color: #f003; outline-color: #f33;}
thead > tr                            {background-color: #eee; color: #eee;}
thead > tr > th                       {color: #333; border-color: transparent #888a #8884 transparent;}
thead > tr > th:last-child            {border-right-color: transparent}
thead > tr > th:hover                 {background: #8883;}
body > table > tbody > tr:hover       {background: #8881; outline-color: #8889;}

@media (prefers-color-scheme: dark) {
:root                                 {background-color: #18181a;}
body                                  {background-color: #272729;}
#od_shadow_thead_sticky               {box-shadow: 0 .3em .8em .3em #272729;}
thead > tr                            {background-color: #333; color: #333;}
thead > tr > th                       {color: #eee;}
#od_topbar a                          {color: #bbb;}
#od_topbar a:hover                    {color: #eee;}
input.od_inp                          {background: #454546;}
}
`);
}

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

// --------------------------------------------------
// ---           WHEN DOCUMENT IS READY           ---
// --------------------------------------------------

if (['complete', 'interactive', 'loaded'].includes(document.readyState)){
  // Document has at least been parsed
  start();
} else {
  // Document is not ready yet, so wait for the event
  document.documentElement.style.opacity = '0';
  document.addEventListener('DOMContentLoaded', start, false);
}