Files
pathfinder/js/app/lib/dragSelect.js
Mark Friedrich 193204bec9 - Improved Login Header (support for *.webp images + 4k resolution)
- Minor bug fixes
- Bump version to `v2.0.0`
2020-04-06 22:42:48 +02:00

402 lines
14 KiB
JavaScript

define(['app/lib/eventHandler'], (EventHandler) => {
'use strict';
let DragSelect = class DragSelect {
constructor(config){
this._config = Object.assign({}, this.constructor.defaultConfig, config);
this._instanceId = ++this.constructor.instanceCount;
this._instanceName = [this._config.namespace, this._instanceId].join('-');
this._animationFrameId = null;
this._mouseIsDown = false;
this._cursorPosition = {x: 0, y: 0};
this._selectBoxDimHash = undefined;
this._deselectedElements = [];
this._targetDim = {
left: 0,
top: 0,
width: 10,
height: 10
};
this._selectBoxOrigin = {
left: 0,
top: 0
};
this.init();
this.debug = (msg,...data) => {
if(this._config.debug){
data = (data || []);
console.debug(msg, ...data);
}
};
this.debugEvent = e => {
if(this._config.debugEvents){
let arrow = '?';
switch(e.type){
case 'mousedown': arrow = '⯆'; break;
case 'mousemove': arrow = '⯈'; break;
case 'mouseup': arrow = '⯅'; break;
}
this.debug('ON ' + arrow + ' %s currentTarget: %o event: %o', e.type, e.currentTarget, e);
}
};
}
/**
* set/update target target element dimension
* -> must be updated if target element is scrolled/or pushed by e.g. slide menu
*/
setTargetDimensions(){
let domRect = this._config.target.getBoundingClientRect();
Object.assign(this._targetDim, this.filterDomRect(domRect));
}
/**
* set/update boundary element dimension [optional]
* -> required for 'intersection' check e.g. for scrollable target
* @returns {DOMRect}
*/
setBoundaryDimensions(){
if(this._config.boundary){
let boundary = this._config.target.closest(this._config.boundary);
if(boundary){
return (this._boundaryDim = boundary.getBoundingClientRect());
}
}
delete this._boundaryDim;
}
/**
* set/update current cursor coordinates
* @param e
*/
setCursorPosition(e){
Object.assign(this._cursorPosition, {
x: e.pageX,
y: e.pageY
});
}
init(){
this.initSelectBox();
this.setTargetDimensions();
EventHandler.addEventListener(this._config.target, this.getNamespaceEvent('mousedown'), this.onMouseDown.bind(this), {passive: false});
EventHandler.addEventListener(this._config.container, this.getNamespaceEvent('mousemove'), this.onMouseMove.bind(this), {passive: false});
EventHandler.addEventListener(this._config.container, this.getNamespaceEvent('mouseup'), this.onMouseUp.bind(this), {passive: true});
}
// Mouse events -----------------------------------------------------------------------------------------------
onMouseDown(e){
if(e.which === 1){
this.debugEvent(e);
this.setTargetDimensions();
this.showSelectBox(e);
this._mouseIsDown = true;
}
}
onMouseMove(e){
if(this._mouseIsDown){
e.preventDefault();
if(this._animationFrameId){
cancelAnimationFrame(this._animationFrameId);
}
this._animationFrameId = requestAnimationFrame(() => {
this.debugEvent(e);
this.setCursorPosition(e);
this.update();
this._animationFrameId = null;
});
}
}
onMouseUp(e){
this.debugEvent(e);
this.selectElements();
this.hideSelectBox();
this._mouseIsDown = false;
this._deselectedElements = [];
}
// SelectBox handler ------------------------------------------------------------------------------------------
/**
* create new selectBox and append to DOM
* -> hidden by CSS until selectBox gets updated
*/
initSelectBox(){
this._selectBox = document.createElement('div');
this._selectBox.id = this._instanceName;
this._selectBox.classList.add(this._config.selectBoxClass);
this._config.target.after(this._selectBox);
}
/**
* show selectBox -> apply CSS for positioning and dimension
* @param e
*/
showSelectBox(e){
Object.assign(this._selectBoxOrigin, {
left: e.pageX - this._targetDim.left,
top: e.pageY - this._targetDim.top
});
// limit render "reflow" bny adding all properties at once
this._selectBox.style.cssText = `--selectBox-left: ${this._selectBoxOrigin.left}px; ` +
`--selectBox-top: ${this._selectBoxOrigin.top}px; ` +
`--selectBox-width: 1px; ` +
`--selectBox-height: 1px;`;
this._selectBox.classList.add(this._config.activeClass);
this.callback('onShow');
}
/**
* update selectBox position and dimension based on cursorPosition
* @returns {boolean}
*/
updateSelectBox(){
let updated = false;
if(!this.isActiveSelectBox()){
return updated;
}
let left = this._cursorPosition.x - this._targetDim.left;
let top = this._cursorPosition.y - this._targetDim.top;
let tempWidth = this._selectBoxOrigin.left - left;
let tempHeight = this._selectBoxOrigin.top - top;
let newLeft = this._selectBoxOrigin.left;
let newTop = this._selectBoxOrigin.top;
let newWidth = left - this._selectBoxOrigin.left;
let newHeight = top - this._selectBoxOrigin.top;
if(newWidth < 0){
newLeft = newLeft - tempWidth;
newWidth = newWidth * -1;
}
if(newHeight < 0){
newTop = newTop - tempHeight;
newHeight = newHeight * -1;
}
// check if dimension has changed -> improve performance
let dimensionHash = [newWidth, newHeight].join('_');
if(this._selectBoxDimHash !== dimensionHash){
this._selectBoxDimHash = dimensionHash;
this._selectBox.style.cssText = `--selectBox-left: ${newLeft}px; ` +
`--selectBox-top: ${newTop}px; ` +
`--selectBox-width: ${newWidth}px; ` +
`--selectBox-height: ${newHeight}px;`;
// set drag position data (which corner)
this._selectBox.dataset.origin = this.getSelectBoxDragOrigin(left, top, newLeft, newTop).join('|');
updated = true;
this.callback('onUpdate');
this.dispatch(this._selectBox, 'update', this);
}
return updated;
}
/**
* hide selectBox
*/
hideSelectBox(){
if(!this.isActiveSelectBox()){
return;
}
if(this.callback('onHide', this._deselectedElements) !== false){
this._selectBox.classList.remove(this._config.activeClass);
}
}
/**
* cursor position corner for selectBox
* @param left
* @param top
* @param newLeft
* @param newTop
* @returns {[string, string]}
*/
getSelectBoxDragOrigin(left, top, newLeft, newTop){
let position = [];
if(
left === newLeft &&
top === newTop
){
position = ['top', 'left'];
}else if(top === newTop){
position = ['top', 'right'];
}else if(left === newLeft){
position = ['bottom', 'left'];
}else{
position = ['bottom', 'right'];
}
return position;
}
/**
* check if there is currently an active selectBox visible
* @returns {boolean}
*/
isActiveSelectBox(){
return this._selectBox.classList.contains(this._config.activeClass) &&
!this._config.target.classList.contains(this._config.disabledClass);
}
// Element select methods -------------------------------------------------------------------------------------
selectableElements(){
return this._config.target.querySelectorAll(this._config.selectables);
}
selectElements(){
if(!this.isActiveSelectBox()){
return;
}
let selectables = this.selectableElements();
let selectBoxDim = this.filterDomRect(this._selectBox.getBoundingClientRect());
selectables.forEach(el => {
let elDim = this.filterDomRect(el.getBoundingClientRect());
if(this.percentCovered(selectBoxDim, elDim) > this._config.percentCovered){
el.classList.add(this._config.selectedClass);
// remove element from "deselected" elements (e.g on add -> remove -> add scenario)
this._deselectedElements = this._deselectedElements.filter(tempEl => tempEl !== el);
}else{
if(el.classList.contains(this._config.selectedClass)){
el.classList.remove(this._config.selectedClass);
// add to "deselected" elements, if not already in array
if(this._deselectedElements.findIndex(tempEl => tempEl === el) === -1){
this._deselectedElements.push(el);
}
}
}
});
}
percentCovered(dim1, dim2){
if(
(dim1.left <= dim2.left) &&
(dim1.top <= dim2.top) &&
((dim1.left + dim1.width) >= (dim2.left + dim2.width)) &&
((dim1.top + dim1.height) > (dim2.top + dim2.height))
){
// The whole thing is covering the whole other thing
return 100;
}else{
// Only parts may be covered, calculate percentage
dim1.right = dim1.left + dim1.width;
dim1.bottom = dim1.top + dim1.height;
dim2.right = dim2.left + dim2.width;
dim2.bottom = dim2.top + dim2.height;
let l = Math.max(dim1.left, dim2.left);
let r = Math.min(dim1.right, dim2.right);
let t = Math.max(dim1.top, dim2.top);
let b = Math.min(dim1.bottom, dim2.bottom);
if(b >= t && r >= l){
return (((r - l) * (b - t)) / (dim2.width * dim2.height)) * 100;
}
}
return 0;
}
// Boundary intersection methods ------------------------------------------------------------------------------
getIntersection(){
let intersection = [];
if(this.isActiveSelectBox && this._boundaryDim){
let selectBoxDim = this._selectBox.getBoundingClientRect();
if(this._boundaryDim.top > selectBoxDim.top){
intersection.push('top');
}
if(this._boundaryDim.bottom < selectBoxDim.bottom){
intersection.push('bottom');
}
if(this._boundaryDim.left > selectBoxDim.left){
intersection.push('left');
}
if(this._boundaryDim.right < selectBoxDim.right){
intersection.push('right');
}
}
return intersection;
}
// Util methods -----------------------------------------------------------------------------------------------
update(){
this.setTargetDimensions();
this.setBoundaryDimensions();
if(this.updateSelectBox() && this._config.selectOnDrag){
this.selectElements();
}
}
getNamespaceEvent(type){
return [type, this._instanceName].join('.');
}
filterDomRect(domRect, filteredKeys = ['left', 'top', 'width', 'height']){
let obj = {};
filteredKeys.forEach(key => {
if(domRect[key] !== undefined){
obj[key] = domRect[key];
}
});
return obj;
//return filteredKeys.reduce((obj, key) => ({ ...obj, [key]: domRect[key] }), {}); // same result but uses "object destruction" ES5
}
callback(callback, ...args){
if(this._config[callback] instanceof Function){
return this._config[callback](...args);
}
}
dispatch(target, type, data = null){
let event = new CustomEvent([type, this._config.namespace].join(':'), {
bubbles: true,
detail: data
});
target.dispatchEvent(event);
}
};
DragSelect.defaultConfig = {
container: document,
target: document.body,
namespace: 'dragSelect',
activeClass: 'active',
disabledClass: 'disabled',
selectables: 'div',
selectedClass: 'dragSelect-selected',
selectBoxClass: 'dragSelect-selectBox',
boundary: undefined, // optional selector for boundary parent box (e.g. scrollable viewport)
selectOnDrag: true,
percentCovered: 25,
onShow: undefined,
onHide: undefined,
onUpdate: undefined,
debug: false,
debugEvents: false
};
DragSelect.instanceCount = 0;
return DragSelect;
});