Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical-playground] Feature: Add touch support for TableCellResizer #7299

Merged
merged 5 commits into from
Mar 7, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@
.TableCellResizer__resizer {
position: absolute;
}

@media (pointer: coarse) {
.TableCellResizer__resizer {
background-color: #adf;
mix-blend-mode: color;
}
}
148 changes: 96 additions & 52 deletions packages/lexical-playground/src/plugins/TableCellResizer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {calculateZoomLevel, mergeRegister} from '@lexical/utils';
import {$getNearestNodeFromDOMNode, isHTMLElement} from 'lexical';
import * as React from 'react';
import {
MouseEventHandler,
CSSProperties,
PointerEventHandler,
ReactPortal,
useCallback,
useEffect,
Expand All @@ -37,12 +38,12 @@ import {
} from 'react';
import {createPortal} from 'react-dom';

type MousePosition = {
type PointerPosition = {
x: number;
y: number;
};

type MouseDraggingDirection = 'right' | 'bottom';
type PointerDraggingDirection = 'right' | 'bottom';

const MIN_ROW_HEIGHT = 33;
const MIN_COLUMN_WIDTH = 92;
Expand All @@ -51,26 +52,27 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
const targetRef = useRef<HTMLElement | null>(null);
const resizerRef = useRef<HTMLDivElement | null>(null);
const tableRectRef = useRef<ClientRect | null>(null);
const pointerTypeRef = useRef<string | null>(null);
const [hasTable, setHasTable] = useState(false);

const mouseStartPosRef = useRef<MousePosition | null>(null);
const [mouseCurrentPos, updateMouseCurrentPos] =
useState<MousePosition | null>(null);
const pointerStartPosRef = useRef<PointerPosition | null>(null);
const [pointerCurrentPos, updatePointerCurrentPos] =
useState<PointerPosition | null>(null);

const [activeCell, updateActiveCell] = useState<TableDOMCell | null>(null);
const [isMouseDown, updateIsMouseDown] = useState<boolean>(false);
const [isPointerDown, updateIsPointerDown] = useState<boolean>(false);
const [draggingDirection, updateDraggingDirection] =
useState<MouseDraggingDirection | null>(null);
useState<PointerDraggingDirection | null>(null);

const resetState = useCallback(() => {
updateActiveCell(null);
targetRef.current = null;
updateDraggingDirection(null);
mouseStartPosRef.current = null;
pointerStartPosRef.current = null;
tableRectRef.current = null;
}, []);

const isMouseDownOnEvent = (event: MouseEvent) => {
const isPointerDownOnEvent = (event: PointerEvent) => {
return (event.buttons & 1) === 1;
};

Expand Down Expand Up @@ -106,20 +108,23 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
return;
}

const onMouseMove = (event: MouseEvent) => {
const onPointerMove = (event: PointerEvent) => {
if (event.pointerType === 'touch') {
return;
}
const target = event.target;
if (!isHTMLElement(target)) {
return;
}

if (draggingDirection) {
updateMouseCurrentPos({
updatePointerCurrentPos({
x: event.clientX,
y: event.clientY,
});
return;
}
updateIsMouseDown(isMouseDownOnEvent(event));
updateIsPointerDown(isPointerDownOnEvent(event));
if (resizerRef.current && resizerRef.current.contains(target)) {
return;
}
Expand Down Expand Up @@ -159,31 +164,69 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
}
};

const onMouseDown = (event: MouseEvent) => {
updateIsMouseDown(true);
const onPointerDown = (event: PointerEvent) => {
pointerTypeRef.current = event.pointerType;
updateIsPointerDown(true);
};

const onMouseUp = (event: MouseEvent) => {
updateIsMouseDown(false);
const onPointerUp = (event: PointerEvent) => {
updateIsPointerDown(false);
};

const onClick = (event: MouseEvent) => {
const pointerType = pointerTypeRef.current || 'mouse';
if (pointerType === 'touch') {
const pointerEvent = new PointerEvent('pointermove', {
...event,
bubbles: true,
cancelable: true,
});
const target = event.target;
target?.dispatchEvent(pointerEvent);
updateIsPointerDown(false);
}
};

const onTouchMove = (event: TouchEvent) => {
if (draggingDirection) {
event.preventDefault();
event.stopPropagation();
if (!event.touches || event.touches.length === 0) {
return;
}
const touch = event.touches[0];
updatePointerCurrentPos({
x: touch.clientX,
y: touch.clientY,
});
}
};

const resizerContainer = resizerRef.current;
resizerContainer?.addEventListener('touchmove', onTouchMove, {
capture: true,
});

const removeRootListener = editor.registerRootListener(
(rootElement, prevRootElement) => {
prevRootElement?.removeEventListener('mousemove', onMouseMove);
prevRootElement?.removeEventListener('mousedown', onMouseDown);
prevRootElement?.removeEventListener('mouseup', onMouseUp);
rootElement?.addEventListener('mousemove', onMouseMove);
rootElement?.addEventListener('mousedown', onMouseDown);
rootElement?.addEventListener('mouseup', onMouseUp);
prevRootElement?.removeEventListener('pointermove', onPointerMove);
prevRootElement?.removeEventListener('click', onClick);
prevRootElement?.removeEventListener('pointerdown', onPointerDown);
prevRootElement?.removeEventListener('pointerup', onPointerUp);
rootElement?.addEventListener('pointermove', onPointerMove);
rootElement?.addEventListener('click', onClick);
rootElement?.addEventListener('pointerdown', onPointerDown);
rootElement?.addEventListener('pointerup', onPointerUp);
},
);

return () => {
removeRootListener();
resizerContainer?.removeEventListener('touchmove', onTouchMove);
};
}, [activeCell, draggingDirection, editor, resetState, hasTable]);

const isHeightChanging = (direction: MouseDraggingDirection) => {
const isHeightChanging = (direction: PointerDraggingDirection) => {
if (direction === 'bottom') {
return true;
}
Expand Down Expand Up @@ -304,18 +347,18 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
[activeCell, editor],
);

const mouseUpHandler = useCallback(
(direction: MouseDraggingDirection) => {
const handler = (event: MouseEvent) => {
const pointerUpHandler = useCallback(
(direction: PointerDraggingDirection) => {
const handler = (event: PointerEvent) => {
event.preventDefault();
event.stopPropagation();

if (!activeCell) {
throw new Error('TableCellResizer: Expected active cell.');
}

if (mouseStartPosRef.current) {
const {x, y} = mouseStartPosRef.current;
if (pointerStartPosRef.current) {
const {x, y} = pointerStartPosRef.current;

if (activeCell === null) {
return;
Expand All @@ -331,7 +374,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
}

resetState();
document.removeEventListener('mouseup', handler);
document.removeEventListener('pointerup', handler);
}
};
return handler;
Expand All @@ -340,7 +383,9 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
);

const toggleResize = useCallback(
(direction: MouseDraggingDirection): MouseEventHandler<HTMLDivElement> =>
(
direction: PointerDraggingDirection,
): PointerEventHandler<HTMLDivElement> =>
(event) => {
event.preventDefault();
event.stopPropagation();
Expand All @@ -349,67 +394,66 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
throw new Error('TableCellResizer: Expected active cell.');
}

mouseStartPosRef.current = {
pointerStartPosRef.current = {
x: event.clientX,
y: event.clientY,
};
updateMouseCurrentPos(mouseStartPosRef.current);
updatePointerCurrentPos(pointerStartPosRef.current);
updateDraggingDirection(direction);

document.addEventListener('mouseup', mouseUpHandler(direction));
document.addEventListener('pointerup', pointerUpHandler(direction));
},
[activeCell, mouseUpHandler],
[activeCell, pointerUpHandler],
);

const getResizers = useCallback(() => {
if (activeCell) {
const {height, width, top, left} =
activeCell.elem.getBoundingClientRect();
const zoom = calculateZoomLevel(activeCell.elem);
const zoneWidth = 10; // Pixel width of the zone where you can drag the edge
const styles = {
const zoneWidth = 16; // Pixel width of the zone where you can drag the edge
const styles: Record<string, CSSProperties> = {
bottom: {
backgroundColor: 'none',
cursor: 'row-resize',
height: `${zoneWidth}px`,
left: `${window.pageXOffset + left}px`,
top: `${window.pageYOffset + top + height - zoneWidth / 2}px`,
left: `${window.scrollX + left}px`,
top: `${window.scrollY + top + height - zoneWidth / 2}px`,
width: `${width}px`,
},
right: {
backgroundColor: 'none',
cursor: 'col-resize',
height: `${height}px`,
left: `${window.pageXOffset + left + width - zoneWidth / 2}px`,
top: `${window.pageYOffset + top}px`,
left: `${window.scrollX + left + width - zoneWidth / 2}px`,
top: `${window.scrollY + top}px`,
width: `${zoneWidth}px`,
},
};

const tableRect = tableRectRef.current;

if (draggingDirection && mouseCurrentPos && tableRect) {
if (draggingDirection && pointerCurrentPos && tableRect) {
if (isHeightChanging(draggingDirection)) {
styles[draggingDirection].left = `${
window.pageXOffset + tableRect.left
window.scrollX + tableRect.left
}px`;
styles[draggingDirection].top = `${
window.pageYOffset + mouseCurrentPos.y / zoom
window.scrollY + pointerCurrentPos.y / zoom
}px`;
styles[draggingDirection].height = '3px';
styles[draggingDirection].width = `${tableRect.width}px`;
} else {
styles[draggingDirection].top = `${
window.pageYOffset + tableRect.top
}px`;
styles[draggingDirection].top = `${window.scrollY + tableRect.top}px`;
styles[draggingDirection].left = `${
window.pageXOffset + mouseCurrentPos.x / zoom
window.scrollX + pointerCurrentPos.x / zoom
}px`;
styles[draggingDirection].width = '3px';
styles[draggingDirection].height = `${tableRect.height}px`;
}

styles[draggingDirection].backgroundColor = '#adf';
styles[draggingDirection].mixBlendMode = 'unset';
}

return styles;
Expand All @@ -421,23 +465,23 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
right: null,
top: null,
};
}, [activeCell, draggingDirection, mouseCurrentPos]);
}, [activeCell, draggingDirection, pointerCurrentPos]);

const resizerStyles = getResizers();

return (
<div ref={resizerRef}>
{activeCell != null && !isMouseDown && (
{activeCell != null && !isPointerDown && (
<>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.right || undefined}
onMouseDown={toggleResize('right')}
onPointerDown={toggleResize('right')}
/>
<div
className="TableCellResizer__resizer TableCellResizer__ui"
style={resizerStyles.bottom || undefined}
onMouseDown={toggleResize('bottom')}
onPointerDown={toggleResize('bottom')}
/>
</>
)}
Expand Down
Loading