@lexical/table 0.47.0 → 0.47.1-nightly.20260713.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LexicalTable.dev.js +185 -32
- package/dist/LexicalTable.dev.mjs +185 -32
- package/dist/LexicalTable.prod.js +1 -1
- package/dist/LexicalTable.prod.mjs +1 -1
- package/package.json +7 -7
- package/src/LexicalTableSelectionHelpers.ts +232 -36
package/dist/LexicalTable.dev.js
CHANGED
|
@@ -2497,6 +2497,27 @@ function $getTableNodeByKeyOrThrow(key) {
|
|
|
2497
2497
|
const isPointerDownOnEvent = event => {
|
|
2498
2498
|
return (event.buttons & 1) === 1;
|
|
2499
2499
|
};
|
|
2500
|
+
|
|
2501
|
+
// Distance (px) from a scroll container edge at which drag auto-scroll kicks
|
|
2502
|
+
// in, and the maximum scroll delta applied per animation frame.
|
|
2503
|
+
const AUTO_SCROLL_EDGE_ZONE = 40;
|
|
2504
|
+
const AUTO_SCROLL_MAX_STEP = 18;
|
|
2505
|
+
|
|
2506
|
+
// Given a pointer position and the start/end edges of a scroll container on one
|
|
2507
|
+
// axis, return the signed per-frame scroll delta: negative near the start edge,
|
|
2508
|
+
// positive near the end edge, 0 while outside both edge zones. The delta ramps
|
|
2509
|
+
// up the deeper the pointer is into the zone (and is capped once it reaches or
|
|
2510
|
+
// passes the edge).
|
|
2511
|
+
function autoScrollStep(pos, start, end) {
|
|
2512
|
+
const speed = depth => Math.max(1, Math.ceil(Math.min(AUTO_SCROLL_EDGE_ZONE, depth) / AUTO_SCROLL_EDGE_ZONE * AUTO_SCROLL_MAX_STEP));
|
|
2513
|
+
if (pos <= start + AUTO_SCROLL_EDGE_ZONE) {
|
|
2514
|
+
return -speed(start + AUTO_SCROLL_EDGE_ZONE - pos);
|
|
2515
|
+
}
|
|
2516
|
+
if (pos >= end - AUTO_SCROLL_EDGE_ZONE) {
|
|
2517
|
+
return speed(pos - (end - AUTO_SCROLL_EDGE_ZONE));
|
|
2518
|
+
}
|
|
2519
|
+
return 0;
|
|
2520
|
+
}
|
|
2500
2521
|
function isHTMLTableElement(el) {
|
|
2501
2522
|
return lexical.isHTMLElement(el) && el.nodeName === 'TABLE';
|
|
2502
2523
|
}
|
|
@@ -2588,22 +2609,177 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
|
|
|
2588
2609
|
tableObserver.$setAnchorCellForSelection(startingCell);
|
|
2589
2610
|
});
|
|
2590
2611
|
}
|
|
2591
|
-
|
|
2612
|
+
let lastClientX = event.clientX;
|
|
2613
|
+
let lastClientY = event.clientY;
|
|
2614
|
+
let autoScrollRafId = null;
|
|
2615
|
+
const stopSelecting = () => {
|
|
2592
2616
|
tableObserver.isSelecting = false;
|
|
2617
|
+
if (autoScrollRafId !== null) {
|
|
2618
|
+
editorWindow.cancelAnimationFrame(autoScrollRafId);
|
|
2619
|
+
autoScrollRafId = null;
|
|
2620
|
+
}
|
|
2593
2621
|
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
2594
2622
|
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
2595
2623
|
};
|
|
2624
|
+
|
|
2625
|
+
// Resolve the table cell under the given viewport coordinates via the
|
|
2626
|
+
// table's own root so elementsFromPoint isn't retargeted; narrow with the
|
|
2627
|
+
// type guards rather than casting so the detached-table case (Node) falls
|
|
2628
|
+
// through to no hit-test.
|
|
2629
|
+
const resolveFocusCellFromPoint = (clientX, clientY) => {
|
|
2630
|
+
const tableRoot = tableElement.getRootNode();
|
|
2631
|
+
if (!lexical.isDOMDocumentNode(tableRoot) && !lexical.isDOMShadowRoot(tableRoot)) {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
for (const el of tableRoot.elementsFromPoint(clientX, clientY)) {
|
|
2635
|
+
const cell = getDOMCellInTableFromTarget(tableElement, el);
|
|
2636
|
+
if (cell) {
|
|
2637
|
+
return cell;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return null;
|
|
2641
|
+
};
|
|
2642
|
+
const applyFocusCell = (focusCell, override) => {
|
|
2643
|
+
// Fallback: set anchor if still missing (handles race conditions)
|
|
2644
|
+
if (tableObserver.anchorCell === null) {
|
|
2645
|
+
editor.update(() => {
|
|
2646
|
+
tableObserver.$setAnchorCellForSelection(focusCell);
|
|
2647
|
+
});
|
|
2648
|
+
}
|
|
2649
|
+
if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
|
|
2650
|
+
tableObservers.setNextFocus({
|
|
2651
|
+
focusCell,
|
|
2652
|
+
override,
|
|
2653
|
+
tableKey: tableObserver.tableNodeKey
|
|
2654
|
+
});
|
|
2655
|
+
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
|
|
2656
|
+
}
|
|
2657
|
+
};
|
|
2658
|
+
|
|
2659
|
+
// Walk up from the table to the nearest ancestor that can actually scroll
|
|
2660
|
+
// on the requested axis (the scrollable-tables wrapper for 'x'). Returns
|
|
2661
|
+
// null when none is found, in which case the caller may fall back to the
|
|
2662
|
+
// window.
|
|
2663
|
+
const findScrollContainer = axis => {
|
|
2664
|
+
for (let el = tableElement.parentElement; el; el = el.parentElement) {
|
|
2665
|
+
const canScroll = axis === 'x' ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight;
|
|
2666
|
+
if (canScroll) {
|
|
2667
|
+
const style = editorWindow.getComputedStyle(el);
|
|
2668
|
+
const overflow = axis === 'x' ? style.overflowX : style.overflowY;
|
|
2669
|
+
if (overflow === 'auto' || overflow === 'scroll') {
|
|
2670
|
+
return el;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return null;
|
|
2675
|
+
};
|
|
2676
|
+
|
|
2677
|
+
// Scroll `container` (or the window when null) on `axis` if the pointer is
|
|
2678
|
+
// within the edge zone. Returns whether it actually scrolled.
|
|
2679
|
+
const scrollAxis = (container, pos, axis) => {
|
|
2680
|
+
let start;
|
|
2681
|
+
let end;
|
|
2682
|
+
if (container === null) {
|
|
2683
|
+
start = 0;
|
|
2684
|
+
end = axis === 'x' ? editorWindow.innerWidth : editorWindow.innerHeight;
|
|
2685
|
+
} else {
|
|
2686
|
+
const rect = container.getBoundingClientRect();
|
|
2687
|
+
start = axis === 'x' ? rect.left : rect.top;
|
|
2688
|
+
end = axis === 'x' ? rect.right : rect.bottom;
|
|
2689
|
+
}
|
|
2690
|
+
const step = autoScrollStep(pos, start, end);
|
|
2691
|
+
if (step === 0) {
|
|
2692
|
+
return false;
|
|
2693
|
+
}
|
|
2694
|
+
if (container === null) {
|
|
2695
|
+
const before = axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY;
|
|
2696
|
+
editorWindow.scrollBy(axis === 'x' ? step : 0, axis === 'x' ? 0 : step);
|
|
2697
|
+
return (axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY) !== before;
|
|
2698
|
+
}
|
|
2699
|
+
if (axis === 'x') {
|
|
2700
|
+
const before = container.scrollLeft;
|
|
2701
|
+
container.scrollLeft += step;
|
|
2702
|
+
return container.scrollLeft !== before;
|
|
2703
|
+
}
|
|
2704
|
+
const before = container.scrollTop;
|
|
2705
|
+
container.scrollTop += step;
|
|
2706
|
+
return container.scrollTop !== before;
|
|
2707
|
+
};
|
|
2708
|
+
|
|
2709
|
+
// Clamp the last pointer position into the visible bounds of the scroll
|
|
2710
|
+
// container(s) so a pointer dragged past an edge still hit-tests onto the
|
|
2711
|
+
// newly-revealed cell instead of empty space beyond the table.
|
|
2712
|
+
const clampHitPoint = (hContainer, vContainer) => {
|
|
2713
|
+
let x = lastClientX;
|
|
2714
|
+
let y = lastClientY;
|
|
2715
|
+
if (hContainer === null) {
|
|
2716
|
+
x = Math.min(Math.max(x, 1), editorWindow.innerWidth - 1);
|
|
2717
|
+
} else {
|
|
2718
|
+
const rect = hContainer.getBoundingClientRect();
|
|
2719
|
+
x = Math.min(Math.max(x, rect.left + 1), rect.right - 1);
|
|
2720
|
+
}
|
|
2721
|
+
if (vContainer === null) {
|
|
2722
|
+
y = Math.min(Math.max(y, 1), editorWindow.innerHeight - 1);
|
|
2723
|
+
} else {
|
|
2724
|
+
const rect = vContainer.getBoundingClientRect();
|
|
2725
|
+
y = Math.min(Math.max(y, rect.top + 1), rect.bottom - 1);
|
|
2726
|
+
}
|
|
2727
|
+
return [x, y];
|
|
2728
|
+
};
|
|
2729
|
+
const isNearScrollEdge = () => {
|
|
2730
|
+
const hContainer = findScrollContainer('x');
|
|
2731
|
+
if (hContainer !== null) {
|
|
2732
|
+
const rect = hContainer.getBoundingClientRect();
|
|
2733
|
+
if (autoScrollStep(lastClientX, rect.left, rect.right) !== 0) {
|
|
2734
|
+
return true;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
const vContainer = findScrollContainer('y');
|
|
2738
|
+
const vStart = vContainer === null ? 0 : vContainer.getBoundingClientRect().top;
|
|
2739
|
+
const vEnd = vContainer === null ? editorWindow.innerHeight : vContainer.getBoundingClientRect().bottom;
|
|
2740
|
+
return autoScrollStep(lastClientY, vStart, vEnd) !== 0;
|
|
2741
|
+
};
|
|
2742
|
+
const tickAutoScroll = () => {
|
|
2743
|
+
autoScrollRafId = null;
|
|
2744
|
+
if (!tableObserver.isSelecting) {
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const hContainer = findScrollContainer('x');
|
|
2748
|
+
const vContainer = findScrollContainer('y');
|
|
2749
|
+
// Only the table's own wrapper scrolls horizontally; pages don't
|
|
2750
|
+
// auto-scroll sideways. Vertically we fall back to the window.
|
|
2751
|
+
const scrolledX = hContainer !== null && scrollAxis(hContainer, lastClientX, 'x');
|
|
2752
|
+
const scrolledY = scrollAxis(vContainer, lastClientY, 'y');
|
|
2753
|
+
if (scrolledX || scrolledY) {
|
|
2754
|
+
const [hitX, hitY] = clampHitPoint(hContainer, vContainer);
|
|
2755
|
+
const focusCell = resolveFocusCellFromPoint(hitX, hitY);
|
|
2756
|
+
if (focusCell) {
|
|
2757
|
+
applyFocusCell(focusCell, false);
|
|
2758
|
+
}
|
|
2759
|
+
autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
|
|
2760
|
+
}
|
|
2761
|
+
};
|
|
2762
|
+
const maybeStartAutoScroll = () => {
|
|
2763
|
+
// Touch taps don't initiate table selection, so they shouldn't scroll.
|
|
2764
|
+
if (autoScrollRafId !== null || tableObserver.pointerType === 'touch' || !isNearScrollEdge()) {
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
|
|
2768
|
+
};
|
|
2769
|
+
const onPointerUp = () => {
|
|
2770
|
+
stopSelecting();
|
|
2771
|
+
};
|
|
2596
2772
|
const onPointerMove = moveEvent => {
|
|
2597
2773
|
if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
|
|
2598
|
-
|
|
2599
|
-
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
2600
|
-
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
2774
|
+
stopSelecting();
|
|
2601
2775
|
return;
|
|
2602
2776
|
}
|
|
2603
2777
|
const moveTarget = lexical.getComposedEventTarget(moveEvent);
|
|
2604
2778
|
if (!lexical.isDOMNode(moveTarget)) {
|
|
2605
2779
|
return;
|
|
2606
2780
|
}
|
|
2781
|
+
lastClientX = moveEvent.clientX;
|
|
2782
|
+
lastClientY = moveEvent.clientY;
|
|
2607
2783
|
let focusCell = null;
|
|
2608
2784
|
// In firefox the moveEvent.target may be captured so we must always
|
|
2609
2785
|
// consult the coordinates #7245
|
|
@@ -2611,37 +2787,14 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
|
|
|
2611
2787
|
if (override) {
|
|
2612
2788
|
focusCell = getDOMCellInTableFromTarget(tableElement, moveTarget);
|
|
2613
2789
|
} else {
|
|
2614
|
-
|
|
2615
|
-
// retargeted; narrow with the type guards rather than casting so the
|
|
2616
|
-
// detached-table case (Node) falls through to no hit-test.
|
|
2617
|
-
const tableRoot = tableElement.getRootNode();
|
|
2618
|
-
if (!lexical.isDOMDocumentNode(tableRoot) && !lexical.isDOMShadowRoot(tableRoot)) {
|
|
2619
|
-
return;
|
|
2620
|
-
}
|
|
2621
|
-
for (const el of tableRoot.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
|
|
2622
|
-
focusCell = getDOMCellInTableFromTarget(tableElement, el);
|
|
2623
|
-
if (focusCell) {
|
|
2624
|
-
break;
|
|
2625
|
-
}
|
|
2626
|
-
}
|
|
2790
|
+
focusCell = resolveFocusCellFromPoint(moveEvent.clientX, moveEvent.clientY);
|
|
2627
2791
|
}
|
|
2628
2792
|
if (focusCell) {
|
|
2629
|
-
|
|
2630
|
-
// Fallback: set anchor if still missing (handles race conditions)
|
|
2631
|
-
if (tableObserver.anchorCell === null) {
|
|
2632
|
-
editor.update(() => {
|
|
2633
|
-
tableObserver.$setAnchorCellForSelection(anchorCell);
|
|
2634
|
-
});
|
|
2635
|
-
}
|
|
2636
|
-
if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
|
|
2637
|
-
tableObservers.setNextFocus({
|
|
2638
|
-
focusCell,
|
|
2639
|
-
override,
|
|
2640
|
-
tableKey: tableObserver.tableNodeKey
|
|
2641
|
-
});
|
|
2642
|
-
editor.dispatchCommand(lexical.SELECTION_CHANGE_COMMAND, undefined);
|
|
2643
|
-
}
|
|
2793
|
+
applyFocusCell(focusCell, override);
|
|
2644
2794
|
}
|
|
2795
|
+
// Keep the selection reachable when dragging toward/past an edge of a
|
|
2796
|
+
// scrollable table (#7153).
|
|
2797
|
+
maybeStartAutoScroll();
|
|
2645
2798
|
};
|
|
2646
2799
|
editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
|
|
2647
2800
|
editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
|
|
@@ -2495,6 +2495,27 @@ function $getTableNodeByKeyOrThrow(key) {
|
|
|
2495
2495
|
const isPointerDownOnEvent = event => {
|
|
2496
2496
|
return (event.buttons & 1) === 1;
|
|
2497
2497
|
};
|
|
2498
|
+
|
|
2499
|
+
// Distance (px) from a scroll container edge at which drag auto-scroll kicks
|
|
2500
|
+
// in, and the maximum scroll delta applied per animation frame.
|
|
2501
|
+
const AUTO_SCROLL_EDGE_ZONE = 40;
|
|
2502
|
+
const AUTO_SCROLL_MAX_STEP = 18;
|
|
2503
|
+
|
|
2504
|
+
// Given a pointer position and the start/end edges of a scroll container on one
|
|
2505
|
+
// axis, return the signed per-frame scroll delta: negative near the start edge,
|
|
2506
|
+
// positive near the end edge, 0 while outside both edge zones. The delta ramps
|
|
2507
|
+
// up the deeper the pointer is into the zone (and is capped once it reaches or
|
|
2508
|
+
// passes the edge).
|
|
2509
|
+
function autoScrollStep(pos, start, end) {
|
|
2510
|
+
const speed = depth => Math.max(1, Math.ceil(Math.min(AUTO_SCROLL_EDGE_ZONE, depth) / AUTO_SCROLL_EDGE_ZONE * AUTO_SCROLL_MAX_STEP));
|
|
2511
|
+
if (pos <= start + AUTO_SCROLL_EDGE_ZONE) {
|
|
2512
|
+
return -speed(start + AUTO_SCROLL_EDGE_ZONE - pos);
|
|
2513
|
+
}
|
|
2514
|
+
if (pos >= end - AUTO_SCROLL_EDGE_ZONE) {
|
|
2515
|
+
return speed(pos - (end - AUTO_SCROLL_EDGE_ZONE));
|
|
2516
|
+
}
|
|
2517
|
+
return 0;
|
|
2518
|
+
}
|
|
2498
2519
|
function isHTMLTableElement(el) {
|
|
2499
2520
|
return isHTMLElement(el) && el.nodeName === 'TABLE';
|
|
2500
2521
|
}
|
|
@@ -2586,22 +2607,177 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
|
|
|
2586
2607
|
tableObserver.$setAnchorCellForSelection(startingCell);
|
|
2587
2608
|
});
|
|
2588
2609
|
}
|
|
2589
|
-
|
|
2610
|
+
let lastClientX = event.clientX;
|
|
2611
|
+
let lastClientY = event.clientY;
|
|
2612
|
+
let autoScrollRafId = null;
|
|
2613
|
+
const stopSelecting = () => {
|
|
2590
2614
|
tableObserver.isSelecting = false;
|
|
2615
|
+
if (autoScrollRafId !== null) {
|
|
2616
|
+
editorWindow.cancelAnimationFrame(autoScrollRafId);
|
|
2617
|
+
autoScrollRafId = null;
|
|
2618
|
+
}
|
|
2591
2619
|
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
2592
2620
|
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
2593
2621
|
};
|
|
2622
|
+
|
|
2623
|
+
// Resolve the table cell under the given viewport coordinates via the
|
|
2624
|
+
// table's own root so elementsFromPoint isn't retargeted; narrow with the
|
|
2625
|
+
// type guards rather than casting so the detached-table case (Node) falls
|
|
2626
|
+
// through to no hit-test.
|
|
2627
|
+
const resolveFocusCellFromPoint = (clientX, clientY) => {
|
|
2628
|
+
const tableRoot = tableElement.getRootNode();
|
|
2629
|
+
if (!isDOMDocumentNode(tableRoot) && !isDOMShadowRoot(tableRoot)) {
|
|
2630
|
+
return null;
|
|
2631
|
+
}
|
|
2632
|
+
for (const el of tableRoot.elementsFromPoint(clientX, clientY)) {
|
|
2633
|
+
const cell = getDOMCellInTableFromTarget(tableElement, el);
|
|
2634
|
+
if (cell) {
|
|
2635
|
+
return cell;
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
return null;
|
|
2639
|
+
};
|
|
2640
|
+
const applyFocusCell = (focusCell, override) => {
|
|
2641
|
+
// Fallback: set anchor if still missing (handles race conditions)
|
|
2642
|
+
if (tableObserver.anchorCell === null) {
|
|
2643
|
+
editor.update(() => {
|
|
2644
|
+
tableObserver.$setAnchorCellForSelection(focusCell);
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
|
|
2648
|
+
tableObservers.setNextFocus({
|
|
2649
|
+
focusCell,
|
|
2650
|
+
override,
|
|
2651
|
+
tableKey: tableObserver.tableNodeKey
|
|
2652
|
+
});
|
|
2653
|
+
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
|
|
2657
|
+
// Walk up from the table to the nearest ancestor that can actually scroll
|
|
2658
|
+
// on the requested axis (the scrollable-tables wrapper for 'x'). Returns
|
|
2659
|
+
// null when none is found, in which case the caller may fall back to the
|
|
2660
|
+
// window.
|
|
2661
|
+
const findScrollContainer = axis => {
|
|
2662
|
+
for (let el = tableElement.parentElement; el; el = el.parentElement) {
|
|
2663
|
+
const canScroll = axis === 'x' ? el.scrollWidth > el.clientWidth : el.scrollHeight > el.clientHeight;
|
|
2664
|
+
if (canScroll) {
|
|
2665
|
+
const style = editorWindow.getComputedStyle(el);
|
|
2666
|
+
const overflow = axis === 'x' ? style.overflowX : style.overflowY;
|
|
2667
|
+
if (overflow === 'auto' || overflow === 'scroll') {
|
|
2668
|
+
return el;
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
return null;
|
|
2673
|
+
};
|
|
2674
|
+
|
|
2675
|
+
// Scroll `container` (or the window when null) on `axis` if the pointer is
|
|
2676
|
+
// within the edge zone. Returns whether it actually scrolled.
|
|
2677
|
+
const scrollAxis = (container, pos, axis) => {
|
|
2678
|
+
let start;
|
|
2679
|
+
let end;
|
|
2680
|
+
if (container === null) {
|
|
2681
|
+
start = 0;
|
|
2682
|
+
end = axis === 'x' ? editorWindow.innerWidth : editorWindow.innerHeight;
|
|
2683
|
+
} else {
|
|
2684
|
+
const rect = container.getBoundingClientRect();
|
|
2685
|
+
start = axis === 'x' ? rect.left : rect.top;
|
|
2686
|
+
end = axis === 'x' ? rect.right : rect.bottom;
|
|
2687
|
+
}
|
|
2688
|
+
const step = autoScrollStep(pos, start, end);
|
|
2689
|
+
if (step === 0) {
|
|
2690
|
+
return false;
|
|
2691
|
+
}
|
|
2692
|
+
if (container === null) {
|
|
2693
|
+
const before = axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY;
|
|
2694
|
+
editorWindow.scrollBy(axis === 'x' ? step : 0, axis === 'x' ? 0 : step);
|
|
2695
|
+
return (axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY) !== before;
|
|
2696
|
+
}
|
|
2697
|
+
if (axis === 'x') {
|
|
2698
|
+
const before = container.scrollLeft;
|
|
2699
|
+
container.scrollLeft += step;
|
|
2700
|
+
return container.scrollLeft !== before;
|
|
2701
|
+
}
|
|
2702
|
+
const before = container.scrollTop;
|
|
2703
|
+
container.scrollTop += step;
|
|
2704
|
+
return container.scrollTop !== before;
|
|
2705
|
+
};
|
|
2706
|
+
|
|
2707
|
+
// Clamp the last pointer position into the visible bounds of the scroll
|
|
2708
|
+
// container(s) so a pointer dragged past an edge still hit-tests onto the
|
|
2709
|
+
// newly-revealed cell instead of empty space beyond the table.
|
|
2710
|
+
const clampHitPoint = (hContainer, vContainer) => {
|
|
2711
|
+
let x = lastClientX;
|
|
2712
|
+
let y = lastClientY;
|
|
2713
|
+
if (hContainer === null) {
|
|
2714
|
+
x = Math.min(Math.max(x, 1), editorWindow.innerWidth - 1);
|
|
2715
|
+
} else {
|
|
2716
|
+
const rect = hContainer.getBoundingClientRect();
|
|
2717
|
+
x = Math.min(Math.max(x, rect.left + 1), rect.right - 1);
|
|
2718
|
+
}
|
|
2719
|
+
if (vContainer === null) {
|
|
2720
|
+
y = Math.min(Math.max(y, 1), editorWindow.innerHeight - 1);
|
|
2721
|
+
} else {
|
|
2722
|
+
const rect = vContainer.getBoundingClientRect();
|
|
2723
|
+
y = Math.min(Math.max(y, rect.top + 1), rect.bottom - 1);
|
|
2724
|
+
}
|
|
2725
|
+
return [x, y];
|
|
2726
|
+
};
|
|
2727
|
+
const isNearScrollEdge = () => {
|
|
2728
|
+
const hContainer = findScrollContainer('x');
|
|
2729
|
+
if (hContainer !== null) {
|
|
2730
|
+
const rect = hContainer.getBoundingClientRect();
|
|
2731
|
+
if (autoScrollStep(lastClientX, rect.left, rect.right) !== 0) {
|
|
2732
|
+
return true;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
const vContainer = findScrollContainer('y');
|
|
2736
|
+
const vStart = vContainer === null ? 0 : vContainer.getBoundingClientRect().top;
|
|
2737
|
+
const vEnd = vContainer === null ? editorWindow.innerHeight : vContainer.getBoundingClientRect().bottom;
|
|
2738
|
+
return autoScrollStep(lastClientY, vStart, vEnd) !== 0;
|
|
2739
|
+
};
|
|
2740
|
+
const tickAutoScroll = () => {
|
|
2741
|
+
autoScrollRafId = null;
|
|
2742
|
+
if (!tableObserver.isSelecting) {
|
|
2743
|
+
return;
|
|
2744
|
+
}
|
|
2745
|
+
const hContainer = findScrollContainer('x');
|
|
2746
|
+
const vContainer = findScrollContainer('y');
|
|
2747
|
+
// Only the table's own wrapper scrolls horizontally; pages don't
|
|
2748
|
+
// auto-scroll sideways. Vertically we fall back to the window.
|
|
2749
|
+
const scrolledX = hContainer !== null && scrollAxis(hContainer, lastClientX, 'x');
|
|
2750
|
+
const scrolledY = scrollAxis(vContainer, lastClientY, 'y');
|
|
2751
|
+
if (scrolledX || scrolledY) {
|
|
2752
|
+
const [hitX, hitY] = clampHitPoint(hContainer, vContainer);
|
|
2753
|
+
const focusCell = resolveFocusCellFromPoint(hitX, hitY);
|
|
2754
|
+
if (focusCell) {
|
|
2755
|
+
applyFocusCell(focusCell, false);
|
|
2756
|
+
}
|
|
2757
|
+
autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
|
|
2758
|
+
}
|
|
2759
|
+
};
|
|
2760
|
+
const maybeStartAutoScroll = () => {
|
|
2761
|
+
// Touch taps don't initiate table selection, so they shouldn't scroll.
|
|
2762
|
+
if (autoScrollRafId !== null || tableObserver.pointerType === 'touch' || !isNearScrollEdge()) {
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
|
|
2766
|
+
};
|
|
2767
|
+
const onPointerUp = () => {
|
|
2768
|
+
stopSelecting();
|
|
2769
|
+
};
|
|
2594
2770
|
const onPointerMove = moveEvent => {
|
|
2595
2771
|
if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
|
|
2596
|
-
|
|
2597
|
-
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
2598
|
-
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
2772
|
+
stopSelecting();
|
|
2599
2773
|
return;
|
|
2600
2774
|
}
|
|
2601
2775
|
const moveTarget = getComposedEventTarget(moveEvent);
|
|
2602
2776
|
if (!isDOMNode(moveTarget)) {
|
|
2603
2777
|
return;
|
|
2604
2778
|
}
|
|
2779
|
+
lastClientX = moveEvent.clientX;
|
|
2780
|
+
lastClientY = moveEvent.clientY;
|
|
2605
2781
|
let focusCell = null;
|
|
2606
2782
|
// In firefox the moveEvent.target may be captured so we must always
|
|
2607
2783
|
// consult the coordinates #7245
|
|
@@ -2609,37 +2785,14 @@ function $handleTableClick(editor, event, selectedDOMCell, tableElement, tableOb
|
|
|
2609
2785
|
if (override) {
|
|
2610
2786
|
focusCell = getDOMCellInTableFromTarget(tableElement, moveTarget);
|
|
2611
2787
|
} else {
|
|
2612
|
-
|
|
2613
|
-
// retargeted; narrow with the type guards rather than casting so the
|
|
2614
|
-
// detached-table case (Node) falls through to no hit-test.
|
|
2615
|
-
const tableRoot = tableElement.getRootNode();
|
|
2616
|
-
if (!isDOMDocumentNode(tableRoot) && !isDOMShadowRoot(tableRoot)) {
|
|
2617
|
-
return;
|
|
2618
|
-
}
|
|
2619
|
-
for (const el of tableRoot.elementsFromPoint(moveEvent.clientX, moveEvent.clientY)) {
|
|
2620
|
-
focusCell = getDOMCellInTableFromTarget(tableElement, el);
|
|
2621
|
-
if (focusCell) {
|
|
2622
|
-
break;
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2788
|
+
focusCell = resolveFocusCellFromPoint(moveEvent.clientX, moveEvent.clientY);
|
|
2625
2789
|
}
|
|
2626
2790
|
if (focusCell) {
|
|
2627
|
-
|
|
2628
|
-
// Fallback: set anchor if still missing (handles race conditions)
|
|
2629
|
-
if (tableObserver.anchorCell === null) {
|
|
2630
|
-
editor.update(() => {
|
|
2631
|
-
tableObserver.$setAnchorCellForSelection(anchorCell);
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
if (tableObserver.focusCell === null || focusCell.elem !== tableObserver.focusCell.elem) {
|
|
2635
|
-
tableObservers.setNextFocus({
|
|
2636
|
-
focusCell,
|
|
2637
|
-
override,
|
|
2638
|
-
tableKey: tableObserver.tableNodeKey
|
|
2639
|
-
});
|
|
2640
|
-
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
2641
|
-
}
|
|
2791
|
+
applyFocusCell(focusCell, override);
|
|
2642
2792
|
}
|
|
2793
|
+
// Keep the selection reachable when dragging toward/past an edge of a
|
|
2794
|
+
// scrollable table (#7153).
|
|
2795
|
+
maybeStartAutoScroll();
|
|
2643
2796
|
};
|
|
2644
2797
|
editorWindow.addEventListener('pointerup', onPointerUp, tableObserver.listenerOptions);
|
|
2645
2798
|
editorWindow.addEventListener('pointermove', onPointerMove, tableObserver.listenerOptions);
|