@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
|
@@ -124,6 +124,34 @@ const isPointerDownOnEvent = (event: PointerEvent) => {
|
|
|
124
124
|
return (event.buttons & 1) === 1;
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
// Distance (px) from a scroll container edge at which drag auto-scroll kicks
|
|
128
|
+
// in, and the maximum scroll delta applied per animation frame.
|
|
129
|
+
const AUTO_SCROLL_EDGE_ZONE = 40;
|
|
130
|
+
const AUTO_SCROLL_MAX_STEP = 18;
|
|
131
|
+
|
|
132
|
+
// Given a pointer position and the start/end edges of a scroll container on one
|
|
133
|
+
// axis, return the signed per-frame scroll delta: negative near the start edge,
|
|
134
|
+
// positive near the end edge, 0 while outside both edge zones. The delta ramps
|
|
135
|
+
// up the deeper the pointer is into the zone (and is capped once it reaches or
|
|
136
|
+
// passes the edge).
|
|
137
|
+
function autoScrollStep(pos: number, start: number, end: number): number {
|
|
138
|
+
const speed = (depth: number) =>
|
|
139
|
+
Math.max(
|
|
140
|
+
1,
|
|
141
|
+
Math.ceil(
|
|
142
|
+
(Math.min(AUTO_SCROLL_EDGE_ZONE, depth) / AUTO_SCROLL_EDGE_ZONE) *
|
|
143
|
+
AUTO_SCROLL_MAX_STEP,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
if (pos <= start + AUTO_SCROLL_EDGE_ZONE) {
|
|
147
|
+
return -speed(start + AUTO_SCROLL_EDGE_ZONE - pos);
|
|
148
|
+
}
|
|
149
|
+
if (pos >= end - AUTO_SCROLL_EDGE_ZONE) {
|
|
150
|
+
return speed(pos - (end - AUTO_SCROLL_EDGE_ZONE));
|
|
151
|
+
}
|
|
152
|
+
return 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
127
155
|
export function isHTMLTableElement(el: unknown): el is HTMLTableElement {
|
|
128
156
|
return isHTMLElement(el) && el.nodeName === 'TABLE';
|
|
129
157
|
}
|
|
@@ -274,23 +302,217 @@ function $handleTableClick(
|
|
|
274
302
|
});
|
|
275
303
|
}
|
|
276
304
|
|
|
277
|
-
|
|
305
|
+
let lastClientX = event.clientX;
|
|
306
|
+
let lastClientY = event.clientY;
|
|
307
|
+
let autoScrollRafId: number | null = null;
|
|
308
|
+
|
|
309
|
+
const stopSelecting = () => {
|
|
278
310
|
tableObserver.isSelecting = false;
|
|
311
|
+
if (autoScrollRafId !== null) {
|
|
312
|
+
editorWindow.cancelAnimationFrame(autoScrollRafId);
|
|
313
|
+
autoScrollRafId = null;
|
|
314
|
+
}
|
|
279
315
|
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
280
316
|
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
281
317
|
};
|
|
282
318
|
|
|
319
|
+
// Resolve the table cell under the given viewport coordinates via the
|
|
320
|
+
// table's own root so elementsFromPoint isn't retargeted; narrow with the
|
|
321
|
+
// type guards rather than casting so the detached-table case (Node) falls
|
|
322
|
+
// through to no hit-test.
|
|
323
|
+
const resolveFocusCellFromPoint = (
|
|
324
|
+
clientX: number,
|
|
325
|
+
clientY: number,
|
|
326
|
+
): TableDOMCell | null => {
|
|
327
|
+
const tableRoot = tableElement.getRootNode();
|
|
328
|
+
if (!isDOMDocumentNode(tableRoot) && !isDOMShadowRoot(tableRoot)) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
for (const el of tableRoot.elementsFromPoint(clientX, clientY)) {
|
|
332
|
+
const cell = getDOMCellInTableFromTarget(tableElement, el);
|
|
333
|
+
if (cell) {
|
|
334
|
+
return cell;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const applyFocusCell = (focusCell: TableDOMCell, override: boolean) => {
|
|
341
|
+
// Fallback: set anchor if still missing (handles race conditions)
|
|
342
|
+
if (tableObserver.anchorCell === null) {
|
|
343
|
+
editor.update(() => {
|
|
344
|
+
tableObserver.$setAnchorCellForSelection(focusCell);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
if (
|
|
348
|
+
tableObserver.focusCell === null ||
|
|
349
|
+
focusCell.elem !== tableObserver.focusCell.elem
|
|
350
|
+
) {
|
|
351
|
+
tableObservers.setNextFocus({
|
|
352
|
+
focusCell,
|
|
353
|
+
override,
|
|
354
|
+
tableKey: tableObserver.tableNodeKey,
|
|
355
|
+
});
|
|
356
|
+
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
// Walk up from the table to the nearest ancestor that can actually scroll
|
|
361
|
+
// on the requested axis (the scrollable-tables wrapper for 'x'). Returns
|
|
362
|
+
// null when none is found, in which case the caller may fall back to the
|
|
363
|
+
// window.
|
|
364
|
+
const findScrollContainer = (axis: 'x' | 'y'): HTMLElement | null => {
|
|
365
|
+
for (
|
|
366
|
+
let el: HTMLElement | null = tableElement.parentElement;
|
|
367
|
+
el;
|
|
368
|
+
el = el.parentElement
|
|
369
|
+
) {
|
|
370
|
+
const canScroll =
|
|
371
|
+
axis === 'x'
|
|
372
|
+
? el.scrollWidth > el.clientWidth
|
|
373
|
+
: el.scrollHeight > el.clientHeight;
|
|
374
|
+
if (canScroll) {
|
|
375
|
+
const style = editorWindow.getComputedStyle(el);
|
|
376
|
+
const overflow = axis === 'x' ? style.overflowX : style.overflowY;
|
|
377
|
+
if (overflow === 'auto' || overflow === 'scroll') {
|
|
378
|
+
return el;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Scroll `container` (or the window when null) on `axis` if the pointer is
|
|
386
|
+
// within the edge zone. Returns whether it actually scrolled.
|
|
387
|
+
const scrollAxis = (
|
|
388
|
+
container: HTMLElement | null,
|
|
389
|
+
pos: number,
|
|
390
|
+
axis: 'x' | 'y',
|
|
391
|
+
): boolean => {
|
|
392
|
+
let start: number;
|
|
393
|
+
let end: number;
|
|
394
|
+
if (container === null) {
|
|
395
|
+
start = 0;
|
|
396
|
+
end = axis === 'x' ? editorWindow.innerWidth : editorWindow.innerHeight;
|
|
397
|
+
} else {
|
|
398
|
+
const rect = container.getBoundingClientRect();
|
|
399
|
+
start = axis === 'x' ? rect.left : rect.top;
|
|
400
|
+
end = axis === 'x' ? rect.right : rect.bottom;
|
|
401
|
+
}
|
|
402
|
+
const step = autoScrollStep(pos, start, end);
|
|
403
|
+
if (step === 0) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (container === null) {
|
|
407
|
+
const before =
|
|
408
|
+
axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY;
|
|
409
|
+
editorWindow.scrollBy(axis === 'x' ? step : 0, axis === 'x' ? 0 : step);
|
|
410
|
+
return (
|
|
411
|
+
(axis === 'x' ? editorWindow.scrollX : editorWindow.scrollY) !==
|
|
412
|
+
before
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (axis === 'x') {
|
|
416
|
+
const before = container.scrollLeft;
|
|
417
|
+
container.scrollLeft += step;
|
|
418
|
+
return container.scrollLeft !== before;
|
|
419
|
+
}
|
|
420
|
+
const before = container.scrollTop;
|
|
421
|
+
container.scrollTop += step;
|
|
422
|
+
return container.scrollTop !== before;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Clamp the last pointer position into the visible bounds of the scroll
|
|
426
|
+
// container(s) so a pointer dragged past an edge still hit-tests onto the
|
|
427
|
+
// newly-revealed cell instead of empty space beyond the table.
|
|
428
|
+
const clampHitPoint = (
|
|
429
|
+
hContainer: HTMLElement | null,
|
|
430
|
+
vContainer: HTMLElement | null,
|
|
431
|
+
): [number, number] => {
|
|
432
|
+
let x = lastClientX;
|
|
433
|
+
let y = lastClientY;
|
|
434
|
+
if (hContainer === null) {
|
|
435
|
+
x = Math.min(Math.max(x, 1), editorWindow.innerWidth - 1);
|
|
436
|
+
} else {
|
|
437
|
+
const rect = hContainer.getBoundingClientRect();
|
|
438
|
+
x = Math.min(Math.max(x, rect.left + 1), rect.right - 1);
|
|
439
|
+
}
|
|
440
|
+
if (vContainer === null) {
|
|
441
|
+
y = Math.min(Math.max(y, 1), editorWindow.innerHeight - 1);
|
|
442
|
+
} else {
|
|
443
|
+
const rect = vContainer.getBoundingClientRect();
|
|
444
|
+
y = Math.min(Math.max(y, rect.top + 1), rect.bottom - 1);
|
|
445
|
+
}
|
|
446
|
+
return [x, y];
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const isNearScrollEdge = (): boolean => {
|
|
450
|
+
const hContainer = findScrollContainer('x');
|
|
451
|
+
if (hContainer !== null) {
|
|
452
|
+
const rect = hContainer.getBoundingClientRect();
|
|
453
|
+
if (autoScrollStep(lastClientX, rect.left, rect.right) !== 0) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const vContainer = findScrollContainer('y');
|
|
458
|
+
const vStart =
|
|
459
|
+
vContainer === null ? 0 : vContainer.getBoundingClientRect().top;
|
|
460
|
+
const vEnd =
|
|
461
|
+
vContainer === null
|
|
462
|
+
? editorWindow.innerHeight
|
|
463
|
+
: vContainer.getBoundingClientRect().bottom;
|
|
464
|
+
return autoScrollStep(lastClientY, vStart, vEnd) !== 0;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const tickAutoScroll = () => {
|
|
468
|
+
autoScrollRafId = null;
|
|
469
|
+
if (!tableObserver.isSelecting) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const hContainer = findScrollContainer('x');
|
|
473
|
+
const vContainer = findScrollContainer('y');
|
|
474
|
+
// Only the table's own wrapper scrolls horizontally; pages don't
|
|
475
|
+
// auto-scroll sideways. Vertically we fall back to the window.
|
|
476
|
+
const scrolledX =
|
|
477
|
+
hContainer !== null && scrollAxis(hContainer, lastClientX, 'x');
|
|
478
|
+
const scrolledY = scrollAxis(vContainer, lastClientY, 'y');
|
|
479
|
+
if (scrolledX || scrolledY) {
|
|
480
|
+
const [hitX, hitY] = clampHitPoint(hContainer, vContainer);
|
|
481
|
+
const focusCell = resolveFocusCellFromPoint(hitX, hitY);
|
|
482
|
+
if (focusCell) {
|
|
483
|
+
applyFocusCell(focusCell, false);
|
|
484
|
+
}
|
|
485
|
+
autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const maybeStartAutoScroll = () => {
|
|
490
|
+
// Touch taps don't initiate table selection, so they shouldn't scroll.
|
|
491
|
+
if (
|
|
492
|
+
autoScrollRafId !== null ||
|
|
493
|
+
tableObserver.pointerType === 'touch' ||
|
|
494
|
+
!isNearScrollEdge()
|
|
495
|
+
) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
autoScrollRafId = editorWindow.requestAnimationFrame(tickAutoScroll);
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
const onPointerUp = () => {
|
|
502
|
+
stopSelecting();
|
|
503
|
+
};
|
|
504
|
+
|
|
283
505
|
const onPointerMove = (moveEvent: PointerEvent) => {
|
|
284
506
|
if (!isPointerDownOnEvent(moveEvent) && tableObserver.isSelecting) {
|
|
285
|
-
|
|
286
|
-
editorWindow.removeEventListener('pointerup', onPointerUp);
|
|
287
|
-
editorWindow.removeEventListener('pointermove', onPointerMove);
|
|
507
|
+
stopSelecting();
|
|
288
508
|
return;
|
|
289
509
|
}
|
|
290
510
|
const moveTarget = getComposedEventTarget(moveEvent);
|
|
291
511
|
if (!isDOMNode(moveTarget)) {
|
|
292
512
|
return;
|
|
293
513
|
}
|
|
514
|
+
lastClientX = moveEvent.clientX;
|
|
515
|
+
lastClientY = moveEvent.clientY;
|
|
294
516
|
let focusCell: null | TableDOMCell = null;
|
|
295
517
|
// In firefox the moveEvent.target may be captured so we must always
|
|
296
518
|
// consult the coordinates #7245
|
|
@@ -298,43 +520,17 @@ function $handleTableClick(
|
|
|
298
520
|
if (override) {
|
|
299
521
|
focusCell = getDOMCellInTableFromTarget(tableElement, moveTarget);
|
|
300
522
|
} else {
|
|
301
|
-
|
|
302
|
-
// retargeted; narrow with the type guards rather than casting so the
|
|
303
|
-
// detached-table case (Node) falls through to no hit-test.
|
|
304
|
-
const tableRoot = tableElement.getRootNode();
|
|
305
|
-
if (!isDOMDocumentNode(tableRoot) && !isDOMShadowRoot(tableRoot)) {
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
for (const el of tableRoot.elementsFromPoint(
|
|
523
|
+
focusCell = resolveFocusCellFromPoint(
|
|
309
524
|
moveEvent.clientX,
|
|
310
525
|
moveEvent.clientY,
|
|
311
|
-
)
|
|
312
|
-
focusCell = getDOMCellInTableFromTarget(tableElement, el);
|
|
313
|
-
if (focusCell) {
|
|
314
|
-
break;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
526
|
+
);
|
|
317
527
|
}
|
|
318
528
|
if (focusCell) {
|
|
319
|
-
|
|
320
|
-
// Fallback: set anchor if still missing (handles race conditions)
|
|
321
|
-
if (tableObserver.anchorCell === null) {
|
|
322
|
-
editor.update(() => {
|
|
323
|
-
tableObserver.$setAnchorCellForSelection(anchorCell);
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
if (
|
|
327
|
-
tableObserver.focusCell === null ||
|
|
328
|
-
focusCell.elem !== tableObserver.focusCell.elem
|
|
329
|
-
) {
|
|
330
|
-
tableObservers.setNextFocus({
|
|
331
|
-
focusCell,
|
|
332
|
-
override,
|
|
333
|
-
tableKey: tableObserver.tableNodeKey,
|
|
334
|
-
});
|
|
335
|
-
editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined);
|
|
336
|
-
}
|
|
529
|
+
applyFocusCell(focusCell, override);
|
|
337
530
|
}
|
|
531
|
+
// Keep the selection reachable when dragging toward/past an edge of a
|
|
532
|
+
// scrollable table (#7153).
|
|
533
|
+
maybeStartAutoScroll();
|
|
338
534
|
};
|
|
339
535
|
editorWindow.addEventListener(
|
|
340
536
|
'pointerup',
|