@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 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 onMove = (e) => {
363
- 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;
364
472
  const pos = pointToPosition(e.clientX, e.clientY);
365
- const sel = editor.getSelection();
366
- 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
+ }
367
517
  };
368
- const onUp = () => {
369
- 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();
370
524
  };
371
- window.addEventListener("mousemove", onMove);
372
- 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);
373
529
  return () => {
374
- window.removeEventListener("mousemove", onMove);
375
- window.removeEventListener("mouseup", onUp);
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
- const onMouseDown = (e) => {
383
- if (e.button !== 0) return;
384
- const scroller = scrollerRef.current;
385
- 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;
386
547
  return;
387
548
  }
388
- inputRef.current?.focus();
389
- const pos = pointToPosition(e.clientX, e.clientY);
390
- if (!pos) return;
391
- e.preventDefault();
392
- const sel = editor.getSelection();
393
- if (e.shiftKey && sel) {
394
- editor.setSelection({ anchor: sel.anchor, focus: pos });
395
- } else {
396
- 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
+ }
397
564
  }
398
- 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 };
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
- commitInput();
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("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(
447
656
  "div",
448
657
  {
449
- className: "ori-content",
450
- ref: contentRef,
451
- style: { maxWidth, marginInline: "auto", position: "relative" },
452
- 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(
453
663
  "div",
454
664
  {
455
- className: "ori-canvas",
456
- style: { position: "relative", width: "100%", height: snapshot.totalHeight },
457
- children: [
458
- /* @__PURE__ */ jsxRuntime.jsx(SelectionLayer, { editor, snapshot }),
459
- snapshot.visible.map((block) => /* @__PURE__ */ jsxRuntime.jsx(BlockView, { editor, block }, block.id)),
460
- /* @__PURE__ */ jsxRuntime.jsx(CaretLayer, { editor, snapshot, focused }),
461
- snapshot.empty && placeholder ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ori-placeholder", "aria-hidden": true, children: placeholder }) : null,
462
- !readOnly ? /* @__PURE__ */ jsxRuntime.jsx(
463
- "textarea",
464
- {
465
- ref: inputRef,
466
- className: "ori-input",
467
- style: inputStyle,
468
- spellCheck: false,
469
- autoCapitalize: "off",
470
- autoCorrect: "off",
471
- onKeyDown,
472
- onInput,
473
- onCompositionStart,
474
- onCompositionEnd,
475
- onFocus: () => setFocused(true),
476
- onBlur: () => setFocused(false),
477
- onCopy: (e) => {
478
- e.preventDefault();
479
- e.clipboardData.setData("text/plain", editor.getSelectedText());
480
- },
481
- onCut: (e) => {
482
- e.preventDefault();
483
- e.clipboardData.setData("text/plain", editor.getSelectedText());
484
- editor.deleteBackward();
485
- },
486
- onPaste: (e) => {
487
- e.preventDefault();
488
- pasteText(editor, e.clipboardData.getData("text/plain"));
489
- }
490
- }
491
- ) : null
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", {