@wingleeio/ori-react 0.0.3 → 0.1.0

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.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { isCollapsed, EditorController, createCanvasMeasurer } from '@wingleeio/ori-core';
2
2
  export { DEFAULT_TYPOGRAPHY, EditorController, applyUpdate, base64ToBytes, bytesToBase64, createCanvasMeasurer, createNoteDoc, docFromUpdate, encodeDoc, getBlocks, snapshotBlocks } from '@wingleeio/ori-core';
3
- import { createContext, forwardRef, useMemo, useRef, useState, useImperativeHandle, useLayoutEffect, useEffect, useSyncExternalStore, useContext } from 'react';
3
+ import { forwardRef, useRef, useState, useImperativeHandle, useEffect, useLayoutEffect, createContext, useSyncExternalStore, useContext } from 'react';
4
+ import { createRoot } from 'react-dom/client';
4
5
  import { jsx, jsxs } from 'react/jsx-runtime';
5
6
 
6
- // src/useEditor.ts
7
+ var __defProp = Object.defineProperty;
8
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
9
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
7
10
  function useEditor(options = {}) {
8
11
  const ref = useRef(null);
9
12
  if (ref.current === null) {
@@ -31,470 +34,555 @@ function useActiveMarks(editor) {
31
34
  void snapshot.revision;
32
35
  return editor.getActiveMarks();
33
36
  }
34
- var EMPTY = { blocks: {}, atoms: {} };
35
- var RenderersContext = createContext(EMPTY);
36
- var RenderersProvider = RenderersContext.Provider;
37
- var useRenderers = () => useContext(RenderersContext);
38
- function fragmentStyle(frag) {
39
- const f = frag.font;
40
- const decoration = [];
41
- if (frag.marks.underline) decoration.push("underline");
42
- if (frag.marks.strike) decoration.push("line-through");
43
- return {
44
- fontFamily: f.fontFamily,
45
- fontSize: f.fontSize,
46
- fontWeight: f.fontWeight,
47
- fontStyle: f.italic ? "italic" : "normal",
48
- letterSpacing: f.letterSpacing || void 0,
49
- textDecorationLine: decoration.length ? decoration.join(" ") : void 0
50
- };
37
+
38
+ // src/ce/dom.ts
39
+ function esc(s) {
40
+ return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(s) : s.replace(/["\\]/g, "\\$&");
51
41
  }
52
- function AtomFragment({
53
- editor,
54
- atom,
55
- render
56
- }) {
57
- return /* @__PURE__ */ jsx(
58
- "span",
59
- {
60
- className: "ori-atom",
61
- style: {
62
- display: "inline-block",
63
- width: atom.width,
64
- verticalAlign: "middle",
65
- whiteSpace: "normal"
66
- },
67
- children: render ? render({ editor, atom }) : null
68
- }
69
- );
42
+ function blockElOf(node, root) {
43
+ let n = node;
44
+ while (n && n !== root) {
45
+ if (n instanceof HTMLElement && n.dataset.blockId) return n;
46
+ n = n.parentNode;
47
+ }
48
+ return null;
70
49
  }
71
- function LineView({
72
- line,
73
- editor,
74
- atoms
75
- }) {
76
- return /* @__PURE__ */ jsx(
77
- "div",
78
- {
79
- className: "ori-line",
80
- style: { height: line.height, lineHeight: `${line.height}px`, whiteSpace: "pre" },
81
- children: line.fragments.map((frag) => {
82
- if (frag.atom) {
83
- return /* @__PURE__ */ jsx(
84
- AtomFragment,
85
- {
86
- editor,
87
- atom: frag.atom,
88
- render: atoms[frag.atom.type]
89
- },
90
- frag.start
91
- );
92
- }
93
- const className = ["ori-frag"];
94
- if (frag.marks.code) className.push("ori-frag-code");
95
- if (frag.marks.link) className.push("ori-frag-link");
96
- return /* @__PURE__ */ jsx(
97
- "span",
98
- {
99
- className: className.join(" "),
100
- style: fragmentStyle(frag),
101
- "data-start": frag.start,
102
- children: frag.text
103
- },
104
- frag.start
105
- );
106
- })
107
- }
108
- );
50
+ function spanOf(node) {
51
+ let n = node;
52
+ while (n) {
53
+ if (n instanceof HTMLElement && n.dataset.off != null) return n;
54
+ n = n.parentNode;
55
+ }
56
+ return null;
109
57
  }
110
- function BlockView({ editor, block }) {
111
- const { blocks, atoms } = useRenderers();
112
- const layout = editor.getLayout(block.id);
113
- if (!layout) return null;
114
- const custom = blocks[block.type];
115
- return /* @__PURE__ */ jsx(
116
- "div",
117
- {
118
- className: `ori-block ori-block-${block.type}`,
119
- "data-block-id": block.id,
120
- style: {
121
- position: "absolute",
122
- top: block.top,
123
- left: 0,
124
- width: "100%",
125
- height: block.height
126
- },
127
- children: custom ? custom({ editor, block, layout }) : layout.lines.map((line) => /* @__PURE__ */ jsx(LineView, { line, editor, atoms }, line.index))
128
- }
129
- );
58
+ function domToModel(root, node, offset) {
59
+ const blockEl = blockElOf(node, root);
60
+ if (!blockEl) return null;
61
+ const blockId = blockEl.dataset.blockId;
62
+ if (node && node.nodeType === Node.TEXT_NODE) {
63
+ const span = spanOf(node);
64
+ const base = span ? Number(span.dataset.off) : 0;
65
+ return { blockId, offset: base + offset };
66
+ }
67
+ const el = node;
68
+ if (el.dataset?.off != null) {
69
+ return { blockId, offset: Number(el.dataset.off) + (offset > 0 ? spanLen(el) : 0) };
70
+ }
71
+ const kids = Array.from(el.childNodes);
72
+ for (let i = offset; i < kids.length; i++) {
73
+ const k = kids[i];
74
+ if (k instanceof HTMLElement && k.dataset.off != null) return { blockId, offset: Number(k.dataset.off) };
75
+ }
76
+ let end = 0;
77
+ for (const k of kids) if (k instanceof HTMLElement && k.dataset.off != null) end = Math.max(end, Number(k.dataset.off) + spanLen(k));
78
+ return { blockId, offset: end };
130
79
  }
131
- function SelectionLayer({
132
- editor,
133
- snapshot
134
- }) {
135
- void snapshot.revision;
136
- const rects = editor.selectionRectsForViewport();
137
- if (rects.length === 0) return null;
138
- return /* @__PURE__ */ jsx("div", { className: "ori-selection-layer", "aria-hidden": true, children: rects.map((r, i) => /* @__PURE__ */ jsx(
139
- "div",
140
- {
141
- className: "ori-selection-rect",
142
- style: { position: "absolute", left: r.x, top: r.y, width: r.width, height: r.height }
143
- },
144
- `${r.blockId}:${i}`
145
- )) });
80
+ function spanLen(span) {
81
+ return span.dataset.len != null ? Number(span.dataset.len) : (span.textContent ?? "").length;
146
82
  }
147
- function CaretLayer({
148
- editor,
149
- snapshot,
150
- focused
151
- }) {
152
- const sel = snapshot.selection;
153
- if (!focused || !sel || !isCollapsed(sel)) return null;
154
- const rect = editor.caretRect();
155
- if (!rect) return null;
156
- return /* @__PURE__ */ jsx(
157
- "div",
158
- {
159
- className: "ori-caret",
160
- style: { position: "absolute", left: rect.x, top: rect.y, height: rect.height },
161
- "aria-hidden": true
83
+ function modelToDom(root, blockId, offset) {
84
+ const blockEl = root.querySelector(`[data-block-id="${esc(blockId)}"]`);
85
+ if (!blockEl) return null;
86
+ const spans = Array.from(blockEl.querySelectorAll("[data-off]"));
87
+ if (spans.length === 0) {
88
+ return { node: blockEl, offset: 0 };
89
+ }
90
+ for (const span of spans) {
91
+ const start = Number(span.dataset.off);
92
+ const len = spanLen(span);
93
+ if (offset <= start + len) {
94
+ if (span.dataset.atom != null) {
95
+ const idx = Array.prototype.indexOf.call(blockEl.childNodes, span);
96
+ return { node: blockEl, offset: offset <= start ? idx : idx + 1 };
97
+ }
98
+ const textNode2 = span.firstChild ?? span;
99
+ return { node: textNode2, offset: Math.max(0, Math.min(offset - start, (textNode2.textContent ?? "").length)) };
162
100
  }
163
- );
101
+ }
102
+ const last = spans[spans.length - 1];
103
+ const textNode = last.firstChild ?? last;
104
+ return { node: textNode, offset: (textNode.textContent ?? "").length };
164
105
  }
165
- function useCallbackRef(fn) {
166
- const ref = useRef(fn);
167
- ref.current = fn;
168
- return useMemo(() => (...args) => ref.current(...args), []);
106
+ function markClass(marks) {
107
+ const m = marks ?? {};
108
+ const cls = ["ori-frag"];
109
+ if (m.bold) cls.push("ori-m-bold");
110
+ if (m.italic) cls.push("ori-m-italic");
111
+ if (m.underline) cls.push("ori-m-underline");
112
+ if (m.strike) cls.push("ori-m-strike");
113
+ if (m.code) cls.push("ori-frag-code");
114
+ if (m.link) cls.push("ori-frag-link");
115
+ return cls.join(" ");
116
+ }
117
+ function buildRun(item) {
118
+ const span = document.createElement("span");
119
+ span.className = markClass(item.marks);
120
+ span.dataset.off = String(item.start);
121
+ span.dataset.len = String(item.text.length);
122
+ span.textContent = item.text;
123
+ return span;
169
124
  }
170
125
 
171
- // src/keymap.ts
172
- function pasteText(editor, text) {
173
- const parts = text.replace(/\r\n?/g, "\n").split("\n");
174
- editor.insertText(parts[0]);
175
- for (let i = 1; i < parts.length; i += 1) {
176
- editor.insertParagraphBreak();
177
- if (parts[i]) editor.insertText(parts[i]);
126
+ // src/ce/view.ts
127
+ var PLACEHOLDER = "\uFFFC";
128
+ var EditorView = class {
129
+ constructor(root, editor, opts) {
130
+ __publicField(this, "root", root);
131
+ __publicField(this, "editor", editor);
132
+ __publicField(this, "opts", opts);
133
+ __publicField(this, "roots", /* @__PURE__ */ new Map());
134
+ __publicField(this, "composing", false);
135
+ __publicField(this, "applyingModel", false);
136
+ __publicField(this, "detachers", []);
137
+ /** The model revision the DOM currently reflects (so external changes — remote
138
+ * edits, app commands — re-render, but our own edits don't clobber the caret). */
139
+ __publicField(this, "lastRevision", -1);
140
+ root.setAttribute("contenteditable", opts.readOnly ? "false" : "true");
141
+ root.setAttribute("spellcheck", opts.readOnly ? "false" : "true");
142
+ root.setAttribute("role", "textbox");
143
+ root.setAttribute("aria-multiline", "true");
144
+ this.renderBlocks();
145
+ this.lastRevision = this.rev();
146
+ const on = (t, h, o) => {
147
+ root.addEventListener(t, h, o);
148
+ this.detachers.push(() => root.removeEventListener(t, h, o));
149
+ };
150
+ on("beforeinput", (e) => this.onBeforeInput(e));
151
+ on("input", () => this.onInput());
152
+ on("keydown", (e) => this.onKeyDown(e));
153
+ on("compositionstart", () => this.composing = true);
154
+ on("compositionend", () => {
155
+ this.composing = false;
156
+ this.onInput();
157
+ });
158
+ const onSelChange = () => {
159
+ if (this.applyingModel || this.composing) return;
160
+ const sel = this.readSelection();
161
+ if (!sel) return;
162
+ this.editor.setSelection(sel);
163
+ this.lastRevision = this.rev();
164
+ };
165
+ document.addEventListener("selectionchange", onSelChange);
166
+ this.detachers.push(() => document.removeEventListener("selectionchange", onSelChange));
178
167
  }
179
- }
180
- function handleKeyDown(editor, e, opts = {}) {
181
- if (e.nativeEvent.isComposing) return false;
182
- if (e.altKey) return false;
183
- const mod = e.metaKey || e.ctrlKey;
184
- const shift = e.shiftKey;
185
- const ro = !!opts.readOnly;
186
- if (mod) {
187
- switch (e.key.toLowerCase()) {
188
- case "b":
189
- e.preventDefault();
190
- if (!ro) editor.toggleMark("bold");
191
- return true;
192
- case "i":
193
- e.preventDefault();
194
- if (!ro) editor.toggleMark("italic");
195
- return true;
196
- case "u":
197
- e.preventDefault();
198
- if (!ro) editor.toggleMark("underline");
199
- return true;
200
- case "e":
201
- e.preventDefault();
202
- if (!ro) editor.toggleMark("code");
203
- return true;
204
- case "a":
205
- e.preventDefault();
206
- editor.selectAll();
207
- return true;
208
- case "z":
209
- e.preventDefault();
210
- if (!ro) shift ? editor.redo() : editor.undo();
211
- return true;
212
- case "y":
213
- e.preventDefault();
214
- if (!ro) editor.redo();
215
- return true;
216
- case "arrowleft":
217
- e.preventDefault();
218
- editor.moveCaret("lineStart", shift);
219
- return true;
220
- case "arrowright":
221
- e.preventDefault();
222
- editor.moveCaret("lineEnd", shift);
223
- return true;
224
- // Let the browser raise copy/cut/paste on the textarea.
225
- case "c":
226
- case "x":
227
- case "v":
228
- return false;
229
- default:
230
- return false;
168
+ destroy() {
169
+ this.detachers.forEach((d) => d());
170
+ this.roots.forEach((r) => r.unmount());
171
+ this.roots.clear();
172
+ }
173
+ focus() {
174
+ this.root.focus();
175
+ }
176
+ // --- rendering ---------------------------------------------------------
177
+ rev() {
178
+ return this.editor.getSnapshot().revision;
179
+ }
180
+ /**
181
+ * Called by React on every model change. Only re-renders when the model moved
182
+ * ahead of what we last drew (an *external* change — app command, undo, remote);
183
+ * our own edits already updated the DOM and must not be clobbered.
184
+ */
185
+ sync() {
186
+ const rev = this.rev();
187
+ if (rev === this.lastRevision) return;
188
+ const changed = this.renderBlocks();
189
+ if (changed) this.writeSelection();
190
+ this.lastRevision = rev;
191
+ }
192
+ /** After a controlled (preventDefault'd) edit: re-render + restore the caret. */
193
+ commit() {
194
+ this.renderBlocks();
195
+ this.writeSelection();
196
+ this.lastRevision = this.rev();
197
+ }
198
+ /** A content signature for a block, so unchanged blocks aren't re-rendered. */
199
+ sig(id) {
200
+ return this.editor.getBlockType(id) + "|" + JSON.stringify(this.editor.getInline(id));
201
+ }
202
+ /**
203
+ * Reconcile the DOM to the *visible window* of blocks (virtualization): a top
204
+ * spacer, the windowed blocks, then a bottom spacer — heights from the
205
+ * controller's offscreen measurement. On-screen blocks are reused by id so a
206
+ * caret inside one survives a scroll. Returns true if the DOM was mutated.
207
+ */
208
+ renderBlocks() {
209
+ let changed = false;
210
+ const snap = this.editor.getSnapshot();
211
+ const vis = snap.visible;
212
+ const topH = vis.length ? vis[0].top : 0;
213
+ const botH = vis.length ? Math.max(0, snap.totalHeight - (vis[vis.length - 1].top + vis[vis.length - 1].height)) : Math.max(0, snap.totalHeight);
214
+ const TOP = "\0top";
215
+ const BOTTOM = "\0bottom";
216
+ const want = [TOP, ...vis.map((v) => v.id), BOTTOM];
217
+ const keyOf = (el) => {
218
+ const e = el;
219
+ return e.dataset.spacer ? "\0" + e.dataset.spacer : e.dataset.blockId ?? "";
220
+ };
221
+ const have = /* @__PURE__ */ new Map();
222
+ for (const c of Array.from(this.root.children)) have.set(keyOf(c), c);
223
+ let prev = null;
224
+ for (const k of want) {
225
+ let el = have.get(k);
226
+ if (el) {
227
+ have.delete(k);
228
+ } else {
229
+ el = k === TOP || k === BOTTOM ? this.makeSpacer(k.slice(1)) : this.makeBlock(k);
230
+ changed = true;
231
+ }
232
+ const anchor = prev ? prev.nextSibling : this.root.firstChild;
233
+ if (anchor !== el) {
234
+ this.root.insertBefore(el, anchor);
235
+ changed = true;
236
+ }
237
+ prev = el;
231
238
  }
239
+ for (const el of have.values()) {
240
+ if (el.dataset.blockId) this.unmountRootsIn(el);
241
+ el.remove();
242
+ changed = true;
243
+ }
244
+ const top = this.root.firstElementChild;
245
+ if (top && top.style.height !== `${topH}px`) top.style.height = `${topH}px`;
246
+ const bot = this.root.lastElementChild;
247
+ if (bot && bot.style.height !== `${botH}px`) bot.style.height = `${botH}px`;
248
+ for (const vb of vis) {
249
+ const el = this.root.querySelector(`[data-block-id="${esc(vb.id)}"]`);
250
+ if (!el) continue;
251
+ const sig = this.sig(vb.id);
252
+ if (el.dataset.sig !== sig) {
253
+ el.dataset.sig = sig;
254
+ this.renderBlockInner(el, vb.id);
255
+ changed = true;
256
+ }
257
+ }
258
+ return changed;
232
259
  }
233
- switch (e.key) {
234
- case "Backspace":
235
- e.preventDefault();
236
- if (!ro) editor.deleteBackward();
237
- return true;
238
- case "Delete":
239
- e.preventDefault();
240
- if (!ro) editor.deleteForward();
241
- return true;
242
- case "Enter":
260
+ makeBlock(id) {
261
+ const el = document.createElement("div");
262
+ el.dataset.blockId = id;
263
+ return el;
264
+ }
265
+ makeSpacer(which) {
266
+ const el = document.createElement("div");
267
+ el.dataset.spacer = which;
268
+ el.setAttribute("contenteditable", "false");
269
+ el.setAttribute("aria-hidden", "true");
270
+ el.style.userSelect = "none";
271
+ el.style.pointerEvents = "none";
272
+ return el;
273
+ }
274
+ renderBlockInner(el, id) {
275
+ this.unmountRootsIn(el);
276
+ const type = this.editor.getBlockType(id);
277
+ el.className = `ori-block ori-block-${type}`;
278
+ const blockRenderer = this.opts.renderBlock(type);
279
+ if (blockRenderer) {
280
+ el.contentEditable = "false";
281
+ el.textContent = "";
282
+ const root = createRoot(el);
283
+ root.render(blockRenderer({ editor: this.editor, block: { id, type, index: 0, top: 0, height: 0 }, layout: this.editor.getLayout(id) }));
284
+ this.roots.set(el, root);
285
+ return;
286
+ }
287
+ el.contentEditable = "inherit";
288
+ el.textContent = "";
289
+ const items = this.editor.getInline(id);
290
+ if (items.length === 0) {
291
+ el.appendChild(document.createElement("br"));
292
+ return;
293
+ }
294
+ for (const item of items) {
295
+ if (item.atom) {
296
+ const span = document.createElement("span");
297
+ span.className = "ori-atom";
298
+ span.contentEditable = "false";
299
+ span.dataset.atom = "true";
300
+ span.dataset.off = String(item.start);
301
+ span.dataset.len = "1";
302
+ el.appendChild(span);
303
+ const renderer = this.opts.renderAtom(item.atom.type);
304
+ if (renderer) {
305
+ const r = createRoot(span);
306
+ r.render(renderer({ editor: this.editor, atom: item.atom }));
307
+ this.roots.set(span, r);
308
+ }
309
+ } else {
310
+ el.appendChild(buildRun(item));
311
+ }
312
+ }
313
+ }
314
+ unmountRootsIn(el) {
315
+ for (const [node, root] of this.roots) {
316
+ if (el === node || el.contains(node)) {
317
+ root.unmount();
318
+ this.roots.delete(node);
319
+ }
320
+ }
321
+ }
322
+ // --- selection ---------------------------------------------------------
323
+ readSelection() {
324
+ const s = window.getSelection();
325
+ if (!s || s.rangeCount === 0 || !this.root.contains(s.anchorNode)) return null;
326
+ const a = domToModel(this.root, s.anchorNode, s.anchorOffset);
327
+ const f = domToModel(this.root, s.focusNode, s.focusOffset);
328
+ if (!a || !f) return null;
329
+ return { anchor: { blockId: a.blockId, offset: a.offset }, focus: { blockId: f.blockId, offset: f.offset } };
330
+ }
331
+ /** Push the controller's selection back into the DOM (after a model op). */
332
+ writeSelection() {
333
+ const sel = this.editor.getSelection();
334
+ if (!sel) return;
335
+ const a = modelToDom(this.root, sel.anchor.blockId, sel.anchor.offset);
336
+ const f = modelToDom(this.root, sel.focus.blockId, sel.focus.offset);
337
+ if (!a || !f) return;
338
+ const r = document.createRange();
339
+ const s = window.getSelection();
340
+ if (!s) return;
341
+ this.applyingModel = true;
342
+ try {
343
+ r.setStart(a.node, a.offset);
344
+ s.removeAllRanges();
345
+ s.addRange(r);
346
+ s.extend(f.node, f.offset);
347
+ } catch {
348
+ } finally {
349
+ this.applyingModel = false;
350
+ }
351
+ }
352
+ /** The block text as the model sees it (atoms collapse to one placeholder). */
353
+ domBlockText(el) {
354
+ let out = "";
355
+ for (const child of Array.from(el.childNodes)) {
356
+ if (child instanceof HTMLElement && child.dataset.atom != null) {
357
+ out += PLACEHOLDER;
358
+ } else {
359
+ out += child.textContent ?? "";
360
+ }
361
+ }
362
+ return out;
363
+ }
364
+ // --- input -------------------------------------------------------------
365
+ /** Formatting + history shortcuts (the browser fires these as keydown). */
366
+ onKeyDown(e) {
367
+ if (this.opts.readOnly) return;
368
+ const mod = e.metaKey || e.ctrlKey;
369
+ if (!mod || e.altKey) return;
370
+ const k = e.key.toLowerCase();
371
+ const mark = { b: "bold", i: "italic", u: "underline", e: "code" }[k];
372
+ if (mark) {
243
373
  e.preventDefault();
244
- if (!ro) editor.insertParagraphBreak();
245
- return true;
246
- case "Tab":
374
+ const sel = this.readSelection();
375
+ if (sel) this.editor.setSelection(sel);
376
+ this.editor.toggleMark(mark);
377
+ this.commit();
378
+ } else if (k === "z") {
247
379
  e.preventDefault();
248
- if (!ro) editor.insertText(" ");
249
- return true;
250
- case "ArrowLeft":
380
+ if (e.shiftKey) this.editor.redo();
381
+ else this.editor.undo();
382
+ this.commit();
383
+ } else if (k === "y") {
251
384
  e.preventDefault();
252
- editor.moveCaret("left", shift);
253
- return true;
254
- case "ArrowRight":
385
+ this.editor.redo();
386
+ this.commit();
387
+ }
388
+ }
389
+ onBeforeInput(e) {
390
+ if (this.opts.readOnly) {
255
391
  e.preventDefault();
256
- editor.moveCaret("right", shift);
257
- return true;
258
- case "ArrowUp":
392
+ return;
393
+ }
394
+ const sel = this.readSelection();
395
+ if (!sel) return;
396
+ this.editor.setSelection(sel);
397
+ const collapsed = isCollapsed(sel);
398
+ const startOffset = this.editor.orderedSelection()?.start.offset ?? sel.focus.offset;
399
+ const t = e.inputType;
400
+ if (collapsed && (t === "insertText" || t === "insertCompositionText" || t === "insertReplacementText")) return;
401
+ if (collapsed && t === "deleteContentForward") return;
402
+ if (collapsed && t === "deleteContentBackward" && startOffset > 0) return;
403
+ const ed = this.editor;
404
+ if (t === "insertParagraph") {
259
405
  e.preventDefault();
260
- editor.moveCaret("up", shift);
261
- return true;
262
- case "ArrowDown":
406
+ ed.insertParagraphBreak();
407
+ } else if (t.startsWith("delete")) {
263
408
  e.preventDefault();
264
- editor.moveCaret("down", shift);
265
- return true;
266
- case "Home":
409
+ if (t === "deleteContentForward") ed.deleteForward();
410
+ else ed.deleteBackward();
411
+ } else if (t === "insertText" || t === "insertReplacementText" || t === "insertFromPaste") {
267
412
  e.preventDefault();
268
- editor.moveCaret("lineStart", shift);
269
- return true;
270
- case "End":
413
+ const text = e.data ?? e.dataTransfer?.getData("text/plain") ?? "";
414
+ if (text) ed.insertText(text);
415
+ } else if (t === "insertLineBreak") {
271
416
  e.preventDefault();
272
- editor.moveCaret("lineEnd", shift);
273
- return true;
274
- default:
275
- return false;
417
+ ed.insertText("\n");
418
+ } else {
419
+ return;
420
+ }
421
+ this.commit();
422
+ }
423
+ onInput() {
424
+ if (this.composing || this.opts.readOnly) return;
425
+ const blockEl = blockElOf(window.getSelection()?.anchorNode ?? null, this.root);
426
+ if (!blockEl) {
427
+ this.renderBlocks();
428
+ this.lastRevision = this.rev();
429
+ return;
430
+ }
431
+ const id = blockEl.dataset.blockId;
432
+ const next = this.domBlockText(blockEl);
433
+ const cur = this.editor.getBlockText(id);
434
+ if (next === cur) return;
435
+ const max = Math.min(cur.length, next.length);
436
+ let p = 0;
437
+ while (p < max && cur[p] === next[p]) p++;
438
+ let s = 0;
439
+ while (s < max - p && cur[cur.length - 1 - s] === next[next.length - 1 - s]) s++;
440
+ const from = p;
441
+ const to = cur.length - s;
442
+ const insert = next.slice(p, next.length - s);
443
+ this.editor.setSelection({ anchor: { blockId: id, offset: from }, focus: { blockId: id, offset: to } });
444
+ if (to > from) this.editor.deleteBackward();
445
+ if (insert) this.editor.insertText(insert);
446
+ this.reindex(blockEl);
447
+ this.lastRevision = this.rev();
448
+ }
449
+ /** Re-derive data-off / data-len after a native edit (no node replacement). */
450
+ reindex(el) {
451
+ let off = 0;
452
+ for (const child of Array.from(el.children)) {
453
+ if (child.dataset.off == null) continue;
454
+ child.dataset.off = String(off);
455
+ const len = child.dataset.atom != null ? 1 : (child.textContent ?? "").length;
456
+ child.dataset.len = String(len);
457
+ off += len;
458
+ }
276
459
  }
460
+ };
461
+ function caretClientRect() {
462
+ const s = window.getSelection();
463
+ if (!s || s.rangeCount === 0) return null;
464
+ const r = s.getRangeAt(0).cloneRange();
465
+ r.collapse(s.focusNode === r.endContainer && s.focusOffset === r.endOffset ? false : true);
466
+ const rects = r.getClientRects();
467
+ if (rects.length) return rects[rects.length - 1];
468
+ const b = r.getBoundingClientRect();
469
+ return b.height || b.width ? b : null;
277
470
  }
278
- var NoteEditor = forwardRef(function NoteEditor2({
279
- editor,
280
- className,
281
- style,
282
- maxWidth = 720,
283
- placeholder = "Start writing\u2026",
284
- autoFocus,
285
- readOnly,
286
- blockRenderers,
287
- atomRenderers
288
- }, ref) {
471
+ var NoteEditor = forwardRef(function NoteEditor2({ editor, className, style, maxWidth = 720, placeholder, autoFocus, readOnly, blockRenderers, atomRenderers }, ref) {
289
472
  const snapshot = useEditorSnapshot(editor);
290
- const renderers = useMemo(
291
- () => ({ blocks: blockRenderers ?? {}, atoms: atomRenderers ?? {} }),
292
- [blockRenderers, atomRenderers]
293
- );
294
473
  const scrollerRef = useRef(null);
295
474
  const contentRef = useRef(null);
296
- const inputRef = useRef(null);
297
- const draggingRef = useRef(false);
298
- const composingRef = useRef(false);
475
+ const viewRef = useRef(null);
299
476
  const [focused, setFocused] = useState(false);
477
+ const [caret, setCaret] = useState(null);
478
+ const renderersRef = useRef({ blockRenderers, atomRenderers });
479
+ renderersRef.current = { blockRenderers, atomRenderers };
300
480
  useImperativeHandle(
301
481
  ref,
302
482
  () => ({
303
- focus: () => inputRef.current?.focus(),
483
+ focus: () => contentRef.current?.focus(),
304
484
  getCaretRect: () => {
305
- const c = editor.caretRect();
306
- const content = contentRef.current;
307
- if (!c || !content) return null;
308
- const r = content.getBoundingClientRect();
309
- return { x: r.left + c.x, y: r.top + c.y, height: c.height };
485
+ const r = caretClientRect();
486
+ return r ? { x: r.left, y: r.top, height: r.height || 16 } : null;
310
487
  },
311
488
  getSelectionRect: () => {
312
- const content = contentRef.current;
313
- if (!content) return null;
314
- const rects = editor.selectionRectsForViewport();
315
- if (rects.length === 0) return null;
316
- const r = content.getBoundingClientRect();
317
- let top = Infinity;
318
- let left = Infinity;
319
- let bottom = -Infinity;
320
- let right = -Infinity;
321
- for (const rc of rects) {
322
- top = Math.min(top, r.top + rc.y);
323
- left = Math.min(left, r.left + rc.x);
324
- bottom = Math.max(bottom, r.top + rc.y + rc.height);
325
- right = Math.max(right, r.left + rc.x + rc.width);
326
- }
327
- return { top, left, right, bottom, width: right - left, height: bottom - top };
489
+ const s = window.getSelection();
490
+ if (!s || s.rangeCount === 0 || s.isCollapsed) return null;
491
+ const b = s.getRangeAt(0).getBoundingClientRect();
492
+ if (!b.width && !b.height) return null;
493
+ return { top: b.top, left: b.left, right: b.right, bottom: b.bottom, width: b.width, height: b.height };
328
494
  },
329
495
  getScrollElement: () => scrollerRef.current
330
496
  }),
331
- [editor]
497
+ []
332
498
  );
499
+ useEffect(() => {
500
+ const el = contentRef.current;
501
+ if (!el) return;
502
+ const view = new EditorView(el, editor, {
503
+ readOnly,
504
+ renderAtom: (t) => renderersRef.current.atomRenderers?.[t],
505
+ renderBlock: (t) => renderersRef.current.blockRenderers?.[t]
506
+ });
507
+ viewRef.current = view;
508
+ return () => {
509
+ view.destroy();
510
+ viewRef.current = null;
511
+ };
512
+ }, [editor, readOnly]);
513
+ useEffect(() => {
514
+ viewRef.current?.sync();
515
+ }, [snapshot.revision]);
516
+ useEffect(() => {
517
+ if (autoFocus) contentRef.current?.focus();
518
+ }, [autoFocus]);
519
+ useEffect(() => {
520
+ const update = () => {
521
+ const content = contentRef.current;
522
+ const s = window.getSelection();
523
+ if (!content || !s || s.rangeCount === 0 || !s.isCollapsed || !content.contains(s.anchorNode)) {
524
+ setCaret(null);
525
+ return;
526
+ }
527
+ const r = caretClientRect();
528
+ const box = content.getBoundingClientRect();
529
+ if (r) setCaret({ x: r.left - box.left, y: r.top - box.top, h: r.height || 18 });
530
+ };
531
+ document.addEventListener("selectionchange", update);
532
+ const ro = new ResizeObserver(update);
533
+ if (contentRef.current) ro.observe(contentRef.current);
534
+ update();
535
+ return () => {
536
+ document.removeEventListener("selectionchange", update);
537
+ ro.disconnect();
538
+ };
539
+ }, []);
333
540
  useLayoutEffect(() => {
334
- const scroller = scrollerRef.current;
541
+ const sc = scrollerRef.current;
335
542
  const content = contentRef.current;
336
- if (!scroller || !content) return;
543
+ if (!sc || !content) return;
337
544
  const sync = () => {
338
545
  editor.setWidth(content.clientWidth);
339
- editor.setViewport(scroller.scrollTop, scroller.clientHeight);
546
+ editor.setViewport(sc.scrollTop, sc.clientHeight);
340
547
  };
341
548
  sync();
342
549
  const ro = new ResizeObserver(sync);
343
- ro.observe(scroller);
550
+ ro.observe(sc);
344
551
  ro.observe(content);
345
552
  return () => ro.disconnect();
346
553
  }, [editor]);
347
- useEffect(() => {
348
- const fonts = document.fonts;
349
- if (fonts?.ready) void fonts.ready.then(() => editor.invalidateMeasurements());
350
- }, [editor]);
351
- useEffect(() => {
352
- if (autoFocus) inputRef.current?.focus();
353
- }, [autoFocus]);
354
- const pointToPosition = useCallbackRef((clientX, clientY) => {
355
- const content = contentRef.current;
356
- if (!content) return null;
357
- const rect = content.getBoundingClientRect();
358
- return editor.positionFromPoint(clientX - rect.left, clientY - rect.top);
359
- });
360
- useEffect(() => {
361
- const onMove = (e) => {
362
- if (!draggingRef.current) return;
363
- const pos = pointToPosition(e.clientX, e.clientY);
364
- const sel = editor.getSelection();
365
- if (pos && sel) editor.setSelection({ anchor: sel.anchor, focus: pos });
366
- };
367
- const onUp = () => {
368
- draggingRef.current = false;
369
- };
370
- window.addEventListener("mousemove", onMove);
371
- window.addEventListener("mouseup", onUp);
372
- return () => {
373
- window.removeEventListener("mousemove", onMove);
374
- window.removeEventListener("mouseup", onUp);
375
- };
376
- }, [editor, pointToPosition]);
377
554
  const onScroll = () => {
378
- const scroller = scrollerRef.current;
379
- if (scroller) editor.setViewport(scroller.scrollTop, scroller.clientHeight);
380
- };
381
- const onMouseDown = (e) => {
382
- if (e.button !== 0) return;
383
- const scroller = scrollerRef.current;
384
- if (scroller && e.clientX - scroller.getBoundingClientRect().left >= scroller.clientWidth) {
385
- return;
386
- }
387
- inputRef.current?.focus();
388
- const pos = pointToPosition(e.clientX, e.clientY);
389
- if (!pos) return;
390
- e.preventDefault();
391
- const sel = editor.getSelection();
392
- if (e.shiftKey && sel) {
393
- editor.setSelection({ anchor: sel.anchor, focus: pos });
394
- } else {
395
- editor.collapse(pos);
396
- }
397
- draggingRef.current = true;
398
- };
399
- const onKeyDown = (e) => {
400
- handleKeyDown(editor, e, { readOnly });
555
+ const sc = scrollerRef.current;
556
+ if (sc) editor.setViewport(sc.scrollTop, sc.clientHeight);
401
557
  };
402
- const commitInput = () => {
403
- const el = inputRef.current;
404
- if (!el) return;
405
- const value = el.value;
406
- if (value) {
407
- if (!readOnly) editor.insertText(value);
408
- el.value = "";
409
- }
410
- };
411
- const onInput = (e) => {
412
- if (composingRef.current || e.nativeEvent.isComposing) return;
413
- commitInput();
414
- };
415
- const onCompositionStart = () => {
416
- composingRef.current = true;
417
- };
418
- const onCompositionEnd = (e) => {
419
- composingRef.current = false;
420
- const el = inputRef.current;
421
- if (el) {
422
- if (e.data && !readOnly) editor.insertText(e.data);
423
- el.value = "";
424
- }
425
- };
426
- const caret = editor.caretRect();
427
- const inputStyle = {
428
- position: "absolute",
429
- left: caret ? caret.x : 0,
430
- top: caret ? caret.y : 0,
431
- width: 1,
432
- height: caret ? caret.height : 16,
433
- opacity: 0,
434
- padding: 0,
435
- border: 0,
436
- outline: "none",
437
- resize: "none",
438
- background: "transparent",
439
- caretColor: "transparent",
440
- color: "transparent",
441
- overflow: "hidden",
442
- whiteSpace: "pre",
443
- zIndex: 1
444
- };
445
- return /* @__PURE__ */ jsx(RenderersProvider, { value: renderers, children: /* @__PURE__ */ jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsx("div", { className: "ori-scroller", ref: scrollerRef, onScroll, onMouseDown, children: /* @__PURE__ */ jsx(
446
- "div",
447
- {
448
- className: "ori-content",
449
- ref: contentRef,
450
- style: { maxWidth, marginInline: "auto", position: "relative" },
451
- children: /* @__PURE__ */ jsxs(
452
- "div",
453
- {
454
- className: "ori-canvas",
455
- style: { position: "relative", width: "100%", height: snapshot.totalHeight },
456
- children: [
457
- /* @__PURE__ */ jsx(SelectionLayer, { editor, snapshot }),
458
- snapshot.visible.map((block) => /* @__PURE__ */ jsx(BlockView, { editor, block }, block.id)),
459
- /* @__PURE__ */ jsx(CaretLayer, { editor, snapshot, focused }),
460
- snapshot.empty && placeholder ? /* @__PURE__ */ jsx("div", { className: "ori-placeholder", "aria-hidden": true, children: placeholder }) : null,
461
- !readOnly ? /* @__PURE__ */ jsx(
462
- "textarea",
463
- {
464
- ref: inputRef,
465
- className: "ori-input",
466
- style: inputStyle,
467
- spellCheck: false,
468
- autoCapitalize: "off",
469
- autoCorrect: "off",
470
- onKeyDown,
471
- onInput,
472
- onCompositionStart,
473
- onCompositionEnd,
474
- onFocus: () => setFocused(true),
475
- onBlur: () => setFocused(false),
476
- onCopy: (e) => {
477
- e.preventDefault();
478
- e.clipboardData.setData("text/plain", editor.getSelectedText());
479
- },
480
- onCut: (e) => {
481
- e.preventDefault();
482
- e.clipboardData.setData("text/plain", editor.getSelectedText());
483
- editor.deleteBackward();
484
- },
485
- onPaste: (e) => {
486
- e.preventDefault();
487
- pasteText(editor, e.clipboardData.getData("text/plain"));
488
- }
489
- }
490
- ) : null
491
- ]
492
- }
493
- )
494
- }
495
- ) }) }) });
558
+ const showCaret = focused && !!caret && !readOnly;
559
+ return /* @__PURE__ */ jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsx("div", { className: "ori-scroller", ref: scrollerRef, onScroll, children: /* @__PURE__ */ jsxs("div", { className: "ori-content", style: { maxWidth, marginInline: "auto", position: "relative" }, children: [
560
+ /* @__PURE__ */ jsx(
561
+ "div",
562
+ {
563
+ className: "ori-canvas ori-ce",
564
+ ref: contentRef,
565
+ onFocus: () => setFocused(true),
566
+ onBlur: () => setFocused(false),
567
+ suppressContentEditableWarning: true
568
+ }
569
+ ),
570
+ showCaret && caret ? /* @__PURE__ */ jsx(
571
+ "div",
572
+ {
573
+ className: "ori-caret",
574
+ style: { position: "absolute", left: caret.x, top: caret.y, height: caret.h, pointerEvents: "none" },
575
+ "aria-hidden": true
576
+ }
577
+ ) : null,
578
+ snapshot.empty && placeholder ? /* @__PURE__ */ jsx("div", { className: "ori-placeholder", "aria-hidden": true, children: placeholder }) : null
579
+ ] }) }) });
496
580
  });
581
+ var EMPTY = { blocks: {}, atoms: {} };
582
+ var RenderersContext = createContext(EMPTY);
583
+ RenderersContext.Provider;
584
+ var useRenderers = () => useContext(RenderersContext);
497
585
 
498
- export { BlockView, CaretLayer, NoteEditor, SelectionLayer, handleKeyDown, pasteText, useActiveMarks, useEditor, useEditorSnapshot, useRenderers };
586
+ export { NoteEditor, useActiveMarks, useEditor, useEditorSnapshot, useRenderers };
499
587
  //# sourceMappingURL=index.js.map
500
588
  //# sourceMappingURL=index.js.map