Skip to content

Commit ef0238a

Browse files
authored
refactor: Isolate action instances (#15)
1 parent ac56cd7 commit ef0238a

File tree

4 files changed

+257
-142
lines changed

4 files changed

+257
-142
lines changed

dev/src/App.svelte

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,55 @@
11
<script>
22
import { useDropOutside } from '../../src'
33
4+
const colors = [
5+
'#865C54',
6+
'#8F5447',
7+
'#A65846',
8+
'#A9715E',
9+
'#AD8C72',
10+
'#C2B091',
11+
'#172B41',
12+
'#32465C',
13+
'#617899',
14+
'#9BA2BC',
15+
'#847999',
16+
'#50526A',
17+
'#8B8C6B',
18+
'#97A847',
19+
'#5B652C',
20+
'#6A6A40',
21+
'#F2D9BF',
22+
'#F5BAAE',
23+
'#F1A191',
24+
]
25+
426
const _onDropOutside = (node, area) => {
5-
alert(`You\'ve just dropped #${node.id} outside #${area.id}`)
27+
node.remove()
628
}
729
8-
const _onDropInside = (node, area) => {
9-
alert(`You\'ve just dropped #${node.id} inside #${area.id}`)
30+
const _onDropInside = () => {
31+
console.log('Dropped inside!')
1032
}
1133
12-
const _onDragCancel = (node) => {
13-
alert(`You\'ve just cancelled the drag of #${node.id}`)
34+
const _onDragCancel = () => {
35+
console.log('Drag cancelled!')
1436
}
1537
</script>
1638

1739
<main>
1840
<div class="container">
41+
<p class="instruction">Drop the color slots outside the white area to delete them</p>
1942
<div id="area" class="area">
20-
<div
21-
id="target"
22-
use:useDropOutside={{
23-
areaSelector: '.area',
24-
dragClassName: 'drag',
25-
onDropOutside: _onDropOutside,
26-
onDropInside: _onDropInside,
27-
onDragCancel: _onDragCancel,
28-
}}
29-
class="target"
30-
>
31-
Drag me outside the white area
32-
</div>
43+
<ul class="slot-list">
44+
{#each colors as color, index}
45+
<li use:useDropOutside={{
46+
areaSelector: '.area',
47+
onDropOutside: _onDropOutside,
48+
onDropInside: _onDropInside,
49+
onDragCancel: _onDragCancel,
50+
}} style={`background-color: ${color}`} class="slot"></li>
51+
{/each}
52+
</ul>
3353
</div>
3454
</div>
3555
</main>
@@ -49,9 +69,17 @@
4969
display: flex;
5070
flex-direction: column;
5171
align-items: center;
52-
row-gap: 3rem;
72+
row-gap: 0.5rem;
5373
}
5474
75+
.instruction {
76+
margin: 0;
77+
padding: 0;
78+
color: white;
79+
font-family: Georgia,serif;
80+
width: 300px;
81+
}
82+
5583
.area {
5684
width: 300px;
5785
height: 300px;
@@ -62,16 +90,25 @@
6290
box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.5);
6391
}
6492
65-
.target {
66-
width: 10rem;
67-
background-color: black;
68-
color: white;
69-
text-align: center;
70-
display: flex;
71-
align-items: center;
72-
justify-content: center;
73-
padding: 1rem;
74-
}
93+
.slot-list {
94+
list-style: none;
95+
margin: 0;
96+
padding: 0;
97+
display: grid;
98+
grid-template-columns: repeat(4, 1fr);
99+
grid-gap: 1rem;
100+
align-items: center;
101+
justify-items: center;
102+
}
103+
104+
.slot {
105+
width: 24px;
106+
height: 24px;
107+
margin: 0;
108+
padding: 0;
109+
border: 1px solid rgba(0, 0, 0, 0.2);
110+
border-radius: 50%;
111+
}
75112
76113
:global(.drag) {
77114
opacity: .5;

src/DragAndDrop.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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

src/__tests__/useDropOutside.test.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import { fireEvent, screen } from '@testing-library/dom'
66

77
import { createElement } from '@untemps/utils/dom/createElement'
8-
import { removeElement } from '@untemps/utils/dom/removeElement'
98
import { getElement } from '@untemps/utils/dom/getElement'
109
import { standby } from '@untemps/utils/async/standby'
1110

11+
import DragAndDrop from '../DragAndDrop'
1212
import useDropOutside from '../useDropOutside'
1313

1414
const areaSize = 200
@@ -82,6 +82,8 @@ describe('useDropOutside', () => {
8282
document.body.innerHTML = ''
8383

8484
useReturn?.destroy()
85+
86+
DragAndDrop.destroy()
8587
})
8688

8789
describe('init', () => {
@@ -164,14 +166,12 @@ describe('useDropOutside', () => {
164166
})
165167

166168
it('Sets custom class to dragged element', async () => {
167-
console.log('===')
168169
useReturn = useDropOutside(target, { ...options, dragClassName: 'gag' })
169170
fireEvent.mouseDown(target)
170171
fireEvent.mouseMove(document)
171172
screen.debug()
172173
expect(screen.getByRole('presentation')).toBeInTheDocument()
173174
expect(screen.getByRole('presentation')).toHaveStyle('background-color: black;')
174-
console.log('===')
175175
})
176176

177177
it('Sets unknown custom class to dragged element', async () => {
@@ -189,5 +189,14 @@ describe('useDropOutside', () => {
189189
fireEvent.mouseMove(document)
190190
expect(screen.getByAltText('bar')).toBeInTheDocument()
191191
})
192+
193+
it('Stores and clears instances in static class property', async () => {
194+
useDropOutside(target, options)
195+
useDropOutside(target, options)
196+
useDropOutside(target, options)
197+
expect(DragAndDrop.instances).toHaveLength(3)
198+
DragAndDrop.destroy()
199+
expect(DragAndDrop.instances).toHaveLength(0)
200+
})
192201
})
193202
})

0 commit comments

Comments
 (0)