From d79deef5c05962cd69db488a2f8e991a751e6f33 Mon Sep 17 00:00:00 2001 From: axolotle Date: Tue, 13 May 2025 12:26:21 +0200 Subject: [PATCH] Handle draggable lines on touch screens --- .../shared/components/draggables/index.js | 88 ++++++++++++++----- .../draggables/styles/draggables.scss | 5 ++ 2 files changed, 73 insertions(+), 20 deletions(-) diff --git a/conversejs/custom/shared/components/draggables/index.js b/conversejs/custom/shared/components/draggables/index.js index 2147a367..068cb50e 100644 --- a/conversejs/custom/shared/components/draggables/index.js +++ b/conversejs/custom/shared/components/draggables/index.js @@ -1,4 +1,5 @@ // SPDX-FileCopyrightText: 2024 John Livingston +// SPDX-FileCopyrightText: 2025 Nicolas Chesnais // // SPDX-License-Identifier: AGPL-3.0-only @@ -11,6 +12,7 @@ import './styles/draggables.scss' */ export class DraggablesCustomElement extends CustomElement { currentDragged = null + droppableEl = null /** * The tag name for draggable elements. @@ -37,6 +39,9 @@ export class DraggablesCustomElement extends CustomElement { this._handleDragLeaveBinded = this._handleDragLeave.bind(this) this._handleDragEndBinded = this._handleDragEnd.bind(this) this._handleDropBinded = this._handleDrop.bind(this) + this._handleTouchStartBinded = this._handleTouchStart.bind(this) + this._handleTouchMoveBinded = this._handleTouchMove.bind(this) + this._handleTouchEndBinded = this._handleTouchEnd.bind(this) return super.initialize() } @@ -44,21 +49,29 @@ export class DraggablesCustomElement extends CustomElement { connectedCallback () { super.connectedCallback() this.currentDragged = null + this.droppableEl = null this.addEventListener('dragstart', this._handleDragStartBinded) this.addEventListener('dragover', this._handleDragOverBinded) this.addEventListener('dragleave', this._handleDragLeaveBinded) this.addEventListener('dragend', this._handleDragEndBinded) this.addEventListener('drop', this._handleDropBinded) + this.addEventListener('touchstart', this._handleTouchStartBinded) + this.addEventListener('touchmove', this._handleTouchMoveBinded) + this.addEventListener('touchend', this._handleTouchEndBinded) } disconnectedCallback () { super.disconnectedCallback() this.currentDragged = null + this.droppableEl = null this.removeEventListener('dragstart', this._handleDragStartBinded) this.removeEventListener('dragover', this._handleDragOverBinded) this.removeEventListener('dragleave', this._handleDragLeaveBinded) this.removeEventListener('dragend', this._handleDragEndBinded) this.removeEventListener('drop', this._handleDropBinded) + this.removeEventListener('touchstart', this._handleTouchStartBinded) + this.removeEventListener('touchmove', this._handleTouchMoveBinded) + this.removeEventListener('touchend', this._handleTouchEndBinded) } _isADraggableEl (target) { @@ -69,8 +82,7 @@ export class DraggablesCustomElement extends CustomElement { return target.closest?.(this.droppableTagNames.join(',')) } - _isOnTopHalf (ev, el) { - const y = ev.clientY + _isOnTopHalf (y, el) { const bounding = el.getBoundingClientRect() return (y <= bounding.y + (bounding.height / 2)) } @@ -81,13 +93,39 @@ export class DraggablesCustomElement extends CustomElement { ) } + _setCurrentDragged (draggedEl) { + console.log('[livechat drag&drop] Starting to drag a ' + this.draggableTagName + '...') + this.currentDragged = draggedEl + this._resetDropOver() + } + + _setDroppableClasses (droppableEl, y) { + // Are we on the top or bottom part of the droppableEl? + let topHalf = false + if (!this.droppableAlwaysBottomTagNames.includes(droppableEl.nodeName.toLowerCase())) { + topHalf = this._isOnTopHalf(y, droppableEl) + } + droppableEl.classList.add(topHalf ? 'livechat-drag-top-half' : 'livechat-drag-bottom-half') + droppableEl.classList.remove(topHalf ? 'livechat-drag-bottom-half' : 'livechat-drag-top-half') + } + + _tryToDrop (droppedOnEl) { + if (!droppedOnEl) return + + console.log('[livechat drag&drop] ' + this.draggableTagName + ' dropped...') + try { + this._dropDone(this.currentDragged, droppedOnEl, droppedOnEl.classList.contains('livechat-drag-top-half')) + } catch (err) { + console.error(err) + } + this._resetDropOver() + } + _handleDragStart (ev) { // The draggable=true is on a child bode const possibleEl = ev.target.parentElement if (!this._isADraggableEl(possibleEl)) { return } - console.log('[livechat drag&drop] Starting to drag a ' + this.draggableTagName + '...') - this.currentDragged = possibleEl - this._resetDropOver() + this._setCurrentDragged(possibleEl) } _handleDragOver (ev) { @@ -97,14 +135,7 @@ export class DraggablesCustomElement extends CustomElement { // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/drop_event says we should preventDefault ev.preventDefault() - - // Are we on the top or bottom part of the droppableEl? - let topHalf = false - if (!this.droppableAlwaysBottomTagNames.includes(droppableEl.nodeName.toLowerCase())) { - topHalf = this._isOnTopHalf(ev, droppableEl) - } - droppableEl.classList.add(topHalf ? 'livechat-drag-top-half' : 'livechat-drag-bottom-half') - droppableEl.classList.remove(topHalf ? 'livechat-drag-bottom-half' : 'livechat-drag-top-half') + this._setDroppableClasses(droppableEl, ev.clientY) } _handleDragLeave (ev) { @@ -124,16 +155,33 @@ export class DraggablesCustomElement extends CustomElement { let droppedOnEl = document.querySelector('.livechat-drag-bottom-half, .livechat-drag-top-half') droppedOnEl = this._getParentDroppableEl(droppedOnEl) - if (!droppedOnEl) { return } + this._tryToDrop(droppedOnEl) + } - console.log('[livechat drag&drop] ' + this.draggableTagName + ' dropped...') + _handleTouchStart (ev) { + const possibleEl = this._getParentDroppableEl(ev.target) + if (!possibleEl) return + this._setCurrentDragged(possibleEl) + } - try { - this._dropDone(this.currentDragged, droppedOnEl, droppedOnEl.classList.contains('livechat-drag-top-half')) - } catch (err) { - console.error(err) - } + _handleTouchMove (ev) { + if (!this.currentDragged) return + + const { clientX, clientY } = ev.touches[0] + const droppableEl = this._getParentDroppableEl(document.elementFromPoint(clientX, clientY)) + if (!droppableEl || droppableEl === this.droppableEl) return + + this.droppableEl = droppableEl this._resetDropOver() + this._setDroppableClasses(droppableEl, clientY) + } + + _handleTouchEnd (_ev) { + if (!this.currentDragged || !this.droppableEl) return + + this._tryToDrop(this.droppableEl) + this.currentDragged = null + this.droppableEl = null } /** diff --git a/conversejs/custom/shared/components/draggables/styles/draggables.scss b/conversejs/custom/shared/components/draggables/styles/draggables.scss index 93279389..240dec95 100644 --- a/conversejs/custom/shared/components/draggables/styles/draggables.scss +++ b/conversejs/custom/shared/components/draggables/styles/draggables.scss @@ -1,5 +1,6 @@ /* * SPDX-FileCopyrightText: 2024 John Livingston + * SPDX-FileCopyrightText: 2025 Nicolas Chesnais * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -16,4 +17,8 @@ .livechat-drag-top-half > .draggables-line { border-top: 4px solid blue; } + + [draggable="true"] { + touch-action: none; + } }