@tanstack/table-core
Version:
Headless UI for building powerful tables & datagrids for TS/JS.
277 lines (275 loc) • 8.93 kB
JavaScript
import { cloneState, makeObjectMap } from "../../utils.js";
import { column_getSize, header_getSize, table_setColumnSizing } from "../column-sizing/columnSizingFeature.utils.js";
//#region src/features/column-resizing/columnResizingFeature.utils.ts
/**
* Creates the default transient column resizing state.
*
* The feature default represents no active drag interaction. Reset APIs use
* this value when `defaultState` is `true`.
*
* @example
* ```ts
* const resizeInfo = getDefaultColumnResizingState()
* ```
*/
function getDefaultColumnResizingState() {
return {
startOffset: null,
startSize: null,
deltaOffset: null,
deltaPercentage: null,
isResizingColumn: false,
columnSizingStart: []
};
}
/**
* Checks whether this column can start a resize interaction.
*
* Both `columnDef.enableResizing` and table `enableColumnResizing` default to
* `true`.
*
* @example
* ```ts
* const canResize = column_getCanResize(column)
* ```
*/
function column_getCanResize(column) {
return (column.columnDef.enableResizing ?? true) && (column.table.options.enableColumnResizing ?? true);
}
/**
* Checks whether this column is the active column resize target.
*
* The value is read from `state.columnResizing.isResizingColumn`.
*
* @example
* ```ts
* const isResizing = column_getIsResizing(column)
* ```
*/
function column_getIsResizing(column) {
return column.table.atoms.columnResizing?.get()?.isResizingColumn === column.id;
}
/**
* Creates the pointer/touch start handler for resizing a header.
*
* The handler records starting sizes for all leaf headers, tracks drag deltas,
* writes transient resize info, and commits column sizes on change or drag end
* depending on `columnResizeMode`.
*
* @example
* ```ts
* const onMouseDown = header_getResizeHandler(header)
* ```
*/
function header_getResizeHandler(header, _contextDocument) {
const column = header.table.getColumn(header.column.id);
const canResize = column_getCanResize(column);
return (event) => {
if (!canResize) return;
if (isTouchStartEvent(event)) {
if (event.touches.length > 1) return;
}
const startSize = header_getSize(header);
const columnSizingStart = header.getLeafHeaders().map((leafHeader) => [leafHeader.column.id, column_getSize(leafHeader.column)]);
const clientX = isTouchStartEvent(event) ? Math.round(event.touches[0].clientX) : event.clientX;
const newColumnSizing = makeObjectMap();
const updateOffset = (eventType, clientXPos) => {
if (typeof clientXPos !== "number") return;
const table = column.table;
const isCommit = table.options.columnResizeMode === "onChange" || eventType === "end";
table._reactivity.batch(() => {
table_setColumnResizing(table, (old) => {
const deltaDirection = table.options.columnResizeDirection === "rtl" ? -1 : 1;
const deltaOffset = (clientXPos - (old.startOffset ?? 0)) * deltaDirection;
const startSize = old.startSize ?? 0;
const deltaPercentage = Math.max(startSize > 0 ? deltaOffset / startSize : 0, -.999999);
if (isCommit) {
const columnSizingStart = old.columnSizingStart;
for (let i = 0; i < columnSizingStart.length; i++) {
const entry = columnSizingStart[i];
const headerSize = entry[1];
newColumnSizing[entry[0]] = Math.round(Math.max(headerSize > 0 ? headerSize + headerSize * deltaPercentage : deltaOffset / columnSizingStart.length, 0) * 100) / 100;
}
}
return {
...old,
deltaOffset,
deltaPercentage
};
});
if (isCommit) table_setColumnSizing(table, (old) => Object.assign(makeObjectMap(), old, newColumnSizing));
});
};
let moveRafId = null;
let hasPendingMove = false;
let latestMoveX;
const flushMove = () => {
if (hasPendingMove) {
hasPendingMove = false;
updateOffset("move", latestMoveX);
moveRafId = requestAnimationFrame(flushMove);
} else moveRafId = null;
};
const onMove = (clientXPos) => {
latestMoveX = clientXPos;
if (typeof requestAnimationFrame !== "function") {
updateOffset("move", clientXPos);
return;
}
if (moveRafId !== null) {
hasPendingMove = true;
return;
}
updateOffset("move", clientXPos);
moveRafId = requestAnimationFrame(flushMove);
};
const onEnd = (clientXPos) => {
if (moveRafId !== null) {
cancelAnimationFrame(moveRafId);
moveRafId = null;
hasPendingMove = false;
}
column.table._reactivity.batch(() => {
updateOffset("end", clientXPos ?? latestMoveX);
table_setColumnResizing(column.table, (old) => ({
...old,
isResizingColumn: false,
startOffset: null,
startSize: null,
deltaOffset: null,
deltaPercentage: null,
columnSizingStart: []
}));
});
};
const contextDocument = _contextDocument || (typeof document !== "undefined" ? document : null);
const mouseEvents = {
moveHandler: (e) => onMove(e.clientX),
upHandler: (e) => {
contextDocument?.removeEventListener("mousemove", mouseEvents.moveHandler);
contextDocument?.removeEventListener("mouseup", mouseEvents.upHandler);
onEnd(e.clientX);
}
};
const touchEvents = {
moveHandler: (touchEvent) => {
if (touchEvent.cancelable) {
touchEvent.preventDefault();
touchEvent.stopPropagation();
}
onMove(touchEvent.touches[0].clientX);
return false;
},
upHandler: (e) => {
removeTouchEvents();
if (e.cancelable) {
e.preventDefault();
e.stopPropagation();
}
onEnd(e.touches[0]?.clientX);
},
cancelHandler: () => {
removeTouchEvents();
onEnd();
}
};
const removeTouchEvents = () => {
contextDocument?.removeEventListener("touchmove", touchEvents.moveHandler);
contextDocument?.removeEventListener("touchend", touchEvents.upHandler);
contextDocument?.removeEventListener("touchcancel", touchEvents.cancelHandler);
};
const passiveIfSupported = passiveEventSupported() ? { passive: false } : false;
if (isTouchStartEvent(event)) {
contextDocument?.addEventListener("touchmove", touchEvents.moveHandler, passiveIfSupported);
contextDocument?.addEventListener("touchend", touchEvents.upHandler, passiveIfSupported);
contextDocument?.addEventListener("touchcancel", touchEvents.cancelHandler, passiveIfSupported);
} else {
contextDocument?.addEventListener("mousemove", mouseEvents.moveHandler, passiveIfSupported);
contextDocument?.addEventListener("mouseup", mouseEvents.upHandler, passiveIfSupported);
}
table_setColumnResizing(column.table, (old) => ({
...old,
startOffset: clientX,
startSize,
deltaOffset: 0,
deltaPercentage: 0,
columnSizingStart,
isResizingColumn: column.id
}));
};
}
/**
* Routes a transient column resizing updater through the table's resize handler.
*
* This state tracks the active drag interaction; committed widths live in
* `columnSizing`.
*
* @example
* ```ts
* table_setColumnResizing(table, (old) => ({ ...old, deltaOffset: 12 }))
* ```
*/
function table_setColumnResizing(table, updater) {
table.options.onColumnResizingChange?.(updater);
}
/**
* Resets `columnResizing` to the configured initial state or feature default.
*
* With no argument, the reset clones `table.initialState.columnResizing` when
* it exists. Passing `true` ignores initial state and resets to the no-drag
* default state.
*
* @example
* ```ts
* table_resetHeaderSizeInfo(table)
* table_resetHeaderSizeInfo(table, true)
* ```
*/
function table_resetHeaderSizeInfo(table, defaultState) {
table_setColumnResizing(table, defaultState ? getDefaultColumnResizingState() : cloneState(table.initialState.columnResizing ?? getDefaultColumnResizingState()));
}
let passiveSupported = null;
/**
* Detects whether the current environment supports passive event listeners.
*
* Column resizing uses this to register pointer and touch listeners with
* `passive: false` only when the environment understands passive options.
*
* @example
* ```ts
* const canUsePassiveListeners = passiveEventSupported()
* ```
*/
function passiveEventSupported() {
if (typeof passiveSupported === "boolean") return passiveSupported;
let supported = false;
try {
const options = { get passive() {
supported = true;
return false;
} };
const noop = () => {};
window.addEventListener("test", noop, options);
window.removeEventListener("test", noop);
} catch (err) {
supported = false;
}
passiveSupported = supported;
return passiveSupported;
}
/**
* Narrows an unknown event to a `touchstart` event.
*
* Column resizing uses this before reading touch coordinates and installing
* touch-specific listeners.
*
* @example
* ```ts
* const isTouch = isTouchStartEvent(event)
* ```
*/
function isTouchStartEvent(e) {
return e.type === "touchstart";
}
//#endregion
export { column_getCanResize, column_getIsResizing, getDefaultColumnResizingState, header_getResizeHandler, isTouchStartEvent, passiveEventSupported, table_resetHeaderSizeInfo, table_setColumnResizing };