/** * A UI utility to make an element draggable. * @param {Application} app The Application that is being made draggable. * @param {jQuery} element A JQuery reference to the Application's outer-most element. * @param {HTMLElement|boolean} handle The element that acts as a drag handle. Supply false to disable dragging. * @param {boolean|object} resizable Is the application resizable? Supply an object to configure resizing behaviour * or true to have it automatically configured. * @param {string} [resizable.selector] A selector for the resize handle. * @param {boolean} [resizable.resizeX=true] Enable resizing in the X direction. * @param {boolean} [resizable.resizeY=true] Enable resizing in the Y direction. * @param {boolean} [resizable.rtl] Modify the resizing direction to be right-to-left. */ class Draggable { constructor(app, element, handle, resizable) { // Setup element data this.app = app; this.element = element[0]; this.handle = handle ?? this.element; this.resizable = resizable || false; /** * Duplicate the application's starting position to track differences * @type {Object} */ this.position = null; /** * Remember event handlers associated with this Draggable class so they may be later unregistered * @type {Object} */ this.handlers = {}; /** * Throttle mousemove event handling to 60fps * @type {number} */ this._moveTime = 0; // Activate interactivity this.activateListeners(); } /* ----------------------------------------- */ /** * Activate event handling for a Draggable application * Attach handlers for floating, dragging, and resizing */ activateListeners() { this._activateDragListeners(); this._activateResizeListeners(); } /* ----------------------------------------- */ /** * Attach handlers for dragging and floating. * @protected */ _activateDragListeners() { if ( !this.handle ) return; // Float to top this.handlers["click"] = ["pointerdown", ev => this.app.bringToTop(), {capture: true, passive: true}]; this.element.addEventListener(...this.handlers.click); // Drag handlers this.handlers["dragDown"] = ["pointerdown", e => this._onDragMouseDown(e), false]; this.handlers["dragMove"] = ["pointermove", e => this._onDragMouseMove(e), false]; this.handlers["dragUp"] = ["pointerup", e => this._onDragMouseUp(e), false]; this.handle.addEventListener(...this.handlers.dragDown); this.handle.classList.add("draggable"); } /* ----------------------------------------- */ /** * Attach handlers for resizing. * @protected */ _activateResizeListeners() { if ( !this.resizable ) return; let handle = this.element.querySelector(this.resizable.selector); if ( !handle ) { handle = $('
')[0]; this.element.appendChild(handle); } // Register handlers this.handlers["resizeDown"] = ["pointerdown", e => this._onResizeMouseDown(e), false]; this.handlers["resizeMove"] = ["pointermove", e => this._onResizeMouseMove(e), false]; this.handlers["resizeUp"] = ["pointerup", e => this._onResizeMouseUp(e), false]; // Attach the click handler and CSS class handle.addEventListener(...this.handlers.resizeDown); if ( this.handle ) this.handle.classList.add("resizable"); } /* ----------------------------------------- */ /** * Handle the initial mouse click which activates dragging behavior for the application * @private */ _onDragMouseDown(event) { event.preventDefault(); // Record initial position this.position = foundry.utils.deepClone(this.app.position); this._initial = {x: event.clientX, y: event.clientY}; // Add temporary handlers window.addEventListener(...this.handlers.dragMove); window.addEventListener(...this.handlers.dragUp); } /* ----------------------------------------- */ /** * Move the window with the mouse, bounding the movement to ensure the window stays within bounds of the viewport * @private */ _onDragMouseMove(event) { event.preventDefault(); // Limit dragging to 60 updates per second const now = Date.now(); if ( (now - this._moveTime) < (1000/60) ) return; this._moveTime = now; // Update application position this.app.setPosition({ left: this.position.left + (event.clientX - this._initial.x), top: this.position.top + (event.clientY - this._initial.y) }); } /* ----------------------------------------- */ /** * Conclude the dragging behavior when the mouse is release, setting the final position and removing listeners * @private */ _onDragMouseUp(event) { event.preventDefault(); window.removeEventListener(...this.handlers.dragMove); window.removeEventListener(...this.handlers.dragUp); } /* ----------------------------------------- */ /** * Handle the initial mouse click which activates dragging behavior for the application * @private */ _onResizeMouseDown(event) { event.preventDefault(); // Limit dragging to 60 updates per second const now = Date.now(); if ( (now - this._moveTime) < (1000/60) ) return; this._moveTime = now; // Record initial position this.position = foundry.utils.deepClone(this.app.position); if ( this.position.height === "auto" ) this.position.height = this.element.clientHeight; if ( this.position.width === "auto" ) this.position.width = this.element.clientWidth; this._initial = {x: event.clientX, y: event.clientY}; // Add temporary handlers window.addEventListener(...this.handlers.resizeMove); window.addEventListener(...this.handlers.resizeUp); } /* ----------------------------------------- */ /** * Move the window with the mouse, bounding the movement to ensure the window stays within bounds of the viewport * @private */ _onResizeMouseMove(event) { event.preventDefault(); const scale = this.app.position.scale ?? 1; let deltaX = (event.clientX - this._initial.x) / scale; const deltaY = (event.clientY - this._initial.y) / scale; if ( this.resizable.rtl === true ) deltaX *= -1; const newPosition = { width: this.position.width + deltaX, height: this.position.height + deltaY }; if ( this.resizable.resizeX === false ) delete newPosition.width; if ( this.resizable.resizeY === false ) delete newPosition.height; this.app.setPosition(newPosition); } /* ----------------------------------------- */ /** * Conclude the dragging behavior when the mouse is release, setting the final position and removing listeners * @private */ _onResizeMouseUp(event) { event.preventDefault(); window.removeEventListener(...this.handlers.resizeMove); window.removeEventListener(...this.handlers.resizeUp); this.app._onResize(event); } }