@memorysmithapp/markdown-renderer 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/NoteActions.d.ts +21 -2
- package/dist/NoteActions.js +14 -7
- package/dist/NoteBody.d.ts +17 -2
- package/dist/NoteBody.js +9 -8
- package/dist/edit/NoteEditor.d.ts +7 -1
- package/dist/edit/NoteEditor.js +5 -4
- package/dist/edit/blocks.d.ts +12 -0
- package/dist/edit/blocks.js +106 -0
- package/dist/edit/heights.d.ts +68 -0
- package/dist/edit/heights.js +162 -0
- package/dist/edit/image-size.d.ts +36 -0
- package/dist/edit/image-size.js +61 -0
- package/dist/edit/live.d.ts +9 -2
- package/dist/edit/live.js +415 -64
- package/dist/host.d.ts +16 -2
- package/dist/index.d.ts +1 -0
- package/dist/mode-icons.d.ts +1 -0
- package/dist/mode-icons.js +2 -0
- package/dist/strings.d.ts +8 -0
- package/dist/strings.js +4 -0
- package/dist/theme.d.ts +11 -3
- package/dist/theme.js +1 -0
- package/package.json +2 -2
- package/structure.css +69 -2
- package/theme-default.css +65 -2
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ import 'katex/dist/katex.min.css';
|
|
|
20
20
|
resolve={(name) => addressOfTheOneNoteNamed(name)} // or null: the link is pending
|
|
21
21
|
keeps={(name) => notebookKeepsAFile(name)} // only an embed reaches a file
|
|
22
22
|
fileOf={(name) => describe(name)} // { mimeType, bytes, url, description }: drawn by its type, as MemorySmith does
|
|
23
|
-
renderLink={({ href, className, children }) => <
|
|
23
|
+
renderLink={({ href, className, children }) => <a href={href} className={className} onClick={follow(href)}>{children}</a>} // follow: your router's navigate, captured where it is
|
|
24
24
|
loadNote={(name) => fetchTheBodyOf(name)} // for an embedded note; the package never fetches
|
|
25
25
|
writable
|
|
26
26
|
onToggleTask={(ordinal) => save(toggleTaskAt(latest(), ordinal))} // a promise locks the item under "Updating…" until it settles
|
|
@@ -29,7 +29,7 @@ import 'katex/dist/katex.min.css';
|
|
|
29
29
|
/>;
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Every prop is optional, and every default is the safe one: with no `resolve` every link is pending, with no `keeps` no embed reaches a file. A file `fileOf` describes is drawn by its media type, never its name: an image, an audio or a video player, and anything else a card with its type, size, description, *Open* where a browser shows it and *Download*; give `renderAttachment` instead to draw files yourself. The stylesheet styles nothing outside `.markdown`. The faces of the default theme are Inter and Space Grotesk, which the page loads.
|
|
32
|
+
Every prop is optional, and every default is the safe one: with no `resolve` every link is pending, with no `keeps` no embed reaches a file. A file `fileOf` describes is drawn by its media type, never its name: an image, an audio or a video player, and anything else a card with its type, size, description, *Open* where a browser shows it and *Download*; give `renderAttachment` instead to draw files yourself. In edit mode, `renderLink`, `renderAttachment` and a theme's slots may be drawn in a root of their own, outside your React tree, so what they return must not read your context: a router's `<Link>` throws there, and the block shows its Markdown under a notice instead. The stylesheet styles nothing outside `.markdown`. The faces of the default theme are Inter and Space Grotesk, which the page loads.
|
|
33
33
|
|
|
34
34
|
## Themes
|
|
35
35
|
|
|
@@ -86,7 +86,7 @@ A note the host makes `editable` carries a button at its top right that switches
|
|
|
86
86
|
suggest={(typed) => namesOfNotesLike(typed)} // what [[ offers
|
|
87
87
|
attach={(file, { name, description }, progress) => keepInNotebook(file, name, description, progress)} // the name it is kept under
|
|
88
88
|
files={() => filesOfTheNotebook()} // what the attach dialog lists: { name, mimeType, bytes, url, description }
|
|
89
|
-
|
|
89
|
+
buttons="responsive" // the corner's buttons and the dialog's Copy: their word on a wide note; "label" always; "icon", the default, never
|
|
90
90
|
/>
|
|
91
91
|
```
|
|
92
92
|
|
package/dist/NoteActions.d.ts
CHANGED
|
@@ -3,7 +3,26 @@
|
|
|
3
3
|
* shows the note's Markdown in a dialog (#63), and one that copies it. Apart from the editor, so a
|
|
4
4
|
* note that is only read offers them without loading it.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { type ReactNode } from 'react';
|
|
7
|
+
/**
|
|
8
|
+
* How the buttons a note draws around its text show their word (#96): `icon`, the icon alone;
|
|
9
|
+
* `label`, the icon and the word; `responsive`, the word beside the icon on a note at least
|
|
10
|
+
* `--markdown-button-words-from` wide and hidden on a narrower one. Wherever the word may be
|
|
11
|
+
* hidden it is the button's name.
|
|
12
|
+
*/
|
|
13
|
+
export type NoteButtons = 'icon' | 'label' | 'responsive';
|
|
14
|
+
/** A button's icon and, unless it is drawn alone, its word, which the stylesheet may hide. */
|
|
15
|
+
export declare function ButtonFace({ icon, word, display, wordClass }: {
|
|
16
|
+
icon: ReactNode;
|
|
17
|
+
word: string;
|
|
18
|
+
display: NoteButtons;
|
|
19
|
+
wordClass?: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* Show Markdown and Copy Markdown; `text` answers the bytes when a button is used, and `display`
|
|
23
|
+
* how they and the dialog's Copy show their word.
|
|
24
|
+
*/
|
|
25
|
+
export declare function NoteActions({ text, display }: {
|
|
8
26
|
text: () => string;
|
|
27
|
+
display?: NoteButtons;
|
|
9
28
|
}): import("react").JSX.Element;
|
package/dist/NoteActions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* What stands beside the switch between reading and editing, in both modes (#76): a button that
|
|
4
4
|
* shows the note's Markdown in a dialog (#63), and one that copies it. Apart from the editor, so a
|
|
@@ -7,6 +7,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
import { useEffect, useRef, useState } from 'react';
|
|
8
8
|
import { useHost } from './host.js';
|
|
9
9
|
import { ICON_CHECK, ICON_CLOSE, ICON_COPY, ICON_MARKDOWN } from './mode-icons.js';
|
|
10
|
+
/** A button's icon and, unless it is drawn alone, its word, which the stylesheet may hide. */
|
|
11
|
+
export function ButtonFace({ icon, word, display, wordClass }) {
|
|
12
|
+
return (_jsxs(_Fragment, { children: [icon, display !== 'icon' && _jsx("span", { className: wordClass ? `note-button-word ${wordClass}` : 'note-button-word', children: word })] }));
|
|
13
|
+
}
|
|
10
14
|
/** Puts the text on the clipboard; answers whether it got there. */
|
|
11
15
|
function copyText(text) {
|
|
12
16
|
const clipboard = typeof navigator === 'undefined' ? undefined : navigator.clipboard;
|
|
@@ -19,7 +23,7 @@ function copyText(text) {
|
|
|
19
23
|
* closed by Escape, its button or a click outside it, the focus then back on the button that
|
|
20
24
|
* opened it.
|
|
21
25
|
*/
|
|
22
|
-
function SourceDialog({ text, onClose, onClosed }) {
|
|
26
|
+
function SourceDialog({ text, display, onClose, onClosed }) {
|
|
23
27
|
const host = useHost();
|
|
24
28
|
const words = host.strings.editor;
|
|
25
29
|
const dialog = useRef(null);
|
|
@@ -42,17 +46,20 @@ function SourceDialog({ text, onClose, onClosed }) {
|
|
|
42
46
|
closed.current();
|
|
43
47
|
};
|
|
44
48
|
}, []);
|
|
45
|
-
return (_jsx("dialog", { ref: dialog, className:
|
|
49
|
+
return (_jsx("dialog", { ref: dialog, className: `edit-source${display === 'responsive' ? ' has-responsive-buttons' : ''}`, "aria-modal": "true", "aria-labelledby": "edit-source-title", onCancel: (event) => {
|
|
46
50
|
event.preventDefault();
|
|
47
51
|
onClose();
|
|
48
52
|
},
|
|
49
53
|
// The dialog has no padding of its own, so a click on it and not on its content is outside.
|
|
50
|
-
onClick: (event) => event.target === dialog.current && onClose(), children: _jsxs("div", { className: "edit-source-frame", children: [_jsxs("header", { className: "edit-source-header", children: [_jsx("h2", { id: "edit-source-title", className: "edit-source-title", children: words.sourceTitle }), _jsx("span", { className: "edit-source-copied", role: "status", children: copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : '' }), _jsx("button", { type: "button", className:
|
|
54
|
+
onClick: (event) => event.target === dialog.current && onClose(), children: _jsxs("div", { className: "edit-source-frame", children: [_jsxs("header", { className: "edit-source-header", children: [_jsx("h2", { id: "edit-source-title", className: "edit-source-title", children: words.sourceTitle }), _jsx("span", { className: "edit-source-copied", role: "status", children: copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : '' }), _jsx("button", { type: "button", className: `edit-control edit-source-copy${display === 'label' ? ' edit-control-labelled' : ''}`, "data-buttons": display, title: words.copy, "aria-label": display === 'label' ? undefined : words.copy, onClick: () => void copyText(text).then((done) => setCopied(done ? 'copied' : 'failed')), children: _jsx(ButtonFace, { icon: copied === 'copied' ? ICON_CHECK : ICON_COPY, word: words.copy, display: display }) }), _jsx("button", { type: "button", className: "edit-control edit-source-close", "aria-label": host.strings.close, title: host.strings.close, onClick: onClose, children: ICON_CLOSE })] }), _jsx("pre", { className: "edit-source-text", tabIndex: 0, children: text })] }) }));
|
|
51
55
|
}
|
|
52
56
|
/** How long Copy Markdown shows its check, in milliseconds. */
|
|
53
57
|
const COPIED_MS = 1600;
|
|
54
|
-
/**
|
|
55
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Show Markdown and Copy Markdown; `text` answers the bytes when a button is used, and `display`
|
|
60
|
+
* how they and the dialog's Copy show their word.
|
|
61
|
+
*/
|
|
62
|
+
export function NoteActions({ text, display = 'icon' }) {
|
|
56
63
|
const words = useHost().strings.editor;
|
|
57
64
|
const show = useRef(null);
|
|
58
65
|
const [source, setSource] = useState(null);
|
|
@@ -64,5 +71,5 @@ export function NoteActions({ text }) {
|
|
|
64
71
|
return () => clearTimeout(timer);
|
|
65
72
|
}, [copied]);
|
|
66
73
|
const copyLabel = copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : words.copyMarkdown;
|
|
67
|
-
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: show, type: "button", className:
|
|
74
|
+
return (_jsxs(_Fragment, { children: [_jsx("button", { ref: show, type: "button", className: `note-action note-action-${display}`, "data-buttons": display, title: words.showMarkdown, "aria-label": display === 'label' ? undefined : words.showMarkdown, "aria-haspopup": "dialog", onClick: () => setSource(text()), children: _jsx(ButtonFace, { icon: ICON_MARKDOWN, word: words.showMarkdown, display: display }) }), _jsx("button", { type: "button", className: `note-action note-action-${display}`, "data-buttons": display, "data-copied": copied ?? undefined, title: copyLabel, "aria-label": words.copyMarkdown, onClick: () => void copyText(text()).then((done) => setCopied(done ? 'copied' : 'failed')), children: _jsx(ButtonFace, { icon: copied === 'copied' ? ICON_CHECK : ICON_COPY, word: words.copyMarkdown, display: display }) }), _jsx("span", { className: "visually-hidden", role: "status", children: copied === 'copied' ? words.copied : copied === 'failed' ? words.copyFailed : '' }), source !== null && _jsx(SourceDialog, { text: source, display: display, onClose: () => setSource(null), onClosed: () => show.current?.focus() })] }));
|
|
68
75
|
}
|
package/dist/NoteBody.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type NoteButtons } from './NoteActions.js';
|
|
1
2
|
import { type NoteHost } from './host.js';
|
|
2
3
|
export interface NoteBodyProps extends NoteHost {
|
|
3
4
|
/**
|
|
@@ -25,7 +26,21 @@ export interface NoteBodyProps extends NoteHost {
|
|
|
25
26
|
* a dialog with the host's words and its actions (#86).
|
|
26
27
|
*/
|
|
27
28
|
readonly onChange?: (markdown: string) => unknown;
|
|
28
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* How every button the note draws around its text shows its word — Show Markdown, Copy Markdown,
|
|
31
|
+
* the switch, and Copy in the dialog of Show Markdown (#96): `icon`, the icon alone; `label`, the
|
|
32
|
+
* icon and the word; `responsive`, the word beside the icon on a note at least
|
|
33
|
+
* `--markdown-button-words-from` wide (560px by default) and hidden on a narrower one. Wherever
|
|
34
|
+
* the word is not drawn, or may be hidden, it is the button's name and its title. Defaults to
|
|
35
|
+
* `icon`, or to `modeButton` when only that is given.
|
|
36
|
+
*/
|
|
37
|
+
readonly buttons?: NoteButtons;
|
|
38
|
+
/**
|
|
39
|
+
* The switch alone as its icon or its icon and its word (#92).
|
|
40
|
+
*
|
|
41
|
+
* @deprecated Use `buttons`, which governs the switch and the buttons beside it; this is read
|
|
42
|
+
* only when `buttons` is left out.
|
|
43
|
+
*/
|
|
29
44
|
readonly modeButton?: 'icon' | 'label';
|
|
30
45
|
/**
|
|
31
46
|
* Whether the note offers Show Markdown and Copy Markdown at its top right, editable or not:
|
|
@@ -42,4 +57,4 @@ export type NoteMode = 'view' | 'edit';
|
|
|
42
57
|
* It renders inside `.markdown`, so the stylesheet of the package applies, and asks the host for
|
|
43
58
|
* every piece of notebook data through the props of `NoteHost`.
|
|
44
59
|
*/
|
|
45
|
-
export declare function NoteBody({ editable, mode, defaultMode, onModeChange, onChange, modeButton, actions, ...rest }: NoteBodyProps): import("react").JSX.Element;
|
|
60
|
+
export declare function NoteBody({ editable, mode, defaultMode, onModeChange, onChange, buttons, modeButton, actions, ...rest }: NoteBodyProps): import("react").JSX.Element;
|
package/dist/NoteBody.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { lazy, Suspense, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { ICON_EDIT, ICON_VIEW } from './mode-icons.js';
|
|
4
|
-
import { NoteActions } from './NoteActions.js';
|
|
4
|
+
import { ButtonFace, NoteActions } from './NoteActions.js';
|
|
5
5
|
import { drawSlot } from './slots.js';
|
|
6
6
|
import { HOST_KEYS, HostProvider, resolveHost, useHost } from './host.js';
|
|
7
7
|
import { planFootnotes } from './footnotes.js';
|
|
@@ -26,7 +26,7 @@ function ModeSwitch({ mode, display, toggle }) {
|
|
|
26
26
|
const icon = mode === 'view' ? ICON_EDIT : ICON_VIEW;
|
|
27
27
|
if (slot)
|
|
28
28
|
return _jsx(_Fragment, { children: drawSlot(slot, { mode, label, icon, display, toggle }) });
|
|
29
|
-
return (
|
|
29
|
+
return (_jsx("button", { type: "button", className: `mode-switch mode-switch-${display}`, "data-mode": mode, "data-buttons": display, title: label, "aria-label": display === 'label' ? undefined : label, onClick: toggle, children: _jsx(ButtonFace, { icon: icon, word: label, display: display, wordClass: "mode-switch-label" }) }));
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* The body of a note, drawn as MemorySmith's reading page draws it.
|
|
@@ -34,13 +34,14 @@ function ModeSwitch({ mode, display, toggle }) {
|
|
|
34
34
|
* It renders inside `.markdown`, so the stylesheet of the package applies, and asks the host for
|
|
35
35
|
* every piece of notebook data through the props of `NoteHost`.
|
|
36
36
|
*/
|
|
37
|
-
export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeChange, onChange, modeButton = '
|
|
37
|
+
export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeChange, onChange, buttons, modeButton = 'icon', actions = true, ...rest }) {
|
|
38
38
|
const [own, setOwn] = useState(defaultMode);
|
|
39
39
|
// The author asked to go back to reading, and the editor is finishing: a save, the toolbar folding.
|
|
40
40
|
const [leaving, setLeaving] = useState(false);
|
|
41
41
|
// The last text handed to the host in edit mode, which Show and Copy Markdown give there (#76).
|
|
42
42
|
const handed = useRef(null);
|
|
43
43
|
const current = editable ? (mode ?? own) : 'view';
|
|
44
|
+
const display = buttons ?? modeButton;
|
|
44
45
|
if (!editable && !actions)
|
|
45
46
|
return _jsx(NoteView, { ...rest });
|
|
46
47
|
const go = (next) => {
|
|
@@ -69,12 +70,12 @@ export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeC
|
|
|
69
70
|
return (_jsx(HostOnly, { host: rest, children: (resolved) => {
|
|
70
71
|
// Show and Copy Markdown for whoever reads the note, in both modes (#76, #87), and the
|
|
71
72
|
// switch only where the note is editable.
|
|
72
|
-
const corner = (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: "note-corner", children: [actions && _jsx(NoteActions, { text: () => (current === 'edit' ? (handed.current ?? rest.markdown) : rest.markdown) }), editable && _jsx(ModeSwitch, { mode: current, display:
|
|
73
|
+
const corner = (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: "note-corner", children: [actions && _jsx(NoteActions, { display: display, text: () => (current === 'edit' ? (handed.current ?? rest.markdown) : rest.markdown) }), editable && _jsx(ModeSwitch, { mode: current, display: display, toggle: toggle })] }) }));
|
|
73
74
|
if (current === 'view')
|
|
74
|
-
return _jsx(NoteView, { ...rest, corner: corner, switched: editable });
|
|
75
|
+
return _jsx(NoteView, { ...rest, corner: corner, switched: editable, buttons: display });
|
|
75
76
|
return (
|
|
76
77
|
// While the editor loads, the note stays on the page as it was.
|
|
77
|
-
_jsx(Suspense, { fallback: _jsx(NoteView, { ...rest, corner: corner, switched: true }), children: _jsx(NoteEditor, { ...host, onChange: hand, corner: corner, autoFocus: true, leaving: leaving, onLeave: onLeave }) }));
|
|
78
|
+
_jsx(Suspense, { fallback: _jsx(NoteView, { ...rest, corner: corner, switched: true, buttons: display }), children: _jsx(NoteEditor, { ...host, onChange: hand, corner: corner, buttons: display, autoFocus: true, leaving: leaving, onLeave: onLeave }) }));
|
|
78
79
|
} }));
|
|
79
80
|
}
|
|
80
81
|
/** The host resolved once, for what is drawn around the note. */
|
|
@@ -83,7 +84,7 @@ function HostOnly({ host, children }) {
|
|
|
83
84
|
return _jsx(_Fragment, { children: children(resolved) });
|
|
84
85
|
}
|
|
85
86
|
/** The note drawn for reading: the page MemorySmith's reading page draws. */
|
|
86
|
-
function NoteView({ markdown, writable = false, onToggleTask, corner, switched = false, ...host }) {
|
|
87
|
+
function NoteView({ markdown, writable = false, onToggleTask, corner, switched = false, buttons = 'icon', ...host }) {
|
|
87
88
|
const resolved = useMemo(() => resolveHost(host),
|
|
88
89
|
// Every member of the contract, so a host that passes the same functions keeps the same host.
|
|
89
90
|
HOST_KEYS.map((key) => host[key]));
|
|
@@ -147,7 +148,7 @@ function NoteView({ markdown, writable = false, onToggleTask, corner, switched =
|
|
|
147
148
|
// The runs and whether an embed cut them are the whole input.
|
|
148
149
|
[runs.join('\u0000'), runs.length < segments.length]);
|
|
149
150
|
let run = 0;
|
|
150
|
-
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name}${corner ? ' has-note-corner' : ''}${switched ? ' has-mode-switch' : ''}`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [corner, _jsx(Properties, { properties: properties }), segments.map((segment, index) => {
|
|
151
|
+
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name}${corner ? ' has-note-corner' : ''}${switched ? ' has-mode-switch' : ''}${corner && buttons === 'responsive' ? ' has-responsive-buttons' : ''}`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [corner, _jsx(Properties, { properties: properties }), segments.map((segment, index) => {
|
|
151
152
|
if (!('rendered' in segment)) {
|
|
152
153
|
return _jsx(Transclusion, { target: segment.target, anchor: segment.anchor }, index);
|
|
153
154
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type ReactNode } from 'react';
|
|
11
11
|
import { type NoteHost } from '../host.js';
|
|
12
|
+
import { type NoteButtons } from '../NoteActions.js';
|
|
12
13
|
export interface NoteEditorProps extends Omit<NoteHost, 'writable' | 'onToggleTask'> {
|
|
13
14
|
/** The note as the host keeps it, frontmatter included. A new value that differs from the editor's is taken in. */
|
|
14
15
|
readonly markdown: string;
|
|
@@ -22,6 +23,11 @@ export interface NoteEditorProps extends Omit<NoteHost, 'writable' | 'onToggleTa
|
|
|
22
23
|
readonly editable?: boolean;
|
|
23
24
|
/** What sits at the top right of the note, such as the switch back to reading. */
|
|
24
25
|
readonly corner?: ReactNode;
|
|
26
|
+
/**
|
|
27
|
+
* How the buttons at the top right show their word, as `NoteBody`'s `buttons` (#96): the bar is
|
|
28
|
+
* what a `responsive` button measures. Defaults to `icon`.
|
|
29
|
+
*/
|
|
30
|
+
readonly buttons?: NoteButtons;
|
|
25
31
|
/** Whether the editor takes the focus when it opens. */
|
|
26
32
|
readonly autoFocus?: boolean;
|
|
27
33
|
/**
|
|
@@ -41,4 +47,4 @@ export declare function lineBreakOf(text: string): '\n' | '\r\n' | null;
|
|
|
41
47
|
* Edits a note on the page that draws it. Exported at `@memorysmithapp/markdown-renderer/edit`
|
|
42
48
|
* for a host that edits without the switch of `NoteBody`; `NoteBody` loads it on demand.
|
|
43
49
|
*/
|
|
44
|
-
export declare function NoteEditor({ markdown: given, onChange, editable, corner, autoFocus, leaving, onLeave, ...host }: NoteEditorProps): import("react").JSX.Element;
|
|
50
|
+
export declare function NoteEditor({ markdown: given, onChange, editable, corner, buttons, autoFocus, leaving, onLeave, ...host }: NoteEditorProps): import("react").JSX.Element;
|
package/dist/edit/NoteEditor.js
CHANGED
|
@@ -19,6 +19,7 @@ import { rootStyle } from '../theme.js';
|
|
|
19
19
|
import { ICONS } from './blocks.js';
|
|
20
20
|
import { BLOCKS, command, headingLevel, insertBlock, insertFootnote, insertLink, insertWikilink, setHeading, toggleComment, toggleList, toggleMark, toggleQuote, } from './commands.js';
|
|
21
21
|
import { analysisField, editorEnvironment, livePage } from './live.js';
|
|
22
|
+
import { measureMetrics } from './heights.js';
|
|
22
23
|
import { NoteActions } from '../NoteActions.js';
|
|
23
24
|
import { AttachDialog, webImage } from './AttachDialog.js';
|
|
24
25
|
import { ContextMenu } from './ContextMenu.js';
|
|
@@ -170,7 +171,7 @@ function SaveStatus({ saves, blocked, host, onDecided }) {
|
|
|
170
171
|
* Edits a note on the page that draws it. Exported at `@memorysmithapp/markdown-renderer/edit`
|
|
171
172
|
* for a host that edits without the switch of `NoteBody`; `NoteBody` loads it on demand.
|
|
172
173
|
*/
|
|
173
|
-
export function NoteEditor({ markdown: given, onChange, editable = true, corner, autoFocus = false, leaving = false, onLeave, ...host }) {
|
|
174
|
+
export function NoteEditor({ markdown: given, onChange, editable = true, corner, buttons = 'icon', autoFocus = false, leaving = false, onLeave, ...host }) {
|
|
174
175
|
const resolved = useMemo(() => resolveHost(host),
|
|
175
176
|
// Every member of the contract, so a host that passes the same functions keeps the same host.
|
|
176
177
|
HOST_KEYS.map((key) => host[key]));
|
|
@@ -358,7 +359,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
358
359
|
markdown(),
|
|
359
360
|
EditorView.lineWrapping,
|
|
360
361
|
EditorView.contentAttributes.of({ 'aria-multiline': 'true', spellcheck: 'true' }),
|
|
361
|
-
livePage(),
|
|
362
|
+
livePage(measureMetrics(parent.current)),
|
|
362
363
|
autocompletion({ override: [wikilinkCompletion(() => latest.current.host)], icons: false }),
|
|
363
364
|
shortcuts(() => created),
|
|
364
365
|
keymap.of([...markdownKeymap, ...historyKeymap, indentWithTab, ...defaultKeymap]),
|
|
@@ -503,7 +504,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
503
504
|
clearTimeout(timer);
|
|
504
505
|
};
|
|
505
506
|
}, [leaving, saves.state]);
|
|
506
|
-
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name} markdown-editing`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [_jsx("div", { ref: bar, className:
|
|
507
|
+
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name} markdown-editing`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [_jsx("div", { ref: bar, className: `edit-bar${buttons === 'responsive' ? ' has-responsive-buttons' : ''}`, "data-leaving": folding ? '' : undefined, children: _jsx(Toolbar, { view: view, host: resolved, opened: opened.current, onAttach: () => {
|
|
507
508
|
// Opening the paperclip is the author acting on a refusal: the dialog says it once more.
|
|
508
509
|
if (attaching.state === 'failed') {
|
|
509
510
|
setRefusal(attaching.message);
|
|
@@ -512,7 +513,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
512
513
|
else
|
|
513
514
|
setRefusal(null);
|
|
514
515
|
setAttachOpen(true);
|
|
515
|
-
}, attaching: attaching, attachButton: attachButton, end: _jsxs("div", { className: "edit-corner", "aria-busy": leaving && saves.state === 'saving' ? true : undefined, "data-blocked": blocked ? '' : undefined, children: [_jsx(SaveStatus, { saves: saves, blocked: blocked, host: resolved, onDecided: () => view?.focus() }), corner ?? _jsx(NoteActions, { text: () => (view ? output(view.state) : given) })] }, blocked ? `blocked-${refusals}` : 'corner') }) }), menu && view && (_jsx(ContextMenu, { x: menu.x, y: menu.y, label: resolved.strings.editor.menu, groups: menuGroups(view), onClose: (back) => {
|
|
516
|
+
}, attaching: attaching, attachButton: attachButton, end: _jsxs("div", { className: "edit-corner", "aria-busy": leaving && saves.state === 'saving' ? true : undefined, "data-blocked": blocked ? '' : undefined, children: [_jsx(SaveStatus, { saves: saves, blocked: blocked, host: resolved, onDecided: () => view?.focus() }), corner ?? _jsx(NoteActions, { display: buttons, text: () => (view ? output(view.state) : given) })] }, blocked ? `blocked-${refusals}` : 'corner') }) }), menu && view && (_jsx(ContextMenu, { x: menu.x, y: menu.y, label: resolved.strings.editor.menu, groups: menuGroups(view), onClose: (back) => {
|
|
516
517
|
setMenu(null);
|
|
517
518
|
if (back)
|
|
518
519
|
view.focus();
|
package/dist/edit/blocks.d.ts
CHANGED
|
@@ -29,6 +29,18 @@ export declare function CalloutBlock({ view, text, marker }: {
|
|
|
29
29
|
fold: '+' | '-' | null;
|
|
30
30
|
};
|
|
31
31
|
}): import("react").JSX.Element;
|
|
32
|
+
/**
|
|
33
|
+
* An image drawn in edit mode with the size it is drawn at set in place (#98): a width in pixels,
|
|
34
|
+
* its height following, a handle at its corner that drags it, and a button back to its own size.
|
|
35
|
+
* Each writes the dimensions of its notation — `` or `![[photo.jpg|300]]` —
|
|
36
|
+
* and no other byte. The controls appear once an image is drawn inside it, so an embed of a file
|
|
37
|
+
* that is not an image, or a host's own drawing without one, offers none.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ImageBlock({ view, source, children }: {
|
|
40
|
+
view: EditorView;
|
|
41
|
+
source: string;
|
|
42
|
+
children?: ReactNode;
|
|
43
|
+
}): import("react").JSX.Element;
|
|
32
44
|
/**
|
|
33
45
|
* A table whose cells are edited where they stand. A cell commits its text when it loses the focus,
|
|
34
46
|
* or on Enter (to the cell below) and Tab (to the next one); each commit rewrites that cell's
|
package/dist/edit/blocks.js
CHANGED
|
@@ -17,6 +17,8 @@ import { isTrue, readProperties } from '../properties.js';
|
|
|
17
17
|
import { Transclusion } from '../Transclusion.js';
|
|
18
18
|
import { splitEmbeds } from '../transclusion.js';
|
|
19
19
|
import { wikilinkAddress } from '../wikilink-address.js';
|
|
20
|
+
import { ICON_OWN_SIZE } from '../mode-icons.js';
|
|
21
|
+
import { MAX_IMAGE_WIDTH, readImageSize, resizeImage } from './image-size.js';
|
|
20
22
|
import { addEntry, readFrontmatter, removeEntry, renameKey, setList, setScalar, } from './frontmatter.js';
|
|
21
23
|
import { addColumn, addRow, alignColumn, columnCount, readTable, removeColumn, removeRow, setCell, } from './table.js';
|
|
22
24
|
/** Whether a block's edit is being applied: the block is drawn anew, and the field it held lets the focus go. */
|
|
@@ -71,6 +73,110 @@ export function CalloutBlock({ view, text, marker }) {
|
|
|
71
73
|
const rewrite = (type, fold) => write(view, [{ from: marker.from, to: marker.to, insert: `[!${type}]${fold ?? ''}` }]);
|
|
72
74
|
return (_jsxs("div", { className: "edit-block edit-callout", children: [_jsxs("div", { className: "edit-block-controls", "data-ms-control": "", children: [_jsx("select", { "aria-label": words.calloutType, title: words.calloutType, value: marker.type, onChange: (event) => rewrite(event.target.value, marker.fold), children: types.map((type) => (_jsx("option", { value: type, children: strings.callouts[type] ?? type }, type))) }), _jsxs("select", { "aria-label": words.fold, title: words.fold, value: marker.fold ?? '', onChange: (event) => rewrite(marker.type, (event.target.value || null)), children: [_jsx("option", { value: "", children: words.foldNone }), _jsx("option", { value: "+", children: words.foldOpen }), _jsx("option", { value: "-", children: words.foldClosed })] })] }), _jsx(Snippet, { text: text })] }));
|
|
73
75
|
}
|
|
76
|
+
// ── Images ───────────────────────────────────────────────────────────────────
|
|
77
|
+
/** The narrowest an image is dragged to, so its handle stays within reach. */
|
|
78
|
+
const MIN_DRAGGED_WIDTH = 24;
|
|
79
|
+
/**
|
|
80
|
+
* An image drawn in edit mode with the size it is drawn at set in place (#98): a width in pixels,
|
|
81
|
+
* its height following, a handle at its corner that drags it, and a button back to its own size.
|
|
82
|
+
* Each writes the dimensions of its notation — `` or `![[photo.jpg|300]]` —
|
|
83
|
+
* and no other byte. The controls appear once an image is drawn inside it, so an embed of a file
|
|
84
|
+
* that is not an image, or a host's own drawing without one, offers none.
|
|
85
|
+
*/
|
|
86
|
+
export function ImageBlock({ view, source, children }) {
|
|
87
|
+
const words = useHost().strings.editor;
|
|
88
|
+
const frame = useRef(null);
|
|
89
|
+
const size = readImageSize(source);
|
|
90
|
+
const [image, setImage] = useState(null);
|
|
91
|
+
const [natural, setNatural] = useState(null);
|
|
92
|
+
const [typed, setTyped] = useState(null);
|
|
93
|
+
const [dragged, setDragged] = useState(null);
|
|
94
|
+
const drag = useRef(null);
|
|
95
|
+
// An embed's image arrives when the host answers; the controls wait for it.
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
const element = frame.current;
|
|
98
|
+
if (!element)
|
|
99
|
+
return;
|
|
100
|
+
const look = () => setImage(element.querySelector('img'));
|
|
101
|
+
look();
|
|
102
|
+
if (typeof MutationObserver === 'undefined')
|
|
103
|
+
return;
|
|
104
|
+
const observer = new MutationObserver(look);
|
|
105
|
+
observer.observe(element, { childList: true, subtree: true });
|
|
106
|
+
return () => observer.disconnect();
|
|
107
|
+
}, []);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!image)
|
|
110
|
+
return;
|
|
111
|
+
const read = () => setNatural(image.naturalWidth || null);
|
|
112
|
+
read();
|
|
113
|
+
image.addEventListener('load', read);
|
|
114
|
+
return () => image.removeEventListener('load', read);
|
|
115
|
+
}, [image]);
|
|
116
|
+
const resize = (width) => {
|
|
117
|
+
const widget = frame.current?.closest('.cm-ms-inline-block, .cm-ms-block');
|
|
118
|
+
if (!widget)
|
|
119
|
+
return;
|
|
120
|
+
const from = view.posAtDOM(widget);
|
|
121
|
+
// The widget stands where its source was: anything else means the note moved under it.
|
|
122
|
+
if (view.state.sliceDoc(from, from + source.length) !== source)
|
|
123
|
+
return;
|
|
124
|
+
write(view, resizeImage(source, from, width));
|
|
125
|
+
};
|
|
126
|
+
const commitTyped = () => {
|
|
127
|
+
if (typed === null)
|
|
128
|
+
return;
|
|
129
|
+
const width = Number(typed.trim());
|
|
130
|
+
setTyped(null);
|
|
131
|
+
if (typed.trim() === '')
|
|
132
|
+
resize(null);
|
|
133
|
+
else if (Number.isFinite(width) && width >= 1)
|
|
134
|
+
resize(width);
|
|
135
|
+
};
|
|
136
|
+
const onKeyDown = (event) => {
|
|
137
|
+
if (event.key === 'Enter') {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
commitTyped();
|
|
140
|
+
}
|
|
141
|
+
else if (event.key === 'Escape') {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
setTyped(null);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const shown = dragged ?? size?.width ?? null;
|
|
147
|
+
const offered = size !== null && image !== null;
|
|
148
|
+
return (_jsxs("span", { ref: frame, className: "edit-image", "data-resizing": dragged === null ? undefined : '', children: [children, offered && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "edit-block-controls edit-image-controls", "data-ms-control": "", children: [_jsx("input", { className: "edit-field edit-image-width", type: "number", inputMode: "numeric", min: 1, max: MAX_IMAGE_WIDTH, "aria-label": words.imageWidth, title: words.imageWidth, placeholder: natural === null ? '' : String(natural), value: typed ?? (shown === null ? '' : String(shown)), onChange: (event) => setTyped(event.target.value), onKeyDown: onKeyDown, onBlur: commitTyped }), _jsx("span", { className: "edit-image-unit", "aria-hidden": "true", children: "px" }), _jsx(Control, { label: words.imageOwnSize, onClick: () => resize(null), pressed: size.width === null, children: ICON_OWN_SIZE })] }), _jsx("span", { className: "edit-image-handle", "data-ms-control": "", title: words.imageResize, "aria-hidden": "true", onPointerDown: (event) => {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
151
|
+
drag.current = { x: event.clientX, width: image.getBoundingClientRect().width, pointer: event.pointerId };
|
|
152
|
+
setDragged(Math.round(drag.current.width));
|
|
153
|
+
}, onPointerMove: (event) => {
|
|
154
|
+
const start = drag.current;
|
|
155
|
+
if (!start || start.pointer !== event.pointerId)
|
|
156
|
+
return;
|
|
157
|
+
const width = Math.max(MIN_DRAGGED_WIDTH, Math.round(start.width + event.clientX - start.x));
|
|
158
|
+
image.style.width = `${width}px`;
|
|
159
|
+
image.style.height = 'auto';
|
|
160
|
+
// What the page lets it be: never wider than the column.
|
|
161
|
+
setDragged(Math.round(image.getBoundingClientRect().width));
|
|
162
|
+
}, onPointerUp: (event) => {
|
|
163
|
+
const start = drag.current;
|
|
164
|
+
if (!start || start.pointer !== event.pointerId)
|
|
165
|
+
return;
|
|
166
|
+
drag.current = null;
|
|
167
|
+
const width = Math.round(image.getBoundingClientRect().width);
|
|
168
|
+
setDragged(null);
|
|
169
|
+
image.style.width = '';
|
|
170
|
+
image.style.height = '';
|
|
171
|
+
if (Math.abs(width - start.width) >= 1)
|
|
172
|
+
resize(width);
|
|
173
|
+
}, onPointerCancel: () => {
|
|
174
|
+
drag.current = null;
|
|
175
|
+
setDragged(null);
|
|
176
|
+
image.style.width = '';
|
|
177
|
+
image.style.height = '';
|
|
178
|
+
} })] }))] }));
|
|
179
|
+
}
|
|
74
180
|
// ── Tables ───────────────────────────────────────────────────────────────────
|
|
75
181
|
/** Where the focus goes back to when a table is drawn again after an edit of one of its cells. */
|
|
76
182
|
let pendingFocus = null;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How tall a drawn block will be before it is drawn (#91).
|
|
3
|
+
*
|
|
4
|
+
* CodeMirror lays out the blocks it has not drawn yet by the height their widget estimates, and
|
|
5
|
+
* reads that estimate once, when the block's decoration is made; a block drawn taller than its
|
|
6
|
+
* estimate grows the document under the scrollbar as the author scrolls to it. Each block
|
|
7
|
+
* estimates its height from what it draws — its lines wrapped at the editor's width, its rows, its
|
|
8
|
+
* properties — and a block drawn once at a width is remembered at its measured height, so the same
|
|
9
|
+
* block made again, moved by an edit above it or in the next edit of the note, starts right.
|
|
10
|
+
*/
|
|
11
|
+
/** What an estimate reads of the editor: its text column, its line height and its characters. */
|
|
12
|
+
export interface BlockMetrics {
|
|
13
|
+
readonly width: number;
|
|
14
|
+
readonly lineHeight: number;
|
|
15
|
+
readonly charWidth: number;
|
|
16
|
+
/**
|
|
17
|
+
* Each level of heading as the theme draws it in edit mode, `h1` first, and a line of a code
|
|
18
|
+
* block: its line height, its characters, the space above it and the width its sides take (#94).
|
|
19
|
+
* Absent where nothing is laid out.
|
|
20
|
+
*/
|
|
21
|
+
readonly headings?: readonly LineMetrics[];
|
|
22
|
+
readonly code?: LineMetrics;
|
|
23
|
+
}
|
|
24
|
+
/** A kind of line as the theme draws it, which differs from the text's: a heading, a line of code. */
|
|
25
|
+
export interface LineMetrics {
|
|
26
|
+
readonly lineHeight: number;
|
|
27
|
+
readonly charWidth: number;
|
|
28
|
+
/** The space above it, on a heading. */
|
|
29
|
+
readonly padding: number;
|
|
30
|
+
/** The width its sides take from the column: a code block's padding and border. */
|
|
31
|
+
readonly inset: number;
|
|
32
|
+
}
|
|
33
|
+
/** What an estimate assumes before the editor has been measured, as in a test with no layout. */
|
|
34
|
+
export declare const DEFAULT_METRICS: BlockMetrics;
|
|
35
|
+
/**
|
|
36
|
+
* The metrics of a text column laid out in `parent`, read before the editor exists there, so the
|
|
37
|
+
* first estimates are made at the width the note is drawn at. Falls back to `DEFAULT_METRICS` where
|
|
38
|
+
* nothing is laid out.
|
|
39
|
+
*/
|
|
40
|
+
export declare function measureMetrics(parent: HTMLElement): BlockMetrics;
|
|
41
|
+
/**
|
|
42
|
+
* Each level of heading and a line of code laid out in `parent` as the editor draws them, by the
|
|
43
|
+
* classes of their line, so whatever the theme gives them — a face, a size, the space above, the
|
|
44
|
+
* sides of a code block — is what is measured (#94).
|
|
45
|
+
*/
|
|
46
|
+
export declare function measureLines(parent: HTMLElement): {
|
|
47
|
+
headings: LineMetrics[];
|
|
48
|
+
code: LineMetrics;
|
|
49
|
+
};
|
|
50
|
+
/** Metrics as measured, any that could not be read replaced by the default. */
|
|
51
|
+
export declare function withinReason(metrics: BlockMetrics): BlockMetrics;
|
|
52
|
+
/**
|
|
53
|
+
* The height of a line of the text the editor shows, `length` characters visible, as the page draws
|
|
54
|
+
* it where CodeMirror would guess it from the text's metrics alone (#94): a heading at its `level`,
|
|
55
|
+
* a line of code, or a line of text, which on a narrow column wraps sooner than CodeMirror counts.
|
|
56
|
+
* `null` where that kind of line was not measured.
|
|
57
|
+
*/
|
|
58
|
+
export declare function lineHeightOf(kind: 'text' | 'code' | {
|
|
59
|
+
readonly heading: number;
|
|
60
|
+
}, length: number, metrics: BlockMetrics): number | null;
|
|
61
|
+
/** What a block is, for its estimate. */
|
|
62
|
+
export type BlockKind = 'frontmatter' | 'callout' | 'table' | 'embed' | 'diagram' | 'preview' | 'add-properties' | 'image';
|
|
63
|
+
/** The height a block of this kind and these lines is drawn at, near enough to lay out the page. */
|
|
64
|
+
export declare function estimateHeight(kind: BlockKind, source: string, metrics: BlockMetrics): number;
|
|
65
|
+
/** The height a block of this content was drawn at at this width, if it was. */
|
|
66
|
+
export declare function measuredHeight(identity: string, metrics: BlockMetrics): number | undefined;
|
|
67
|
+
/** Remembers the height a block was drawn at. */
|
|
68
|
+
export declare function rememberHeight(identity: string, metrics: BlockMetrics, height: number): void;
|