File Regexplorer

Verze ze dne 03. 06. 2023. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name              File Regexplorer
// @version           0.1.0
// @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:///*/
// @namespace         file_regexplorer
// @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                    ---
// --------------------------------------------------
document.documentElement.style.visibility = 'hidden';

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 _patternFlagI;
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 (_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(){
  const dataJSON = await GM.getValue(_urlHash);
  document.documentElement.style.removeProperty('visibility');
  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;
  _orderBy = data.orderBy ? +data.orderBy : 0;
  _order = data.order ? -1 : 1;

  fillOutputColumn(_inpPattern.value, _inpOutput.value);
  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="">${_name} v${_vers}</a>`;
document.body.appendChild(_topbar);


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

// Pattern fields
_form.id = 'od_form';
_form.innerHTML = `
<div 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" id="od_lbl_pattern_flag_i"><input type="checkbox" class="od_ckb" id="od_ckb_pattern_flag_i"> Case Insensitive</label>
</div>
<label class="od_lbl" id="od_lbl_output">Output: <input class="od_inp" id="od_inp_output" value="" spellcheck="false"></label>
`;
_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');

// 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 => {
  const me = e.target;
  _patternFlagI = me.checked;
  submitForm();
});


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

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

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


  Array.from(_tbody.children).forEach((tr) => {
    const td0 = tr.children[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')){
      // On Firefox, pass the values to the data attribute for each td
      Array.from(tr.children).forEach((td) => {
        td.dataset.value = td.getAttribute('sortable-data');
        td.removeAttribute('sortable-data');
      });
    }
    // Add output column
    let td = document.createElement('td');
    //td.classList.add('od_nowrap')
    tr.insertBefore(td, tr.children[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
  _arrRows = Array.from(_tbody.rows);
  sortBy(0);
  _arrRows = Array.from(_tbody.rows);
})();

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

  Array.from(_tbody.children).forEach((tr, index) => {
    const valName = tr.children[0].innerText;
    const matches = valName.match(reg);
    const td = tr.children[1];
    if (matches === null){
      td.innerHTML = '<span class="od_null"></span>';
      // 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 = text;
      td.dataset.value = 1 + text;
    }
  });
}
function sortBy(column, keepOrder = false){
  const _rows = [..._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]);
  }
}

// --------------------------------------------------
// ---                   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;
  padding-left: 40px;
  height: 30px;
  background: 8px/24px url(${_icon}) no-repeat;
}
#od_topbar a {
  text-decoration: none;
}

/* -------------------------------------------------- */
/* ---                    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%;
  padding: .5em;
  border: 1px solid;
}
input.od_inp:invalid {
    outline: 1px solid;
}
#od_container_pattern {
  position: relative;
}
#od_lbl_pattern_flag_i {
  display: flex;
  align-items: center;
  position: absolute;
  top: 0;
  right: 0;
  padding-left: 18px;
  margin-right: 10px;
}
#od_ckb_pattern_flag_i {
  position: absolute;
  left: 0;
  margin: 0;
}

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

body > table {
  min-width: 100%;
  margin: 0 auto;
  border-collapse: separate;
  border-spacing: 0 1px;
}
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;
}

body > table > * > tr > * {
  padding-block: 4px !important;
  padding-inline: 8px !important;
}
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 > 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;
}
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;
}
.od_null {
  display: block;
  border-top: 1px solid #e5683e;
}
.od_nowrap {
  white-space: nowrap;
}

/* Order icon on 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: "˅";
}

/* Remove FF ellipsis functionality */
table.ellipsis {table-layout: auto;}
table.ellipsis, table.ellipsis :not(a) {display: inline-block;}
table.ellipsis a {white-space: normal;}

@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       {outline-color: #888b;}

@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.addEventListener('DOMContentLoaded', start, false);
}