refactor: extract splitter drag logic into shared utility

Replace duplicated inline drag handlers in queue_methods.js and
missing_browser_methods.js with a new startSplitterDrag() helper in
web/resolver/utils/splitter_drag.js.

The utility centralizes pointer/mouse event binding, RAF throttling,
and touch-cancellation cleanup, cutting net code across the two
callers while preserving existing behaviour (bounds, edge-open
threshold, collapse preview, and settle/cleanup timing).
This commit is contained in:
Dariusz L
2026-07-02 01:57:37 +02:00
parent dc7ed980b9
commit d97450f119
3 changed files with 283 additions and 247 deletions
+131 -153
View File
@@ -2,6 +2,7 @@ import { app } from "../../../../../scripts/app.js";
import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { startSplitterDrag } from "../utils/splitter_drag.js";
export const queueMethods = {
createContent() {
// Wrap the body in a two-column layout: left = items, right = queued panel
@@ -1895,162 +1896,139 @@ export const queueMethods = {
this._splitPreviewCollapsed = !!startCollapsedPreview;
this._splitEdgeOpen = !!edgeOpen;
this._splitEdgeOpenedPastThreshold = false;
this._onSplitMove = (ev) => this.onSplitDrag(ev);
this._onSplitUp = (ev) => this.endSplitDrag(ev);
this._splitMoveEvent = e?.type === 'pointerdown' ? 'pointermove' : 'mousemove';
this._splitUpEvent = e?.type === 'pointerdown' ? 'pointerup' : 'mouseup';
this._splitCancelEvent = e?.type === 'pointerdown' ? 'pointercancel' : null;
document.addEventListener(this._splitMoveEvent, this._onSplitMove, true);
document.addEventListener(this._splitUpEvent, this._onSplitUp, { once: true, capture: true });
if (this._splitCancelEvent) {
document.addEventListener(this._splitCancelEvent, this._onSplitUp, { once: true, capture: true });
}
this._splitInteraction = startSplitterDrag(e, {
anchor: 'right',
startWidth: initialWidth,
bounds,
onBeforeDrag: (newW) => {
if (newW < bounds.min) newW = bounds.min;
if (newW > bounds.max) newW = bounds.max;
return newW;
},
onDrag: (width) => {
this._pendingSplitWidth = width;
if (this._splitEdgeOpen && this._pendingSplitWidth > 140) {
this._splitEdgeOpenedPastThreshold = true;
if (this.queueCollapsed) {
this.queueCollapsed = false;
this.persistQueueCollapsedState(false, 500);
this.updateQueueToggleIcon();
}
if (this._queuePanelDirty) {
this.updateQueuePanel({ force: true });
}
}
const previewCollapsed = this._splitEdgeOpen
? (!this._splitEdgeOpenedPastThreshold || this._pendingSplitWidth <= 140)
: this._pendingSplitWidth <= 140;
if (previewCollapsed !== this._splitPreviewCollapsed) {
this._splitPreviewCollapsed = previewCollapsed;
const body = document.getElementById('model-resolver-body');
body?.classList.toggle('mr-queue-preview-collapsed', previewCollapsed);
this.queueElement.style.display = previewCollapsed ? 'none' : '';
if (previewCollapsed) {
const collapsedBounds = this.getQueueSplitBounds(this._splitStart.containerWidth, { edgeOpen: true });
this.applyQueueSplitWidth(0, {
bounds: collapsedBounds,
containerWidth: this._splitStart.containerWidth,
skipIfUnchanged: true
});
}
}
if (this._splitPreviewCollapsed) return;
this.applyQueueSplitWidth(width, {
bounds: this._splitStart?.bounds || bounds,
containerWidth: this._splitStart?.containerWidth,
skipIfUnchanged: true
});
},
onEnd: (finalWidth) => {
this._splitDragging = false;
if (this.queueElement && finalWidth && !this._splitPreviewCollapsed) {
this.applyQueueSplitWidth(finalWidth, {
bounds: this._splitStart?.bounds,
containerWidth: this._splitStart?.containerWidth,
skipIfUnchanged: true
});
}
const body = this.getQueueSplitBody();
if (this._splitEdgeOpen && !this._pendingSplitWidth) {
try {
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
this.applyQueueSplitWidth(restoreWidth, {
bounds: this._splitStart?.bounds,
containerWidth: this._splitStart?.containerWidth
});
this.queueElement.style.display = '';
if (this.splitterElement) this.splitterElement.style.display = '';
this.persistQueueSplitWidth(restoreWidth, 500);
} catch (err) { }
this.queueCollapsed = false;
this.persistQueueCollapsedState(false, 500);
this.updateQueueVisibility();
this.updateQueuePanel({ force: true });
this._splitEdgeOpen = false;
this._pendingSplitWidth = null;
this._appliedSplitWidth = null;
this._splitPreviewCollapsed = false;
this.deferQueueSplitReleaseUi(body);
this._splitEdgeOpenedPastThreshold = false;
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
this._splitInteraction = null;
return;
}
const shouldCollapse = !!this._splitPreviewCollapsed;
if (shouldCollapse) {
try {
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
this.applyQueueSplitWidth(restoreWidth, {
bounds: this._splitStart?.bounds,
containerWidth: this._splitStart?.containerWidth
});
this.queueElement.style.display = '';
this.persistQueueSplitWidth(restoreWidth);
} catch (err) { }
this._pendingSplitWidth = null;
this._appliedSplitWidth = null;
this._splitPreviewCollapsed = false;
this._splitEdgeOpen = false;
this._splitEdgeOpenedPastThreshold = false;
this.deferQueueSplitReleaseUi(body);
this.setQueueCollapsed(true);
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
this._splitInteraction = null;
return;
}
if (this.queueElement) this.queueElement.style.display = '';
try {
const width = finalWidth
|| Number.parseInt(String(this.queueElement.style.width || ''), 10)
|| this.getQueuePaneWidth(this.queueElement);
this.persistQueueSplitWidth(width);
} catch (err) { }
this._pendingSplitWidth = null;
this._appliedSplitWidth = null;
this._splitPreviewCollapsed = false;
this._splitEdgeOpen = false;
this._splitEdgeOpenedPastThreshold = false;
this.deferQueueSplitReleaseUi(body);
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
this._splitInteraction = null;
}
});
} catch (err) { /* ignore */ }
},
onSplitDrag(e) {
if (!this._splitDragging || !this._splitStart || !this.queueElement) return;
e?.preventDefault?.();
e?.stopPropagation?.();
const dx = e.clientX - this._splitStart.x;
let newW = this._splitStart.startWidth - dx;
const bounds = this._splitStart.bounds || this.getQueueSplitBounds(this._splitStart.containerWidth, { edgeOpen: this._splitEdgeOpen });
if (newW < bounds.min) newW = bounds.min;
if (newW > bounds.max) newW = bounds.max;
this._pendingSplitWidth = Math.round(newW);
if (this._splitEdgeOpen && this._pendingSplitWidth > 140) {
this._splitEdgeOpenedPastThreshold = true;
if (this.queueCollapsed) {
this.queueCollapsed = false;
this.persistQueueCollapsedState(false, 500);
this.updateQueueToggleIcon();
}
if (this._queuePanelDirty) {
this.updateQueuePanel({ force: true });
}
}
const previewCollapsed = this._splitEdgeOpen
? (!this._splitEdgeOpenedPastThreshold || this._pendingSplitWidth <= 140)
: this._pendingSplitWidth <= 140;
if (previewCollapsed !== this._splitPreviewCollapsed) {
this._splitPreviewCollapsed = previewCollapsed;
const body = document.getElementById('model-resolver-body');
body?.classList.toggle('mr-queue-preview-collapsed', previewCollapsed);
this.queueElement.style.display = previewCollapsed ? 'none' : '';
if (previewCollapsed) {
const collapsedBounds = this.getQueueSplitBounds(this._splitStart.containerWidth, { edgeOpen: true });
this.applyQueueSplitWidth(0, {
bounds: collapsedBounds,
containerWidth: this._splitStart.containerWidth,
skipIfUnchanged: true
});
}
}
if (this._splitPreviewCollapsed) return;
if (!this._splitDragFrame) {
this._splitDragFrame = requestAnimationFrame(() => {
this._splitDragFrame = null;
if (!this._splitDragging || this._splitPreviewCollapsed) return;
this.applyQueueSplitWidth(this._pendingSplitWidth, {
bounds: this._splitStart?.bounds || bounds,
containerWidth: this._splitStart?.containerWidth,
skipIfUnchanged: true
});
});
}
},
endSplitDrag(e = null) {
if (!this._splitDragging) return;
e?.preventDefault?.();
e?.stopPropagation?.();
this._splitDragging = false;
if (this._splitDragFrame) {
cancelAnimationFrame(this._splitDragFrame);
this._splitDragFrame = null;
}
if (this.queueElement && this._pendingSplitWidth && !this._splitPreviewCollapsed) {
this.applyQueueSplitWidth(this._pendingSplitWidth, {
bounds: this._splitStart?.bounds,
containerWidth: this._splitStart?.containerWidth,
skipIfUnchanged: true
});
}
const body = this.getQueueSplitBody();
if (this._splitMoveEvent) {
document.removeEventListener(this._splitMoveEvent, this._onSplitMove, true);
}
if (this._splitUpEvent) {
document.removeEventListener(this._splitUpEvent, this._onSplitUp, true);
}
if (this._splitCancelEvent) {
document.removeEventListener(this._splitCancelEvent, this._onSplitUp, true);
}
if (this._splitEdgeOpen && !this._pendingSplitWidth) {
try {
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
this.applyQueueSplitWidth(restoreWidth, {
bounds: this._splitStart?.bounds,
containerWidth: this._splitStart?.containerWidth
});
this.queueElement.style.display = '';
if (this.splitterElement) this.splitterElement.style.display = '';
this.persistQueueSplitWidth(restoreWidth, 500);
} catch (e) { }
this.queueCollapsed = false;
this.persistQueueCollapsedState(false, 500);
this.updateQueueVisibility();
this.updateQueuePanel({ force: true });
this._splitEdgeOpen = false;
this._pendingSplitWidth = null;
this._appliedSplitWidth = null;
this._splitPreviewCollapsed = false;
this.deferQueueSplitReleaseUi(body);
this._splitEdgeOpenedPastThreshold = false;
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
return;
}
const shouldCollapse = !!this._splitPreviewCollapsed;
if (shouldCollapse) {
try {
const restoreWidth = Math.max(240, Math.round(this._splitStart?.startWidth || 320));
this.applyQueueSplitWidth(restoreWidth, {
bounds: this._splitStart?.bounds,
containerWidth: this._splitStart?.containerWidth
});
this.queueElement.style.display = '';
this.persistQueueSplitWidth(restoreWidth);
} catch (e) { }
this._pendingSplitWidth = null;
this._appliedSplitWidth = null;
this._splitPreviewCollapsed = false;
this._splitEdgeOpen = false;
this._splitEdgeOpenedPastThreshold = false;
this.deferQueueSplitReleaseUi(body);
this.setQueueCollapsed(true);
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
return;
}
if (this.queueElement) this.queueElement.style.display = '';
try {
const width = this._pendingSplitWidth
|| this._appliedSplitWidth
|| Number.parseInt(String(this.queueElement.style.width || ''), 10)
|| this.getQueuePaneWidth(this.queueElement);
this.persistQueueSplitWidth(width);
} catch (e) { }
this._pendingSplitWidth = null;
this._appliedSplitWidth = null;
this._splitPreviewCollapsed = false;
this._splitEdgeOpen = false;
this._splitEdgeOpenedPastThreshold = false;
this.deferQueueSplitReleaseUi(body);
this._splitMoveEvent = null;
this._splitUpEvent = null;
this._splitCancelEvent = null;
setTimeout(() => { this._suppressQueueEdgeClick = false; }, 0);
},
/**
* Queue a resolution for later batch apply
+97
View File
@@ -0,0 +1,97 @@
/**
* 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 },
onBeforeDrag = null,
onDrag = () => {},
onEnd = () => {}
}) {
if (event?.button !== undefined && event.button !== 0) return null;
event?.preventDefault?.();
event?.stopPropagation?.();
const startX = event.clientX;
let pendingWidth = startWidth;
let appliedWidth = startWidth;
let animationFrame = null;
let isDragging = true;
const moveEvent = event?.type === 'pointerdown' ? 'pointermove' : 'mousemove';
const upEvent = event?.type === 'pointerdown' ? 'pointerup' : 'mouseup';
const cancelEvent = event?.type === 'pointerdown' ? 'pointercancel' : null;
const handleMove = (e) => {
if (!isDragging) return;
e?.preventDefault?.();
e?.stopPropagation?.();
const dx = e.clientX - startX;
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;
}
pendingWidth = Math.round(newWidth);
if (pendingWidth === appliedWidth) return;
if (!animationFrame) {
animationFrame = requestAnimationFrame(() => {
animationFrame = null;
if (!isDragging) return;
onDrag(pendingWidth);
appliedWidth = pendingWidth;
});
}
};
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;
}
onEnd(pendingWidth || 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;
}
}
};
}
+55 -94
View File
@@ -4,6 +4,7 @@ import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { createFloatingTreePicker } from "../utils/tree_picker.js";
import { showNotification as showNotificationUtils } from "../../utils/notification_utils.js";
import { startSplitterDrag } from "../utils/splitter_drag.js";
export const missingBrowserMethods = {
getMissingFilename(missing = {}) {
@@ -966,12 +967,15 @@ export const missingBrowserMethods = {
this._pendingMissingBrowserRestoreWidth = null;
this._pendingMissingBrowserRestoreUseDefault = false;
this.cancelMissingBrowserSplitWidthPersist();
const releaseSplitter = splitter instanceof HTMLElement
? splitter
: browser.querySelector('.mr-missing-browser-splitter');
this._missingBrowserSplitBrowser = browser;
this._missingBrowserSplitListPane = listPane;
this._missingBrowserSplitDetailPane = detailPane;
this._missingBrowserSplitSplitter = splitter instanceof HTMLElement
? splitter
: browser.querySelector('.mr-missing-browser-splitter');
this._missingBrowserSplitSplitter = releaseSplitter;
this._missingBrowserSplitDragging = true;
this._missingBrowserSplitStart = {
x: event.clientX,
@@ -980,19 +984,56 @@ export const missingBrowserMethods = {
browserWidth,
availableWidth: bounds.available
};
this._appliedMissingBrowserSplitWidth = Math.round(this._missingBrowserSplitStart.width);
this._appliedMissingBrowserSplitWidth = detailWidth;
this._pendingMissingBrowserSplitWidth = detailWidth;
this._missingBrowserSplitUiActive = false;
this._onMissingBrowserSplitMove = (moveEvent) => this.onMissingBrowserSplitDrag(moveEvent);
this._onMissingBrowserSplitUp = (upEvent) => this.endMissingBrowserSplitDrag(upEvent);
this._missingBrowserSplitMoveEvent = event.type === 'pointerdown' ? 'pointermove' : 'mousemove';
this._missingBrowserSplitUpEvent = event.type === 'pointerdown' ? 'pointerup' : 'mouseup';
this._missingBrowserSplitCancelEvent = event.type === 'pointerdown' ? 'pointercancel' : null;
document.addEventListener(this._missingBrowserSplitMoveEvent, this._onMissingBrowserSplitMove, true);
document.addEventListener(this._missingBrowserSplitUpEvent, this._onMissingBrowserSplitUp, { once: true, capture: true });
if (this._missingBrowserSplitCancelEvent) {
document.addEventListener(this._missingBrowserSplitCancelEvent, this._onMissingBrowserSplitUp, { once: true, capture: true });
}
this._missingBrowserSplitInteraction = startSplitterDrag(event, {
anchor: 'right',
startWidth: detailWidth,
bounds,
onDrag: (width) => {
this._pendingMissingBrowserSplitWidth = width;
this.activateMissingBrowserSplitUi();
this.applyMissingBrowserDetailWidth(detailPane, width, {
browserWidth,
availableWidth: bounds.available,
splitBounds: bounds,
skipIfUnchanged: true
});
this._appliedMissingBrowserSplitWidth = width;
},
onEnd: (finalWidth) => {
const settlingNow = typeof performance === 'object' && typeof performance.now === 'function'
? performance.now()
: Date.now();
this._missingBrowserSplitSettlingUntil = settlingNow + 350;
this._missingBrowserSplitDragging = false;
if (detailPane && finalWidth) {
this.applyMissingBrowserDetailWidth(detailPane, finalWidth, {
browserWidth,
availableWidth: bounds.available,
splitBounds: bounds
});
this._appliedMissingBrowserSplitWidth = finalWidth;
}
if (finalWidth) {
this.persistMissingBrowserSplitWidth(finalWidth, { delay: 600 });
}
this.deferMissingBrowserSplitReleaseUi(releaseSplitter, !!this._missingBrowserSplitUiActive);
this._missingBrowserSplitBrowser = null;
this._missingBrowserSplitListPane = null;
this._missingBrowserSplitDetailPane = null;
this._missingBrowserSplitSplitter = null;
this._missingBrowserSplitStart = null;
this._missingBrowserSplitUiActive = false;
this._pendingMissingBrowserSplitWidth = null;
this._appliedMissingBrowserSplitWidth = null;
this._missingBrowserSplitInteraction = null;
}
});
},
activateMissingBrowserSplitUi() {
@@ -1012,86 +1053,6 @@ export const missingBrowserMethods = {
});
},
onMissingBrowserSplitDrag(event) {
if (!this._missingBrowserSplitDragging || !this._missingBrowserSplitStart || !this._missingBrowserSplitBrowser) return;
event.preventDefault?.();
event.stopPropagation?.();
const nextWidth = this._missingBrowserSplitStart.width - (event.clientX - this._missingBrowserSplitStart.x);
const bounds = this._missingBrowserSplitStart.bounds;
this._pendingMissingBrowserSplitWidth = Math.round(Math.max(bounds.min, Math.min(bounds.max, nextWidth)));
if (this._pendingMissingBrowserSplitWidth === this._appliedMissingBrowserSplitWidth) return;
if (!this._missingBrowserSplitDetailPane) return;
this.activateMissingBrowserSplitUi();
if (!this._missingBrowserSplitFrame) {
this._missingBrowserSplitFrame = requestAnimationFrame(() => {
this._missingBrowserSplitFrame = null;
if (!this._missingBrowserSplitDragging || !this._missingBrowserSplitDetailPane) return;
this.applyMissingBrowserDetailWidth(this._missingBrowserSplitDetailPane, this._pendingMissingBrowserSplitWidth, {
browserWidth: this._missingBrowserSplitStart?.browserWidth,
availableWidth: this._missingBrowserSplitStart?.availableWidth,
splitBounds: this._missingBrowserSplitStart?.bounds,
skipIfUnchanged: true
});
this._appliedMissingBrowserSplitWidth = this._pendingMissingBrowserSplitWidth;
});
}
},
endMissingBrowserSplitDrag(event = null) {
if (!this._missingBrowserSplitDragging) return;
event?.preventDefault?.();
event?.stopPropagation?.();
const settlingNow = typeof performance === 'object' && typeof performance.now === 'function'
? performance.now()
: Date.now();
this._missingBrowserSplitSettlingUntil = settlingNow + 350;
const releaseSplitter = this._missingBrowserSplitSplitter;
const wasUiActive = !!this._missingBrowserSplitUiActive;
const finalWidth = this._pendingMissingBrowserSplitWidth || this._appliedMissingBrowserSplitWidth;
this._missingBrowserSplitDragging = false;
if (this._missingBrowserSplitMoveEvent) {
document.removeEventListener(this._missingBrowserSplitMoveEvent, this._onMissingBrowserSplitMove, true);
}
if (this._missingBrowserSplitCancelEvent) {
document.removeEventListener(this._missingBrowserSplitCancelEvent, this._onMissingBrowserSplitUp, true);
}
if (this._missingBrowserSplitUpEvent) {
document.removeEventListener(this._missingBrowserSplitUpEvent, this._onMissingBrowserSplitUp, true);
}
if (this._missingBrowserSplitFrame) {
cancelAnimationFrame(this._missingBrowserSplitFrame);
this._missingBrowserSplitFrame = null;
}
if (this._missingBrowserSplitDetailPane && this._pendingMissingBrowserSplitWidth) {
if (this._pendingMissingBrowserSplitWidth !== this._appliedMissingBrowserSplitWidth) {
this.applyMissingBrowserDetailWidth(this._missingBrowserSplitDetailPane, this._pendingMissingBrowserSplitWidth, {
browserWidth: this._missingBrowserSplitStart?.browserWidth,
availableWidth: this._missingBrowserSplitStart?.availableWidth,
splitBounds: this._missingBrowserSplitStart?.bounds
});
this._appliedMissingBrowserSplitWidth = this._pendingMissingBrowserSplitWidth;
}
}
if (finalWidth) {
this.persistMissingBrowserSplitWidth(finalWidth, { delay: 600 });
}
this.deferMissingBrowserSplitReleaseUi(releaseSplitter, wasUiActive);
this._missingBrowserSplitBrowser = null;
this._missingBrowserSplitListPane = null;
this._missingBrowserSplitDetailPane = null;
this._missingBrowserSplitSplitter = null;
this._missingBrowserSplitStart = null;
this._missingBrowserSplitUiActive = false;
this._pendingMissingBrowserSplitWidth = null;
this._appliedMissingBrowserSplitWidth = null;
this._missingBrowserSplitMoveEvent = null;
this._missingBrowserSplitUpEvent = null;
this._missingBrowserSplitCancelEvent = null;
},
applyMissingBrowserSplitReleaseCleanup(cleanup) {
if (!cleanup) return;
cleanup.splitter?.classList?.remove('is-resizing');