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/README.md
CHANGED
|
@@ -41,14 +41,14 @@ A document is an array of `QuoinBlock`, each `{ id, type, data }`. Five block ty
|
|
|
41
41
|
|
|
42
42
|
`BlockCanvas` behaves like a controlled `<input value onChange>`. Update `blocks` synchronously from `onChange`.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Applying the edit late is tolerated, as long as what you eventually hand back is one of the arrays the canvas gave you. It recognises its own output even when the echo is several keystrokes behind, so `startTransition` and a debounced `setBlocks` both keep typing intact.
|
|
45
45
|
|
|
46
46
|
```tsx
|
|
47
47
|
import { startTransition, useState } from 'react';
|
|
48
48
|
import { BlockCanvas, type QuoinBlock } from 'quoin-editor';
|
|
49
49
|
import 'quoin-editor/styles.css';
|
|
50
50
|
|
|
51
|
-
export function
|
|
51
|
+
export function TransitionEditor() {
|
|
52
52
|
const [blocks, setBlocks] = useState<QuoinBlock[]>([]);
|
|
53
53
|
return (
|
|
54
54
|
<BlockCanvas
|
|
@@ -59,6 +59,8 @@ export function BrokenEditor() {
|
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
+
What breaks is a store that rebuilds the document on the way through, so what comes back is a structural copy rather than the array the canvas emitted. Arriving behind the canvas's own state, that copy reads as a foreign document and replaces live text with an older snapshot. The damage is not a clean revert: the characters the snapshot predates are dropped and the ones typed after them are kept, so `ZZTYPED hello world` can land as `ZZYED hello wold`. A Redux or Zustand round trip that clones the document, and a server-synced store, are the usual sources.
|
|
63
|
+
|
|
62
64
|
Debounce the side effect, not the state. `useAutosave` does exactly that: it tracks the values you give it, waits for the edits to settle, then calls your `save`, and it retries a failed save with exponential backoff.
|
|
63
65
|
|
|
64
66
|
```tsx
|
|
@@ -228,6 +230,46 @@ A definition has these fields:
|
|
|
228
230
|
|
|
229
231
|
A definition that sets `alwaysEditing` should also supply a `preview`. A `readOnly` canvas draws the preview and never the editor, so without a preview the block's content is invisible there.
|
|
230
232
|
|
|
233
|
+
### Reaching uploads from a custom block
|
|
234
|
+
|
|
235
|
+
A block editor receives only `data` and `onChange`. The canvas configuration reaches it through `useQuoin()`, which returns `blockTypes`, `upload`, `resolveAssetUrl` and `onError`. It throws outside a `QuoinProvider`, so call it from a block the canvas is rendering.
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { useQuoin, type BlockEditorProps } from 'quoin-editor';
|
|
239
|
+
|
|
240
|
+
type AttachmentData = { storageKey: string; filename: string };
|
|
241
|
+
|
|
242
|
+
export function AttachmentEditor({ data, onChange }: BlockEditorProps<AttachmentData>) {
|
|
243
|
+
const { upload, resolveAssetUrl, onError } = useQuoin();
|
|
244
|
+
|
|
245
|
+
async function store(file: File) {
|
|
246
|
+
if (!upload) return;
|
|
247
|
+
try {
|
|
248
|
+
const stored = await upload(file, 'attachment');
|
|
249
|
+
onChange({ storageKey: stored.storageKey, filename: stored.filename });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
onError(error instanceof Error ? error : new Error('The upload failed.'));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<div>
|
|
257
|
+
{data.storageKey !== '' && <a href={resolveAssetUrl(data.storageKey)}>{data.filename}</a>}
|
|
258
|
+
<input
|
|
259
|
+
type="file"
|
|
260
|
+
disabled={!upload}
|
|
261
|
+
onChange={(event) => {
|
|
262
|
+
const file = event.target.files?.[0];
|
|
263
|
+
if (file) void store(file);
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`upload` is absent when the host wired none, which is why the control above disables itself rather than failing on the pick. `resolveAssetUrl` defaults to returning the key unchanged, and `onError` to `console.error`.
|
|
272
|
+
|
|
231
273
|
The preview is also how you bring your own markdown renderer. Register a `MARKDOWN` definition last, reuse the core editor, and swap the preview:
|
|
232
274
|
|
|
233
275
|
```tsx
|
|
@@ -347,6 +389,32 @@ Override any of them from your own CSS. They are declared inside `@layer base`,
|
|
|
347
389
|
|
|
348
390
|
A project that already defines shadcn-style tokens with these names inherits its theme for free, with nothing to configure.
|
|
349
391
|
|
|
392
|
+
### When a token name collides
|
|
393
|
+
|
|
394
|
+
The same unprefixed names that make that inheritance free also let a collision happen by accident. A project that already declares one of them for something else hands Quoin a value nobody meant it to read, and Quoin cannot tell that apart from a theme deliberately shared with it.
|
|
395
|
+
|
|
396
|
+
The stock Vite React + TypeScript template is the case to know about. It declares `--accent` as a bright purple and `--border` on `:root` with meanings of its own, so a fresh `npm create vite` project that installs Quoin draws the selected slash-palette row on that purple while keeping Quoin's own near-black `--accent-foreground`. The pair measures around 3.2:1, under the 4.5:1 that WCAG AA asks for, with nothing done wrong by the consumer.
|
|
397
|
+
|
|
398
|
+
Declare the tokens you did not mean to share on `.quoin` instead. The canvas root carries that class, and so do the menus, palette, select and dialog Quoin renders into `document.body`, so one rule covers all of them. It sits closer to the elements reading the token than your `:root` rule does, so it wins without `!important` and leaves your own `--accent` untouched.
|
|
399
|
+
|
|
400
|
+
```css
|
|
401
|
+
.quoin {
|
|
402
|
+
--accent: oklch(0.97 0 0);
|
|
403
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
404
|
+
--border: oklch(0.922 0 0);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.dark .quoin {
|
|
408
|
+
--accent: oklch(0.269 0 0);
|
|
409
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
410
|
+
--border: oklch(1 0 0 / 10%);
|
|
411
|
+
}
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Both rules are needed once you take a token over: pinning `--accent` on `.quoin` alone would also beat Quoin's own `.dark` value and freeze that token in its light form.
|
|
415
|
+
|
|
416
|
+
Quoin keeps the unprefixed names rather than reading `--quoin-accent`. A prefix would stop the accident, but it would also cancel the free inheritance for every project that is already relying on it, and the package cannot tell a deliberate `--accent` from an accidental one.
|
|
417
|
+
|
|
350
418
|
Dark mode keys on a `.dark` class on an ancestor (for example `<html class="dark">`), not on `prefers-color-scheme`. Quoin redefines the same variables under `.dark`, and its `dark:` variants follow the same class, so the two cannot disagree when a visitor's OS theme differs from your app's theme.
|
|
351
419
|
|
|
352
420
|
Quoin does not reset your page. The stylesheet contains no `html`, `body`, heading, list or other element rules, and no universal `*` reset. Its own reset (box sizing, zero margins, unstyled lists and form controls) applies only inside a `.quoin` element: the canvas root, and the menus, palette, select and dialog that Quoin renders into `document.body`, which carry the class themselves. Your headings, paragraphs, lists and buttons keep whatever styles you gave them, or the browser's.
|
|
@@ -393,7 +461,7 @@ export function bold(text: string, start: number, end: number): string {
|
|
|
393
461
|
}
|
|
394
462
|
```
|
|
395
463
|
|
|
396
|
-
`useMarkdownKeymap` and `useAutoGrow` are
|
|
464
|
+
`useMarkdownKeymap` and `useAutoGrow` are importable from a React Server Component, because the module that exports them carries no `'use client'` banner. Being hooks, they are callable only from a client component.
|
|
397
465
|
|
|
398
466
|
```tsx
|
|
399
467
|
'use client';
|
|
@@ -426,11 +494,21 @@ export function Notes() {
|
|
|
426
494
|
|
|
427
495
|
The keymap swallows a `/` typed at the start of a word (at the start of the text, or after a newline, space or tab, outside a code fence) and calls `onOpenPalette` instead, so open a palette of your own there, or insert the slash yourself.
|
|
428
496
|
|
|
429
|
-
|
|
497
|
+
Everything in the table is re-exported from the main `quoin-editor` entry too, alongside `TEXT_COMMANDS`, the built-in slash-palette entries. Import from `quoin-editor/headless` when you want the transforms without the components behind them, and from `quoin-editor` when you are already importing `BlockCanvas`.
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
import { toggleLinePrefix } from 'quoin-editor';
|
|
501
|
+
|
|
502
|
+
export function heading(text: string, start: number, end: number): string {
|
|
503
|
+
return toggleLinePrefix('h2', text, start, end).value;
|
|
504
|
+
}
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
The main entry carries a `'use client'` banner, so a React Server Component has to import from `quoin-editor/headless` rather than from `quoin-editor`.
|
|
430
508
|
|
|
431
509
|
## Exports
|
|
432
510
|
|
|
433
|
-
From `quoin`: `BlockCanvas`, `QuoinProvider`, `useQuoin`, `defineBlock`, `blockByType`, `paletteBlocks`, `coreBlocks`, `TEXT_COMMANDS`, `useBlockList`, `useAutosave`, `useAutoGrow`, and the headless transforms and hooks, with their types (`QuoinBlock`, `UploadFn`, `UploadResult`, `BlockDefinition`, `AnyBlockDefinition`, `BlockEditorProps`, `BlockPreviewProps`, `BlockCanvasProps` and others).
|
|
511
|
+
From `quoin-editor`: `BlockCanvas`, `QuoinProvider`, `useQuoin`, `defineBlock`, `blockByType`, `paletteBlocks`, `coreBlocks`, `TEXT_COMMANDS`, `useBlockList`, `useAutosave`, `useAutoGrow`, and the headless transforms and hooks, with their types (`QuoinBlock`, `UploadFn`, `UploadResult`, `BlockDefinition`, `AnyBlockDefinition`, `BlockEditorProps`, `BlockPreviewProps`, `BlockCanvasProps` and others).
|
|
434
512
|
|
|
435
513
|
From `quoin-editor/headless`: the transforms and the two hooks listed above, with no UI.
|
|
436
514
|
|
package/dist/index.cjs
CHANGED
|
@@ -34,14 +34,26 @@ __export(src_exports, {
|
|
|
34
34
|
BlockCanvas: () => BlockCanvas,
|
|
35
35
|
QuoinProvider: () => QuoinProvider,
|
|
36
36
|
TEXT_COMMANDS: () => TEXT_COMMANDS,
|
|
37
|
+
applyEdit: () => applyEdit,
|
|
37
38
|
blockByType: () => blockByType,
|
|
39
|
+
caretRect: () => caretRect,
|
|
40
|
+
continueList: () => continueList,
|
|
38
41
|
coreBlocks: () => coreBlocks,
|
|
39
42
|
defineBlock: () => defineBlock,
|
|
43
|
+
formatShortcut: () => formatShortcut,
|
|
44
|
+
indentList: () => indentList,
|
|
45
|
+
insertCommand: () => insertCommand,
|
|
46
|
+
lineAt: () => lineAt,
|
|
47
|
+
linkPaste: () => linkPaste,
|
|
40
48
|
paletteBlocks: () => paletteBlocks,
|
|
49
|
+
shouldOpenPalette: () => shouldOpenPalette,
|
|
50
|
+
toggleLinePrefix: () => toggleLinePrefix,
|
|
41
51
|
useAutoGrow: () => useAutoGrow,
|
|
42
52
|
useAutosave: () => useAutosave,
|
|
43
53
|
useBlockList: () => useBlockList,
|
|
44
|
-
|
|
54
|
+
useMarkdownKeymap: () => useMarkdownKeymap,
|
|
55
|
+
useQuoin: () => useQuoin,
|
|
56
|
+
wrapSelection: () => wrapSelection
|
|
45
57
|
});
|
|
46
58
|
module.exports = __toCommonJS(src_exports);
|
|
47
59
|
|
|
@@ -1144,7 +1156,10 @@ function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
|
|
|
1144
1156
|
const palette = useSlashPalette(ref);
|
|
1145
1157
|
const [shortcutsOpen, setShortcutsOpen] = React5.useState(false);
|
|
1146
1158
|
const set = (patch) => onChange({ ...data, ...patch });
|
|
1147
|
-
const commit = (
|
|
1159
|
+
const commit = React5.useCallback(
|
|
1160
|
+
(next) => onChange({ ...data, markdown: next }),
|
|
1161
|
+
[data, onChange]
|
|
1162
|
+
);
|
|
1148
1163
|
const keymap = useMarkdownKeymap({ commit, onOpenPalette: palette.openAtCaret });
|
|
1149
1164
|
function reopenBox() {
|
|
1150
1165
|
const el = ref.current;
|
|
@@ -1605,6 +1620,7 @@ function newBlockId() {
|
|
|
1605
1620
|
|
|
1606
1621
|
// src/hooks/useBlockList.ts
|
|
1607
1622
|
var removalSeq = 0;
|
|
1623
|
+
var UNDO_DEPTH = 20;
|
|
1608
1624
|
function reducer(state, action) {
|
|
1609
1625
|
switch (action.type) {
|
|
1610
1626
|
case "insertAt": {
|
|
@@ -1623,6 +1639,11 @@ function reducer(state, action) {
|
|
|
1623
1639
|
if (index === -1) return state;
|
|
1624
1640
|
const removals = new Map(state.removals);
|
|
1625
1641
|
removals.set(action.token, { block: state.blocks[index], index });
|
|
1642
|
+
while (removals.size > UNDO_DEPTH) {
|
|
1643
|
+
const oldest = removals.keys().next();
|
|
1644
|
+
if (oldest.done) break;
|
|
1645
|
+
removals.delete(oldest.value);
|
|
1646
|
+
}
|
|
1626
1647
|
return { blocks: state.blocks.filter((b) => b.id !== action.id), removals };
|
|
1627
1648
|
}
|
|
1628
1649
|
case "undo": {
|
|
@@ -1860,18 +1881,48 @@ function BlockEditor({
|
|
|
1860
1881
|
}
|
|
1861
1882
|
|
|
1862
1883
|
// src/canvas/source-offset.ts
|
|
1884
|
+
function caretPositionAt(x, y) {
|
|
1885
|
+
const doc = document;
|
|
1886
|
+
const position = doc.caretPositionFromPoint?.(x, y);
|
|
1887
|
+
if (position) return position;
|
|
1888
|
+
const range = doc.caretRangeFromPoint?.(x, y);
|
|
1889
|
+
return range ? { offsetNode: range.startContainer, offset: range.startOffset } : null;
|
|
1890
|
+
}
|
|
1891
|
+
function renderedOffsetAt(container, x, y) {
|
|
1892
|
+
const position = caretPositionAt(x, y);
|
|
1893
|
+
if (!position || position.offsetNode.nodeType !== Node.TEXT_NODE) return null;
|
|
1894
|
+
if (!container.contains(position.offsetNode)) return null;
|
|
1895
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
1896
|
+
let before = 0;
|
|
1897
|
+
while (walker.nextNode()) {
|
|
1898
|
+
if (walker.currentNode === position.offsetNode) return before + position.offset;
|
|
1899
|
+
before += walker.currentNode.textContent?.length ?? 0;
|
|
1900
|
+
}
|
|
1901
|
+
return null;
|
|
1902
|
+
}
|
|
1863
1903
|
var PROBE_LENGTHS = [40, 20, 8];
|
|
1864
|
-
function
|
|
1865
|
-
const
|
|
1866
|
-
if (
|
|
1867
|
-
const
|
|
1868
|
-
|
|
1904
|
+
function search(source, probe) {
|
|
1905
|
+
const literal = probe.clickedText.trim();
|
|
1906
|
+
if (literal === "" || literal === probe.rendered.trim()) return source.length;
|
|
1907
|
+
const candidates = [literal, literal.replace(/\s+/g, " ")];
|
|
1908
|
+
for (const text of candidates) {
|
|
1909
|
+
const whole = source.indexOf(text);
|
|
1910
|
+
if (whole !== -1) return whole;
|
|
1911
|
+
}
|
|
1869
1912
|
for (const length of PROBE_LENGTHS) {
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1913
|
+
for (const text of candidates) {
|
|
1914
|
+
if (text.length < length) continue;
|
|
1915
|
+
const at = source.indexOf(text.slice(0, length));
|
|
1916
|
+
if (at !== -1) return at;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return source.length;
|
|
1920
|
+
}
|
|
1921
|
+
function sourceOffsetFor(source, probe) {
|
|
1922
|
+
if (probe.renderedOffset !== null && probe.rendered === source) {
|
|
1923
|
+
return Math.max(0, Math.min(probe.renderedOffset, source.length));
|
|
1873
1924
|
}
|
|
1874
|
-
return
|
|
1925
|
+
return search(source, probe);
|
|
1875
1926
|
}
|
|
1876
1927
|
|
|
1877
1928
|
// src/canvas/BlockCard.tsx
|
|
@@ -1892,14 +1943,14 @@ function BlockCardImpl({
|
|
|
1892
1943
|
onUpdate,
|
|
1893
1944
|
onInsertBlock
|
|
1894
1945
|
}) {
|
|
1895
|
-
const { blockTypes } = useQuoin();
|
|
1946
|
+
const { blockTypes, onError } = useQuoin();
|
|
1896
1947
|
const definition = blockByType(blockTypes, block.type);
|
|
1897
1948
|
const Preview = definition?.preview;
|
|
1898
1949
|
const [editing, setEditing] = React8.useState(editingByDefault && !readOnly);
|
|
1899
1950
|
const editButtonRef = React8.useRef(null);
|
|
1900
1951
|
const editorContainerRef = React8.useRef(null);
|
|
1901
1952
|
const returnFocusOnClose = React8.useRef(false);
|
|
1902
|
-
const
|
|
1953
|
+
const clickedRef = React8.useRef(null);
|
|
1903
1954
|
const scrollTopRef = React8.useRef(null);
|
|
1904
1955
|
const handleInsertBlock = React8.useCallback(
|
|
1905
1956
|
(type) => onInsertBlock?.(index + 1, type),
|
|
@@ -1943,8 +1994,8 @@ function BlockCardImpl({
|
|
|
1943
1994
|
'textarea, input, [role="combobox"]'
|
|
1944
1995
|
);
|
|
1945
1996
|
if (!field) return;
|
|
1946
|
-
const clicked =
|
|
1947
|
-
|
|
1997
|
+
const clicked = clickedRef.current;
|
|
1998
|
+
clickedRef.current = null;
|
|
1948
1999
|
if (clicked === null || !(field instanceof HTMLTextAreaElement)) {
|
|
1949
2000
|
field.focus();
|
|
1950
2001
|
if (field instanceof HTMLTextAreaElement) {
|
|
@@ -1969,6 +2020,12 @@ function BlockCardImpl({
|
|
|
1969
2020
|
returnFocusOnClose.current = false;
|
|
1970
2021
|
editButtonRef.current?.focus();
|
|
1971
2022
|
}, [editing]);
|
|
2023
|
+
const reportedType = React8.useRef(null);
|
|
2024
|
+
React8.useEffect(() => {
|
|
2025
|
+
if (definition || reportedType.current === block.type) return;
|
|
2026
|
+
reportedType.current = block.type;
|
|
2027
|
+
onError(new Error(`No block type is registered for "${block.type}".`));
|
|
2028
|
+
}, [definition, block.type, onError]);
|
|
1972
2029
|
if (definition?.alwaysEditing && !readOnly) {
|
|
1973
2030
|
return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
1974
2031
|
BlockEditor,
|
|
@@ -2008,7 +2065,7 @@ function BlockCardImpl({
|
|
|
2008
2065
|
}
|
|
2009
2066
|
);
|
|
2010
2067
|
}
|
|
2011
|
-
const rest = Preview ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Preview, { data: block.data, blockId: block.id }) :
|
|
2068
|
+
const rest = Preview ? /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(Preview, { data: block.data, blockId: block.id }) : /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("p", { className: "py-1 text-sm text-muted-foreground", children: definition?.label ?? block.type });
|
|
2012
2069
|
if (readOnly) return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { children: rest });
|
|
2013
2070
|
return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)("div", { className: "group/card relative", children: [
|
|
2014
2071
|
/* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
|
|
@@ -2018,7 +2075,11 @@ function BlockCardImpl({
|
|
|
2018
2075
|
const target = e.target;
|
|
2019
2076
|
if (target.closest(INTERACTIVE_SELECTOR)) return;
|
|
2020
2077
|
if (hasSelectionWithin(e.currentTarget)) return;
|
|
2021
|
-
|
|
2078
|
+
clickedRef.current = {
|
|
2079
|
+
rendered: e.currentTarget.textContent ?? "",
|
|
2080
|
+
clickedText: target.textContent ?? "",
|
|
2081
|
+
renderedOffset: renderedOffsetAt(e.currentTarget, e.clientX, e.clientY)
|
|
2082
|
+
};
|
|
2022
2083
|
scrollTopRef.current = (document.scrollingElement ?? document.documentElement).scrollTop;
|
|
2023
2084
|
setEditing(true);
|
|
2024
2085
|
},
|
|
@@ -2252,6 +2313,15 @@ function SortableBlock({
|
|
|
2252
2313
|
|
|
2253
2314
|
// src/canvas/BlockCanvas.tsx
|
|
2254
2315
|
var import_jsx_runtime26 = require("react/jsx-runtime");
|
|
2316
|
+
var UNACKNOWLEDGED_LIMIT = 64;
|
|
2317
|
+
function remember(outstanding, blocks) {
|
|
2318
|
+
outstanding.add(blocks);
|
|
2319
|
+
while (outstanding.size > UNACKNOWLEDGED_LIMIT) {
|
|
2320
|
+
const oldest = outstanding.values().next();
|
|
2321
|
+
if (oldest.done) return;
|
|
2322
|
+
outstanding.delete(oldest.value);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2255
2325
|
function Canvas({
|
|
2256
2326
|
blocks,
|
|
2257
2327
|
onChange,
|
|
@@ -2278,16 +2348,22 @@ function Canvas({
|
|
|
2278
2348
|
);
|
|
2279
2349
|
const lastEmitted = React11.useRef(blocks);
|
|
2280
2350
|
const lastSupplied = React11.useRef(blocks);
|
|
2351
|
+
const unacknowledged = React11.useRef(/* @__PURE__ */ new Set());
|
|
2281
2352
|
React11.useEffect(() => {
|
|
2282
2353
|
if (list.blocks === lastEmitted.current) return;
|
|
2283
2354
|
lastEmitted.current = list.blocks;
|
|
2355
|
+
remember(unacknowledged.current, list.blocks);
|
|
2284
2356
|
onChange(list.blocks);
|
|
2285
2357
|
}, [list.blocks, onChange]);
|
|
2286
2358
|
React11.useEffect(() => {
|
|
2287
2359
|
const previous = lastSupplied.current;
|
|
2288
2360
|
lastSupplied.current = blocks;
|
|
2289
|
-
if (
|
|
2361
|
+
if (unacknowledged.current.has(blocks)) {
|
|
2362
|
+
if (blocks === lastEmitted.current) unacknowledged.current.clear();
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2290
2365
|
if (sameBlocks(blocks, previous) || sameBlocks(blocks, list.blocks)) return;
|
|
2366
|
+
unacknowledged.current.clear();
|
|
2291
2367
|
lastEmitted.current = blocks;
|
|
2292
2368
|
reset(blocks);
|
|
2293
2369
|
}, [blocks, reset]);
|
|
@@ -2493,13 +2569,25 @@ function useAutosave({
|
|
|
2493
2569
|
BlockCanvas,
|
|
2494
2570
|
QuoinProvider,
|
|
2495
2571
|
TEXT_COMMANDS,
|
|
2572
|
+
applyEdit,
|
|
2496
2573
|
blockByType,
|
|
2574
|
+
caretRect,
|
|
2575
|
+
continueList,
|
|
2497
2576
|
coreBlocks,
|
|
2498
2577
|
defineBlock,
|
|
2578
|
+
formatShortcut,
|
|
2579
|
+
indentList,
|
|
2580
|
+
insertCommand,
|
|
2581
|
+
lineAt,
|
|
2582
|
+
linkPaste,
|
|
2499
2583
|
paletteBlocks,
|
|
2584
|
+
shouldOpenPalette,
|
|
2585
|
+
toggleLinePrefix,
|
|
2500
2586
|
useAutoGrow,
|
|
2501
2587
|
useAutosave,
|
|
2502
2588
|
useBlockList,
|
|
2503
|
-
|
|
2589
|
+
useMarkdownKeymap,
|
|
2590
|
+
useQuoin,
|
|
2591
|
+
wrapSelection
|
|
2504
2592
|
});
|
|
2505
2593
|
//# sourceMappingURL=index.cjs.map
|