builtin-programs/editor-control.folk

When /page/ has editor code /editorCode/ & /page/ has program code /programCode/ {
    Claim $page has base64 editor code [binary encode base64 $editorCode] \
                           program code [binary encode base64 $programCode]
}

Wish the web server handles route "/editor-control" with hidden true handler {
    html {
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>Editor copy/paste</title>
        <script src="/lib/folk.js"></script>
    </head>
    <body>
        <span id="status">Status</span>
        <p>
            Select a keyboard: <select id="keyboard-select"></select>
        </p>
        <textarea id="code" cols="120" rows="40"></textarea>
        <script>
const ws = new FolkWS(document.getElementById('status'));
const keyboardSelect = document.querySelector("#keyboard-select");
const textarea = document.querySelector("#code");

var currentKeyboard = null;
var programCode = ""; // not the same as editor code
var cursorPosition = [0, 0];

// temporarily disable event processing after sending new code to prevent recursive event sends
var allowLocalEventsToProcess = true;
var allowRemoteEventsToProcess = true;
var _remoteTimoutHandle;
var _localTimeoutHandle;
function disableRemoteEventProcessing(durationMs) {
    if (_remoteTimoutHandle) clearTimeout(_remoteTimoutHandle);
    allowRemoteEventsToProcess = false;

    _remoteTimoutHandle = setTimeout(() => {
        allowRemoteEventsToProcess = true;
    }, durationMs);
}

function disableLocalEventProcessing(durationMs) {
    if (_localTimeoutHandle) clearTimeout(_localTimeoutHandle);
    allowLocalEventsToProcess = false;

    _localTimeoutHandle = setTimeout(() => {
        allowLocalEventsToProcess = true;
    }, durationMs);
}

function updateProgramCode() {
    disableRemoteEventProcessing(500);

    const { page, kbPath } = currentKeyboard;

    const currentCode = textarea.value;
    programCode = currentCode;

    const id = page + kbPath;
    ws.run(tcl`
        Hold (non-capturing) (on builtin-programs/editor.folk) ${"cursor" + kbPath} {
            Claim the ${kbPath} cursor is [list ${cursorPosition[0]} ${cursorPosition[1]}]
            Hold (on builtin-programs/editor.folk) ${"code" + kbPath} {
                Claim ${id} has program code [binary decode base64 ${btoa(currentCode)}]
                Claim ${id} has editor code [binary decode base64 ${btoa(currentCode)}]
            }
        }
    `);
}

function updateCursorAndCode(ev) {
    if (!allowLocalEventsToProcess) return;
    disableRemoteEventProcessing(500);

    const { page, kbPath } = currentKeyboard;

    const newCode = ev.target.value;

    // figure out cursor position
    const currentPosition = textarea.selectionStart;
    const linesBefore = newCode.substring(0, currentPosition).split("\n");
    const y = linesBefore.length - 1;
    const x = linesBefore[linesBefore.length - 1].length;

    cursorPosition = [x, y];

    const id = page + kbPath;
    ws.run(tcl`
        Hold (non-capturing) (on builtin-programs/editor.folk) ${"cursor" + kbPath} {
            Claim the ${kbPath} cursor is [list ${x} ${y}]
            Hold (on builtin-programs/editor.folk) ${"code" + kbPath} {
                Claim ${id} has program code [binary decode base64 ${btoa(programCode)}]
                Claim ${id} has editor code [binary decode base64 ${btoa(newCode)}]
            }
        }
    `);
}

textarea.addEventListener("input", updateCursorAndCode);
textarea.addEventListener("selectionchange", updateCursorAndCode);
textarea.addEventListener("keydown", ev => {
    if(ev.keyCode === 83 /* s */ && (navigator.platform.match("Mac") ? ev.metaKey : ev.ctrlKey)) {
        ev.preventDefault();
        updateProgramCode();
    }
});

var lastKeyboard; // to clean up the previous keyboard when another is picked
async function selectKeyboard({ page, kbPath }) {
    if (lastKeyboard) lastKeyboard.stop();

    currentKeyboard = { page, kbPath };

    const id = page + kbPath;
    lastKeyboard = await ws.watch(`${id} has base64 editor code /editorCode/ program code /programCode/ & the ${kbPath} cursor is /cursor/`, {
        add: ({ editorCode, programCode: _programCode, cursor }) => {
            if (!allowRemoteEventsToProcess) return;
            disableLocalEventProcessing(500);

            programCode = atob(_programCode);

            editorCode = atob(editorCode);
            textarea.value = editorCode;

            // figure out where the cursor is
            let [x, y] = loadList(cursor);
            x = parseInt(x); y = parseInt(y);

            cursorPosition = [x, y];

            const lines = editorCode.split("\n");

            let pos = 0;
            for (let i = 0; i < y; i++) {
                pos += lines[i].length + 1; // + 1 for newline
            }
            pos += x;

            textarea.focus();
            textarea.selectionStart = pos;
            textarea.selectionEnd = pos;
        }
    });
}

// update keyboard list as it changes
ws.watchCollected("/page/ is an editor & /page/ is a keyboard with path /kbPath/", keyboards => {
    keyboardSelect.innerHTML = "";

    for (let keyboard of keyboards) {
        let {page, kbPath} = keyboard;
        keyboardSelect.innerHTML += `<option value="${JSON.stringify(keyboard)}">${page} (${kbPath})</option>`;
    }

    if (keyboards.length === 1) {
        selectKeyboard(keyboards[0]);
    }
});

// fired when selected keyboard changes
keyboardSelect.addEventListener("input", (ev) => {
    selectKeyboard(JSON.parse(ev.target.value));
});
        </script>
    </body>
</html>
    }
}