@reiwuzen/blocky-react 1.0.1 → 1.0.3

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,8 +49,8 @@ __export(index_exports, {
49
49
  module.exports = __toCommonJS(index_exports);
50
50
 
51
51
  // src/components/editor.tsx
52
- var import_react6 = require("react");
53
- var import_blocky5 = require("@reiwuzen/blocky");
52
+ var import_react5 = require("react");
53
+ var import_blocky4 = require("@reiwuzen/blocky");
54
54
 
55
55
  // src/store/editor-store.ts
56
56
  var import_zustand = require("zustand");
@@ -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_react4 = 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,197 +148,122 @@ function useEditorActions() {
153
148
  }
154
149
 
155
150
  // src/components/block.tsx
156
- var import_react4 = require("react");
157
-
158
- // src/hooks/useBlockKeyboard.ts
159
- var import_react2 = require("react");
160
- var import_blocky2 = require("@reiwuzen/blocky");
161
- function useBlockKeyboard({ block, onFocus }) {
162
- const { insertBlockAfter: insertBlockAfter2, removeBlock, updateBlock } = useEditorActions();
163
- const blocks = useBlocks();
164
- return (0, import_react2.useCallback)((e) => {
165
- const fresh = blocks.find((b) => b.id === block.id) ?? block;
166
- const sel = window.getSelection();
167
- const flat = sel?.anchorOffset ?? 0;
168
- if (e.key === "Enter" && !e.shiftKey) {
169
- e.preventDefault();
170
- (0, import_blocky2.flatToPosition)(fresh, flat).match(
171
- ({ nodeIndex, offset }) => {
172
- (0, import_blocky2.splitBlock)(fresh, nodeIndex, offset).match(
173
- ([original, newBlock]) => {
174
- updateBlock(original);
175
- const newId = insertBlockAfter2(fresh.id, "paragraph");
176
- if (newId) {
177
- updateBlock({ ...newBlock, id: newId });
178
- onFocus(newId);
179
- setTimeout(() => {
180
- document.querySelector(`[data-block-id="${newId}"]`)?.focus();
181
- }, 0);
182
- }
183
- },
184
- () => {
185
- }
186
- );
187
- },
188
- () => {
189
- }
190
- );
191
- return;
192
- }
193
- if (e.key === "Backspace" && flat === 0) {
194
- const index = blocks.findIndex((b) => b.id === fresh.id);
195
- if (index === 0) return;
196
- const prev = blocks[index - 1];
197
- if (prev.type === "code" || prev.type === "equation") return;
198
- e.preventDefault();
199
- (0, import_blocky2.mergeBlocks)(prev, fresh).match(
200
- (merged) => {
201
- updateBlock(merged);
202
- removeBlock(fresh.id);
203
- onFocus(merged.id);
204
- setTimeout(() => {
205
- document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
206
- }, 0);
207
- },
208
- () => {
209
- }
210
- );
211
- return;
212
- }
213
- if (e.key === " ") {
214
- (0, import_blocky2.applyMarkdownTransform)(fresh, flat).match(
215
- ({ block: transformed, converted }) => {
216
- if (converted) {
217
- e.preventDefault();
218
- updateBlock(transformed);
219
- }
220
- },
221
- () => {
222
- }
223
- );
224
- return;
225
- }
226
- if (e.key === "Tab") {
227
- e.preventDefault();
228
- const fn = e.shiftKey ? import_blocky2.outdentBlock : import_blocky2.indentBlock;
229
- fn(fresh).match(
230
- (b) => updateBlock(b),
231
- () => {
232
- }
233
- );
234
- }
235
- }, [block.id, blocks, insertBlockAfter2, removeBlock, updateBlock, onFocus]);
236
- }
151
+ var import_react3 = require("react");
237
152
 
238
153
  // src/components/blocks/editableContent.tsx
239
154
  var import_jsx_runtime = require("react/jsx-runtime");
240
155
  function EditableContent({
241
156
  block,
242
157
  className,
243
- placeholder,
244
158
  editable,
245
- onFocus,
246
- blockRefs,
247
- hydratedBlocks
159
+ blockRefs
248
160
  }) {
249
- const { updateBlock } = useEditorActions();
250
- const blocks = useBlocks();
251
- 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
- };
258
161
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
259
- "span",
162
+ "div",
260
163
  {
261
- "data-block-id": block.id,
262
- className: `blocky-block-content ${className ?? ""}`,
263
- "data-placeholder": placeholder,
164
+ className: `blocky-content ${className ?? ""}`,
264
165
  contentEditable: editable,
265
166
  suppressContentEditableWarning: true,
266
167
  ref: (el) => {
267
168
  if (!el) return;
268
169
  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
170
  },
273
- onInput: handleInput,
274
- onKeyDown: handleKeyDown,
275
- onFocus: () => onFocus(block.id)
171
+ children: block.content.map((node, i) => {
172
+ const { classString, dataAttrs } = nodeToAttrs(node);
173
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
174
+ "span",
175
+ {
176
+ className: classString,
177
+ ...parseDataAttrs(dataAttrs),
178
+ children: node.type === "equation" ? node.latex : node.text
179
+ },
180
+ i
181
+ );
182
+ })
276
183
  }
277
184
  );
278
185
  }
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("");
186
+ function nodeToAttrs(n) {
187
+ const classes = [];
188
+ const data = [];
189
+ if (n.type === "code") classes.push("blocky-code");
190
+ if (n.type === "equation") classes.push("blocky-equation");
191
+ if (n.type === "text") {
192
+ if (n.bold) classes.push("blocky-bold");
193
+ if (n.italic) classes.push("blocky-italic");
194
+ if (n.underline) classes.push("blocky-underline");
195
+ if (n.strikethrough) classes.push("blocky-strike");
196
+ if (n.highlighted) classes.push(`blocky-highlight-${n.highlighted}`);
197
+ if (n.color) classes.push(`blocky-color-${n.color}`);
198
+ if (n.link) {
199
+ data.push(`data-link="${escAttr(n.link)}"`);
200
+ }
201
+ }
202
+ if (n.type === "equation") {
203
+ data.push(`data-latex="${escAttr(n.latex)}"`);
204
+ }
205
+ return {
206
+ classString: classes.join(" "),
207
+ dataAttrs: data.join(" ")
208
+ };
286
209
  }
287
210
  function nodesToHtml(nodes) {
288
211
  return nodes.map((n) => {
289
- if (n.type === "code") return `<code><span>${esc(n.text)}</span></code>`;
290
- if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
291
- let inner = `<span>${esc(n.text)}</span>`;
292
- if (n.bold) inner = `<strong>${inner}</strong>`;
293
- if (n.italic) inner = `<em>${inner}</em>`;
294
- if (n.underline) inner = `<u>${inner}</u>`;
295
- if (n.strikethrough) inner = `<s>${inner}</s>`;
296
- if (n.highlighted) inner = `<mark class="blocky-highlight-${n.highlighted}">${inner}</mark>`;
297
- if (n.color) inner = `<span class="blocky-color-${n.color}">${inner}</span>`;
298
- if (n.link) inner = `<a href="${n.link}">${inner}</a>`;
299
- return inner;
212
+ if (n.type === "equation") {
213
+ return `<span class="blocky-equation">${esc(n.latex)}</span>`;
214
+ }
215
+ if (n.type === "code") {
216
+ return `<span class="blocky-code">${esc(n.text)}</span>`;
217
+ }
218
+ if (n.type === "text") {
219
+ const classes = [];
220
+ if (n.bold) classes.push("blocky-bold");
221
+ if (n.italic) classes.push("blocky-italic");
222
+ if (n.underline) classes.push("blocky-underline");
223
+ if (n.strikethrough) classes.push("blocky-strike");
224
+ if (n.highlighted) classes.push(`blocky-highlight-${n.highlighted}`);
225
+ if (n.color) classes.push(`blocky-color-${n.color}`);
226
+ if (n.link) classes.push("blocky-link");
227
+ const linkAttr = n.link ? ` data-link="${escAttr(n.link)}"` : "";
228
+ const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
229
+ return `<span${classAttr}${linkAttr}>${esc(n.text)}</span>`;
230
+ }
231
+ return "";
300
232
  }).join("");
301
233
  }
302
234
  function domToNodes(el) {
303
235
  const nodes = [];
304
- const walk = (node, formats = {}) => {
305
- if (node.nodeType === window.Node.TEXT_NODE) {
306
- const text = node.textContent ?? "";
307
- if (!text) return;
308
- nodes.push({ type: "text", text, ...formats });
309
- return;
310
- }
311
- if (!(node instanceof HTMLElement)) return;
312
- const tag = node.tagName.toLowerCase();
313
- const inherited = { ...formats };
314
- if (tag === "strong" || tag === "b") inherited.bold = true;
315
- if (tag === "em" || tag === "i") inherited.italic = true;
316
- if (tag === "u") inherited.underline = true;
317
- if (tag === "s") inherited.strikethrough = true;
318
- if (tag === "a") inherited.link = node.getAttribute("href") ?? void 0;
319
- if (tag === "mark") {
320
- inherited.highlighted = node.className.includes("green") ? "green" : "yellow";
321
- }
322
- if (tag === "code") {
323
- nodes.push({ type: "code", text: node.innerText });
324
- return;
325
- }
326
- if (tag === "span" && node.classList.contains("blocky-equation")) {
327
- nodes.push({ type: "equation", latex: node.innerText });
328
- return;
329
- }
330
- node.childNodes.forEach((child) => walk(child, inherited));
331
- };
332
- el.childNodes.forEach((child) => walk(child));
333
- const merged = [];
334
- for (const node of nodes) {
335
- const prev = merged[merged.length - 1];
336
- if (prev && node.type === "text" && prev.type === "text" && JSON.stringify({ ...prev, text: "" }) === JSON.stringify({ ...node, text: "" })) {
337
- prev.text += node.text;
338
- } else {
339
- merged.push({ ...node });
340
- }
341
- }
342
- return merged.length > 0 ? merged : [{ type: "text", text: "" }];
236
+ el.querySelectorAll("span").forEach((span) => {
237
+ const text = span.innerText ?? "";
238
+ const node = {
239
+ type: span.classList.contains("blocky-equation") ? "equation" : span.classList.contains("blocky-code") ? "code" : "text",
240
+ text
241
+ };
242
+ if (span.classList.contains("blocky-bold")) node.bold = true;
243
+ if (span.classList.contains("blocky-italic")) node.italic = true;
244
+ if (span.classList.contains("blocky-underline")) node.underline = true;
245
+ if (span.classList.contains("blocky-strike")) node.strikethrough = true;
246
+ if (span.dataset.link) node.link = span.dataset.link;
247
+ if (span.dataset.latex) node.latex = span.dataset.latex;
248
+ nodes.push(node);
249
+ });
250
+ return nodes.length ? nodes : [{ type: "text", text: "" }];
343
251
  }
344
252
  function esc(s) {
345
253
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
346
254
  }
255
+ function escAttr(s) {
256
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
257
+ }
258
+ function parseDataAttrs(dataAttrs) {
259
+ const obj = {};
260
+ dataAttrs.split(" ").forEach((pair) => {
261
+ if (!pair) return;
262
+ const [k, v] = pair.split("=");
263
+ obj[k] = v?.replace(/"/g, "") ?? "";
264
+ });
265
+ return obj;
266
+ }
347
267
 
348
268
  // src/components/drag/DragHandle.tsx
349
269
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -372,7 +292,7 @@ function DragHandle({ blockId, onDragStart }) {
372
292
  }
373
293
 
374
294
  // src/components/toolbar/BlockTypeSwitcher.tsx
375
- var import_blocky3 = require("@reiwuzen/blocky");
295
+ var import_blocky2 = require("@reiwuzen/blocky");
376
296
  var import_jsx_runtime3 = require("react/jsx-runtime");
377
297
  var BLOCK_TYPES = [
378
298
  { type: "paragraph", label: "Text" },
@@ -393,7 +313,7 @@ function BlockTypeSwitcher({ block, onClose }) {
393
313
  className: `blocky-type-option ${block.type === type ? "blocky-type-option--active" : ""}`,
394
314
  onMouseDown: (e) => {
395
315
  e.preventDefault();
396
- (0, import_blocky3.changeBlockType)(block, type).match(
316
+ (0, import_blocky2.changeBlockType)(block, type).match(
397
317
  (b) => {
398
318
  updateBlock(b);
399
319
  onClose();
@@ -409,14 +329,14 @@ function BlockTypeSwitcher({ block, onClose }) {
409
329
  }
410
330
 
411
331
  // src/components/toolbar/FormatToolbar.tsx
412
- var import_react3 = require("react");
413
- var import_blocky4 = require("@reiwuzen/blocky");
332
+ var import_react2 = require("react");
333
+ var import_blocky3 = require("@reiwuzen/blocky");
414
334
  var import_jsx_runtime4 = require("react/jsx-runtime");
415
335
  function FormatToolbar({ block }) {
416
- const ref = (0, import_react3.useRef)(null);
417
- const [pos, setPos] = (0, import_react3.useState)(null);
336
+ const ref = (0, import_react2.useRef)(null);
337
+ const [pos, setPos] = (0, import_react2.useState)(null);
418
338
  const { updateBlock } = useEditorActions();
419
- (0, import_react3.useEffect)(() => {
339
+ (0, import_react2.useEffect)(() => {
420
340
  const onSelectionChange = () => {
421
341
  const sel = window.getSelection();
422
342
  if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
@@ -439,7 +359,7 @@ function FormatToolbar({ block }) {
439
359
  if (!sel) return;
440
360
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
441
361
  const end = Math.max(sel.anchorOffset, sel.focusOffset);
442
- (0, import_blocky4.flatToSelection)(block, start, end).match(
362
+ (0, import_blocky3.flatToSelection)(block, start, end).match(
443
363
  (nodeSel) => fn(block.content, nodeSel).match(
444
364
  (content) => updateBlock({ ...block, content }),
445
365
  () => {
@@ -457,14 +377,14 @@ function FormatToolbar({ block }) {
457
377
  style: { top: pos.top, left: pos.left },
458
378
  onMouseDown: (e) => e.preventDefault(),
459
379
  children: [
460
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn", onMouseDown: () => applyFormat(import_blocky4.toggleBold), children: "B" }),
461
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--italic", onMouseDown: () => applyFormat(import_blocky4.toggleItalic), children: "I" }),
462
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--underline", onMouseDown: () => applyFormat(import_blocky4.toggleUnderline), children: "U" }),
463
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--strike", onMouseDown: () => applyFormat(import_blocky4.toggleStrikethrough), children: "S" }),
380
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn", onMouseDown: () => applyFormat(import_blocky3.toggleBold), children: "B" }),
381
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--italic", onMouseDown: () => applyFormat(import_blocky3.toggleItalic), children: "I" }),
382
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--underline", onMouseDown: () => applyFormat(import_blocky3.toggleUnderline), children: "U" }),
383
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--strike", onMouseDown: () => applyFormat(import_blocky3.toggleStrikethrough), children: "S" }),
464
384
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "blocky-toolbar-divider" }),
465
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--highlight", onMouseDown: () => applyFormat((n, s) => (0, import_blocky4.toggleHighlight)(n, s, "yellow")), children: "H" }),
466
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--red", onMouseDown: () => applyFormat((n, s) => (0, import_blocky4.toggleColor)(n, s, "red")), children: "A" }),
467
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--blue", onMouseDown: () => applyFormat((n, s) => (0, import_blocky4.toggleColor)(n, s, "blue")), children: "A" })
385
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--highlight", onMouseDown: () => applyFormat((n, s) => (0, import_blocky3.toggleHighlight)(n, s, "yellow")), children: "H" }),
386
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--red", onMouseDown: () => applyFormat((n, s) => (0, import_blocky3.toggleColor)(n, s, "red")), children: "A" }),
387
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--blue", onMouseDown: () => applyFormat((n, s) => (0, import_blocky3.toggleColor)(n, s, "blue")), children: "A" })
468
388
  ]
469
389
  }
470
390
  );
@@ -482,8 +402,8 @@ function Block({
482
402
  blockRefs,
483
403
  hydratedBlocks
484
404
  }) {
485
- const [showSwitcher, setShowSwitcher] = (0, import_react4.useState)(false);
486
- const [isDragOver, setIsDragOver] = (0, import_react4.useState)(false);
405
+ const [showSwitcher, setShowSwitcher] = (0, import_react3.useState)(false);
406
+ const [isDragOver, setIsDragOver] = (0, import_react3.useState)(false);
487
407
  const { updateBlock } = useEditorActions();
488
408
  const { className, placeholder } = blockMeta(block);
489
409
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -585,7 +505,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
585
505
  const blocks = useBlocks();
586
506
  const activeBlockId = useActiveBlockId();
587
507
  const { setBlocks, setActiveBlockId } = useEditorActions();
588
- const handleDrop = (0, import_react5.useCallback)((dragId, dropId) => {
508
+ const handleDrop = (0, import_react4.useCallback)((dragId, dropId) => {
589
509
  const from = blocks.findIndex((b) => b.id === dragId);
590
510
  const to = blocks.findIndex((b) => b.id === dropId);
591
511
  if (from === -1 || to === -1) return;
@@ -612,7 +532,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
612
532
 
613
533
  // src/components/editor.tsx
614
534
  var import_jsx_runtime7 = require("react/jsx-runtime");
615
- var EditorContext = (0, import_react6.createContext)(null);
535
+ var EditorContext = (0, import_react5.createContext)(null);
616
536
  function Editor({
617
537
  blocks: seedBlocks,
618
538
  onChange,
@@ -620,27 +540,27 @@ function Editor({
620
540
  className,
621
541
  placeholder = "Start writing..."
622
542
  }) {
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)(
543
+ const blockRefs = (0, import_react5.useRef)(/* @__PURE__ */ new Map());
544
+ const hydratedBlocks = (0, import_react5.useRef)(/* @__PURE__ */ new Set());
545
+ const prevEditable = (0, import_react5.useRef)(editable);
546
+ const store = (0, import_react5.useMemo)(
627
547
  () => createEditorStore({ initialBlocks: seedBlocks }),
628
548
  // eslint-disable-next-line react-hooks/exhaustive-deps
629
549
  []
630
550
  );
631
- (0, import_react6.useLayoutEffect)(() => {
551
+ (0, import_react5.useLayoutEffect)(() => {
632
552
  const state = store.getState();
633
553
  if (seedBlocks && seedBlocks.length > 0) {
634
554
  state.setBlocks(seedBlocks);
635
555
  } else if (state.blocks.length === 0) {
636
- (0, import_blocky5.createBlock)("paragraph").match(
556
+ (0, import_blocky4.createBlock)("paragraph").match(
637
557
  (b) => state.setBlocks([b]),
638
558
  () => {
639
559
  }
640
560
  );
641
561
  }
642
562
  }, []);
643
- (0, import_react6.useEffect)(() => {
563
+ (0, import_react5.useEffect)(() => {
644
564
  const wasEditable = prevEditable.current;
645
565
  prevEditable.current = editable;
646
566
  if (!wasEditable || editable) return;
@@ -674,18 +594,98 @@ function Editor({
674
594
  }
675
595
 
676
596
  // src/hooks/useSelection.ts
677
- var import_react7 = require("react");
678
- var import_blocky6 = require("@reiwuzen/blocky");
597
+ var import_react6 = require("react");
598
+ var import_blocky5 = require("@reiwuzen/blocky");
679
599
  function useSelection(block) {
680
- return (0, import_react7.useCallback)(() => {
600
+ return (0, import_react6.useCallback)(() => {
681
601
  const sel = window.getSelection();
682
602
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
683
603
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
684
604
  const end = Math.max(sel.anchorOffset, sel.focusOffset);
685
- const result = (0, import_blocky6.flatToSelection)(block, start, end);
605
+ const result = (0, import_blocky5.flatToSelection)(block, start, end);
686
606
  return result.ok ? result.value : null;
687
607
  }, [block]);
688
608
  }
609
+
610
+ // src/hooks/useBlockKeyboard.ts
611
+ var import_react7 = require("react");
612
+ var import_blocky6 = require("@reiwuzen/blocky");
613
+ function useBlockKeyboard({ block, onFocus }) {
614
+ const { createBlockAfter: createBlockAfter2, removeBlock, updateBlock } = useEditorActions();
615
+ const blocks = useBlocks();
616
+ return (0, import_react7.useCallback)((e) => {
617
+ const fresh = blocks.find((b) => b.id === block.id) ?? block;
618
+ const sel = window.getSelection();
619
+ const flat = sel?.anchorOffset ?? 0;
620
+ if (e.key === "Enter" && !e.shiftKey) {
621
+ e.preventDefault();
622
+ (0, import_blocky6.flatToPosition)(fresh, flat).match(
623
+ ({ nodeIndex, offset }) => {
624
+ (0, import_blocky6.splitBlock)(fresh, nodeIndex, offset).match(
625
+ ([original, newBlock]) => {
626
+ updateBlock(original);
627
+ const newId = createBlockAfter2(fresh.id, "paragraph");
628
+ if (newId) {
629
+ updateBlock({ ...newBlock, id: newId });
630
+ onFocus(newId);
631
+ setTimeout(() => {
632
+ document.querySelector(`[data-block-id="${newId}"]`)?.focus();
633
+ }, 0);
634
+ }
635
+ },
636
+ () => {
637
+ }
638
+ );
639
+ },
640
+ () => {
641
+ }
642
+ );
643
+ return;
644
+ }
645
+ if (e.key === "Backspace" && flat === 0) {
646
+ const index = blocks.findIndex((b) => b.id === fresh.id);
647
+ if (index === 0) return;
648
+ const prev = blocks[index - 1];
649
+ if (prev.type === "code" || prev.type === "equation") return;
650
+ e.preventDefault();
651
+ (0, import_blocky6.mergeBlocks)(prev, fresh).match(
652
+ (merged) => {
653
+ updateBlock(merged);
654
+ removeBlock(fresh.id);
655
+ onFocus(merged.id);
656
+ setTimeout(() => {
657
+ document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
658
+ }, 0);
659
+ },
660
+ () => {
661
+ }
662
+ );
663
+ return;
664
+ }
665
+ if (e.key === " ") {
666
+ (0, import_blocky6.applyMarkdownTransform)(fresh, flat).match(
667
+ ({ block: transformed, converted }) => {
668
+ if (converted) {
669
+ e.preventDefault();
670
+ updateBlock(transformed);
671
+ }
672
+ },
673
+ () => {
674
+ }
675
+ );
676
+ return;
677
+ }
678
+ if (e.key === "Tab") {
679
+ e.preventDefault();
680
+ const fn = e.shiftKey ? import_blocky6.outdentBlock : import_blocky6.indentBlock;
681
+ fn(fresh).match(
682
+ (b) => updateBlock(b),
683
+ () => {
684
+ }
685
+ );
686
+ }
687
+ }, [block.id, blocks, createBlockAfter2, removeBlock, updateBlock, onFocus]);
688
+ }
689
689
  // Annotate the CommonJS export names for ESM import in node:
690
690
  0 && (module.exports = {
691
691
  Block,
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;
@@ -43,8 +43,8 @@ type Props$4 = {
43
43
  onFocus: (id: string) => void;
44
44
  onDragStart: (id: string) => void;
45
45
  onDrop: (dragId: string, dropId: string) => void;
46
- blockRefs: React.MutableRefObject<Map<string, HTMLSpanElement>>;
47
- hydratedBlocks: React.MutableRefObject<Set<string>>;
46
+ blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
47
+ hydratedBlocks: React.RefObject<Set<string>>;
48
48
  };
49
49
  declare function Block({ block, isActive, editable, onFocus, onDragStart, onDrop, blockRefs, hydratedBlocks, }: Props$4): react_jsx_runtime.JSX.Element;
50
50
 
@@ -54,10 +54,16 @@ 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>>;
58
+ hydratedBlocks: React.RefObject<Set<string>>;
59
59
  };
60
- declare function EditableContent({ block, className, placeholder, editable, onFocus, blockRefs, hydratedBlocks, }: Props$3): react_jsx_runtime.JSX.Element;
60
+ /**
61
+ * The inner span is wrapped in a memo that NEVER re-renders after mount.
62
+ * React.memo(() => true) tells React "props didn't change, skip re-render".
63
+ * This means the DOM is 100% owned by the browser after first paint.
64
+ * Keyboard handlers stay fresh via a forwarded ref (handlerRef).
65
+ */
66
+ declare function EditableContent({ block, className, editable, blockRefs, }: Props$3): react_jsx_runtime.JSX.Element;
61
67
  declare function nodesToHtml(nodes: Node[]): string;
62
68
  declare function domToNodes(el: HTMLElement): Node[];
63
69
 
@@ -84,7 +90,7 @@ declare function useActiveBlockId(): string | null;
84
90
  declare function useEditorActions(): {
85
91
  setBlocks: (blocks: _reiwuzen_blocky.AnyBlock[]) => void;
86
92
  updateBlock: (block: _reiwuzen_blocky.AnyBlock) => void;
87
- insertBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
93
+ createBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
88
94
  removeBlock: (id: string) => string;
89
95
  duplicateBlock: (id: string) => void;
90
96
  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;
@@ -43,8 +43,8 @@ type Props$4 = {
43
43
  onFocus: (id: string) => void;
44
44
  onDragStart: (id: string) => void;
45
45
  onDrop: (dragId: string, dropId: string) => void;
46
- blockRefs: React.MutableRefObject<Map<string, HTMLSpanElement>>;
47
- hydratedBlocks: React.MutableRefObject<Set<string>>;
46
+ blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
47
+ hydratedBlocks: React.RefObject<Set<string>>;
48
48
  };
49
49
  declare function Block({ block, isActive, editable, onFocus, onDragStart, onDrop, blockRefs, hydratedBlocks, }: Props$4): react_jsx_runtime.JSX.Element;
50
50
 
@@ -54,10 +54,16 @@ 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>>;
58
+ hydratedBlocks: React.RefObject<Set<string>>;
59
59
  };
60
- declare function EditableContent({ block, className, placeholder, editable, onFocus, blockRefs, hydratedBlocks, }: Props$3): react_jsx_runtime.JSX.Element;
60
+ /**
61
+ * The inner span is wrapped in a memo that NEVER re-renders after mount.
62
+ * React.memo(() => true) tells React "props didn't change, skip re-render".
63
+ * This means the DOM is 100% owned by the browser after first paint.
64
+ * Keyboard handlers stay fresh via a forwarded ref (handlerRef).
65
+ */
66
+ declare function EditableContent({ block, className, editable, blockRefs, }: Props$3): react_jsx_runtime.JSX.Element;
61
67
  declare function nodesToHtml(nodes: Node[]): string;
62
68
  declare function domToNodes(el: HTMLElement): Node[];
63
69
 
@@ -84,7 +90,7 @@ declare function useActiveBlockId(): string | null;
84
90
  declare function useEditorActions(): {
85
91
  setBlocks: (blocks: _reiwuzen_blocky.AnyBlock[]) => void;
86
92
  updateBlock: (block: _reiwuzen_blocky.AnyBlock) => void;
87
- insertBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
93
+ createBlockAfter: (afterId: string, type?: _reiwuzen_blocky.AnyBlock["type"]) => string | null;
88
94
  removeBlock: (id: string) => string;
89
95
  duplicateBlock: (id: string) => void;
90
96
  moveBlock: (id: string, direction: "up" | "down") => void;
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import { createBlock } from "@reiwuzen/blocky";
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) => {
@@ -64,7 +59,7 @@ function createEditorStore(config = {}) {
64
59
  }
65
60
 
66
61
  // src/components/blockList.tsx
67
- import { useCallback as useCallback2 } from "react";
62
+ import { useCallback } from "react";
68
63
 
69
64
  // src/hooks/useEditor.ts
70
65
  import { useContext, useMemo } from "react";
@@ -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,202 +105,120 @@ function useEditorActions() {
110
105
  // src/components/block.tsx
111
106
  import { useState as useState2 } from "react";
112
107
 
113
- // src/hooks/useBlockKeyboard.ts
114
- import { useCallback } from "react";
115
- import {
116
- splitBlock,
117
- mergeBlocks,
118
- flatToPosition,
119
- applyMarkdownTransform,
120
- indentBlock,
121
- outdentBlock
122
- } from "@reiwuzen/blocky";
123
- function useBlockKeyboard({ block, onFocus }) {
124
- const { insertBlockAfter: insertBlockAfter2, removeBlock, updateBlock } = useEditorActions();
125
- const blocks = useBlocks();
126
- return useCallback((e) => {
127
- const fresh = blocks.find((b) => b.id === block.id) ?? block;
128
- const sel = window.getSelection();
129
- const flat = sel?.anchorOffset ?? 0;
130
- if (e.key === "Enter" && !e.shiftKey) {
131
- e.preventDefault();
132
- flatToPosition(fresh, flat).match(
133
- ({ nodeIndex, offset }) => {
134
- splitBlock(fresh, nodeIndex, offset).match(
135
- ([original, newBlock]) => {
136
- updateBlock(original);
137
- const newId = insertBlockAfter2(fresh.id, "paragraph");
138
- if (newId) {
139
- updateBlock({ ...newBlock, id: newId });
140
- onFocus(newId);
141
- setTimeout(() => {
142
- document.querySelector(`[data-block-id="${newId}"]`)?.focus();
143
- }, 0);
144
- }
145
- },
146
- () => {
147
- }
148
- );
149
- },
150
- () => {
151
- }
152
- );
153
- return;
154
- }
155
- if (e.key === "Backspace" && flat === 0) {
156
- const index = blocks.findIndex((b) => b.id === fresh.id);
157
- if (index === 0) return;
158
- const prev = blocks[index - 1];
159
- if (prev.type === "code" || prev.type === "equation") return;
160
- e.preventDefault();
161
- mergeBlocks(prev, fresh).match(
162
- (merged) => {
163
- updateBlock(merged);
164
- removeBlock(fresh.id);
165
- onFocus(merged.id);
166
- setTimeout(() => {
167
- document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
168
- }, 0);
169
- },
170
- () => {
171
- }
172
- );
173
- return;
174
- }
175
- if (e.key === " ") {
176
- applyMarkdownTransform(fresh, flat).match(
177
- ({ block: transformed, converted }) => {
178
- if (converted) {
179
- e.preventDefault();
180
- updateBlock(transformed);
181
- }
182
- },
183
- () => {
184
- }
185
- );
186
- return;
187
- }
188
- if (e.key === "Tab") {
189
- e.preventDefault();
190
- const fn = e.shiftKey ? outdentBlock : indentBlock;
191
- fn(fresh).match(
192
- (b) => updateBlock(b),
193
- () => {
194
- }
195
- );
196
- }
197
- }, [block.id, blocks, insertBlockAfter2, removeBlock, updateBlock, onFocus]);
198
- }
199
-
200
108
  // src/components/blocks/editableContent.tsx
201
109
  import { jsx } from "react/jsx-runtime";
202
110
  function EditableContent({
203
111
  block,
204
112
  className,
205
- placeholder,
206
113
  editable,
207
- onFocus,
208
- blockRefs,
209
- hydratedBlocks
114
+ blockRefs
210
115
  }) {
211
- const { updateBlock } = useEditorActions();
212
- const blocks = useBlocks();
213
- 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
- };
220
116
  return /* @__PURE__ */ jsx(
221
- "span",
117
+ "div",
222
118
  {
223
- "data-block-id": block.id,
224
- className: `blocky-block-content ${className ?? ""}`,
225
- "data-placeholder": placeholder,
119
+ className: `blocky-content ${className ?? ""}`,
226
120
  contentEditable: editable,
227
121
  suppressContentEditableWarning: true,
228
122
  ref: (el) => {
229
123
  if (!el) return;
230
124
  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
125
  },
235
- onInput: handleInput,
236
- onKeyDown: handleKeyDown,
237
- onFocus: () => onFocus(block.id)
126
+ children: block.content.map((node, i) => {
127
+ const { classString, dataAttrs } = nodeToAttrs(node);
128
+ return /* @__PURE__ */ jsx(
129
+ "span",
130
+ {
131
+ className: classString,
132
+ ...parseDataAttrs(dataAttrs),
133
+ children: node.type === "equation" ? node.latex : node.text
134
+ },
135
+ i
136
+ );
137
+ })
238
138
  }
239
139
  );
240
140
  }
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("");
141
+ function nodeToAttrs(n) {
142
+ const classes = [];
143
+ const data = [];
144
+ if (n.type === "code") classes.push("blocky-code");
145
+ if (n.type === "equation") classes.push("blocky-equation");
146
+ if (n.type === "text") {
147
+ if (n.bold) classes.push("blocky-bold");
148
+ if (n.italic) classes.push("blocky-italic");
149
+ if (n.underline) classes.push("blocky-underline");
150
+ if (n.strikethrough) classes.push("blocky-strike");
151
+ if (n.highlighted) classes.push(`blocky-highlight-${n.highlighted}`);
152
+ if (n.color) classes.push(`blocky-color-${n.color}`);
153
+ if (n.link) {
154
+ data.push(`data-link="${escAttr(n.link)}"`);
155
+ }
156
+ }
157
+ if (n.type === "equation") {
158
+ data.push(`data-latex="${escAttr(n.latex)}"`);
159
+ }
160
+ return {
161
+ classString: classes.join(" "),
162
+ dataAttrs: data.join(" ")
163
+ };
248
164
  }
249
165
  function nodesToHtml(nodes) {
250
166
  return nodes.map((n) => {
251
- if (n.type === "code") return `<code><span>${esc(n.text)}</span></code>`;
252
- if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
253
- let inner = `<span>${esc(n.text)}</span>`;
254
- if (n.bold) inner = `<strong>${inner}</strong>`;
255
- if (n.italic) inner = `<em>${inner}</em>`;
256
- if (n.underline) inner = `<u>${inner}</u>`;
257
- if (n.strikethrough) inner = `<s>${inner}</s>`;
258
- if (n.highlighted) inner = `<mark class="blocky-highlight-${n.highlighted}">${inner}</mark>`;
259
- if (n.color) inner = `<span class="blocky-color-${n.color}">${inner}</span>`;
260
- if (n.link) inner = `<a href="${n.link}">${inner}</a>`;
261
- return inner;
167
+ if (n.type === "equation") {
168
+ return `<span class="blocky-equation">${esc(n.latex)}</span>`;
169
+ }
170
+ if (n.type === "code") {
171
+ return `<span class="blocky-code">${esc(n.text)}</span>`;
172
+ }
173
+ if (n.type === "text") {
174
+ const classes = [];
175
+ if (n.bold) classes.push("blocky-bold");
176
+ if (n.italic) classes.push("blocky-italic");
177
+ if (n.underline) classes.push("blocky-underline");
178
+ if (n.strikethrough) classes.push("blocky-strike");
179
+ if (n.highlighted) classes.push(`blocky-highlight-${n.highlighted}`);
180
+ if (n.color) classes.push(`blocky-color-${n.color}`);
181
+ if (n.link) classes.push("blocky-link");
182
+ const linkAttr = n.link ? ` data-link="${escAttr(n.link)}"` : "";
183
+ const classAttr = classes.length > 0 ? ` class="${classes.join(" ")}"` : "";
184
+ return `<span${classAttr}${linkAttr}>${esc(n.text)}</span>`;
185
+ }
186
+ return "";
262
187
  }).join("");
263
188
  }
264
189
  function domToNodes(el) {
265
190
  const nodes = [];
266
- const walk = (node, formats = {}) => {
267
- if (node.nodeType === window.Node.TEXT_NODE) {
268
- const text = node.textContent ?? "";
269
- if (!text) return;
270
- nodes.push({ type: "text", text, ...formats });
271
- return;
272
- }
273
- if (!(node instanceof HTMLElement)) return;
274
- const tag = node.tagName.toLowerCase();
275
- const inherited = { ...formats };
276
- if (tag === "strong" || tag === "b") inherited.bold = true;
277
- if (tag === "em" || tag === "i") inherited.italic = true;
278
- if (tag === "u") inherited.underline = true;
279
- if (tag === "s") inherited.strikethrough = true;
280
- if (tag === "a") inherited.link = node.getAttribute("href") ?? void 0;
281
- if (tag === "mark") {
282
- inherited.highlighted = node.className.includes("green") ? "green" : "yellow";
283
- }
284
- if (tag === "code") {
285
- nodes.push({ type: "code", text: node.innerText });
286
- return;
287
- }
288
- if (tag === "span" && node.classList.contains("blocky-equation")) {
289
- nodes.push({ type: "equation", latex: node.innerText });
290
- return;
291
- }
292
- node.childNodes.forEach((child) => walk(child, inherited));
293
- };
294
- el.childNodes.forEach((child) => walk(child));
295
- const merged = [];
296
- for (const node of nodes) {
297
- const prev = merged[merged.length - 1];
298
- if (prev && node.type === "text" && prev.type === "text" && JSON.stringify({ ...prev, text: "" }) === JSON.stringify({ ...node, text: "" })) {
299
- prev.text += node.text;
300
- } else {
301
- merged.push({ ...node });
302
- }
303
- }
304
- return merged.length > 0 ? merged : [{ type: "text", text: "" }];
191
+ el.querySelectorAll("span").forEach((span) => {
192
+ const text = span.innerText ?? "";
193
+ const node = {
194
+ type: span.classList.contains("blocky-equation") ? "equation" : span.classList.contains("blocky-code") ? "code" : "text",
195
+ text
196
+ };
197
+ if (span.classList.contains("blocky-bold")) node.bold = true;
198
+ if (span.classList.contains("blocky-italic")) node.italic = true;
199
+ if (span.classList.contains("blocky-underline")) node.underline = true;
200
+ if (span.classList.contains("blocky-strike")) node.strikethrough = true;
201
+ if (span.dataset.link) node.link = span.dataset.link;
202
+ if (span.dataset.latex) node.latex = span.dataset.latex;
203
+ nodes.push(node);
204
+ });
205
+ return nodes.length ? nodes : [{ type: "text", text: "" }];
305
206
  }
306
207
  function esc(s) {
307
208
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
308
209
  }
210
+ function escAttr(s) {
211
+ return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
212
+ }
213
+ function parseDataAttrs(dataAttrs) {
214
+ const obj = {};
215
+ dataAttrs.split(" ").forEach((pair) => {
216
+ if (!pair) return;
217
+ const [k, v] = pair.split("=");
218
+ obj[k] = v?.replace(/"/g, "") ?? "";
219
+ });
220
+ return obj;
221
+ }
309
222
 
310
223
  // src/components/drag/DragHandle.tsx
311
224
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
@@ -547,7 +460,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
547
460
  const blocks = useBlocks();
548
461
  const activeBlockId = useActiveBlockId();
549
462
  const { setBlocks, setActiveBlockId } = useEditorActions();
550
- const handleDrop = useCallback2((dragId, dropId) => {
463
+ const handleDrop = useCallback((dragId, dropId) => {
551
464
  const from = blocks.findIndex((b) => b.id === dragId);
552
465
  const to = blocks.findIndex((b) => b.id === dropId);
553
466
  if (from === -1 || to === -1) return;
@@ -636,10 +549,10 @@ function Editor({
636
549
  }
637
550
 
638
551
  // src/hooks/useSelection.ts
639
- import { useCallback as useCallback3 } from "react";
552
+ import { useCallback as useCallback2 } from "react";
640
553
  import { flatToSelection as flatToSelection2 } from "@reiwuzen/blocky";
641
554
  function useSelection(block) {
642
- return useCallback3(() => {
555
+ return useCallback2(() => {
643
556
  const sel = window.getSelection();
644
557
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
645
558
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
@@ -648,6 +561,93 @@ function useSelection(block) {
648
561
  return result.ok ? result.value : null;
649
562
  }, [block]);
650
563
  }
564
+
565
+ // src/hooks/useBlockKeyboard.ts
566
+ import { useCallback as useCallback3 } from "react";
567
+ import {
568
+ splitBlock,
569
+ mergeBlocks,
570
+ flatToPosition,
571
+ applyMarkdownTransform,
572
+ indentBlock,
573
+ outdentBlock
574
+ } from "@reiwuzen/blocky";
575
+ function useBlockKeyboard({ block, onFocus }) {
576
+ const { createBlockAfter: createBlockAfter2, removeBlock, updateBlock } = useEditorActions();
577
+ const blocks = useBlocks();
578
+ return useCallback3((e) => {
579
+ const fresh = blocks.find((b) => b.id === block.id) ?? block;
580
+ const sel = window.getSelection();
581
+ const flat = sel?.anchorOffset ?? 0;
582
+ if (e.key === "Enter" && !e.shiftKey) {
583
+ e.preventDefault();
584
+ flatToPosition(fresh, flat).match(
585
+ ({ nodeIndex, offset }) => {
586
+ splitBlock(fresh, nodeIndex, offset).match(
587
+ ([original, newBlock]) => {
588
+ updateBlock(original);
589
+ const newId = createBlockAfter2(fresh.id, "paragraph");
590
+ if (newId) {
591
+ updateBlock({ ...newBlock, id: newId });
592
+ onFocus(newId);
593
+ setTimeout(() => {
594
+ document.querySelector(`[data-block-id="${newId}"]`)?.focus();
595
+ }, 0);
596
+ }
597
+ },
598
+ () => {
599
+ }
600
+ );
601
+ },
602
+ () => {
603
+ }
604
+ );
605
+ return;
606
+ }
607
+ if (e.key === "Backspace" && flat === 0) {
608
+ const index = blocks.findIndex((b) => b.id === fresh.id);
609
+ if (index === 0) return;
610
+ const prev = blocks[index - 1];
611
+ if (prev.type === "code" || prev.type === "equation") return;
612
+ e.preventDefault();
613
+ mergeBlocks(prev, fresh).match(
614
+ (merged) => {
615
+ updateBlock(merged);
616
+ removeBlock(fresh.id);
617
+ onFocus(merged.id);
618
+ setTimeout(() => {
619
+ document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
620
+ }, 0);
621
+ },
622
+ () => {
623
+ }
624
+ );
625
+ return;
626
+ }
627
+ if (e.key === " ") {
628
+ applyMarkdownTransform(fresh, flat).match(
629
+ ({ block: transformed, converted }) => {
630
+ if (converted) {
631
+ e.preventDefault();
632
+ updateBlock(transformed);
633
+ }
634
+ },
635
+ () => {
636
+ }
637
+ );
638
+ return;
639
+ }
640
+ if (e.key === "Tab") {
641
+ e.preventDefault();
642
+ const fn = e.shiftKey ? outdentBlock : indentBlock;
643
+ fn(fresh).match(
644
+ (b) => updateBlock(b),
645
+ () => {
646
+ }
647
+ );
648
+ }
649
+ }, [block.id, blocks, createBlockAfter2, removeBlock, updateBlock, onFocus]);
650
+ }
651
651
  export {
652
652
  Block,
653
653
  BlockList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reiwuzen/blocky-react",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "React UI layer for @reiwuzen/blocky — composable block editor components.",
5
5
  "author": "Rei WuZen",
6
6
  "license": "ISC",