diff options
author | elioat <elioat@tilde.institute> | 2024-06-08 09:39:06 -0400 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-06-08 09:39:06 -0400 |
commit | 55450c3c9e1b03b4139810fb6755d151dea6f37d (patch) | |
tree | 490778daa35bf1dde36bf1f2428b4d1050ffe43a | |
parent | da2446b087bc5a78a683607a2219138ae2aae44d (diff) | |
download | tour-55450c3c9e1b03b4139810fb6755d151dea6f37d.tar.gz |
*
-rw-r--r-- | html/yon.html | 669 |
1 files changed, 669 insertions, 0 deletions
diff --git a/html/yon.html b/html/yon.html new file mode 100644 index 0000000..3640173 --- /dev/null +++ b/html/yon.html @@ -0,0 +1,669 @@ +<!DOCTYPE html> +<html lang="en"> +<!-- + +Forked by eli_oat circa 2024 + +MIT License + +Copyright (c) m15o + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--> +<head> + <meta charset="utf-8"> + <title>yon</title> +</head> +<style> + html, body, .column, textarea, table, td { + height: 100%; + font-size: 1.05em; + } + + body { + padding: 0; + margin: 0; + background-color: darkgoldenrod; + } + + header { + display: flex; + border-bottom: 1px solid; + border-top: 1px solid; + } + + header > * { + border-right: 1px solid; + } + + .column { + display: flex; + flex-direction: column; + min-width: 10px; + } + + section { + display: flex; + flex-direction: column; + } + + .name { + box-sizing: border-box; + width: 100%; + padding: 2px 5px; + margin: 0; + outline: none; + border: 0; + border-right: 1px solid; + background-color: lavender; + } + + #search { + display: none; + } + + #search.searching { + display: block; + position: fixed; + top: 0; + left: 0; + right: 0; + background-color: darkblue; + color: white; + } + + textarea { + box-sizing: border-box; + padding: 5px; + margin: 0; + border: 0; + outline: none; + resize: none; + background-color: bisque; + } + + .action { + padding: 2px 5px; + cursor: pointer; + } + + .back, .forward { + background-color: silver; + } + + .hist { + background-color: inherit; + } + + .folded .content { + display: none; + flex-shrink: 1; + } + + section:not(.folded) { + flex-grow: 1; + } + + table { + width: 100%; + border-collapse: collapse; + } + + td { + padding: 0; + border: 1px solid; + } + + .menu { + background-color: azure; + } + + .menu > * { + background-color: azure; + padding: 0 4px; + cursor: pointer; + } + + .menu > *:hover { + background-color: silver; + } + + .active-p .content { + background-color: white; + } + + .active { + background-color: turquoise; + } + + .col { + position: relative; + } + + .resize:hover { + background-color: rebeccapurple; + } + + .resize { + width: 2px; + background-color: tan; + cursor: col-resize; + position: absolute; + top: 0; + right: 0; + bottom: 0; + } + + #settings { + display: none; + } + + #settings.visible { + display: table-row; + } +</style> +<body> +<datalist id="files"></datalist> +<table> + <tr> + <td colspan="2" class="menu" style="height: 0;"> + <span class="new">new</span><span class="log">log</span><span class="ls">ls</span><span + class="ref">ref</span><span class="mv">mv</span><span class="del">del</span><span class="settings">settings</span> + </td> + </tr> + <tr id="settings"> + <td colspan="2" style="height: 0;"> + <span class="export">export</span> <span class="reset">reset</span> <input type="file" id="import" value=""> + </td> + </tr> + <tr> + <td class="col"> + <div class="active column"></div> + <div class="resize"></div> + </td> + <td> + <div class="column"></div> + </td> + </tr> +</table> +<input list="files" spellcheck="false" id="search" autocomplete="off"/> +<script> + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Helpers + + function $(elt) { + return document.querySelector(elt); + } + + function $$(elt) { + return document.querySelectorAll(elt); + } + + function $$active(elt) { + return $panel => ($panel && $panel.querySelector(elt)) || $('.active-p ' + elt); + } + + function $name($panel) { + return $$active('.name')($panel); + } + + function $content($panel) { + return $$active('.content')($panel); + } + + function t$(e) { + return e.target; + } + + function closest$(e, sel) { + return t$(e).closest(sel); + } + + function $search() { + return $('#search'); + } + + function $panel() { + return $('.active-p'); + } + + function hasCls($elt, cls) { + return $elt.classList.contains(cls); + } + + function date() { + return (new Date()).toLocaleDateString('en-CA'); + } + + function makeElt(elt, cls, attrs, html, value) { + let e = document.createElement(elt); + cls.forEach(c => e.classList.add(c)); + Object.keys(attrs).forEach(k => e.setAttribute(k, attrs[k])); + html && (e.innerHTML = html); + value && (e.value = value); + return e; + } + + function togCls(cls, sel, $elt) { + $$(sel).forEach((e) => e.classList.remove(cls)); + $elt.classList.add(cls); + } + + function setActive($section) { + togCls('active-p', 'section', $section); + } + + function closeSearch() { + $search().classList.remove('searching'); + $content().focus(); + } + + function refPane(name) { + let p; + $$('section').forEach(e => e.querySelector('.name').value.startsWith('+ref') && (p = e)); + if (!p) { + p = createPane(); + } + setActive(p); + load(name); + return p; + } + + function isChordNav(e) { + return e.metaKey || e.ctrlKey; + } + + function isChordNavBlank(e) { + return isChordNav(e) && e.altKey; + } + + function navBlank(name) { + createPane(); + load(name); + } + + function debounce(fn, ms) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => fn(...args), ms); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Core + + function save() { + let name = $name().value; + let content = $content().value; + if (!name || name[0] === '+') return; + localStorage.setItem(name, content); + refresh(); + } + + const saveD = debounce(save, 200); + + function updateHistory(name) { + let cname = $panel().dataset['name']; + if (!name || name === cname) return; + hpush(hist().back, cname); + hist().forward = []; + } + + function ls() { + return Object.keys(localStorage).map(k => '[[' + k + ']]').join('\n'); + } + + function load(name, noHist) { + if (!name) { + $name().value = ''; + $content().value = ''; + return; + } + !noHist && updateHistory(name) + if (name === '+ls') { + $content().value = ls(); + } else if (name.startsWith('+ref')) { + $content().value = ref(name.split(':')[1]); + } else { + $content().value = localStorage.getItem(name); + } + $name().value = name; + $panel().dataset['name'] = name; + $panel().querySelector('.back').classList.toggle('hist', !!hist().back.length); + $panel().querySelector('.forward').classList.toggle('hist', !!hist().forward.length); + } + + function refresh() { + const $elt = $('#files'); + $elt.innerHTML = ''; + Object.keys(localStorage).forEach(k => $elt.appendChild(makeElt('option', [], {}, null, k))); + } + + let paneID = 0; + + function createHistory(paneID) { + history[paneID] = {back: [], forward: []}; + } + + function removeHistory(paneID) { + delete history[paneID]; + } + + function deletePane($pane) { + removeHistory($pane.dataset['id']); + $pane.remove(); + } + + function createPane() { + const header = document.createElement('header'); + header.append( + makeElt('span', ['back', 'action'], {}, '<'), + makeElt('span', ['forward', 'action'], {}, '>'), + makeElt('input', ['name'], {list: 'files', autocomplete: 'off'}), + makeElt('span', ['fold', 'action'], {}, '-'), + makeElt('span', ['max', 'action'], {}, '+'), + makeElt('span', ['move', 'action'], {}, '~'), + makeElt('span', ['close', 'action'], {}, 'x') + ); + const id = paneID++; + const section = document.createElement('section'); + section.setAttribute('data-id', '' + id); + section.append( + header, + makeElt('textarea', ['content'], {spellcheck: false, onkeyup: 'saveD()'}) + ) + createHistory(id); + $(".column.active").appendChild(section); + setActive(section); + $content().focus(); + return section; + } + + function ref(str) { + return Object.keys(localStorage).map(k => { + const v = localStorage.getItem(k); + const m = v.split('\n').filter(l => l.includes('[[' + str + ']]')); + if (!m.length) return null; + return ['[[' + k + ']]', '----------', ...m, ''].join('\n'); + }).filter(Boolean).join('\n'); + } + + function mv(before, after) { + return Object.keys(localStorage).forEach(k => { + const v = localStorage.getItem(k); + localStorage.setItem(k, v.replaceAll('[[' + before + ']]', '[[' + after + ']]')) + }) + } + + function link(textarea) { + const text = textarea.value; + const pos = textarea.selectionStart; + let start, end; + + for (start = pos; start > -1 && text[start] !== '['; start--) { + if (/\n/.test(text[start]) || text[start] === ']') return null; + } + if (start === -1) return null; + + for (end = pos; end < text.length && text[end] !== ']'; end++) { + if (/\n/.test(text[end]) || text[end] === '[') return null; + } + if (end === text.length) return null; + + return text.substring(start + 1, end); + } + + function insert(textarea, text) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const before = textarea.value.substring(0, start); + const after = textarea.value.substring(end, textarea.value.length); + + textarea.value = before + text + after; + textarea.selectionStart = textarea.selectionEnd = start + text.length; + } + + const history = {}; + + function hist() { + return history[$panel().dataset['id']]; + } + + function hpush(h, name) { + if (h[h.length - 1] === name) return; + h.push(name); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Menu + + function menuNew() { + createPane(); + } + + function menuLog() { + $name() || createPane(); + load(date()); + } + + function menuLs() { + load('+ls'); + } + + function menuReset() { + if (confirm('delete everything?')) { + localStorage.clear(); + location.reload(); + } + } + + function menuRef() { + refPane('+ref:' + $name().value); + } + + function menuMv() { + let prev = $panel().dataset['name']; + mv(prev, $name().value); + save(); + localStorage.removeItem(prev); + $prevPanel = $panel(); + $$('section').forEach($pane => { + setActive($pane); + load($name().value, false); + }) + setActive($prevPanel); + refresh(); + } + + function menuExport() { + const blob = new Blob([JSON.stringify(localStorage)], {type: 'application/json'}); + const link = document.createElement('a'); + link.download = 'yon-export.json'; + link.href = window.URL.createObjectURL(blob); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + function menuDel() { + let name = $name().value; + if (name && confirm('delete ' + name + '?')) { + localStorage.removeItem(name); + refresh(); + deletePane($panel()); + } + } + + function menuSettings() { + $('#settings').classList.toggle('visible'); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Pane actions + + function paneFold(e) { + closest$(e, 'section').classList.toggle('folded'); + } + + function paneMax(e) { + closest$(e, '.column').querySelectorAll("section").forEach(e => e.classList.add("folded")); + closest$(e, 'section').classList.remove("folded"); + } + + function paneMove(e) { + let $c; + $$('.column').forEach(c => { + if (c !== closest$(e, '.column')) $c = c; + }); + $c.appendChild(closest$(e, 'section')); + } + + function paneClose(e) { + deletePane(closest$(e, 'section')); + } + + function paneHist(from, to) { + return function (e) { + if (!from.length) return; + let name = from.pop(); + if (isChordNavBlank(e)) { + from.push(name); + createPane(); + } else if (isChordNav(e)) { + setActive($prevPanel); + load(name); + $content().focus(); + } else { + hpush(to, $panel().dataset['name']); + } + load(name, true); + } + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Listeners + + let $prevPanel; + let $resize; + + document.addEventListener('mousedown', function (e) { + if (hasCls(t$(e), 'resize')) { + $resize = closest$(e, 'td'); + } else { + $prevPanel = $panel() || $prevPanel; + let section = closest$(e, 'section'); + section && setActive(section); + let column = closest$(e, '.column'); + if (column && (!e.altKey && !e.metaKey && !e.ctrlKey)) { + togCls('active', '.column', column); + } + } + }); + + document.addEventListener('mousemove', function (e) { + if (!$resize) return; + $resize.width = e.clientX; + }) + + document.addEventListener('mouseup', () => $resize = null); + + document.addEventListener('click', function (e) { + // @formatter:off + switch (t$(e).classList[0]) { + case 'new': menuNew(); return; + case 'log': menuLog(); return; + case 'ls': menuLs(); return; + case 'reset': menuReset(); return; + case 'export': menuExport(); return; + case 'ref': menuRef(); return; + case 'mv': menuMv(); return; + case 'del': menuDel(); return; + case 'settings': menuSettings(); return; + case 'close': paneClose(e); return; + case 'fold': paneFold(e); return; + case 'max': paneMax(e); return; + case 'move': paneMove(e); return; + case 'back': paneHist(hist().back, hist().forward)(e); return; + case 'forward': paneHist(hist().forward, hist().back)(e); return; + } + // @formatter:on + if (isChordNavBlank(e) && e.button === 0) { + link($content()) && navBlank(link($content())); + } else if (isChordNav(e) && e.button === 0) { + let name = link($content()); + if (name) { + setActive($prevPanel); + load(name); + $content().focus(); + } + } + }); + + function handleSearch(e) { + e.preventDefault(); + e.stopPropagation(); + if (isChordNavBlank(e)) { + navBlank(t$(e).value); + closeSearch(); + } else if (isChordNav(e)) { + if (!$content()) createPane(); + closeSearch(); + load(t$(e).value); + } else { + if (!$content()) return; + closeSearch(); + insert($content(), "[[" + t$(e).value + "]]"); + } + } + + document.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + if (t$(e).id === 'search') { + handleSearch(e); + } else if (isChordNav(e)) { + $search().value = ''; + $search().classList.add('searching'); + $search().focus(); + } else if (hasCls(e.target, 'name')) { + load($name().value); + } + } else if (e.key === 'Escape' && e.target.id === 'search') { + $search().classList.remove('searching'); + $content().focus(); + } + }); + + document.getElementById('import').addEventListener('change', function (e) { + const file = e.target.files[0]; + const r = new FileReader(); + r.onload = e => { + let data = JSON.parse('' + e.target.result); + Object.keys(data).forEach(k => localStorage.setItem(k, data[k])); + } + r.readAsText(file); + refresh(); + }); + + refresh(); + createPane(); + load(); +</script> +</body> +</html> |