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>
}
}