@reiwuzen/blocky-react 1.0.1 → 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 +94 -81
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +84 -71
- package/package.json +1 -1
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
|
|
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
|
-
|
|
72
|
-
const result = (0, import_blocky.
|
|
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
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
254
|
+
StaticSpan,
|
|
260
255
|
{
|
|
261
|
-
|
|
262
|
-
className:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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,
|
|
417
|
-
const [pos, setPos] = (0,
|
|
430
|
+
const ref = (0, import_react4.useRef)(null);
|
|
431
|
+
const [pos, setPos] = (0, import_react4.useState)(null);
|
|
418
432
|
const { updateBlock } = useEditorActions();
|
|
419
|
-
(0,
|
|
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,
|
|
486
|
-
const [isDragOver, setIsDragOver] = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
624
|
-
const hydratedBlocks = (0,
|
|
625
|
-
const prevEditable = (0,
|
|
626
|
-
const store = (0,
|
|
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,
|
|
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,
|
|
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
|
|
690
|
+
var import_react8 = require("react");
|
|
678
691
|
var import_blocky6 = require("@reiwuzen/blocky");
|
|
679
692
|
function useSelection(block) {
|
|
680
|
-
return (0,
|
|
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
|
-
|
|
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.
|
|
58
|
-
hydratedBlocks: React.MutableRefObject<Set<string>>;
|
|
57
|
+
blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
|
|
59
58
|
};
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
58
|
-
hydratedBlocks: React.MutableRefObject<Set<string>>;
|
|
57
|
+
blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
|
|
59
58
|
};
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
const result =
|
|
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
|
|
42
|
-
if (
|
|
43
|
-
const
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
216
|
+
StaticSpan,
|
|
222
217
|
{
|
|
223
|
-
|
|
224
|
-
className:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
586
|
-
const hydratedBlocks =
|
|
587
|
-
const prevEditable =
|
|
588
|
-
const store =
|
|
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
|
[]
|