210 lines
6.9 KiB
JavaScript
210 lines
6.9 KiB
JavaScript
/**
|
|
* 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 = $('<div class="window-resizable-handle"><i class="fas fa-arrows-alt-h"></i></div>')[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);
|
|
}
|
|
}
|