Compare commits
2 Commits
de3abed6d7
...
11cae7df36
| Author | SHA1 | Date | |
|---|---|---|---|
| 11cae7df36 | |||
| 7be8bec446 |
@@ -34,6 +34,11 @@ make editor # runs `npm ci && npm run build` in editor-build/, rewrites
|
|||||||
Commit the regenerated `codemirror.bundle.js` and the updated
|
Commit the regenerated `codemirror.bundle.js` and the updated
|
||||||
`editor-build/package-lock.json`. `editor-build/node_modules/` is gitignored.
|
`editor-build/package-lock.json`. `editor-build/node_modules/` is gitignored.
|
||||||
|
|
||||||
|
The bundle is served immutable under a stable filename, so the edit template
|
||||||
|
appends `?v=<content-hash>` to its `<script>` src (`editorBundleVersion` in
|
||||||
|
`main.go`). The hash changes whenever the bundle bytes change, so a rebuilt
|
||||||
|
bundle busts client caches automatically — no manual version bump needed.
|
||||||
|
|
||||||
## HTTP API Surface
|
## HTTP API Surface
|
||||||
|
|
||||||
| Method | Path | Behaviour |
|
| Method | Path | Behaviour |
|
||||||
|
|||||||
@@ -5,12 +5,15 @@
|
|||||||
|
|
||||||
{{define "content"}}
|
{{define "content"}}
|
||||||
<script>
|
<script>
|
||||||
if (sessionStorage.getItem('editor-wide') === '1') document.body.classList.add('editor-wide');
|
document.body.classList.add('edit-mode');
|
||||||
</script>
|
</script>
|
||||||
<form id="edit-form" class="edit-form" method="POST" action="{{.PostURL}}">
|
<form id="edit-form" class="edit-form" method="POST" action="{{.PostURL}}">
|
||||||
{{if ge .SectionIndex 0}}<input type="hidden" name="section" value="{{.SectionIndex}}">{{end}}
|
{{if ge .SectionIndex 0}}<input type="hidden" name="section" value="{{.SectionIndex}}">{{end}}
|
||||||
{{if ge .InsertBefore 0}}<input type="hidden" name="insert_before" value="{{.InsertBefore}}">{{end}}
|
{{if ge .InsertBefore 0}}<input type="hidden" name="insert_before" value="{{.InsertBefore}}">{{end}}
|
||||||
<div class="editor-toolbar">
|
<div class="editor-toolbar">
|
||||||
|
<button type="button" class="btn btn-tool" data-action="undo" title="Undo">↶</button>
|
||||||
|
<button type="button" class="btn btn-tool" data-action="redo" title="Redo">↷</button>
|
||||||
|
<span class="toolbar-sep"></span>
|
||||||
<button type="button" class="btn btn-tool" data-action="bold" data-key="B" title="Bold (B)">**</button>
|
<button type="button" class="btn btn-tool" data-action="bold" data-key="B" title="Bold (B)">**</button>
|
||||||
<button type="button" class="btn btn-tool" data-action="italic" data-key="I" title="Italic (I)">*</button>
|
<button type="button" class="btn btn-tool" data-action="italic" data-key="I" title="Italic (I)">*</button>
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
@@ -41,6 +44,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<button type="button" class="btn btn-tool" data-action="quote" data-key="Q" title="Blockquote (Q)">></button>
|
<button type="button" class="btn btn-tool" data-action="quote" data-key="Q" title="Blockquote (Q)">></button>
|
||||||
<button type="button" class="btn btn-tool" data-action="hr" data-key="R" title="Horizontal rule (R)">---</button>
|
<button type="button" class="btn btn-tool" data-action="hr" data-key="R" title="Horizontal rule (R)">---</button>
|
||||||
|
<button type="button" class="btn btn-tool" data-action="deleteline" data-key="Y" title="Delete line (Y)">⌦</button>
|
||||||
<span class="toolbar-sep"></span>
|
<span class="toolbar-sep"></span>
|
||||||
<span class="dropdown">
|
<span class="dropdown">
|
||||||
<button type="button" class="btn btn-tool dropdown-toggle" title="Table (T)">T▾</button>
|
<button type="button" class="btn btn-tool dropdown-toggle" title="Table (T)">T▾</button>
|
||||||
@@ -68,13 +72,11 @@
|
|||||||
<button type="button" class="btn btn-tool btn-block" data-action="movie" data-key="V" title="Import movie (V)">Import movie</button>
|
<button type="button" class="btn btn-tool btn-block" data-action="movie" data-key="V" title="Import movie (V)">Import movie</button>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span class="toolbar-sep"></span>
|
|
||||||
<button type="button" class="btn btn-tool" data-action="wide" data-key="Z" title="Toggle wide mode (Z)">⇔</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="editor" class="editor-cm"></div>
|
<div id="editor" class="editor-cm"></div>
|
||||||
<textarea name="content" id="editor-content" hidden>{{.RawContent}}</textarea>
|
<textarea name="content" id="editor-content" hidden>{{.RawContent}}</textarea>
|
||||||
</form>
|
</form>
|
||||||
<script src="/_/editor/vendor/codemirror.bundle.js"></script>
|
<script src="/_/editor/vendor/codemirror.bundle.js?v={{editorBundleVersion}}"></script>
|
||||||
<script src="/_/editor/tables.js"></script>
|
<script src="/_/editor/tables.js"></script>
|
||||||
<script src="/_/editor/dates.js"></script>
|
<script src="/_/editor/dates.js"></script>
|
||||||
<script src="/_/editor/movie.js"></script>
|
<script src="/_/editor/movie.js"></script>
|
||||||
|
|||||||
+23
-6
@@ -142,6 +142,9 @@
|
|||||||
|
|
||||||
var actions = {
|
var actions = {
|
||||||
save: function () { form.requestSubmit(); },
|
save: function () { form.requestSubmit(); },
|
||||||
|
undo: function () { CM.undo(view); view.focus(); },
|
||||||
|
redo: function () { CM.redo(view); view.focus(); },
|
||||||
|
deleteline: function () { CM.deleteLine(view); view.focus(); },
|
||||||
bold: function () { wrap('**', '**', 'bold text'); },
|
bold: function () { wrap('**', '**', 'bold text'); },
|
||||||
italic: function () { wrap('*', '*', 'italic text'); },
|
italic: function () { wrap('*', '*', 'italic text'); },
|
||||||
h1: function () { linePrefix('# '); },
|
h1: function () { linePrefix('# '); },
|
||||||
@@ -167,11 +170,6 @@
|
|||||||
dateiso: function () { insertAtCursor(D.isoDate()); },
|
dateiso: function () { insertAtCursor(D.isoDate()); },
|
||||||
datelong: function () { insertAtCursor(D.longDate()); },
|
datelong: function () { insertAtCursor(D.longDate()); },
|
||||||
movie: function () { M.run(movieCtx); },
|
movie: function () { M.run(movieCtx); },
|
||||||
wide: function () {
|
|
||||||
var enabled = !document.body.classList.contains('editor-wide');
|
|
||||||
document.body.classList.toggle('editor-wide', enabled);
|
|
||||||
sessionStorage.setItem('editor-wide', enabled ? '1' : '0');
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wiki link button: drop an empty [[]] at the cursor and open the `[[`
|
// Wiki link button: drop an empty [[]] at the cursor and open the `[[`
|
||||||
@@ -214,5 +212,24 @@
|
|||||||
|
|
||||||
// --- Dropdowns ---
|
// --- Dropdowns ---
|
||||||
|
|
||||||
document.querySelectorAll('.dropdown-toggle').forEach(wireDropdown);
|
// The toolbar scrolls horizontally on mobile, which makes it an overflow
|
||||||
|
// container that would clip its absolutely-positioned dropdown menus. Pin an
|
||||||
|
// open menu to the viewport under its trigger so it escapes the clip.
|
||||||
|
var toolbar = document.querySelector('.editor-toolbar');
|
||||||
|
function pinMenu(toggle, menu) {
|
||||||
|
if (!menu.classList.contains('is-open')) return;
|
||||||
|
var r = toggle.getBoundingClientRect();
|
||||||
|
menu.style.position = 'fixed';
|
||||||
|
menu.style.top = r.bottom + 'px';
|
||||||
|
var left = Math.min(r.left, document.documentElement.clientWidth - menu.offsetWidth - 4);
|
||||||
|
menu.style.left = Math.max(4, left) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.dropdown-toggle').forEach(function (toggle) {
|
||||||
|
wireDropdown(toggle);
|
||||||
|
var menu = toggle.parentElement.querySelector('.dropdown-menu');
|
||||||
|
if (!menu || !toolbar || !toolbar.contains(toggle)) return;
|
||||||
|
// Runs after wireDropdown's own click handler has toggled is-open.
|
||||||
|
toggle.addEventListener('click', function () { pinMenu(toggle, menu); });
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
+14
-14
File diff suppressed because one or more lines are too long
+27
-3
@@ -352,16 +352,24 @@ main > h2 {
|
|||||||
.suggest-path { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.1rem; }
|
.suggest-path { color: var(--text-muted); font-size: 0.8rem; margin-top: 0.1rem; }
|
||||||
.suggest-footer > td { color: var(--link); font-size: var(--font-sm); }
|
.suggest-footer > td { color: var(--link); font-size: var(--font-sm); }
|
||||||
|
|
||||||
/* === Editor toolbar === */
|
/* === Editor toolbar ===
|
||||||
|
Single non-wrapping row that scrolls horizontally (swipe on mobile) rather
|
||||||
|
than breaking into stacked rows. A horizontal-scroll container also clips
|
||||||
|
overflow-y, so open dropdown menus are pinned to the viewport via JS
|
||||||
|
(editor/main.js pinMenu) to escape the clip. */
|
||||||
.editor-toolbar {
|
.editor-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: var(--space-1);
|
gap: var(--space-1);
|
||||||
border: var(--border);
|
border: var(--border);
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
padding: 0.4rem 0.6rem;
|
padding: 0.4rem 0.6rem;
|
||||||
background: var(--bg-panel-hover);
|
background: var(--bg-panel-hover);
|
||||||
|
overflow-x: auto;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
.editor-toolbar::-webkit-scrollbar { display: none; }
|
||||||
|
.editor-toolbar > * { flex-shrink: 0; }
|
||||||
.toolbar-sep {
|
.toolbar-sep {
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background: var(--secondary);
|
background: var(--secondary);
|
||||||
@@ -371,7 +379,9 @@ main > h2 {
|
|||||||
|
|
||||||
/* === Edit form === */
|
/* === Edit form === */
|
||||||
.edit-form { display: flex; flex-direction: column; }
|
.edit-form { display: flex; flex-direction: column; }
|
||||||
body.editor-wide .page-wrap { max-width: none; }
|
/* The sidebar is always empty while editing, so the editor uses the full
|
||||||
|
viewport: drop the reserved 14rem sidebar track and the centered max-width. */
|
||||||
|
body.edit-mode .page-wrap { grid-template-columns: minmax(0, 1fr); max-width: none; }
|
||||||
/* CodeMirror mount. The .cm-editor visual treatment (border, bg, font, padding)
|
/* CodeMirror mount. The .cm-editor visual treatment (border, bg, font, padding)
|
||||||
lives in the CM theme (editor-build/entry.js), keyed off the same :root
|
lives in the CM theme (editor-build/entry.js), keyed off the same :root
|
||||||
variables; this only sizes the container. */
|
variables; this only sizes the container. */
|
||||||
@@ -675,6 +685,20 @@ aside.sidebar:empty { display: none; }
|
|||||||
main { padding: var(--space-4) var(--space-3); }
|
main { padding: var(--space-4) var(--space-3); }
|
||||||
.editor-cm { min-height: 50vh; }
|
.editor-cm { min-height: 50vh; }
|
||||||
.sidebar { width: calc(100% - 1.5rem); }
|
.sidebar { width: calc(100% - 1.5rem); }
|
||||||
|
/* Editing on mobile is full-bleed: drop the page/main inset so the toolbar
|
||||||
|
and editor use the entire viewport width. */
|
||||||
|
body.edit-mode .page-wrap { padding: 0; gap: 0; }
|
||||||
|
body.edit-mode main { padding: 0; }
|
||||||
|
/* Fingers, not cursors: give every toolbar control a ~44px tap target. */
|
||||||
|
.editor-toolbar { gap: var(--space-2); padding: var(--space-2); }
|
||||||
|
.editor-toolbar .btn-tool {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
padding: 0 var(--space-2);
|
||||||
|
}
|
||||||
.modal-backdrop { padding: var(--space-2); align-items: flex-start; }
|
.modal-backdrop { padding: var(--space-2); align-items: flex-start; }
|
||||||
.modal { max-width: none; margin-top: var(--space-4); }
|
.modal { max-width: none; margin-top: var(--space-4); }
|
||||||
.modal .panel-header { cursor: default; }
|
.modal .panel-header { cursor: default; }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// :root CSS variables so there are no hardcoded colors/spacing here.
|
// :root CSS variables so there are no hardcoded colors/spacing here.
|
||||||
import { EditorState, EditorSelection, Compartment, Prec } from "@codemirror/state";
|
import { EditorState, EditorSelection, Compartment, Prec } from "@codemirror/state";
|
||||||
import { EditorView, keymap, drawSelection } from "@codemirror/view";
|
import { EditorView, keymap, drawSelection } from "@codemirror/view";
|
||||||
import { history, historyKeymap, defaultKeymap, indentWithTab } from "@codemirror/commands";
|
import { history, historyKeymap, defaultKeymap, indentWithTab, undo, redo, deleteLine } from "@codemirror/commands";
|
||||||
import { markdown, markdownLanguage, markdownKeymap } from "@codemirror/lang-markdown";
|
import { markdown, markdownLanguage, markdownKeymap } from "@codemirror/lang-markdown";
|
||||||
import { syntaxHighlighting, HighlightStyle, indentOnInput } from "@codemirror/language";
|
import { syntaxHighlighting, HighlightStyle, indentOnInput } from "@codemirror/language";
|
||||||
import {
|
import {
|
||||||
@@ -89,6 +89,9 @@ window.CM = {
|
|||||||
historyKeymap,
|
historyKeymap,
|
||||||
defaultKeymap,
|
defaultKeymap,
|
||||||
indentWithTab,
|
indentWithTab,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
deleteLine,
|
||||||
markdown,
|
markdown,
|
||||||
markdownLanguage,
|
markdownLanguage,
|
||||||
markdownKeymap,
|
markdownKeymap,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/hex"
|
||||||
"flag"
|
"flag"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -19,9 +21,26 @@ import (
|
|||||||
//go:embed assets
|
//go:embed assets
|
||||||
var assets embed.FS
|
var assets embed.FS
|
||||||
|
|
||||||
|
// editorBundleVersion is a short content hash of the vendored CodeMirror bundle,
|
||||||
|
// appended as ?v=… to its <script> src. The bundle is served immutable under a
|
||||||
|
// stable filename, so without this query a rebuilt bundle would never reach a
|
||||||
|
// client that already cached the old one (this is the editor cache-bust knob).
|
||||||
|
var editorBundleVersion = hashAsset("assets/editor/vendor/codemirror.bundle.js")
|
||||||
|
|
||||||
|
func hashAsset(name string) string {
|
||||||
|
b, err := assets.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256(b)
|
||||||
|
return hex.EncodeToString(sum[:])[:12]
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
pageTmpl = template.Must(template.ParseFS(assets, "assets/layout.html", "assets/page/main.html"))
|
pageTmpl = template.Must(template.ParseFS(assets, "assets/layout.html", "assets/page/main.html"))
|
||||||
editTmpl = template.Must(template.ParseFS(assets, "assets/layout.html", "assets/editor/main.html"))
|
editTmpl = template.Must(template.New("edit").Funcs(template.FuncMap{
|
||||||
|
"editorBundleVersion": func() string { return editorBundleVersion },
|
||||||
|
}).ParseFS(assets, "assets/layout.html", "assets/editor/main.html"))
|
||||||
searchTmpl = template.Must(template.ParseFS(assets, "assets/layout.html", "assets/search/main.html"))
|
searchTmpl = template.Must(template.ParseFS(assets, "assets/layout.html", "assets/search/main.html"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user