about summary refs log tree commit diff stats
path: root/html
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-06-08 09:39:06 -0400
committerelioat <elioat@tilde.institute>2024-06-08 09:39:06 -0400
commit55450c3c9e1b03b4139810fb6755d151dea6f37d (patch)
tree490778daa35bf1dde36bf1f2428b4d1050ffe43a /html
parentda2446b087bc5a78a683607a2219138ae2aae44d (diff)
downloadtour-55450c3c9e1b03b4139810fb6755d151dea6f37d.tar.gz
*
Diffstat (limited to 'html')
-rw-r--r--html/yon.html669
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>