mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-14 10:38:11 +02:00
Properties, and ticking checkboxes without editing (web)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d4c018a402
commit
c8d29ad6f8
4 files changed
+343
-10
No files matched your search
+110
-6
@@ -3,13 +3,15 @@
|
||||
// > and list markers) hidden. Wherever the cursor is, the syntax comes back so
|
||||
// it can be edited: a heading's #s on its line, a bold's ** inside it.
|
||||
// The colours are the same roles as .MarkdownBody (Styles/Theme.css).
|
||||
// ```mermaid blocks show as their diagram until the cursor goes into them.
|
||||
// ```mermaid blocks show as their diagram until the cursor goes into them,
|
||||
// and the properties block at the top (Properties.js) as its panel.
|
||||
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import { StateField } from "@codemirror/state";
|
||||
import { StateEffect, StateField } from "@codemirror/state";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
|
||||
import { RenderDiagram } from "./Mermaid.js";
|
||||
import { ParseProperties, PropertiesHtml, ToggleLine } from "./Properties.js";
|
||||
|
||||
class BulletWidget extends WidgetType {
|
||||
eq() {
|
||||
@@ -103,6 +105,57 @@ class DiagramWidget extends WidgetType {
|
||||
}
|
||||
}
|
||||
|
||||
function EscapeHtml(Text) {
|
||||
return Text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
|
||||
// The properties block drawn as its panel. A true/false checkbox flips the
|
||||
// value without opening the block; clicking anywhere else opens it to edit.
|
||||
class PropertiesWidget extends WidgetType {
|
||||
constructor(Html) {
|
||||
super();
|
||||
this.Html = Html;
|
||||
}
|
||||
|
||||
eq(Other) {
|
||||
return this.Html === Other.Html;
|
||||
}
|
||||
|
||||
toDOM(View) {
|
||||
const Element = document.createElement("div");
|
||||
Element.className = "MarkdownBody cm-LiveProperties";
|
||||
Element.style.padding = "0";
|
||||
Element.innerHTML = this.Html;
|
||||
Element.addEventListener("mousedown", (Event) => {
|
||||
Event.preventDefault();
|
||||
const Box = Event.target.closest(".PropertyCheckbox");
|
||||
const Doc = View.state.doc;
|
||||
if (Box) {
|
||||
const Line = Doc.line(Number(Box.dataset.line) + 1);
|
||||
const Changed = ToggleLine(Line.text, 0);
|
||||
View.dispatch({ changes: { from: Line.from, to: Line.to, insert: Changed } });
|
||||
return;
|
||||
}
|
||||
View.dispatch({ selection: { anchor: Doc.line(Math.min(2, Doc.lines)).from } });
|
||||
View.focus();
|
||||
});
|
||||
return Element;
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Where the properties block ends (0 if there isn't one): the plugin below
|
||||
// leaves it alone, since Markdown would read it as a rule and a heading.
|
||||
function PropertiesEnd(State) {
|
||||
const Doc = State.doc;
|
||||
if (!/^---\s*$/.test(Doc.line(1).text)) return 0;
|
||||
const Parsed = ParseProperties(Doc.sliceString(0, Math.min(Doc.length, 20000)));
|
||||
return Parsed ? Parsed.EndOffset : 0;
|
||||
}
|
||||
|
||||
const Hidden = Decoration.replace({});
|
||||
const Bullet = Decoration.replace({ widget: new BulletWidget() });
|
||||
const Rule = Decoration.replace({ widget: new RuleWidget() });
|
||||
@@ -157,11 +210,15 @@ function BuildDecorations(View) {
|
||||
Ranges.push(Hidden.range(From, End));
|
||||
}
|
||||
|
||||
const PropertiesTo = PropertiesEnd(State);
|
||||
if (PropertiesTo > 0) AddLineClass(0, PropertiesTo, "cm-LiveFrontmatter");
|
||||
|
||||
for (const { from, to } of View.visibleRanges) {
|
||||
syntaxTree(State).iterate({
|
||||
from,
|
||||
to,
|
||||
enter(Node) {
|
||||
if (Node.from < PropertiesTo) return Node.to <= PropertiesTo ? false : undefined;
|
||||
const Name = Node.name;
|
||||
const Parent = Node.node.parent;
|
||||
const ParentName = Parent ? Parent.name : "";
|
||||
@@ -257,8 +314,8 @@ function BuildDecorations(View) {
|
||||
}
|
||||
|
||||
// Diagrams replace whole lines, which CodeMirror only allows from a state
|
||||
// field, not from the plugin above. Without focus info here, a block shows
|
||||
// as code while the cursor (or selection) is in it, even when unfocused.
|
||||
// field, not from the plugin above. A block shows as code while the cursor
|
||||
// (or selection) is in it and the editor has focus.
|
||||
function BuildDiagrams(State) {
|
||||
const Doc = State.doc;
|
||||
const Ranges = [];
|
||||
@@ -271,7 +328,7 @@ function BuildDiagrams(State) {
|
||||
|
||||
const From = Doc.lineAt(Node.from).from;
|
||||
const To = Doc.lineAt(Node.to).to;
|
||||
if (State.selection.ranges.some((Range) => Range.from <= To && Range.to >= From)) return false;
|
||||
if (CursorIn(State, From, To)) return false;
|
||||
|
||||
const Code = Node.node.getChild("CodeText");
|
||||
const Source = Code ? Doc.sliceString(Code.from, Code.to) : "";
|
||||
@@ -283,17 +340,64 @@ function BuildDiagrams(State) {
|
||||
return Decoration.set(Ranges, true);
|
||||
}
|
||||
|
||||
// Whether the editor has focus, for the fields below (the plugin above asks
|
||||
// the view; fields only see the state). Unfocused, everything shows rendered.
|
||||
const SetFocused = StateEffect.define();
|
||||
const Focused = StateField.define({
|
||||
create: () => false,
|
||||
update(Current, Transaction) {
|
||||
for (const Effect of Transaction.effects) {
|
||||
if (Effect.is(SetFocused)) return Effect.value;
|
||||
}
|
||||
return Current;
|
||||
},
|
||||
});
|
||||
const TrackFocus = EditorView.focusChangeEffect.of((State, Focusing) => SetFocused.of(Focusing));
|
||||
|
||||
function CursorIn(State, From, To) {
|
||||
if (!State.field(Focused, false)) return false;
|
||||
return State.selection.ranges.some((Range) => Range.from <= To && Range.to >= From);
|
||||
}
|
||||
|
||||
function FocusChanged(Transaction) {
|
||||
return Transaction.effects.some((Effect) => Effect.is(SetFocused));
|
||||
}
|
||||
|
||||
function BuildProperties(State) {
|
||||
const To = PropertiesEnd(State);
|
||||
if (To === 0) return Decoration.none;
|
||||
if (CursorIn(State, 0, To)) return Decoration.none;
|
||||
const Parsed = ParseProperties(State.doc.sliceString(0, To));
|
||||
if (!Parsed || Parsed.Properties.length === 0) return Decoration.none;
|
||||
const Html = PropertiesHtml(Parsed, EscapeHtml, EscapeHtml, true);
|
||||
return Decoration.set([Decoration.replace({ widget: new PropertiesWidget(Html), block: true }).range(0, To)]);
|
||||
}
|
||||
|
||||
const PropertiesPanel = StateField.define({
|
||||
create: BuildProperties,
|
||||
update(Current, Transaction) {
|
||||
if (Transaction.docChanged || Transaction.selection || FocusChanged(Transaction)) return BuildProperties(Transaction.state);
|
||||
return Current;
|
||||
},
|
||||
provide: (Field) => EditorView.decorations.from(Field),
|
||||
});
|
||||
|
||||
const Diagrams = StateField.define({
|
||||
create: BuildDiagrams,
|
||||
update(Current, Transaction) {
|
||||
const TreeChanged = syntaxTree(Transaction.state) !== syntaxTree(Transaction.startState);
|
||||
if (Transaction.docChanged || Transaction.selection || TreeChanged) return BuildDiagrams(Transaction.state);
|
||||
if (Transaction.docChanged || Transaction.selection || TreeChanged || FocusChanged(Transaction)) {
|
||||
return BuildDiagrams(Transaction.state);
|
||||
}
|
||||
return Current;
|
||||
},
|
||||
provide: (Field) => EditorView.decorations.from(Field),
|
||||
});
|
||||
|
||||
export const LivePreview = [
|
||||
Focused,
|
||||
TrackFocus,
|
||||
PropertiesPanel,
|
||||
Diagrams,
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
|
||||
Reference in new issue
Block a user