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 all 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 @@ -9,4 +9,12 @@

.TableCellResizer__resizer {
position: absolute;
touch-action: none;
}

@media (pointer: coarse) {
.TableCellResizer__resizer {
background-color: #adf;
mix-blend-mode: color;
}
}
113 changes: 57 additions & 56 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 @@ -53,27 +54,22 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
const tableRectRef = useRef<ClientRect | 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 [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) => {
return (event.buttons & 1) === 1;
};

useEffect(() => {
const tableKeys = new Set<NodeKey>();
return mergeRegister(
Expand Down Expand Up @@ -106,20 +102,21 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
return;
}

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

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

const onMouseDown = (event: MouseEvent) => {
updateIsMouseDown(true);
const onPointerDown = (event: PointerEvent) => {
const isTouchEvent = event.pointerType === 'touch';
if (isTouchEvent) {
onPointerMove(event);
}
};

const onMouseUp = (event: MouseEvent) => {
updateIsMouseDown(false);
};
const resizerContainer = resizerRef.current;
resizerContainer?.addEventListener('pointermove', onPointerMove, {
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('pointerdown', onPointerDown);
rootElement?.addEventListener('pointermove', onPointerMove);
rootElement?.addEventListener('pointerdown', onPointerDown);
},
);

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

const isHeightChanging = (direction: MouseDraggingDirection) => {
const isHeightChanging = (direction: PointerDraggingDirection) => {
if (direction === 'bottom') {
return true;
}
Expand Down Expand Up @@ -304,18 +304,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 +331,7 @@ function TableCellResizer({editor}: {editor: LexicalEditor}): JSX.Element {
}

resetState();
document.removeEventListener('mouseup', handler);
document.removeEventListener('pointerup', handler);
}
};
return handler;
Expand All @@ -340,7 +340,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 +351,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 +422,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 && (
<>
<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