@wingleeio/ori-react 0.0.2 → 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 CHANGED
@@ -19,7 +19,8 @@ function useEditor(options = {}) {
19
19
  }
20
20
  react.useEffect(() => {
21
21
  const editor = ref.current;
22
- return () => editor?.destroy();
22
+ editor?.connect();
23
+ return () => editor?.disconnect();
23
24
  }, []);
24
25
  return ref.current;
25
26
  }
@@ -144,6 +145,67 @@ function SelectionLayer({
144
145
  `${r.blockId}:${i}`
145
146
  )) });
146
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
+ }
147
209
  function CaretLayer({
148
210
  editor,
149
211
  snapshot,
@@ -275,6 +337,14 @@ function handleKeyDown(editor, e, opts = {}) {
275
337
  return false;
276
338
  }
277
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
+ }
278
348
  var NoteEditor = react.forwardRef(function NoteEditor2({
279
349
  editor,
280
350
  className,
@@ -294,9 +364,11 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
294
364
  const scrollerRef = react.useRef(null);
295
365
  const contentRef = react.useRef(null);
296
366
  const inputRef = react.useRef(null);
297
- const draggingRef = react.useRef(false);
298
367
  const composingRef = react.useRef(false);
368
+ const mirrorRef = react.useRef(null);
299
369
  const [focused, setFocused] = react.useState(false);
370
+ const [coarse, setCoarse] = react.useState(false);
371
+ const [touchSelecting, setTouchSelecting] = react.useState(false);
300
372
  react.useImperativeHandle(
301
373
  ref,
302
374
  () => ({
@@ -358,43 +430,177 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
358
430
  return editor.positionFromPoint(clientX - rect.left, clientY - rect.top);
359
431
  });
360
432
  react.useEffect(() => {
361
- const onMove = (e) => {
362
- if (!draggingRef.current) return;
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;
363
472
  const pos = pointToPosition(e.clientX, e.clientY);
364
- const sel = editor.getSelection();
365
- if (pos && sel) editor.setSelection({ anchor: sel.anchor, focus: pos });
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
+ }
366
517
  };
367
- const onUp = () => {
368
- draggingRef.current = false;
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();
369
524
  };
370
- window.addEventListener("mousemove", onMove);
371
- window.addEventListener("mouseup", onUp);
525
+ scroller.addEventListener("pointerdown", onDown, { passive: false });
526
+ window.addEventListener("pointermove", onMove, { passive: false });
527
+ window.addEventListener("pointerup", onUp);
528
+ window.addEventListener("pointercancel", end);
372
529
  return () => {
373
- window.removeEventListener("mousemove", onMove);
374
- window.removeEventListener("mouseup", onUp);
530
+ scroller.removeEventListener("pointerdown", onDown);
531
+ window.removeEventListener("pointermove", onMove);
532
+ window.removeEventListener("pointerup", onUp);
533
+ window.removeEventListener("pointercancel", end);
375
534
  };
376
535
  }, [editor, pointToPosition]);
377
536
  const onScroll = () => {
378
537
  const scroller = scrollerRef.current;
379
538
  if (scroller) editor.setViewport(scroller.scrollTop, scroller.clientHeight);
380
539
  };
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) {
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;
385
547
  return;
386
548
  }
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);
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
+ }
396
564
  }
397
- draggingRef.current = true;
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 };
398
604
  };
399
605
  const onKeyDown = (e) => {
400
606
  handleKeyDown(editor, e, { readOnly });
@@ -410,7 +616,8 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
410
616
  };
411
617
  const onInput = (e) => {
412
618
  if (composingRef.current || e.nativeEvent.isComposing) return;
413
- commitInput();
619
+ if (coarse && mirrorRef.current) applyMirrorDiff();
620
+ else commitInput();
414
621
  };
415
622
  const onCompositionStart = () => {
416
623
  composingRef.current = true;
@@ -418,7 +625,10 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
418
625
  const onCompositionEnd = (e) => {
419
626
  composingRef.current = false;
420
627
  const el = inputRef.current;
421
- if (el) {
628
+ if (!el) return;
629
+ if (coarse && mirrorRef.current) {
630
+ applyMirrorDiff();
631
+ } else {
422
632
  if (e.data && !readOnly) editor.insertText(e.data);
423
633
  el.value = "";
424
634
  }
@@ -442,57 +652,69 @@ var NoteEditor = react.forwardRef(function NoteEditor2({
442
652
  whiteSpace: "pre",
443
653
  zIndex: 1
444
654
  };
445
- return /* @__PURE__ */ jsxRuntime.jsx(RenderersProvider, { value: renderers, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: `ori-root${className ? ` ${className}` : ""}`, style, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-scroller", ref: scrollerRef, onScroll, onMouseDown, 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(
446
656
  "div",
447
657
  {
448
- className: "ori-content",
449
- ref: contentRef,
450
- style: { maxWidth, marginInline: "auto", position: "relative" },
451
- children: /* @__PURE__ */ jsxRuntime.jsxs(
658
+ className: "ori-scroller",
659
+ ref: scrollerRef,
660
+ onScroll,
661
+ "data-touch-selecting": touchSelecting ? "" : void 0,
662
+ children: /* @__PURE__ */ jsxRuntime.jsx(
452
663
  "div",
453
664
  {
454
- className: "ori-canvas",
455
- style: { position: "relative", width: "100%", height: snapshot.totalHeight },
456
- children: [
457
- /* @__PURE__ */ jsxRuntime.jsx(SelectionLayer, { editor, snapshot }),
458
- snapshot.visible.map((block) => /* @__PURE__ */ jsxRuntime.jsx(BlockView, { editor, block }, block.id)),
459
- /* @__PURE__ */ jsxRuntime.jsx(CaretLayer, { editor, snapshot, focused }),
460
- snapshot.empty && placeholder ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-placeholder", "aria-hidden": true, children: placeholder }) : null,
461
- !readOnly ? /* @__PURE__ */ jsxRuntime.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
- ]
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
+ )
492
714
  }
493
715
  )
494
716
  }
495
- ) }) }) });
717
+ ) }) });
496
718
  });
497
719
 
498
720
  Object.defineProperty(exports, "DEFAULT_TYPOGRAPHY", {