src/Plugins/SortAnimation/SortAnimation.js
import AbstractPlugin from 'shared/AbstractPlugin';
const onSortableSorted = Symbol('onSortableSorted');
const onSortableSort = Symbol('onSortableSort');
/**
* SortAnimation default options
* @property {Object} defaultOptions
* @property {Number} defaultOptions.duration
* @property {String} defaultOptions.easingFunction
* @type {Object}
*/
export const defaultOptions = {
duration: 150,
easingFunction: 'ease-in-out',
};
/**
* SortAnimation plugin adds sort animation for sortable
* @class SortAnimation
* @module SortAnimation
* @extends AbstractPlugin
*/
export default class SortAnimation extends AbstractPlugin {
/**
* SortAnimation constructor.
* @constructs SortAnimation
* @param {Draggable} draggable - Draggable instance
*/
constructor(draggable) {
super(draggable);
/**
* SortAnimation options
* @property {Object} options
* @property {Number} defaultOptions.duration
* @property {String} defaultOptions.easingFunction
* @type {Object}
*/
this.options = {
...defaultOptions,
...this.getOptions(),
};
/**
* Last animation frame
* @property {Number} lastAnimationFrame
* @type {Number}
*/
this.lastAnimationFrame = null;
this.lastElements = [];
this[onSortableSorted] = this[onSortableSorted].bind(this);
this[onSortableSort] = this[onSortableSort].bind(this);
}
/**
* Attaches plugins event listeners
*/
attach() {
this.draggable.on('sortable:sort', this[onSortableSort]);
this.draggable.on('sortable:sorted', this[onSortableSorted]);
}
/**
* Detaches plugins event listeners
*/
detach() {
this.draggable.off('sortable:sort', this[onSortableSort]);
this.draggable.off('sortable:sorted', this[onSortableSorted]);
}
/**
* Returns options passed through draggable
* @return {Object}
*/
getOptions() {
return this.draggable.options.sortAnimation || {};
}
/**
* Sortable sort handler
* @param {SortableSortEvent} sortableEvent
* @private
*/
[onSortableSort]({dragEvent}) {
const {sourceContainer} = dragEvent;
const elements = this.draggable.getDraggableElementsForContainer(sourceContainer);
this.lastElements = Array.from(elements).map((el) => {
return {
domEl: el,
offsetTop: el.offsetTop,
offsetLeft: el.offsetLeft,
};
});
}
/**
* Sortable sorted handler
* @param {SortableSortedEvent} sortableEvent
* @private
*/
[onSortableSorted]({oldIndex, newIndex}) {
if (oldIndex === newIndex) {
return;
}
const effectedElements = [];
let start;
let end;
let num;
if (oldIndex > newIndex) {
start = newIndex;
end = oldIndex - 1;
num = 1;
} else {
start = oldIndex + 1;
end = newIndex;
num = -1;
}
for (let i = start; i <= end; i++) {
const from = this.lastElements[i];
const to = this.lastElements[i + num];
effectedElements.push({from, to});
}
cancelAnimationFrame(this.lastAnimationFrame);
// Can be done in a separate frame
this.lastAnimationFrame = requestAnimationFrame(() => {
effectedElements.forEach((element) => animate(element, this.options));
});
}
}
/**
* Animates two elements
* @param {Object} element
* @param {Object} element.from
* @param {Object} element.to
* @param {Object} options
* @param {Number} options.duration
* @param {String} options.easingFunction
* @private
*/
function animate({from, to}, {duration, easingFunction}) {
const domEl = from.domEl;
const x = from.offsetLeft - to.offsetLeft;
const y = from.offsetTop - to.offsetTop;
domEl.style.pointerEvents = 'none';
domEl.style.transform = `translate3d(${x}px, ${y}px, 0)`;
requestAnimationFrame(() => {
domEl.addEventListener('transitionend', resetElementOnTransitionEnd);
domEl.style.transition = `transform ${duration}ms ${easingFunction}`;
domEl.style.transform = '';
});
}
/**
* Resets animation style properties after animation has completed
* @param {Event} event
* @private
*/
function resetElementOnTransitionEnd(event) {
event.target.style.transition = '';
event.target.style.pointerEvents = '';
event.target.removeEventListener('transitionend', resetElementOnTransitionEnd);
}