|
| 1 | +import { DOMObserver } from '@untemps/dom-observer' |
| 2 | + |
| 3 | +import { resolveDragImage } from './utils/resolveDragImage' |
| 4 | +import { getCSSDeclaration } from './utils/getCSSDeclaration' |
| 5 | +import { doElementsOverlap } from './utils/doElementsOverlap' |
| 6 | + |
| 7 | +import './useDropOutside.css' |
| 8 | + |
| 9 | +class DragAndDrop { |
| 10 | + static instances = [] |
| 11 | + |
| 12 | + #target = null |
| 13 | + #dragImage = null |
| 14 | + #dragClassName = null |
| 15 | + #onDropOutside = null |
| 16 | + #onDropInside = null |
| 17 | + #onDragCancel = null |
| 18 | + |
| 19 | + #observer = null |
| 20 | + #area = null |
| 21 | + #drag = null |
| 22 | + #holdX = 0 |
| 23 | + #holdY = 0 |
| 24 | + #dragWidth = 0 |
| 25 | + #dragHeight = 0 |
| 26 | + |
| 27 | + #boundMouseOverHandler = null |
| 28 | + #boundMouseOutHandler = null |
| 29 | + #boundMouseDownHandler = null |
| 30 | + #boundMouseMoveHandler = null |
| 31 | + #boundMouseUpHandler = null |
| 32 | + |
| 33 | + static destroy() { |
| 34 | + DragAndDrop.instances.forEach((instance) => { |
| 35 | + instance.destroy() |
| 36 | + }) |
| 37 | + DragAndDrop.instances = [] |
| 38 | + } |
| 39 | + |
| 40 | + constructor(target, areaSelector, dragImage, dragClassName, onDropOutside, onDropInside, onDragCancel) { |
| 41 | + this.#target = target |
| 42 | + this.#dragImage = dragImage |
| 43 | + this.#dragClassName = dragClassName |
| 44 | + this.#onDropOutside = onDropOutside |
| 45 | + this.#onDropInside = onDropInside |
| 46 | + this.#onDragCancel = onDragCancel |
| 47 | + |
| 48 | + this.#area = document.querySelector(areaSelector) |
| 49 | + |
| 50 | + this.#drag = this.#dragImage ? resolveDragImage(this.#dragImage) : this.#target.cloneNode(true) |
| 51 | + this.#drag.setAttribute('draggable', false) |
| 52 | + this.#drag.setAttribute('id', 'drag') |
| 53 | + this.#drag.setAttribute('role', 'presentation') |
| 54 | + this.#drag.classList.add('__drag') |
| 55 | + if (!!this.#dragClassName) { |
| 56 | + const cssText = getCSSDeclaration(this.#dragClassName, true) |
| 57 | + if (!!cssText) { |
| 58 | + this.#drag.style.cssText = cssText |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + this.#observer = new DOMObserver() |
| 63 | + this.#observer.wait(this.#drag, null, { events: [DOMObserver.ADD] }).then(() => { |
| 64 | + const { width, height } = this.#drag.getBoundingClientRect() |
| 65 | + this.#dragWidth = width |
| 66 | + this.#dragHeight = height |
| 67 | + }) |
| 68 | + |
| 69 | + this.#boundMouseOverHandler = this.#onMouseOver.bind(this) |
| 70 | + this.#boundMouseOutHandler = this.#onMouseOut.bind(this) |
| 71 | + this.#boundMouseDownHandler = this.#onMouseDown.bind(this) |
| 72 | + |
| 73 | + this.#target.addEventListener('mouseover', this.#boundMouseOverHandler, false) |
| 74 | + this.#target.addEventListener('mouseout', this.#boundMouseOutHandler, false) |
| 75 | + this.#target.addEventListener('mousedown', this.#boundMouseDownHandler, false) |
| 76 | + this.#target.addEventListener('touchstart', this.#boundMouseDownHandler, false) |
| 77 | + |
| 78 | + DragAndDrop.instances.push(this) |
| 79 | + } |
| 80 | + |
| 81 | + destroy() { |
| 82 | + this.#target.removeEventListener('mouseover', this.#boundMouseOverHandler) |
| 83 | + this.#target.removeEventListener('mouseout', this.#boundMouseOutHandler) |
| 84 | + this.#target.removeEventListener('mousedown', this.#boundMouseDownHandler) |
| 85 | + this.#target.removeEventListener('touchstart', this.#boundMouseDownHandler) |
| 86 | + |
| 87 | + this.#boundMouseOverHandler = null |
| 88 | + this.#boundMouseOutHandler = null |
| 89 | + this.#boundMouseDownHandler = null |
| 90 | + |
| 91 | + this.#observer?.clear() |
| 92 | + this.#observer = null |
| 93 | + } |
| 94 | + |
| 95 | + #onMouseOver(e) { |
| 96 | + e.target.style.cursor = 'grab' |
| 97 | + } |
| 98 | + |
| 99 | + #onMouseOut(e) { |
| 100 | + e.target.style.cursor = 'default' |
| 101 | + } |
| 102 | + |
| 103 | + #onMouseMove(e) { |
| 104 | + if (this.#drag.style.visibility === 'hidden') { |
| 105 | + this.#drag.style.visibility = 'visible' |
| 106 | + } |
| 107 | + |
| 108 | + const pageX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX |
| 109 | + const pageY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY |
| 110 | + |
| 111 | + this.#drag.style.left = pageX - (this.#dragImage ? this.#dragWidth >> 1 : this.#holdX) + 'px' |
| 112 | + this.#drag.style.top = pageY - (this.#dragImage ? this.#dragHeight >> 1 : this.#holdY) + 'px' |
| 113 | + } |
| 114 | + |
| 115 | + #onMouseDown(e) { |
| 116 | + const clientX = e.type === 'touchstart' ? e.targetTouches[0].clientX : e.clientX |
| 117 | + const clientY = e.type === 'touchstart' ? e.targetTouches[0].clientY : e.clientY |
| 118 | + this.#holdX = clientX - this.#target.getBoundingClientRect().left |
| 119 | + this.#holdY = clientY - this.#target.getBoundingClientRect().top |
| 120 | + |
| 121 | + this.#drag.style.visibility = 'hidden' |
| 122 | + this.#drag.style.cursor = 'grabbing' |
| 123 | + |
| 124 | + this.#boundMouseMoveHandler = this.#onMouseMove.bind(this) |
| 125 | + this.#boundMouseUpHandler = this.#onMouseUp.bind(this) |
| 126 | + |
| 127 | + document.addEventListener('mousemove', this.#boundMouseMoveHandler, false) |
| 128 | + document.addEventListener('mouseup', this.#boundMouseUpHandler, false) |
| 129 | + document.addEventListener('touchmove', this.#boundMouseMoveHandler, false) |
| 130 | + document.addEventListener('keydown', this.#boundMouseUpHandler) |
| 131 | + this.#target.addEventListener('touchend', this.#boundMouseUpHandler, false) |
| 132 | + this.#target.addEventListener('touchcancel', this.#boundMouseUpHandler, false) |
| 133 | + |
| 134 | + this.#target.parentNode.appendChild(this.#drag) |
| 135 | + } |
| 136 | + |
| 137 | + #onMouseUp(e) { |
| 138 | + if (e.type.startsWith('key') && e.key !== 'Escape') { |
| 139 | + return |
| 140 | + } |
| 141 | + |
| 142 | + document.removeEventListener('mousemove', this.#boundMouseMoveHandler) |
| 143 | + document.removeEventListener('mouseup', this.#boundMouseUpHandler) |
| 144 | + document.removeEventListener('touchmove', this.#boundMouseMoveHandler) |
| 145 | + document.removeEventListener('keydown', this.#boundMouseUpHandler) |
| 146 | + this.#target.removeEventListener('touchend', this.#boundMouseUpHandler) |
| 147 | + this.#target.removeEventListener('touchcancel', this.#boundMouseUpHandler) |
| 148 | + |
| 149 | + this.#boundMouseMoveHandler = null |
| 150 | + this.#boundMouseUpHandler = null |
| 151 | + |
| 152 | + const doOverlap = doElementsOverlap(this.#area, this.#drag) |
| 153 | + |
| 154 | + this.#drag.remove() |
| 155 | + |
| 156 | + setTimeout(() => { |
| 157 | + if (e.type.startsWith('key')) { |
| 158 | + this.#onDragCancel?.(this.#target, this.#area) |
| 159 | + } else if (doOverlap) { |
| 160 | + this.#onDropInside?.(this.#target, this.#area) |
| 161 | + } else { |
| 162 | + this.#onDropOutside?.(this.#target, this.#area) |
| 163 | + } |
| 164 | + }, 10) |
| 165 | + } |
| 166 | +} |
| 167 | + |
| 168 | +export default DragAndDrop |
0 commit comments