File Regexplorer

Per 03-06-2023. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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