UNPKG

@ou-imdt/utils

Version:

Utility library for interactive media development

177 lines (151 loc) 5.41 kB
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 } }