120 lines
3.1 KiB
Svelte
120 lines
3.1 KiB
Svelte
|
<!--
|
||
|
Represent a point on the screen that can optionally be moved by the user.
|
||
|
-->
|
||
|
|
||
|
<script>
|
||
|
import { onMount, onDestroy, createEventDispatcher } from 'svelte';
|
||
|
|
||
|
/** Horizontal position of the point. */
|
||
|
export let x = 0;
|
||
|
|
||
|
/** Vertical position of the point. */
|
||
|
export let y = 0;
|
||
|
|
||
|
/** Radius of the circle that represents the point. */
|
||
|
export let size = 15;
|
||
|
|
||
|
/** Whether the user is allowed to move the point. */
|
||
|
export let movable = true;
|
||
|
|
||
|
// Reference to the <div> element that represents the point
|
||
|
let element = null;
|
||
|
|
||
|
// Bounds of the parent node, computed only once when the user
|
||
|
// starts dragging
|
||
|
let bounds;
|
||
|
|
||
|
// Whether the user is currently dragging the point
|
||
|
let dragging = false;
|
||
|
|
||
|
let dispatch = createEventDispatcher();
|
||
|
|
||
|
const start = evt =>
|
||
|
{
|
||
|
if (!movable) return;
|
||
|
|
||
|
bounds = element.parentNode.getBoundingClientRect();
|
||
|
dragging = true;
|
||
|
|
||
|
window.addEventListener('mousemove', move);
|
||
|
window.addEventListener('touchmove', move);
|
||
|
window.addEventListener('mouseup', end);
|
||
|
window.addEventListener('touchend', end);
|
||
|
};
|
||
|
|
||
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||
|
|
||
|
const move = evt =>
|
||
|
{
|
||
|
const [userX, userY] = evt.changedTouches
|
||
|
? [evt.changedTouches[0].clientX, evt.changedTouches[0].clientY]
|
||
|
: [evt.clientX, evt.clientY];
|
||
|
|
||
|
dispatch('move', {
|
||
|
x: clamp(userX - bounds.left, 0, bounds.width),
|
||
|
y: clamp(userY - bounds.top, 0, bounds.height),
|
||
|
shift: evt.shiftKey
|
||
|
});
|
||
|
};
|
||
|
|
||
|
const end = evt =>
|
||
|
{
|
||
|
dragging = false;
|
||
|
window.removeEventListener('mousemove', move);
|
||
|
window.removeEventListener('touchmove', move);
|
||
|
window.removeEventListener('mouseup', end);
|
||
|
window.removeEventListener('touchend', end);
|
||
|
};
|
||
|
|
||
|
onMount(() =>
|
||
|
{
|
||
|
element.addEventListener('mousedown', start);
|
||
|
element.addEventListener('touchstart', start);
|
||
|
});
|
||
|
|
||
|
onDestroy(() =>
|
||
|
{
|
||
|
element.removeEventListener('mousedown', start);
|
||
|
element.removeEventListener('touchstart', start);
|
||
|
window.removeEventListener('mousemove', move);
|
||
|
window.removeEventListener('touchmove', move);
|
||
|
window.removeEventListener('mouseup', end);
|
||
|
window.removeEventListener('touchend', end);
|
||
|
});
|
||
|
</script>
|
||
|
|
||
|
<style>
|
||
|
div
|
||
|
{
|
||
|
display: block;
|
||
|
position: absolute;
|
||
|
border-radius: 100%;
|
||
|
background: #333;
|
||
|
|
||
|
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
|
||
|
transition:
|
||
|
box-shadow .15s var(--easing),
|
||
|
transform .15s var(--easing),
|
||
|
opacity .15s var(--easing);
|
||
|
}
|
||
|
|
||
|
div.movable
|
||
|
{
|
||
|
cursor: pointer;
|
||
|
}
|
||
|
|
||
|
div.movable.dragging
|
||
|
{
|
||
|
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2);
|
||
|
transform: scale(1.5);
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
<div
|
||
|
style="width: {size}px; height: {size}px; left: {x - size / 2}px; top: {y - size / 2}px;"
|
||
|
class:dragging
|
||
|
class:movable
|
||
|
draggable="false"
|
||
|
bind:this={element}
|
||
|
/>
|