@memorysmithapp/markdown-renderer 0.8.1 → 0.8.3
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 +103 -13
- package/dist/edit/saves.d.ts +5 -0
- package/dist/edit/saves.js +16 -4
- package/package.json +2 -2
- package/structure.css +10 -0
- package/theme-default.css +0 -3
|
@@ -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;
|
|
@@ -150,6 +225,8 @@ export function ImageBlock({ view, source, children }) {
|
|
|
150
225
|
const frame = useRef(null);
|
|
151
226
|
const size = readImageSize(source);
|
|
152
227
|
const [image, setImage] = useState(null);
|
|
228
|
+
// A kept file that is not a picture — its card, its player — takes the column, as it is read (#122).
|
|
229
|
+
const [file, setFile] = useState(false);
|
|
153
230
|
const [natural, setNatural] = useState(null);
|
|
154
231
|
const [locked, setLocked] = useState(() => size?.height == null);
|
|
155
232
|
const [unit, setUnit] = useState(() => (size?.percent == null ? 'px' : '%'));
|
|
@@ -164,7 +241,10 @@ export function ImageBlock({ view, source, children }) {
|
|
|
164
241
|
const element = frame.current;
|
|
165
242
|
if (!element)
|
|
166
243
|
return;
|
|
167
|
-
const look = () =>
|
|
244
|
+
const look = () => {
|
|
245
|
+
setImage(element.querySelector('img'));
|
|
246
|
+
setFile(element.querySelector('.attachment-card, .attachment-player') !== null);
|
|
247
|
+
};
|
|
168
248
|
look();
|
|
169
249
|
if (typeof MutationObserver === 'undefined')
|
|
170
250
|
return;
|
|
@@ -438,10 +518,14 @@ export function ImageBlock({ view, source, children }) {
|
|
|
438
518
|
image.style.maxWidth = percent === null ? '' : shareWidth(percent);
|
|
439
519
|
}
|
|
440
520
|
hold();
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
521
|
+
// The picture keeps its place while it is written and drawn again at its new size (#116).
|
|
522
|
+
const settle = () => {
|
|
523
|
+
start.pin.hold();
|
|
524
|
+
requestAnimationFrame(start.pin.hold);
|
|
525
|
+
setTimeout(start.pin.release, SCROLL_HOLD_MS);
|
|
526
|
+
};
|
|
527
|
+
if (cancelled || !end || (Math.abs(end.width - start.width) < 1 && Math.abs(end.height - start.height) < 1))
|
|
528
|
+
return settle();
|
|
445
529
|
if (unit === '%') {
|
|
446
530
|
const percent = percentOf(end.width);
|
|
447
531
|
if (percent !== null)
|
|
@@ -451,6 +535,7 @@ export function ImageBlock({ view, source, children }) {
|
|
|
451
535
|
resize({ width: end.width, height: end.height, percent: null });
|
|
452
536
|
else
|
|
453
537
|
resize({ width: end.width, height: null, percent: null });
|
|
538
|
+
settle();
|
|
454
539
|
};
|
|
455
540
|
/** Shows the image's notation, as a table's button does: the caret goes before it (#112). */
|
|
456
541
|
const showSource = () => {
|
|
@@ -461,7 +546,7 @@ export function ImageBlock({ view, source, children }) {
|
|
|
461
546
|
const offered = size !== null && image !== null;
|
|
462
547
|
const unitWord = unit === '%' ? '%' : 'px';
|
|
463
548
|
const invalid = (name) => (refused?.field === name ? { 'aria-invalid': true, title: words.imageOutOfRange(refused.range.min, refused.range.max, name === 'width' ? unitWord : 'px') } : {});
|
|
464
|
-
return (_jsxs("span", { ref: frame, className: "edit-image", "data-resizing": dragged === null ? undefined : '', "data-ms-in-place": offered ? '' : undefined, onFocus: () => frame.current && (frame.current.dataset['msBusy'] = ''), onBlur: hold, onMouseDown: (event) => {
|
|
549
|
+
return (_jsxs("span", { ref: frame, className: "edit-image", "data-resizing": dragged === null ? undefined : '', "data-ms-in-place": offered ? '' : undefined, "data-ms-file": file ? '' : undefined, onFocus: () => frame.current && (frame.current.dataset['msBusy'] = ''), onBlur: hold, onMouseDown: (event) => {
|
|
465
550
|
// The frame's own area is the space between the picture and its controls: a press there is
|
|
466
551
|
// one meant for them, and shows nothing (#107).
|
|
467
552
|
if (event.target === event.currentTarget) {
|
|
@@ -471,19 +556,23 @@ export function ImageBlock({ view, source, children }) {
|
|
|
471
556
|
}, children: [children, offered && (_jsxs(_Fragment, { children: [_jsxs("span", { className: "edit-image-controls", "data-ms-control": "", onPointerDown: hold, onTouchStart: hold, onMouseDown: (event) => {
|
|
472
557
|
// A press anywhere but on a field, a choice or a button — a gap between them, the
|
|
473
558
|
// padding, a word beside a field — keeps the focus where it is. Its default would put
|
|
474
|
-
// the editor's caret where the press fell, and the image would show its source (#107).
|
|
475
|
-
|
|
559
|
+
// the editor's caret where the press fell, and the image would show its source (#107). A
|
|
560
|
+
// disabled one is a gap too: it takes no focus, and the caret would go where it fell (#121).
|
|
561
|
+
if (!event.target.closest('input:enabled, select:enabled, textarea:enabled, button:enabled'))
|
|
476
562
|
event.preventDefault();
|
|
477
563
|
}, children: [size.description ? (_jsx("input", { className: "edit-field edit-image-description", type: "text", "aria-label": words.imageDescription, title: words.imageDescription, placeholder: image.alt, value: typed.description ?? size.description.text, ...field('description') })) : (
|
|
478
564
|
// An embed writes no description (§5.8): the file's own is shown, and is not edited here.
|
|
479
|
-
_jsx("input", { className: "edit-field edit-image-description", type: "text", readOnly: true, "aria-label": words.imageDescription, title: words.imageDescription, placeholder: image.alt, value: "" })), _jsxs("span", { className: "edit-image-size", children: [_jsx("input", { className: "edit-field edit-image-width", type: "text", inputMode: "numeric", autoComplete: "off", "aria-label": words.imageWidth, title: words.imageWidth, placeholder: unit === '%' ? '' : natural === null ? '' : String(natural.width), value: typed.width ?? (shownWidth === null ? '' : String(shownWidth)), ...field('width'), ...invalid('width') }), _jsxs("select", { className: "edit-image-unit", "aria-label": words.imageUnit, title: words.imageUnit, value: unit, onChange: (event) => chooseUnit(event.target.value), children: [_jsx("option", { value: "px", children: "px" }), _jsx("option", { value: "%", children: "%" })] }), _jsx(Control, { label: locked ? words.imageKeepRatio : words.imageFreeRatio, onClick: toggleLock, pressed: locked, disabled: unit === '%', focusable: true, children: locked ? ICON_RATIO_LOCKED : ICON_RATIO_FREE }), _jsx("input", { className: "edit-field edit-image-height", type: "text", inputMode: "numeric", autoComplete: "off", "aria-label": words.imageHeight, title: words.imageHeight, disabled: unit === '%', placeholder: natural === null || unit === '%' ? '' : String(natural.height), value: unit === '%' ? '' : (typed.height ?? (shownHeight === null ? '' : String(shownHeight))), ...field('height'), ...invalid('height') }), _jsx("span", { className: "edit-image-unit-label", "aria-hidden": "true", children: "px" }), _jsx(Control, { label: words.imageOwnSize, onClick: () => resize(null), pressed: size.width === null && size.percent === null, focusable: true, children: ICON_OWN_SIZE }), _jsx(Control, { label: words.source, onClick: showSource, children: ICONS.source })] }), refused && (_jsx("span", { className: "edit-image-refused", role: "status", children: words.imageOutOfRange(refused.range.min, refused.range.max, refused.field === 'width' ? unitWord : 'px') }))] }), _jsx("span", { className: "edit-image-handle", "data-ms-control": "", "data-free": free ? '' : undefined, title: free ? words.imageResizeFree : words.imageResizeKeep, "aria-hidden": "true", onPointerDown: (event) => {
|
|
565
|
+
_jsx("input", { className: "edit-field edit-image-description", type: "text", readOnly: true, "aria-label": words.imageDescription, title: words.imageDescription, placeholder: image.alt, value: "" })), _jsxs("span", { className: "edit-image-size", "data-unit": unit, children: [_jsx("input", { className: "edit-field edit-image-width", type: "text", inputMode: "numeric", autoComplete: "off", "aria-label": words.imageWidth, title: words.imageWidth, placeholder: unit === '%' ? '' : natural === null ? '' : String(natural.width), value: typed.width ?? (shownWidth === null ? '' : String(shownWidth)), ...field('width'), ...invalid('width') }), _jsxs("select", { className: "edit-image-unit", "aria-label": words.imageUnit, title: words.imageUnit, value: unit, onChange: (event) => chooseUnit(event.target.value), children: [_jsx("option", { value: "px", children: "px" }), _jsx("option", { value: "%", children: "%" })] }), _jsx(Control, { label: locked ? words.imageKeepRatio : words.imageFreeRatio, onClick: toggleLock, pressed: locked, disabled: unit === '%', focusable: true, children: locked ? ICON_RATIO_LOCKED : ICON_RATIO_FREE }), _jsx("input", { className: "edit-field edit-image-height", type: "text", inputMode: "numeric", autoComplete: "off", "aria-label": words.imageHeight, title: words.imageHeight, disabled: unit === '%', placeholder: natural === null || unit === '%' ? '' : String(natural.height), value: unit === '%' ? '' : (typed.height ?? (shownHeight === null ? '' : String(shownHeight))), ...field('height'), ...invalid('height') }), _jsx("span", { className: "edit-image-unit-label", "aria-hidden": "true", children: "px" }), _jsx(Control, { label: words.imageOwnSize, onClick: () => resize(null), pressed: size.width === null && size.percent === null, focusable: true, children: ICON_OWN_SIZE }), _jsx(Control, { label: words.source, onClick: showSource, children: ICONS.source })] }), refused && (_jsx("span", { className: "edit-image-refused", role: "status", children: words.imageOutOfRange(refused.range.min, refused.range.max, refused.field === 'width' ? unitWord : 'px') }))] }), _jsx("span", { className: "edit-image-handle", "data-ms-control": "", "data-free": free ? '' : undefined, title: free ? words.imageResizeFree : words.imageResizeKeep, "aria-hidden": "true", onPointerDown: (event) => {
|
|
480
566
|
event.preventDefault();
|
|
481
567
|
event.stopPropagation();
|
|
482
568
|
const now = drawn();
|
|
483
569
|
if (!now)
|
|
484
570
|
return;
|
|
485
571
|
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
486
|
-
|
|
572
|
+
const widget = frame.current?.closest('.cm-ms-inline-block, .cm-ms-block') ?? null;
|
|
573
|
+
// The picture the widget draws, the one drawn anew once it is written included.
|
|
574
|
+
const pin = pinTop(view, () => widget?.querySelector('.edit-image img') ?? null);
|
|
575
|
+
drag.current = { x: event.clientX, y: event.clientY, ...now, pointer: event.pointerId, pin };
|
|
487
576
|
if (frame.current)
|
|
488
577
|
frame.current.dataset['msBusy'] = '';
|
|
489
578
|
setShift(event.shiftKey);
|
|
@@ -501,6 +590,7 @@ export function ImageBlock({ view, source, children }) {
|
|
|
501
590
|
}
|
|
502
591
|
else
|
|
503
592
|
image.style.height = 'auto';
|
|
593
|
+
start.pin.hold();
|
|
504
594
|
// What the page lets it be: never wider than the column.
|
|
505
595
|
setDragged(drawn());
|
|
506
596
|
}, 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.3",
|
|
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.3",
|
|
83
83
|
"@types/hast": "3.0.5",
|
|
84
84
|
"@types/react": "19.2.18",
|
|
85
85
|
"@types/react-dom": "19.2.4",
|
package/structure.css
CHANGED
|
@@ -469,6 +469,11 @@
|
|
|
469
469
|
max-width: 100%;
|
|
470
470
|
vertical-align: middle;
|
|
471
471
|
}
|
|
472
|
+
/* A kept file that is not a picture, a card or a player, has no width of its own
|
|
473
|
+
to shrink the frame to: it takes the column, as in reading mode (#122). */
|
|
474
|
+
.markdown .edit-image[data-ms-file] {
|
|
475
|
+
display: block;
|
|
476
|
+
}
|
|
472
477
|
.markdown .edit-image-controls {
|
|
473
478
|
position: absolute;
|
|
474
479
|
top: var(--edit-image-bottom, 100%);
|
|
@@ -493,6 +498,11 @@
|
|
|
493
498
|
.markdown .edit-image-height {
|
|
494
499
|
width: 4.2em;
|
|
495
500
|
}
|
|
501
|
+
/* Under a share of the page, the lock, the height and its unit mean nothing: they
|
|
502
|
+
are not shown, and keep their place so the controls after them stay put (#121). */
|
|
503
|
+
.markdown .edit-image-size[data-unit='%'] > :is(.edit-control:disabled, .edit-image-height, .edit-image-unit-label) {
|
|
504
|
+
visibility: hidden;
|
|
505
|
+
}
|
|
496
506
|
.markdown .edit-image-description {
|
|
497
507
|
flex: 1 1 10rem;
|
|
498
508
|
min-width: 0;
|
package/theme-default.css
CHANGED
|
@@ -1983,9 +1983,6 @@
|
|
|
1983
1983
|
border-color: rgb(var(--ms-callout-stop));
|
|
1984
1984
|
outline: 1px solid rgb(var(--ms-callout-stop));
|
|
1985
1985
|
}
|
|
1986
|
-
.markdown .edit-image-controls .edit-field:disabled {
|
|
1987
|
-
opacity: 0.5;
|
|
1988
|
-
}
|
|
1989
1986
|
.markdown .edit-image-unit {
|
|
1990
1987
|
height: 1.9rem;
|
|
1991
1988
|
border: 1px solid var(--ms-border);
|