@wingleeio/ori-react 0.0.4 → 0.1.1
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 +537 -638
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -41
- package/dist/index.d.ts +6 -41
- package/dist/index.js +539 -635
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/styles.css +63 -22
package/dist/index.cjs
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
var oriCore = require('@wingleeio/ori-core');
|
|
4
4
|
var react = require('react');
|
|
5
|
+
var client = require('react-dom/client');
|
|
5
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
10
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
8
11
|
function useEditor(options = {}) {
|
|
9
12
|
const ref = react.useRef(null);
|
|
10
13
|
if (ref.current === null) {
|
|
@@ -32,690 +35,591 @@ function useActiveMarks(editor) {
|
|
|
32
35
|
void snapshot.revision;
|
|
33
36
|
return editor.getActiveMarks();
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
function fragmentStyle(frag) {
|
|
40
|
-
const f = frag.font;
|
|
41
|
-
const decoration = [];
|
|
42
|
-
if (frag.marks.underline) decoration.push("underline");
|
|
43
|
-
if (frag.marks.strike) decoration.push("line-through");
|
|
44
|
-
return {
|
|
45
|
-
fontFamily: f.fontFamily,
|
|
46
|
-
fontSize: f.fontSize,
|
|
47
|
-
fontWeight: f.fontWeight,
|
|
48
|
-
fontStyle: f.italic ? "italic" : "normal",
|
|
49
|
-
letterSpacing: f.letterSpacing || void 0,
|
|
50
|
-
textDecorationLine: decoration.length ? decoration.join(" ") : void 0
|
|
51
|
-
};
|
|
38
|
+
|
|
39
|
+
// src/ce/dom.ts
|
|
40
|
+
function esc(s) {
|
|
41
|
+
return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(s) : s.replace(/["\\]/g, "\\$&");
|
|
52
42
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
{
|
|
61
|
-
className: "ori-atom",
|
|
62
|
-
style: {
|
|
63
|
-
display: "inline-block",
|
|
64
|
-
width: atom.width,
|
|
65
|
-
verticalAlign: "middle",
|
|
66
|
-
whiteSpace: "normal"
|
|
67
|
-
},
|
|
68
|
-
children: render ? render({ editor, atom }) : null
|
|
69
|
-
}
|
|
70
|
-
);
|
|
43
|
+
function blockElOf(node, root) {
|
|
44
|
+
let n = node;
|
|
45
|
+
while (n && n !== root) {
|
|
46
|
+
if (n instanceof HTMLElement && n.dataset.blockId) return n;
|
|
47
|
+
n = n.parentNode;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
71
50
|
}
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
className: "ori-line",
|
|
81
|
-
style: { height: line.height, lineHeight: `${line.height}px`, whiteSpace: "pre" },
|
|
82
|
-
children: line.fragments.map((frag) => {
|
|
83
|
-
if (frag.atom) {
|
|
84
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
85
|
-
AtomFragment,
|
|
86
|
-
{
|
|
87
|
-
editor,
|
|
88
|
-
atom: frag.atom,
|
|
89
|
-
render: atoms[frag.atom.type]
|
|
90
|
-
},
|
|
91
|
-
frag.start
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
const className = ["ori-frag"];
|
|
95
|
-
if (frag.marks.code) className.push("ori-frag-code");
|
|
96
|
-
if (frag.marks.link) className.push("ori-frag-link");
|
|
97
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
98
|
-
"span",
|
|
99
|
-
{
|
|
100
|
-
className: className.join(" "),
|
|
101
|
-
style: fragmentStyle(frag),
|
|
102
|
-
"data-start": frag.start,
|
|
103
|
-
children: frag.text
|
|
104
|
-
},
|
|
105
|
-
frag.start
|
|
106
|
-
);
|
|
107
|
-
})
|
|
108
|
-
}
|
|
109
|
-
);
|
|
51
|
+
function spanOf(node) {
|
|
52
|
+
let n = node;
|
|
53
|
+
while (n) {
|
|
54
|
+
if (n instanceof HTMLElement && n.dataset.off != null) return n;
|
|
55
|
+
n = n.parentNode;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
110
58
|
}
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
59
|
+
function domToModel(root, node, offset) {
|
|
60
|
+
const blockEl = blockElOf(node, root);
|
|
61
|
+
if (!blockEl) return null;
|
|
62
|
+
const blockId = blockEl.dataset.blockId;
|
|
63
|
+
if (node && node.nodeType === Node.TEXT_NODE) {
|
|
64
|
+
const span = spanOf(node);
|
|
65
|
+
const base = span ? Number(span.dataset.off) : 0;
|
|
66
|
+
return { blockId, offset: base + offset };
|
|
67
|
+
}
|
|
68
|
+
const el = node;
|
|
69
|
+
if (el.dataset?.off != null) {
|
|
70
|
+
return { blockId, offset: Number(el.dataset.off) + (offset > 0 ? spanLen(el) : 0) };
|
|
71
|
+
}
|
|
72
|
+
const kids = Array.from(el.childNodes);
|
|
73
|
+
for (let i = offset; i < kids.length; i++) {
|
|
74
|
+
const k = kids[i];
|
|
75
|
+
if (k instanceof HTMLElement && k.dataset.off != null) return { blockId, offset: Number(k.dataset.off) };
|
|
76
|
+
}
|
|
77
|
+
let end = 0;
|
|
78
|
+
for (const k of kids) if (k instanceof HTMLElement && k.dataset.off != null) end = Math.max(end, Number(k.dataset.off) + spanLen(k));
|
|
79
|
+
return { blockId, offset: end };
|
|
131
80
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
snapshot
|
|
135
|
-
}) {
|
|
136
|
-
void snapshot.revision;
|
|
137
|
-
const rects = editor.selectionRectsForViewport();
|
|
138
|
-
if (rects.length === 0) return null;
|
|
139
|
-
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-selection-layer", "aria-hidden": true, children: rects.map((r, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
140
|
-
"div",
|
|
141
|
-
{
|
|
142
|
-
className: "ori-selection-rect",
|
|
143
|
-
style: { position: "absolute", left: r.x, top: r.y, width: r.width, height: r.height }
|
|
144
|
-
},
|
|
145
|
-
`${r.blockId}:${i}`
|
|
146
|
-
)) });
|
|
81
|
+
function spanLen(span) {
|
|
82
|
+
return span.dataset.len != null ? Number(span.dataset.len) : (span.textContent ?? "").length;
|
|
147
83
|
}
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (!r) return;
|
|
166
|
-
e.preventDefault();
|
|
167
|
-
e.stopPropagation();
|
|
168
|
-
drag.current = { fixed: which === "start" ? r.end : r.start };
|
|
169
|
-
try {
|
|
170
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
171
|
-
} catch {
|
|
172
|
-
}
|
|
173
|
-
};
|
|
174
|
-
const move = (e) => {
|
|
175
|
-
const d = drag.current;
|
|
176
|
-
if (!d) return;
|
|
177
|
-
e.preventDefault();
|
|
178
|
-
const pos = pointToPosition(e.clientX, e.clientY);
|
|
179
|
-
if (pos) editor.setSelection({ anchor: d.fixed, focus: pos });
|
|
180
|
-
};
|
|
181
|
-
const up = (e) => {
|
|
182
|
-
if (!drag.current) return;
|
|
183
|
-
try {
|
|
184
|
-
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
185
|
-
} catch {
|
|
186
|
-
}
|
|
187
|
-
drag.current = null;
|
|
188
|
-
};
|
|
189
|
-
const handle = (kind, x, y, h) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
190
|
-
"div",
|
|
191
|
-
{
|
|
192
|
-
className: `ori-handle ori-handle-${kind}`,
|
|
193
|
-
style: { position: "absolute", left: x, top: y, height: h },
|
|
194
|
-
onPointerDown: start(kind),
|
|
195
|
-
onPointerMove: move,
|
|
196
|
-
onPointerUp: up,
|
|
197
|
-
onPointerCancel: up,
|
|
198
|
-
children: [
|
|
199
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ori-handle-knob" }),
|
|
200
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ori-handle-bar" })
|
|
201
|
-
]
|
|
84
|
+
function modelToDom(root, blockId, offset) {
|
|
85
|
+
const blockEl = root.querySelector(`[data-block-id="${esc(blockId)}"]`);
|
|
86
|
+
if (!blockEl) return null;
|
|
87
|
+
const spans = Array.from(blockEl.querySelectorAll("[data-off]"));
|
|
88
|
+
if (spans.length === 0) {
|
|
89
|
+
return { node: blockEl, offset: 0 };
|
|
90
|
+
}
|
|
91
|
+
for (const span of spans) {
|
|
92
|
+
const start = Number(span.dataset.off);
|
|
93
|
+
const len = spanLen(span);
|
|
94
|
+
if (offset <= start + len) {
|
|
95
|
+
if (span.dataset.atom != null) {
|
|
96
|
+
const idx = Array.prototype.indexOf.call(blockEl.childNodes, span);
|
|
97
|
+
return { node: blockEl, offset: offset <= start ? idx : idx + 1 };
|
|
98
|
+
}
|
|
99
|
+
const textNode2 = span.firstChild ?? span;
|
|
100
|
+
return { node: textNode2, offset: Math.max(0, Math.min(offset - start, (textNode2.textContent ?? "").length)) };
|
|
202
101
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
] });
|
|
102
|
+
}
|
|
103
|
+
const last = spans[spans.length - 1];
|
|
104
|
+
const textNode = last.firstChild ?? last;
|
|
105
|
+
return { node: textNode, offset: (textNode.textContent ?? "").length };
|
|
208
106
|
}
|
|
209
|
-
function
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
return
|
|
219
|
-
"div",
|
|
220
|
-
{
|
|
221
|
-
className: "ori-caret",
|
|
222
|
-
style: { position: "absolute", left: rect.x, top: rect.y, height: rect.height },
|
|
223
|
-
"aria-hidden": true
|
|
224
|
-
}
|
|
225
|
-
);
|
|
107
|
+
function markClass(marks) {
|
|
108
|
+
const m = marks ?? {};
|
|
109
|
+
const cls = ["ori-frag"];
|
|
110
|
+
if (m.bold) cls.push("ori-m-bold");
|
|
111
|
+
if (m.italic) cls.push("ori-m-italic");
|
|
112
|
+
if (m.underline) cls.push("ori-m-underline");
|
|
113
|
+
if (m.strike) cls.push("ori-m-strike");
|
|
114
|
+
if (m.code) cls.push("ori-frag-code");
|
|
115
|
+
if (m.link) cls.push("ori-frag-link");
|
|
116
|
+
return cls.join(" ");
|
|
226
117
|
}
|
|
227
|
-
function
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
118
|
+
function buildRun(item) {
|
|
119
|
+
const span = document.createElement("span");
|
|
120
|
+
span.className = markClass(item.marks);
|
|
121
|
+
span.dataset.off = String(item.start);
|
|
122
|
+
span.dataset.len = String(item.text.length);
|
|
123
|
+
span.textContent = item.text;
|
|
124
|
+
return span;
|
|
231
125
|
}
|
|
232
126
|
|
|
233
|
-
// src/
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
editor
|
|
237
|
-
|
|
238
|
-
editor
|
|
239
|
-
|
|
127
|
+
// src/ce/view.ts
|
|
128
|
+
var PLACEHOLDER = "\uFFFC";
|
|
129
|
+
var EditorView = class {
|
|
130
|
+
constructor(root, editor, opts) {
|
|
131
|
+
__publicField(this, "root", root);
|
|
132
|
+
__publicField(this, "editor", editor);
|
|
133
|
+
__publicField(this, "opts", opts);
|
|
134
|
+
__publicField(this, "roots", /* @__PURE__ */ new Map());
|
|
135
|
+
__publicField(this, "composing", false);
|
|
136
|
+
__publicField(this, "applyingModel", false);
|
|
137
|
+
__publicField(this, "detachers", []);
|
|
138
|
+
/** The model revision the DOM currently reflects (so external changes — remote
|
|
139
|
+
* edits, app commands — re-render, but our own edits don't clobber the caret). */
|
|
140
|
+
__publicField(this, "lastRevision", -1);
|
|
141
|
+
root.setAttribute("contenteditable", opts.readOnly ? "false" : "true");
|
|
142
|
+
root.setAttribute("spellcheck", opts.readOnly ? "false" : "true");
|
|
143
|
+
root.setAttribute("role", "textbox");
|
|
144
|
+
root.setAttribute("aria-multiline", "true");
|
|
145
|
+
this.renderBlocks();
|
|
146
|
+
this.lastRevision = this.rev();
|
|
147
|
+
const on = (t, h, o) => {
|
|
148
|
+
root.addEventListener(t, h, o);
|
|
149
|
+
this.detachers.push(() => root.removeEventListener(t, h, o));
|
|
150
|
+
};
|
|
151
|
+
on("beforeinput", (e) => this.onBeforeInput(e));
|
|
152
|
+
on("input", () => this.onInput());
|
|
153
|
+
on("keydown", (e) => this.onKeyDown(e));
|
|
154
|
+
on("blur", () => {
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
if (document.activeElement === this.root || !document.hasFocus()) return;
|
|
157
|
+
const sel = this.editor.getSelection();
|
|
158
|
+
if (sel && !oriCore.isCollapsed(sel)) {
|
|
159
|
+
this.editor.collapse(sel.focus);
|
|
160
|
+
this.lastRevision = this.rev();
|
|
161
|
+
}
|
|
162
|
+
}, 0);
|
|
163
|
+
});
|
|
164
|
+
on("compositionstart", () => this.composing = true);
|
|
165
|
+
on("compositionend", () => {
|
|
166
|
+
this.composing = false;
|
|
167
|
+
this.onInput();
|
|
168
|
+
});
|
|
169
|
+
const onSelChange = () => {
|
|
170
|
+
if (this.applyingModel || this.composing) return;
|
|
171
|
+
const sel = this.readSelection();
|
|
172
|
+
if (!sel) return;
|
|
173
|
+
this.editor.setSelection(sel);
|
|
174
|
+
this.lastRevision = this.rev();
|
|
175
|
+
};
|
|
176
|
+
document.addEventListener("selectionchange", onSelChange);
|
|
177
|
+
this.detachers.push(() => document.removeEventListener("selectionchange", onSelChange));
|
|
240
178
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
292
|
-
|
|
179
|
+
destroy() {
|
|
180
|
+
this.detachers.forEach((d) => d());
|
|
181
|
+
this.roots.forEach((r) => r.unmount());
|
|
182
|
+
this.roots.clear();
|
|
183
|
+
}
|
|
184
|
+
focus() {
|
|
185
|
+
this.root.focus();
|
|
186
|
+
}
|
|
187
|
+
// --- rendering ---------------------------------------------------------
|
|
188
|
+
rev() {
|
|
189
|
+
return this.editor.getSnapshot().revision;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Called by React on every model change. Only re-renders when the model moved
|
|
193
|
+
* ahead of what we last drew (an *external* change — app command, undo, remote);
|
|
194
|
+
* our own edits already updated the DOM and must not be clobbered.
|
|
195
|
+
*/
|
|
196
|
+
sync() {
|
|
197
|
+
const rev = this.rev();
|
|
198
|
+
if (rev === this.lastRevision) return;
|
|
199
|
+
const changed = this.renderBlocks();
|
|
200
|
+
if (changed) this.writeSelection();
|
|
201
|
+
this.lastRevision = rev;
|
|
202
|
+
}
|
|
203
|
+
/** After a controlled (preventDefault'd) edit: re-render + restore the caret. */
|
|
204
|
+
commit() {
|
|
205
|
+
this.renderBlocks();
|
|
206
|
+
this.writeSelection();
|
|
207
|
+
this.lastRevision = this.rev();
|
|
208
|
+
}
|
|
209
|
+
/** A content signature for a block, so unchanged blocks aren't re-rendered. */
|
|
210
|
+
sig(id) {
|
|
211
|
+
return this.editor.getBlockType(id) + "|" + JSON.stringify(this.editor.getInline(id));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Reconcile the DOM to the *visible window* of blocks (virtualization): a top
|
|
215
|
+
* spacer, the windowed blocks, then a bottom spacer — heights from the
|
|
216
|
+
* controller's offscreen measurement. On-screen blocks are reused by id so a
|
|
217
|
+
* caret inside one survives a scroll. Returns true if the DOM was mutated.
|
|
218
|
+
*/
|
|
219
|
+
renderBlocks() {
|
|
220
|
+
let changed = false;
|
|
221
|
+
const snap = this.editor.getSnapshot();
|
|
222
|
+
const vis = snap.visible;
|
|
223
|
+
const topH = vis.length ? vis[0].top : 0;
|
|
224
|
+
const botH = vis.length ? Math.max(0, snap.totalHeight - (vis[vis.length - 1].top + vis[vis.length - 1].height)) : Math.max(0, snap.totalHeight);
|
|
225
|
+
const TOP = "\0top";
|
|
226
|
+
const BOTTOM = "\0bottom";
|
|
227
|
+
const want = [TOP, ...vis.map((v) => v.id), BOTTOM];
|
|
228
|
+
const keyOf = (el) => {
|
|
229
|
+
const e = el;
|
|
230
|
+
return e.dataset.spacer ? "\0" + e.dataset.spacer : e.dataset.blockId ?? "";
|
|
231
|
+
};
|
|
232
|
+
const have = /* @__PURE__ */ new Map();
|
|
233
|
+
for (const c of Array.from(this.root.children)) have.set(keyOf(c), c);
|
|
234
|
+
let prev = null;
|
|
235
|
+
for (const k of want) {
|
|
236
|
+
let el = have.get(k);
|
|
237
|
+
if (el) {
|
|
238
|
+
have.delete(k);
|
|
239
|
+
} else {
|
|
240
|
+
el = k === TOP || k === BOTTOM ? this.makeSpacer(k.slice(1)) : this.makeBlock(k);
|
|
241
|
+
changed = true;
|
|
242
|
+
}
|
|
243
|
+
const anchor = prev ? prev.nextSibling : this.root.firstChild;
|
|
244
|
+
if (anchor !== el) {
|
|
245
|
+
this.root.insertBefore(el, anchor);
|
|
246
|
+
changed = true;
|
|
247
|
+
}
|
|
248
|
+
prev = el;
|
|
249
|
+
}
|
|
250
|
+
for (const el of have.values()) {
|
|
251
|
+
if (el.dataset.blockId) this.unmountRootsIn(el);
|
|
252
|
+
el.remove();
|
|
253
|
+
changed = true;
|
|
254
|
+
}
|
|
255
|
+
const top = this.root.firstElementChild;
|
|
256
|
+
if (top && top.style.height !== `${topH}px`) top.style.height = `${topH}px`;
|
|
257
|
+
const bot = this.root.lastElementChild;
|
|
258
|
+
if (bot && bot.style.height !== `${botH}px`) bot.style.height = `${botH}px`;
|
|
259
|
+
for (const vb of vis) {
|
|
260
|
+
const el = this.root.querySelector(`[data-block-id="${esc(vb.id)}"]`);
|
|
261
|
+
if (!el) continue;
|
|
262
|
+
const sig = this.sig(vb.id);
|
|
263
|
+
if (el.dataset.sig !== sig) {
|
|
264
|
+
el.dataset.sig = sig;
|
|
265
|
+
this.renderBlockInner(el, vb.id);
|
|
266
|
+
changed = true;
|
|
267
|
+
}
|
|
293
268
|
}
|
|
269
|
+
return changed;
|
|
294
270
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
271
|
+
makeBlock(id) {
|
|
272
|
+
const el = document.createElement("div");
|
|
273
|
+
el.dataset.blockId = id;
|
|
274
|
+
return el;
|
|
275
|
+
}
|
|
276
|
+
makeSpacer(which) {
|
|
277
|
+
const el = document.createElement("div");
|
|
278
|
+
el.dataset.spacer = which;
|
|
279
|
+
el.setAttribute("contenteditable", "false");
|
|
280
|
+
el.setAttribute("aria-hidden", "true");
|
|
281
|
+
el.style.userSelect = "none";
|
|
282
|
+
el.style.pointerEvents = "none";
|
|
283
|
+
return el;
|
|
284
|
+
}
|
|
285
|
+
renderBlockInner(el, id) {
|
|
286
|
+
this.unmountRootsIn(el);
|
|
287
|
+
const type = this.editor.getBlockType(id);
|
|
288
|
+
el.className = `ori-block ori-block-${type}`;
|
|
289
|
+
const blockRenderer = this.opts.renderBlock(type);
|
|
290
|
+
if (blockRenderer) {
|
|
291
|
+
el.contentEditable = "false";
|
|
292
|
+
el.textContent = "";
|
|
293
|
+
const root = client.createRoot(el);
|
|
294
|
+
root.render(blockRenderer({ editor: this.editor, block: { id, type, index: 0, top: 0, height: 0 }, layout: this.editor.getLayout(id) }));
|
|
295
|
+
this.roots.set(el, root);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
el.contentEditable = "inherit";
|
|
299
|
+
el.textContent = "";
|
|
300
|
+
const items = this.editor.getInline(id);
|
|
301
|
+
if (items.length === 0) {
|
|
302
|
+
el.appendChild(document.createElement("br"));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
for (const item of items) {
|
|
306
|
+
if (item.atom) {
|
|
307
|
+
const span = document.createElement("span");
|
|
308
|
+
span.className = "ori-atom";
|
|
309
|
+
span.contentEditable = "false";
|
|
310
|
+
span.dataset.atom = "true";
|
|
311
|
+
span.dataset.off = String(item.start);
|
|
312
|
+
span.dataset.len = "1";
|
|
313
|
+
el.appendChild(span);
|
|
314
|
+
const renderer = this.opts.renderAtom(item.atom.type);
|
|
315
|
+
if (renderer) {
|
|
316
|
+
const r = client.createRoot(span);
|
|
317
|
+
r.render(renderer({ editor: this.editor, atom: item.atom }));
|
|
318
|
+
this.roots.set(span, r);
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
el.appendChild(buildRun(item));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
unmountRootsIn(el) {
|
|
326
|
+
for (const [node, root] of this.roots) {
|
|
327
|
+
if (el === node || el.contains(node)) {
|
|
328
|
+
root.unmount();
|
|
329
|
+
this.roots.delete(node);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// --- selection ---------------------------------------------------------
|
|
334
|
+
readSelection() {
|
|
335
|
+
const s = window.getSelection();
|
|
336
|
+
if (!s || s.rangeCount === 0 || !this.root.contains(s.anchorNode)) return null;
|
|
337
|
+
const a = domToModel(this.root, s.anchorNode, s.anchorOffset);
|
|
338
|
+
const f = domToModel(this.root, s.focusNode, s.focusOffset);
|
|
339
|
+
if (!a || !f) return null;
|
|
340
|
+
return { anchor: { blockId: a.blockId, offset: a.offset }, focus: { blockId: f.blockId, offset: f.offset } };
|
|
341
|
+
}
|
|
342
|
+
/** Push the controller's selection back into the DOM (after a model op). */
|
|
343
|
+
writeSelection() {
|
|
344
|
+
const sel = this.editor.getSelection();
|
|
345
|
+
if (!sel) return;
|
|
346
|
+
const a = modelToDom(this.root, sel.anchor.blockId, sel.anchor.offset);
|
|
347
|
+
const f = modelToDom(this.root, sel.focus.blockId, sel.focus.offset);
|
|
348
|
+
if (!a || !f) return;
|
|
349
|
+
const r = document.createRange();
|
|
350
|
+
const s = window.getSelection();
|
|
351
|
+
if (!s) return;
|
|
352
|
+
this.applyingModel = true;
|
|
353
|
+
try {
|
|
354
|
+
r.setStart(a.node, a.offset);
|
|
355
|
+
s.removeAllRanges();
|
|
356
|
+
s.addRange(r);
|
|
357
|
+
s.extend(f.node, f.offset);
|
|
358
|
+
} catch {
|
|
359
|
+
} finally {
|
|
360
|
+
this.applyingModel = false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
/** The block text as the model sees it (atoms collapse to one placeholder). */
|
|
364
|
+
domBlockText(el) {
|
|
365
|
+
let out = "";
|
|
366
|
+
for (const child of Array.from(el.childNodes)) {
|
|
367
|
+
if (child instanceof HTMLElement && child.dataset.atom != null) {
|
|
368
|
+
out += PLACEHOLDER;
|
|
369
|
+
} else {
|
|
370
|
+
out += child.textContent ?? "";
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
// --- input -------------------------------------------------------------
|
|
376
|
+
/** Formatting + history shortcuts (the browser fires these as keydown). */
|
|
377
|
+
onKeyDown(e) {
|
|
378
|
+
if (this.opts.readOnly) return;
|
|
379
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
380
|
+
if (!mod || e.altKey) return;
|
|
381
|
+
const k = e.key.toLowerCase();
|
|
382
|
+
const mark = { b: "bold", i: "italic", u: "underline", e: "code" }[k];
|
|
383
|
+
if (mark) {
|
|
305
384
|
e.preventDefault();
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
385
|
+
const sel = this.readSelection();
|
|
386
|
+
if (sel) this.editor.setSelection(sel);
|
|
387
|
+
this.editor.toggleMark(mark);
|
|
388
|
+
this.commit();
|
|
389
|
+
} else if (k === "z") {
|
|
309
390
|
e.preventDefault();
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
|
|
391
|
+
if (e.shiftKey) this.editor.redo();
|
|
392
|
+
else this.editor.undo();
|
|
393
|
+
this.commit();
|
|
394
|
+
} else if (k === "y") {
|
|
313
395
|
e.preventDefault();
|
|
314
|
-
editor.
|
|
315
|
-
|
|
316
|
-
|
|
396
|
+
this.editor.redo();
|
|
397
|
+
this.commit();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
onBeforeInput(e) {
|
|
401
|
+
if (this.opts.readOnly) {
|
|
317
402
|
e.preventDefault();
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const sel = this.readSelection();
|
|
406
|
+
if (!sel) return;
|
|
407
|
+
this.editor.setSelection(sel);
|
|
408
|
+
const collapsed = oriCore.isCollapsed(sel);
|
|
409
|
+
const startOffset = this.editor.orderedSelection()?.start.offset ?? sel.focus.offset;
|
|
410
|
+
const t = e.inputType;
|
|
411
|
+
if (collapsed && (t === "insertText" || t === "insertCompositionText" || t === "insertReplacementText")) return;
|
|
412
|
+
if (collapsed && t === "deleteContentForward") return;
|
|
413
|
+
if (collapsed && t === "deleteContentBackward" && startOffset > 0) return;
|
|
414
|
+
const ed = this.editor;
|
|
415
|
+
if (t === "insertParagraph") {
|
|
321
416
|
e.preventDefault();
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
case "ArrowDown":
|
|
417
|
+
ed.insertParagraphBreak();
|
|
418
|
+
} else if (t.startsWith("delete")) {
|
|
325
419
|
e.preventDefault();
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
420
|
+
if (t === "deleteContentForward") ed.deleteForward();
|
|
421
|
+
else ed.deleteBackward();
|
|
422
|
+
} else if (t === "insertText" || t === "insertReplacementText" || t === "insertFromPaste") {
|
|
329
423
|
e.preventDefault();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
424
|
+
const text = e.data ?? e.dataTransfer?.getData("text/plain") ?? "";
|
|
425
|
+
if (text) ed.insertText(text);
|
|
426
|
+
} else if (t === "insertLineBreak") {
|
|
333
427
|
e.preventDefault();
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
428
|
+
ed.insertText("\n");
|
|
429
|
+
} else {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.commit();
|
|
338
433
|
}
|
|
434
|
+
onInput() {
|
|
435
|
+
if (this.composing || this.opts.readOnly) return;
|
|
436
|
+
const blockEl = blockElOf(window.getSelection()?.anchorNode ?? null, this.root);
|
|
437
|
+
if (!blockEl) {
|
|
438
|
+
this.renderBlocks();
|
|
439
|
+
this.lastRevision = this.rev();
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const id = blockEl.dataset.blockId;
|
|
443
|
+
const next = this.domBlockText(blockEl);
|
|
444
|
+
const cur = this.editor.getBlockText(id);
|
|
445
|
+
if (next === cur) return;
|
|
446
|
+
const max = Math.min(cur.length, next.length);
|
|
447
|
+
let p = 0;
|
|
448
|
+
while (p < max && cur[p] === next[p]) p++;
|
|
449
|
+
let s = 0;
|
|
450
|
+
while (s < max - p && cur[cur.length - 1 - s] === next[next.length - 1 - s]) s++;
|
|
451
|
+
const from = p;
|
|
452
|
+
const to = cur.length - s;
|
|
453
|
+
const insert = next.slice(p, next.length - s);
|
|
454
|
+
this.editor.setSelection({ anchor: { blockId: id, offset: from }, focus: { blockId: id, offset: to } });
|
|
455
|
+
if (to > from) this.editor.deleteBackward();
|
|
456
|
+
if (insert) this.editor.insertText(insert);
|
|
457
|
+
this.reindex(blockEl);
|
|
458
|
+
this.lastRevision = this.rev();
|
|
459
|
+
}
|
|
460
|
+
/** Re-derive data-off / data-len after a native edit (no node replacement). */
|
|
461
|
+
reindex(el) {
|
|
462
|
+
let off = 0;
|
|
463
|
+
for (const child of Array.from(el.children)) {
|
|
464
|
+
if (child.dataset.off == null) continue;
|
|
465
|
+
child.dataset.off = String(off);
|
|
466
|
+
const len = child.dataset.atom != null ? 1 : (child.textContent ?? "").length;
|
|
467
|
+
child.dataset.len = String(len);
|
|
468
|
+
off += len;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
function caretClientRect() {
|
|
473
|
+
const s = window.getSelection();
|
|
474
|
+
if (!s || s.rangeCount === 0) return null;
|
|
475
|
+
const r = s.getRangeAt(0).cloneRange();
|
|
476
|
+
r.collapse(s.focusNode === r.endContainer && s.focusOffset === r.endOffset ? false : true);
|
|
477
|
+
const rects = r.getClientRects();
|
|
478
|
+
if (rects.length) return rects[rects.length - 1];
|
|
479
|
+
const b = r.getBoundingClientRect();
|
|
480
|
+
if (b.height || b.width) return b;
|
|
481
|
+
const node = r.startContainer;
|
|
482
|
+
const el = (node.nodeType === Node.TEXT_NODE ? node.parentElement : node) ?? null;
|
|
483
|
+
if (!el) return null;
|
|
484
|
+
const eb = el.getBoundingClientRect();
|
|
485
|
+
const cs = getComputedStyle(el);
|
|
486
|
+
const lh = parseFloat(cs.lineHeight) || parseFloat(cs.fontSize) * 1.4 || 18;
|
|
487
|
+
const padL = parseFloat(cs.paddingLeft) || 0;
|
|
488
|
+
const padT = parseFloat(cs.paddingTop) || 0;
|
|
489
|
+
return new DOMRect(eb.left + padL, eb.top + padT, 0, lh);
|
|
339
490
|
}
|
|
340
|
-
function
|
|
341
|
-
const max = Math.min(oldText.length, newText.length);
|
|
342
|
-
let p = 0;
|
|
343
|
-
while (p < max && oldText[p] === newText[p]) p++;
|
|
344
|
-
let s = 0;
|
|
345
|
-
while (s < max - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
|
|
346
|
-
return { from: p, to: oldText.length - s, insert: newText.slice(p, newText.length - s) };
|
|
347
|
-
}
|
|
348
|
-
var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
349
|
-
editor,
|
|
350
|
-
className,
|
|
351
|
-
style,
|
|
352
|
-
maxWidth = 720,
|
|
353
|
-
placeholder = "Start writing\u2026",
|
|
354
|
-
autoFocus,
|
|
355
|
-
readOnly,
|
|
356
|
-
blockRenderers,
|
|
357
|
-
atomRenderers
|
|
358
|
-
}, ref) {
|
|
491
|
+
var NoteEditor = react.forwardRef(function NoteEditor2({ editor, className, style, maxWidth = 720, placeholder, autoFocus, readOnly, blockRenderers, atomRenderers }, ref) {
|
|
359
492
|
const snapshot = useEditorSnapshot(editor);
|
|
360
|
-
const renderers = react.useMemo(
|
|
361
|
-
() => ({ blocks: blockRenderers ?? {}, atoms: atomRenderers ?? {} }),
|
|
362
|
-
[blockRenderers, atomRenderers]
|
|
363
|
-
);
|
|
364
493
|
const scrollerRef = react.useRef(null);
|
|
365
494
|
const contentRef = react.useRef(null);
|
|
366
|
-
const
|
|
367
|
-
const composingRef = react.useRef(false);
|
|
368
|
-
const mirrorRef = react.useRef(null);
|
|
495
|
+
const viewRef = react.useRef(null);
|
|
369
496
|
const [focused, setFocused] = react.useState(false);
|
|
370
|
-
const [
|
|
371
|
-
const
|
|
497
|
+
const [caret, setCaret] = react.useState(null);
|
|
498
|
+
const renderersRef = react.useRef({ blockRenderers, atomRenderers });
|
|
499
|
+
renderersRef.current = { blockRenderers, atomRenderers };
|
|
372
500
|
react.useImperativeHandle(
|
|
373
501
|
ref,
|
|
374
502
|
() => ({
|
|
375
|
-
focus: () =>
|
|
503
|
+
focus: () => contentRef.current?.focus(),
|
|
376
504
|
getCaretRect: () => {
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
if (!c || !content) return null;
|
|
380
|
-
const r = content.getBoundingClientRect();
|
|
381
|
-
return { x: r.left + c.x, y: r.top + c.y, height: c.height };
|
|
505
|
+
const r = caretClientRect();
|
|
506
|
+
return r ? { x: r.left, y: r.top, height: r.height || 16 } : null;
|
|
382
507
|
},
|
|
383
508
|
getSelectionRect: () => {
|
|
384
|
-
const
|
|
385
|
-
if (!
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
388
|
-
|
|
389
|
-
let top = Infinity;
|
|
390
|
-
let left = Infinity;
|
|
391
|
-
let bottom = -Infinity;
|
|
392
|
-
let right = -Infinity;
|
|
393
|
-
for (const rc of rects) {
|
|
394
|
-
top = Math.min(top, r.top + rc.y);
|
|
395
|
-
left = Math.min(left, r.left + rc.x);
|
|
396
|
-
bottom = Math.max(bottom, r.top + rc.y + rc.height);
|
|
397
|
-
right = Math.max(right, r.left + rc.x + rc.width);
|
|
398
|
-
}
|
|
399
|
-
return { top, left, right, bottom, width: right - left, height: bottom - top };
|
|
509
|
+
const s = window.getSelection();
|
|
510
|
+
if (!s || s.rangeCount === 0 || s.isCollapsed) return null;
|
|
511
|
+
const b = s.getRangeAt(0).getBoundingClientRect();
|
|
512
|
+
if (!b.width && !b.height) return null;
|
|
513
|
+
return { top: b.top, left: b.left, right: b.right, bottom: b.bottom, width: b.width, height: b.height };
|
|
400
514
|
},
|
|
401
515
|
getScrollElement: () => scrollerRef.current
|
|
402
516
|
}),
|
|
403
|
-
[
|
|
517
|
+
[]
|
|
404
518
|
);
|
|
519
|
+
react.useEffect(() => {
|
|
520
|
+
const el = contentRef.current;
|
|
521
|
+
if (!el) return;
|
|
522
|
+
const view = new EditorView(el, editor, {
|
|
523
|
+
readOnly,
|
|
524
|
+
renderAtom: (t) => renderersRef.current.atomRenderers?.[t],
|
|
525
|
+
renderBlock: (t) => renderersRef.current.blockRenderers?.[t]
|
|
526
|
+
});
|
|
527
|
+
viewRef.current = view;
|
|
528
|
+
return () => {
|
|
529
|
+
view.destroy();
|
|
530
|
+
viewRef.current = null;
|
|
531
|
+
};
|
|
532
|
+
}, [editor, readOnly]);
|
|
533
|
+
react.useEffect(() => {
|
|
534
|
+
viewRef.current?.sync();
|
|
535
|
+
}, [snapshot.revision]);
|
|
536
|
+
react.useEffect(() => {
|
|
537
|
+
if (autoFocus) contentRef.current?.focus();
|
|
538
|
+
}, [autoFocus]);
|
|
539
|
+
react.useEffect(() => {
|
|
540
|
+
const update = () => {
|
|
541
|
+
const content = contentRef.current;
|
|
542
|
+
const s = window.getSelection();
|
|
543
|
+
if (!content || !s || s.rangeCount === 0 || !s.isCollapsed || !content.contains(s.anchorNode)) {
|
|
544
|
+
setCaret(null);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const r = caretClientRect();
|
|
548
|
+
const box = content.getBoundingClientRect();
|
|
549
|
+
if (r) setCaret({ x: r.left - box.left, y: r.top - box.top, h: r.height || 18 });
|
|
550
|
+
};
|
|
551
|
+
document.addEventListener("selectionchange", update);
|
|
552
|
+
const ro = new ResizeObserver(update);
|
|
553
|
+
if (contentRef.current) ro.observe(contentRef.current);
|
|
554
|
+
update();
|
|
555
|
+
return () => {
|
|
556
|
+
document.removeEventListener("selectionchange", update);
|
|
557
|
+
ro.disconnect();
|
|
558
|
+
};
|
|
559
|
+
}, []);
|
|
405
560
|
react.useLayoutEffect(() => {
|
|
406
|
-
const
|
|
561
|
+
const sc = scrollerRef.current;
|
|
407
562
|
const content = contentRef.current;
|
|
408
|
-
if (!
|
|
563
|
+
if (!sc || !content) return;
|
|
409
564
|
const sync = () => {
|
|
410
565
|
editor.setWidth(content.clientWidth);
|
|
411
|
-
editor.setViewport(
|
|
566
|
+
editor.setViewport(sc.scrollTop, sc.clientHeight);
|
|
412
567
|
};
|
|
413
568
|
sync();
|
|
414
569
|
const ro = new ResizeObserver(sync);
|
|
415
|
-
ro.observe(
|
|
570
|
+
ro.observe(sc);
|
|
416
571
|
ro.observe(content);
|
|
417
572
|
return () => ro.disconnect();
|
|
418
573
|
}, [editor]);
|
|
419
|
-
react.useEffect(() => {
|
|
420
|
-
const fonts = document.fonts;
|
|
421
|
-
if (fonts?.ready) void fonts.ready.then(() => editor.invalidateMeasurements());
|
|
422
|
-
}, [editor]);
|
|
423
|
-
react.useEffect(() => {
|
|
424
|
-
if (autoFocus) inputRef.current?.focus();
|
|
425
|
-
}, [autoFocus]);
|
|
426
|
-
const pointToPosition = useCallbackRef((clientX, clientY) => {
|
|
427
|
-
const content = contentRef.current;
|
|
428
|
-
if (!content) return null;
|
|
429
|
-
const rect = content.getBoundingClientRect();
|
|
430
|
-
return editor.positionFromPoint(clientX - rect.left, clientY - rect.top);
|
|
431
|
-
});
|
|
432
|
-
react.useEffect(() => {
|
|
433
|
-
const mq = window.matchMedia?.("(pointer: coarse)");
|
|
434
|
-
if (!mq) return;
|
|
435
|
-
const update = () => setCoarse(mq.matches);
|
|
436
|
-
update();
|
|
437
|
-
mq.addEventListener?.("change", update);
|
|
438
|
-
return () => mq.removeEventListener?.("change", update);
|
|
439
|
-
}, []);
|
|
440
|
-
const gestureRef = react.useRef(null);
|
|
441
|
-
const lastTapRef = react.useRef({ t: 0, x: 0, y: 0, count: 0 });
|
|
442
|
-
react.useEffect(() => {
|
|
443
|
-
const scroller = scrollerRef.current;
|
|
444
|
-
if (!scroller) return;
|
|
445
|
-
const TAP_SLOP = 10;
|
|
446
|
-
const MULTI_DIST = 28;
|
|
447
|
-
const MULTI_MS = 400;
|
|
448
|
-
const LONG_MS = 480;
|
|
449
|
-
const end = () => {
|
|
450
|
-
const g = gestureRef.current;
|
|
451
|
-
if (g?.longPress) clearTimeout(g.longPress);
|
|
452
|
-
gestureRef.current = null;
|
|
453
|
-
setTouchSelecting(false);
|
|
454
|
-
};
|
|
455
|
-
const tap = (x, y, type) => {
|
|
456
|
-
const pos = pointToPosition(x, y);
|
|
457
|
-
if (!pos) return;
|
|
458
|
-
if (type !== "mouse") inputRef.current?.focus();
|
|
459
|
-
const now = Date.now();
|
|
460
|
-
const lt = lastTapRef.current;
|
|
461
|
-
const near = Math.hypot(x - lt.x, y - lt.y) <= MULTI_DIST;
|
|
462
|
-
const count = near && now - lt.t <= MULTI_MS ? Math.min(lt.count + 1, 3) : 1;
|
|
463
|
-
lastTapRef.current = { t: now, x, y, count };
|
|
464
|
-
if (count === 2) editor.selectWordAt(pos);
|
|
465
|
-
else if (count === 3) editor.selectBlockAt(pos);
|
|
466
|
-
else editor.collapse(pos);
|
|
467
|
-
};
|
|
468
|
-
const onDown = (e) => {
|
|
469
|
-
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
470
|
-
if (e.target?.closest?.(".ori-handle")) return;
|
|
471
|
-
if (e.clientX - scroller.getBoundingClientRect().left >= scroller.clientWidth) return;
|
|
472
|
-
const pos = pointToPosition(e.clientX, e.clientY);
|
|
473
|
-
const g = {
|
|
474
|
-
id: e.pointerId,
|
|
475
|
-
type: e.pointerType,
|
|
476
|
-
x: e.clientX,
|
|
477
|
-
y: e.clientY,
|
|
478
|
-
pos,
|
|
479
|
-
mode: "idle",
|
|
480
|
-
moved: false,
|
|
481
|
-
longPress: void 0
|
|
482
|
-
};
|
|
483
|
-
gestureRef.current = g;
|
|
484
|
-
if (e.pointerType === "mouse") {
|
|
485
|
-
inputRef.current?.focus();
|
|
486
|
-
if (pos) {
|
|
487
|
-
e.preventDefault();
|
|
488
|
-
const sel = editor.getSelection();
|
|
489
|
-
if (e.shiftKey && sel) editor.setSelection({ anchor: sel.anchor, focus: pos });
|
|
490
|
-
else editor.collapse(pos);
|
|
491
|
-
g.mode = "mouseDrag";
|
|
492
|
-
}
|
|
493
|
-
} else {
|
|
494
|
-
g.longPress = setTimeout(() => {
|
|
495
|
-
const gg = gestureRef.current;
|
|
496
|
-
if (!gg || gg.mode !== "idle" || gg.moved || !gg.pos) return;
|
|
497
|
-
gg.mode = "touchSelect";
|
|
498
|
-
setTouchSelecting(true);
|
|
499
|
-
inputRef.current?.focus();
|
|
500
|
-
editor.selectWordAt(gg.pos);
|
|
501
|
-
}, LONG_MS);
|
|
502
|
-
}
|
|
503
|
-
};
|
|
504
|
-
const onMove = (e) => {
|
|
505
|
-
const g = gestureRef.current;
|
|
506
|
-
if (!g || e.pointerId !== g.id) return;
|
|
507
|
-
if (Math.hypot(e.clientX - g.x, e.clientY - g.y) > TAP_SLOP) g.moved = true;
|
|
508
|
-
if (g.mode === "mouseDrag" || g.mode === "touchSelect") {
|
|
509
|
-
const pos = pointToPosition(e.clientX, e.clientY);
|
|
510
|
-
const sel = editor.getSelection();
|
|
511
|
-
if (pos && sel) editor.setSelection({ anchor: sel.anchor, focus: pos });
|
|
512
|
-
e.preventDefault();
|
|
513
|
-
} else if (g.mode === "idle" && g.moved && g.type !== "mouse") {
|
|
514
|
-
if (g.longPress) clearTimeout(g.longPress);
|
|
515
|
-
g.mode = "scroll";
|
|
516
|
-
}
|
|
517
|
-
};
|
|
518
|
-
const onUp = (e) => {
|
|
519
|
-
const g = gestureRef.current;
|
|
520
|
-
if (!g || e.pointerId !== g.id) return;
|
|
521
|
-
const wasTap = g.mode === "idle" || g.mode === "mouseDrag" && !g.moved;
|
|
522
|
-
if (wasTap) tap(e.clientX, e.clientY, g.type);
|
|
523
|
-
end();
|
|
524
|
-
};
|
|
525
|
-
scroller.addEventListener("pointerdown", onDown, { passive: false });
|
|
526
|
-
window.addEventListener("pointermove", onMove, { passive: false });
|
|
527
|
-
window.addEventListener("pointerup", onUp);
|
|
528
|
-
window.addEventListener("pointercancel", end);
|
|
529
|
-
return () => {
|
|
530
|
-
scroller.removeEventListener("pointerdown", onDown);
|
|
531
|
-
window.removeEventListener("pointermove", onMove);
|
|
532
|
-
window.removeEventListener("pointerup", onUp);
|
|
533
|
-
window.removeEventListener("pointercancel", end);
|
|
534
|
-
};
|
|
535
|
-
}, [editor, pointToPosition]);
|
|
536
574
|
const onScroll = () => {
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
575
|
+
const sc = scrollerRef.current;
|
|
576
|
+
if (sc) editor.setViewport(sc.scrollTop, sc.clientHeight);
|
|
539
577
|
};
|
|
540
|
-
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
578
|
+
const onPointerDown = (e) => {
|
|
579
|
+
if (readOnly) return;
|
|
580
|
+
if (e.target.closest("[data-block-id]")) return;
|
|
581
|
+
const content = contentRef.current;
|
|
582
|
+
const blocks = content?.querySelectorAll("[data-block-id]");
|
|
583
|
+
const last = blocks && blocks[blocks.length - 1];
|
|
584
|
+
if (!content || !last) return;
|
|
585
|
+
if (e.clientY <= last.getBoundingClientRect().bottom) return;
|
|
586
|
+
e.preventDefault();
|
|
587
|
+
content.focus();
|
|
588
|
+
const sel = window.getSelection();
|
|
589
|
+
if (!sel) return;
|
|
590
|
+
const range = document.createRange();
|
|
591
|
+
range.selectNodeContents(last);
|
|
592
|
+
range.collapse(false);
|
|
593
|
+
sel.removeAllRanges();
|
|
594
|
+
sel.addRange(range);
|
|
595
|
+
};
|
|
596
|
+
const showCaret = focused && !!caret && !readOnly;
|
|
597
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-scroller", ref: scrollerRef, onScroll, onPointerDown, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ori-content", style: { maxWidth, marginInline: "auto", position: "relative" }, children: [
|
|
598
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
599
|
+
"div",
|
|
600
|
+
{
|
|
601
|
+
className: "ori-canvas ori-ce",
|
|
602
|
+
ref: contentRef,
|
|
603
|
+
onFocus: () => setFocused(true),
|
|
604
|
+
onBlur: () => setFocused(false),
|
|
605
|
+
suppressContentEditableWarning: true
|
|
563
606
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const m = mirrorRef.current;
|
|
572
|
-
if (!el || !m || composingRef.current || document.activeElement !== el) return;
|
|
573
|
-
if (el.value !== m.text) return;
|
|
574
|
-
const a = el.selectionStart ?? 0;
|
|
575
|
-
const b = el.selectionEnd ?? 0;
|
|
576
|
-
const backward = el.selectionDirection === "backward";
|
|
577
|
-
const anchorOff = backward ? b : a;
|
|
578
|
-
const focusOff = backward ? a : b;
|
|
579
|
-
const cur = editor.getSelection();
|
|
580
|
-
if (cur && cur.anchor.blockId === m.blockId && cur.focus.blockId === m.blockId && cur.anchor.offset === anchorOff && cur.focus.offset === focusOff) {
|
|
581
|
-
return;
|
|
607
|
+
),
|
|
608
|
+
showCaret && caret ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
609
|
+
"div",
|
|
610
|
+
{
|
|
611
|
+
className: "ori-caret",
|
|
612
|
+
style: { position: "absolute", left: caret.x, top: caret.y, height: caret.h, pointerEvents: "none" },
|
|
613
|
+
"aria-hidden": true
|
|
582
614
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
});
|
|
587
|
-
};
|
|
588
|
-
document.addEventListener("selectionchange", onSelChange);
|
|
589
|
-
return () => document.removeEventListener("selectionchange", onSelChange);
|
|
590
|
-
}, [coarse, editor]);
|
|
591
|
-
const applyMirrorDiff = () => {
|
|
592
|
-
const el = inputRef.current;
|
|
593
|
-
const m = mirrorRef.current;
|
|
594
|
-
if (!el || !m || readOnly) return;
|
|
595
|
-
const { from, to, insert } = diffReplace(m.text, el.value);
|
|
596
|
-
if (from === to && insert === "") return;
|
|
597
|
-
editor.setSelection({
|
|
598
|
-
anchor: { blockId: m.blockId, offset: from },
|
|
599
|
-
focus: { blockId: m.blockId, offset: to }
|
|
600
|
-
});
|
|
601
|
-
if (to > from) editor.deleteBackward();
|
|
602
|
-
if (insert) editor.insertText(insert);
|
|
603
|
-
mirrorRef.current = { blockId: m.blockId, text: el.value };
|
|
604
|
-
};
|
|
605
|
-
const onKeyDown = (e) => {
|
|
606
|
-
handleKeyDown(editor, e, { readOnly });
|
|
607
|
-
};
|
|
608
|
-
const commitInput = () => {
|
|
609
|
-
const el = inputRef.current;
|
|
610
|
-
if (!el) return;
|
|
611
|
-
const value = el.value;
|
|
612
|
-
if (value) {
|
|
613
|
-
if (!readOnly) editor.insertText(value);
|
|
614
|
-
el.value = "";
|
|
615
|
-
}
|
|
616
|
-
};
|
|
617
|
-
const onInput = (e) => {
|
|
618
|
-
if (composingRef.current || e.nativeEvent.isComposing) return;
|
|
619
|
-
if (coarse && mirrorRef.current) applyMirrorDiff();
|
|
620
|
-
else commitInput();
|
|
621
|
-
};
|
|
622
|
-
const onCompositionStart = () => {
|
|
623
|
-
composingRef.current = true;
|
|
624
|
-
};
|
|
625
|
-
const onCompositionEnd = (e) => {
|
|
626
|
-
composingRef.current = false;
|
|
627
|
-
const el = inputRef.current;
|
|
628
|
-
if (!el) return;
|
|
629
|
-
if (coarse && mirrorRef.current) {
|
|
630
|
-
applyMirrorDiff();
|
|
631
|
-
} else {
|
|
632
|
-
if (e.data && !readOnly) editor.insertText(e.data);
|
|
633
|
-
el.value = "";
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
const caret = editor.caretRect();
|
|
637
|
-
const inputStyle = {
|
|
638
|
-
position: "absolute",
|
|
639
|
-
left: caret ? caret.x : 0,
|
|
640
|
-
top: caret ? caret.y : 0,
|
|
641
|
-
width: 1,
|
|
642
|
-
height: caret ? caret.height : 16,
|
|
643
|
-
opacity: 0,
|
|
644
|
-
padding: 0,
|
|
645
|
-
border: 0,
|
|
646
|
-
outline: "none",
|
|
647
|
-
resize: "none",
|
|
648
|
-
background: "transparent",
|
|
649
|
-
caretColor: "transparent",
|
|
650
|
-
color: "transparent",
|
|
651
|
-
overflow: "hidden",
|
|
652
|
-
whiteSpace: "pre",
|
|
653
|
-
zIndex: 1
|
|
654
|
-
};
|
|
655
|
-
return /* @__PURE__ */ jsxRuntime.jsx(RenderersProvider, { value: renderers, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
656
|
-
"div",
|
|
657
|
-
{
|
|
658
|
-
className: "ori-scroller",
|
|
659
|
-
ref: scrollerRef,
|
|
660
|
-
onScroll,
|
|
661
|
-
"data-touch-selecting": touchSelecting ? "" : void 0,
|
|
662
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
663
|
-
"div",
|
|
664
|
-
{
|
|
665
|
-
className: "ori-content",
|
|
666
|
-
ref: contentRef,
|
|
667
|
-
style: { maxWidth, marginInline: "auto", position: "relative" },
|
|
668
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
669
|
-
"div",
|
|
670
|
-
{
|
|
671
|
-
className: "ori-canvas",
|
|
672
|
-
style: { position: "relative", width: "100%", height: snapshot.totalHeight },
|
|
673
|
-
children: [
|
|
674
|
-
/* @__PURE__ */ jsxRuntime.jsx(SelectionLayer, { editor, snapshot }),
|
|
675
|
-
snapshot.visible.map((block) => /* @__PURE__ */ jsxRuntime.jsx(BlockView, { editor, block }, block.id)),
|
|
676
|
-
/* @__PURE__ */ jsxRuntime.jsx(CaretLayer, { editor, snapshot, focused }),
|
|
677
|
-
coarse ? /* @__PURE__ */ jsxRuntime.jsx(SelectionHandles, { editor, snapshot, pointToPosition }) : null,
|
|
678
|
-
snapshot.empty && placeholder ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-placeholder", "aria-hidden": true, children: placeholder }) : null,
|
|
679
|
-
!readOnly ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
680
|
-
"textarea",
|
|
681
|
-
{
|
|
682
|
-
ref: inputRef,
|
|
683
|
-
className: "ori-input",
|
|
684
|
-
style: inputStyle,
|
|
685
|
-
spellCheck: false,
|
|
686
|
-
autoCapitalize: "off",
|
|
687
|
-
autoCorrect: "off",
|
|
688
|
-
autoComplete: "off",
|
|
689
|
-
inputMode: "text",
|
|
690
|
-
onKeyDown,
|
|
691
|
-
onInput,
|
|
692
|
-
onCompositionStart,
|
|
693
|
-
onCompositionEnd,
|
|
694
|
-
onFocus: () => setFocused(true),
|
|
695
|
-
onBlur: () => setFocused(false),
|
|
696
|
-
onCopy: (e) => {
|
|
697
|
-
e.preventDefault();
|
|
698
|
-
e.clipboardData.setData("text/plain", editor.getSelectedText());
|
|
699
|
-
},
|
|
700
|
-
onCut: (e) => {
|
|
701
|
-
e.preventDefault();
|
|
702
|
-
e.clipboardData.setData("text/plain", editor.getSelectedText());
|
|
703
|
-
editor.deleteBackward();
|
|
704
|
-
},
|
|
705
|
-
onPaste: (e) => {
|
|
706
|
-
e.preventDefault();
|
|
707
|
-
pasteText(editor, e.clipboardData.getData("text/plain"));
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
) : null
|
|
711
|
-
]
|
|
712
|
-
}
|
|
713
|
-
)
|
|
714
|
-
}
|
|
715
|
-
)
|
|
716
|
-
}
|
|
717
|
-
) }) });
|
|
615
|
+
) : null,
|
|
616
|
+
snapshot.empty && placeholder ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-placeholder", "aria-hidden": true, children: placeholder }) : null
|
|
617
|
+
] }) }) });
|
|
718
618
|
});
|
|
619
|
+
var EMPTY = { blocks: {}, atoms: {} };
|
|
620
|
+
var RenderersContext = react.createContext(EMPTY);
|
|
621
|
+
RenderersContext.Provider;
|
|
622
|
+
var useRenderers = () => react.useContext(RenderersContext);
|
|
719
623
|
|
|
720
624
|
Object.defineProperty(exports, "DEFAULT_TYPOGRAPHY", {
|
|
721
625
|
enumerable: true,
|
|
@@ -761,12 +665,7 @@ Object.defineProperty(exports, "snapshotBlocks", {
|
|
|
761
665
|
enumerable: true,
|
|
762
666
|
get: function () { return oriCore.snapshotBlocks; }
|
|
763
667
|
});
|
|
764
|
-
exports.BlockView = BlockView;
|
|
765
|
-
exports.CaretLayer = CaretLayer;
|
|
766
668
|
exports.NoteEditor = NoteEditor;
|
|
767
|
-
exports.SelectionLayer = SelectionLayer;
|
|
768
|
-
exports.handleKeyDown = handleKeyDown;
|
|
769
|
-
exports.pasteText = pasteText;
|
|
770
669
|
exports.useActiveMarks = useActiveMarks;
|
|
771
670
|
exports.useEditor = useEditor;
|
|
772
671
|
exports.useEditorSnapshot = useEditorSnapshot;
|