UNPKG

@squirrel-forge/ui-util

Version:

A collection of utilities, classes, functions and abstracts made for the browser and babel compatible.

493 lines (437 loc) 14.9 kB
/** * Requires */ import { Exception } from '../Error/Exception.js'; import { cloneObject } from '../Object/cloneObject.js'; import { mergeObject } from '../Object/mergeObject.js'; /** * Draggables exception * @class * @extends Exception */ class DraggablesException extends Exception {} /** * Draggable data * @typedef {Object} DraggableData * @property {HTMLElement} draggable - Draggable element * @property {HTMLElement} override - Draggable event target override * @property {HTMLElement} container - Constraint container * @property {null|DraggableOnBefore|Function} onbefore - On before callback * @property {null|DraggableOnStart|Function} onstart - On start callback * @property {null|DraggableOnEnd|Function} onend - On end callback * @property {null|DraggableOnMove|Function} onmove - On drag set position callback * @property {null|DraggableOnClick|Function} onclick - On click callback * @property {('both'|'x'|'y')} axis - Draggable axis * @property {('start'|'left'|'center'|'middle'|'right'|'end')} offsetX - Draggable element x offset orientation * @property {('start'|'top'|'center'|'middle'|'bottom'|'end')} offsetY - Draggable element y offset orientation * @property {boolean} overflowX - Allow drag overflow on x * @property {boolean} overflowY - Allow drag overflow on y * @property {boolean} local - Use local click handler * @property {boolean} position - Calculate position * @property {Draggables} parent - Parent instance */ /** * Draggable before drag callback * @callback DraggableOnBefore * @param {MouseEvent} event - Mouse down event * @param {DraggableData} _dgbl - Draggable data * @return {boolean} - Return false to prevent drag start */ /** * Draggable started drag callback * @callback DraggableOnStart * @param {MouseEvent} event - Mouse down event * @param {DraggableData} _dgbl - Draggable data * @return {void} */ /** * Draggable ended drag callback * @callback DraggableOnEnd * @param {MouseEvent} event - Mouse up event * @param {DraggablePosition} position - Current position * @param {DraggablePositionChange} change - Position change * @param {DraggableData} _dgbl - Draggable data * @return {void} */ /** * Draggable move callback * @callback DraggableOnMove * @param {MouseEvent} event - Mouse move event * @param {DraggablePosition} position - Current position * @param {DraggablePositionChange} change - Position change * @param {DraggableData} _dgbl - Draggable data * @return {void} */ /** * Draggable click callback * @callback DraggableOnClick * @param {MouseEvent} event - Mouse up event * @param {DraggablePositionChange} change - Position change * @param {DraggableData} _dgbl - Draggable data * @return {void} */ /** * Draggable axis values * @typedef {Object} DraggableAxisValues * @property {undefined|number} x - X value * @property {undefined|number} y - Y value */ /** * Draggable axis position * @typedef {Object} DraggableAxisPosition * @property {number} px - Relative pixel position * @property {number} percent - Relative percent position */ /** * Draggable position * @typedef {Object} DraggablePosition * @property {undefined|DraggableAxisPosition} x - Horizontal axis position * @property {undefined|DraggableAxisPosition} y - Vertical axis position */ /** * Draggable position change * @typedef {Object} DraggablePositionChange * @property {number} deltaX - Delta x change * @property {number} deltaY - Delta y change * @property {boolean} xMoved - X axis moved * @property {boolean} yMoved - Y axis moved */ /** * Draggables * @class */ export class Draggables { /** * Default draggable data * @private * @type {DraggableData} */ #defaults = { draggable : null, override : null, container : null, onbefore : null, onstart : null, onend : null, onmove : null, onclick : null, axis : 'both', offsetX : 'start', offsetY : 'start', overflowX : false, overflowY : false, local : false, position : true, }; /** * Active draggable data * @private * @type {null|DraggableData} */ #active = null; /** * Axis min thresholds * @private * @type {DraggableAxisValues} */ #min = { x : 0, y : 0 }; /** * Axis start values * @private * @type {DraggableAxisValues} */ #start = { x : 0, y : 0 }; /** * Axis offset values * @private * @type {DraggableAxisValues} */ #offset = { x : 0, y : 0 }; /** * Draggable context * @private * @type {null|window|HTMLElement} */ #context = null; /** * Constructor * @constructor * @param {null|DraggableData|Array<DraggableData>} draggables - Draggable data * @param {window|HTMLElement} context - Context */ constructor( draggables = null, context = window ) { if ( context !== window && !( context instanceof HTMLElement && context.isConnected ) ) { throw new DraggablesException( 'Argument context must be window or a connected HTMLElement' ); } this.#context = context; this.#bind_global(); if ( draggables ) this.bind( draggables ); } /** * Threshold X getter * @public * @return {number} - Horizontal min threshold */ get thresholdX() { return this.#min.x; } /** * Threshold X setter * @public * @param {number} value - Horizontal min threshold * @return {void} */ set thresholdX( value ) { if ( typeof value !== 'number' || Number.isNaN( value ) || value < 0 ) { throw new DraggablesException( 'Invalid thresholdX, value must be 0 or a positive number' ); } this.#min.x = value; } /** * Threshold Y getter * @public * @return {number} - Vertical min threshold */ get thresholdY() { return this.#min.y; } /** * Threshold Y setter * @public * @param {number} value - Vertical min threshold * @return {void} */ set thresholdY( value ) { if ( typeof value !== 'number' || Number.isNaN( value ) || value < 0 ) { throw new DraggablesException( 'Invalid thresholdY, value must be 0 or a positive number' ); } this.#min.y = value; } /** * Bind global event handlers * @private * @return {void} */ #bind_global() { this.#context.addEventListener( 'mousemove', ( event ) => { this.#event_global_mousemove( event ); }, { passive : true } ); this.#context.addEventListener( 'mouseup', ( event ) => { this.#event_global_mouseup( event ); } ); } /** * Bind draggable * @private * @param {DraggableData} _dgbl - Draggable data * @return {DraggableData} - Draggable data */ #data( _dgbl ) { const data = cloneObject( this.#defaults ); mergeObject( data, _dgbl ); Object.defineProperty( data, 'parent', { value : this, writable : false, configurable : false, enumerable : true, } ); return data; } /** * Validate draggable data * @private * @param {DraggableData} _dgbl - Draggable data * @return {void} */ #validate( _dgbl ) { if ( !( _dgbl.draggable instanceof HTMLElement ) ) { throw new DraggablesException( 'Argument draggable must be a HTMLElement' ); } if ( !( _dgbl.container instanceof HTMLElement ) ) { throw new DraggablesException( 'Argument container must be a HTMLElement' ); } if ( typeof _dgbl.onmove !== 'function' && typeof _dgbl.onend !== 'function' ) { throw new DraggablesException( 'Argument onmove or onend must be a Function' ); } } /** * Event global mousemove * @private * @param {MouseEvent} event - Mouse move event * @return {void} */ #event_global_mousemove( event ) { /** * Draggable data * @private * @type {null|DraggableData} */ const _dgbl = this.#active; if ( !_dgbl ) return; // Get delta distances const delta = this.#get_delta( event ); // One of must be at threshold to start dragging if ( _dgbl.axis === 'both' && !delta.xMoved && !delta.yMoved || _dgbl.axis === 'x' && !delta.xMoved || _dgbl.axis === 'y' && !delta.yMoved ) { return; } // Get current X and Y const position = _dgbl.position ? this.#get_position( delta.deltaX, delta.deltaY, _dgbl ) : null; // Call on move handler _dgbl.onmove( event, position, delta, _dgbl ); } /** * Get delta values * @private * @param {MouseEvent} event - Mouse move event * @return {DraggablePositionChange} - Position change data */ #get_delta( event ) { const deltaX = event.clientX - this.#start.x; const deltaY = event.clientY - this.#start.y; const xMoved = Math.abs( deltaX ) > this.#min.x; const yMoved = Math.abs( deltaY ) > this.#min.y; return { deltaX, deltaY, xMoved, yMoved }; } /** * Get full position * @private * @param {number} deltaX - Delta x change * @param {number} deltaY - Delta y change * @param {DraggableData} _dgbl - Draggable data * @return {DraggablePosition} - Current position */ #get_position( deltaX, deltaY, _dgbl ) { const parent = _dgbl.container.getBoundingClientRect(); const element = ( _dgbl.override || _dgbl.draggable ).getBoundingClientRect(); let x, y; if ( _dgbl.axis === 'both' || _dgbl.axis === 'x' ) x = this.#get_axis_pos( _dgbl, deltaX, 'x', 'left', 'width', parent, element ); if ( _dgbl.axis === 'both' || _dgbl.axis === 'y' ) y = this.#get_axis_pos( _dgbl, deltaY, 'y', 'top', 'height', parent, element ); return { x, y }; } /** * Get axis position * @private * @param {DraggableData} _dgbl - Draggable data * @param {number} delta - Axis delta change * @param {('x'|'y')} axis - Axis to calculate * @param {string} rel - Axis parent relative * @param {string} size - Axis parent and element size * @param {DOMRect} parent - Parent dom rectangle * @param {DOMRect} element - Element dom rectangle * @return {DraggableAxisPosition} - Axis position */ #get_axis_pos( _dgbl, delta, axis, rel, size, parent, element ) { // Calculate base position let px = this.#start[ axis ] + delta - parent[ rel ] - this.#offset[ axis ]; // Add offset if required switch ( _dgbl[ 'offset' + axis.toUpperCase() ] ) { case 'end' : case 'right' : case 'bottom' : px += element[ size ]; break; case 'middle' : case 'center' : px += element[ size ] / 2; break; } // Enforce limits if ( !_dgbl[ 'overflow' + axis.toUpperCase() ] ) { if ( px < 0 ) { px = 0; } else if ( px > parent[ size ] ) { px = parent[ size ]; } } // Get relative percentage const percent = px / parent[ size ] * 100; // Return new position return { px, percent }; } /** * Event global mouseup * @private * @param {MouseEvent} event - Mouse up event * @return {void} */ #event_global_mouseup( event ) { this.#event_local_mouseup( event, this.#active ); } /** * Event local mouseup * @private * @param {MouseEvent} event - Mouse up event * @param {DraggableData} _dgbl - Draggable data * @return {void} */ #event_local_mouseup( event, _dgbl ) { if ( !_dgbl || _dgbl !== this.#active ) return; // Clear active this.#active = null; // Get delta distances const delta = this.#get_delta( event ); // Get current X and Y const position = _dgbl.position ? this.#get_position( delta.deltaX, delta.deltaY, _dgbl ) : null; // Run click handler if not moved if ( _dgbl.onclick && ( _dgbl.axis === 'both' && !delta.xMoved && !delta.yMoved || _dgbl.axis === 'x' && !delta.xMoved || _dgbl.axis === 'y' && !delta.yMoved ) ) { _dgbl.onclick( event, position, delta, _dgbl ); } else if ( _dgbl.onend ) { // Run drag end handler _dgbl.onend( event, position, delta, _dgbl ); } } /** * Event local mousedown * @private * @param {MouseEvent} event - Mouse down event * @param {DraggableData} _dgbl - Draggable data * @return {void} */ #event_local_mousedown( event, _dgbl ) { // Draggable check callback if ( _dgbl.onbefore && _dgbl.onbefore( event, _dgbl ) === false ) return; // Get start position this.#start.x = event.clientX; this.#start.y = event.clientY; // Define offset for draggable object const target = ( _dgbl.override || event.target ).getBoundingClientRect(); this.#offset.x = event.clientX - target.left; this.#offset.y = event.clientY - target.top; // Set active this.#active = _dgbl; // Run start callback if ( _dgbl.onstart ) _dgbl.onstart( event, _dgbl ); } /** * Bind draggable * @private * @param {DraggableData} _dgbl - Draggable data * @return {DraggableData} - Compiled draggable data */ #bind( _dgbl ) { _dgbl = this.#data( _dgbl ); this.#validate( _dgbl ); _dgbl.draggable.addEventListener( 'mousedown', ( event ) => { this.#event_local_mousedown( event, _dgbl ); } ); if ( _dgbl.local ) _dgbl.draggable.addEventListener( 'mouseup', ( event ) => { this.#event_local_mouseup( event, _dgbl ); } ); return _dgbl; } /** * Bind draggable/s * @public * @param {DraggableData|Array<DraggableData>} draggables - Draggable data * @return {DraggableData|Array<DraggableData>} - Compiled draggable data */ bind( draggables ) { if ( !draggables ) throw new Error( 'Argument draggables must be an Object or Array of DraggableData' ); let was_array = true; if ( !( draggables instanceof Array ) ) { was_array = false; draggables = [ draggables ]; } const result = []; for ( let i = 0; i < draggables.length; i++ ) { result.push( this.#bind( draggables[ i ] ) ); } return was_array ? result : result.pop(); } }