Pinafore-Web-Client-Frontend/src/routes/_components/Draggable.html

159 lines
5.1 KiB
HTML

<div class="draggable-area {draggableClassAfterRaf}"
on:pointerMove="onPointerMove(event)"
on:pointerLeave="onPointerLeave(event)"
on:pointerUp="onPointerUp(event)"
on:click="onClick(event)"
ref:area
>
<div class="draggable-indicator {computedIndicatorClassAfterRaf}"
style={indicatorStyleAfterRaf}
on:pointerDown="onPointerDown(event)"
ref:indicator
>
<div class="draggable-indicator-inner">
<slot></slot>
</div>
</div>
</div>
<style>
.draggable-area {
position: relative;
touch-action: none;
cursor: pointer;
}
.draggable-indicator {
position: absolute;
cursor: grab;
}
.draggable-indicator.grabbing {
cursor: grabbing;
}
.draggable-indicator-inner {
pointer-events: none;
display: flex;
}
</style>
<script>
import { observe } from 'svelte-extras'
import { throttleTimer } from '../_utils/throttleTimer.js'
import { pointerUp, pointerDown, pointerLeave, pointerMove } from '../_utils/pointerEvents.js'
import { requestPostAnimationFrame } from '../_utils/requestPostAnimationFrame.js'
import { classname } from '../_utils/classname.js'
// ensure DOM writes only happen once after a rAF
const updateIndicatorStyle = throttleTimer(requestAnimationFrame)
const updateIndicatorClass = throttleTimer(requestAnimationFrame)
const updateDraggableClass = throttleTimer(requestAnimationFrame)
// ensure DOM reads only happen once after a rPAF
const calculateGBCR = throttleTimer(requestPostAnimationFrame)
const clamp = x => Math.max(0, Math.min(1, x))
export default {
oncreate () {
this.observe('dragging', dragging => {
if (dragging) {
this.fire('dragStart')
} else {
const { x, y } = this.get()
this.fire('dragEnd')
this.fire('change', { x, y })
}
}, { init: false })
this.observe('indicatorStyle', () => {
updateIndicatorStyle(() => {
const { indicatorStyle } = this.get()
this.set({ indicatorStyleAfterRaf: indicatorStyle })
})
})
this.observe('computedIndicatorClass', () => {
updateIndicatorClass(() => {
const { computedIndicatorClass } = this.get()
this.set(({ computedIndicatorClassAfterRaf: computedIndicatorClass }))
})
})
this.observe('draggableClass', () => {
updateDraggableClass(() => {
const { draggableClass } = this.get()
this.set({ draggableClassAfterRaf: draggableClass })
})
})
},
data: () => ({
dragging: false,
draggableClass: '',
draggableClassAfterRaf: '',
indicatorClass: '',
computedIndicatorClassAfterRaf: '',
x: 0,
y: 0,
indicatorWidth: 0,
indicatorHeight: 0,
indicatorStyleAfterRaf: ''
}),
computed: {
indicatorStyle: ({ x, y, indicatorWidth, indicatorHeight }) => (
`left: calc(${x * 100}% - ${indicatorWidth / 2}px); top: calc(${y * 100}% - ${indicatorHeight / 2}px);`
),
computedIndicatorClass: ({ dragging, indicatorClass }) => classname(dragging && 'grabbing', indicatorClass)
},
methods: {
observe,
onPointerDown (e) {
console.log('Draggable: onPointerDown')
const rect = this.refs.indicator.getBoundingClientRect()
console.log('Draggable: e.clientX', e.clientX)
console.log('Draggable: e.clientY', e.clientY)
this.set({
dragging: true,
dragOffsetX: e.clientX - rect.left,
dragOffsetY: e.clientY - rect.top
})
},
onPointerMove (e) {
console.log('Draggable: onPointerMove')
const { dragging, indicatorWidth, indicatorHeight, dragOffsetX, dragOffsetY } = this.get()
if (dragging) {
console.log('Draggable: dragging')
calculateGBCR(() => {
const rect = this.refs.area.getBoundingClientRect()
const offsetX = dragOffsetX - (indicatorWidth / 2)
const offsetY = dragOffsetY - (indicatorHeight / 2)
const x = clamp((e.clientX - rect.left - offsetX) / rect.width)
const y = clamp((e.clientY - rect.top - offsetY) / rect.height)
this.set({ x, y })
})
}
},
onPointerUp (e) {
console.log('Draggable: onPointerUp')
this.set({ dragging: false })
},
onPointerLeave (e) {
console.log('Draggable: onPointerLeave')
this.set({ dragging: false })
},
onClick (e) {
console.log('Draggable: onClick')
console.log('Draggable: target classList', e.target.classList)
console.log('Draggable: currentTarget classList', e.currentTarget.classList)
if (!e.target.classList.contains('draggable-indicator')) {
console.log('Draggable: onClick handled')
const rect = this.refs.area.getBoundingClientRect()
const x = clamp((e.clientX - rect.left) / rect.width)
const y = clamp((e.clientY - rect.top) / rect.height)
this.set({ x, y })
this.fire('change', { x, y })
}
}
},
events: {
pointerUp,
pointerDown,
pointerLeave,
pointerMove
}
}
</script>