class InforDraggable extends HTMLElement {
static callbacks = {};
constructor() {
super();
this.grabbed_item = null;
this.grabbed_copy = null;
this.offset_x = 0;
this.offset_y = 0;
this.container = null;
this.movement_x_delta = 0;
this.movement_y_delta = 0;
this.appendStyle();
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
}
static get observedAttributes() {
return ['id', 'onchange', 'wire:onchange'];
}
connectedCallback(){
if(!this.id){
this.id = 'infor-draggable-' + Math.random().toString(36).substring(2, 15);
}
this.addEventListener('mousedown', this.onMouseDown);
}
disconnectedCallback() {
this.removeEventListener('mousedown', this.onMouseDown);
}
onMouseDown(event){
if(event.button !== 0){ return; }
const item = event.target.closest('infor-draggable-item');
if(!item){ return; }
const parent = item.parentElement;
if(parent.tagName !== 'INFOR-DRAGGABLE' || parent !== this){ return; }
event.preventDefault();
event.stopPropagation();
const container = parent
const rect = item.getBoundingClientRect();
this.offset_x = event.clientX - rect.left;
this.offset_y = event.clientY - rect.top;
this.container = container;
this.grabbed_item = item;
document.addEventListener('mousemove', this.onMouseMove);
document.addEventListener('mouseup', this.onMouseUp);
this.current_values = this.values;
this.createDraggableCopy()
this.updateDraggableCopyPosition(event);
}
onMouseMove(event){
this.setMoveDelta(event);
this.updateDraggableCopyPosition(event);
this.placeItemIntoBestPosition();
}
onMouseUp(){
document.removeEventListener('mousemove', this.onMouseMove);
document.removeEventListener('mouseup', this.onMouseUp);
this.destroyDraggableCopy();
if(this.valuesChanged()){
this.callback(this.values, this.id);
this.onchange(this.values, this.id);
this.livewireCallback(this.values, this.id);
}
}
appendStyle(){
if(InforDraggable.style_appended){
return;
}
InforDraggable.style_appended = true;
const style = document.createElement('style');
style.textContent = `
infor-draggable{
position: relative;
display: flex;
flex-direction: column;
gap: .5rem;
}
infor-draggable-item{
display: block;
cursor: grab;
}
`;
document.head.appendChild(style);
}
get values(){
return this.items.map(el => el.value);
}
get items(){
return Array.from(this.children)
.filter(el => {
return el.tagName === 'INFOR-DRAGGABLE-ITEM'
&& !el.hasAttribute('data-copy');
});
}
set current_values(values){
this._current_values = values;
}
get current_values(){
return this._current_values;
}
valuesChanged(){
return JSON.stringify(this.current_values) !== JSON.stringify(this.values);
}
get id(){
return this.getAttribute('id');
}
set id(id){
this.setAttribute('id', id);
}
get callback(){
return InforDraggable.callbacks[this.id] || ((values) => {});
}
get onchange(){
const onchange = this.getAttribute('onchange');
if(!onchange || typeof window[onchange] !== 'function'){
return (values) => { };
}
return window[onchange];
}
get livewireCallback(){
const onchange = this.getAttribute('wire:onchange');
if(!window.Livewire || !onchange){
return (values) => { }
}
return (values, id) => {
window.Livewire.emit(onchange, values, id);
}
}
setMoveDelta(event){
this.movement_x_delta = event.movementX;
this.movement_y_delta = event.movementY;
}
createDraggableCopy() {
const copy = this.grabbed_item.cloneNode(true);
const rect = this.grabbed_item.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
copy.style.position = 'absolute';
copy.style.width = `${width}px`;
copy.style.height = `${height}px`;
copy.style.cursor = 'grabbing';
copy.setAttribute('data-copy', 'true');
this.container.appendChild(copy);
this.grabbed_copy = copy;
this.grabbed_item.style.opacity = '0';
}
destroyDraggableCopy(){
this.grabbed_copy.remove();
this.grabbed_copy = null;
this.grabbed_item.style.opacity = '1';
}
updateDraggableCopyPosition(event){
const container_rect = this.container.getBoundingClientRect();
const container_left = container_rect.left + window.scrollX;
const container_top = container_rect.top + window.scrollY;
const container_width = this.container.clientWidth;
const container_height = this.container.clientHeight;
const container_style = getComputedStyle(this.container);
const container_padding_left = parseFloat(container_style.paddingLeft);
const container_padding_top = parseFloat(container_style.paddingTop);
const item_rect = this.grabbed_item.getBoundingClientRect();
const item_width = item_rect.width;
const item_height = item_rect.height;
let x = event.pageX - container_left - this.offset_x;
let y = event.pageY - container_top - this.offset_y;
//Clamp to container if overflows horizontally
if(x + item_width > container_width + container_padding_left){
x = container_width - item_width + container_padding_left;
}
else if(x < container_padding_left){
x = container_padding_left;
}
//Clamp to container if overflows vertically
if(y + item_height > container_height + container_padding_top){
y = container_height - item_height + container_padding_top;
}
else if(y < container_padding_top){
y = container_padding_top;
}
this.grabbed_copy.style.position = 'absolute';
this.grabbed_copy.style.left = `${x}px`;
this.grabbed_copy.style.top = `${y}px`;
}
placeItemIntoBestPosition(){
if(!this.grabbed_item || !this.grabbed_copy){ return; }
let items = this.items.filter(el => el !== this.grabbed_item);
// const items = this.items;
const copy_bounding_rect = this.grabbed_copy.getBoundingClientRect();
//This is necessary to avoid mutation while iterating
if (this.movement_y_delta < 0) {
items = items.reverse();
}
items.forEach((item, index) => {
const item_bounding_rect = item.getBoundingClientRect();
const distance_below = Math.abs(copy_bounding_rect.bottom - item_bounding_rect.top);
const distance_above = Math.abs(copy_bounding_rect.top - item_bounding_rect.bottom);
//Mouse movement to the bottom, and copy bottom i 5px below next item top
if(
this.movement_y_delta > 0
&& copy_bounding_rect.bottom > item_bounding_rect.top
&& distance_below > 5
){
item.after(this.grabbed_item);
}
//Mouse movement to the top, and copy top is 5px above item bottom
if(
this.movement_y_delta < 0
&& copy_bounding_rect.top < item_bounding_rect.bottom
&& distance_above > 5
){
item.before(this.grabbed_item);
}
})
}
static onChange(id, callback){
InforDraggable.callbacks[id] = callback;
}
static values(id){
return document.getElementById(id).values;
}
}
class InforDraggableItem extends HTMLElement {
constructor() {
super();
}
static get observedAttributes() {
return ['value'];
}
get value() {
return this.getAttribute('value');
}
set value(val) {
this.setAttribute('value', val);
}
}
customElements.define('infor-draggable', InforDraggable);
customElements.define('infor-draggable-item', InforDraggableItem);