/** @license Copyright (c) 2024 trading_peter This program is available under Apache License Version 2.0 */ import { LitElement, html, css } from 'lit'; import { EventHelpers } from '@tp/helpers/event-helpers.js'; import { closest } from '@tp/helpers/closest.js'; class TpSortable extends EventHelpers(LitElement) { static get styles() { return [ css` :host { display: block; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } ` ]; } render() { return html` `; } static get properties() { return { selector: { type: String }, handle: { type: String }, // Use long press gesture to activate dragging. longPress: { type: Boolean }, sorting: { type: Boolean, reflect: true }, tracking: { type: Boolean }, _sortables: { type: Array } }; } constructor() { super(); this.selector = 'div'; this.handle = null; this.longPress = false; this.sorting = false; this.tracking = false; this._sortables = []; } firstUpdated() { this.listen(this, 'track', '_onTrack'); this.listen(this, 'mousedown', '_onMouseDown'); } _onTrack(e) { switch (e.detail.state) { case 'track': if (!this.sorting && this._longPressJob !== false && (Math.abs(e.detail.dy) > 10 || Math.abs(e.detail.dx) > 10)) { this._cancelLongPress(); return; } if (this.sorting) { this._dy = e.detail.dy; this._direction = this._dy < 0 ? 1 : -1; this._translate3d('0px', this._dy + 'px', '0px', this._target); this._moveSiblings(); } break; case 'end': if (this.sorting) { const result = this._calcOverlap(); this._target.style.zIndex = ''; this._target.removeAttribute('is-dragged'); this._dy = 0; this._translate3d('0px', '0px', '0px', this._target); this._resetSortables(); this.sorting = false; this.dispatchEvent(new CustomEvent('items-rearranged', { detail: result, bubbles: true, composed: true })); } break; } } _onMouseDown(e) { this._longPressTarget = closest(e.composedPath()[0], this.handle || this.selector, true); if (this._longPressTarget) { this._startX = e.detail.x; this._startY = e.detail.y; const path = e.composedPath(); this._longPressJob = setTimeout(() => { this._setTouchAction(this, 'none'); this._setupTarget(path); this._longPressTarget.setAttribute('long-pressed', ''); }, this.longPress ? 200 : 0); } } onMouseUp() { this._cancelLongPress(); setTimeout(() => { this.sorting = false; }); } _setupTarget(path) { this._target = closest(path[0], this.selector, true); if (this._target) { this._target.setAttribute('is-dragged', ''); this._rect = this._target.getBoundingClientRect(); this._registerSortableElements(); this.sorting = true; this._translate3d('0px', this._dy + 'px', '0px', this._target); } } _cancelLongPress() { clearTimeout(this._longPressJob); this._longPressJob = false; if (this._longPressTarget) { this._longPressTarget.removeAttribute('long-pressed'); } this._setTouchAction(this, 'auto'); } _moveSiblings() { const targetTop = this._rect.top + this._dy; const targetHeight = this._rect.height; const isTrackingDown = () => this._dy > 0; const itemsBeforeTarget = () => { const idx = this._sortables.findIndex(item => item.el === this._target); return this._sortables.slice(0, idx); } const itemsAfterTarget = () => { const idx = this._sortables.findIndex(item => item.el === this._target); return this._sortables.slice(idx + 1); } if (isTrackingDown()) { const items = itemsAfterTarget(); for (let i = 0, li = items.length; i < li; ++i) { const item = items[i]; if (item.el !== this._target) { if (targetTop + targetHeight > item.rect.top + (item.rect.height * 0.5)) { this._translate3d('0px', -targetHeight + 'px', '0px', item.el); } if (targetTop + targetHeight < item.rect.top + (item.rect.height * 0.5)) { this._translate3d('0px', '0px', '0px', item.el); } } } } else { const items = itemsBeforeTarget(); for (let i = 0, li = items.length; i < li; ++i) { const item = items[i]; if (item.el !== this._target) { if (targetTop - (item.rect.height * 0.5) > item.rect.top) { this._translate3d('0px', '0px', '0px', item.el); } if (targetTop - (item.rect.height * 0.5) < item.rect.top) { this._translate3d('0px', targetHeight + 'px', '0px', item.el); } } } } } /** * Finds the next nearest sibling and returns the distance. * @return {Object} Distance and item of the next nearest sibling. */ _calcOverlap() { let oldIndex = 0; const list = []; for (let i = 0, li = this._sortables.length; i < li; ++i) { if (this._sortables[i].el === this._target) { oldIndex = i; } if (this._sortables[i].el !== undefined) { list.push({ top: parseInt(this._sortables[i].el.getBoundingClientRect().top, 10), el: this._sortables[i].el }); } } list.sort((a, b) => { return a.top - b.top; }); const newOrder = list.map(item => item.el); const targetIdx = newOrder.findIndex(el => el === this._target); return { oldIndex: oldIndex, newIndex: targetIdx, newOrder: newOrder, siblingBefore: targetIdx > 0 ? newOrder[targetIdx - 1] : null, siblingAfter: targetIdx < newOrder.length - 1 ? newOrder[targetIdx + 1] : null, target: this._target }; } _registerSortableElements() { this._sortables = []; let sortables = Array.from(this.querySelectorAll(this.selector)); for (let i = 0, li = sortables.length; i < li; ++i) { const el = sortables[i]; el.style.position = el.style.position || 'relative'; el.style.transition = 'transform 300ms ease-in-out'; el.style.zIndex = '0'; this._sortables.push({ el: el, rect: el.getBoundingClientRect() }); } this._target.style.zIndex = '9999'; this._target.style.transition = ''; } _resetSortables() { for (var i = 0, li = this._sortables.length; i < li; ++i) { var item = this._sortables[i]; item.el.style.transition = ''; item.el.style.transform = ''; item.el.style.zIndex = ''; } this._target = null; } _translate3d(x, y, z, node) { node = (node || this); const transformText = 'translate3d(' + x + ',' + y + ',' + z + ')'; node.style.webkitTransform = transformText; node.style.transform = transformText; } _setTouchAction(node, value) { node.style.touchAction = value; } } window.customElements.define('tp-sortable', TpSortable);