mirror of
https://git.srhtnin.garden/sarah/SaraView.git
synced 2026-10-15 10:43:17 +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
@@ -11,6 +11,7 @@ import MarkdownItFootnote from "markdown-it-footnote";
|
|||||||
|
|
||||||
import { LivePreview } from "../LivePreview.js";
|
import { LivePreview } from "../LivePreview.js";
|
||||||
import { DiagramHtml, RenderDiagramsIn } from "../Mermaid.js";
|
import { DiagramHtml, RenderDiagramsIn } from "../Mermaid.js";
|
||||||
|
import { ParseProperties, PropertiesHtml, TaskLines, ToggleLine, WithoutProperties } from "../Properties.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FetchBlame,
|
FetchBlame,
|
||||||
@@ -54,6 +55,17 @@ MarkdownRenderer.renderer.rules.fence = (Tokens, Index, Options, Env, Self) => {
|
|||||||
return DefaultFenceRenderer(Tokens, Index, Options, Env, Self);
|
return DefaultFenceRenderer(Tokens, Index, Options, Env, Self);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function RenderPropertiesPanel(Text, CanEdit) {
|
||||||
|
const Parsed = ParseProperties(Text);
|
||||||
|
if (!Parsed || Parsed.Properties.length === 0) return "";
|
||||||
|
return PropertiesHtml(
|
||||||
|
Parsed,
|
||||||
|
(Value) => MarkdownRenderer.renderInline(Value),
|
||||||
|
MarkdownRenderer.utils.escapeHtml,
|
||||||
|
CanEdit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const TextTypes = ["markdown", "text", "config", "html"];
|
const TextTypes = ["markdown", "text", "config", "html"];
|
||||||
|
|
||||||
const LinkedFileTypeMap = {
|
const LinkedFileTypeMap = {
|
||||||
@@ -221,10 +233,21 @@ export default function PreviewPane({
|
|||||||
}
|
}
|
||||||
}, [RootName, FilePath, FileType, ShowGitBlame]);
|
}, [RootName, FilePath, FileType, ShowGitBlame]);
|
||||||
|
|
||||||
// Draw the rendered view's diagrams once its HTML is on the page.
|
// Once the rendered view's HTML is on the page: draw its diagrams, and
|
||||||
|
// switch off task checkboxes that can't be ticked here. They're matched to
|
||||||
|
// the note's lines in order, which only works when the server's version
|
||||||
|
// (with portals filled in) has the same tasks as the note itself.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (MarkdownBodyRef.current) RenderDiagramsIn(MarkdownBodyRef.current);
|
const Body = MarkdownBodyRef.current;
|
||||||
}, [ResolvedContent, FileType, IsEditing, MarkdownEditing, IsGuest]);
|
if (!Body) return;
|
||||||
|
RenderDiagramsIn(Body);
|
||||||
|
const Boxes = Body.querySelectorAll(".task-list-item-checkbox");
|
||||||
|
const Matches = Boxes.length === TaskLines(Content).length;
|
||||||
|
Boxes.forEach((Box) => {
|
||||||
|
Box.disabled = IsGuest || !Matches;
|
||||||
|
Box.title = IsGuest ? "Log in to change this" : Matches ? "" : "This note embeds others: edit it to change this";
|
||||||
|
});
|
||||||
|
}, [Content, ResolvedContent, FileType, IsEditing, MarkdownEditing, IsGuest]);
|
||||||
|
|
||||||
if (!FilePath) {
|
if (!FilePath) {
|
||||||
return <div className="PreviewEmpty">Select a file to preview.</div>;
|
return <div className="PreviewEmpty">Select a file to preview.</div>;
|
||||||
@@ -250,7 +273,39 @@ export default function PreviewPane({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ToggleAndSave(LineIndex, Box) {
|
||||||
|
const Previous = Content;
|
||||||
|
const Changed = ToggleLine(Content, LineIndex);
|
||||||
|
SetContent(Changed);
|
||||||
|
SetSaveError(null);
|
||||||
|
try {
|
||||||
|
await SaveFile(RootName, FilePath, Changed);
|
||||||
|
RefreshResolvedContent();
|
||||||
|
if (ShowGitBlame) {
|
||||||
|
FetchBlame(RootName, FilePath)
|
||||||
|
.then(SetBlameMap)
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (Error) {
|
||||||
|
SetContent(Previous);
|
||||||
|
Box.checked = !Box.checked;
|
||||||
|
SetSaveError(Error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function HandleMarkdownClick(Event) {
|
function HandleMarkdownClick(Event) {
|
||||||
|
const Box = Event.target.closest('input[type="checkbox"]');
|
||||||
|
if (Box) {
|
||||||
|
if (Box.classList.contains("PropertyCheckbox")) {
|
||||||
|
ToggleAndSave(Number(Box.dataset.line), Box);
|
||||||
|
} else if (Box.classList.contains("task-list-item-checkbox")) {
|
||||||
|
const Boxes = Array.from(MarkdownBodyRef.current.querySelectorAll(".task-list-item-checkbox"));
|
||||||
|
const Line = TaskLines(Content)[Boxes.indexOf(Box)];
|
||||||
|
if (Line !== undefined) ToggleAndSave(Line, Box);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const AnchorElement = Event.target.closest("a");
|
const AnchorElement = Event.target.closest("a");
|
||||||
if (!AnchorElement) return;
|
if (!AnchorElement) return;
|
||||||
|
|
||||||
@@ -349,13 +404,14 @@ export default function PreviewPane({
|
|||||||
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
<a className="DownloadButton" href={GetDownloadUrl(RootName, FilePath)}>
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
|
{SaveError && <span className="SaveErrorText">{SaveError}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="MarkdownBody"
|
className="MarkdownBody"
|
||||||
ref={MarkdownBodyRef}
|
ref={MarkdownBodyRef}
|
||||||
onClick={HandleMarkdownClick}
|
onClick={HandleMarkdownClick}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: MarkdownRenderer.render(ResolvedContent, {
|
__html: RenderPropertiesPanel(Content, !IsGuest) + MarkdownRenderer.render(WithoutProperties(ResolvedContent), {
|
||||||
ResolveImageSrc: (Src) => {
|
ResolveImageSrc: (Src) => {
|
||||||
const ResolvedPath = ResolveRelativeLink(FilePath, Src);
|
const ResolvedPath = ResolveRelativeLink(FilePath, Src);
|
||||||
return ResolvedPath === null ? Src : GetFileUrl(RootName, ResolvedPath);
|
return ResolvedPath === null ? Src : GetFileUrl(RootName, ResolvedPath);
|
||||||
|
|||||||
+110
-6
@@ -3,13 +3,15 @@
|
|||||||
// > and list markers) hidden. Wherever the cursor is, the syntax comes back so
|
// > 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.
|
// 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).
|
// 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 { syntaxTree } from "@codemirror/language";
|
||||||
import { StateField } from "@codemirror/state";
|
import { StateEffect, StateField } from "@codemirror/state";
|
||||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||||
|
|
||||||
import { RenderDiagram } from "./Mermaid.js";
|
import { RenderDiagram } from "./Mermaid.js";
|
||||||
|
import { ParseProperties, PropertiesHtml, ToggleLine } from "./Properties.js";
|
||||||
|
|
||||||
class BulletWidget extends WidgetType {
|
class BulletWidget extends WidgetType {
|
||||||
eq() {
|
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 Hidden = Decoration.replace({});
|
||||||
const Bullet = Decoration.replace({ widget: new BulletWidget() });
|
const Bullet = Decoration.replace({ widget: new BulletWidget() });
|
||||||
const Rule = Decoration.replace({ widget: new RuleWidget() });
|
const Rule = Decoration.replace({ widget: new RuleWidget() });
|
||||||
@@ -157,11 +210,15 @@ function BuildDecorations(View) {
|
|||||||
Ranges.push(Hidden.range(From, End));
|
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) {
|
for (const { from, to } of View.visibleRanges) {
|
||||||
syntaxTree(State).iterate({
|
syntaxTree(State).iterate({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
enter(Node) {
|
enter(Node) {
|
||||||
|
if (Node.from < PropertiesTo) return Node.to <= PropertiesTo ? false : undefined;
|
||||||
const Name = Node.name;
|
const Name = Node.name;
|
||||||
const Parent = Node.node.parent;
|
const Parent = Node.node.parent;
|
||||||
const ParentName = Parent ? Parent.name : "";
|
const ParentName = Parent ? Parent.name : "";
|
||||||
@@ -257,8 +314,8 @@ function BuildDecorations(View) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Diagrams replace whole lines, which CodeMirror only allows from a state
|
// Diagrams replace whole lines, which CodeMirror only allows from a state
|
||||||
// field, not from the plugin above. Without focus info here, a block shows
|
// field, not from the plugin above. A block shows as code while the cursor
|
||||||
// as code while the cursor (or selection) is in it, even when unfocused.
|
// (or selection) is in it and the editor has focus.
|
||||||
function BuildDiagrams(State) {
|
function BuildDiagrams(State) {
|
||||||
const Doc = State.doc;
|
const Doc = State.doc;
|
||||||
const Ranges = [];
|
const Ranges = [];
|
||||||
@@ -271,7 +328,7 @@ function BuildDiagrams(State) {
|
|||||||
|
|
||||||
const From = Doc.lineAt(Node.from).from;
|
const From = Doc.lineAt(Node.from).from;
|
||||||
const To = Doc.lineAt(Node.to).to;
|
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 Code = Node.node.getChild("CodeText");
|
||||||
const Source = Code ? Doc.sliceString(Code.from, Code.to) : "";
|
const Source = Code ? Doc.sliceString(Code.from, Code.to) : "";
|
||||||
@@ -283,17 +340,64 @@ function BuildDiagrams(State) {
|
|||||||
return Decoration.set(Ranges, true);
|
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({
|
const Diagrams = StateField.define({
|
||||||
create: BuildDiagrams,
|
create: BuildDiagrams,
|
||||||
update(Current, Transaction) {
|
update(Current, Transaction) {
|
||||||
const TreeChanged = syntaxTree(Transaction.state) !== syntaxTree(Transaction.startState);
|
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;
|
return Current;
|
||||||
},
|
},
|
||||||
provide: (Field) => EditorView.decorations.from(Field),
|
provide: (Field) => EditorView.decorations.from(Field),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LivePreview = [
|
export const LivePreview = [
|
||||||
|
Focused,
|
||||||
|
TrackFocus,
|
||||||
|
PropertiesPanel,
|
||||||
Diagrams,
|
Diagrams,
|
||||||
ViewPlugin.fromClass(
|
ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Properties, like Obsidian's: a block at the very top of a note,
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// Key: Value
|
||||||
|
// Done: true
|
||||||
|
// ---
|
||||||
|
//
|
||||||
|
// shown as a Properties panel instead of text. true/false values are
|
||||||
|
// checkboxes that can be ticked without editing the note, like task list
|
||||||
|
// items (- [ ]); the helpers for both are here, shared by the rendered view
|
||||||
|
// (PreviewPane.jsx) and the live editor (LivePreview.js).
|
||||||
|
|
||||||
|
const OpenOrClose = /^---\s*$/;
|
||||||
|
const PropertyLine = /^([^:\s][^:]*?)\s*:\s*(.*)$/;
|
||||||
|
const TaskLine = /^(\s*(?:[-*+]|\d+[.)])\s+)\[( |x|X)\]/;
|
||||||
|
const FenceLine = /^ {0,3}(`{3,}|~{3,})/;
|
||||||
|
|
||||||
|
// The block at the top, or null. Lines are the note's (0-based), so a
|
||||||
|
// property can be changed in place. EndLine is the closing ---.
|
||||||
|
export function ParseProperties(Text) {
|
||||||
|
const Lines = Text.split("\n");
|
||||||
|
if (!OpenOrClose.test(Lines[0] || "")) return null;
|
||||||
|
const EndLine = Lines.findIndex((Line, Index) => Index > 0 && OpenOrClose.test(Line));
|
||||||
|
if (EndLine === -1) return null;
|
||||||
|
|
||||||
|
const Properties = [];
|
||||||
|
for (let Index = 1; Index < EndLine; Index++) {
|
||||||
|
const Line = Lines[Index];
|
||||||
|
const Match = PropertyLine.exec(Line);
|
||||||
|
if (Match) {
|
||||||
|
Properties.push({ Key: Match[1], Value: Match[2].trim(), Line: Index });
|
||||||
|
} else if (Line.trim().startsWith("- ") && Properties.length > 0) {
|
||||||
|
// A list under the previous key ("Tags:" then "- one", "- two").
|
||||||
|
const Last = Properties[Properties.length - 1];
|
||||||
|
Last.List = [...(Last.List || []), Line.trim().slice(2).trim()];
|
||||||
|
} else if (Line.trim() !== "") {
|
||||||
|
// Not properties after all: a note that just starts with a --- rule.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const EndOffset = Lines.slice(0, EndLine + 1).join("\n").length;
|
||||||
|
return { Properties, EndLine, EndOffset };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IsBoolean(Value) {
|
||||||
|
return /^(true|false)$/i.test(Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The note without its properties block (what Markdown renders).
|
||||||
|
export function WithoutProperties(Text) {
|
||||||
|
const Parsed = ParseProperties(Text);
|
||||||
|
return Parsed ? Text.slice(Parsed.EndOffset).replace(/^\n/, "") : Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flips a true/false property, or a task's [ ]/[x], on one line of Text.
|
||||||
|
export function ToggleLine(Text, LineIndex) {
|
||||||
|
const Lines = Text.split("\n");
|
||||||
|
const Line = Lines[LineIndex];
|
||||||
|
if (Line === undefined) return Text;
|
||||||
|
const Task = TaskLine.exec(Line);
|
||||||
|
if (Task) {
|
||||||
|
const Mark = Task[2] === " " ? "x" : " ";
|
||||||
|
Lines[LineIndex] = `${Task[1]}[${Mark}]${Line.slice(Task[0].length)}`;
|
||||||
|
} else {
|
||||||
|
Lines[LineIndex] = Line.replace(/:\s*(true|false)\s*$/i, (Whole, Value) =>
|
||||||
|
Whole.replace(Value, Value.toLowerCase() === "true" ? "false" : "true"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The lines of the note's task items, in order, outside code blocks and the
|
||||||
|
// properties block: the nth checkbox the rendered view shows is TaskLines[n].
|
||||||
|
export function TaskLines(Text) {
|
||||||
|
const Lines = Text.split("\n");
|
||||||
|
const Parsed = ParseProperties(Text);
|
||||||
|
const Found = [];
|
||||||
|
let Fence = null;
|
||||||
|
for (let Index = Parsed ? Parsed.EndLine + 1 : 0; Index < Lines.length; Index++) {
|
||||||
|
const Line = Lines[Index];
|
||||||
|
const FenceMatch = FenceLine.exec(Line);
|
||||||
|
if (FenceMatch) {
|
||||||
|
if (Fence === null) Fence = FenceMatch[1];
|
||||||
|
else if (FenceMatch[1][0] === Fence[0] && FenceMatch[1].length >= Fence.length) Fence = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Fence === null && TaskLine.test(Line)) Found.push(Index);
|
||||||
|
}
|
||||||
|
return Found;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rendered view's panel. RenderInline renders a value's Markdown (links,
|
||||||
|
// bold); Escape escapes text. Checkboxes carry their line in data-line.
|
||||||
|
export function PropertiesHtml(Parsed, RenderInline, Escape, CanEdit) {
|
||||||
|
const Rows = Parsed.Properties.map(({ Key, Value, Line, List }) => {
|
||||||
|
let Shown;
|
||||||
|
if (IsBoolean(Value)) {
|
||||||
|
const Checked = Value.toLowerCase() === "true" ? " checked" : "";
|
||||||
|
const Disabled = CanEdit ? "" : " disabled";
|
||||||
|
Shown = `<input type="checkbox" class="PropertyCheckbox" data-line="${Line}"${Checked}${Disabled}>`;
|
||||||
|
} else if (List) {
|
||||||
|
Shown = List.map((Item) => `<span class="PropertyChip">${RenderInline(Item)}</span>`).join("");
|
||||||
|
} else {
|
||||||
|
Shown = Value ? RenderInline(Value) : `<span class="PropertyEmpty">Empty</span>`;
|
||||||
|
}
|
||||||
|
return `<div class="PropertyRow"><span class="PropertyKey">${Escape(Key)}</span><span class="PropertyValue">${Shown}</span></div>`;
|
||||||
|
});
|
||||||
|
return `<div class="Properties"><div class="PropertiesTitle">Properties</div>${Rows.join("")}</div>`;
|
||||||
|
}
|
||||||
@@ -510,6 +510,70 @@ img,
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Properties {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--Surface);
|
||||||
|
border: 1px solid var(--Border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertiesTitle {
|
||||||
|
color: var(--TextMuted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertyRow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertyKey {
|
||||||
|
flex: 0 0 30%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--TextMuted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertyValue {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertyChip {
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--SurfaceAlt);
|
||||||
|
color: var(--Purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertyEmpty {
|
||||||
|
color: var(--TextMuted);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.PropertyCheckbox {
|
||||||
|
accent-color: var(--Purple);
|
||||||
|
margin: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LiveEditor .cm-LiveProperties {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.LiveEditor .cm-LiveFrontmatter {
|
||||||
|
color: var(--TextMuted);
|
||||||
|
}
|
||||||
|
|
||||||
.MarkdownBody blockquote {
|
.MarkdownBody blockquote {
|
||||||
margin: 12px 0;
|
margin: 12px 0;
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
|
|||||||
Reference in new issue
Block a user