@ou-imdt/utils
Version:
Utility library for interactive media development
177 lines (151 loc) • 5.41 kB
JavaScript
import mix from '../mix.js';
import { BaseMixin, defaultState, name } from '../class/Base.js';
import { default as DispatchEventMixin, eventDetail } from '../class/mixins/DispatchEventMixin.js';
// ref - https://justinribeiro.com/chronicle/2020/07/14/handling-web-components-and-drag-and-drop-with-event.composedpath/
export const currentDraggable = Symbol('currentDraggable');
export const currentDroppable = Symbol('currentDroppable');
export const currentDragOver = Symbol('currentDragOver');
export default class DraggableModule extends mix(EventTarget).with(BaseMixin, DispatchEventMixin) { // extend EventTarget, mix base, dispatchCustomEventMixin
static get [defaultState]() {
return {
[name]: 'draggable',
draggableSelector: '',
droppableSelector: '',
draggingClass: 'dragging',
dragoverClass: 'dragover',
dropEffect: 'move',
effectAllowed: 'all',
data: (target) => ['text/html', target.outerHTML]
};
};
#shadowRootEventTargets = [];
#currentDraggable = null;
#currentDroppable = null;
#currentDragOver = null; // better name?
#active = false;
get state() {
return {
...super.state,
...this[eventDetail]
}
}
set state(value) {
super.state = value;
}
get [eventDetail]() {
return {
draggable: this[currentDraggable],
droppable: this[currentDroppable],
over: this[currentDragOver]
};
}
get [currentDraggable]() {
return this.#currentDraggable;
}
set [currentDraggable](target) {
this.#currentDraggable?.classList.remove(this.draggingClass);
target?.classList.add(this.draggingClass);
this.#currentDraggable = target;
}
get [currentDroppable]() {
return this.#currentDroppable;
}
set [currentDroppable](target) {
this.#currentDroppable?.classList.remove(this.dragoverClass);
target?.classList.add(this.dragoverClass);
this.#currentDroppable = target;
}
get [currentDragOver]() {
return this.#currentDragOver;
}
set [currentDragOver](target) {
this.#currentDragOver?.classList.remove(this.dragoverClass);
target?.classList.add(this.dragoverClass);
this.#currentDragOver = target;
}
/**
* adds drag/drop listeners to window
*/
on() {
if (this.#active) return;
this.#addEventListeners(window);
this.#active = true;
}
/**
* removes drag/drop listeners from window
*/
off() {
if (!this.#active) return;
this.#removeEventListeners(window);
this.#active = false;
}
/**
* adds drop related event listeners to target
* @note must be added to shadow roots on enter to get around event retargeting on window
* @param {EventTarget} target
*/
#addEventListeners(target) {
target.addEventListener('dragstart', this);
target.addEventListener('dragend', this);
target.addEventListener('dragenter', this);
target.addEventListener('dragover', this);
target.addEventListener('dragleave', this);
target.addEventListener('drop', this);
}
/**
* removes drop related event listeners from target
* @param {EventTarget} target
*/
#removeEventListeners(target) {
target.removeEventListener('dragstart', this);
target.removeEventListener('dragend', this);
target.removeEventListener('dragenter', this);
target.removeEventListener('dragover', this);
target.removeEventListener('dragleave', this);
target.removeEventListener('drop', this);
}
#addShadowRootEventListeners(target) {
if (this.#shadowRootEventTargets.includes(target)) return;
if (target.host.matches(`${this.droppableSelector}, ${this.draggableSelector}`)) return; // TODO check this
this.#addEventListeners(target);
this.#shadowRootEventTargets.push(target);
}
#removeShadowRootEventListeners() {
this.#shadowRootEventTargets.forEach(target => this.#removeEventListeners(target));
this.#shadowRootEventTargets.splice(0);
}
#setTransferData(e) {
e.dataTransfer.effectAllowed = this.effectAllowed;
e.dataTransfer.dropEffect = this.dropEffect;
e.dataTransfer.setData(...this.data(e));
}
handleEvent(e) {
const { type, target } = e;
if (type !== 'dragstart' && this[currentDraggable] === null) return;
e.stopPropagation(); // to other (potential) root nodes
switch (type) {
case 'dragstart':
const draggable = e.composedPath().find(el => el.matches?.(this.draggableSelector)) ?? null;
if (draggable === null) return;
this.#setTransferData(e);
this[currentDraggable] = draggable;
break;
case 'dragenter':
if (target.shadowRoot) this.#addShadowRootEventListeners(target.shadowRoot);
const droppable = e.composedPath().find(el => el.matches?.(this.droppableSelector)) ?? null;
this[currentDragOver] = target; // regardless of droppable?
if (droppable === null) return;
this[currentDroppable] = droppable;
break;
case 'dragend':
this[currentDraggable] = null;
this[currentDroppable] = null;
this[currentDragOver] = null;
this.#removeShadowRootEventListeners();
break;
}
this.dispatchEvent(type.replace('drag', ''), { target, detail: { originalEvent: e } });
if (this[currentDroppable] === null) return;
if (['dragenter', 'dragover'].includes(type)) e.preventDefault(); // allow drop
}
}