@reiwuzen/blocky-react 1.0.2 → 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_react7 = 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");
@@ -104,7 +104,7 @@ function createEditorStore(config = {}) {
104
104
  }
105
105
 
106
106
  // src/components/blockList.tsx
107
- var import_react6 = require("react");
107
+ var import_react4 = require("react");
108
108
 
109
109
  // src/hooks/useEditor.ts
110
110
  var import_react = require("react");
@@ -148,216 +148,122 @@ function useEditorActions() {
148
148
  }
149
149
 
150
150
  // src/components/block.tsx
151
- var import_react5 = require("react");
152
-
153
- // src/components/blocks/editableContent.tsx
154
- var import_react3 = __toESM(require("react"), 1);
155
-
156
- // src/hooks/useBlockKeyboard.ts
157
- var import_react2 = require("react");
158
- var import_blocky2 = require("@reiwuzen/blocky");
159
- function useBlockKeyboard({ block, onFocus }) {
160
- const { createBlockAfter: createBlockAfter2, removeBlock, updateBlock } = useEditorActions();
161
- const blocks = useBlocks();
162
- return (0, import_react2.useCallback)((e) => {
163
- const fresh = blocks.find((b) => b.id === block.id) ?? block;
164
- const sel = window.getSelection();
165
- const flat = sel?.anchorOffset ?? 0;
166
- if (e.key === "Enter" && !e.shiftKey) {
167
- e.preventDefault();
168
- (0, import_blocky2.flatToPosition)(fresh, flat).match(
169
- ({ nodeIndex, offset }) => {
170
- (0, import_blocky2.splitBlock)(fresh, nodeIndex, offset).match(
171
- ([original, newBlock]) => {
172
- updateBlock(original);
173
- const newId = createBlockAfter2(fresh.id, "paragraph");
174
- if (newId) {
175
- updateBlock({ ...newBlock, id: newId });
176
- onFocus(newId);
177
- setTimeout(() => {
178
- document.querySelector(`[data-block-id="${newId}"]`)?.focus();
179
- }, 0);
180
- }
181
- },
182
- () => {
183
- }
184
- );
185
- },
186
- () => {
187
- }
188
- );
189
- return;
190
- }
191
- if (e.key === "Backspace" && flat === 0) {
192
- const index = blocks.findIndex((b) => b.id === fresh.id);
193
- if (index === 0) return;
194
- const prev = blocks[index - 1];
195
- if (prev.type === "code" || prev.type === "equation") return;
196
- e.preventDefault();
197
- (0, import_blocky2.mergeBlocks)(prev, fresh).match(
198
- (merged) => {
199
- updateBlock(merged);
200
- removeBlock(fresh.id);
201
- onFocus(merged.id);
202
- setTimeout(() => {
203
- document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
204
- }, 0);
205
- },
206
- () => {
207
- }
208
- );
209
- return;
210
- }
211
- if (e.key === " ") {
212
- (0, import_blocky2.applyMarkdownTransform)(fresh, flat).match(
213
- ({ block: transformed, converted }) => {
214
- if (converted) {
215
- e.preventDefault();
216
- updateBlock(transformed);
217
- }
218
- },
219
- () => {
220
- }
221
- );
222
- return;
223
- }
224
- if (e.key === "Tab") {
225
- e.preventDefault();
226
- const fn = e.shiftKey ? import_blocky2.outdentBlock : import_blocky2.indentBlock;
227
- fn(fresh).match(
228
- (b) => updateBlock(b),
229
- () => {
230
- }
231
- );
232
- }
233
- }, [block.id, blocks, createBlockAfter2, removeBlock, updateBlock, onFocus]);
234
- }
151
+ var import_react3 = require("react");
235
152
 
236
153
  // src/components/blocks/editableContent.tsx
237
154
  var import_jsx_runtime = require("react/jsx-runtime");
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
- });
244
- const handleKeyDown = useBlockKeyboard({ block, onFocus });
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
- );
155
+ function EditableContent({
156
+ block,
157
+ className,
158
+ editable,
159
+ blockRefs
160
+ }) {
253
161
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
254
- StaticSpan,
162
+ "div",
255
163
  {
256
- blockId: block.id,
257
- className: className ?? "",
258
- placeholder: placeholder ?? "",
259
- editable,
260
- initialHtml,
261
- blockRefs,
262
- keyDownRef,
263
- focusRef
164
+ className: `blocky-content ${className ?? ""}`,
165
+ contentEditable: editable,
166
+ suppressContentEditableWarning: true,
167
+ ref: (el) => {
168
+ if (!el) return;
169
+ blockRefs.current.set(block.id, el);
170
+ },
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
+ })
264
183
  }
265
184
  );
266
185
  }
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
- );
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
+ };
209
+ }
301
210
  function nodesToHtml(nodes) {
302
211
  return nodes.map((n) => {
303
- if (n.type === "code") return `<code>${esc(n.text)}</code>`;
304
- if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
305
- let inner = esc(n.text);
306
- if (n.bold) inner = `<strong>${inner}</strong>`;
307
- if (n.italic) inner = `<em>${inner}</em>`;
308
- if (n.underline) inner = `<u>${inner}</u>`;
309
- if (n.strikethrough) inner = `<s>${inner}</s>`;
310
- if (n.highlighted) inner = `<mark class="blocky-highlight-${n.highlighted}">${inner}</mark>`;
311
- if (n.color) inner = `<span class="blocky-color-${n.color}">${inner}</span>`;
312
- if (n.link) inner = `<a href="${n.link}">${inner}</a>`;
313
- 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 "";
314
232
  }).join("");
315
233
  }
316
234
  function domToNodes(el) {
317
235
  const nodes = [];
318
- const walk = (node, formats = {}) => {
319
- if (node.nodeType === window.Node.TEXT_NODE) {
320
- const text = node.textContent ?? "";
321
- if (!text) return;
322
- nodes.push({ type: "text", text, ...formats });
323
- return;
324
- }
325
- if (!(node instanceof HTMLElement)) return;
326
- const tag = node.tagName.toLowerCase();
327
- const inherited = { ...formats };
328
- if (tag === "strong" || tag === "b") inherited.bold = true;
329
- if (tag === "em" || tag === "i") inherited.italic = true;
330
- if (tag === "u") inherited.underline = true;
331
- if (tag === "s") inherited.strikethrough = true;
332
- if (tag === "a") inherited.link = node.getAttribute("href") ?? void 0;
333
- if (tag === "mark") {
334
- inherited.highlighted = node.className.includes("green") ? "green" : "yellow";
335
- }
336
- if (tag === "code") {
337
- nodes.push({ type: "code", text: node.innerText });
338
- return;
339
- }
340
- if (tag === "span" && node.classList.contains("blocky-equation")) {
341
- nodes.push({ type: "equation", latex: node.innerText });
342
- return;
343
- }
344
- node.childNodes.forEach((child) => walk(child, inherited));
345
- };
346
- el.childNodes.forEach((child) => walk(child));
347
- const merged = [];
348
- for (const node of nodes) {
349
- const prev = merged[merged.length - 1];
350
- if (prev && node.type === "text" && prev.type === "text" && JSON.stringify({ ...prev, text: "" }) === JSON.stringify({ ...node, text: "" })) {
351
- prev.text += node.text;
352
- } else {
353
- merged.push({ ...node });
354
- }
355
- }
356
- 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: "" }];
357
251
  }
358
252
  function esc(s) {
359
253
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
360
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
+ }
361
267
 
362
268
  // src/components/drag/DragHandle.tsx
363
269
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -386,7 +292,7 @@ function DragHandle({ blockId, onDragStart }) {
386
292
  }
387
293
 
388
294
  // src/components/toolbar/BlockTypeSwitcher.tsx
389
- var import_blocky3 = require("@reiwuzen/blocky");
295
+ var import_blocky2 = require("@reiwuzen/blocky");
390
296
  var import_jsx_runtime3 = require("react/jsx-runtime");
391
297
  var BLOCK_TYPES = [
392
298
  { type: "paragraph", label: "Text" },
@@ -407,7 +313,7 @@ function BlockTypeSwitcher({ block, onClose }) {
407
313
  className: `blocky-type-option ${block.type === type ? "blocky-type-option--active" : ""}`,
408
314
  onMouseDown: (e) => {
409
315
  e.preventDefault();
410
- (0, import_blocky3.changeBlockType)(block, type).match(
316
+ (0, import_blocky2.changeBlockType)(block, type).match(
411
317
  (b) => {
412
318
  updateBlock(b);
413
319
  onClose();
@@ -423,14 +329,14 @@ function BlockTypeSwitcher({ block, onClose }) {
423
329
  }
424
330
 
425
331
  // src/components/toolbar/FormatToolbar.tsx
426
- var import_react4 = require("react");
427
- var import_blocky4 = require("@reiwuzen/blocky");
332
+ var import_react2 = require("react");
333
+ var import_blocky3 = require("@reiwuzen/blocky");
428
334
  var import_jsx_runtime4 = require("react/jsx-runtime");
429
335
  function FormatToolbar({ block }) {
430
- const ref = (0, import_react4.useRef)(null);
431
- const [pos, setPos] = (0, import_react4.useState)(null);
336
+ const ref = (0, import_react2.useRef)(null);
337
+ const [pos, setPos] = (0, import_react2.useState)(null);
432
338
  const { updateBlock } = useEditorActions();
433
- (0, import_react4.useEffect)(() => {
339
+ (0, import_react2.useEffect)(() => {
434
340
  const onSelectionChange = () => {
435
341
  const sel = window.getSelection();
436
342
  if (!sel || sel.isCollapsed || sel.rangeCount === 0) {
@@ -453,7 +359,7 @@ function FormatToolbar({ block }) {
453
359
  if (!sel) return;
454
360
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
455
361
  const end = Math.max(sel.anchorOffset, sel.focusOffset);
456
- (0, import_blocky4.flatToSelection)(block, start, end).match(
362
+ (0, import_blocky3.flatToSelection)(block, start, end).match(
457
363
  (nodeSel) => fn(block.content, nodeSel).match(
458
364
  (content) => updateBlock({ ...block, content }),
459
365
  () => {
@@ -471,14 +377,14 @@ function FormatToolbar({ block }) {
471
377
  style: { top: pos.top, left: pos.left },
472
378
  onMouseDown: (e) => e.preventDefault(),
473
379
  children: [
474
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn", onMouseDown: () => applyFormat(import_blocky4.toggleBold), children: "B" }),
475
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--italic", onMouseDown: () => applyFormat(import_blocky4.toggleItalic), children: "I" }),
476
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--underline", onMouseDown: () => applyFormat(import_blocky4.toggleUnderline), children: "U" }),
477
- /* @__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" }),
478
384
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "blocky-toolbar-divider" }),
479
- /* @__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" }),
480
- /* @__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" }),
481
- /* @__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" })
482
388
  ]
483
389
  }
484
390
  );
@@ -496,8 +402,8 @@ function Block({
496
402
  blockRefs,
497
403
  hydratedBlocks
498
404
  }) {
499
- const [showSwitcher, setShowSwitcher] = (0, import_react5.useState)(false);
500
- const [isDragOver, setIsDragOver] = (0, import_react5.useState)(false);
405
+ const [showSwitcher, setShowSwitcher] = (0, import_react3.useState)(false);
406
+ const [isDragOver, setIsDragOver] = (0, import_react3.useState)(false);
501
407
  const { updateBlock } = useEditorActions();
502
408
  const { className, placeholder } = blockMeta(block);
503
409
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
@@ -559,7 +465,8 @@ function Block({
559
465
  placeholder,
560
466
  editable,
561
467
  onFocus,
562
- blockRefs
468
+ blockRefs,
469
+ hydratedBlocks
563
470
  }
564
471
  ),
565
472
  editable && isActive && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(FormatToolbar, { block }),
@@ -598,7 +505,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
598
505
  const blocks = useBlocks();
599
506
  const activeBlockId = useActiveBlockId();
600
507
  const { setBlocks, setActiveBlockId } = useEditorActions();
601
- const handleDrop = (0, import_react6.useCallback)((dragId, dropId) => {
508
+ const handleDrop = (0, import_react4.useCallback)((dragId, dropId) => {
602
509
  const from = blocks.findIndex((b) => b.id === dragId);
603
510
  const to = blocks.findIndex((b) => b.id === dropId);
604
511
  if (from === -1 || to === -1) return;
@@ -625,7 +532,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
625
532
 
626
533
  // src/components/editor.tsx
627
534
  var import_jsx_runtime7 = require("react/jsx-runtime");
628
- var EditorContext = (0, import_react7.createContext)(null);
535
+ var EditorContext = (0, import_react5.createContext)(null);
629
536
  function Editor({
630
537
  blocks: seedBlocks,
631
538
  onChange,
@@ -633,27 +540,27 @@ function Editor({
633
540
  className,
634
541
  placeholder = "Start writing..."
635
542
  }) {
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)(
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)(
640
547
  () => createEditorStore({ initialBlocks: seedBlocks }),
641
548
  // eslint-disable-next-line react-hooks/exhaustive-deps
642
549
  []
643
550
  );
644
- (0, import_react7.useLayoutEffect)(() => {
551
+ (0, import_react5.useLayoutEffect)(() => {
645
552
  const state = store.getState();
646
553
  if (seedBlocks && seedBlocks.length > 0) {
647
554
  state.setBlocks(seedBlocks);
648
555
  } else if (state.blocks.length === 0) {
649
- (0, import_blocky5.createBlock)("paragraph").match(
556
+ (0, import_blocky4.createBlock)("paragraph").match(
650
557
  (b) => state.setBlocks([b]),
651
558
  () => {
652
559
  }
653
560
  );
654
561
  }
655
562
  }, []);
656
- (0, import_react7.useEffect)(() => {
563
+ (0, import_react5.useEffect)(() => {
657
564
  const wasEditable = prevEditable.current;
658
565
  prevEditable.current = editable;
659
566
  if (!wasEditable || editable) return;
@@ -687,18 +594,98 @@ function Editor({
687
594
  }
688
595
 
689
596
  // src/hooks/useSelection.ts
690
- var import_react8 = require("react");
691
- var import_blocky6 = require("@reiwuzen/blocky");
597
+ var import_react6 = require("react");
598
+ var import_blocky5 = require("@reiwuzen/blocky");
692
599
  function useSelection(block) {
693
- return (0, import_react8.useCallback)(() => {
600
+ return (0, import_react6.useCallback)(() => {
694
601
  const sel = window.getSelection();
695
602
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
696
603
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
697
604
  const end = Math.max(sel.anchorOffset, sel.focusOffset);
698
- const result = (0, import_blocky6.flatToSelection)(block, start, end);
605
+ const result = (0, import_blocky5.flatToSelection)(block, start, end);
699
606
  return result.ok ? result.value : null;
700
607
  }, [block]);
701
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
+ }
702
689
  // Annotate the CommonJS export names for ESM import in node:
703
690
  0 && (module.exports = {
704
691
  Block,
package/dist/index.d.cts CHANGED
@@ -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
 
@@ -55,6 +55,7 @@ type Props$3 = {
55
55
  editable: boolean;
56
56
  onFocus: (id: string) => void;
57
57
  blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
58
+ hydratedBlocks: React.RefObject<Set<string>>;
58
59
  };
59
60
  /**
60
61
  * The inner span is wrapped in a memo that NEVER re-renders after mount.
@@ -62,7 +63,7 @@ type Props$3 = {
62
63
  * This means the DOM is 100% owned by the browser after first paint.
63
64
  * Keyboard handlers stay fresh via a forwarded ref (handlerRef).
64
65
  */
65
- declare function EditableContent(props: Props$3): react_jsx_runtime.JSX.Element;
66
+ declare function EditableContent({ block, className, editable, blockRefs, }: Props$3): react_jsx_runtime.JSX.Element;
66
67
  declare function nodesToHtml(nodes: Node[]): string;
67
68
  declare function domToNodes(el: HTMLElement): Node[];
68
69
 
package/dist/index.d.ts CHANGED
@@ -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
 
@@ -55,6 +55,7 @@ type Props$3 = {
55
55
  editable: boolean;
56
56
  onFocus: (id: string) => void;
57
57
  blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
58
+ hydratedBlocks: React.RefObject<Set<string>>;
58
59
  };
59
60
  /**
60
61
  * The inner span is wrapped in a memo that NEVER re-renders after mount.
@@ -62,7 +63,7 @@ type Props$3 = {
62
63
  * This means the DOM is 100% owned by the browser after first paint.
63
64
  * Keyboard handlers stay fresh via a forwarded ref (handlerRef).
64
65
  */
65
- declare function EditableContent(props: Props$3): react_jsx_runtime.JSX.Element;
66
+ declare function EditableContent({ block, className, editable, blockRefs, }: Props$3): react_jsx_runtime.JSX.Element;
66
67
  declare function nodesToHtml(nodes: Node[]): string;
67
68
  declare function domToNodes(el: HTMLElement): Node[];
68
69
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/editor.tsx
2
- import { createContext, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo3, useRef as useRef4 } from "react";
2
+ import { createContext, useEffect as useEffect2, useLayoutEffect, useMemo as useMemo2, useRef as useRef3 } from "react";
3
3
  import { createBlock } from "@reiwuzen/blocky";
4
4
 
5
5
  // src/store/editor-store.ts
@@ -59,7 +59,7 @@ function createEditorStore(config = {}) {
59
59
  }
60
60
 
61
61
  // src/components/blockList.tsx
62
- import { useCallback as useCallback2 } from "react";
62
+ import { useCallback } from "react";
63
63
 
64
64
  // src/hooks/useEditor.ts
65
65
  import { useContext, useMemo } from "react";
@@ -105,221 +105,120 @@ function useEditorActions() {
105
105
  // src/components/block.tsx
106
106
  import { useState as useState2 } from "react";
107
107
 
108
- // src/components/blocks/editableContent.tsx
109
- import React, { useRef, useMemo as useMemo2 } from "react";
110
-
111
- // src/hooks/useBlockKeyboard.ts
112
- import { useCallback } from "react";
113
- import {
114
- splitBlock,
115
- mergeBlocks,
116
- flatToPosition,
117
- applyMarkdownTransform,
118
- indentBlock,
119
- outdentBlock
120
- } from "@reiwuzen/blocky";
121
- function useBlockKeyboard({ block, onFocus }) {
122
- const { createBlockAfter: createBlockAfter2, removeBlock, updateBlock } = useEditorActions();
123
- const blocks = useBlocks();
124
- return useCallback((e) => {
125
- const fresh = blocks.find((b) => b.id === block.id) ?? block;
126
- const sel = window.getSelection();
127
- const flat = sel?.anchorOffset ?? 0;
128
- if (e.key === "Enter" && !e.shiftKey) {
129
- e.preventDefault();
130
- flatToPosition(fresh, flat).match(
131
- ({ nodeIndex, offset }) => {
132
- splitBlock(fresh, nodeIndex, offset).match(
133
- ([original, newBlock]) => {
134
- updateBlock(original);
135
- const newId = createBlockAfter2(fresh.id, "paragraph");
136
- if (newId) {
137
- updateBlock({ ...newBlock, id: newId });
138
- onFocus(newId);
139
- setTimeout(() => {
140
- document.querySelector(`[data-block-id="${newId}"]`)?.focus();
141
- }, 0);
142
- }
143
- },
144
- () => {
145
- }
146
- );
147
- },
148
- () => {
149
- }
150
- );
151
- return;
152
- }
153
- if (e.key === "Backspace" && flat === 0) {
154
- const index = blocks.findIndex((b) => b.id === fresh.id);
155
- if (index === 0) return;
156
- const prev = blocks[index - 1];
157
- if (prev.type === "code" || prev.type === "equation") return;
158
- e.preventDefault();
159
- mergeBlocks(prev, fresh).match(
160
- (merged) => {
161
- updateBlock(merged);
162
- removeBlock(fresh.id);
163
- onFocus(merged.id);
164
- setTimeout(() => {
165
- document.querySelector(`[data-block-id="${merged.id}"]`)?.focus();
166
- }, 0);
167
- },
168
- () => {
169
- }
170
- );
171
- return;
172
- }
173
- if (e.key === " ") {
174
- applyMarkdownTransform(fresh, flat).match(
175
- ({ block: transformed, converted }) => {
176
- if (converted) {
177
- e.preventDefault();
178
- updateBlock(transformed);
179
- }
180
- },
181
- () => {
182
- }
183
- );
184
- return;
185
- }
186
- if (e.key === "Tab") {
187
- e.preventDefault();
188
- const fn = e.shiftKey ? outdentBlock : indentBlock;
189
- fn(fresh).match(
190
- (b) => updateBlock(b),
191
- () => {
192
- }
193
- );
194
- }
195
- }, [block.id, blocks, createBlockAfter2, removeBlock, updateBlock, onFocus]);
196
- }
197
-
198
108
  // src/components/blocks/editableContent.tsx
199
109
  import { jsx } from "react/jsx-runtime";
200
- function EditableContent(props) {
201
- const { block, className, placeholder, editable, onFocus, blockRefs } = props;
202
- const keyDownRef = useRef(() => {
203
- });
204
- const focusRef = useRef(() => {
205
- });
206
- const handleKeyDown = useBlockKeyboard({ block, onFocus });
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
- );
110
+ function EditableContent({
111
+ block,
112
+ className,
113
+ editable,
114
+ blockRefs
115
+ }) {
215
116
  return /* @__PURE__ */ jsx(
216
- StaticSpan,
117
+ "div",
217
118
  {
218
- blockId: block.id,
219
- className: className ?? "",
220
- placeholder: placeholder ?? "",
221
- editable,
222
- initialHtml,
223
- blockRefs,
224
- keyDownRef,
225
- focusRef
119
+ className: `blocky-content ${className ?? ""}`,
120
+ contentEditable: editable,
121
+ suppressContentEditableWarning: true,
122
+ ref: (el) => {
123
+ if (!el) return;
124
+ blockRefs.current.set(block.id, el);
125
+ },
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
+ })
226
138
  }
227
139
  );
228
140
  }
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
- );
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
+ };
164
+ }
263
165
  function nodesToHtml(nodes) {
264
166
  return nodes.map((n) => {
265
- if (n.type === "code") return `<code>${esc(n.text)}</code>`;
266
- if (n.type === "equation") return `<span class="blocky-equation">${esc(n.latex)}</span>`;
267
- let inner = esc(n.text);
268
- if (n.bold) inner = `<strong>${inner}</strong>`;
269
- if (n.italic) inner = `<em>${inner}</em>`;
270
- if (n.underline) inner = `<u>${inner}</u>`;
271
- if (n.strikethrough) inner = `<s>${inner}</s>`;
272
- if (n.highlighted) inner = `<mark class="blocky-highlight-${n.highlighted}">${inner}</mark>`;
273
- if (n.color) inner = `<span class="blocky-color-${n.color}">${inner}</span>`;
274
- if (n.link) inner = `<a href="${n.link}">${inner}</a>`;
275
- 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 "";
276
187
  }).join("");
277
188
  }
278
189
  function domToNodes(el) {
279
190
  const nodes = [];
280
- const walk = (node, formats = {}) => {
281
- if (node.nodeType === window.Node.TEXT_NODE) {
282
- const text = node.textContent ?? "";
283
- if (!text) return;
284
- nodes.push({ type: "text", text, ...formats });
285
- return;
286
- }
287
- if (!(node instanceof HTMLElement)) return;
288
- const tag = node.tagName.toLowerCase();
289
- const inherited = { ...formats };
290
- if (tag === "strong" || tag === "b") inherited.bold = true;
291
- if (tag === "em" || tag === "i") inherited.italic = true;
292
- if (tag === "u") inherited.underline = true;
293
- if (tag === "s") inherited.strikethrough = true;
294
- if (tag === "a") inherited.link = node.getAttribute("href") ?? void 0;
295
- if (tag === "mark") {
296
- inherited.highlighted = node.className.includes("green") ? "green" : "yellow";
297
- }
298
- if (tag === "code") {
299
- nodes.push({ type: "code", text: node.innerText });
300
- return;
301
- }
302
- if (tag === "span" && node.classList.contains("blocky-equation")) {
303
- nodes.push({ type: "equation", latex: node.innerText });
304
- return;
305
- }
306
- node.childNodes.forEach((child) => walk(child, inherited));
307
- };
308
- el.childNodes.forEach((child) => walk(child));
309
- const merged = [];
310
- for (const node of nodes) {
311
- const prev = merged[merged.length - 1];
312
- if (prev && node.type === "text" && prev.type === "text" && JSON.stringify({ ...prev, text: "" }) === JSON.stringify({ ...node, text: "" })) {
313
- prev.text += node.text;
314
- } else {
315
- merged.push({ ...node });
316
- }
317
- }
318
- 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: "" }];
319
206
  }
320
207
  function esc(s) {
321
208
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
322
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
+ }
323
222
 
324
223
  // src/components/drag/DragHandle.tsx
325
224
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
@@ -385,11 +284,11 @@ function BlockTypeSwitcher({ block, onClose }) {
385
284
  }
386
285
 
387
286
  // src/components/toolbar/FormatToolbar.tsx
388
- import { useEffect, useRef as useRef2, useState } from "react";
287
+ import { useEffect, useRef, useState } from "react";
389
288
  import { toggleBold, toggleItalic, toggleUnderline, toggleStrikethrough, toggleHighlight, toggleColor, flatToSelection } from "@reiwuzen/blocky";
390
289
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
391
290
  function FormatToolbar({ block }) {
392
- const ref = useRef2(null);
291
+ const ref = useRef(null);
393
292
  const [pos, setPos] = useState(null);
394
293
  const { updateBlock } = useEditorActions();
395
294
  useEffect(() => {
@@ -521,7 +420,8 @@ function Block({
521
420
  placeholder,
522
421
  editable,
523
422
  onFocus,
524
- blockRefs
423
+ blockRefs,
424
+ hydratedBlocks
525
425
  }
526
426
  ),
527
427
  editable && isActive && /* @__PURE__ */ jsx5(FormatToolbar, { block }),
@@ -560,7 +460,7 @@ function BlockList({ editable, blockRefs, hydratedBlocks }) {
560
460
  const blocks = useBlocks();
561
461
  const activeBlockId = useActiveBlockId();
562
462
  const { setBlocks, setActiveBlockId } = useEditorActions();
563
- const handleDrop = useCallback2((dragId, dropId) => {
463
+ const handleDrop = useCallback((dragId, dropId) => {
564
464
  const from = blocks.findIndex((b) => b.id === dragId);
565
465
  const to = blocks.findIndex((b) => b.id === dropId);
566
466
  if (from === -1 || to === -1) return;
@@ -595,10 +495,10 @@ function Editor({
595
495
  className,
596
496
  placeholder = "Start writing..."
597
497
  }) {
598
- const blockRefs = useRef4(/* @__PURE__ */ new Map());
599
- const hydratedBlocks = useRef4(/* @__PURE__ */ new Set());
600
- const prevEditable = useRef4(editable);
601
- const store = useMemo3(
498
+ const blockRefs = useRef3(/* @__PURE__ */ new Map());
499
+ const hydratedBlocks = useRef3(/* @__PURE__ */ new Set());
500
+ const prevEditable = useRef3(editable);
501
+ const store = useMemo2(
602
502
  () => createEditorStore({ initialBlocks: seedBlocks }),
603
503
  // eslint-disable-next-line react-hooks/exhaustive-deps
604
504
  []
@@ -649,10 +549,10 @@ function Editor({
649
549
  }
650
550
 
651
551
  // src/hooks/useSelection.ts
652
- import { useCallback as useCallback3 } from "react";
552
+ import { useCallback as useCallback2 } from "react";
653
553
  import { flatToSelection as flatToSelection2 } from "@reiwuzen/blocky";
654
554
  function useSelection(block) {
655
- return useCallback3(() => {
555
+ return useCallback2(() => {
656
556
  const sel = window.getSelection();
657
557
  if (!sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
658
558
  const start = Math.min(sel.anchorOffset, sel.focusOffset);
@@ -661,6 +561,93 @@ function useSelection(block) {
661
561
  return result.ok ? result.value : null;
662
562
  }, [block]);
663
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
+ }
664
651
  export {
665
652
  Block,
666
653
  BlockList,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reiwuzen/blocky-react",
3
- "version": "1.0.2",
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",