@memorysmithapp/markdown-renderer 0.8.1 → 0.8.2
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/dist/edit/NoteEditor.d.ts +0 -4
- package/dist/edit/NoteEditor.js +16 -8
- package/dist/edit/blocks.js +92 -8
- package/dist/edit/saves.d.ts +5 -0
- package/dist/edit/saves.js +16 -4
- package/package.json +2 -2
|
@@ -43,8 +43,4 @@ export interface NoteEditorProps extends Omit<NoteHost, 'writable' | 'onToggleTa
|
|
|
43
43
|
* for a note that mixes them, or has a lone `\r`, which the editor cannot keep byte for byte.
|
|
44
44
|
*/
|
|
45
45
|
export declare function lineBreakOf(text: string): '\n' | '\r\n' | null;
|
|
46
|
-
/**
|
|
47
|
-
* Edits a note on the page that draws it. Exported at `@memorysmithapp/markdown-renderer/edit`
|
|
48
|
-
* for a host that edits without the switch of `NoteBody`; `NoteBody` loads it on demand.
|
|
49
|
-
*/
|
|
50
46
|
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
|
@@ -139,7 +139,7 @@ function Toolbar({ view, host, end, opened, onAttach, attaching, attachButton, }
|
|
|
139
139
|
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));
|
|
140
140
|
const block = (name) => button(name, insertBlockCommand(name));
|
|
141
141
|
const level = view ? headingLevel(view.state) : 0;
|
|
142
|
-
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] }));
|
|
142
|
+
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, Mod-Y')] }), _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] }));
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* Where the save of the author's last change stands, in the toolbar before the switch: an icon for
|
|
@@ -155,12 +155,12 @@ function SaveStatus({ saves, blocked, deciding, open, setOpen, host, onDecided,
|
|
|
155
155
|
const { state, notice } = saves;
|
|
156
156
|
const words = host.strings.editor;
|
|
157
157
|
const button = useRef(null);
|
|
158
|
-
const base = state === 'saving' ? words.saving : state === 'saved' ? host.strings.saved : state === 'failed' ? host.strings.notSaved : '';
|
|
158
|
+
const base = saves.running ? host.strings.updating : state === 'saving' ? words.saving : state === 'saved' ? host.strings.saved : state === 'failed' ? host.strings.notSaved : '';
|
|
159
159
|
const shown = notice ? (deciding ? `${notice.title}. ${words.chooseToEdit}` : notice.title) : base;
|
|
160
160
|
const said = state === 'failed' && blocked ? (notice ? `${words.stayInEdit}. ${shown}` : words.stayInEdit) : shown;
|
|
161
161
|
const icon = state === 'saving' ? ICONS.saving : state === 'saved' ? ICONS.saved : state === 'alert' ? ICONS.alert : state === 'failed' ? ICONS.notSaved : null;
|
|
162
162
|
const retryLabel = `${said}. ${words.retry}`;
|
|
163
|
-
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: shown, "aria-label": shown, "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) => {
|
|
163
|
+
return (_jsxs("div", { className: "edit-save", "data-save": state, "aria-busy": saves.running ? true : undefined, title: state === 'failed' || notice ? undefined : said || undefined, children: [notice ? (_jsx("button", { ref: button, type: "button", className: "edit-save-icon edit-save-notice", title: shown, "aria-label": shown, "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) => {
|
|
164
164
|
setOpen(false);
|
|
165
165
|
saves.run(action);
|
|
166
166
|
} }))] }));
|
|
@@ -169,6 +169,12 @@ function SaveStatus({ saves, blocked, deciding, open, setOpen, host, onDecided,
|
|
|
169
169
|
* Edits a note on the page that draws it. Exported at `@memorysmithapp/markdown-renderer/edit`
|
|
170
170
|
* for a host that edits without the switch of `NoteBody`; `NoteBody` loads it on demand.
|
|
171
171
|
*/
|
|
172
|
+
/**
|
|
173
|
+
* Redo on Ctrl+Shift+Z, Cmd+Shift+Z on macOS, on every platform, as well as on Ctrl+Y (#120):
|
|
174
|
+
* CodeMirror binds Ctrl+Shift+Z to redo on Linux only, and elsewhere drops the Shift of the key
|
|
175
|
+
* and undoes, so the key most people redo with lost them one more step.
|
|
176
|
+
*/
|
|
177
|
+
const redoKeys = [{ key: 'Mod-Shift-z', run: redo, preventDefault: true }];
|
|
172
178
|
export function NoteEditor({ markdown: given, onChange, editable = true, corner, buttons = 'icon', autoFocus = false, leaving = false, onLeave, ...host }) {
|
|
173
179
|
const resolved = useMemo(() => resolveHost(host),
|
|
174
180
|
// Every member of the contract, so a host that passes the same functions keeps the same host.
|
|
@@ -183,14 +189,14 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
183
189
|
* A refusal that gives the author choices — another person wrote the note, say — is decided
|
|
184
190
|
* before the note is written in again: every save after it would be refused too (#101).
|
|
185
191
|
*/
|
|
186
|
-
const deciding = saves.state === 'failed' && (saves.notice?.actions.length ?? 0) > 0;
|
|
192
|
+
const deciding = (saves.state === 'failed' && (saves.notice?.actions.length ?? 0) > 0) || saves.running;
|
|
187
193
|
const [noticeOpen, setNoticeOpen] = useState(false);
|
|
188
194
|
// A notice with choices opens by itself as it arrives; one the save no longer carries closes.
|
|
189
195
|
useEffect(() => {
|
|
190
196
|
setNoticeOpen(!!saves.notice && saves.notice.actions.length > 0);
|
|
191
197
|
}, [saves.notice]);
|
|
192
|
-
const latest = useRef({ hand: saves.hand, host: resolved, onLeave, deciding, ask: () => setNoticeOpen(true) });
|
|
193
|
-
latest.current = { hand: saves.hand, host: resolved, onLeave, deciding, ask: () => setNoticeOpen(true) };
|
|
198
|
+
const latest = useRef({ hand: saves.hand, host: resolved, onLeave, deciding, running: saves.running, ask: () => setNoticeOpen(true) });
|
|
199
|
+
latest.current = { hand: saves.hand, host: resolved, onLeave, deciding, running: saves.running, ask: () => setNoticeOpen(true) };
|
|
194
200
|
/**
|
|
195
201
|
* The texts handed to the host that it has not given back yet. A host that saves before it
|
|
196
202
|
* passes the note again gives back a text the editor has since moved past; that echo is not a
|
|
@@ -373,7 +379,7 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
373
379
|
livePage(measureMetrics(parent.current)),
|
|
374
380
|
autocompletion({ override: [wikilinkCompletion(() => latest.current.host)], icons: false }),
|
|
375
381
|
shortcuts(() => created),
|
|
376
|
-
keymap.of([...markdownKeymap, ...historyKeymap, indentWithTab, ...defaultKeymap]),
|
|
382
|
+
keymap.of([...markdownKeymap, ...redoKeys, ...historyKeymap, indentWithTab, ...defaultKeymap]),
|
|
377
383
|
// A file pasted or dropped is attached, where the host keeps files (#75).
|
|
378
384
|
menuKeys((target) => {
|
|
379
385
|
if (target.state.readOnly)
|
|
@@ -391,7 +397,9 @@ export function NoteEditor({ markdown: given, onChange, editable = true, corner,
|
|
|
391
397
|
if (event.key.length !== 1 && !WRITING_KEYS.has(event.key))
|
|
392
398
|
return false;
|
|
393
399
|
event.preventDefault();
|
|
394
|
-
|
|
400
|
+
// While the choice runs there is nothing to choose again: the key writes nothing (#119).
|
|
401
|
+
if (!latest.current.running)
|
|
402
|
+
latest.current.ask();
|
|
395
403
|
return true;
|
|
396
404
|
},
|
|
397
405
|
// The editing menu where the pointer is; Shift keeps the browser's, and so do the
|
package/dist/edit/blocks.js
CHANGED
|
@@ -98,9 +98,7 @@ const SCROLL_JUMP = 120;
|
|
|
98
98
|
*/
|
|
99
99
|
function holdInPlace(view, block, run) {
|
|
100
100
|
const top = block.getBoundingClientRect().top;
|
|
101
|
-
|
|
102
|
-
while (scroller && !(scroller.scrollHeight > scroller.clientHeight && /(auto|scroll)/.test(getComputedStyle(scroller).overflowY)))
|
|
103
|
-
scroller = scroller.parentElement;
|
|
101
|
+
const scroller = scrollerOf(view);
|
|
104
102
|
const scrolled = () => (scroller ? scroller.scrollTop : window.scrollY);
|
|
105
103
|
const before = scrolled();
|
|
106
104
|
const back = () => {
|
|
@@ -117,6 +115,83 @@ function holdInPlace(view, block, run) {
|
|
|
117
115
|
window.addEventListener('scroll', back, true);
|
|
118
116
|
setTimeout(() => window.removeEventListener('scroll', back, true), SCROLL_HOLD_MS);
|
|
119
117
|
}
|
|
118
|
+
/** What the editor scrolls in: its own scroller, a pane of the host that scrolls, or `null` for the window. */
|
|
119
|
+
function scrollerOf(view) {
|
|
120
|
+
let scroller = view.scrollDOM;
|
|
121
|
+
while (scroller && !(scroller.scrollHeight > scroller.clientHeight && /(auto|scroll)/.test(getComputedStyle(scroller).overflowY)))
|
|
122
|
+
scroller = scroller.parentElement;
|
|
123
|
+
return scroller;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Room left after the last line of a note while an image is resized near its end (#116): the
|
|
127
|
+
* pixels of padding added under the text, and its padding before. Kept once the drag ends, so the
|
|
128
|
+
* page does not jump back when the image is written smaller, and given back as the author scrolls
|
|
129
|
+
* up, a pixel of it for a pixel of scroll, where it is out of sight.
|
|
130
|
+
*/
|
|
131
|
+
const roomBelow = new WeakMap();
|
|
132
|
+
function setRoom(view, added) {
|
|
133
|
+
const room = roomBelow.get(view) ?? { added: 0, base: parseFloat(getComputedStyle(view.contentDOM).paddingBottom || '0') || 0, dragging: false, trim: null };
|
|
134
|
+
room.added = Math.max(0, Math.round(added));
|
|
135
|
+
view.contentDOM.style.paddingBottom = room.added > 0 ? `${room.base + room.added}px` : '';
|
|
136
|
+
roomBelow.set(view, room);
|
|
137
|
+
const scroller = scrollerOf(view);
|
|
138
|
+
const target = scroller ?? window;
|
|
139
|
+
if (room.added > 0 && !room.trim) {
|
|
140
|
+
room.trim = () => {
|
|
141
|
+
if (room.dragging)
|
|
142
|
+
return;
|
|
143
|
+
const max = scroller ? scroller.scrollHeight - scroller.clientHeight : document.documentElement.scrollHeight - window.innerHeight;
|
|
144
|
+
const slack = max - (scroller ? scroller.scrollTop : window.scrollY);
|
|
145
|
+
if (slack > 0)
|
|
146
|
+
setRoom(view, room.added - slack);
|
|
147
|
+
};
|
|
148
|
+
target.addEventListener('scroll', room.trim, { passive: true });
|
|
149
|
+
}
|
|
150
|
+
else if (room.added === 0 && room.trim) {
|
|
151
|
+
target.removeEventListener('scroll', room.trim);
|
|
152
|
+
room.trim = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Holds the top of a picture where it is on the screen while its handle is dragged and once it is
|
|
157
|
+
* written (#116): only the text below it moves. The page could not stay put by itself: shrinking
|
|
158
|
+
* a picture near the end of a note made the page shorter than where it was scrolled to, and the
|
|
159
|
+
* browser brought everything above down; growing it after that, the browser's scroll anchoring
|
|
160
|
+
* kept the text below in place and the picture grew upward, away from the pointer. So the note is
|
|
161
|
+
* given room after its last line for as much as the picture may shrink, the anchoring is turned off
|
|
162
|
+
* on what scrolls for the drag, and any move of the picture's top left is scrolled back.
|
|
163
|
+
*/
|
|
164
|
+
function pinTop(view, picture) {
|
|
165
|
+
const scroller = scrollerOf(view);
|
|
166
|
+
const anchored = scroller ?? document.documentElement;
|
|
167
|
+
const anchoring = anchored.style.overflowAnchor;
|
|
168
|
+
anchored.style.overflowAnchor = 'none';
|
|
169
|
+
const top = picture()?.getBoundingClientRect().top ?? null;
|
|
170
|
+
const height = picture()?.getBoundingClientRect().height ?? 0;
|
|
171
|
+
const room = roomBelow.get(view);
|
|
172
|
+
setRoom(view, Math.max(room?.added ?? 0, height));
|
|
173
|
+
roomBelow.get(view).dragging = true;
|
|
174
|
+
const hold = () => {
|
|
175
|
+
const now = picture()?.getBoundingClientRect().top;
|
|
176
|
+
if (top === null || now === undefined)
|
|
177
|
+
return;
|
|
178
|
+
const moved = now - top;
|
|
179
|
+
if (Math.abs(moved) < 1)
|
|
180
|
+
return;
|
|
181
|
+
if (scroller)
|
|
182
|
+
scroller.scrollBy(0, moved);
|
|
183
|
+
else
|
|
184
|
+
window.scrollBy(0, moved);
|
|
185
|
+
};
|
|
186
|
+
const release = () => {
|
|
187
|
+
hold();
|
|
188
|
+
anchored.style.overflowAnchor = anchoring;
|
|
189
|
+
const left = roomBelow.get(view);
|
|
190
|
+
if (left)
|
|
191
|
+
left.dragging = false;
|
|
192
|
+
};
|
|
193
|
+
return { hold, release };
|
|
194
|
+
}
|
|
120
195
|
/** A number as a field holds it: digits and nothing else, or `null`. */
|
|
121
196
|
function wholeNumber(text) {
|
|
122
197
|
return /^\d{1,6}$/.test(text.trim()) ? Number(text.trim()) : null;
|
|
@@ -438,10 +513,14 @@ export function ImageBlock({ view, source, children }) {
|
|
|
438
513
|
image.style.maxWidth = percent === null ? '' : shareWidth(percent);
|
|
439
514
|
}
|
|
440
515
|
hold();
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
516
|
+
// The picture keeps its place while it is written and drawn again at its new size (#116).
|
|
517
|
+
const settle = () => {
|
|
518
|
+
start.pin.hold();
|
|
519
|
+
requestAnimationFrame(start.pin.hold);
|
|
520
|
+
setTimeout(start.pin.release, SCROLL_HOLD_MS);
|
|
521
|
+
};
|
|
522
|
+
if (cancelled || !end || (Math.abs(end.width - start.width) < 1 && Math.abs(end.height - start.height) < 1))
|
|
523
|
+
return settle();
|
|
445
524
|
if (unit === '%') {
|
|
446
525
|
const percent = percentOf(end.width);
|
|
447
526
|
if (percent !== null)
|
|
@@ -451,6 +530,7 @@ export function ImageBlock({ view, source, children }) {
|
|
|
451
530
|
resize({ width: end.width, height: end.height, percent: null });
|
|
452
531
|
else
|
|
453
532
|
resize({ width: end.width, height: null, percent: null });
|
|
533
|
+
settle();
|
|
454
534
|
};
|
|
455
535
|
/** Shows the image's notation, as a table's button does: the caret goes before it (#112). */
|
|
456
536
|
const showSource = () => {
|
|
@@ -483,7 +563,10 @@ export function ImageBlock({ view, source, children }) {
|
|
|
483
563
|
if (!now)
|
|
484
564
|
return;
|
|
485
565
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
486
|
-
|
|
566
|
+
const widget = frame.current?.closest('.cm-ms-inline-block, .cm-ms-block') ?? null;
|
|
567
|
+
// The picture the widget draws, the one drawn anew once it is written included.
|
|
568
|
+
const pin = pinTop(view, () => widget?.querySelector('.edit-image img') ?? null);
|
|
569
|
+
drag.current = { x: event.clientX, y: event.clientY, ...now, pointer: event.pointerId, pin };
|
|
487
570
|
if (frame.current)
|
|
488
571
|
frame.current.dataset['msBusy'] = '';
|
|
489
572
|
setShift(event.shiftKey);
|
|
@@ -501,6 +584,7 @@ export function ImageBlock({ view, source, children }) {
|
|
|
501
584
|
}
|
|
502
585
|
else
|
|
503
586
|
image.style.height = 'auto';
|
|
587
|
+
start.pin.hold();
|
|
504
588
|
// What the page lets it be: never wider than the column.
|
|
505
589
|
setDragged(drawn());
|
|
506
590
|
}, onPointerUp: (event) => endDrag(event, false), onPointerCancel: (event) => endDrag(event, true), children: ICON_RESIZE })] }))] }));
|
package/dist/edit/saves.d.ts
CHANGED
|
@@ -42,6 +42,11 @@ export interface EditSaves {
|
|
|
42
42
|
readonly retry: () => void;
|
|
43
43
|
/** Runs one of the notice's actions and follows it like a save. */
|
|
44
44
|
readonly run: (action: SaveAction) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Whether an action the author chose is still running (#119): the note stays held until it
|
|
47
|
+
* settles, and a moment after, so the text the host gives back is in before the author writes.
|
|
48
|
+
*/
|
|
49
|
+
readonly running: boolean;
|
|
45
50
|
}
|
|
46
51
|
/** Whether a value is a notice a host gave: a title, a message and its actions. */
|
|
47
52
|
export declare function isSaveNotice(value: unknown): value is SaveNotice;
|
package/dist/edit/saves.js
CHANGED
|
@@ -26,6 +26,7 @@ export function isSaveNotice(value) {
|
|
|
26
26
|
}
|
|
27
27
|
export function useEditSaves(onChange) {
|
|
28
28
|
const [status, setStatus] = useState({ state: 'idle', notice: null });
|
|
29
|
+
const [running, setRunning] = useState(false);
|
|
29
30
|
const latest = useRef({ onChange, count: 0, text: null, alive: true });
|
|
30
31
|
latest.current.onChange = onChange;
|
|
31
32
|
useEffect(() => {
|
|
@@ -35,13 +36,18 @@ export function useEditSaves(onChange) {
|
|
|
35
36
|
self.alive = false;
|
|
36
37
|
};
|
|
37
38
|
}, []);
|
|
38
|
-
/**
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Follows what the host answered, a change's save or an action; `done` is the state of an answer
|
|
41
|
+
* that is not a promise, and `settled` is told once it is over.
|
|
42
|
+
*/
|
|
43
|
+
const follow = useCallback((answer, done, settled) => {
|
|
40
44
|
const self = latest.current;
|
|
41
45
|
const count = ++self.count;
|
|
42
46
|
const settle = (state, notice = null) => {
|
|
43
47
|
if (self.alive && count === self.count)
|
|
44
48
|
setStatus({ state, notice });
|
|
49
|
+
if (state !== 'saving')
|
|
50
|
+
settled?.();
|
|
45
51
|
};
|
|
46
52
|
let result;
|
|
47
53
|
try {
|
|
@@ -68,6 +74,12 @@ export function useEditSaves(onChange) {
|
|
|
68
74
|
hand(text);
|
|
69
75
|
}, [hand]);
|
|
70
76
|
// An action that answers nothing has done what it does: the author decided, and nothing is left to say.
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
// The note is held from the choice until the action settles, and for a turn of the event loop after
|
|
78
|
+
// it, so a text the host gives back as it settles is drawn before a key reaches it (#119).
|
|
79
|
+
const run = useCallback((action) => {
|
|
80
|
+
const self = latest.current;
|
|
81
|
+
setRunning(true);
|
|
82
|
+
follow(() => action.run(), 'saved', () => setTimeout(() => self.alive && setRunning(false)));
|
|
83
|
+
}, [follow]);
|
|
84
|
+
return { state: status.state, notice: status.notice, hand, retry, run, running };
|
|
73
85
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memorysmithapp/markdown-renderer",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "A React component that draws a MemorySmith note exactly as MemorySmith's reading page does.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"unified": "11.0.5"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
|
-
"@memorysmithapp/markdown-spec": "0.8.
|
|
82
|
+
"@memorysmithapp/markdown-spec": "0.8.2",
|
|
83
83
|
"@types/hast": "3.0.5",
|
|
84
84
|
"@types/react": "19.2.18",
|
|
85
85
|
"@types/react-dom": "19.2.4",
|