src/Plugins/Collidable/Collidable.js
import AbstractPlugin from 'shared/AbstractPlugin';
import {closest} from 'shared/utils';
import {CollidableInEvent, CollidableOutEvent} from './CollidableEvent';
const onDragMove = Symbol('onDragMove');
const onDragStop = Symbol('onDragStop');
const onRequestAnimationFrame = Symbol('onRequestAnimationFrame');
/**
* Collidable plugin which detects colliding elements while dragging
* @class Collidable
* @module Collidable
* @extends AbstractPlugin
*/
export default class Collidable extends AbstractPlugin {
/**
* Collidable constructor.
* @constructs Collidable
* @param {Draggable} draggable - Draggable instance
*/
constructor(draggable) {
super(draggable);
/**
* Keeps track of currently colliding elements
* @property {HTMLElement|null} currentlyCollidingElement
* @type {HTMLElement|null}
*/
this.currentlyCollidingElement = null;
/**
* Keeps track of currently colliding elements
* @property {HTMLElement|null} lastCollidingElement
* @type {HTMLElement|null}
*/
this.lastCollidingElement = null;
/**
* Animation frame for finding colliding elements
* @property {Number|null} currentAnimationFrame
* @type {Number|null}
*/
this.currentAnimationFrame = null;
this[onDragMove] = this[onDragMove].bind(this);
this[onDragStop] = this[onDragStop].bind(this);
this[onRequestAnimationFrame] = this[onRequestAnimationFrame].bind(this);
}
/**
* Attaches plugins event listeners
*/
attach() {
this.draggable.on('drag:move', this[onDragMove]).on('drag:stop', this[onDragStop]);
}
/**
* Detaches plugins event listeners
*/
detach() {
this.draggable.off('drag:move', this[onDragMove]).off('drag:stop', this[onDragStop]);
}
/**
* Returns current collidables based on `collidables` option
* @return {HTMLElement[]}
*/
getCollidables() {
const collidables = this.draggable.options.collidables;
if (typeof collidables === 'string') {
return Array.prototype.slice.call(document.querySelectorAll(collidables));
} else if (collidables instanceof NodeList || collidables instanceof Array) {
return Array.prototype.slice.call(collidables);
} else if (collidables instanceof HTMLElement) {
return [collidables];
} else if (typeof collidables === 'function') {
return collidables();
} else {
return [];
}
}
/**
* Drag move handler
* @private
* @param {DragMoveEvent} event - Drag move event
*/
[onDragMove](event) {
const target = event.sensorEvent.target;
this.currentAnimationFrame = requestAnimationFrame(this[onRequestAnimationFrame](target));
if (this.currentlyCollidingElement) {
event.cancel();
}
const collidableInEvent = new CollidableInEvent({
dragEvent: event,
collidingElement: this.currentlyCollidingElement,
});
const collidableOutEvent = new CollidableOutEvent({
dragEvent: event,
collidingElement: this.lastCollidingElement,
});
const enteringCollidable = Boolean(
this.currentlyCollidingElement && this.lastCollidingElement !== this.currentlyCollidingElement,
);
const leavingCollidable = Boolean(!this.currentlyCollidingElement && this.lastCollidingElement);
if (enteringCollidable) {
if (this.lastCollidingElement) {
this.draggable.trigger(collidableOutEvent);
}
this.draggable.trigger(collidableInEvent);
} else if (leavingCollidable) {
this.draggable.trigger(collidableOutEvent);
}
this.lastCollidingElement = this.currentlyCollidingElement;
}
/**
* Drag stop handler
* @private
* @param {DragStopEvent} event - Drag stop event
*/
[onDragStop](event) {
const lastCollidingElement = this.currentlyCollidingElement || this.lastCollidingElement;
const collidableOutEvent = new CollidableOutEvent({
dragEvent: event,
collidingElement: lastCollidingElement,
});
if (lastCollidingElement) {
this.draggable.trigger(collidableOutEvent);
}
this.lastCollidingElement = null;
this.currentlyCollidingElement = null;
}
/**
* Animation frame function
* @private
* @param {HTMLElement} target - Current move target
* @return {Function}
*/
[onRequestAnimationFrame](target) {
return () => {
const collidables = this.getCollidables();
this.currentlyCollidingElement = closest(target, (element) => collidables.includes(element));
};
}
}