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.previous_items_length = 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', 'wire:onchange']; } connectedCallback(){ 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.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.livewireCallback(this.values); } } 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'); } get callback(){ return InforDraggable.callbacks[this.id] || ((values) => {}); } get livewireCallback(){ const onchange = this.getAttribute('wire:onchange'); if(!window.Livewire || !onchange){ return (values) => { } } return (values) => { window.Livewire.emit(onchange, values, this.id); } } 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.pointerEvents = 'none'; 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; } const items = Array.from(this.container.children) .filter(el => el !== this.grabbed_copy); const copy_bounding_rect = this.grabbed_copy.getBoundingClientRect(); const center_y = copy_bounding_rect.top + (copy_bounding_rect.height / 2); items.forEach((item, index) => { const item_bounding_rect = item.getBoundingClientRect(); //Normal case, dragged center is below next items top; if(center_y > item_bounding_rect.top){ item.after(this.grabbed_item); } //Special case, dragged items distance to the top is less than half its height const top_distance = Math.abs(copy_bounding_rect.top - item_bounding_rect.top); if(index === 0 && top_distance < copy_bounding_rect.height / 2){ item.before(this.grabbed_item); } }) } static onChange(id, callback){ InforDraggable.callbacks[id] = callback; } } 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);