quoin-editor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -143,12 +143,7 @@ function Button({
143
143
 
144
144
  // src/upload/FilePickerButton.tsx
145
145
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
146
- function FilePickerButton({
147
- label,
148
- accept,
149
- disabled,
150
- onPick
151
- }) {
146
+ function FilePickerButton({ label, accept, disabled, onPick }) {
152
147
  return /* @__PURE__ */ jsxs("span", { className: "relative inline-flex rounded-md has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50", children: [
153
148
  /* @__PURE__ */ jsx3(Button, { asChild: true, variant: "outline", size: "sm", children: /* @__PURE__ */ jsxs("span", { "aria-hidden": "true", className: disabled ? "pointer-events-none opacity-50" : void 0, children: [
154
149
  /* @__PURE__ */ jsx3(Upload, { "aria-hidden": "true" }),
@@ -1117,7 +1112,10 @@ function MarkdownBlockEditor({ data, onChange, onInsertBlock, onDone }) {
1117
1112
  const palette = useSlashPalette(ref);
1118
1113
  const [shortcutsOpen, setShortcutsOpen] = React5.useState(false);
1119
1114
  const set = (patch) => onChange({ ...data, ...patch });
1120
- const commit = (next) => onChange({ ...data, markdown: next });
1115
+ const commit = React5.useCallback(
1116
+ (next) => onChange({ ...data, markdown: next }),
1117
+ [data, onChange]
1118
+ );
1121
1119
  const keymap = useMarkdownKeymap({ commit, onOpenPalette: palette.openAtCaret });
1122
1120
  function reopenBox() {
1123
1121
  const el = ref.current;
@@ -1363,12 +1361,7 @@ import { useEffect as useEffect2, useRef as useRef3 } from "react";
1363
1361
  // src/upload/UploadField.tsx
1364
1362
  import * as React6 from "react";
1365
1363
  import { jsx as jsx15, jsxs as jsxs10 } from "react/jsx-runtime";
1366
- function UploadField({
1367
- category,
1368
- label,
1369
- accept,
1370
- onUploaded
1371
- }) {
1364
+ function UploadField({ category, label, accept, onUploaded }) {
1372
1365
  const { upload, onError } = useQuoin();
1373
1366
  const [busy, setBusy] = React6.useState(false);
1374
1367
  const [error, setError] = React6.useState(null);
@@ -1578,6 +1571,7 @@ function newBlockId() {
1578
1571
 
1579
1572
  // src/hooks/useBlockList.ts
1580
1573
  var removalSeq = 0;
1574
+ var UNDO_DEPTH = 20;
1581
1575
  function reducer(state, action) {
1582
1576
  switch (action.type) {
1583
1577
  case "insertAt": {
@@ -1596,6 +1590,11 @@ function reducer(state, action) {
1596
1590
  if (index === -1) return state;
1597
1591
  const removals = new Map(state.removals);
1598
1592
  removals.set(action.token, { block: state.blocks[index], index });
1593
+ while (removals.size > UNDO_DEPTH) {
1594
+ const oldest = removals.keys().next();
1595
+ if (oldest.done) break;
1596
+ removals.delete(oldest.value);
1597
+ }
1599
1598
  return { blocks: state.blocks.filter((b) => b.id !== action.id), removals };
1600
1599
  }
1601
1600
  case "undo": {
@@ -1833,18 +1832,48 @@ function BlockEditor({
1833
1832
  }
1834
1833
 
1835
1834
  // src/canvas/source-offset.ts
1835
+ function caretPositionAt(x, y) {
1836
+ const doc = document;
1837
+ const position = doc.caretPositionFromPoint?.(x, y);
1838
+ if (position) return position;
1839
+ const range = doc.caretRangeFromPoint?.(x, y);
1840
+ return range ? { offsetNode: range.startContainer, offset: range.startOffset } : null;
1841
+ }
1842
+ function renderedOffsetAt(container, x, y) {
1843
+ const position = caretPositionAt(x, y);
1844
+ if (!position || position.offsetNode.nodeType !== Node.TEXT_NODE) return null;
1845
+ if (!container.contains(position.offsetNode)) return null;
1846
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
1847
+ let before = 0;
1848
+ while (walker.nextNode()) {
1849
+ if (walker.currentNode === position.offsetNode) return before + position.offset;
1850
+ before += walker.currentNode.textContent?.length ?? 0;
1851
+ }
1852
+ return null;
1853
+ }
1836
1854
  var PROBE_LENGTHS = [40, 20, 8];
1837
- function sourceOffsetFor(source, clickedText) {
1838
- const text = clickedText.replace(/\s+/g, " ").trim();
1839
- if (text === "") return 0;
1840
- const whole = source.indexOf(text);
1841
- if (whole !== -1) return whole;
1855
+ function search(source, probe) {
1856
+ const literal = probe.clickedText.trim();
1857
+ if (literal === "" || literal === probe.rendered.trim()) return source.length;
1858
+ const candidates = [literal, literal.replace(/\s+/g, " ")];
1859
+ for (const text of candidates) {
1860
+ const whole = source.indexOf(text);
1861
+ if (whole !== -1) return whole;
1862
+ }
1842
1863
  for (const length of PROBE_LENGTHS) {
1843
- if (text.length < length) continue;
1844
- const at = source.indexOf(text.slice(0, length));
1845
- if (at !== -1) return at;
1864
+ for (const text of candidates) {
1865
+ if (text.length < length) continue;
1866
+ const at = source.indexOf(text.slice(0, length));
1867
+ if (at !== -1) return at;
1868
+ }
1846
1869
  }
1847
- return 0;
1870
+ return source.length;
1871
+ }
1872
+ function sourceOffsetFor(source, probe) {
1873
+ if (probe.renderedOffset !== null && probe.rendered === source) {
1874
+ return Math.max(0, Math.min(probe.renderedOffset, source.length));
1875
+ }
1876
+ return search(source, probe);
1848
1877
  }
1849
1878
 
1850
1879
  // src/canvas/BlockCard.tsx
@@ -1865,14 +1894,14 @@ function BlockCardImpl({
1865
1894
  onUpdate,
1866
1895
  onInsertBlock
1867
1896
  }) {
1868
- const { blockTypes } = useQuoin();
1897
+ const { blockTypes, onError } = useQuoin();
1869
1898
  const definition = blockByType(blockTypes, block.type);
1870
1899
  const Preview = definition?.preview;
1871
1900
  const [editing, setEditing] = React8.useState(editingByDefault && !readOnly);
1872
1901
  const editButtonRef = React8.useRef(null);
1873
1902
  const editorContainerRef = React8.useRef(null);
1874
1903
  const returnFocusOnClose = React8.useRef(false);
1875
- const clickedTextRef = React8.useRef(null);
1904
+ const clickedRef = React8.useRef(null);
1876
1905
  const scrollTopRef = React8.useRef(null);
1877
1906
  const handleInsertBlock = React8.useCallback(
1878
1907
  (type) => onInsertBlock?.(index + 1, type),
@@ -1916,8 +1945,8 @@ function BlockCardImpl({
1916
1945
  'textarea, input, [role="combobox"]'
1917
1946
  );
1918
1947
  if (!field) return;
1919
- const clicked = clickedTextRef.current;
1920
- clickedTextRef.current = null;
1948
+ const clicked = clickedRef.current;
1949
+ clickedRef.current = null;
1921
1950
  if (clicked === null || !(field instanceof HTMLTextAreaElement)) {
1922
1951
  field.focus();
1923
1952
  if (field instanceof HTMLTextAreaElement) {
@@ -1942,6 +1971,12 @@ function BlockCardImpl({
1942
1971
  returnFocusOnClose.current = false;
1943
1972
  editButtonRef.current?.focus();
1944
1973
  }, [editing]);
1974
+ const reportedType = React8.useRef(null);
1975
+ React8.useEffect(() => {
1976
+ if (definition || reportedType.current === block.type) return;
1977
+ reportedType.current = block.type;
1978
+ onError(new Error(`No block type is registered for "${block.type}".`));
1979
+ }, [definition, block.type, onError]);
1945
1980
  if (definition?.alwaysEditing && !readOnly) {
1946
1981
  return /* @__PURE__ */ jsx22(
1947
1982
  BlockEditor,
@@ -1981,7 +2016,7 @@ function BlockCardImpl({
1981
2016
  }
1982
2017
  );
1983
2018
  }
1984
- const rest = Preview ? /* @__PURE__ */ jsx22(Preview, { data: block.data, blockId: block.id }) : definition ? /* @__PURE__ */ jsx22("p", { className: "py-1 text-sm text-muted-foreground", children: definition.label }) : null;
2019
+ 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
2020
  if (readOnly) return /* @__PURE__ */ jsx22("div", { children: rest });
1986
2021
  return /* @__PURE__ */ jsxs15("div", { className: "group/card relative", children: [
1987
2022
  /* @__PURE__ */ jsx22(
@@ -1991,7 +2026,11 @@ function BlockCardImpl({
1991
2026
  const target = e.target;
1992
2027
  if (target.closest(INTERACTIVE_SELECTOR)) return;
1993
2028
  if (hasSelectionWithin(e.currentTarget)) return;
1994
- clickedTextRef.current = target.textContent ?? "";
2029
+ clickedRef.current = {
2030
+ rendered: e.currentTarget.textContent ?? "",
2031
+ clickedText: target.textContent ?? "",
2032
+ renderedOffset: renderedOffsetAt(e.currentTarget, e.clientX, e.clientY)
2033
+ };
1995
2034
  scrollTopRef.current = (document.scrollingElement ?? document.documentElement).scrollTop;
1996
2035
  setEditing(true);
1997
2036
  },
@@ -2140,6 +2179,16 @@ function DocumentStarter({
2140
2179
  ] });
2141
2180
  }
2142
2181
 
2182
+ // src/canvas/ReadOnlyCanvas.tsx
2183
+ import { jsx as jsx25 } from "react/jsx-runtime";
2184
+ function ReadOnlyCanvas({
2185
+ blocks,
2186
+ onUpdate,
2187
+ root
2188
+ }) {
2189
+ return /* @__PURE__ */ jsx25("div", { ...root, children: /* @__PURE__ */ jsx25("ul", { className: "flex flex-col", children: blocks.map((b, i) => /* @__PURE__ */ jsx25("li", { children: /* @__PURE__ */ jsx25("div", { "data-block-index": i, className: "rounded-lg px-2 py-1", children: /* @__PURE__ */ jsx25(BlockCard, { block: b, index: i, editingByDefault: false, readOnly: true, onUpdate }) }) }, b.id)) }) });
2190
+ }
2191
+
2143
2192
  // src/canvas/reorder-indices.ts
2144
2193
  function reorderIndices(ids, activeId, overId) {
2145
2194
  if (overId === null || activeId === overId) return null;
@@ -2169,7 +2218,7 @@ function sameBlocks(a, b) {
2169
2218
  import { useSortable } from "@dnd-kit/sortable";
2170
2219
  import { CSS } from "@dnd-kit/utilities";
2171
2220
  import { GripVertical } from "lucide-react";
2172
- import { jsx as jsx25, jsxs as jsxs18 } from "react/jsx-runtime";
2221
+ import { jsx as jsx26, jsxs as jsxs18 } from "react/jsx-runtime";
2173
2222
  var GUTTER = [
2174
2223
  "flex items-center justify-end gap-0.5 pb-1",
2175
2224
  "lg:absolute lg:right-full lg:top-1 lg:justify-start lg:pb-0 lg:pr-1",
@@ -2192,7 +2241,7 @@ function SortableBlock({
2192
2241
  children: [
2193
2242
  /* @__PURE__ */ jsxs18("div", { className: GUTTER, children: [
2194
2243
  actions,
2195
- /* @__PURE__ */ jsx25(
2244
+ /* @__PURE__ */ jsx26(
2196
2245
  Button,
2197
2246
  {
2198
2247
  type: "button",
@@ -2203,17 +2252,17 @@ function SortableBlock({
2203
2252
  "aria-label": `Reorder block ${index + 1}`,
2204
2253
  ...attributes,
2205
2254
  ...listeners,
2206
- children: /* @__PURE__ */ jsx25(GripVertical, { "aria-hidden": "true" })
2255
+ children: /* @__PURE__ */ jsx26(GripVertical, { "aria-hidden": "true" })
2207
2256
  }
2208
2257
  )
2209
2258
  ] }),
2210
- /* @__PURE__ */ jsx25(
2259
+ /* @__PURE__ */ jsx26(
2211
2260
  "div",
2212
2261
  {
2213
2262
  "data-block-index": index,
2214
2263
  className: cn(
2215
2264
  "rounded-lg px-2 py-1 transition-colors",
2216
- isDragging ? "bg-card shadow-[var(--shadow-md)]" : "group-hover/block:bg-muted/40"
2265
+ isDragging ? "bg-popover shadow-md" : "group-hover/block:bg-muted/40"
2217
2266
  ),
2218
2267
  children
2219
2268
  }
@@ -2223,11 +2272,23 @@ function SortableBlock({
2223
2272
  );
2224
2273
  }
2225
2274
 
2275
+ // src/canvas/unacknowledged.ts
2276
+ var UNACKNOWLEDGED_LIMIT = 64;
2277
+ function remember(outstanding, blocks) {
2278
+ outstanding.add(blocks);
2279
+ while (outstanding.size > UNACKNOWLEDGED_LIMIT) {
2280
+ const oldest = outstanding.values().next();
2281
+ if (oldest.done) return;
2282
+ outstanding.delete(oldest.value);
2283
+ }
2284
+ }
2285
+
2226
2286
  // src/canvas/BlockCanvas.tsx
2227
- import { jsx as jsx26, jsxs as jsxs19 } from "react/jsx-runtime";
2287
+ import { jsx as jsx27, jsxs as jsxs19 } from "react/jsx-runtime";
2228
2288
  function Canvas({
2229
2289
  blocks,
2230
2290
  onChange,
2291
+ onDelete,
2231
2292
  blockTypes,
2232
2293
  readOnly,
2233
2294
  className,
@@ -2245,22 +2306,32 @@ function Canvas({
2245
2306
  const [seedSelection, setSeedSelection] = React11.useState(null);
2246
2307
  const listRef = React11.useRef(null);
2247
2308
  const pendingDeleteIndexRef = React11.useRef(null);
2309
+ const removedRef = React11.useRef(null);
2248
2310
  const announcements = React11.useMemo(
2249
2311
  () => buildAnnouncements(list.blocks, blockTypes),
2250
2312
  [list.blocks, blockTypes]
2251
2313
  );
2252
2314
  const lastEmitted = React11.useRef(blocks);
2253
2315
  const lastSupplied = React11.useRef(blocks);
2316
+ const unacknowledged = React11.useRef(/* @__PURE__ */ new Set());
2254
2317
  React11.useEffect(() => {
2255
2318
  if (list.blocks === lastEmitted.current) return;
2256
2319
  lastEmitted.current = list.blocks;
2320
+ remember(unacknowledged.current, list.blocks);
2257
2321
  onChange(list.blocks);
2258
- }, [list.blocks, onChange]);
2322
+ const removed = removedRef.current;
2323
+ removedRef.current = null;
2324
+ if (removed) onDelete?.(removed.block, removed.index);
2325
+ }, [list.blocks, onChange, onDelete]);
2259
2326
  React11.useEffect(() => {
2260
2327
  const previous = lastSupplied.current;
2261
2328
  lastSupplied.current = blocks;
2262
- if (blocks === lastEmitted.current) return;
2329
+ if (unacknowledged.current.has(blocks)) {
2330
+ if (blocks === lastEmitted.current) unacknowledged.current.clear();
2331
+ return;
2332
+ }
2263
2333
  if (sameBlocks(blocks, previous) || sameBlocks(blocks, list.blocks)) return;
2334
+ unacknowledged.current.clear();
2264
2335
  lastEmitted.current = blocks;
2265
2336
  reset(blocks);
2266
2337
  }, [blocks, reset]);
@@ -2273,7 +2344,10 @@ function Canvas({
2273
2344
  [insertAt, blockTypes]
2274
2345
  );
2275
2346
  function handleDelete(index, id2) {
2347
+ const block = list.blocks[index];
2276
2348
  pendingDeleteIndexRef.current = index;
2349
+ if (block) removedRef.current = { block, index };
2350
+ setNewBlockId((current) => current === id2 ? null : current);
2277
2351
  remove(id2);
2278
2352
  }
2279
2353
  React11.useEffect(() => {
@@ -2300,12 +2374,10 @@ function Canvas({
2300
2374
  role: ariaLabel ? "group" : void 0,
2301
2375
  "aria-label": ariaLabel
2302
2376
  };
2303
- if (readOnly) {
2304
- return /* @__PURE__ */ jsx26("div", { ...rootProps, children: /* @__PURE__ */ jsx26("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ jsx26("li", { children: /* @__PURE__ */ jsx26("div", { "data-block-index": i, className: "rounded-lg px-2 py-1", children: /* @__PURE__ */ jsx26(BlockCard, { block: b, index: i, editingByDefault: false, readOnly: true, onUpdate: update }) }) }, b.id)) }) });
2305
- }
2377
+ if (readOnly) return /* @__PURE__ */ jsx27(ReadOnlyCanvas, { blocks: list.blocks, onUpdate: update, root: rootProps });
2306
2378
  return /* @__PURE__ */ jsxs19("div", { ...rootProps, children: [
2307
- /* @__PURE__ */ jsx26(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2308
- /* @__PURE__ */ jsx26(
2379
+ /* @__PURE__ */ jsx27(BlockInserter, { index: 0, onInsert: insertPlain, label: "Insert a block at the start" }),
2380
+ /* @__PURE__ */ jsx27(
2309
2381
  DndContext,
2310
2382
  {
2311
2383
  id: dndId,
@@ -2314,13 +2386,13 @@ function Canvas({
2314
2386
  modifiers: [restrictToVerticalAxis],
2315
2387
  accessibility: { announcements },
2316
2388
  onDragEnd: handleDragEnd,
2317
- children: /* @__PURE__ */ jsx26(SortableContext, { items: list.blocks.map((b) => b.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx26("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ jsxs19("li", { children: [
2318
- /* @__PURE__ */ jsx26(
2389
+ children: /* @__PURE__ */ jsx27(SortableContext, { items: list.blocks.map((b) => b.id), strategy: verticalListSortingStrategy, children: /* @__PURE__ */ jsx27("ul", { className: "flex flex-col", children: list.blocks.map((b, i) => /* @__PURE__ */ jsxs19("li", { children: [
2390
+ /* @__PURE__ */ jsx27(
2319
2391
  SortableBlock,
2320
2392
  {
2321
2393
  id: b.id,
2322
2394
  index: i,
2323
- actions: /* @__PURE__ */ jsx26(
2395
+ actions: /* @__PURE__ */ jsx27(
2324
2396
  BlockActionsMenu,
2325
2397
  {
2326
2398
  index: i,
@@ -2330,7 +2402,7 @@ function Canvas({
2330
2402
  onDelete: () => handleDelete(i, b.id)
2331
2403
  }
2332
2404
  ),
2333
- children: /* @__PURE__ */ jsx26(
2405
+ children: /* @__PURE__ */ jsx27(
2334
2406
  BlockCard,
2335
2407
  {
2336
2408
  block: b,
@@ -2343,7 +2415,7 @@ function Canvas({
2343
2415
  )
2344
2416
  }
2345
2417
  ),
2346
- /* @__PURE__ */ jsx26(
2418
+ /* @__PURE__ */ jsx27(
2347
2419
  BlockInserter,
2348
2420
  {
2349
2421
  index: i + 1,
@@ -2354,7 +2426,7 @@ function Canvas({
2354
2426
  ] }, b.id)) }) })
2355
2427
  }
2356
2428
  ),
2357
- /* @__PURE__ */ jsx26(
2429
+ /* @__PURE__ */ jsx27(
2358
2430
  DocumentStarter,
2359
2431
  {
2360
2432
  autoFocus: list.blocks.length === 0,
@@ -2370,6 +2442,7 @@ function Canvas({
2370
2442
  function BlockCanvas({
2371
2443
  blocks,
2372
2444
  onChange,
2445
+ onDelete,
2373
2446
  blockTypes = coreBlocks,
2374
2447
  upload,
2375
2448
  resolveAssetUrl,
@@ -2383,11 +2456,12 @@ function BlockCanvas({
2383
2456
  () => ({ blockTypes, upload, resolveAssetUrl, onError }),
2384
2457
  [blockTypes, upload, resolveAssetUrl, onError]
2385
2458
  );
2386
- return /* @__PURE__ */ jsx26(QuoinProvider, { value: config, children: /* @__PURE__ */ jsx26(
2459
+ return /* @__PURE__ */ jsx27(QuoinProvider, { value: config, children: /* @__PURE__ */ jsx27(
2387
2460
  Canvas,
2388
2461
  {
2389
2462
  blocks,
2390
2463
  onChange,
2464
+ onDelete,
2391
2465
  blockTypes,
2392
2466
  readOnly,
2393
2467
  className,
@@ -2463,15 +2537,29 @@ function useAutosave({
2463
2537
  }
2464
2538
  export {
2465
2539
  BlockCanvas,
2540
+ FilePickerButton,
2466
2541
  QuoinProvider,
2467
2542
  TEXT_COMMANDS,
2543
+ UploadField,
2544
+ applyEdit,
2468
2545
  blockByType,
2546
+ caretRect,
2547
+ continueList,
2469
2548
  coreBlocks,
2470
2549
  defineBlock,
2550
+ formatShortcut,
2551
+ indentList,
2552
+ insertCommand,
2553
+ lineAt,
2554
+ linkPaste,
2471
2555
  paletteBlocks,
2556
+ shouldOpenPalette,
2557
+ toggleLinePrefix,
2472
2558
  useAutoGrow,
2473
2559
  useAutosave,
2474
2560
  useBlockList,
2475
- useQuoin
2561
+ useMarkdownKeymap,
2562
+ useQuoin,
2563
+ wrapSelection
2476
2564
  };
2477
2565
  //# sourceMappingURL=index.js.map