@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 +213 -226
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +197 -210
- package/package.json +1 -1
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
|
|
53
|
-
var
|
|
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
|
|
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
|
|
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(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
162
|
+
"div",
|
|
255
163
|
{
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
{
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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 === "
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (n.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (n.
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
if (
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
360
254
|
}
|
|
255
|
+
function escAttr(s) {
|
|
256
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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,
|
|
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
|
|
427
|
-
var
|
|
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,
|
|
431
|
-
const [pos, setPos] = (0,
|
|
336
|
+
const ref = (0, import_react2.useRef)(null);
|
|
337
|
+
const [pos, setPos] = (0, import_react2.useState)(null);
|
|
432
338
|
const { updateBlock } = useEditorActions();
|
|
433
|
-
(0,
|
|
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,
|
|
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(
|
|
475
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--italic", onMouseDown: () => applyFormat(
|
|
476
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--underline", onMouseDown: () => applyFormat(
|
|
477
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--strike", onMouseDown: () => applyFormat(
|
|
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,
|
|
480
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--red", onMouseDown: () => applyFormat((n, s) => (0,
|
|
481
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--blue", onMouseDown: () => applyFormat((n, s) => (0,
|
|
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,
|
|
500
|
-
const [isDragOver, setIsDragOver] = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
637
|
-
const hydratedBlocks = (0,
|
|
638
|
-
const prevEditable = (0,
|
|
639
|
-
const store = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
691
|
-
var
|
|
597
|
+
var import_react6 = require("react");
|
|
598
|
+
var import_blocky5 = require("@reiwuzen/blocky");
|
|
692
599
|
function useSelection(block) {
|
|
693
|
-
return (0,
|
|
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,
|
|
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.
|
|
47
|
-
hydratedBlocks: React.
|
|
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(
|
|
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.
|
|
47
|
-
hydratedBlocks: React.
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
117
|
+
"div",
|
|
217
118
|
{
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
{
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 === "
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if (n.
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (n.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
if (
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
322
209
|
}
|
|
210
|
+
function escAttr(s) {
|
|
211
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
599
|
-
const hydratedBlocks =
|
|
600
|
-
const prevEditable =
|
|
601
|
-
const store =
|
|
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
|
|
552
|
+
import { useCallback as useCallback2 } from "react";
|
|
653
553
|
import { flatToSelection as flatToSelection2 } from "@reiwuzen/blocky";
|
|
654
554
|
function useSelection(block) {
|
|
655
|
-
return
|
|
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,
|