@alegendstale/holly-components
Version:
Reusable UI components created using lit
122 lines (121 loc) • 5.1 kB
JavaScript
export class DragDrop {
constructor(dropzones, draggables, onDrop) {
this.dropzones = dropzones;
this.draggables = draggables;
this.result = { beforeElement: null, insertedElement: null, afterElement: null, order: this.draggables };
this.onDrop = onDrop;
this.load();
}
dragStart(draggable, e) {
draggable.classList.toggle('is-dragging', true);
}
dragOver(dropzone, e) {
if (e.dataTransfer) {
const hasElement = Array.from(e.dataTransfer.items).some((item) => {
return item.kind !== 'element';
});
// Return early if element being dragged is not an element
if (hasElement)
return;
}
e.preventDefault();
this.result.beforeElement = this.getDragBeforeElement(e.clientX);
this.result.insertedElement = this.getDraggingElement();
this.result.afterElement = this.getDragAfterElement(e.clientX);
const insertedIndex = this.draggables.indexOf(this.result.insertedElement);
// If at the end of draggables
if (this.result.afterElement === null) {
// Append element
dropzone.appendChild(this.result.insertedElement);
// Remove element from order
this.result.order.splice(insertedIndex, 1);
// Add to end of order
this.result.order.push(this.result.insertedElement);
}
else {
// Place element between
dropzone.insertBefore(this.result.insertedElement, this.result.afterElement);
// Remove element from order
this.result.order.splice(insertedIndex, 1);
// Get the after index, post element removal
const afterIndex = this.draggables.indexOf(this.result.afterElement);
// Insert the element before the after index
this.result.order.splice(afterIndex, 0, this.result.insertedElement);
}
}
dragEnd(draggable, e) {
draggable.classList.toggle('is-dragging', false);
this.onDrop(e, this.result);
this.result = { beforeElement: null, insertedElement: null, afterElement: null, order: this.draggables };
}
load() {
for (let draggable of this.draggables) {
// Important, so that all types of elements can be dragged, including divs & spans
draggable.setAttribute('draggable', "true");
draggable.addEventListener("dragstart", (e) => this.dragStart(draggable, e));
draggable.addEventListener("dragend", (e) => this.dragEnd(draggable, e));
}
for (let dropzone of this.dropzones) {
dropzone.addEventListener("dragover", (e) => this.dragOver(dropzone, e));
}
}
unload() {
for (let draggable of this.draggables) {
draggable.removeEventListener('dragstart', (e) => this.dragStart(draggable, e));
draggable.removeEventListener('dragend', (e) => this.dragEnd(draggable, e));
}
for (let dropzone of this.dropzones) {
dropzone.removeEventListener('dragover', (e) => this.dragOver(dropzone, e));
}
}
/**
* @returns The element which is being dragged
*/
getDraggingElement() {
return this.draggables.filter((draggable) => draggable.classList.contains('is-dragging'))[0];
}
/**
* @returns All draggable elements not being dragged
*/
getDraggableElements() {
return this.draggables.filter((draggable) => !draggable.classList.contains('is-dragging'));
}
/**
* Gets the closest element before the dragged element
*/
getDragBeforeElement(x) {
return this.getDraggableElements().reduce((closest, child) => {
const rect = child.getBoundingClientRect();
const leftPosition = x - rect.left;
const halfChildWidth = rect.width / 2;
const offset = leftPosition - halfChildWidth;
// Return the closest element to the right of the cursor
if (offset > 0 && offset < closest.offset)
return { offset, element: child };
else
return closest;
},
// Initial closest value
{ offset: Number.POSITIVE_INFINITY, element: null })
.element;
}
/**
* Gets the closest element after the dragged element
*/
getDragAfterElement(x) {
return this.getDraggableElements().reduce((closest, child) => {
const rect = child.getBoundingClientRect();
const leftPosition = x - rect.left;
const halfChildWidth = rect.width / 2;
const offset = leftPosition - halfChildWidth;
// Return the closest element to the left of the cursor
if (offset < 0 && offset > closest.offset)
return { offset, element: child };
else
return closest;
},
// Initial closest value
{ offset: Number.NEGATIVE_INFINITY, element: null })
.element;
}
}