src/Swappable/Swappable.js
import Draggable from '../Draggable';
import {SwappableStartEvent, SwappableSwapEvent, SwappableSwappedEvent, SwappableStopEvent} from './SwappableEvent';
const onDragStart = Symbol('onDragStart');
const onDragOver = Symbol('onDragOver');
const onDragStop = Symbol('onDragStop');
/**
* Returns an announcement message when the Draggable element is swapped with another draggable element
* @param {SwappableSwappedEvent} swappableEvent
* @return {String}
*/
function onSwappableSwappedDefaultAnnouncement({dragEvent, swappedElement}) {
const sourceText = dragEvent.source.textContent.trim() || dragEvent.source.id || 'swappable element';
const overText = swappedElement.textContent.trim() || swappedElement.id || 'swappable element';
return `Swapped ${sourceText} with ${overText}`;
}
/**
* @const {Object} defaultAnnouncements
* @const {Function} defaultAnnouncements['swappabled:swapped']
*/
const defaultAnnouncements = {
'swappabled:swapped': onSwappableSwappedDefaultAnnouncement,
};
/**
* Swappable is built on top of Draggable and allows swapping of draggable elements.
* Order is irrelevant to Swappable.
* @class Swappable
* @module Swappable
* @extends Draggable
*/
export default class Swappable extends Draggable {
/**
* Swappable constructor.
* @constructs Swappable
* @param {HTMLElement[]|NodeList|HTMLElement} containers - Swappable containers
* @param {Object} options - Options for Swappable
*/
constructor(containers = [], options = {}) {
super(containers, {
...options,
announcements: {
...defaultAnnouncements,
...(options.announcements || {}),
},
});
/**
* Last draggable element that was dragged over
* @property lastOver
* @type {HTMLElement}
*/
this.lastOver = null;
this[onDragStart] = this[onDragStart].bind(this);
this[onDragOver] = this[onDragOver].bind(this);
this[onDragStop] = this[onDragStop].bind(this);
this.on('drag:start', this[onDragStart])
.on('drag:over', this[onDragOver])
.on('drag:stop', this[onDragStop]);
}
/**
* Destroys Swappable instance.
*/
destroy() {
super.destroy();
this.off('drag:start', this._onDragStart)
.off('drag:over', this._onDragOver)
.off('drag:stop', this._onDragStop);
}
/**
* Drag start handler
* @private
* @param {DragStartEvent} event - Drag start event
*/
[onDragStart](event) {
const swappableStartEvent = new SwappableStartEvent({
dragEvent: event,
});
this.trigger(swappableStartEvent);
if (swappableStartEvent.canceled()) {
event.cancel();
}
}
/**
* Drag over handler
* @private
* @param {DragOverEvent} event - Drag over event
*/
[onDragOver](event) {
if (event.over === event.originalSource || event.over === event.source || event.canceled()) {
return;
}
const swappableSwapEvent = new SwappableSwapEvent({
dragEvent: event,
over: event.over,
overContainer: event.overContainer,
});
this.trigger(swappableSwapEvent);
if (swappableSwapEvent.canceled()) {
return;
}
// swap originally swapped element back
if (this.lastOver && this.lastOver !== event.over) {
swap(this.lastOver, event.source);
}
if (this.lastOver === event.over) {
this.lastOver = null;
} else {
this.lastOver = event.over;
}
swap(event.source, event.over);
const swappableSwappedEvent = new SwappableSwappedEvent({
dragEvent: event,
swappedElement: event.over,
});
this.trigger(swappableSwappedEvent);
}
/**
* Drag stop handler
* @private
* @param {DragStopEvent} event - Drag stop event
*/
[onDragStop](event) {
const swappableStopEvent = new SwappableStopEvent({
dragEvent: event,
});
this.trigger(swappableStopEvent);
this.lastOver = null;
}
}
function withTempElement(callback) {
const tmpElement = document.createElement('div');
callback(tmpElement);
tmpElement.parentNode.removeChild(tmpElement);
}
function swap(source, over) {
const overParent = over.parentNode;
const sourceParent = source.parentNode;
withTempElement((tmpElement) => {
sourceParent.insertBefore(tmpElement, source);
overParent.insertBefore(source, over);
sourceParent.insertBefore(over, tmpElement);
});
}