diff --git a/apps/roam/src/components/results-view/ResultsTable.tsx b/apps/roam/src/components/results-view/ResultsTable.tsx index f5f9e1d8d..172206a74 100644 --- a/apps/roam/src/components/results-view/ResultsTable.tsx +++ b/apps/roam/src/components/results-view/ResultsTable.tsx @@ -41,9 +41,8 @@ const ExtraContextRow = ({ uid }: { uid: string }) => { ); }; -const dragImage = document.createElement("img"); -dragImage.src = - "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; +const COLUMN_RESIZING_CLASS = "roamjs-query-column-resizing"; +const MIN_COLUMN_WIDTH = 40; const ResultHeader = React.forwardRef< Record, @@ -174,9 +173,10 @@ const CellRender = ({ content, uid }: { content: string; uid: string }) => { type ResultRowProps = { r: Result; columns: Column[]; - onDragStart: (e: React.DragEvent) => void; - onDrag: (e: React.DragEvent) => void; - onDragEnd: (e: React.DragEvent) => void; + onResizeStart: (e: React.PointerEvent) => void; + onResize: (e: React.PointerEvent) => void; + onResizeEnd: (e: React.PointerEvent) => void; + onResizeLostPointerCapture: (e: React.PointerEvent) => void; parentUid: string; ctrlClick?: (e: Result) => void; views: { column: string; mode: string; value: string }[]; @@ -189,9 +189,10 @@ const ResultRow = ({ parentUid, ctrlClick, views, - onDragStart, - onDrag, - onDragEnd, + onResizeStart, + onResize, + onResizeEnd, + onResizeLostPointerCapture, onRefresh, }: ResultRowProps) => { const storedRelationsEnabled = getStoredRelationsEnabled(); @@ -392,18 +393,16 @@ const ResultRow = ({ right: 0, bottom: 0, background: `rgba(16,22,26,0.15)`, + touchAction: "none", }} data-left-column-uid={columnUid} data-right-column-uid={columns[i + 1].uid} data-column={columnUid} - draggable - onDragStart={(e) => { - e.dataTransfer.setData("text/plain", ""); - e.dataTransfer.setDragImage(dragImage, 0, 0); - onDragStart(e); - }} - onDrag={onDrag} - onDragEnd={onDragEnd} + onPointerDown={onResizeStart} + onPointerMove={onResize} + onPointerUp={onResizeEnd} + onPointerCancel={onResizeEnd} + onLostPointerCapture={onResizeLostPointerCapture} /> )} @@ -419,13 +418,25 @@ type ColumnWidths = { }; type DragInfo = { + pointerId: number | null; startX: number; - leftColumnUid: string | null; - rightColumnUid: string | null; + moved: boolean; + leftHeader: HTMLElement | null; + rightHeader: HTMLElement | null; leftStartWidth: number; rightStartWidth: number; }; +const getInitialDragInfo = (): DragInfo => ({ + pointerId: null, + startX: 0, + moved: false, + leftHeader: null, + rightHeader: null, + leftStartWidth: 0, + rightStartWidth: 0, +}); + const ResultsTable = ({ columns, results, @@ -457,13 +468,7 @@ const ResultsTable = ({ showInterface?: boolean; }) => { const tableRef = useRef(null); - const dragInfo = useRef({ - startX: 0, - leftColumnUid: null, - rightColumnUid: null, - leftStartWidth: 0, - rightStartWidth: 0, - }); + const dragInfo = useRef(getInitialDragInfo()); const viewsByColumn = useMemo( () => Object.fromEntries(views.map((v) => [v.column, v])), @@ -477,28 +482,8 @@ const ResultsTable = ({ return filtered.length ? filtered : columns; }, [columns, viewsByColumn]); - const rafIdRef = useRef(null); - const throttledSetColumnWidths = useCallback((update: ColumnWidths) => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - } - rafIdRef.current = requestAnimationFrame(() => - setColumnWidths( - (prev) => - ({ - ...prev, - ...update, - }) as ColumnWidths, - ), - ); - }, []); - useEffect(() => { - return () => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - } - }; + return () => document.body.classList.remove(COLUMN_RESIZING_CLASS); }, []); const [columnWidths, setColumnWidths] = useState(() => { @@ -515,119 +500,154 @@ const ResultsTable = ({ return allWidths; }); - const onDragStart = useCallback((e: React.DragEvent) => { + const onResizeStart = useCallback((e: React.PointerEvent) => { + if (e.button !== 0) return; + if (dragInfo.current.pointerId !== null) return; const { leftColumnUid, rightColumnUid } = e.currentTarget.dataset; if (!leftColumnUid || !rightColumnUid || !tableRef.current) return; - const leftHeader = tableRef.current?.querySelector( + const leftHeader = tableRef.current.querySelector( `thead td[data-column="${leftColumnUid}"]`, ); - const rightHeader = tableRef.current?.querySelector( + const rightHeader = tableRef.current.querySelector( `thead td[data-column="${rightColumnUid}"]`, ); if (!leftHeader || !rightHeader) return; + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.setPointerCapture(e.pointerId); + document.body.classList.add(COLUMN_RESIZING_CLASS); + dragInfo.current = { + pointerId: e.pointerId, startX: e.clientX, - leftColumnUid, - rightColumnUid, - leftStartWidth: (leftHeader as HTMLElement).offsetWidth, - rightStartWidth: (rightHeader as HTMLElement).offsetWidth, + moved: false, + leftHeader, + rightHeader, + leftStartWidth: leftHeader.offsetWidth, + rightStartWidth: rightHeader.offsetWidth, }; }, []); - const onDrag = useCallback((e: React.DragEvent) => { - if (e.clientX === 0) return; + const onResize = useCallback((e: React.PointerEvent) => { + if (dragInfo.current.pointerId !== e.pointerId) return; + e.preventDefault(); + e.stopPropagation(); - const { - startX, - leftColumnUid, - rightColumnUid, - leftStartWidth, - rightStartWidth, - } = dragInfo.current; + const { startX, leftHeader, rightHeader, leftStartWidth, rightStartWidth } = + dragInfo.current; - if (!leftColumnUid || !rightColumnUid) return; + if (!leftHeader || !rightHeader) return; const delta = e.clientX - startX; - const minWidth = 40; + if (delta !== 0) dragInfo.current.moved = true; let newLeftWidth = leftStartWidth + delta; let newRightWidth = rightStartWidth - delta; - const leftBelow = newLeftWidth < minWidth; - const rightBelow = newRightWidth < minWidth; + const leftBelow = newLeftWidth < MIN_COLUMN_WIDTH; + const rightBelow = newRightWidth < MIN_COLUMN_WIDTH; if (leftBelow && !rightBelow) { - const adjustment = minWidth - newLeftWidth; - newLeftWidth = minWidth; + const adjustment = MIN_COLUMN_WIDTH - newLeftWidth; + newLeftWidth = MIN_COLUMN_WIDTH; newRightWidth -= adjustment; } else if (rightBelow && !leftBelow) { - const adjustment = minWidth - newRightWidth; - newRightWidth = minWidth; + const adjustment = MIN_COLUMN_WIDTH - newRightWidth; + newRightWidth = MIN_COLUMN_WIDTH; newLeftWidth -= adjustment; } else if (leftBelow && rightBelow) { - const totalMin = minWidth * 2; + const totalMin = MIN_COLUMN_WIDTH * 2; const startTotal = leftStartWidth + rightStartWidth; if (startTotal > totalMin) { const scale = totalMin / startTotal; - newLeftWidth = Math.max(minWidth, leftStartWidth * scale); - newRightWidth = Math.max(minWidth, rightStartWidth * scale); + newLeftWidth = Math.max(MIN_COLUMN_WIDTH, leftStartWidth * scale); + newRightWidth = Math.max(MIN_COLUMN_WIDTH, rightStartWidth * scale); } else { newLeftWidth = leftStartWidth; newRightWidth = rightStartWidth; } } - throttledSetColumnWidths({ - [leftColumnUid]: `${newLeftWidth}px`, - [rightColumnUid]: `${newRightWidth}px`, - }); + leftHeader.style.width = `${newLeftWidth}px`; + rightHeader.style.width = `${newRightWidth}px`; }, []); - const onDragEnd = useCallback(() => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; - } + const finishResize = useCallback( + ({ + pointerId, + resizeHandle, + }: { + pointerId: number; + resizeHandle?: HTMLDivElement; + }) => { + const currentDrag = dragInfo.current; + if (currentDrag.pointerId !== pointerId) return; + + dragInfo.current = getInitialDragInfo(); + + if (resizeHandle?.hasPointerCapture(pointerId)) { + resizeHandle.releasePointerCapture(pointerId); + } - const totalWidth = tableRef.current?.offsetWidth; - if (!totalWidth || totalWidth === 0) { - return; - } - const minWidth = 40; - const minPercent = (minWidth / totalWidth) * 100; - - const finalWidths: ColumnWidths = { ...columnWidths }; - const uids = visibleColumns.map((c) => c.uid); - uids.forEach((uid) => { - const header = tableRef.current?.querySelector( - `thead td[data-column="${uid}"]`, - ); - if (header) { - const headerWidth = (header as HTMLElement).offsetWidth; - if (headerWidth > 0) { - const percent = (headerWidth / totalWidth) * 100; - finalWidths[uid] = `${Math.max(minPercent, percent)}%`; - } else { - finalWidths[uid] = columnWidths[uid] || "5%"; - } + document.body.classList.remove(COLUMN_RESIZING_CLASS); + if (!currentDrag.moved) return; + + const totalWidth = tableRef.current?.offsetWidth; + if (!totalWidth || totalWidth === 0) { + return; } - }); - setColumnWidths(finalWidths); - - if (preventSavingSettings) return; - const layoutUid = getSubTree({ parentUid, key: "layout" }).uid; - if (layoutUid) { - setInputSettings({ - blockUid: layoutUid, - key: "widths", - values: Object.entries(finalWidths).map(([k, v]) => `${k} - ${v}`), + const minPercent = (MIN_COLUMN_WIDTH / totalWidth) * 100; + + const finalWidths: ColumnWidths = { ...columnWidths }; + const uids = visibleColumns.map((c) => c.uid); + uids.forEach((uid) => { + const header = tableRef.current?.querySelector( + `thead td[data-column="${uid}"]`, + ); + if (header) { + const headerElement = header as HTMLElement; + const headerWidth = headerElement.offsetWidth; + let finalWidth = columnWidths[uid] || "5%"; + if (headerWidth > 0) { + const percent = (headerWidth / totalWidth) * 100; + finalWidth = `${Math.max(minPercent, percent)}%`; + } + finalWidths[uid] = finalWidth; + headerElement.style.width = finalWidth; + } }); - } - }, [parentUid, columnWidths, visibleColumns, preventSavingSettings]); + setColumnWidths(finalWidths); + + if (preventSavingSettings) return; + const layoutUid = getSubTree({ parentUid, key: "layout" }).uid; + if (layoutUid) { + setInputSettings({ + blockUid: layoutUid, + key: "widths", + values: Object.entries(finalWidths).map(([k, v]) => `${k} - ${v}`), + }); + } + }, + [parentUid, columnWidths, visibleColumns, preventSavingSettings], + ); + const onResizeEnd = useCallback( + (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + finishResize({ pointerId: e.pointerId, resizeHandle: e.currentTarget }); + }, + [finishResize], + ); + const onResizeLostPointerCapture = useCallback( + (e: React.PointerEvent) => { + finishResize({ pointerId: e.pointerId }); + }, + [finishResize], + ); const resultHeaderSetFilters = React.useCallback( (fs: FilterData) => { @@ -752,9 +772,10 @@ const ResultsTable = ({ views={views} onRefresh={onRefresh} columns={visibleColumns} - onDragStart={onDragStart} - onDrag={onDrag} - onDragEnd={onDragEnd} + onResizeStart={onResizeStart} + onResize={onResize} + onResizeEnd={onResizeEnd} + onResizeLostPointerCapture={onResizeLostPointerCapture} /> {extraRowUid === r.uid && ( diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index ef6ccf4f8..bee0461fd 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -36,6 +36,12 @@ background: #ffff00; } +body.roamjs-query-column-resizing, +body.roamjs-query-column-resizing * { + cursor: ew-resize !important; + user-select: none; +} + .roamjs-query-embed .rm-block-separator { display: none; }