- Add drag threshold and layout frame stride to splitter drag utility - Use onPreview callback to animate splitter offset via translate3d during drag - Simplify resize class and cleanup logic in missing browser methods - Replace deferred release UI cleanup with direct activation/cleanup calls - Update CSS to add resizing overlay and remove transition during resize - Add test covering compositor preview behavior and CSS constraints
135 lines
4.2 KiB
JavaScript
135 lines
4.2 KiB
JavaScript
/**
|
|
* Helper to initiate a splitter drag interaction.
|
|
* Handles mouse/pointer event listening, touch/cancellation, and RAF throttling.
|
|
*/
|
|
export function startSplitterDrag(event, {
|
|
anchor = 'right',
|
|
startWidth,
|
|
bounds = { min: 100, max: 800 },
|
|
dragThreshold = 0,
|
|
layoutFrameStride = 1,
|
|
onBeforeDrag = null,
|
|
onPreview = null,
|
|
onDrag = () => {},
|
|
onEnd = () => {}
|
|
}) {
|
|
if (event?.button !== undefined && event.button !== 0) return null;
|
|
event?.preventDefault?.();
|
|
event?.stopPropagation?.();
|
|
|
|
const startX = event.clientX;
|
|
const movementThreshold = Math.max(0, Number(dragThreshold) || 0);
|
|
const frameStride = Math.max(1, Math.round(Number(layoutFrameStride) || 1));
|
|
let pendingWidth = startWidth;
|
|
let appliedWidth = startWidth;
|
|
let animationFrame = null;
|
|
let isDragging = true;
|
|
let didDrag = false;
|
|
let frameCount = 0;
|
|
|
|
const moveEvent = event?.type === 'pointerdown' ? 'pointermove' : 'mousemove';
|
|
const upEvent = event?.type === 'pointerdown' ? 'pointerup' : 'mouseup';
|
|
const cancelEvent = event?.type === 'pointerdown' ? 'pointercancel' : null;
|
|
|
|
const scheduleFrame = () => {
|
|
if (animationFrame) return;
|
|
animationFrame = requestAnimationFrame(() => {
|
|
animationFrame = null;
|
|
if (!isDragging) return;
|
|
|
|
frameCount += 1;
|
|
const shouldApplyLayout = frameCount % frameStride === 0;
|
|
if (shouldApplyLayout && pendingWidth !== appliedWidth) {
|
|
onDrag(pendingWidth);
|
|
appliedWidth = pendingWidth;
|
|
}
|
|
|
|
onPreview?.(pendingWidth, appliedWidth, {
|
|
didApplyLayout: shouldApplyLayout,
|
|
final: false
|
|
});
|
|
|
|
if (pendingWidth !== appliedWidth) {
|
|
scheduleFrame();
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleMove = (e) => {
|
|
if (!isDragging) return;
|
|
e?.preventDefault?.();
|
|
e?.stopPropagation?.();
|
|
|
|
const dx = e.clientX - startX;
|
|
if (!didDrag && Math.abs(dx) < movementThreshold) return;
|
|
didDrag = true;
|
|
let newWidth = anchor === 'right' ? startWidth - dx : startWidth + dx;
|
|
|
|
if (onBeforeDrag) {
|
|
newWidth = onBeforeDrag(newWidth);
|
|
} else {
|
|
if (newWidth < bounds.min) newWidth = bounds.min;
|
|
if (newWidth > bounds.max) newWidth = bounds.max;
|
|
}
|
|
|
|
const nextWidth = Math.round(newWidth);
|
|
if (nextWidth === pendingWidth) return;
|
|
pendingWidth = nextWidth;
|
|
scheduleFrame();
|
|
};
|
|
|
|
const handleUp = (e) => {
|
|
if (!isDragging) return;
|
|
e?.preventDefault?.();
|
|
e?.stopPropagation?.();
|
|
|
|
isDragging = false;
|
|
|
|
document.removeEventListener(moveEvent, handleMove, true);
|
|
document.removeEventListener(upEvent, handleUp, true);
|
|
if (cancelEvent) {
|
|
document.removeEventListener(cancelEvent, handleUp, true);
|
|
}
|
|
|
|
if (animationFrame) {
|
|
cancelAnimationFrame(animationFrame);
|
|
animationFrame = null;
|
|
}
|
|
|
|
if (didDrag) {
|
|
onPreview?.(pendingWidth, pendingWidth, {
|
|
didApplyLayout: pendingWidth === appliedWidth,
|
|
final: true
|
|
});
|
|
}
|
|
onEnd(pendingWidth, { didDrag, appliedWidth });
|
|
};
|
|
|
|
document.addEventListener(moveEvent, handleMove, true);
|
|
document.addEventListener(upEvent, handleUp, { once: true, capture: true });
|
|
if (cancelEvent) {
|
|
document.addEventListener(cancelEvent, handleUp, { once: true, capture: true });
|
|
}
|
|
|
|
return {
|
|
cancel: () => {
|
|
isDragging = false;
|
|
document.removeEventListener(moveEvent, handleMove, true);
|
|
document.removeEventListener(upEvent, handleUp, true);
|
|
if (cancelEvent) {
|
|
document.removeEventListener(cancelEvent, handleUp, true);
|
|
}
|
|
if (animationFrame) {
|
|
cancelAnimationFrame(animationFrame);
|
|
animationFrame = null;
|
|
}
|
|
if (didDrag) {
|
|
onPreview?.(appliedWidth, appliedWidth, {
|
|
cancelled: true,
|
|
final: true
|
|
});
|
|
}
|
|
}
|
|
};
|
|
}
|