codemirror

This commit is contained in:
2026-06-12 12:12:49 -04:00
parent c39c50b9a0
commit 0adb980cf7
7 changed files with 869 additions and 236 deletions

File diff suppressed because one or more lines are too long

View File

@@ -11,15 +11,13 @@
var conflictApply = document.querySelector("[data-conflict-apply]"); var conflictApply = document.querySelector("[data-conflict-apply]");
var conflictDismiss = document.querySelector("[data-conflict-dismiss]"); var conflictDismiss = document.querySelector("[data-conflict-dismiss]");
var conflictResolution = document.querySelector("[data-conflict-resolution]"); var conflictResolution = document.querySelector("[data-conflict-resolution]");
var conflictResolutionMount = document.querySelector("[data-conflict-monaco-mount]"); var conflictResolutionMount = document.querySelector("[data-conflict-codemirror-mount]");
var conflictUseServer = document.querySelector("[data-conflict-use='server']"); var conflictUseServer = document.querySelector("[data-conflict-use='server']");
var conflictUseLocal = document.querySelector("[data-conflict-use='local']"); var conflictUseLocal = document.querySelector("[data-conflict-use='local']");
var conflictUseBoth = document.querySelector("[data-conflict-use='both']"); var conflictUseBoth = document.querySelector("[data-conflict-use='both']");
var monacoContainer = document.querySelector("[data-monaco-mount]"); var codeMirrorContainer = document.querySelector("[data-codemirror-mount]");
var monacoLoaded = false; var codeMirrorReadyPromise = null;
var monacoReadyPromise = null; var codeMirrorModules = null;
var monacoDarkTheme = "cairnquire-dark";
var monacoLightTheme = "cairnquire-light";
if (!form || !textarea || statusNodes.length === 0) { if (!form || !textarea || statusNodes.length === 0) {
return; return;
@@ -36,6 +34,9 @@
var conflictDiffView = null; var conflictDiffView = null;
var resolutionEditor = null; var resolutionEditor = null;
var suppressResolutionEvents = false; var suppressResolutionEvents = false;
var doneLink = document.querySelector("[data-done-link]");
var isSaving = false;
var suppressBeforeUnload = false;
function setStatus(nextStatus) { function setStatus(nextStatus) {
statusNodes.forEach(function (node) { statusNodes.forEach(function (node) {
@@ -223,7 +224,7 @@
var value = getEditorValue(); var value = getEditorValue();
if (value === lastSavedValue) { if (value === lastSavedValue) {
setStatus("Saved"); setStatus("Saved");
return; return "unchanged";
} }
isSaving = true; isSaving = true;
@@ -233,7 +234,7 @@
if (!window.MDHubSync) { if (!window.MDHubSync) {
isSaving = false; isSaving = false;
setStatus("Queued"); setStatus("Queued");
return; return "queued";
} }
try { try {
@@ -249,16 +250,17 @@
} }
setStatus("Saved"); setStatus("Saved");
isSaving = false; isSaving = false;
return; return "saved";
} }
if (result.status === "conflict") { if (result.status === "conflict") {
showConflict(result.conflict, value); showConflict(result.conflict, value);
isSaving = false; isSaving = false;
return; return "conflict";
} }
setStatus("Queued"); setStatus("Queued");
return "queued";
} catch (err) { } catch (err) {
setStatus("Queued"); setStatus("Queued");
throw err; throw err;
@@ -328,138 +330,236 @@
}, 2000); }, 2000);
} }
function loadMonaco() { function doneHref() {
if (monacoReadyPromise) { return doneLink ? doneLink.getAttribute("href") : "/docs/" + path.replace(/\.md$/, "");
return monacoReadyPromise; }
}
if (monacoLoaded && window.monaco) {
monacoReadyPromise = Promise.resolve();
return monacoReadyPromise;
}
monacoLoaded = true;
function clearScheduledSave() {
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
}
function leaveEditorNow() {
clearScheduledSave();
suppressBeforeUnload = true;
window.location.href = doneHref();
}
function saveThenLeaveEditor() {
var href = doneHref();
if (currentConflict) {
setStatus("Conflict");
if (conflictNotice) {
conflictNotice.scrollIntoView({ behavior: "smooth", block: "center" });
}
return;
}
clearScheduledSave();
if (isSaving) {
waitForSaveThenNavigate(href);
return;
}
setStatus("Saving");
saveNow()
.then(function (result) {
if (result === "saved" || result === "unchanged") {
suppressBeforeUnload = true;
window.location.href = href;
return;
}
if (result === "conflict") {
setStatus("Conflict");
return;
}
setStatus("Queued");
if (window.confirm("Could not save. Leave without saving?")) {
suppressBeforeUnload = true;
window.location.href = href;
}
})
.catch(function () {
setStatus("Queued");
if (window.confirm("Could not save. Leave without saving?")) {
suppressBeforeUnload = true;
window.location.href = href;
}
});
}
function loadCodeMirror() {
if (codeMirrorReadyPromise) {
return codeMirrorReadyPromise;
}
codeMirrorReadyPromise = Promise.all([
import("/static/codemirror.bundle.js"),
]).then(function (modules) {
var bundle = modules[0];
codeMirrorModules = {
cm: bundle,
markdown: bundle,
vim: bundle,
};
registerVimCommands();
return codeMirrorModules;
});
return codeMirrorReadyPromise;
}
function registerVimCommands() {
var Vim = codeMirrorModules && codeMirrorModules.vim && codeMirrorModules.vim.Vim;
if (!Vim) {
return;
}
Vim.defineEx("write", "w", function () {
saveNow().catch(function () {
setStatus("Queued");
});
});
Vim.defineEx("quit", "q", function () {
leaveEditorNow();
});
Vim.defineEx("wq", "wq", function () {
saveThenLeaveEditor();
});
Vim.defineEx("xit", "x", function () {
saveThenLeaveEditor();
});
Vim.map("<Esc>", ":q<CR>");
}
function codeMirrorTheme() {
var EditorView = codeMirrorModules.cm.EditorView;
var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
monacoReadyPromise = new Promise(function (resolve) { return EditorView.theme({
var script = document.createElement("script"); "&": {
script.src = "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs/loader.js"; height: "100%",
script.onload = function () { minHeight: "60vh",
require.config({ backgroundColor: "var(--panel)",
paths: { vs: "https://cdn.jsdelivr.net/npm/monaco-editor@0.52.2/min/vs" }, color: "var(--text)",
}); fontSize: "14px",
require(["vs/editor/editor.main"], function () { },
monaco.editor.defineTheme(monacoDarkTheme, { "&.cm-focused": {
base: "vs-dark", outline: "none",
inherit: true, },
rules: [ ".cm-scroller": {
{ token: "comment", foreground: "8B9DAF", fontStyle: "italic" }, fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace",
{ token: "keyword", foreground: "E8943A" }, lineHeight: "22px",
{ token: "string", foreground: "C9A96E" }, },
{ token: "number", foreground: "C9A96E" }, ".cm-content": {
{ token: "type", foreground: "E8943A" }, padding: "12px 0",
{ token: "tag", foreground: "E8943A" }, caretColor: "var(--accent)",
{ token: "attribute.name", foreground: "D4A05A" }, },
{ token: "attribute.value", foreground: "C9A96E" }, ".cm-line": {
{ token: "delimiter", foreground: "B0B8C4" }, padding: "0 14px",
{ token: "variable", foreground: "D4D8DE" }, },
], ".cm-gutters": {
colors: { backgroundColor: "var(--panel-strong)",
"editor.background": "#1E1E1E", borderRight: "1px solid var(--border)",
"editor.foreground": "#E8ECF0", color: "var(--muted)",
"editor.lineHighlightBackground": "#282828", },
"editor.selectionBackground": "#3A3A3A", ".cm-activeLine, .cm-activeLineGutter": {
"editorLineNumber.foreground": "#555555", backgroundColor: isDark ? "oklch(0.24 0.012 55)" : "#f0ede5",
"editorLineNumber.activeForeground": "#E8943A", },
"editor.inactiveSelectionBackground": "#333333", ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": {
"editorCursor.foreground": "#E8943A", backgroundColor: "color-mix(in srgb, var(--accent) 22%, transparent)",
"editorIndentGuide.background": "#2A2A2A", },
"editorIndentGuide.activeBackground": "#3A3A3A", ".cm-cursor": {
"editorWhitespace.foreground": "#2A2A2A", borderLeftColor: "var(--accent)",
"editorGutter.background": "#1E1E1E", },
"editorOverviewRuler.border": "#1E1E1E", ".cm-panel": {
"scrollbarSlider.background": "#3A3A3A88", backgroundColor: "var(--panel-strong)",
"scrollbarSlider.hoverBackground": "#4A4A4A88", borderTop: "1px solid var(--border)",
"scrollbarSlider.activeBackground": "#5A5A5A88", color: "var(--text)",
"minimap.background": "#1E1E1E", fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace",
}, },
}); ".cm-vim-panel input": {
color: "var(--text)",
},
}, { dark: isDark });
}
monaco.editor.defineTheme(monacoLightTheme, { function codeMirrorHighlighting() {
base: "vs", var cm = codeMirrorModules.cm;
inherit: true, var tags = codeMirrorModules.cm.tags;
rules: [ var urlColor = window.matchMedia("(prefers-color-scheme: dark)").matches
{ token: "comment", foreground: "6A7B8C", fontStyle: "italic" }, ? "oklch(0.78 0.14 190)"
{ token: "keyword", foreground: "C67A2A" }, : "#047c78";
{ 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) { return cm.syntaxHighlighting(cm.HighlightStyle.define([
editor = monaco.editor.create(monacoContainer, { {
value: textarea.value, tag: [tags.url, tags.link],
language: "markdown", color: urlColor,
theme: isDark ? monacoDarkTheme : monacoLightTheme, textDecoration: "underline",
fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace", textUnderlineOffset: "2px",
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; function createCodeMirrorEditor(mount, initialValue, options) {
monacoContainer.classList.add("monaco-mounted"); var cm = codeMirrorModules.cm;
var markdownModule = codeMirrorModules.markdown;
var vimModule = codeMirrorModules.vim;
var onChange = options && options.onChange ? options.onChange : function () {};
var extensions = [
vimModule.vim({ status: true }),
cm.basicSetup,
markdownModule.markdown({
base: markdownModule.markdownLanguage,
}),
cm.EditorView.lineWrapping,
codeMirrorTheme(),
codeMirrorHighlighting(),
cm.EditorView.updateListener.of(function (update) {
if (update.docChanged) {
onChange(update.state.doc.toString());
}
}),
];
editor.onDidChangeModelContent(function () { mount.replaceChildren();
textarea.value = editor.getValue(); mount.classList.add("codemirror-mounted");
scheduleSave();
});
if (window.MDHubSync) { var view = new cm.EditorView({
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, function () { doc: initialValue,
saveNow().catch(function () { extensions: extensions,
setStatus("Queued"); parent: mount,
});
});
}
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;
return {
view: view,
getValue: function () {
return view.state.doc.toString();
},
setValue: function (value) {
if (view.state.doc.toString() === value) {
return;
}
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: value },
});
},
focus: function () {
view.focus();
},
hasTextFocus: function () {
return view.hasFocus;
},
destroy: function () {
view.destroy();
},
};
} }
function ensureResolutionEditor() { function ensureResolutionEditor() {
@@ -467,54 +567,42 @@
return; return;
} }
loadMonaco().then(function () { loadCodeMirror().then(function () {
if (!window.monaco || resolutionEditor) { if (resolutionEditor) {
return; return;
} }
var isDark = window.matchMedia("(prefers-color-scheme: dark)").matches; resolutionEditor = createCodeMirrorEditor(conflictResolutionMount, conflictResolution.value || resolvedConflictContent, {
resolutionEditor = monaco.editor.create(conflictResolutionMount, { onChange: function (value) {
value: conflictResolution.value || resolvedConflictContent, if (suppressResolutionEvents) {
language: "markdown", return;
theme: isDark ? monacoDarkTheme : monacoLightTheme, }
fontFamily: "'Iosevka', 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace", resolvedConflictContent = value;
fontSize: 14, conflictResolution.value = value;
lineHeight: 22, if (conflictApply) {
minimap: { enabled: false }, conflictApply.disabled = false;
wordWrap: "on", }
scrollBeyondLastLine: false, setConflictMessage("Manual resolution updated. Apply it to save the resolved document.");
padding: { top: 12 }, },
renderLineHighlight: "line",
smoothScrolling: true,
cursorBlinking: "smooth",
cursorSmoothCaretAnimation: "on",
bracketPairColorization: { enabled: true },
automaticLayout: true,
}); });
conflictResolution.hidden = 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(); resolutionEditor.focus();
}).catch(function () { }).catch(function () {
conflictResolution.hidden = false; conflictResolution.hidden = false;
}); });
} }
if (monacoContainer) { if (codeMirrorContainer) {
loadMonaco().catch(function () { loadCodeMirror().then(function () {
editor = createCodeMirrorEditor(codeMirrorContainer, textarea.value, {
onChange: function (value) {
textarea.value = value;
scheduleSave();
},
});
textarea.hidden = true;
editor.focus();
}).catch(function () {
textarea.addEventListener("input", scheduleSave); textarea.addEventListener("input", scheduleSave);
}); });
} else { } else {
@@ -582,59 +670,10 @@
restorePendingConflict(); restorePendingConflict();
var doneLink = document.querySelector("[data-done-link]");
var isSaving = false;
if (doneLink) { if (doneLink) {
doneLink.addEventListener("click", function (event) { doneLink.addEventListener("click", function (event) {
var currentValue = getEditorValue();
var href = doneLink.getAttribute("href");
// If there's an unresolved conflict, warn the user
if (currentConflict) {
event.preventDefault();
setStatus("Conflict");
if (conflictNotice) {
conflictNotice.scrollIntoView({ behavior: "smooth", block: "center" });
}
return;
}
// If nothing changed, allow normal navigation
if (currentValue === lastSavedValue) {
return;
}
// Unsaved changes — cancel any pending timer and save immediately
event.preventDefault(); event.preventDefault();
saveThenLeaveEditor();
if (saveTimer) {
clearTimeout(saveTimer);
saveTimer = null;
}
if (isSaving) {
// Already saving, just wait and then navigate
waitForSaveThenNavigate(href);
return;
}
isSaving = true;
setStatus("Saving");
saveNow()
.then(function () {
isSaving = false;
window.location.href = href;
})
.catch(function () {
isSaving = false;
setStatus("Queued");
// If save fails, ask user if they want to leave anyway
if (window.confirm("Could not save. Leave without saving?")) {
window.location.href = href;
}
});
}); });
} }
@@ -642,6 +681,14 @@
var checkInterval = setInterval(function () { var checkInterval = setInterval(function () {
if (!isSaving) { if (!isSaving) {
clearInterval(checkInterval); clearInterval(checkInterval);
if (currentConflict) {
setStatus("Conflict");
if (conflictNotice) {
conflictNotice.scrollIntoView({ behavior: "smooth", block: "center" });
}
return;
}
suppressBeforeUnload = true;
window.location.href = href; window.location.href = href;
} }
}, 100); }, 100);
@@ -653,15 +700,42 @@
// Warn on browser unload if there are unsaved changes // Warn on browser unload if there are unsaved changes
window.addEventListener("beforeunload", function (e) { window.addEventListener("beforeunload", function (e) {
if (suppressBeforeUnload) {
return;
}
if (getEditorValue() !== lastSavedValue || currentConflict) { if (getEditorValue() !== lastSavedValue || currentConflict) {
e.preventDefault(); e.preventDefault();
e.returnValue = ""; e.returnValue = "";
} }
}); });
function editorSurfaceHasFocus(target) {
if (!target) {
return false;
}
if (target === textarea || target.closest("[data-codemirror-mount], [data-conflict-codemirror-mount], [data-document-editor], .cm-editor")) {
return true;
}
return Boolean(editor && editor.hasTextFocus && editor.hasTextFocus());
}
document.addEventListener("keydown", function (e) { document.addEventListener("keydown", function (e) {
if ((e.metaKey || e.ctrlKey) && e.key === "s") { if ((e.metaKey || e.ctrlKey) && e.key === "s") {
e.preventDefault(); e.preventDefault();
saveNow().catch(function () {
setStatus("Queued");
});
return;
} }
});
if (e.metaKey && e.key === "Enter") {
e.preventDefault();
saveThenLeaveEditor();
return;
}
if (!editorSurfaceHasFocus(e.target)) {
return;
}
}, true);
})(); })();

View File

@@ -0,0 +1,471 @@
(function () {
"use strict";
if (document.querySelector("[data-editor-form]")) {
return;
}
var browser = document.querySelector(".miller-browser");
if (!browser) {
return;
}
var documentShell = document.querySelector(".document-shell[data-document-path]");
var markdownBody = document.querySelector(".markdown-body");
var mode = "browser";
var selectedColumnIndex = 0;
var selectedLink = null;
var selectedParagraphIndex = -1;
function getColumns() {
return Array.prototype.slice.call(browser.querySelectorAll(".miller-column"));
}
function getColumnLinks(columnIndex) {
var columns = getColumns();
if (!columns[columnIndex]) {
return [];
}
return Array.prototype.slice.call(columns[columnIndex].querySelectorAll("a[href]"));
}
function getParagraphTargets() {
if (!markdownBody) {
return [];
}
return Array.prototype.slice.call(
markdownBody.querySelectorAll(".commentable, p, h1, h2, h3, h4, h5, h6, li, blockquote, pre")
).filter(function (el) {
return (el.textContent || "").trim().length > 0;
});
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
function isEditableTarget(target) {
if (!target) {
return false;
}
return Boolean(target.closest(
"input, textarea, select, [contenteditable='true'], [contenteditable=''], [role='textbox'], .monaco-editor"
));
}
function isKeyboardNavigationTarget(target) {
if (!target) {
return false;
}
return Boolean(target.closest(".miller-browser a[href], .markdown-body .is-keyboard-selected"));
}
function shouldIgnoreTarget(target) {
if (isEditableTarget(target)) {
return true;
}
if (isKeyboardNavigationTarget(target)) {
return false;
}
return Boolean(target && target.closest("a[href], button, summary, details, [role='button'], [role='menuitem']"));
}
function showKeyboardUI() {
document.body.classList.add("keyboard-navigation-active");
}
function clearBrowserSelection() {
browser.querySelectorAll(".is-keyboard-selected").forEach(function (node) {
node.classList.remove("is-keyboard-selected");
node.removeAttribute("aria-selected");
});
}
function clearParagraphSelection() {
getParagraphTargets().forEach(function (node) {
node.classList.remove("is-keyboard-selected");
node.removeAttribute("tabindex");
node.removeAttribute("aria-selected");
});
}
function scrollBrowserColumnIntoView(column) {
if (!column) {
return;
}
var leftEdge = column.offsetLeft;
var rightEdge = leftEdge + column.offsetWidth;
var targetLeft = browser.scrollLeft;
if (leftEdge < browser.scrollLeft) {
targetLeft = leftEdge - 8;
} else if (rightEdge > browser.scrollLeft + browser.clientWidth) {
targetLeft = rightEdge - browser.clientWidth + 8;
}
if (targetLeft !== browser.scrollLeft) {
browser.scrollTo({ left: Math.max(0, targetLeft), behavior: "smooth" });
}
}
function expandColumnForKeyboard(column) {
getColumns().forEach(function (candidate) {
if (candidate !== column) {
candidate.classList.remove("is-preview-expanded");
candidate.style.removeProperty("--expanded-column-width");
}
});
if (!column || !column.matches(".miller-column:not(:first-child):not(:last-child)")) {
return;
}
var expandedWidth = Math.min(Math.max(column.scrollWidth + 12, 192), 384);
column.style.setProperty("--expanded-column-width", expandedWidth + "px");
column.classList.add("is-preview-expanded");
}
function updateBrowserSelection(focusSelected) {
clearBrowserSelection();
var columns = getColumns();
var column = columns[selectedColumnIndex];
if (!column) {
return;
}
expandColumnForKeyboard(column);
scrollBrowserColumnIntoView(column);
if (!selectedLink || !column.contains(selectedLink)) {
selectedLink = column.querySelector("a.is-active[href]") || column.querySelector("a[href]");
}
if (!selectedLink) {
return;
}
selectedLink.classList.add("is-keyboard-selected");
selectedLink.setAttribute("aria-selected", "true");
if (focusSelected) {
selectedLink.focus({ preventScroll: true });
}
}
function selectBrowserColumn(columnIndex, focusSelected) {
var columns = getColumns();
if (!columns.length) {
return;
}
mode = "browser";
selectedColumnIndex = clamp(columnIndex, 0, columns.length - 1);
var links = getColumnLinks(selectedColumnIndex);
selectedLink = links.find(function (link) {
return link.classList.contains("is-active");
}) || links[0] || null;
clearParagraphSelection();
updateBrowserSelection(focusSelected);
}
function moveBrowserVertical(delta) {
var links = getColumnLinks(selectedColumnIndex);
if (!links.length) {
return;
}
var currentIndex = links.indexOf(selectedLink);
if (currentIndex < 0) {
currentIndex = links.findIndex(function (link) {
return link.classList.contains("is-active");
});
}
if (currentIndex < 0) {
currentIndex = 0;
}
selectedLink = links[clamp(currentIndex + delta, 0, links.length - 1)];
updateBrowserSelection(true);
}
function navigateTo(link) {
if (!link || !link.href) {
return;
}
window.location.href = link.href;
}
function moveBrowserRight() {
var columns = getColumns();
if (!columns.length) {
return;
}
if (selectedColumnIndex < columns.length - 1 && selectedLink && selectedLink.classList.contains("is-active")) {
selectBrowserColumn(selectedColumnIndex + 1, true);
return;
}
if (selectedColumnIndex < columns.length - 1 && !selectedLink) {
selectBrowserColumn(selectedColumnIndex + 1, true);
return;
}
if (selectedLink && !selectedLink.classList.contains("is-active")) {
navigateTo(selectedLink);
return;
}
if (markdownBody && getParagraphTargets().length) {
enterParagraphMode(0);
} else if (selectedLink) {
navigateTo(selectedLink);
}
}
function nearestParagraphIndex() {
var targets = getParagraphTargets();
if (!targets.length) {
return -1;
}
if (selectedParagraphIndex >= 0 && selectedParagraphIndex < targets.length) {
return selectedParagraphIndex;
}
var shellRect = documentShell ? documentShell.getBoundingClientRect() : null;
var targetY = shellRect ? shellRect.top + shellRect.height * 0.35 : window.innerHeight * 0.35;
var bestIndex = 0;
var bestDistance = Number.POSITIVE_INFINITY;
targets.forEach(function (el, index) {
var distance = Math.abs(el.getBoundingClientRect().top - targetY);
if (distance < bestDistance) {
bestDistance = distance;
bestIndex = index;
}
});
return bestIndex;
}
function selectParagraph(index) {
var targets = getParagraphTargets();
if (!targets.length) {
return;
}
clearBrowserSelection();
clearParagraphSelection();
mode = "paragraph";
selectedParagraphIndex = clamp(index, 0, targets.length - 1);
var target = targets[selectedParagraphIndex];
target.classList.add("is-keyboard-selected");
target.setAttribute("aria-selected", "true");
target.setAttribute("tabindex", "-1");
target.focus({ preventScroll: true });
target.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
function enterParagraphMode(offset) {
var index = nearestParagraphIndex();
if (index < 0) {
return;
}
selectParagraph(index + offset);
}
function moveParagraphVertical(delta) {
var targets = getParagraphTargets();
if (!targets.length) {
return;
}
var currentIndex = selectedParagraphIndex;
if (currentIndex < 0 || currentIndex >= targets.length) {
currentIndex = nearestParagraphIndex();
}
selectParagraph(currentIndex + delta);
}
function openSelectedParagraphInEditor() {
var editLink = document.querySelector(".document-actions-dropdown__item[href$='/edit']");
if (editLink) {
navigateTo(editLink);
return;
}
var path = documentShell ? documentShell.getAttribute("data-document-path") : "";
if (!path) {
return;
}
window.location.href = "/docs/" + path.replace(/\.md$/, "") + "/edit";
}
function focusSearch() {
var search = document.querySelector(".site-search input[type='search'], .site-search input[name='q']");
if (!search) {
return;
}
clearBrowserSelection();
clearParagraphSelection();
search.focus();
search.select();
}
function focusCommentsSidebar() {
var desktopToggle = document.querySelector("[data-comment-toggle]");
if (desktopToggle && desktopToggle.getAttribute("aria-pressed") === "false") {
desktopToggle.click();
}
var mobileToggle = document.querySelector("[data-toggle-comments]");
if (mobileToggle && window.matchMedia("(max-width: 760px)").matches && mobileToggle.getAttribute("aria-expanded") === "false") {
mobileToggle.click();
}
var focusTarget = document.querySelector("[data-comments-aside-list] .comments-aside-item") ||
document.querySelector("[data-comments-aside-list]") ||
document.querySelector("[data-comments-aside]");
if (!focusTarget) {
return;
}
clearBrowserSelection();
clearParagraphSelection();
focusTarget.setAttribute("tabindex", "-1");
focusTarget.focus({ preventScroll: false });
}
function focusDocumentActions() {
var dropdown = document.querySelector(".document-actions-dropdown");
if (!dropdown) {
return;
}
dropdown.open = true;
var focusTarget = dropdown.querySelector(".document-actions-dropdown__item[href], .document-actions-dropdown__item:not([disabled])") ||
dropdown.querySelector("summary");
if (!focusTarget) {
return;
}
clearBrowserSelection();
clearParagraphSelection();
focusTarget.focus();
}
function closeDocumentActions() {
var dropdown = document.querySelector(".document-actions-dropdown[open]");
if (dropdown) {
dropdown.open = false;
var summary = dropdown.querySelector("summary");
if (summary) {
summary.focus();
}
return true;
}
return false;
}
function handleEnter() {
if (mode === "paragraph") {
openSelectedParagraphInEditor();
return;
}
navigateTo(selectedLink);
}
function handleKeydown(event) {
if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey || shouldIgnoreTarget(event.target)) {
return;
}
var key = event.key;
var normalized = key.length === 1 ? key.toLowerCase() : key;
if (event.code === "Backslash" || key === "\\") {
event.preventDefault();
focusSearch();
return;
}
if (normalized === "c") {
event.preventDefault();
focusCommentsSidebar();
return;
}
if (normalized === "e") {
event.preventDefault();
focusDocumentActions();
return;
}
if (key === "Escape" && closeDocumentActions()) {
event.preventDefault();
return;
}
if (key === "ArrowUp" || normalized === "k") {
event.preventDefault();
showKeyboardUI();
if (mode === "paragraph") {
moveParagraphVertical(-1);
} else {
moveBrowserVertical(-1);
}
return;
}
if (key === "ArrowDown" || normalized === "j") {
event.preventDefault();
showKeyboardUI();
if (mode === "paragraph") {
moveParagraphVertical(1);
} else {
moveBrowserVertical(1);
}
return;
}
if (key === "ArrowLeft" || normalized === "h") {
event.preventDefault();
showKeyboardUI();
if (mode === "paragraph") {
selectBrowserColumn(getColumns().length - 1, true);
} else {
selectBrowserColumn(selectedColumnIndex - 1, true);
}
return;
}
if (key === "ArrowRight" || normalized === "l") {
event.preventDefault();
showKeyboardUI();
if (mode === "paragraph") {
return;
}
moveBrowserRight();
return;
}
if (key === "Enter") {
event.preventDefault();
showKeyboardUI();
handleEnter();
}
}
function initSelection() {
var columns = getColumns();
if (!columns.length) {
return;
}
var activeLinks = Array.prototype.slice.call(browser.querySelectorAll("a.is-active[href]"));
var activeLink = activeLinks[activeLinks.length - 1] || browser.querySelector("a[href]");
selectedLink = activeLink;
if (activeLink) {
var activeColumn = activeLink.closest(".miller-column");
selectedColumnIndex = Math.max(0, columns.indexOf(activeColumn));
}
}
initSelection();
document.addEventListener("keydown", handleKeydown);
})();

View File

@@ -1540,11 +1540,17 @@ code {
} }
.miller-column a:hover, .miller-column a:hover,
.miller-column a.is-active { .miller-column a.is-active,
.keyboard-navigation-active .miller-column a.is-keyboard-selected {
background: var(--accent-soft); background: var(--accent-soft);
color: var(--accent); color: var(--accent);
} }
.keyboard-navigation-active .miller-column a.is-keyboard-selected {
outline: 2px solid color-mix(in srgb, var(--accent) 70%, transparent);
outline-offset: -2px;
}
.browser-item-name { .browser-item-name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -2080,7 +2086,7 @@ code {
display: none; display: none;
} }
[data-monaco-mount] { [data-codemirror-mount] {
display: none; display: none;
width: 100%; width: 100%;
min-height: 60vh; min-height: 60vh;
@@ -2089,7 +2095,7 @@ code {
overflow: hidden; overflow: hidden;
} }
[data-monaco-mount].monaco-mounted { [data-codemirror-mount].codemirror-mounted {
display: block; display: block;
} }
@@ -2105,26 +2111,31 @@ code {
font: 400 0.98rem/1.6 ui-monospace, SFMono-Regular, monospace; font: 400 0.98rem/1.6 ui-monospace, SFMono-Regular, monospace;
} }
[data-monaco-mount].monaco-mounted ~ textarea { [data-codemirror-mount].codemirror-mounted ~ textarea {
display: none; display: none;
} }
@keyframes monaco-shimmer { @keyframes codemirror-shimmer {
0% { background-position: -200% 0; } 0% { background-position: -200% 0; }
100% { background-position: 200% 0; } 100% { background-position: 200% 0; }
} }
[data-monaco-mount]:not(.monaco-mounted) { [data-codemirror-mount]:not(.codemirror-mounted) {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(90deg, var(--panel-strong) 25%, var(--panel) 50%, var(--panel-strong) 75%); background: linear-gradient(90deg, var(--panel-strong) 25%, var(--panel) 50%, var(--panel-strong) 75%);
background-size: 200% 100%; background-size: 200% 100%;
animation: monaco-shimmer 1.5s ease infinite; animation: codemirror-shimmer 1.5s ease infinite;
color: var(--muted); color: var(--muted);
font-size: 0.85rem; font-size: 0.85rem;
} }
[data-codemirror-mount] .cm-editor,
[data-conflict-codemirror-mount] .cm-editor {
height: 100%;
}
.editor-status { .editor-status {
display: inline-block; display: inline-block;
min-width: 5.5rem; min-width: 5.5rem;
@@ -2290,7 +2301,7 @@ code {
padding: 0.75rem; padding: 0.75rem;
} }
[data-conflict-monaco-mount] { [data-conflict-codemirror-mount] {
display: none; display: none;
width: 100%; width: 100%;
min-height: 18rem; min-height: 18rem;
@@ -2300,11 +2311,11 @@ code {
overflow: hidden; overflow: hidden;
} }
[data-conflict-monaco-mount].monaco-mounted { [data-conflict-codemirror-mount].codemirror-mounted {
display: block; display: block;
} }
[data-conflict-monaco-mount].monaco-mounted + textarea { [data-conflict-codemirror-mount].codemirror-mounted + textarea {
display: none; display: none;
} }
@@ -2833,7 +2844,7 @@ code {
color: var(--text); color: var(--text);
} }
[data-conflict-monaco-mount] { [data-conflict-codemirror-mount] {
border-color: oklch(0.55 0.18 25 / 0.3); border-color: oklch(0.55 0.18 25 / 0.3);
background: oklch(0.14 0.012 55); background: oklch(0.14 0.012 55);
} }
@@ -2913,10 +2924,17 @@ code {
position: relative; position: relative;
} }
.commentable:hover { .commentable:hover,
.keyboard-navigation-active .commentable.is-keyboard-selected,
.keyboard-navigation-active .markdown-body .is-keyboard-selected {
background: var(--accent-soft); background: var(--accent-soft);
} }
.keyboard-navigation-active .markdown-body .is-keyboard-selected {
outline: 2px solid color-mix(in srgb, var(--accent) 68%, transparent);
outline-offset: 0.25rem;
}
.comment-anchor-btn { .comment-anchor-btn {
position: absolute; position: absolute;
right: -1.5rem; right: -1.5rem;

View File

@@ -1,4 +1,4 @@
const STATIC_CACHE = "md-hub-static-v2"; const STATIC_CACHE = "md-hub-static-v6";
const DB_NAME = "md-hub-cache"; const DB_NAME = "md-hub-cache";
const DB_VERSION = 3; const DB_VERSION = 3;
const STORE_PAGES = "pages"; const STORE_PAGES = "pages";
@@ -8,7 +8,9 @@ const STATIC_ASSETS = [
"/static/sync.js", "/static/sync.js",
"/static/realtime.js", "/static/realtime.js",
"/static/editor.js", "/static/editor.js",
"/static/codemirror.bundle.js",
"/static/render.js", "/static/render.js",
"/static/keyboard-nav.js",
"/static/tags.js", "/static/tags.js",
"/static/favicon.png", "/static/favicon.png",
"/static/cairnquire%20logo%402x.webp", "/static/cairnquire%20logo%402x.webp",

View File

@@ -111,6 +111,7 @@
<script src="/static/auth.js" defer></script> <script src="/static/auth.js" defer></script>
<script src="/static/render.js" defer></script> <script src="/static/render.js" defer></script>
<script src="/static/comments.js" defer></script> <script src="/static/comments.js" defer></script>
<script src="/static/keyboard-nav.js" defer></script>
<script src="/static/mobile.js" defer></script> <script src="/static/mobile.js" defer></script>
<script src="/static/archive.js" defer></script> <script src="/static/archive.js" defer></script>
<script src="/static/permissions.js" defer></script> <script src="/static/permissions.js" defer></script>

View File

@@ -70,7 +70,7 @@
<div class="editor-conflict__diff" data-conflict-diff></div> <div class="editor-conflict__diff" data-conflict-diff></div>
<div class="editor-conflict__resolution"> <div class="editor-conflict__resolution">
<label for="conflict-resolution">Resolved document</label> <label for="conflict-resolution">Resolved document</label>
<div data-conflict-monaco-mount></div> <div data-conflict-codemirror-mount></div>
<textarea id="conflict-resolution" data-conflict-resolution spellcheck="false"></textarea> <textarea id="conflict-resolution" data-conflict-resolution spellcheck="false"></textarea>
</div> </div>
</div> </div>
@@ -78,7 +78,7 @@
<form class="editor-form" data-editor-form data-document-path="{{ .Path }}"> <form class="editor-form" data-editor-form data-document-path="{{ .Path }}">
<label class="sr-only" for="document-editor">Markdown editor</label> <label class="sr-only" for="document-editor">Markdown editor</label>
<div data-monaco-mount></div> <div data-codemirror-mount></div>
<textarea id="document-editor" name="content" data-document-editor spellcheck="false">{{ .Source }}</textarea> <textarea id="document-editor" name="content" data-document-editor spellcheck="false">{{ .Source }}</textarea>
</form> </form>
</article> </article>