Files
cairnquire/apps/server/internal/httpserver/static/editor.js

580 lines
19 KiB
JavaScript

(function () {
var form = document.querySelector("[data-editor-form]");
var textarea = document.querySelector("[data-document-editor]");
var statusNodes = document.querySelectorAll("[data-sync-status], [data-sync-status-secondary]");
var hashEl = document.querySelector("[data-editor-hash]");
var documentShell = document.querySelector("[data-document-path][data-document-hash]");
var conflictNotice = document.querySelector("[data-conflict-notice]");
var conflictHash = document.querySelector("[data-conflict-hash]");
var conflictStatus = document.querySelector("[data-conflict-status]");
var conflictDiff = document.querySelector("[data-conflict-diff]");
var conflictApply = document.querySelector("[data-conflict-apply]");
var conflictDismiss = document.querySelector("[data-conflict-dismiss]");
var conflictResolution = document.querySelector("[data-conflict-resolution]");
var conflictResolutionMount = document.querySelector("[data-conflict-monaco-mount]");
var conflictUseServer = document.querySelector("[data-conflict-use='server']");
var conflictUseLocal = document.querySelector("[data-conflict-use='local']");
var conflictUseBoth = document.querySelector("[data-conflict-use='both']");
var monacoContainer = document.querySelector("[data-monaco-mount]");
var monacoLoaded = false;
var monacoReadyPromise = null;
var monacoDarkTheme = "cairnquire-dark";
var monacoLightTheme = "cairnquire-light";
if (!form || !textarea || statusNodes.length === 0) {
return;
}
var path = form.getAttribute("data-document-path");
var baseHash = documentShell ? documentShell.getAttribute("data-document-hash") : "";
var saveTimer = null;
var lastSavedValue = textarea.value;
var editor = null;
var currentConflict = null;
var currentConflictLocalContent = "";
var resolvedConflictContent = "";
var conflictDiffView = null;
var resolutionEditor = null;
var suppressResolutionEvents = false;
function setStatus(nextStatus) {
statusNodes.forEach(function (node) {
node.textContent = nextStatus;
node.dataset.state = nextStatus.toLowerCase();
});
}
function getEditorValue() {
return editor ? editor.getValue() : textarea.value;
}
function setEditorValue(value) {
textarea.value = value;
if (editor && editor.getValue() !== value) {
editor.setValue(value);
}
}
function getResolutionValue() {
return resolutionEditor ? resolutionEditor.getValue() : resolvedConflictContent;
}
function setResolutionEditorValue(value) {
resolvedConflictContent = value;
if (conflictResolution) {
conflictResolution.value = value;
}
if (resolutionEditor && resolutionEditor.getValue() !== value) {
suppressResolutionEvents = true;
resolutionEditor.setValue(value);
suppressResolutionEvents = false;
}
}
function ensureTrailingNewline(value) {
return value.endsWith("\n") ? value : value + "\n";
}
function buildCombinedResolution(serverContent, localContent) {
return ensureTrailingNewline(serverContent) + "\n" + ensureTrailingNewline(localContent);
}
function setConflictMessage(message) {
if (conflictStatus) {
conflictStatus.textContent = message;
}
}
function setResolutionContent(content, message) {
setResolutionEditorValue(content);
if (conflictApply) {
conflictApply.disabled = false;
}
if (message) {
setConflictMessage(message);
}
}
async function loadDiffs() {
var modules = await Promise.all([
import("https://cdn.jsdelivr.net/npm/@pierre/diffs@1.1.22/+esm"),
import("https://cdn.jsdelivr.net/npm/@pierre/diffs@1.1.22/dist/style.js"),
]);
return {
renderer: modules[0],
stylesheet: modules[1].default || "",
};
}
function styleDiffContainer(diffContainer) {
diffContainer.style.setProperty("--diffs-light-bg", "#fcfaf5");
diffContainer.style.setProperty("--diffs-light", "#1c2430");
diffContainer.style.setProperty("--diffs-dark-bg", "#1e1e1e");
diffContainer.style.setProperty("--diffs-dark", "#e8ecf0");
diffContainer.style.setProperty("--diffs-addition-color", "#3f8f59");
diffContainer.style.setProperty("--diffs-deletion-color", "#c8553d");
diffContainer.style.setProperty("--diffs-modified-color", "#c67a2a");
diffContainer.style.setProperty("--diffs-font-family", "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace");
diffContainer.style.setProperty("--diffs-header-font-family", "'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif");
}
function injectDiffStyles(diffContainer, stylesheet) {
var root = diffContainer.shadowRoot;
if (!root || !stylesheet || root.querySelector("[data-cairnquire-diffs-style]")) {
return;
}
var style = document.createElement("style");
style.setAttribute("data-cairnquire-diffs-style", "");
style.textContent = stylesheet;
root.prepend(style);
}
function renderConflictResolver(conflict, localContent) {
if (!conflictDiff) {
return;
}
if (conflictDiffView) {
conflictDiffView.cleanUp();
conflictDiffView = null;
}
conflictDiff.replaceChildren();
currentConflictLocalContent = localContent;
setResolutionEditorValue(localContent);
if (conflictApply) {
conflictApply.disabled = false;
}
ensureResolutionEditor();
setConflictMessage("Loading conflict resolver...");
loadDiffs().then(function (diffsModule) {
var diffs = diffsModule.renderer;
var diffContainer = document.createElement("div");
styleDiffContainer(diffContainer);
conflictDiff.appendChild(diffContainer);
var serverFile = {
name: path,
contents: conflict.currentContent || "",
lang: "markdown",
cacheKey: "server:" + (conflict.currentHash || ""),
};
var localFile = {
name: path,
contents: localContent,
lang: "markdown",
cacheKey: "local:" + (conflict.baseHash || "") + ":" + localContent.length,
};
conflictDiffView = new diffs.FileDiff({
disableFileHeader: false,
diffStyle: "unified",
diffIndicators: "classic",
lineDiffType: "word",
overflow: "scroll",
parseDiffOptions: { context: 4 },
});
conflictDiffView.render({
oldFile: serverFile,
newFile: localFile,
fileContainer: diffContainer,
});
injectDiffStyles(diffContainer, diffsModule.stylesheet);
setConflictMessage("Red deletions are from the server copy. Green additions are from your queued edit.");
}).catch(function () {
setConflictMessage("Could not load the diff renderer. Your edit is still queued locally.");
});
}
function showConflict(conflict, localContent) {
currentConflict = conflict;
setStatus("Conflict");
form.hidden = true;
if (conflictHash && conflict && conflict.currentHash) {
conflictHash.textContent = conflict.currentHash;
}
if (conflictNotice) {
conflictNotice.hidden = false;
}
renderConflictResolver(conflict, localContent || getEditorValue());
}
function hideConflict() {
form.hidden = false;
if (conflictNotice) {
conflictNotice.hidden = true;
}
if (conflictDiff) {
conflictDiff.replaceChildren();
}
if (conflictDiffView) {
conflictDiffView.cleanUp();
conflictDiffView = null;
}
if (conflictApply) {
conflictApply.disabled = true;
}
setResolutionEditorValue("");
currentConflict = null;
currentConflictLocalContent = "";
}
async function saveNow() {
var value = getEditorValue();
if (value === lastSavedValue) {
setStatus("Saved");
return;
}
hideConflict();
setStatus("Saving");
if (!window.MDHubSync) {
setStatus("Queued");
return;
}
var result = await window.MDHubSync.saveDocument(path, value, baseHash);
if (result.status === "saved") {
lastSavedValue = value;
if (hashEl && result.result && result.result.hash) {
hashEl.textContent = result.result.hash;
baseHash = result.result.hash;
if (documentShell) {
documentShell.setAttribute("data-document-hash", result.result.hash);
}
}
setStatus("Saved");
return;
}
if (result.status === "conflict") {
showConflict(result.conflict, value);
return;
}
setStatus("Queued");
}
async function applyResolvedConflict() {
if (!window.MDHubSync || !currentConflict) {
return;
}
var resolutionContent = getResolutionValue();
resolvedConflictContent = resolutionContent;
setConflictMessage("Saving resolved document...");
if (conflictApply) {
conflictApply.disabled = true;
}
var result = await window.MDHubSync.saveDocument(path, resolutionContent, currentConflict.currentHash);
if (result.status === "saved") {
setEditorValue(resolutionContent);
lastSavedValue = resolutionContent;
if (hashEl && result.result && result.result.hash) {
hashEl.textContent = result.result.hash;
baseHash = result.result.hash;
if (documentShell) {
documentShell.setAttribute("data-document-hash", result.result.hash);
}
}
hideConflict();
setStatus("Saved");
return;
}
if (result.status === "conflict") {
showConflict(result.conflict, resolutionContent);
return;
}
setStatus("Queued");
setConflictMessage("Resolved document is queued and will retry when the server is reachable.");
}
function restorePendingConflict() {
if (!window.MDHubCache || !window.MDHubCache.getPendingEdit) {
return;
}
var normalizedPath = window.MDHubCache.normalizePath("/docs/" + path.replace(/^\/+/, ""));
window.MDHubCache.getPendingEdit(normalizedPath).then(function (pending) {
if (pending && pending.conflict) {
showConflict(pending.conflict, pending.content || getEditorValue());
}
}).catch(function () {});
}
function scheduleSave() {
setStatus("Unsaved");
if (saveTimer) {
clearTimeout(saveTimer);
}
saveTimer = setTimeout(function () {
saveNow().catch(function () {
setStatus("Queued");
});
}, 2000);
}
function loadMonaco() {
if (monacoReadyPromise) {
return monacoReadyPromise;
}
if (monacoLoaded && window.monaco) {
monacoReadyPromise = Promise.resolve();
return monacoReadyPromise;
}
monacoLoaded = true;
var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
monacoReadyPromise = new Promise(function (resolve) {
var script = document.createElement("script");
script.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js";
script.onload = function () {
require.config({
paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs" },
});
require(["vs/editor/editor.main"], function () {
monaco.editor.defineTheme(monacoDarkTheme, {
base: "vs-dark",
inherit: true,
rules: [
{ token: "comment", foreground: "8B9DAF", fontStyle: "italic" },
{ token: "keyword", foreground: "E8943A" },
{ token: "string", foreground: "C9A96E" },
{ token: "number", foreground: "C9A96E" },
{ token: "type", foreground: "E8943A" },
{ token: "tag", foreground: "E8943A" },
{ token: "attribute.name", foreground: "D4A05A" },
{ token: "attribute.value", foreground: "C9A96E" },
{ token: "delimiter", foreground: "B0B8C4" },
{ token: "variable", foreground: "D4D8DE" },
],
colors: {
"editor.background": "#1E1E1E",
"editor.foreground": "#E8ECF0",
"editor.lineHighlightBackground": "#282828",
"editor.selectionBackground": "#3A3A3A",
"editorLineNumber.foreground": "#555555",
"editorLineNumber.activeForeground": "#E8943A",
"editor.inactiveSelectionBackground": "#333333",
"editorCursor.foreground": "#E8943A",
"editorIndentGuide.background": "#2A2A2A",
"editorIndentGuide.activeBackground": "#3A3A3A",
"editorWhitespace.foreground": "#2A2A2A",
"editorGutter.background": "#1E1E1E",
"editorOverviewRuler.border": "#1E1E1E",
"scrollbarSlider.background": "#3A3A3A88",
"scrollbarSlider.hoverBackground": "#4A4A4A88",
"scrollbarSlider.activeBackground": "#5A5A5A88",
"minimap.background": "#1E1E1E",
},
});
monaco.editor.defineTheme(monacoLightTheme, {
base: "vs",
inherit: true,
rules: [
{ token: "comment", foreground: "6A7B8C", fontStyle: "italic" },
{ token: "keyword", foreground: "C67A2A" },
{ token: "string", foreground: "8A6D3B" },
{ token: "number", foreground: "8A6D3B" },
{ token: "type", foreground: "C67A2A" },
{ token: "tag", foreground: "C67A2A" },
{ token: "attribute.name", foreground: "A87D3A" },
{ token: "attribute.value", foreground: "8A6D3B" },
],
colors: {
"editor.background": "#FCFAF5",
"editor.foreground": "#1C2430",
"editor.lineHighlightBackground": "#F0EDE5",
"editor.selectionBackground": "#E8943A33",
"editorLineNumber.foreground": "#AAAAAA",
"editorLineNumber.activeForeground": "#C67A2A",
"editorCursor.foreground": "#C67A2A",
"editorIndentGuide.background": "#E8E5DD",
"editorGutter.background": "#FCFAF5",
},
});
if (monacoContainer) {
editor = monaco.editor.create(monacoContainer, {
value: textarea.value,
language: "markdown",
theme: isDark ? monacoDarkTheme : monacoLightTheme,
fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace",
fontSize: 14,
lineHeight: 22,
minimap: { enabled: false },
wordWrap: "on",
scrollBeyondLastLine: false,
padding: { top: 12 },
renderLineHighlight: "line",
smoothScrolling: true,
cursorBlinking: "smooth",
cursorSmoothCaretAnimation: "on",
bracketPairColorization: { enabled: true },
automaticLayout: true,
});
textarea.hidden = true;
monacoContainer.classList.add("monaco-mounted");
editor.onDidChangeModelContent(function () {
textarea.value = editor.getValue();
scheduleSave();
});
if (window.MDHubSync) {
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () {
saveNow().catch(function () {
setStatus("Queued");
});
});
}
var darkMQ = window.matchMedia("(prefers-color-scheme: dark)");
darkMQ.addEventListener("change", function (e) {
monaco.editor.setTheme(e.matches ? monacoDarkTheme : monacoLightTheme);
});
editor.focus();
}
resolve();
});
};
document.head.appendChild(script);
});
return monacoReadyPromise;
}
function ensureResolutionEditor() {
if (!conflictResolution || !conflictResolutionMount) {
return;
}
loadMonaco().then(function () {
if (!window.monaco || resolutionEditor) {
return;
}
var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
resolutionEditor = monaco.editor.create(conflictResolutionMount, {
value: conflictResolution.value || resolvedConflictContent,
language: "markdown",
theme: isDark ? monacoDarkTheme : monacoLightTheme,
fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace",
fontSize: 14,
lineHeight: 22,
minimap: { enabled: false },
wordWrap: "on",
scrollBeyondLastLine: false,
padding: { top: 12 },
renderLineHighlight: "line",
smoothScrolling: true,
cursorBlinking: "smooth",
cursorSmoothCaretAnimation: "on",
bracketPairColorization: { enabled: true },
automaticLayout: true,
});
conflictResolution.hidden = true;
conflictResolutionMount.classList.add("monaco-mounted");
resolutionEditor.onDidChangeModelContent(function () {
if (suppressResolutionEvents) {
return;
}
resolvedConflictContent = resolutionEditor.getValue();
conflictResolution.value = resolvedConflictContent;
if (conflictApply) {
conflictApply.disabled = false;
}
setConflictMessage("Manual resolution updated. Apply it to save the resolved document.");
});
resolutionEditor.focus();
}).catch(function () {
conflictResolution.hidden = false;
});
}
if (monacoContainer) {
loadMonaco().catch(function () {
textarea.addEventListener("input", scheduleSave);
});
} else {
textarea.addEventListener("input", scheduleSave);
}
form.addEventListener("submit", function (event) {
event.preventDefault();
saveNow().catch(function () {
setStatus("Queued");
});
});
if (conflictApply) {
conflictApply.addEventListener("click", function () {
applyResolvedConflict().catch(function () {
setStatus("Queued");
setConflictMessage("Could not save the resolved document. It remains queued locally.");
});
});
}
if (conflictDismiss) {
conflictDismiss.addEventListener("click", function () {
hideConflict();
});
}
if (conflictResolution) {
conflictResolution.addEventListener("input", function () {
resolvedConflictContent = conflictResolution.value;
if (conflictApply) {
conflictApply.disabled = false;
}
setConflictMessage("Manual resolution updated. Apply it to save the resolved document.");
});
}
if (conflictUseServer) {
conflictUseServer.addEventListener("click", function () {
if (!currentConflict) {
return;
}
setResolutionContent(currentConflict.currentContent || "", "Using the server copy as the resolution. You can edit it before applying.");
});
}
if (conflictUseLocal) {
conflictUseLocal.addEventListener("click", function () {
setResolutionContent(currentConflictLocalContent, "Using your queued edit as the resolution. You can edit it before applying.");
});
}
if (conflictUseBoth) {
conflictUseBoth.addEventListener("click", function () {
if (!currentConflict) {
return;
}
setResolutionContent(
buildCombinedResolution(currentConflict.currentContent || "", currentConflictLocalContent),
"Using both versions as the resolution. Edit the combined content before applying."
);
});
}
restorePendingConflict();
document.addEventListener("keydown", function (e) {
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
e.preventDefault();
}
});
})();