@memorysmithapp/markdown-renderer 0.4.0 → 0.6.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 +24 -6
- package/dist/NoteBody.d.ts +16 -4
- package/dist/NoteBody.js +9 -8
- package/dist/edit/AttachDialog.d.ts +3 -1
- package/dist/edit/AttachDialog.js +2 -2
- package/dist/edit/NoteEditor.d.ts +2 -1
- package/dist/edit/NoteEditor.js +83 -23
- package/dist/edit/SaveNoticeDialog.d.ts +15 -0
- package/dist/edit/SaveNoticeDialog.js +41 -0
- package/dist/edit/blocks.d.ts +1 -0
- package/dist/edit/blocks.js +1 -0
- package/dist/edit/heights.d.ts +34 -0
- package/dist/edit/heights.js +101 -0
- package/dist/edit/index.d.ts +1 -0
- package/dist/edit/index.js +1 -0
- package/dist/edit/live.d.ts +7 -2
- package/dist/edit/live.js +150 -27
- package/dist/edit/saves.d.ts +32 -2
- package/dist/edit/saves.js +33 -12
- package/dist/host.d.ts +37 -3
- package/dist/host.js +8 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/strings.d.ts +4 -0
- package/dist/strings.js +2 -0
- package/dist/theme.d.ts +4 -1
- package/package.json +2 -2
- package/structure.css +37 -0
- package/theme-default.css +44 -3
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
|
|
|
@@ -76,7 +76,7 @@ Import `structure.css` instead of `styles.css` for a theme that draws everything
|
|
|
76
76
|
|
|
77
77
|
## Edit
|
|
78
78
|
|
|
79
|
-
A note the host makes `editable` carries a button at its top right that switches between reading and editing it on the same page. The author edits what they see: marks, headings, lists, tasks, links and the names of notes after `[[`, callouts, tables cell by cell, code, formulas and diagrams with their drawing live, the properties as a form, footnotes and comments. The note is the bytes you gave, and every change comes back whole through `onChange`, unchanged wherever the author did not touch it — the order and quoting of the frontmatter, a list's marker, a table's spacing, a comment, a line break. The package never saves. The toolbar grows out of the switch, which becomes its last control; its first tool undoes every change since editing began, and a right-click on the text opens a menu of the same tools where you are (Shift keeps the browser's); beside the switch, in both modes, Show Markdown opens the note's Markdown, as you have it or would receive it, in a dialog, and Copy Markdown copies it.
|
|
79
|
+
A note the host makes `editable` carries a button at its top right that switches between reading and editing it on the same page. The author edits what they see: marks, headings, lists, tasks, links and the names of notes after `[[`, callouts, tables cell by cell, code, formulas and diagrams with their drawing live, the properties as a form, footnotes and comments. The note is the bytes you gave, and every change comes back whole through `onChange`, unchanged wherever the author did not touch it — the order and quoting of the frontmatter, a list's marker, a table's spacing, a comment, a line break. The package never saves. The toolbar grows out of the switch, which becomes its last control; its first tool undoes every change since editing began, and a right-click on the text opens a menu of the same tools where you are (Shift keeps the browser's); beside the switch, in both modes, Show Markdown opens the note's Markdown, as you have it or would receive it, in a dialog, and Copy Markdown copies it. Those two are reading, so a note that is not `editable` carries them too, without the switch; pass `actions={false}` to hide them, for a print, a preview or an embed.
|
|
80
80
|
|
|
81
81
|
```tsx
|
|
82
82
|
<NoteBody
|
|
@@ -84,13 +84,31 @@ A note the host makes `editable` carries a button at its top right that switches
|
|
|
84
84
|
editable // the switch, at the top right
|
|
85
85
|
onChange={(next) => save(next)} // after every change; return the save's promise
|
|
86
86
|
suggest={(typed) => namesOfNotesLike(typed)} // what [[ offers
|
|
87
|
-
attach={(file, { name, description }) => keepInNotebook(file, name, description)} // the name it is kept under
|
|
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
|
-
modeButton="
|
|
89
|
+
modeButton="label" // the icon and its word; "icon", the default, the icon alone
|
|
90
90
|
/>
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
When `onChange` returns the promise of its save, the editor shows before the switch an icon for whether the last change is *Saving…*, *Saved* or *Not saved*, the last one's icon trying again when clicked, and going back to reading waits for the save, staying in edit when it failed. Return nothing and no status is shown.
|
|
93
|
+
When `onChange` returns the promise of its save, the editor shows before the switch an icon for whether the last change is *Saving…*, *Saved* or *Not saved*, the last one's icon trying again when clicked, and going back to reading waits for the save, staying in edit when it failed. Return nothing and no status is shown. When there is something to tell the author, resolve the promise with a `SaveOutcome` (the save went through, and they have a choice to make) or reject it with a `SaveRefusal` (it did not, and why): each is a `title`, a `message` and the `actions` to choose from, in your words. The icon then opens a dialog with them; an action's promise is followed like a save, and the author can close the dialog and come back to it:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
onChange={async (next) => {
|
|
97
|
+
const saved = await save(next);
|
|
98
|
+
if (saved.conflict) throw { title: 'Somebody else wrote this note', message: 'Your edit was not saved.', actions: [
|
|
99
|
+
{ label: 'Discard my edit and reload', run: () => reload() },
|
|
100
|
+
{ label: 'Save anyway', run: () => save(next, { force: true }) },
|
|
101
|
+
] } satisfies SaveRefusal;
|
|
102
|
+
if (saved.renamed) return { title: 'Note renamed', message: `${saved.links} links reached the old name.`, actions: [
|
|
103
|
+
{ label: 'Keep the links broken', run: () => undefined },
|
|
104
|
+
{ label: 'Keep the old name as an alias', run: () => addAlias() },
|
|
105
|
+
] } satisfies SaveOutcome;
|
|
106
|
+
}}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
While `attach` keeps a file, call `progress({ message, fraction })` with each step in your words — *Sending part 12 of 40*, and `0.3` — and the paperclip shows it, a bar filling when you give the fraction. Reject with `{ message }` (an `AttachRefusal`, not an `Error`) to tell the author why the file was refused; the paperclip says it until they open it again. An `Error` keeps the paperclip's own *The file was not attached*.
|
|
110
|
+
|
|
111
|
+
Pass the saved note back as `markdown` whenever you like: a text the editor already moved past is not applied over what the author typed since.
|
|
94
112
|
|
|
95
113
|
`mode` and `onModeChange` let you control the mode yourself, and the words come from `strings` (`edit`, `view` and `editor`). The editor is CodeMirror, loaded the first time a note is edited, so a page that only draws never loads it. To edit without the switch, import it from its own entry point:
|
|
96
114
|
|
package/dist/NoteBody.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface NoteBodyProps extends NoteHost {
|
|
|
7
7
|
readonly markdown: string;
|
|
8
8
|
/**
|
|
9
9
|
* Whether the note offers the switch to edit it, at its top right (#51). Defaults to `false`:
|
|
10
|
-
* no switch,
|
|
10
|
+
* no switch, the note only read, with Show and Copy Markdown as `actions` says.
|
|
11
11
|
*/
|
|
12
12
|
readonly editable?: boolean;
|
|
13
13
|
/** The mode, for a host that controls it; left out, the note keeps its own, starting from `defaultMode`. */
|
|
@@ -19,11 +19,23 @@ export interface NoteBodyProps extends NoteHost {
|
|
|
19
19
|
/**
|
|
20
20
|
* The note after each change made in edit mode, byte for byte the original wherever it was not
|
|
21
21
|
* edited. The package never saves it; a host that answers with the promise of its save has the
|
|
22
|
-
* editor say whether it was saved, and going back to reading wait for it (#64).
|
|
22
|
+
* editor say whether it was saved, and going back to reading wait for it (#64). The promise may
|
|
23
|
+
* resolve with a `SaveOutcome`, a save that succeeded with something the author must decide, or
|
|
24
|
+
* reject with a `SaveRefusal`, the reason it was refused: either opens, from the save's status,
|
|
25
|
+
* a dialog with the host's words and its actions (#86).
|
|
23
26
|
*/
|
|
24
27
|
readonly onChange?: (markdown: string) => unknown;
|
|
25
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Whether the switch shows its icon alone or its icon and its word. Defaults to `icon`, like Show
|
|
30
|
+
* Markdown and Copy Markdown beside it, its word then its name and its title (#92).
|
|
31
|
+
*/
|
|
26
32
|
readonly modeButton?: 'icon' | 'label';
|
|
33
|
+
/**
|
|
34
|
+
* Whether the note offers Show Markdown and Copy Markdown at its top right, editable or not:
|
|
35
|
+
* reading a note's Markdown and copying it are reading (#87). Defaults to `true`; `false` hides
|
|
36
|
+
* them, for a print, a preview or an embed.
|
|
37
|
+
*/
|
|
38
|
+
readonly actions?: boolean;
|
|
27
39
|
}
|
|
28
40
|
/** Whether a note is read or edited. */
|
|
29
41
|
export type NoteMode = 'view' | 'edit';
|
|
@@ -33,4 +45,4 @@ export type NoteMode = 'view' | 'edit';
|
|
|
33
45
|
* It renders inside `.markdown`, so the stylesheet of the package applies, and asks the host for
|
|
34
46
|
* every piece of notebook data through the props of `NoteHost`.
|
|
35
47
|
*/
|
|
36
|
-
export declare function NoteBody({ editable, mode, defaultMode, onModeChange, onChange, modeButton, ...rest }: NoteBodyProps): import("react").JSX.Element;
|
|
48
|
+
export declare function NoteBody({ editable, mode, defaultMode, onModeChange, onChange, modeButton, actions, ...rest }: NoteBodyProps): import("react").JSX.Element;
|
package/dist/NoteBody.js
CHANGED
|
@@ -34,14 +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, 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
|
-
if (!editable)
|
|
44
|
+
if (!editable && !actions)
|
|
45
45
|
return _jsx(NoteView, { ...rest });
|
|
46
46
|
const go = (next) => {
|
|
47
47
|
if (mode === undefined)
|
|
@@ -67,13 +67,14 @@ export function NoteBody({ editable = false, mode, defaultMode = 'view', onModeC
|
|
|
67
67
|
};
|
|
68
68
|
const { writable: _writable, onToggleTask: _toggle, ...host } = rest;
|
|
69
69
|
return (_jsx(HostOnly, { host: rest, children: (resolved) => {
|
|
70
|
-
//
|
|
71
|
-
|
|
70
|
+
// Show and Copy Markdown for whoever reads the note, in both modes (#76, #87), and the
|
|
71
|
+
// 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: modeButton, toggle: toggle })] }) }));
|
|
72
73
|
if (current === 'view')
|
|
73
|
-
return _jsx(NoteView, { ...rest, corner: corner });
|
|
74
|
+
return _jsx(NoteView, { ...rest, corner: corner, switched: editable });
|
|
74
75
|
return (
|
|
75
76
|
// While the editor loads, the note stays on the page as it was.
|
|
76
|
-
_jsx(Suspense, { fallback: _jsx(NoteView, { ...rest, corner: corner }), children: _jsx(NoteEditor, { ...host, onChange: hand, corner: corner, autoFocus: true, leaving: leaving, onLeave: onLeave }) }));
|
|
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 }) }));
|
|
77
78
|
} }));
|
|
78
79
|
}
|
|
79
80
|
/** The host resolved once, for what is drawn around the note. */
|
|
@@ -82,7 +83,7 @@ function HostOnly({ host, children }) {
|
|
|
82
83
|
return _jsx(_Fragment, { children: children(resolved) });
|
|
83
84
|
}
|
|
84
85
|
/** The note drawn for reading: the page MemorySmith's reading page draws. */
|
|
85
|
-
function NoteView({ markdown, writable = false, onToggleTask, corner, ...host }) {
|
|
86
|
+
function NoteView({ markdown, writable = false, onToggleTask, corner, switched = false, ...host }) {
|
|
86
87
|
const resolved = useMemo(() => resolveHost(host),
|
|
87
88
|
// Every member of the contract, so a host that passes the same functions keeps the same host.
|
|
88
89
|
HOST_KEYS.map((key) => host[key]));
|
|
@@ -146,7 +147,7 @@ function NoteView({ markdown, writable = false, onToggleTask, corner, ...host })
|
|
|
146
147
|
// The runs and whether an embed cut them are the whole input.
|
|
147
148
|
[runs.join('\u0000'), runs.length < segments.length]);
|
|
148
149
|
let run = 0;
|
|
149
|
-
return (_jsx(HostProvider, { value: resolved, children: _jsxs("div", { className: `markdown markdown-theme-${resolved.theme.name}${corner ? ' has-mode-switch' : ''}`, "data-theme": resolved.scheme, style: rootStyle(resolved.theme, resolved.scheme), children: [corner, _jsx(Properties, { properties: properties }), segments.map((segment, index) => {
|
|
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) => {
|
|
150
151
|
if (!('rendered' in segment)) {
|
|
151
152
|
return _jsx(Transclusion, { target: segment.target, anchor: segment.anchor }, index);
|
|
152
153
|
}
|
|
@@ -11,7 +11,9 @@ export interface ChosenFile {
|
|
|
11
11
|
}
|
|
12
12
|
/** The Markdown of an image at a web address, its name the alternative text (§3.14). */
|
|
13
13
|
export declare function webImage(name: string, address: string): string;
|
|
14
|
-
export declare function AttachDialog({ onClose, onClosed, onAttach, onEmbed, onLink, }: {
|
|
14
|
+
export declare function AttachDialog({ refusal, onClose, onClosed, onAttach, onEmbed, onLink, }: {
|
|
15
|
+
/** Why the host refused the last file, said at the top of the dialog the author opened after it (#88). */
|
|
16
|
+
refusal?: string | null;
|
|
15
17
|
onClose: () => void;
|
|
16
18
|
/** Once the dialog is closed: whether something was put in the note, which then has the focus. */
|
|
17
19
|
onClosed: (inserted: boolean) => void;
|
|
@@ -17,7 +17,7 @@ export function webImage(name, address) {
|
|
|
17
17
|
const target = /[\s()<>]/.test(address) ? `<${address.replace(/[<>]/g, encodeURIComponent)}>` : address;
|
|
18
18
|
return ``;
|
|
19
19
|
}
|
|
20
|
-
export function AttachDialog({ onClose, onClosed, onAttach, onEmbed, onLink, }) {
|
|
20
|
+
export function AttachDialog({ refusal = null, onClose, onClosed, onAttach, onEmbed, onLink, }) {
|
|
21
21
|
const host = useHost();
|
|
22
22
|
const words = host.strings.editor;
|
|
23
23
|
const ways = [...(host.attach ? ['computer'] : []), ...(host.files ? ['notebook'] : []), 'web'];
|
|
@@ -51,7 +51,7 @@ export function AttachDialog({ onClose, onClosed, onAttach, onEmbed, onLink, })
|
|
|
51
51
|
return (_jsx("dialog", { ref: dialog, className: "edit-source edit-attach", "aria-modal": "true", "aria-labelledby": `${id}-title`, onCancel: (event) => {
|
|
52
52
|
event.preventDefault();
|
|
53
53
|
onClose();
|
|
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: `${id}-title`, className: "edit-source-title", children: words.attachTitle }), _jsx("button", { type: "button", className: "edit-control edit-source-close", "aria-label": host.strings.close, title: host.strings.close, onClick: onClose, children: ICONS.remove })] }), _jsx("div", { className: "edit-attach-ways", role: "tablist", "aria-label": words.attachTitle, children: ways.map((each) => (_jsx("button", { type: "button", role: "tab", id: `${id}-${each}`, "aria-selected": each === way, "aria-controls": `${id}-panel`, tabIndex: each === way ? 0 : -1, className: "edit-attach-way", onClick: () => setWay(each), onKeyDown: (event) => {
|
|
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: `${id}-title`, className: "edit-source-title", children: words.attachTitle }), _jsx("button", { type: "button", className: "edit-control edit-source-close", "aria-label": host.strings.close, title: host.strings.close, onClick: onClose, children: ICONS.remove })] }), refusal && _jsx("p", { className: "edit-attach-error edit-attach-refusal", children: refusal }), _jsx("div", { className: "edit-attach-ways", role: "tablist", "aria-label": words.attachTitle, children: ways.map((each) => (_jsx("button", { type: "button", role: "tab", id: `${id}-${each}`, "aria-selected": each === way, "aria-controls": `${id}-panel`, tabIndex: each === way ? 0 : -1, className: "edit-attach-way", onClick: () => setWay(each), onKeyDown: (event) => {
|
|
55
55
|
const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
|
|
56
56
|
if (step === 0)
|
|
57
57
|
return;
|
|
@@ -14,7 +14,8 @@ export interface NoteEditorProps extends Omit<NoteHost, 'writable' | 'onToggleTa
|
|
|
14
14
|
readonly markdown: string;
|
|
15
15
|
/**
|
|
16
16
|
* The note after each change the author makes, byte for byte the original wherever it was not
|
|
17
|
-
* edited. A promise answered is the save of that change, which the editor reports (#64)
|
|
17
|
+
* edited. A promise answered is the save of that change, which the editor reports (#64); one that
|
|
18
|
+
* resolves with a `SaveOutcome` or rejects with a `SaveRefusal` has the host's words and actions (#86).
|
|
18
19
|
*/
|
|
19
20
|
readonly onChange?: (markdown: string) => unknown;
|
|
20
21
|
/** Whether the author may change the note. Defaults to `true`. */
|
package/dist/edit/NoteEditor.js
CHANGED
|
@@ -14,16 +14,18 @@ import { markdown, markdownKeymap } from '@codemirror/lang-markdown';
|
|
|
14
14
|
import { Annotation, Compartment, EditorState } from '@codemirror/state';
|
|
15
15
|
import { EditorView, keymap } from '@codemirror/view';
|
|
16
16
|
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
17
|
-
import { HOST_KEYS, HostProvider, resolveHost } from '../host.js';
|
|
17
|
+
import { HOST_KEYS, HostProvider, isAttachRefusal, resolveHost } from '../host.js';
|
|
18
18
|
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';
|
|
25
26
|
import { chooseLanguageAt } from './LanguageControl.js';
|
|
26
27
|
import { useEditSaves } from './saves.js';
|
|
28
|
+
import { SaveNoticeDialog } from './SaveNoticeDialog.js';
|
|
27
29
|
/**
|
|
28
30
|
* The line break a note is written with, which the editor writes back: `\n` or `\r\n`. `null`
|
|
29
31
|
* for a note that mixes them, or has a lone `\r`, which the editor cannot keep byte for byte.
|
|
@@ -110,8 +112,23 @@ function insertBlockCommand(name) {
|
|
|
110
112
|
return true;
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
|
-
|
|
115
|
+
const NOT_ATTACHING = { state: 'idle', message: null, fraction: null, queued: 0 };
|
|
116
|
+
/** How long the attach button says a file was not attached, when the host did not say why, in milliseconds. */
|
|
114
117
|
const ATTACH_FAILED_MS = 3000;
|
|
118
|
+
/** A host's fraction, kept between 0 and 1, or none when it gave no number. */
|
|
119
|
+
function fractionOf(report) {
|
|
120
|
+
const fraction = report?.fraction;
|
|
121
|
+
return typeof fraction === 'number' && Number.isFinite(fraction) ? Math.min(1, Math.max(0, fraction)) : null;
|
|
122
|
+
}
|
|
123
|
+
/** What the paperclip says: its word, the host's step and the files waiting, or why the last one failed. */
|
|
124
|
+
function attachSays(attaching, words) {
|
|
125
|
+
if (attaching.state === 'failed')
|
|
126
|
+
return attaching.message ?? words.attachFailed;
|
|
127
|
+
if (attaching.state === 'idle')
|
|
128
|
+
return words.attach;
|
|
129
|
+
const step = attaching.message ?? words.attaching;
|
|
130
|
+
return attaching.queued > 0 ? `${step} (${words.attachQueued(attaching.queued)})` : step;
|
|
131
|
+
}
|
|
115
132
|
/** The toolbar of the editor: the marks, the blocks and the history, each a command. */
|
|
116
133
|
function Toolbar({ view, host, end, opened, onAttach, attaching, attachButton, }) {
|
|
117
134
|
const words = host.strings.editor;
|
|
@@ -120,21 +137,35 @@ function Toolbar({ view, host, end, opened, onAttach, attaching, attachButton, }
|
|
|
120
137
|
const button = (name, action, shortcut) => (_jsx("button", { type: "button", className: "edit-control", title: shortcut ? `${words[name]} (${shortcut})` : words[name], "aria-label": words[name], disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: run(action), children: ICONS[name] }, name));
|
|
121
138
|
const block = (name) => button(name, insertBlockCommand(name));
|
|
122
139
|
const level = view ? headingLevel(view.state) : 0;
|
|
123
|
-
return (_jsxs("div", { className: "edit-toolbar", role: "toolbar", "aria-label": words.toolbar, children: [_jsxs("div", { className: "edit-toolbar-tools", children: [_jsxs("span", { className: "edit-toolbar-group", children: [_jsx("button", { type: "button", className: "edit-control", title: words.revert, "aria-label": words.revert, disabled: !view || view.state.readOnly || view.state.doc.toString() === opened, onMouseDown: (event) => event.preventDefault(), onClick: run(command((state) => revert(state, opened))), children: ICONS.revert }), button('undo', undo, 'Mod-Z'), button('redo', redo, 'Mod-Shift-Z')] }), _jsx("span", { className: "edit-toolbar-group", children: _jsxs("select", { className: "edit-heading", "aria-label": words.heading, title: words.heading, value: level, disabled: disabled, onChange: (event) => view && command((state) => setHeading(state, Number(event.target.value)))(view), children: [_jsx("option", { value: 0, children: words.paragraph }), [1, 2, 3, 4, 5, 6].map((each) => (_jsx("option", { value: each, children: `${words.heading} ${each}` }, each)))] }) }), _jsxs("span", { className: "edit-toolbar-group", children: [button('bold', command((state) => toggleMark(state, '**')), 'Mod-B'), button('italic', command((state) => toggleMark(state, '_')), 'Mod-I'), button('strikethrough', command((state) => toggleMark(state, '~~')), 'Mod-Shift-X'), button('highlight', command((state) => toggleMark(state, '==')), 'Mod-Shift-H'), button('code', command((state) => toggleMark(state, '`')), 'Mod-E')] }), _jsxs("span", { className: "edit-toolbar-group", children: [button('link', command(insertLink), 'Mod-K'), button('wikilink', command(insertWikilink), 'Mod-Shift-K'), button('footnote', command(insertFootnote)), button('comment', command(toggleComment), 'Mod-/'), _jsx("button", { ref: attachButton, type: "button", className: "edit-control", title: attaching
|
|
140
|
+
return (_jsxs("div", { className: "edit-toolbar", role: "toolbar", "aria-label": words.toolbar, children: [_jsxs("div", { className: "edit-toolbar-tools", children: [_jsxs("span", { className: "edit-toolbar-group", children: [_jsx("button", { type: "button", className: "edit-control", title: words.revert, "aria-label": words.revert, disabled: !view || view.state.readOnly || view.state.doc.toString() === opened, onMouseDown: (event) => event.preventDefault(), onClick: run(command((state) => revert(state, opened))), children: ICONS.revert }), button('undo', undo, 'Mod-Z'), button('redo', redo, 'Mod-Shift-Z')] }), _jsx("span", { className: "edit-toolbar-group", children: _jsxs("select", { className: "edit-heading", "aria-label": words.heading, title: words.heading, value: level, disabled: disabled, onChange: (event) => view && command((state) => setHeading(state, Number(event.target.value)))(view), children: [_jsx("option", { value: 0, children: words.paragraph }), [1, 2, 3, 4, 5, 6].map((each) => (_jsx("option", { value: each, children: `${words.heading} ${each}` }, each)))] }) }), _jsxs("span", { className: "edit-toolbar-group", children: [button('bold', command((state) => toggleMark(state, '**')), 'Mod-B'), button('italic', command((state) => toggleMark(state, '_')), 'Mod-I'), button('strikethrough', command((state) => toggleMark(state, '~~')), 'Mod-Shift-X'), button('highlight', command((state) => toggleMark(state, '==')), 'Mod-Shift-H'), button('code', command((state) => toggleMark(state, '`')), 'Mod-E')] }), _jsxs("span", { className: "edit-toolbar-group", children: [button('link', command(insertLink), 'Mod-K'), button('wikilink', command(insertWikilink), 'Mod-Shift-K'), button('footnote', command(insertFootnote)), button('comment', command(toggleComment), 'Mod-/'), _jsxs("span", { className: "edit-attach-control", children: [_jsx("button", { ref: attachButton, type: "button", className: "edit-control", title: attachSays(attaching, words), "aria-label": words.attach, "aria-busy": attaching.state === 'busy' ? true : undefined, "data-attach": attaching.state, disabled: disabled, onMouseDown: (event) => event.preventDefault(), onClick: onAttach, children: attaching.state === 'busy' ? ICONS.saving : attaching.state === 'failed' ? ICONS.notSaved : ICONS.attach }), attaching.state === 'busy' && attaching.fraction !== null && (_jsx("span", { className: "edit-attach-progress", role: "progressbar", "aria-label": words.attach, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": Math.round(attaching.fraction * 100), style: { ['--edit-attach-fraction']: attaching.fraction } })), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: attaching.state === 'idle' ? '' : attachSays(attaching, words) })] })] }), _jsxs("span", { className: "edit-toolbar-group", children: [button('bulletList', command((state) => toggleList(state, 'bullet')), 'Mod-Shift-8'), button('orderedList', command((state) => toggleList(state, 'ordered')), 'Mod-Shift-7'), button('taskList', command((state) => toggleList(state, 'task')), 'Mod-Shift-9'), button('quote', command(toggleQuote), 'Mod-Shift-.')] }), _jsxs("span", { className: "edit-toolbar-group", children: [block('callout'), block('table'), block('codeBlock'), block('math'), block('diagram'), block('rule')] })] }), end] }));
|
|
124
141
|
}
|
|
125
142
|
/**
|
|
126
143
|
* Where the save of the author's last change stands, in the toolbar before the switch: an icon for
|
|
127
144
|
* each state, in a slot of one size, so the toolbar never moves as the author types; its word is
|
|
128
|
-
* its title. A failed save's icon is the button that tries again (#66).
|
|
129
|
-
*
|
|
145
|
+
* its title. A failed save's icon is the button that tries again (#66). When the host said why —
|
|
146
|
+
* an alert on a save that succeeded, or the reason for one it refused — the icon is the button that
|
|
147
|
+
* opens what it said, with the host's title as its tooltip, and the author's choices (#86). Whoever
|
|
148
|
+
* hears the page is told the whole of it, why the note stays in edit included.
|
|
130
149
|
*/
|
|
131
|
-
function SaveStatus({
|
|
150
|
+
function SaveStatus({ saves, blocked, host, onDecided }) {
|
|
151
|
+
const { state, notice } = saves;
|
|
132
152
|
const words = host.strings.editor;
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
153
|
+
const [open, setOpen] = useState(false);
|
|
154
|
+
const button = useRef(null);
|
|
155
|
+
// A notice the save no longer carries has nothing left to show.
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!notice)
|
|
158
|
+
setOpen(false);
|
|
159
|
+
}, [notice]);
|
|
160
|
+
const base = state === 'saving' ? words.saving : state === 'saved' ? host.strings.saved : state === 'failed' ? host.strings.notSaved : '';
|
|
161
|
+
const shown = notice ? notice.title : base;
|
|
162
|
+
const said = state === 'failed' && blocked ? (notice ? `${words.stayInEdit}. ${notice.title}` : words.stayInEdit) : shown;
|
|
163
|
+
const icon = state === 'saving' ? ICONS.saving : state === 'saved' ? ICONS.saved : state === 'alert' ? ICONS.alert : state === 'failed' ? ICONS.notSaved : null;
|
|
136
164
|
const retryLabel = `${said}. ${words.retry}`;
|
|
137
|
-
return (_jsxs("div", { className: "edit-save", "data-save": state, title: state === 'failed' ? undefined : said || undefined, children: [state === 'failed' ? (_jsx("button", { type: "button", className: "edit-save-icon edit-save-retry", title: retryLabel, "aria-label": retryLabel, onClick: retry, children: icon })) : (_jsx("span", { className: "edit-save-icon", children: icon })), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: said })
|
|
165
|
+
return (_jsxs("div", { className: "edit-save", "data-save": state, title: state === 'failed' || notice ? undefined : said || undefined, children: [notice ? (_jsx("button", { ref: button, type: "button", className: "edit-save-icon edit-save-notice", title: notice.title, "aria-label": notice.title, "aria-haspopup": "dialog", onClick: () => setOpen(true), children: icon })) : state === 'failed' ? (_jsx("button", { type: "button", className: "edit-save-icon edit-save-retry", title: retryLabel, "aria-label": retryLabel, onClick: saves.retry, children: icon })) : (_jsx("span", { className: "edit-save-icon", children: icon })), _jsx("span", { className: "visually-hidden", role: "status", "aria-live": "polite", children: said }), open && notice && (_jsx(SaveNoticeDialog, { notice: notice, onClose: () => setOpen(false), onClosed: (chosen) => (chosen ? onDecided() : button.current?.focus()), onAction: (action) => {
|
|
166
|
+
setOpen(false);
|
|
167
|
+
saves.run(action);
|
|
168
|
+
} }))] }));
|
|
138
169
|
}
|
|
139
170
|
/**
|
|
140
171
|
* Edits a note on the page that draws it. Exported at `@memorysmithapp/markdown-renderer/edit`
|
|
@@ -162,11 +193,16 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
162
193
|
const [blocked, setBlocked] = useState(false);
|
|
163
194
|
const [refusals, setRefusals] = useState(0);
|
|
164
195
|
const editorView = useRef(null);
|
|
165
|
-
const [attaching, setAttaching] = useState(
|
|
196
|
+
const [attaching, setAttaching] = useState(NOT_ATTACHING);
|
|
197
|
+
// Each run of attaching, so that a run a later one replaced says nothing more.
|
|
198
|
+
const attachRun = useRef(0);
|
|
199
|
+
// The refusal the paperclip said, shown again at the top of the dialog it opens (#88).
|
|
200
|
+
const [refusal, setRefusal] = useState(null);
|
|
166
201
|
/**
|
|
167
202
|
* Hands each file to the host's `attach`, one after the other, and embeds it as `![[name]]` on a
|
|
168
203
|
* line of its own where the cursor is once the host answered its name; a file the host could not
|
|
169
|
-
* keep writes nothing, and the button says so for a moment (#75)
|
|
204
|
+
* keep writes nothing, and the button says so for a moment (#75) — or, when the host said why,
|
|
205
|
+
* until the author opens it again. The host's steps are shown as they come (#88).
|
|
170
206
|
*/
|
|
171
207
|
const attachFiles = useRef(() => undefined);
|
|
172
208
|
attachFiles.current = (files, at) => {
|
|
@@ -176,20 +212,34 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
176
212
|
return;
|
|
177
213
|
if (at !== undefined)
|
|
178
214
|
target.dispatch({ selection: { anchor: at } });
|
|
179
|
-
|
|
215
|
+
const run = ++attachRun.current;
|
|
216
|
+
const live = () => run === attachRun.current && editorView.current !== null;
|
|
180
217
|
void (async () => {
|
|
181
218
|
let failed = false;
|
|
182
|
-
|
|
219
|
+
let reason = null;
|
|
220
|
+
for (const [index, { file, name: wanted, description }] of files.entries()) {
|
|
221
|
+
const queued = files.length - 1 - index;
|
|
222
|
+
let settled = false;
|
|
223
|
+
setAttaching({ state: 'busy', message: null, fraction: null, queued });
|
|
224
|
+
// A step the host reports after the file settled, or for a run since replaced, says nothing.
|
|
225
|
+
const progress = (report) => {
|
|
226
|
+
if (settled || !live())
|
|
227
|
+
return;
|
|
228
|
+
setAttaching({ state: 'busy', message: typeof report?.message === 'string' && report.message ? report.message : null, fraction: fractionOf(report), queued });
|
|
229
|
+
};
|
|
183
230
|
try {
|
|
184
|
-
const name = await attach(file, { name: wanted, description });
|
|
231
|
+
const name = await attach(file, { name: wanted, description }, progress);
|
|
185
232
|
insertLine(`![[${name}]]`);
|
|
186
233
|
}
|
|
187
|
-
catch {
|
|
234
|
+
catch (rejection) {
|
|
188
235
|
failed = true;
|
|
236
|
+
if (isAttachRefusal(rejection))
|
|
237
|
+
reason = rejection.message;
|
|
189
238
|
}
|
|
239
|
+
settled = true;
|
|
190
240
|
}
|
|
191
|
-
if (
|
|
192
|
-
setAttaching(failed ? 'failed' :
|
|
241
|
+
if (live())
|
|
242
|
+
setAttaching(failed ? { ...NOT_ATTACHING, state: 'failed', message: reason } : NOT_ATTACHING);
|
|
193
243
|
})();
|
|
194
244
|
};
|
|
195
245
|
/** A line of its own at the cursor: an embed, or an image of the web (#78). */
|
|
@@ -277,9 +327,10 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
277
327
|
/** A pasted or dropped file, kept under its own name and with no description. */
|
|
278
328
|
const asChosen = (files) => files.map((file) => ({ file, name: file.name, description: '' }));
|
|
279
329
|
useEffect(() => {
|
|
280
|
-
|
|
330
|
+
// A failure the host explained stays until the author acts; a bare one is said for a moment.
|
|
331
|
+
if (attaching.state !== 'failed' || attaching.message !== null)
|
|
281
332
|
return;
|
|
282
|
-
const timer = setTimeout(() => setAttaching(
|
|
333
|
+
const timer = setTimeout(() => setAttaching(NOT_ATTACHING), ATTACH_FAILED_MS);
|
|
283
334
|
return () => clearTimeout(timer);
|
|
284
335
|
}, [attaching]);
|
|
285
336
|
// The note as edit mode opened it, as the editor holds its text: lines joined by `\n`.
|
|
@@ -308,7 +359,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
308
359
|
markdown(),
|
|
309
360
|
EditorView.lineWrapping,
|
|
310
361
|
EditorView.contentAttributes.of({ 'aria-multiline': 'true', spellcheck: 'true' }),
|
|
311
|
-
livePage(),
|
|
362
|
+
livePage(measureMetrics(parent.current)),
|
|
312
363
|
autocompletion({ override: [wikilinkCompletion(() => latest.current.host)], icons: false }),
|
|
313
364
|
shortcuts(() => created),
|
|
314
365
|
keymap.of([...markdownKeymap, ...historyKeymap, indentWithTab, ...defaultKeymap]),
|
|
@@ -453,9 +504,18 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
453
504
|
clearTimeout(timer);
|
|
454
505
|
};
|
|
455
506
|
}, [leaving, saves.state]);
|
|
456
|
-
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", "data-leaving": folding ? '' : undefined, children: _jsx(Toolbar, { view: view, host: resolved, opened: opened.current, onAttach: () =>
|
|
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", "data-leaving": folding ? '' : undefined, children: _jsx(Toolbar, { view: view, host: resolved, opened: opened.current, onAttach: () => {
|
|
508
|
+
// Opening the paperclip is the author acting on a refusal: the dialog says it once more.
|
|
509
|
+
if (attaching.state === 'failed') {
|
|
510
|
+
setRefusal(attaching.message);
|
|
511
|
+
setAttaching(NOT_ATTACHING);
|
|
512
|
+
}
|
|
513
|
+
else
|
|
514
|
+
setRefusal(null);
|
|
515
|
+
setAttachOpen(true);
|
|
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, { 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) => {
|
|
457
517
|
setMenu(null);
|
|
458
518
|
if (back)
|
|
459
519
|
view.focus();
|
|
460
|
-
} })), attachOpen && (_jsx(AttachDialog, { onClose: () => setAttachOpen(false), onClosed: (inserted) => !inserted && attachButton.current?.focus(), onAttach: (files) => attachFiles.current(files), onEmbed: (name) => insertLine(`![[${name}]]`), onLink: (name, address) => insertLine(webImage(name, address)) })), lineBreak === null && _jsx("p", { className: "edit-unavailable", children: resolved.strings.editUnavailable }), _jsx("div", { ref: parent, className: "markdown-editor" })] }) }));
|
|
520
|
+
} })), attachOpen && (_jsx(AttachDialog, { refusal: refusal, onClose: () => setAttachOpen(false), onClosed: (inserted) => !inserted && attachButton.current?.focus(), onAttach: (files) => attachFiles.current(files), onEmbed: (name) => insertLine(`![[${name}]]`), onLink: (name, address) => insertLine(webImage(name, address)) })), lineBreak === null && _jsx("p", { className: "edit-unavailable", children: resolved.strings.editUnavailable }), _jsx("div", { ref: parent, className: "markdown-editor" })] }) }));
|
|
461
521
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the host said of a save, and the author's choices (#86): a modal dialog with the host's
|
|
3
|
+
* title, its explanation and one button per action. It never closes by itself, since its job is to
|
|
4
|
+
* have the author decide; Escape or its × close it and leave the save where it stood, and running
|
|
5
|
+
* an action closes it before the action runs.
|
|
6
|
+
*/
|
|
7
|
+
import type { SaveAction, SaveNotice } from './saves.js';
|
|
8
|
+
export declare function SaveNoticeDialog({ notice, onClose, onClosed, onAction, }: {
|
|
9
|
+
notice: SaveNotice;
|
|
10
|
+
/** Escape or the ×: the author did not choose. */
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
/** Once the dialog is closed, whether an action was chosen, so the focus goes where it belongs. */
|
|
13
|
+
onClosed: (chosen: boolean) => void;
|
|
14
|
+
onAction: (action: SaveAction) => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* What the host said of a save, and the author's choices (#86): a modal dialog with the host's
|
|
4
|
+
* title, its explanation and one button per action. It never closes by itself, since its job is to
|
|
5
|
+
* have the author decide; Escape or its × close it and leave the save where it stood, and running
|
|
6
|
+
* an action closes it before the action runs.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useId, useRef } from 'react';
|
|
9
|
+
import { useHost } from '../host.js';
|
|
10
|
+
import { ICON_CLOSE } from '../mode-icons.js';
|
|
11
|
+
export function SaveNoticeDialog({ notice, onClose, onClosed, onAction, }) {
|
|
12
|
+
const host = useHost();
|
|
13
|
+
const dialog = useRef(null);
|
|
14
|
+
const chosen = useRef(false);
|
|
15
|
+
const closed = useRef(onClosed);
|
|
16
|
+
closed.current = onClosed;
|
|
17
|
+
const id = useId();
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const element = dialog.current;
|
|
20
|
+
if (!element)
|
|
21
|
+
return;
|
|
22
|
+
if (typeof element.showModal === 'function')
|
|
23
|
+
element.showModal();
|
|
24
|
+
else
|
|
25
|
+
element.setAttribute('open', '');
|
|
26
|
+
element.querySelector('.edit-notice-action')?.focus();
|
|
27
|
+
return () => {
|
|
28
|
+
if (element.open && typeof element.close === 'function')
|
|
29
|
+
element.close();
|
|
30
|
+
// Only once the dialog is closed may the page behind it take the focus.
|
|
31
|
+
closed.current(chosen.current);
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
return (_jsx("dialog", { ref: dialog, className: "edit-source edit-notice", "aria-modal": "true", "aria-labelledby": `${id}-title`, "aria-describedby": `${id}-message`, onCancel: (event) => {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
onClose();
|
|
37
|
+
}, children: _jsxs("div", { className: "edit-source-frame", children: [_jsxs("header", { className: "edit-source-header", children: [_jsx("h2", { id: `${id}-title`, className: "edit-source-title", children: notice.title }), _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("p", { id: `${id}-message`, className: "edit-notice-message", children: notice.message }), _jsx("div", { className: "edit-notice-actions", children: notice.actions.map((action, index) => (_jsx("button", { type: "button", className: "edit-attach-button edit-notice-action", onClick: () => {
|
|
38
|
+
chosen.current = true;
|
|
39
|
+
onAction(action);
|
|
40
|
+
}, children: action.label }, index))) })] }) }));
|
|
41
|
+
}
|
package/dist/edit/blocks.d.ts
CHANGED
package/dist/edit/blocks.js
CHANGED
|
@@ -475,6 +475,7 @@ export const ICONS = {
|
|
|
475
475
|
saving: icon('M21 12a9 9 0 1 1-6.219-8.56'),
|
|
476
476
|
saved: icon('M20 6 9 17l-5-5'),
|
|
477
477
|
notSaved: icon('M12 2a10 10 0 1 0 0 20 10 10 0 1 0 0-20z', 'M12 8v4', 'M12 16h.01'),
|
|
478
|
+
alert: icon('m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3', 'M12 9v4', 'M12 17h.01'),
|
|
478
479
|
edit: icon('M21.17 6.81a1 1 0 0 0-3.99-3.99L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5z', 'm15 5 4 4'),
|
|
479
480
|
view: icon('M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0', 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z'),
|
|
480
481
|
};
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
/** What an estimate assumes before the editor has been measured, as in a test with no layout. */
|
|
18
|
+
export declare const DEFAULT_METRICS: BlockMetrics;
|
|
19
|
+
/**
|
|
20
|
+
* The metrics of a text column laid out in `parent`, read before the editor exists there, so the
|
|
21
|
+
* first estimates are made at the width the note is drawn at. Falls back to `DEFAULT_METRICS` where
|
|
22
|
+
* nothing is laid out.
|
|
23
|
+
*/
|
|
24
|
+
export declare function measureMetrics(parent: HTMLElement): BlockMetrics;
|
|
25
|
+
/** Metrics as measured, any that could not be read replaced by the default. */
|
|
26
|
+
export declare function withinReason(metrics: BlockMetrics): BlockMetrics;
|
|
27
|
+
/** What a block is, for its estimate. */
|
|
28
|
+
export type BlockKind = 'frontmatter' | 'callout' | 'table' | 'embed' | 'diagram' | 'preview' | 'add-properties';
|
|
29
|
+
/** The height a block of this kind and these lines is drawn at, near enough to lay out the page. */
|
|
30
|
+
export declare function estimateHeight(kind: BlockKind, source: string, metrics: BlockMetrics): number;
|
|
31
|
+
/** The height a block of this content was drawn at at this width, if it was. */
|
|
32
|
+
export declare function measuredHeight(identity: string, metrics: BlockMetrics): number | undefined;
|
|
33
|
+
/** Remembers the height a block was drawn at. */
|
|
34
|
+
export declare function rememberHeight(identity: string, metrics: BlockMetrics, height: number): void;
|