quoin-editor 0.1.0 → 0.1.1
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 +83 -5
- package/dist/index.cjs +107 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +80 -4
- package/dist/index.d.ts +80 -4
- package/dist/index.js +94 -18
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +40 -11
package/dist/index.d.cts
CHANGED
|
@@ -87,9 +87,12 @@ declare function paletteBlocks(definitions: AnyBlockDefinition[]): AnyBlockDefin
|
|
|
87
87
|
/** Props for `BlockCanvas`; `blocks` and `onChange` are the whole document contract. */
|
|
88
88
|
type BlockCanvasProps = {
|
|
89
89
|
/**
|
|
90
|
-
* The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange>`.
|
|
91
|
+
* Applying the edit late is tolerated as long as what comes back is one of the arrays the canvas emitted:
|
|
92
|
+
* it recognises its own output even several keystrokes behind, so a transition or a debounced setter is safe.
|
|
93
|
+
* A store that rebuilds the document on the way through hands back a structural copy instead, which arrives
|
|
94
|
+
* behind the canvas's own state and reads as a foreign document. That does not revert the edit cleanly: the
|
|
95
|
+
* characters the copy predates are dropped and the ones typed after them are kept.
|
|
93
96
|
* A new array that differs from the last one you supplied replaces what the canvas shows, which is how a host
|
|
94
97
|
* loads, undoes or merges. An array structurally equal to the last one you supplied is ignored, so a "discard
|
|
95
98
|
* changes" button that hands back the original document does nothing; remount the canvas with a React `key` to
|
|
@@ -145,6 +148,7 @@ type BlockList = {
|
|
|
145
148
|
blocks: QuoinBlock[];
|
|
146
149
|
insertAt: (index: number, type: string, data?: Record<string, unknown>) => string;
|
|
147
150
|
update: (id: string, data: Record<string, unknown>) => void;
|
|
151
|
+
/** Returns a token `undo` restores that block with; only the last 20 removals stay restorable. */
|
|
148
152
|
remove: (id: string) => UndoToken;
|
|
149
153
|
undo: (token: UndoToken) => void;
|
|
150
154
|
move: (index: number, dir: -1 | 1) => void;
|
|
@@ -203,4 +207,76 @@ type TextCommand = {
|
|
|
203
207
|
/** The built-in slash-palette commands, in display order. */
|
|
204
208
|
declare const TEXT_COMMANDS: TextCommand[];
|
|
205
209
|
|
|
206
|
-
|
|
210
|
+
/** A text transform result: the next value and where the selection should land. */
|
|
211
|
+
type Edit = {
|
|
212
|
+
value: string;
|
|
213
|
+
selectionStart: number;
|
|
214
|
+
selectionEnd: number;
|
|
215
|
+
};
|
|
216
|
+
/** Applies an edit to a textarea through the native undo stack when the browser allows it, so Ctrl+Z still works. */
|
|
217
|
+
declare function applyEdit(el: HTMLTextAreaElement, edit: Edit, commit: (value: string) => void): void;
|
|
218
|
+
|
|
219
|
+
type Line = {
|
|
220
|
+
start: number;
|
|
221
|
+
end: number;
|
|
222
|
+
text: string;
|
|
223
|
+
};
|
|
224
|
+
/** The line containing the offset, with its start and end offsets and its text (no trailing newline). */
|
|
225
|
+
declare function lineAt(value: string, offset: number): Line;
|
|
226
|
+
|
|
227
|
+
/** A line-level markdown prefix: a heading level, a bullet or a numbered item. */
|
|
228
|
+
type LinePrefix = 'h1' | 'h2' | 'h3' | 'bullet' | 'ordered';
|
|
229
|
+
/** Adds the prefix to every line in the selection, or removes it when every line already carries it; fenced code is left alone. */
|
|
230
|
+
declare function toggleLinePrefix(kind: LinePrefix, value: string, selectionStart: number, selectionEnd: number): Edit;
|
|
231
|
+
|
|
232
|
+
/** Continues a list item on Enter, or climbs out of the list on an empty item or Backspace at the marker; returns null when the caret is not in a list item or is inside a code fence, so the default key behaviour runs. */
|
|
233
|
+
declare function continueList(value: string, caret: number, key: 'Enter' | 'Backspace'): Edit | null;
|
|
234
|
+
|
|
235
|
+
/** Indents or outdents the selected list items; returns null when the selection is not entirely list items, is inside a code fence, or cannot move further, so Tab keeps its default behaviour. */
|
|
236
|
+
declare function indentList(value: string, selectionStart: number, selectionEnd: number, direction: 1 | -1): Edit | null;
|
|
237
|
+
|
|
238
|
+
/** An inline markdown wrapper: bold, italic, code or a link. */
|
|
239
|
+
type WrapKind = 'bold' | 'italic' | 'code' | 'link';
|
|
240
|
+
/** Wraps the selection in the markers for the kind, or removes them when it is already wrapped. */
|
|
241
|
+
declare function wrapSelection(kind: WrapKind, value: string, selectionStart: number, selectionEnd: number): Edit;
|
|
242
|
+
/** Turns a pasted URL over a selection into a markdown link; returns null when nothing is selected or the paste is not a bare URL, so the paste goes through unchanged. */
|
|
243
|
+
declare function linkPaste(value: string, selectionStart: number, selectionEnd: number, pasted: string): Edit | null;
|
|
244
|
+
|
|
245
|
+
/** The fields `insertCommand` needs. `TextCommand` satisfies this structurally. */
|
|
246
|
+
type InsertableCommand = {
|
|
247
|
+
insert: string;
|
|
248
|
+
caretOffset: number;
|
|
249
|
+
selectLength?: number;
|
|
250
|
+
separate: boolean;
|
|
251
|
+
};
|
|
252
|
+
/** Inserts a command's markdown at the caret, starting a new line (or a blank line if it asks) when the caret is mid-line. */
|
|
253
|
+
declare function insertCommand(command: InsertableCommand, value: string, caret: number): Edit;
|
|
254
|
+
|
|
255
|
+
/** Whether typing "/" here should open the command palette: only at a word start, with no selection and outside a code fence. */
|
|
256
|
+
declare function shouldOpenPalette(value: string, selectionStart: number, selectionEnd: number): boolean;
|
|
257
|
+
|
|
258
|
+
/** Renders a "mod+shift+b" style hint as the platform's own label, such as ⌘⇧B on a Mac and Ctrl+Shift+B elsewhere. */
|
|
259
|
+
declare function formatShortcut(hint: string): string;
|
|
260
|
+
|
|
261
|
+
/** The caret's position and line height, in pixels relative to the textarea's top left corner. */
|
|
262
|
+
type CaretRect = {
|
|
263
|
+
top: number;
|
|
264
|
+
left: number;
|
|
265
|
+
height: number;
|
|
266
|
+
};
|
|
267
|
+
/** A textarea exposes no caret geometry, so the text before the caret is laid
|
|
268
|
+
* out again in a hidden div that copies the box's own metrics, and a marker at
|
|
269
|
+
* the end of it is measured. Returns null when the textarea is not laid out
|
|
270
|
+
* (hidden or detached), because there is nothing to measure. */
|
|
271
|
+
declare function caretRect(el: HTMLTextAreaElement): CaretRect | null;
|
|
272
|
+
|
|
273
|
+
/** Keyboard and paste handlers that give a plain textarea markdown shortcuts, list continuation, Tab indent and the slash palette trigger. */
|
|
274
|
+
declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
|
|
275
|
+
commit: (markdown: string) => void;
|
|
276
|
+
onOpenPalette: () => void;
|
|
277
|
+
}): {
|
|
278
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
279
|
+
onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
|
package/dist/index.d.ts
CHANGED
|
@@ -87,9 +87,12 @@ declare function paletteBlocks(definitions: AnyBlockDefinition[]): AnyBlockDefin
|
|
|
87
87
|
/** Props for `BlockCanvas`; `blocks` and `onChange` are the whole document contract. */
|
|
88
88
|
type BlockCanvasProps = {
|
|
89
89
|
/**
|
|
90
|
-
* The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* The document. Update it synchronously from `onChange`, as with a controlled `<input value onChange>`.
|
|
91
|
+
* Applying the edit late is tolerated as long as what comes back is one of the arrays the canvas emitted:
|
|
92
|
+
* it recognises its own output even several keystrokes behind, so a transition or a debounced setter is safe.
|
|
93
|
+
* A store that rebuilds the document on the way through hands back a structural copy instead, which arrives
|
|
94
|
+
* behind the canvas's own state and reads as a foreign document. That does not revert the edit cleanly: the
|
|
95
|
+
* characters the copy predates are dropped and the ones typed after them are kept.
|
|
93
96
|
* A new array that differs from the last one you supplied replaces what the canvas shows, which is how a host
|
|
94
97
|
* loads, undoes or merges. An array structurally equal to the last one you supplied is ignored, so a "discard
|
|
95
98
|
* changes" button that hands back the original document does nothing; remount the canvas with a React `key` to
|
|
@@ -145,6 +148,7 @@ type BlockList = {
|
|
|
145
148
|
blocks: QuoinBlock[];
|
|
146
149
|
insertAt: (index: number, type: string, data?: Record<string, unknown>) => string;
|
|
147
150
|
update: (id: string, data: Record<string, unknown>) => void;
|
|
151
|
+
/** Returns a token `undo` restores that block with; only the last 20 removals stay restorable. */
|
|
148
152
|
remove: (id: string) => UndoToken;
|
|
149
153
|
undo: (token: UndoToken) => void;
|
|
150
154
|
move: (index: number, dir: -1 | 1) => void;
|
|
@@ -203,4 +207,76 @@ type TextCommand = {
|
|
|
203
207
|
/** The built-in slash-palette commands, in display order. */
|
|
204
208
|
declare const TEXT_COMMANDS: TextCommand[];
|
|
205
209
|
|
|
206
|
-
|
|
210
|
+
/** A text transform result: the next value and where the selection should land. */
|
|
211
|
+
type Edit = {
|
|
212
|
+
value: string;
|
|
213
|
+
selectionStart: number;
|
|
214
|
+
selectionEnd: number;
|
|
215
|
+
};
|
|
216
|
+
/** Applies an edit to a textarea through the native undo stack when the browser allows it, so Ctrl+Z still works. */
|
|
217
|
+
declare function applyEdit(el: HTMLTextAreaElement, edit: Edit, commit: (value: string) => void): void;
|
|
218
|
+
|
|
219
|
+
type Line = {
|
|
220
|
+
start: number;
|
|
221
|
+
end: number;
|
|
222
|
+
text: string;
|
|
223
|
+
};
|
|
224
|
+
/** The line containing the offset, with its start and end offsets and its text (no trailing newline). */
|
|
225
|
+
declare function lineAt(value: string, offset: number): Line;
|
|
226
|
+
|
|
227
|
+
/** A line-level markdown prefix: a heading level, a bullet or a numbered item. */
|
|
228
|
+
type LinePrefix = 'h1' | 'h2' | 'h3' | 'bullet' | 'ordered';
|
|
229
|
+
/** Adds the prefix to every line in the selection, or removes it when every line already carries it; fenced code is left alone. */
|
|
230
|
+
declare function toggleLinePrefix(kind: LinePrefix, value: string, selectionStart: number, selectionEnd: number): Edit;
|
|
231
|
+
|
|
232
|
+
/** Continues a list item on Enter, or climbs out of the list on an empty item or Backspace at the marker; returns null when the caret is not in a list item or is inside a code fence, so the default key behaviour runs. */
|
|
233
|
+
declare function continueList(value: string, caret: number, key: 'Enter' | 'Backspace'): Edit | null;
|
|
234
|
+
|
|
235
|
+
/** Indents or outdents the selected list items; returns null when the selection is not entirely list items, is inside a code fence, or cannot move further, so Tab keeps its default behaviour. */
|
|
236
|
+
declare function indentList(value: string, selectionStart: number, selectionEnd: number, direction: 1 | -1): Edit | null;
|
|
237
|
+
|
|
238
|
+
/** An inline markdown wrapper: bold, italic, code or a link. */
|
|
239
|
+
type WrapKind = 'bold' | 'italic' | 'code' | 'link';
|
|
240
|
+
/** Wraps the selection in the markers for the kind, or removes them when it is already wrapped. */
|
|
241
|
+
declare function wrapSelection(kind: WrapKind, value: string, selectionStart: number, selectionEnd: number): Edit;
|
|
242
|
+
/** Turns a pasted URL over a selection into a markdown link; returns null when nothing is selected or the paste is not a bare URL, so the paste goes through unchanged. */
|
|
243
|
+
declare function linkPaste(value: string, selectionStart: number, selectionEnd: number, pasted: string): Edit | null;
|
|
244
|
+
|
|
245
|
+
/** The fields `insertCommand` needs. `TextCommand` satisfies this structurally. */
|
|
246
|
+
type InsertableCommand = {
|
|
247
|
+
insert: string;
|
|
248
|
+
caretOffset: number;
|
|
249
|
+
selectLength?: number;
|
|
250
|
+
separate: boolean;
|
|
251
|
+
};
|
|
252
|
+
/** Inserts a command's markdown at the caret, starting a new line (or a blank line if it asks) when the caret is mid-line. */
|
|
253
|
+
declare function insertCommand(command: InsertableCommand, value: string, caret: number): Edit;
|
|
254
|
+
|
|
255
|
+
/** Whether typing "/" here should open the command palette: only at a word start, with no selection and outside a code fence. */
|
|
256
|
+
declare function shouldOpenPalette(value: string, selectionStart: number, selectionEnd: number): boolean;
|
|
257
|
+
|
|
258
|
+
/** Renders a "mod+shift+b" style hint as the platform's own label, such as ⌘⇧B on a Mac and Ctrl+Shift+B elsewhere. */
|
|
259
|
+
declare function formatShortcut(hint: string): string;
|
|
260
|
+
|
|
261
|
+
/** The caret's position and line height, in pixels relative to the textarea's top left corner. */
|
|
262
|
+
type CaretRect = {
|
|
263
|
+
top: number;
|
|
264
|
+
left: number;
|
|
265
|
+
height: number;
|
|
266
|
+
};
|
|
267
|
+
/** A textarea exposes no caret geometry, so the text before the caret is laid
|
|
268
|
+
* out again in a hidden div that copies the box's own metrics, and a marker at
|
|
269
|
+
* the end of it is measured. Returns null when the textarea is not laid out
|
|
270
|
+
* (hidden or detached), because there is nothing to measure. */
|
|
271
|
+
declare function caretRect(el: HTMLTextAreaElement): CaretRect | null;
|
|
272
|
+
|
|
273
|
+
/** Keyboard and paste handlers that give a plain textarea markdown shortcuts, list continuation, Tab indent and the slash palette trigger. */
|
|
274
|
+
declare function useMarkdownKeymap({ commit, onOpenPalette, }: {
|
|
275
|
+
commit: (markdown: string) => void;
|
|
276
|
+
onOpenPalette: () => void;
|
|
277
|
+
}): {
|
|
278
|
+
onKeyDown: (event: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
|
279
|
+
onPaste: (event: React.ClipboardEvent<HTMLTextAreaElement>) => void;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export { type AnyBlockDefinition, type AutosaveStatus, BlockCanvas, type BlockCanvasProps, type BlockDefinition, type BlockEditorProps, type BlockList, type BlockPreviewProps, type CaretRect, type Edit, type InsertableCommand, type LinePrefix, type QuoinBlock, type QuoinConfig, type QuoinConfigInput, QuoinProvider, TEXT_COMMANDS, type TextCommand, type UndoToken, type UploadFn, type UploadResult, type UseAutosaveInput, type UseAutosaveResult, type WrapKind, applyEdit, blockByType, caretRect, continueList, coreBlocks, defineBlock, formatShortcut, indentList, insertCommand, lineAt, linkPaste, paletteBlocks, shouldOpenPalette, toggleLinePrefix, useAutoGrow, useAutosave, useBlockList, useMarkdownKeymap, useQuoin, wrapSelection };
|
package/dist/index.js
CHANGED
|
@@ -1117,7 +1117,10 @@ function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
|
|
|
1117
1117
|
const palette = useSlashPalette(ref);
|
|
1118
1118
|
const [shortcutsOpen, setShortcutsOpen] = React5.useState(false);
|
|
1119
1119
|
const set = (patch) => onChange({ ...data, ...patch });
|
|
1120
|
-
const commit = (
|
|
1120
|
+
const commit = React5.useCallback(
|
|
1121
|
+
(next) => onChange({ ...data, markdown: next }),
|
|
1122
|
+
[data, onChange]
|
|
1123
|
+
);
|
|
1121
1124
|
const keymap = useMarkdownKeymap({ commit, onOpenPalette: palette.openAtCaret });
|
|
1122
1125
|
function reopenBox() {
|
|
1123
1126
|
const el = ref.current;
|
|
@@ -1578,6 +1581,7 @@ function newBlockId() {
|
|
|
1578
1581
|
|
|
1579
1582
|
// src/hooks/useBlockList.ts
|
|
1580
1583
|
var removalSeq = 0;
|
|
1584
|
+
var UNDO_DEPTH = 20;
|
|
1581
1585
|
function reducer(state, action) {
|
|
1582
1586
|
switch (action.type) {
|
|
1583
1587
|
case "insertAt": {
|
|
@@ -1596,6 +1600,11 @@ function reducer(state, action) {
|
|
|
1596
1600
|
if (index === -1) return state;
|
|
1597
1601
|
const removals = new Map(state.removals);
|
|
1598
1602
|
removals.set(action.token, { block: state.blocks[index], index });
|
|
1603
|
+
while (removals.size > UNDO_DEPTH) {
|
|
1604
|
+
const oldest = removals.keys().next();
|
|
1605
|
+
if (oldest.done) break;
|
|
1606
|
+
removals.delete(oldest.value);
|
|
1607
|
+
}
|
|
1599
1608
|
return { blocks: state.blocks.filter((b) => b.id !== action.id), removals };
|
|
1600
1609
|
}
|
|
1601
1610
|
case "undo": {
|
|
@@ -1833,18 +1842,48 @@ function BlockEditor({
|
|
|
1833
1842
|
}
|
|
1834
1843
|
|
|
1835
1844
|
// src/canvas/source-offset.ts
|
|
1845
|
+
function caretPositionAt(x, y) {
|
|
1846
|
+
const doc = document;
|
|
1847
|
+
const position = doc.caretPositionFromPoint?.(x, y);
|
|
1848
|
+
if (position) return position;
|
|
1849
|
+
const range = doc.caretRangeFromPoint?.(x, y);
|
|
1850
|
+
return range ? { offsetNode: range.startContainer, offset: range.startOffset } : null;
|
|
1851
|
+
}
|
|
1852
|
+
function renderedOffsetAt(container, x, y) {
|
|
1853
|
+
const position = caretPositionAt(x, y);
|
|
1854
|
+
if (!position || position.offsetNode.nodeType !== Node.TEXT_NODE) return null;
|
|
1855
|
+
if (!container.contains(position.offsetNode)) return null;
|
|
1856
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
1857
|
+
let before = 0;
|
|
1858
|
+
while (walker.nextNode()) {
|
|
1859
|
+
if (walker.currentNode === position.offsetNode) return before + position.offset;
|
|
1860
|
+
before += walker.currentNode.textContent?.length ?? 0;
|
|
1861
|
+
}
|
|
1862
|
+
return null;
|
|
1863
|
+
}
|
|
1836
1864
|
var PROBE_LENGTHS = [40, 20, 8];
|
|
1837
|
-
function
|
|
1838
|
-
const
|
|
1839
|
-
if (
|
|
1840
|
-
const
|
|
1841
|
-
|
|
1865
|
+
function search(source, probe) {
|
|
1866
|
+
const literal = probe.clickedText.trim();
|
|
1867
|
+
if (literal === "" || literal === probe.rendered.trim()) return source.length;
|
|
1868
|
+
const candidates = [literal, literal.replace(/\s+/g, " ")];
|
|
1869
|
+
for (const text of candidates) {
|
|
1870
|
+
const whole = source.indexOf(text);
|
|
1871
|
+
if (whole !== -1) return whole;
|
|
1872
|
+
}
|
|
1842
1873
|
for (const length of PROBE_LENGTHS) {
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1874
|
+
for (const text of candidates) {
|
|
1875
|
+
if (text.length < length) continue;
|
|
1876
|
+
const at = source.indexOf(text.slice(0, length));
|
|
1877
|
+
if (at !== -1) return at;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return source.length;
|
|
1881
|
+
}
|
|
1882
|
+
function sourceOffsetFor(source, probe) {
|
|
1883
|
+
if (probe.renderedOffset !== null && probe.rendered === source) {
|
|
1884
|
+
return Math.max(0, Math.min(probe.renderedOffset, source.length));
|
|
1846
1885
|
}
|
|
1847
|
-
return
|
|
1886
|
+
return search(source, probe);
|
|
1848
1887
|
}
|
|
1849
1888
|
|
|
1850
1889
|
// src/canvas/BlockCard.tsx
|
|
@@ -1865,14 +1904,14 @@ function BlockCardImpl({
|
|
|
1865
1904
|
onUpdate,
|
|
1866
1905
|
onInsertBlock
|
|
1867
1906
|
}) {
|
|
1868
|
-
const { blockTypes } = useQuoin();
|
|
1907
|
+
const { blockTypes, onError } = useQuoin();
|
|
1869
1908
|
const definition = blockByType(blockTypes, block.type);
|
|
1870
1909
|
const Preview = definition?.preview;
|
|
1871
1910
|
const [editing, setEditing] = React8.useState(editingByDefault && !readOnly);
|
|
1872
1911
|
const editButtonRef = React8.useRef(null);
|
|
1873
1912
|
const editorContainerRef = React8.useRef(null);
|
|
1874
1913
|
const returnFocusOnClose = React8.useRef(false);
|
|
1875
|
-
const
|
|
1914
|
+
const clickedRef = React8.useRef(null);
|
|
1876
1915
|
const scrollTopRef = React8.useRef(null);
|
|
1877
1916
|
const handleInsertBlock = React8.useCallback(
|
|
1878
1917
|
(type) => onInsertBlock?.(index + 1, type),
|
|
@@ -1916,8 +1955,8 @@ function BlockCardImpl({
|
|
|
1916
1955
|
'textarea, input, [role="combobox"]'
|
|
1917
1956
|
);
|
|
1918
1957
|
if (!field) return;
|
|
1919
|
-
const clicked =
|
|
1920
|
-
|
|
1958
|
+
const clicked = clickedRef.current;
|
|
1959
|
+
clickedRef.current = null;
|
|
1921
1960
|
if (clicked === null || !(field instanceof HTMLTextAreaElement)) {
|
|
1922
1961
|
field.focus();
|
|
1923
1962
|
if (field instanceof HTMLTextAreaElement) {
|
|
@@ -1942,6 +1981,12 @@ function BlockCardImpl({
|
|
|
1942
1981
|
returnFocusOnClose.current = false;
|
|
1943
1982
|
editButtonRef.current?.focus();
|
|
1944
1983
|
}, [editing]);
|
|
1984
|
+
const reportedType = React8.useRef(null);
|
|
1985
|
+
React8.useEffect(() => {
|
|
1986
|
+
if (definition || reportedType.current === block.type) return;
|
|
1987
|
+
reportedType.current = block.type;
|
|
1988
|
+
onError(new Error(`No block type is registered for "${block.type}".`));
|
|
1989
|
+
}, [definition, block.type, onError]);
|
|
1945
1990
|
if (definition?.alwaysEditing && !readOnly) {
|
|
1946
1991
|
return /* @__PURE__ */ jsx22(
|
|
1947
1992
|
BlockEditor,
|
|
@@ -1981,7 +2026,7 @@ function BlockCardImpl({
|
|
|
1981
2026
|
}
|
|
1982
2027
|
);
|
|
1983
2028
|
}
|
|
1984
|
-
const rest = Preview ? /* @__PURE__ */ jsx22(Preview, { data: block.data, blockId: block.id }) :
|
|
2029
|
+
const rest = Preview ? /* @__PURE__ */ jsx22(Preview, { data: block.data, blockId: block.id }) : /* @__PURE__ */ jsx22("p", { className: "py-1 text-sm text-muted-foreground", children: definition?.label ?? block.type });
|
|
1985
2030
|
if (readOnly) return /* @__PURE__ */ jsx22("div", { children: rest });
|
|
1986
2031
|
return /* @__PURE__ */ jsxs15("div", { className: "group/card relative", children: [
|
|
1987
2032
|
/* @__PURE__ */ jsx22(
|
|
@@ -1991,7 +2036,11 @@ function BlockCardImpl({
|
|
|
1991
2036
|
const target = e.target;
|
|
1992
2037
|
if (target.closest(INTERACTIVE_SELECTOR)) return;
|
|
1993
2038
|
if (hasSelectionWithin(e.currentTarget)) return;
|
|
1994
|
-
|
|
2039
|
+
clickedRef.current = {
|
|
2040
|
+
rendered: e.currentTarget.textContent ?? "",
|
|
2041
|
+
clickedText: target.textContent ?? "",
|
|
2042
|
+
renderedOffset: renderedOffsetAt(e.currentTarget, e.clientX, e.clientY)
|
|
2043
|
+
};
|
|
1995
2044
|
scrollTopRef.current = (document.scrollingElement ?? document.documentElement).scrollTop;
|
|
1996
2045
|
setEditing(true);
|
|
1997
2046
|
},
|
|
@@ -2225,6 +2274,15 @@ function SortableBlock({
|
|
|
2225
2274
|
|
|
2226
2275
|
// src/canvas/BlockCanvas.tsx
|
|
2227
2276
|
import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2277
|
+
var UNACKNOWLEDGED_LIMIT = 64;
|
|
2278
|
+
function remember(outstanding, blocks) {
|
|
2279
|
+
outstanding.add(blocks);
|
|
2280
|
+
while (outstanding.size > UNACKNOWLEDGED_LIMIT) {
|
|
2281
|
+
const oldest = outstanding.values().next();
|
|
2282
|
+
if (oldest.done) return;
|
|
2283
|
+
outstanding.delete(oldest.value);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2228
2286
|
function Canvas({
|
|
2229
2287
|
blocks,
|
|
2230
2288
|
onChange,
|
|
@@ -2251,16 +2309,22 @@ function Canvas({
|
|
|
2251
2309
|
);
|
|
2252
2310
|
const lastEmitted = React11.useRef(blocks);
|
|
2253
2311
|
const lastSupplied = React11.useRef(blocks);
|
|
2312
|
+
const unacknowledged = React11.useRef(/* @__PURE__ */ new Set());
|
|
2254
2313
|
React11.useEffect(() => {
|
|
2255
2314
|
if (list.blocks === lastEmitted.current) return;
|
|
2256
2315
|
lastEmitted.current = list.blocks;
|
|
2316
|
+
remember(unacknowledged.current, list.blocks);
|
|
2257
2317
|
onChange(list.blocks);
|
|
2258
2318
|
}, [list.blocks, onChange]);
|
|
2259
2319
|
React11.useEffect(() => {
|
|
2260
2320
|
const previous = lastSupplied.current;
|
|
2261
2321
|
lastSupplied.current = blocks;
|
|
2262
|
-
if (
|
|
2322
|
+
if (unacknowledged.current.has(blocks)) {
|
|
2323
|
+
if (blocks === lastEmitted.current) unacknowledged.current.clear();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2263
2326
|
if (sameBlocks(blocks, previous) || sameBlocks(blocks, list.blocks)) return;
|
|
2327
|
+
unacknowledged.current.clear();
|
|
2264
2328
|
lastEmitted.current = blocks;
|
|
2265
2329
|
reset(blocks);
|
|
2266
2330
|
}, [blocks, reset]);
|
|
@@ -2465,13 +2529,25 @@ export {
|
|
|
2465
2529
|
BlockCanvas,
|
|
2466
2530
|
QuoinProvider,
|
|
2467
2531
|
TEXT_COMMANDS,
|
|
2532
|
+
applyEdit,
|
|
2468
2533
|
blockByType,
|
|
2534
|
+
caretRect,
|
|
2535
|
+
continueList,
|
|
2469
2536
|
coreBlocks,
|
|
2470
2537
|
defineBlock,
|
|
2538
|
+
formatShortcut,
|
|
2539
|
+
indentList,
|
|
2540
|
+
insertCommand,
|
|
2541
|
+
lineAt,
|
|
2542
|
+
linkPaste,
|
|
2471
2543
|
paletteBlocks,
|
|
2544
|
+
shouldOpenPalette,
|
|
2545
|
+
toggleLinePrefix,
|
|
2472
2546
|
useAutoGrow,
|
|
2473
2547
|
useAutosave,
|
|
2474
2548
|
useBlockList,
|
|
2475
|
-
|
|
2549
|
+
useMarkdownKeymap,
|
|
2550
|
+
useQuoin,
|
|
2551
|
+
wrapSelection
|
|
2476
2552
|
};
|
|
2477
2553
|
//# sourceMappingURL=index.js.map
|