@wingleeio/ori-react 0.0.3 → 0.0.4
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 +292 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +292 -71
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/styles.css +61 -0
package/dist/index.cjs
CHANGED
|
@@ -145,6 +145,67 @@ function SelectionLayer({
|
|
|
145
145
|
`${r.blockId}:${i}`
|
|
146
146
|
)) });
|
|
147
147
|
}
|
|
148
|
+
function SelectionHandles({
|
|
149
|
+
editor,
|
|
150
|
+
snapshot,
|
|
151
|
+
pointToPosition
|
|
152
|
+
}) {
|
|
153
|
+
void snapshot.revision;
|
|
154
|
+
const drag = react.useRef(null);
|
|
155
|
+
const sel = snapshot.selection;
|
|
156
|
+
if (!sel || oriCore.isCollapsed(sel)) return null;
|
|
157
|
+
const range = editor.orderedSelection();
|
|
158
|
+
if (!range) return null;
|
|
159
|
+
const rects = editor.selectionRectsForViewport();
|
|
160
|
+
if (rects.length === 0) return null;
|
|
161
|
+
const first = rects[0];
|
|
162
|
+
const last = rects[rects.length - 1];
|
|
163
|
+
const start = (which) => (e) => {
|
|
164
|
+
const r = editor.orderedSelection();
|
|
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
|
+
]
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ori-handles", "aria-hidden": true, children: [
|
|
205
|
+
handle("start", first.x, first.y, first.height),
|
|
206
|
+
handle("end", last.x + last.width, last.y, last.height)
|
|
207
|
+
] });
|
|
208
|
+
}
|
|
148
209
|
function CaretLayer({
|
|
149
210
|
editor,
|
|
150
211
|
snapshot,
|
|
@@ -276,6 +337,14 @@ function handleKeyDown(editor, e, opts = {}) {
|
|
|
276
337
|
return false;
|
|
277
338
|
}
|
|
278
339
|
}
|
|
340
|
+
function diffReplace(oldText, newText) {
|
|
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
|
+
}
|
|
279
348
|
var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
280
349
|
editor,
|
|
281
350
|
className,
|
|
@@ -295,9 +364,11 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
|
295
364
|
const scrollerRef = react.useRef(null);
|
|
296
365
|
const contentRef = react.useRef(null);
|
|
297
366
|
const inputRef = react.useRef(null);
|
|
298
|
-
const draggingRef = react.useRef(false);
|
|
299
367
|
const composingRef = react.useRef(false);
|
|
368
|
+
const mirrorRef = react.useRef(null);
|
|
300
369
|
const [focused, setFocused] = react.useState(false);
|
|
370
|
+
const [coarse, setCoarse] = react.useState(false);
|
|
371
|
+
const [touchSelecting, setTouchSelecting] = react.useState(false);
|
|
301
372
|
react.useImperativeHandle(
|
|
302
373
|
ref,
|
|
303
374
|
() => ({
|
|
@@ -359,43 +430,177 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
|
359
430
|
return editor.positionFromPoint(clientX - rect.left, clientY - rect.top);
|
|
360
431
|
});
|
|
361
432
|
react.useEffect(() => {
|
|
362
|
-
const
|
|
363
|
-
|
|
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;
|
|
364
472
|
const pos = pointToPosition(e.clientX, e.clientY);
|
|
365
|
-
const
|
|
366
|
-
|
|
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
|
+
}
|
|
367
517
|
};
|
|
368
|
-
const onUp = () => {
|
|
369
|
-
|
|
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();
|
|
370
524
|
};
|
|
371
|
-
|
|
372
|
-
window.addEventListener("
|
|
525
|
+
scroller.addEventListener("pointerdown", onDown, { passive: false });
|
|
526
|
+
window.addEventListener("pointermove", onMove, { passive: false });
|
|
527
|
+
window.addEventListener("pointerup", onUp);
|
|
528
|
+
window.addEventListener("pointercancel", end);
|
|
373
529
|
return () => {
|
|
374
|
-
|
|
375
|
-
window.removeEventListener("
|
|
530
|
+
scroller.removeEventListener("pointerdown", onDown);
|
|
531
|
+
window.removeEventListener("pointermove", onMove);
|
|
532
|
+
window.removeEventListener("pointerup", onUp);
|
|
533
|
+
window.removeEventListener("pointercancel", end);
|
|
376
534
|
};
|
|
377
535
|
}, [editor, pointToPosition]);
|
|
378
536
|
const onScroll = () => {
|
|
379
537
|
const scroller = scrollerRef.current;
|
|
380
538
|
if (scroller) editor.setViewport(scroller.scrollTop, scroller.clientHeight);
|
|
381
539
|
};
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
const
|
|
385
|
-
if (
|
|
540
|
+
react.useEffect(() => {
|
|
541
|
+
if (!coarse) return;
|
|
542
|
+
const el = inputRef.current;
|
|
543
|
+
if (!el || composingRef.current) return;
|
|
544
|
+
const sel = editor.getSelection();
|
|
545
|
+
if (!sel) {
|
|
546
|
+
mirrorRef.current = null;
|
|
386
547
|
return;
|
|
387
548
|
}
|
|
388
|
-
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
549
|
+
const blockId = sel.focus.blockId;
|
|
550
|
+
const text = editor.getBlockText(blockId);
|
|
551
|
+
const pendingNativeInput = document.activeElement === el && el.value !== text && el.value !== mirrorRef.current?.text;
|
|
552
|
+
if (!pendingNativeInput) {
|
|
553
|
+
if (el.value !== text) el.value = text;
|
|
554
|
+
const a = sel.anchor.blockId === blockId ? sel.anchor.offset : sel.focus.offset;
|
|
555
|
+
const f = sel.focus.offset;
|
|
556
|
+
const start = Math.min(a, f);
|
|
557
|
+
const end = Math.max(a, f);
|
|
558
|
+
if (el.selectionStart !== start || el.selectionEnd !== end) {
|
|
559
|
+
try {
|
|
560
|
+
el.setSelectionRange(start, end, f < a ? "backward" : "forward");
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
}
|
|
397
564
|
}
|
|
398
|
-
|
|
565
|
+
mirrorRef.current = { blockId, text };
|
|
566
|
+
}, [coarse, editor, snapshot.revision]);
|
|
567
|
+
react.useEffect(() => {
|
|
568
|
+
if (!coarse) return;
|
|
569
|
+
const onSelChange = () => {
|
|
570
|
+
const el = inputRef.current;
|
|
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;
|
|
582
|
+
}
|
|
583
|
+
editor.setSelection({
|
|
584
|
+
anchor: { blockId: m.blockId, offset: anchorOff },
|
|
585
|
+
focus: { blockId: m.blockId, offset: focusOff }
|
|
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 };
|
|
399
604
|
};
|
|
400
605
|
const onKeyDown = (e) => {
|
|
401
606
|
handleKeyDown(editor, e, { readOnly });
|
|
@@ -411,7 +616,8 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
|
411
616
|
};
|
|
412
617
|
const onInput = (e) => {
|
|
413
618
|
if (composingRef.current || e.nativeEvent.isComposing) return;
|
|
414
|
-
|
|
619
|
+
if (coarse && mirrorRef.current) applyMirrorDiff();
|
|
620
|
+
else commitInput();
|
|
415
621
|
};
|
|
416
622
|
const onCompositionStart = () => {
|
|
417
623
|
composingRef.current = true;
|
|
@@ -419,7 +625,10 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
|
419
625
|
const onCompositionEnd = (e) => {
|
|
420
626
|
composingRef.current = false;
|
|
421
627
|
const el = inputRef.current;
|
|
422
|
-
if (el)
|
|
628
|
+
if (!el) return;
|
|
629
|
+
if (coarse && mirrorRef.current) {
|
|
630
|
+
applyMirrorDiff();
|
|
631
|
+
} else {
|
|
423
632
|
if (e.data && !readOnly) editor.insertText(e.data);
|
|
424
633
|
el.value = "";
|
|
425
634
|
}
|
|
@@ -443,57 +652,69 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
|
|
|
443
652
|
whiteSpace: "pre",
|
|
444
653
|
zIndex: 1
|
|
445
654
|
};
|
|
446
|
-
return /* @__PURE__ */ jsxRuntime.jsx(RenderersProvider, { value: renderers, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
655
|
+
return /* @__PURE__ */ jsxRuntime.jsx(RenderersProvider, { value: renderers, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
447
656
|
"div",
|
|
448
657
|
{
|
|
449
|
-
className: "ori-
|
|
450
|
-
ref:
|
|
451
|
-
|
|
452
|
-
|
|
658
|
+
className: "ori-scroller",
|
|
659
|
+
ref: scrollerRef,
|
|
660
|
+
onScroll,
|
|
661
|
+
"data-touch-selecting": touchSelecting ? "" : void 0,
|
|
662
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
453
663
|
"div",
|
|
454
664
|
{
|
|
455
|
-
className: "ori-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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
|
+
)
|
|
493
714
|
}
|
|
494
715
|
)
|
|
495
716
|
}
|
|
496
|
-
) }) })
|
|
717
|
+
) }) });
|
|
497
718
|
});
|
|
498
719
|
|
|
499
720
|
Object.defineProperty(exports, "DEFAULT_TYPOGRAPHY", {
|