@freshworks/crayons
Version:
Crayons Web Components library
276 lines (275 loc) • 10.1 kB
JavaScript
import { debounce, cloneNodeWithEvents } from './index';
//Global Variables
let dragElement;
const placeholders = [];
const DEFAULT_OPTIONS = {
sortable: false,
acceptFrom: '',
placeholderClass: '',
copy: true,
addOnDrop: true,
};
export class Draggable {
constructor(container, options) {
this.childElements = [];
this.acceptFrom = [];
this.dropped = false;
this.debouncedSetElement = debounce((childElements, draggingElement, y) => {
if (this.cancelDebouncedDrag) {
return;
}
const afterElement = this.getDragAfterElement(childElements, y);
let newElement;
// dragging inside the same container, so no need to add placeholder
if (draggingElement.parentElement.id === this.dragContainer.id) {
newElement = draggingElement;
}
else {
this.placeholder || (this.placeholder = this.createPlaceholder(draggingElement));
newElement = this.placeholder;
}
this.addElement(newElement, afterElement);
}, this, 5);
this.onDragStart = (e) => {
dragElement = e.target;
this.dropped = false;
this.cancelDebouncedDrag = false;
// Set dragElementId for Firefox
e.dataTransfer.setData('text/plain', dragElement.id);
// Set all items inside the drag container except the current element
this.childElements = Array.from(this.dragContainer.children);
const draggingElementIndex = this.childElements.indexOf(dragElement);
this.nextSibling = this.childElements[draggingElementIndex + 1];
this.childElements.splice(draggingElementIndex, 1);
e.stopPropagation();
};
this.onDragEnter = (e) => {
if (!this.canAcceptDragElement()) {
return;
}
const sortContainer = e
.composedPath()
.find((el) => el.id === this.dragContainer.id);
if (sortContainer && sortContainer !== this.previousContainer) {
this.childElements = Array.from(this.dragContainer.children);
// the drag element have entered or re-entered current drag container
this.cancelDebouncedDrag = false;
}
this.previousContainer = sortContainer;
};
this.onDragLeave = (e) => {
if (!this.canAcceptDragElement()) {
return;
}
const outTarget = e.fromElement || e.relatedTarget;
if (!e.currentTarget.contains(outTarget)) {
// Check whether the outTarget's host (in case of shadow dom) exists in currentTarget
const parentHost = this.getMatchingHost(outTarget, this.dragContainer.children[0].tagName);
if (!e.currentTarget.contains(parentHost)) {
// the drag element have left the current container(this.host)
this.previousContainer = undefined;
}
}
};
this.onDragOver = (e) => {
e.preventDefault();
if (!this.canAcceptDragElement()) {
return;
}
this.debouncedSetElement(this.childElements, dragElement, e.clientY);
};
// Both dragend and drop need to used as the drop will be fired only on the container on which the drag is dropped
// and no on the container where drag is originated.
this.onDragEnd = (e) => {
if ((!this.dropped || placeholders.length > 0) && dragElement) {
// The drag element is dropped outside the drag container
this.addElement(dragElement, this.nextSibling);
this.removePlaceholder();
}
this.resetData(e);
};
this.onDrop = (e) => {
if (!this.canAcceptDragElement()) {
return;
}
this.dropped = true;
const sortContainerId = dragElement.parentElement.id;
const newElement = this.placeholder || dragElement;
const droppedIndex = [...this.dragContainer.children].indexOf(newElement);
if (this.placeholder) {
if (this.options.addOnDrop) {
const clone = this.options.copy
? cloneNodeWithEvents(dragElement, true, true)
: dragElement;
this.placeholder.replaceWith(clone);
}
else {
this.removePlaceholder();
}
}
this.dragContainer.dispatchEvent(new CustomEvent('fwDropBase', {
cancelable: true,
bubbles: false,
detail: {
droppedElement: dragElement,
droppedIndex,
dragFromId: sortContainerId,
dropToId: this.dragContainer.id,
},
}));
this.resetData(e);
};
this.dragContainer = container;
this.options = Object.assign({}, DEFAULT_OPTIONS, options);
this.acceptFrom = this.options.acceptFrom
? this.options.acceptFrom.split(',')
: [];
this.options.sortable && this.acceptFrom.push(this.dragContainer.id);
this.addListeners();
}
addListeners() {
this.dragContainer.addEventListener('dragstart', this.onDragStart);
this.dragContainer.addEventListener('dragend', this.onDragEnd);
this.dragContainer.addEventListener('dragenter', this.onDragEnter);
this.dragContainer.addEventListener('dragleave', this.onDragLeave);
this.dragContainer.addEventListener('dragover', this.onDragOver);
this.dragContainer.addEventListener('drop', this.onDrop);
}
removeListeners() {
this.dragContainer.removeEventListener('dragstart', this.onDragStart);
this.dragContainer.removeEventListener('dragend', this.onDragEnd);
this.dragContainer.removeEventListener('dragenter', this.onDragEnter);
this.dragContainer.removeEventListener('dragleave', this.onDragLeave);
this.dragContainer.removeEventListener('dragover', this.onDragOver);
this.dragContainer.removeEventListener('drop', this.onDrop);
}
getDragAfterElement(elements, y) {
return elements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
// Subtracting mouse y position with the middle of the element
// to check whether the dragging element is above an element
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
}
else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
createPlaceholder(sourceElement) {
const placeholderClass = this.options.placeholderClass;
const containerTag = this.dragContainer.tagName;
let placeholder;
if (['UL', 'OL'].includes(containerTag)) {
placeholder = document.createElement('li');
}
else if (['TABLE', 'TBODY'].includes(containerTag)) {
placeholder = document.createElement('tr');
// set colspan to always all rows, otherwise the item can only be dropped in first column
placeholder.innerHTML = '<td colspan="100"></td>';
}
else {
placeholder = document.createElement('div');
}
// set style for the placeholder
if (typeof placeholderClass === 'string' && placeholderClass) {
placeholder.classList.add(...placeholderClass.split(' '));
}
else {
placeholder.style.height = this.getElementHeight(sourceElement) + 'px';
placeholder.style.width = this.getElementWidth(sourceElement) + 'px';
}
placeholders.push(placeholder);
return placeholder;
}
removePlaceholder() {
placeholders.forEach((placeholder) => {
placeholder.remove();
});
// TODO: better way of removing the this.placeholder
}
addElement(newElement, nextElement) {
if (nextElement) {
if (this.canInsertBefore(nextElement) &&
!(newElement === null || newElement === void 0 ? void 0 : newElement.isSameNode(nextElement))) {
this.dragContainer.insertBefore(newElement, nextElement);
}
return;
}
this.canAppendTo(this.dragContainer) &&
this.dragContainer.appendChild(newElement);
}
canAcceptDragElement() {
if (!dragElement) {
return false;
}
const sortContainerId = dragElement.parentElement.id;
return this.acceptFrom.includes(sortContainerId);
}
canInsertBefore(element) {
return element && element.pinned !== 'top';
}
canAppendTo(container) {
return container.lastElementChild.pinned !== 'bottom';
}
getHost(element) {
return element.getRootNode().host;
}
getMatchingHost(element, tagName) {
let matchingElement = element;
while (matchingElement) {
matchingElement = this.getHost(matchingElement);
if (matchingElement && matchingElement.tagName === tagName) {
return matchingElement;
}
}
return undefined;
}
resetData(e) {
e.dataTransfer.clearData();
this.previousContainer = undefined;
dragElement = undefined;
this.placeholder = undefined;
this.cancelDebouncedDrag = true;
}
getElementHeight(element) {
if (!(element instanceof HTMLElement)) {
throw new Error('You must provide a valid dom element');
}
// get calculated style of element
const style = window.getComputedStyle(element);
// get only height if element has box-sizing: border-box specified
if (style.getPropertyValue('box-sizing') === 'border-box') {
return parseInt(style.getPropertyValue('height'), 10);
}
// pick applicable properties, convert to int and reduce by adding
return ['height', 'padding-top', 'padding-bottom']
.map(function (key) {
const int = parseInt(style.getPropertyValue(key), 10);
return isNaN(int) ? 0 : int;
})
.reduce(function (sum, value) {
return sum + value;
});
}
getElementWidth(element) {
if (!(element instanceof HTMLElement)) {
throw new Error('You must provide a valid dom element');
}
// get calculated style of element
const style = window.getComputedStyle(element);
// pick applicable properties, convert to int and reduce by adding
return ['width', 'padding-left', 'padding-right']
.map(function (key) {
const int = parseInt(style.getPropertyValue(key), 10);
return isNaN(int) ? 0 : int;
})
.reduce(function (sum, value) {
return sum + value;
});
}
destroy() {
this.removeListeners();
}
}