@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 +208 -208
- package/dist/index.d.cts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +187 -187
- 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");
|
|
@@ -68,8 +68,8 @@ function createEditorStore(config = {}) {
|
|
|
68
68
|
set({ blocks });
|
|
69
69
|
config.onChange?.(blocks);
|
|
70
70
|
},
|
|
71
|
-
|
|
72
|
-
const result = (0, import_blocky.
|
|
71
|
+
createBlockAfter: (afterId, type = "paragraph") => {
|
|
72
|
+
const result = (0, import_blocky.createBlockAfter)(get().blocks, afterId, type);
|
|
73
73
|
if (!result.ok) return null;
|
|
74
74
|
const { blocks, newId } = result.value;
|
|
75
75
|
set({ blocks, activeBlockId: newId });
|
|
@@ -83,15 +83,10 @@ function createEditorStore(config = {}) {
|
|
|
83
83
|
return prevId;
|
|
84
84
|
},
|
|
85
85
|
duplicate: (id) => {
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
if (!result.ok) return;
|
|
91
|
-
const blocks = result.value.blocks.map(
|
|
92
|
-
(b) => b.id === result.value.newId ? dup : b
|
|
93
|
-
);
|
|
94
|
-
set({ blocks, activeBlockId: dup.id });
|
|
86
|
+
const res = (0, import_blocky.duplicateBlockAfter)(get().blocks, id);
|
|
87
|
+
if (res == null) return;
|
|
88
|
+
const blocks = [...res.value.blocks];
|
|
89
|
+
set({ blocks, activeBlockId: res.value.newFocusId });
|
|
95
90
|
config.onChange?.(blocks);
|
|
96
91
|
},
|
|
97
92
|
move: (id, direction) => {
|
|
@@ -109,7 +104,7 @@ function createEditorStore(config = {}) {
|
|
|
109
104
|
}
|
|
110
105
|
|
|
111
106
|
// src/components/blockList.tsx
|
|
112
|
-
var
|
|
107
|
+
var import_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
|
|
126
|
+
const createBlockAfter2 = useEditor((s) => s.createBlockAfter);
|
|
132
127
|
const removeBlock = useEditor((s) => s.removeBlock);
|
|
133
128
|
const duplicateBlock2 = useEditor((s) => s.duplicate);
|
|
134
129
|
const moveBlock2 = useEditor((s) => s.move);
|
|
@@ -136,7 +131,7 @@ function useEditorActions() {
|
|
|
136
131
|
return (0, import_react.useMemo)(() => ({
|
|
137
132
|
setBlocks,
|
|
138
133
|
updateBlock,
|
|
139
|
-
|
|
134
|
+
createBlockAfter: createBlockAfter2,
|
|
140
135
|
removeBlock,
|
|
141
136
|
duplicateBlock: duplicateBlock2,
|
|
142
137
|
moveBlock: moveBlock2,
|
|
@@ -144,7 +139,7 @@ function useEditorActions() {
|
|
|
144
139
|
}), [
|
|
145
140
|
setBlocks,
|
|
146
141
|
updateBlock,
|
|
147
|
-
|
|
142
|
+
createBlockAfter2,
|
|
148
143
|
removeBlock,
|
|
149
144
|
duplicateBlock2,
|
|
150
145
|
moveBlock2,
|
|
@@ -153,197 +148,122 @@ function useEditorActions() {
|
|
|
153
148
|
}
|
|
154
149
|
|
|
155
150
|
// src/components/block.tsx
|
|
156
|
-
var
|
|
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
|
-
|
|
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
|
-
"
|
|
162
|
+
"div",
|
|
260
163
|
{
|
|
261
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 === "
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (n.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (n.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (
|
|
315
|
-
if (
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
346
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
|
+
}
|
|
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
|
|
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,
|
|
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
|
|
413
|
-
var
|
|
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,
|
|
417
|
-
const [pos, setPos] = (0,
|
|
336
|
+
const ref = (0, import_react2.useRef)(null);
|
|
337
|
+
const [pos, setPos] = (0, import_react2.useState)(null);
|
|
418
338
|
const { updateBlock } = useEditorActions();
|
|
419
|
-
(0,
|
|
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,
|
|
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(
|
|
461
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--italic", onMouseDown: () => applyFormat(
|
|
462
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--underline", onMouseDown: () => applyFormat(
|
|
463
|
-
/* @__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" }),
|
|
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,
|
|
466
|
-
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { className: "blocky-toolbar-btn blocky-toolbar-btn--red", onMouseDown: () => applyFormat((n, s) => (0,
|
|
467
|
-
/* @__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" })
|
|
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,
|
|
486
|
-
const [isDragOver, setIsDragOver] = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
624
|
-
const hydratedBlocks = (0,
|
|
625
|
-
const prevEditable = (0,
|
|
626
|
-
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)(
|
|
627
547
|
() => createEditorStore({ initialBlocks: seedBlocks }),
|
|
628
548
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
629
549
|
[]
|
|
630
550
|
);
|
|
631
|
-
(0,
|
|
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,
|
|
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,
|
|
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
|
|
678
|
-
var
|
|
597
|
+
var import_react6 = require("react");
|
|
598
|
+
var import_blocky5 = require("@reiwuzen/blocky");
|
|
679
599
|
function useSelection(block) {
|
|
680
|
-
return (0,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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
|
|
|
@@ -54,10 +54,16 @@ type Props$3 = {
|
|
|
54
54
|
placeholder?: string;
|
|
55
55
|
editable: boolean;
|
|
56
56
|
onFocus: (id: string) => void;
|
|
57
|
-
blockRefs: React.
|
|
58
|
-
hydratedBlocks: React.
|
|
57
|
+
blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
|
|
58
|
+
hydratedBlocks: React.RefObject<Set<string>>;
|
|
59
59
|
};
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
|
@@ -54,10 +54,16 @@ type Props$3 = {
|
|
|
54
54
|
placeholder?: string;
|
|
55
55
|
editable: boolean;
|
|
56
56
|
onFocus: (id: string) => void;
|
|
57
|
-
blockRefs: React.
|
|
58
|
-
hydratedBlocks: React.
|
|
57
|
+
blockRefs: React.RefObject<Map<string, HTMLSpanElement>>;
|
|
58
|
+
hydratedBlocks: React.RefObject<Set<string>>;
|
|
59
59
|
};
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
+
createBlockAfter,
|
|
9
|
+
duplicateBlockAfter,
|
|
9
10
|
deleteBlock,
|
|
10
|
-
duplicateBlock,
|
|
11
11
|
moveBlock
|
|
12
12
|
} from "@reiwuzen/blocky";
|
|
13
13
|
function createEditorStore(config = {}) {
|
|
@@ -23,8 +23,8 @@ function createEditorStore(config = {}) {
|
|
|
23
23
|
set({ blocks });
|
|
24
24
|
config.onChange?.(blocks);
|
|
25
25
|
},
|
|
26
|
-
|
|
27
|
-
const result =
|
|
26
|
+
createBlockAfter: (afterId, type = "paragraph") => {
|
|
27
|
+
const result = createBlockAfter(get().blocks, afterId, type);
|
|
28
28
|
if (!result.ok) return null;
|
|
29
29
|
const { blocks, newId } = result.value;
|
|
30
30
|
set({ blocks, activeBlockId: newId });
|
|
@@ -38,15 +38,10 @@ function createEditorStore(config = {}) {
|
|
|
38
38
|
return prevId;
|
|
39
39
|
},
|
|
40
40
|
duplicate: (id) => {
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (!result.ok) return;
|
|
46
|
-
const blocks = result.value.blocks.map(
|
|
47
|
-
(b) => b.id === result.value.newId ? dup : b
|
|
48
|
-
);
|
|
49
|
-
set({ blocks, activeBlockId: dup.id });
|
|
41
|
+
const res = duplicateBlockAfter(get().blocks, id);
|
|
42
|
+
if (res == null) return;
|
|
43
|
+
const blocks = [...res.value.blocks];
|
|
44
|
+
set({ blocks, activeBlockId: res.value.newFocusId });
|
|
50
45
|
config.onChange?.(blocks);
|
|
51
46
|
},
|
|
52
47
|
move: (id, direction) => {
|
|
@@ -64,7 +59,7 @@ function createEditorStore(config = {}) {
|
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
// src/components/blockList.tsx
|
|
67
|
-
import { useCallback
|
|
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
|
|
81
|
+
const createBlockAfter2 = useEditor((s) => s.createBlockAfter);
|
|
87
82
|
const removeBlock = useEditor((s) => s.removeBlock);
|
|
88
83
|
const duplicateBlock2 = useEditor((s) => s.duplicate);
|
|
89
84
|
const moveBlock2 = useEditor((s) => s.move);
|
|
@@ -91,7 +86,7 @@ function useEditorActions() {
|
|
|
91
86
|
return useMemo(() => ({
|
|
92
87
|
setBlocks,
|
|
93
88
|
updateBlock,
|
|
94
|
-
|
|
89
|
+
createBlockAfter: createBlockAfter2,
|
|
95
90
|
removeBlock,
|
|
96
91
|
duplicateBlock: duplicateBlock2,
|
|
97
92
|
moveBlock: moveBlock2,
|
|
@@ -99,7 +94,7 @@ function useEditorActions() {
|
|
|
99
94
|
}), [
|
|
100
95
|
setBlocks,
|
|
101
96
|
updateBlock,
|
|
102
|
-
|
|
97
|
+
createBlockAfter2,
|
|
103
98
|
removeBlock,
|
|
104
99
|
duplicateBlock2,
|
|
105
100
|
moveBlock2,
|
|
@@ -110,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
|
-
|
|
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
|
-
"
|
|
117
|
+
"div",
|
|
222
118
|
{
|
|
223
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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 === "
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (n.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (n.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
308
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
|
+
}
|
|
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 =
|
|
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
|
|
552
|
+
import { useCallback as useCallback2 } from "react";
|
|
640
553
|
import { flatToSelection as flatToSelection2 } from "@reiwuzen/blocky";
|
|
641
554
|
function useSelection(block) {
|
|
642
|
-
return
|
|
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,
|