@reiwuzen/blocky-react 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -49,7 +49,7 @@ __export(index_exports, {
49
49
  module.exports = __toCommonJS(index_exports);
50
50
 
51
51
  // src/components/editor.tsx
52
- var import_react6 = require("react");
52
+ var import_react7 = require("react");
53
53
  var import_blocky5 = require("@reiwuzen/blocky");
54
54
 
55
55
  // src/store/editor-store.ts
@@ -68,8 +68,8 @@ function createEditorStore(config = {}) {
68
68
  set({ blocks });
69
69
  config.onChange?.(blocks);
70
70
  },
71
- insertAfter: (afterId, type = "paragraph") => {
72
- const result = (0, import_blocky.insertBlockAfter)(get().blocks, afterId, type);
71
+ createBlockAfter: (afterId, type = "paragraph") => {
72
+ const result = (0, import_blocky.createBlockAfter)(get().blocks, afterId, type);
73
73
  if (!result.ok) return null;
74
74
  const { blocks, newId } = result.value;
75
75
  set({ blocks, activeBlockId: newId });
@@ -83,15 +83,10 @@ function createEditorStore(config = {}) {
83
83
  return prevId;
84
84
  },
85
85
  duplicate: (id) => {
86
- const block = get().blocks.find((b) => b.id === id);
87
- if (!block) return;
88
- const dup = (0, import_blocky.duplicateBlock)(block, crypto.randomUUID());
89
- const result = (0, import_blocky.insertBlockAfter)(get().blocks, id, block.type);
90
- if (!result.ok) return;
91
- const blocks = result.value.blocks.map(
92
- (b) => b.id === result.value.newId ? dup : b
93
- );
94
- set({ blocks, activeBlockId: dup.id });
86
+ const res = (0, import_blocky.duplicateBlockAfter)(get().blocks, id);
87
+ if (res == null) return;
88
+ const blocks = [...res.value.blocks];
89
+ set({ blocks, activeBlockId: res.value.newFocusId });
95
90
  config.onChange?.(blocks);
96
91
  },
97
92
  move: (id, direction) => {
@@ -109,7 +104,7 @@ function createEditorStore(config = {}) {
109
104
  }
110
105
 
111
106
  // src/components/blockList.tsx
112
- var import_react5 = require("react");
107
+ var import_react6 = require("react");
113
108
 
114
109
  // src/hooks/useEditor.ts
115
110
  var import_react = require("react");
@@ -128,7 +123,7 @@ function useActiveBlockId() {
128
123
  function useEditorActions() {
129
124
  const setBlocks = useEditor((s) => s.setBlocks);
130
125
  const updateBlock = useEditor((s) => s.updateBlock);
131
- const insertBlockAfter2 = useEditor((s) => s.insertAfter);
126
+ const createBlockAfter2 = useEditor((s) => s.createBlockAfter);
132
127
  const removeBlock = useEditor((s) => s.removeBlock);
133
128
  const duplicateBlock2 = useEditor((s) => s.duplicate);
134
129
  const moveBlock2 = useEditor((s) => s.move);
@@ -136,7 +131,7 @@ function useEditorActions() {
136
131
  return (0, import_react.useMemo)(() => ({
137
132
  setBlocks,
138
133
  updateBlock,
139
- insertBlockAfter: insertBlockAfter2,
134
+ createBlockAfter: createBlockAfter2,
140
135
  removeBlock,
141
136
  duplicateBlock: duplicateBlock2,
142
137
  moveBlock: moveBlock2,
@@ -144,7 +139,7 @@ function useEditorActions() {
144
139
  }), [
145
140
  setBlocks,
146
141
  updateBlock,
147
- insertBlockAfter2,
142
+ createBlockAfter2,
148
143
  removeBlock,
149
144
  duplicateBlock2,
150
145
  moveBlock2,
@@ -153,13 +148,16 @@ function useEditorActions() {
153
148
  }
154
149
 
155
150
  // src/components/block.tsx
156
- var import_react4 = require("react");
151
+ var import_react5 = require("react");
152
+
153
+ // src/components/blocks/editableContent.tsx
154
+ var import_react3 = __toESM(require("react"), 1);
157
155
 
158
156
  // src/hooks/useBlockKeyboard.ts
159
157
  var import_react2 = require("react");
160
158
  var import_blocky2 = require("@reiwuzen/blocky");
161
159
  function useBlockKeyboard({ block, onFocus }) {
162
- const { insertBlockAfter: insertBlockAfter2, removeBlock, updateBlock } = useEditorActions();
160
+ const { createBlockAfter: createBlockAfter2, removeBlock, updateBlock } = useEditorActions();
163
161
  const blocks = useBlocks();
164
162
  return (0, import_react2.useCallback)((e) => {
165
163
  const fresh = blocks.find((b) => b.id === block.id) ?? block;
@@ -172,7 +170,7 @@ function useBlockKeyboard({ block, onFocus }) {
172
170
  (0, import_blocky2.splitBlock)(fresh, nodeIndex, offset).match(
173
171
  ([original, newBlock]) => {
174
172
  updateBlock(original);
175
- const newId = insertBlockAfter2(fresh.id, "paragraph");
173
+ const newId = createBlockAfter2(fresh.id, "paragraph");
176
174
  if (newId) {
177
175
  updateBlock({ ...newBlock, id: newId });
178
176
  onFocus(newId);
@@ -232,63 +230,79 @@ function useBlockKeyboard({ block, onFocus }) {
232
230
  }
233
231
  );
234
232
  }
235
- }, [block.id, blocks, insertBlockAfter2, removeBlock, updateBlock, onFocus]);
233
+ }, [block.id, blocks, createBlockAfter2, removeBlock, updateBlock, onFocus]);
236
234
  }
237
235
 
238
236
  // src/components/blocks/editableContent.tsx
239
237
  var import_jsx_runtime = require("react/jsx-runtime");
240
- function EditableContent({
241
- block,
242
- className,
243
- placeholder,
244
- editable,
245
- onFocus,
246
- blockRefs,
247
- hydratedBlocks
248
- }) {
249
- const { updateBlock } = useEditorActions();
250
- const blocks = useBlocks();
238
+ function EditableContent(props) {
239
+ const { block, className, placeholder, editable, onFocus, blockRefs } = props;
240
+ const keyDownRef = (0, import_react3.useRef)(() => {
241
+ });
242
+ const focusRef = (0, import_react3.useRef)(() => {
243
+ });
251
244
  const handleKeyDown = useBlockKeyboard({ block, onFocus });
252
- const initialText = getInitialText(block.content);
253
- const handleInput = (e) => {
254
- const el = e.currentTarget;
255
- const text = el.textContent ?? "";
256
- updateBlock({ ...block, content: [{ type: "text", text }] });
257
- };
245
+ keyDownRef.current = handleKeyDown;
246
+ focusRef.current = () => onFocus(block.id);
247
+ const initialHtml = (0, import_react3.useMemo)(
248
+ () => nodesToHtml(block.content),
249
+ // eslint-disable-next-line react-hooks/exhaustive-deps
250
+ []
251
+ // computed once at mount from the seed blocks
252
+ );
258
253
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
259
- "span",
254
+ StaticSpan,
260
255
  {
261
- "data-block-id": block.id,
262
- className: `blocky-block-content ${className ?? ""}`,
263
- "data-placeholder": placeholder,
264
- contentEditable: editable,
265
- suppressContentEditableWarning: true,
266
- ref: (el) => {
267
- if (!el) return;
268
- blockRefs.current.set(block.id, el);
269
- if (hydratedBlocks.current.has(block.id)) return;
270
- el.innerHTML = nodesToHtml(block.content);
271
- hydratedBlocks.current.add(block.id);
272
- },
273
- onInput: handleInput,
274
- onKeyDown: handleKeyDown,
275
- onFocus: () => onFocus(block.id)
256
+ blockId: block.id,
257
+ className: className ?? "",
258
+ placeholder: placeholder ?? "",
259
+ editable,
260
+ initialHtml,
261
+ blockRefs,
262
+ keyDownRef,
263
+ focusRef
276
264
  }
277
265
  );
278
266
  }
279
- function getInitialText(nodes) {
280
- return nodes.map((n) => {
281
- if (n.type === "text") return n.text;
282
- if (n.type === "code") return n.text;
283
- if (n.type === "equation") return n.latex;
284
- return "";
285
- }).join("");
286
- }
267
+ var StaticSpan = import_react3.default.memo(
268
+ function StaticSpan2({
269
+ blockId,
270
+ className,
271
+ placeholder,
272
+ editable,
273
+ initialHtml,
274
+ blockRefs,
275
+ keyDownRef,
276
+ focusRef
277
+ }) {
278
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
279
+ "span",
280
+ {
281
+ "data-block-id": blockId,
282
+ className: `blocky-block-content ${className}`,
283
+ "data-placeholder": placeholder,
284
+ contentEditable: editable,
285
+ suppressContentEditableWarning: true,
286
+ ref: (el) => {
287
+ if (!el) return;
288
+ blockRefs.current.set(blockId, el);
289
+ if (el.dataset.hydrated) return;
290
+ el.innerHTML = initialHtml;
291
+ el.dataset.hydrated = "1";
292
+ },
293
+ onKeyDown: (e) => keyDownRef.current(e),
294
+ onFocus: () => focusRef.current()
295
+ }
296
+ );
297
+ },
298
+ () => true
299
+ // never re-render — DOM belongs to the browser
300
+ );
287
301
  function nodesToHtml(nodes) {
288
302
  return nodes.map((n) => {
289
- if (n.type === "code") return `<code><span>${esc(n.text)}</span></code>`;
303
+ if (n.type === "code") return `<code>${esc(n.text)}</code>`;
290
304
  if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
291
- let inner = `<span>${esc(n.text)}</span>`;
305
+ let inner = esc(n.text);
292
306
  if (n.bold) inner = `<strong>${inner}</strong>`;
293
307
  if (n.italic) inner = `<em>${inner}</em>`;
294
308
  if (n.underline) inner = `<u>${inner}</u>`;
@@ -409,14 +423,14 @@ function BlockTypeSwitcher({ block, onClose }) {
409
423
  }
410
424
 
411
425
  // src/components/toolbar/FormatToolbar.tsx
412
- var import_react3 = require("react");
426
+ var import_react4 = require("react");
413
427
  var import_blocky4 = require("@reiwuzen/blocky");
414
428
  var import_jsx_runtime4 = require("react/jsx-runtime");
415
429
  function FormatToolbar({ block }) {
416
- const ref = (0, import_react3.useRef)(null);
417
- const [pos, setPos] = (0, import_react3.useState)(null);
430
+ const ref = (0, import_react4.useRef)(null);
431
+ const [pos, setPos] = (0, import_react4.useState)(null);
418
432
  const { updateBlock } = useEditorActions();
419
- (0, import_react3.useEffect)(() => {
433
+ (0, import_react4.useEffect)(() => {
420
434
  const onSelectionChange = () => {
421
435
  const sel = window.getSelection();
422
436
  if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
@@ -482,8 +496,8 @@ function Block({
482
496
  blockRefs,
483
497
  hydratedBlocks
484
498
  }) {
485
- const [showSwitcher, setShowSwitcher] = (0, import_react4.useState)(false);
486
- const [isDragOver, setIsDragOver] = (0, import_react4.useState)(false);
499
+ const [showSwitcher, setShowSwitcher] = (0, import_react5.useState)(false);
500
+ const [isDragOver, setIsDragOver] = (0, import_react5.useState)(false);
487
501
  const { updateBlock } = useEditorActions();
488
502
  const { className, placeholder } = blockMeta(block);
489
503
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -545,8 +559,7 @@ function Block({
545
559
  placeholder,
546
560
  editable,
547
561
  onFocus,
548
- blockRefs,
549
- hydratedBlocks
562
+ blockRefs
550
563
  }
551
564
  ),
552
565
  editable && isActive && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(FormatToolbar, { block }),
@@ -585,7 +598,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
585
598
  const blocks = useBlocks();
586
599
  const activeBlockId = useActiveBlockId();
587
600
  const { setBlocks, setActiveBlockId } = useEditorActions();
588
- const handleDrop = (0, import_react5.useCallback)((dragId, dropId) => {
601
+ const handleDrop = (0, import_react6.useCallback)((dragId, dropId) => {
589
602
  const from = blocks.findIndex((b) => b.id === dragId);
590
603
  const to = blocks.findIndex((b) => b.id === dropId);
591
604
  if (from === -1 || to === -1) return;
@@ -612,7 +625,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
612
625
 
613
626
  // src/components/editor.tsx
614
627
  var import_jsx_runtime7 = require("react/jsx-runtime");
615
- var EditorContext = (0, import_react6.createContext)(null);
628
+ var EditorContext = (0, import_react7.createContext)(null);
616
629
  function Editor({
617
630
  blocks: seedBlocks,
618
631
  onChange,
@@ -620,15 +633,15 @@ function Editor({
620
633
  className,
621
634
  placeholder = "Start writing..."
622
635
  }) {
623
- const blockRefs = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
624
- const hydratedBlocks = (0, import_react6.useRef)(/* @__PURE__ */ new Set());
625
- const prevEditable = (0, import_react6.useRef)(editable);
626
- const store = (0, import_react6.useMemo)(
636
+ const blockRefs = (0, import_react7.useRef)(/* @__PURE__ */ new Map());
637
+ const hydratedBlocks = (0, import_react7.useRef)(/* @__PURE__ */ new Set());
638
+ const prevEditable = (0, import_react7.useRef)(editable);
639
+ const store = (0, import_react7.useMemo)(
627
640
  () => createEditorStore({ initialBlocks: seedBlocks }),
628
641
  // eslint-disable-next-line react-hooks/exhaustive-deps
629
642
  []
630
643
  );
631
- (0, import_react6.useLayoutEffect)(() => {
644
+ (0, import_react7.useLayoutEffect)(() => {
632
645
  const state = store.getState();
633
646
  if (seedBlocks && seedBlocks.length > 0) {
634
647
  state.setBlocks(seedBlocks);
@@ -640,7 +653,7 @@ function Editor({
640
653
  );
641
654
  }
642
655
  }, []);
643
- (0, import_react6.useEffect)(() => {
656
+ (0, import_react7.useEffect)(() => {
644
657
  const wasEditable = prevEditable.current;
645
658
  prevEditable.current = editable;
646
659
  if (!wasEditable || editable) return;
@@ -674,10 +687,10 @@ function Editor({
674
687
  }
675
688
 
676
689
  // src/hooks/useSelection.ts
677
- var import_react7 = require("react");
690
+ var import_react8 = require("react");
678
691
  var import_blocky6 = require("@reiwuzen/blocky");
679
692
  function useSelection(block) {
680
- return (0, import_react7.useCallback)(() => {
693
+ return (0, import_react8.useCallback)(() => {
681
694
  const sel = window.getSelection();
682
695
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
683
696
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
package/dist/index.d.cts CHANGED
@@ -8,7 +8,7 @@ type EditorStore = {
8
8
  activeBlockId: string | null;
9
9
  setBlocks: (blocks: AnyBlock[]) => void;
10
10
  updateBlock: (block: AnyBlock) => void;
11
- insertAfter: (afterId: string, type?: AnyBlock["type"]) => string | null;
11
+ createBlockAfter: (afterId: string, type?: AnyBlock["type"]) => string | null;
12
12
  removeBlock: (id: string) => string;
13
13
  duplicate: (id: string) => void;
14
14
  move: (id: string, direction: "up" | "down") => void;
@@ -54,10 +54,15 @@ type Props$3 = {
54
54
  placeholder?: string;
55
55
  editable: boolean;
56
56
  onFocus: (id: string) => void;
57
- blockRefs: React.MutableRefObject<Map<string, HTMLSpanElement>>;
58
- hydratedBlocks: React.MutableRefObject<Set<string>>;
57
+ blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
59
58
  };
60
- declare function EditableContent({ block, className, placeholder, editable, onFocus, blockRefs, hydratedBlocks, }: Props$3): react_jsx_runtime.JSX.Element;
59
+ /**
60
+ * The inner span is wrapped in a memo that NEVER re-renders after mount.
61
+ * React.memo(() => true) tells React "props didn't change, skip re-render".
62
+ * This means the DOM is 100% owned by the browser after first paint.
63
+ * Keyboard handlers stay fresh via a forwarded ref (handlerRef).
64
+ */
65
+ declare function EditableContent(props: Props$3): react_jsx_runtime.JSX.Element;
61
66
  declare function nodesToHtml(nodes: Node[]): string;
62
67
  declare function domToNodes(el: HTMLElement): Node[];
63
68
 
@@ -84,7 +89,7 @@ declare function useActiveBlockId(): string | null;
84
89
  declare function useEditorActions(): {
85
90
  setBlocks: (blocks: _reiwuzen_blocky.AnyBlock[]) => void;
86
91
  updateBlock: (block: _reiwuzen_blocky.AnyBlock) => void;
87
- insertBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
92
+ createBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
88
93
  removeBlock: (id: string) => string;
89
94
  duplicateBlock: (id: string) => void;
90
95
  moveBlock: (id: string, direction: "up" | "down") => void;
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ type EditorStore = {
8
8
  activeBlockId: string | null;
9
9
  setBlocks: (blocks: AnyBlock[]) => void;
10
10
  updateBlock: (block: AnyBlock) => void;
11
- insertAfter: (afterId: string, type?: AnyBlock["type"]) => string | null;
11
+ createBlockAfter: (afterId: string, type?: AnyBlock["type"]) => string | null;
12
12
  removeBlock: (id: string) => string;
13
13
  duplicate: (id: string) => void;
14
14
  move: (id: string, direction: "up" | "down") => void;
@@ -54,10 +54,15 @@ type Props$3 = {
54
54
  placeholder?: string;
55
55
  editable: boolean;
56
56
  onFocus: (id: string) => void;
57
- blockRefs: React.MutableRefObject<Map<string, HTMLSpanElement>>;
58
- hydratedBlocks: React.MutableRefObject<Set<string>>;
57
+ blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
59
58
  };
60
- declare function EditableContent({ block, className, placeholder, editable, onFocus, blockRefs, hydratedBlocks, }: Props$3): react_jsx_runtime.JSX.Element;
59
+ /**
60
+ * The inner span is wrapped in a memo that NEVER re-renders after mount.
61
+ * React.memo(() => true) tells React "props didn't change, skip re-render".
62
+ * This means the DOM is 100% owned by the browser after first paint.
63
+ * Keyboard handlers stay fresh via a forwarded ref (handlerRef).
64
+ */
65
+ declare function EditableContent(props: Props$3): react_jsx_runtime.JSX.Element;
61
66
  declare function nodesToHtml(nodes: Node[]): string;
62
67
  declare function domToNodes(el: HTMLElement): Node[];
63
68
 
@@ -84,7 +89,7 @@ declare function useActiveBlockId(): string | null;
84
89
  declare function useEditorActions(): {
85
90
  setBlocks: (blocks: _reiwuzen_blocky.AnyBlock[]) => void;
86
91
  updateBlock: (block: _reiwuzen_blocky.AnyBlock) => void;
87
- insertBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
92
+ createBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
88
93
  removeBlock: (id: string) => string;
89
94
  duplicateBlock: (id: string) => void;
90
95
  moveBlock: (id: string, direction: "up" | "down") => void;
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  // src/components/editor.tsx
2
- import { createContext, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo2, useRef as useRef3 } from "react";
2
+ import { createContext, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo3, useRef as useRef4 } from "react";
3
3
  import { createBlock } from "@reiwuzen/blocky";
4
4
 
5
5
  // src/store/editor-store.ts
6
6
  import { create } from "zustand";
7
7
  import {
8
- insertBlockAfter,
8
+ createBlockAfter,
9
+ duplicateBlockAfter,
9
10
  deleteBlock,
10
- duplicateBlock,
11
11
  moveBlock
12
12
  } from "@reiwuzen/blocky";
13
13
  function createEditorStore(config = {}) {
@@ -23,8 +23,8 @@ function createEditorStore(config = {}) {
23
23
  set({ blocks });
24
24
  config.onChange?.(blocks);
25
25
  },
26
- insertAfter: (afterId, type = "paragraph") => {
27
- const result = insertBlockAfter(get().blocks, afterId, type);
26
+ createBlockAfter: (afterId, type = "paragraph") => {
27
+ const result = createBlockAfter(get().blocks, afterId, type);
28
28
  if (!result.ok) return null;
29
29
  const { blocks, newId } = result.value;
30
30
  set({ blocks, activeBlockId: newId });
@@ -38,15 +38,10 @@ function createEditorStore(config = {}) {
38
38
  return prevId;
39
39
  },
40
40
  duplicate: (id) => {
41
- const block = get().blocks.find((b) => b.id === id);
42
- if (!block) return;
43
- const dup = duplicateBlock(block, crypto.randomUUID());
44
- const result = insertBlockAfter(get().blocks, id, block.type);
45
- if (!result.ok) return;
46
- const blocks = result.value.blocks.map(
47
- (b) => b.id === result.value.newId ? dup : b
48
- );
49
- set({ blocks, activeBlockId: dup.id });
41
+ const res = duplicateBlockAfter(get().blocks, id);
42
+ if (res == null) return;
43
+ const blocks = [...res.value.blocks];
44
+ set({ blocks, activeBlockId: res.value.newFocusId });
50
45
  config.onChange?.(blocks);
51
46
  },
52
47
  move: (id, direction) => {
@@ -83,7 +78,7 @@ function useActiveBlockId() {
83
78
  function useEditorActions() {
84
79
  const setBlocks = useEditor((s) => s.setBlocks);
85
80
  const updateBlock = useEditor((s) => s.updateBlock);
86
- const insertBlockAfter2 = useEditor((s) => s.insertAfter);
81
+ const createBlockAfter2 = useEditor((s) => s.createBlockAfter);
87
82
  const removeBlock = useEditor((s) => s.removeBlock);
88
83
  const duplicateBlock2 = useEditor((s) => s.duplicate);
89
84
  const moveBlock2 = useEditor((s) => s.move);
@@ -91,7 +86,7 @@ function useEditorActions() {
91
86
  return useMemo(() => ({
92
87
  setBlocks,
93
88
  updateBlock,
94
- insertBlockAfter: insertBlockAfter2,
89
+ createBlockAfter: createBlockAfter2,
95
90
  removeBlock,
96
91
  duplicateBlock: duplicateBlock2,
97
92
  moveBlock: moveBlock2,
@@ -99,7 +94,7 @@ function useEditorActions() {
99
94
  }), [
100
95
  setBlocks,
101
96
  updateBlock,
102
- insertBlockAfter2,
97
+ createBlockAfter2,
103
98
  removeBlock,
104
99
  duplicateBlock2,
105
100
  moveBlock2,
@@ -110,6 +105,9 @@ function useEditorActions() {
110
105
  // src/components/block.tsx
111
106
  import { useState as useState2 } from "react";
112
107
 
108
+ // src/components/blocks/editableContent.tsx
109
+ import React, { useRef, useMemo as useMemo2 } from "react";
110
+
113
111
  // src/hooks/useBlockKeyboard.ts
114
112
  import { useCallback } from "react";
115
113
  import {
@@ -121,7 +119,7 @@ import {
121
119
  outdentBlock
122
120
  } from "@reiwuzen/blocky";
123
121
  function useBlockKeyboard({ block, onFocus }) {
124
- const { insertBlockAfter: insertBlockAfter2, removeBlock, updateBlock } = useEditorActions();
122
+ const { createBlockAfter: createBlockAfter2, removeBlock, updateBlock } = useEditorActions();
125
123
  const blocks = useBlocks();
126
124
  return useCallback((e) => {
127
125
  const fresh = blocks.find((b) => b.id === block.id) ?? block;
@@ -134,7 +132,7 @@ function useBlockKeyboard({ block, onFocus }) {
134
132
  splitBlock(fresh, nodeIndex, offset).match(
135
133
  ([original, newBlock]) => {
136
134
  updateBlock(original);
137
- const newId = insertBlockAfter2(fresh.id, "paragraph");
135
+ const newId = createBlockAfter2(fresh.id, "paragraph");
138
136
  if (newId) {
139
137
  updateBlock({ ...newBlock, id: newId });
140
138
  onFocus(newId);
@@ -194,63 +192,79 @@ function useBlockKeyboard({ block, onFocus }) {
194
192
  }
195
193
  );
196
194
  }
197
- }, [block.id, blocks, insertBlockAfter2, removeBlock, updateBlock, onFocus]);
195
+ }, [block.id, blocks, createBlockAfter2, removeBlock, updateBlock, onFocus]);
198
196
  }
199
197
 
200
198
  // src/components/blocks/editableContent.tsx
201
199
  import { jsx } from "react/jsx-runtime";
202
- function EditableContent({
203
- block,
204
- className,
205
- placeholder,
206
- editable,
207
- onFocus,
208
- blockRefs,
209
- hydratedBlocks
210
- }) {
211
- const { updateBlock } = useEditorActions();
212
- const blocks = useBlocks();
200
+ function EditableContent(props) {
201
+ const { block, className, placeholder, editable, onFocus, blockRefs } = props;
202
+ const keyDownRef = useRef(() => {
203
+ });
204
+ const focusRef = useRef(() => {
205
+ });
213
206
  const handleKeyDown = useBlockKeyboard({ block, onFocus });
214
- const initialText = getInitialText(block.content);
215
- const handleInput = (e) => {
216
- const el = e.currentTarget;
217
- const text = el.textContent ?? "";
218
- updateBlock({ ...block, content: [{ type: "text", text }] });
219
- };
207
+ keyDownRef.current = handleKeyDown;
208
+ focusRef.current = () => onFocus(block.id);
209
+ const initialHtml = useMemo2(
210
+ () => nodesToHtml(block.content),
211
+ // eslint-disable-next-line react-hooks/exhaustive-deps
212
+ []
213
+ // computed once at mount from the seed blocks
214
+ );
220
215
  return /* @__PURE__ */ jsx(
221
- "span",
216
+ StaticSpan,
222
217
  {
223
- "data-block-id": block.id,
224
- className: `blocky-block-content ${className ?? ""}`,
225
- "data-placeholder": placeholder,
226
- contentEditable: editable,
227
- suppressContentEditableWarning: true,
228
- ref: (el) => {
229
- if (!el) return;
230
- blockRefs.current.set(block.id, el);
231
- if (hydratedBlocks.current.has(block.id)) return;
232
- el.innerHTML = nodesToHtml(block.content);
233
- hydratedBlocks.current.add(block.id);
234
- },
235
- onInput: handleInput,
236
- onKeyDown: handleKeyDown,
237
- onFocus: () => onFocus(block.id)
218
+ blockId: block.id,
219
+ className: className ?? "",
220
+ placeholder: placeholder ?? "",
221
+ editable,
222
+ initialHtml,
223
+ blockRefs,
224
+ keyDownRef,
225
+ focusRef
238
226
  }
239
227
  );
240
228
  }
241
- function getInitialText(nodes) {
242
- return nodes.map((n) => {
243
- if (n.type === "text") return n.text;
244
- if (n.type === "code") return n.text;
245
- if (n.type === "equation") return n.latex;
246
- return "";
247
- }).join("");
248
- }
229
+ var StaticSpan = React.memo(
230
+ function StaticSpan2({
231
+ blockId,
232
+ className,
233
+ placeholder,
234
+ editable,
235
+ initialHtml,
236
+ blockRefs,
237
+ keyDownRef,
238
+ focusRef
239
+ }) {
240
+ return /* @__PURE__ */ jsx(
241
+ "span",
242
+ {
243
+ "data-block-id": blockId,
244
+ className: `blocky-block-content ${className}`,
245
+ "data-placeholder": placeholder,
246
+ contentEditable: editable,
247
+ suppressContentEditableWarning: true,
248
+ ref: (el) => {
249
+ if (!el) return;
250
+ blockRefs.current.set(blockId, el);
251
+ if (el.dataset.hydrated) return;
252
+ el.innerHTML = initialHtml;
253
+ el.dataset.hydrated = "1";
254
+ },
255
+ onKeyDown: (e) => keyDownRef.current(e),
256
+ onFocus: () => focusRef.current()
257
+ }
258
+ );
259
+ },
260
+ () => true
261
+ // never re-render — DOM belongs to the browser
262
+ );
249
263
  function nodesToHtml(nodes) {
250
264
  return nodes.map((n) => {
251
- if (n.type === "code") return `<code><span>${esc(n.text)}</span></code>`;
265
+ if (n.type === "code") return `<code>${esc(n.text)}</code>`;
252
266
  if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
253
- let inner = `<span>${esc(n.text)}</span>`;
267
+ let inner = esc(n.text);
254
268
  if (n.bold) inner = `<strong>${inner}</strong>`;
255
269
  if (n.italic) inner = `<em>${inner}</em>`;
256
270
  if (n.underline) inner = `<u>${inner}</u>`;
@@ -371,11 +385,11 @@ function BlockTypeSwitcher({ block, onClose }) {
371
385
  }
372
386
 
373
387
  // src/components/toolbar/FormatToolbar.tsx
374
- import { useEffect, useRef, useState } from "react";
388
+ import { useEffect, useRef as useRef2, useState } from "react";
375
389
  import { toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough, toggleHighlight, toggleColor, flatToSelection } from "@reiwuzen/blocky";
376
390
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
377
391
  function FormatToolbar({ block }) {
378
- const ref = useRef(null);
392
+ const ref = useRef2(null);
379
393
  const [pos, setPos] = useState(null);
380
394
  const { updateBlock } = useEditorActions();
381
395
  useEffect(() => {
@@ -507,8 +521,7 @@ function Block({
507
521
  placeholder,
508
522
  editable,
509
523
  onFocus,
510
- blockRefs,
511
- hydratedBlocks
524
+ blockRefs
512
525
  }
513
526
  ),
514
527
  editable && isActive && /* @__PURE__ */ jsx5(FormatToolbar, { block }),
@@ -582,10 +595,10 @@ function Editor({
582
595
  className,
583
596
  placeholder = "Start writing..."
584
597
  }) {
585
- const blockRefs = useRef3(/* @__PURE__ */ new Map());
586
- const hydratedBlocks = useRef3(/* @__PURE__ */ new Set());
587
- const prevEditable = useRef3(editable);
588
- const store = useMemo2(
598
+ const blockRefs = useRef4(/* @__PURE__ */ new Map());
599
+ const hydratedBlocks = useRef4(/* @__PURE__ */ new Set());
600
+ const prevEditable = useRef4(editable);
601
+ const store = useMemo3(
589
602
  () => createEditorStore({ initialBlocks: seedBlocks }),
590
603
  // eslint-disable-next-line react-hooks/exhaustive-deps
591
604
  []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reiwuzen/blocky-react",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "React UI layer for @reiwuzen/blocky — composable block editor components.",
5
5
  "author": "Rei WuZen",
6
6
  "license": "ISC",
@@ -30,7 +30,7 @@
30
30
  "react-dom": ">=18"
31
31
  },
32
32
  "dependencies": {
33
- "@reiwuzen/blocky": "^1.2.0",
33
+ "@reiwuzen/blocky": "^1.3.0",
34
34
  "zustand": "^5.0.0"
35
35
  },
36
36
  "devDependencies": {