diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index b6dd56e..304c1e0 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -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 diff --git a/web/resolver/utils/splitter_drag.js b/web/resolver/utils/splitter_drag.js new file mode 100644 index 0000000..768c53b --- /dev/null +++ b/web/resolver/utils/splitter_drag.js @@ -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; + } + } + }; +} diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 53525d6..3c1a924 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -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');