Enable left/right side targetting for DND

This commit is contained in:
Andrew Kingston 2021-09-21 08:36:02 +01:00
parent 7663bdb534
commit ee2e2799d9
3 changed files with 103 additions and 24 deletions

View File

@ -1,3 +1,12 @@
<script context="module">
export const Sides = {
Top: "Top",
Right: "Right",
Bottom: "Bottom",
Left: "Left",
}
</script>
<script>
import { onMount } from "svelte"
import { get } from "svelte/store"
@ -8,6 +17,22 @@
let dragInfo
let dropInfo
const getEdges = (bounds, mousePoint) => {
const { width, height, top, left } = bounds
return {
[Sides.Top]: [mousePoint[0], top],
[Sides.Right]: [left + width, mousePoint[1]],
[Sides.Bottom]: [mousePoint[0], top + height],
[Sides.Left]: [left, mousePoint[1]],
}
}
const calculatePointDelta = (point1, point2) => {
const deltaX = Math.abs(point1[0] - point2[0])
const deltaY = Math.abs(point1[1] - point2[1])
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
}
const getDOMNodeForComponent = component => {
const parent = component.closest(".component")
const children = Array.from(parent.childNodes)
@ -61,23 +86,57 @@
e.preventDefault()
const { droppableInside, bounds } = dropInfo
const { top, height } = bounds
const { top, left, height, width } = bounds
const mouseY = e.clientY
const mouseX = e.clientX
const snapFactor = droppableInside ? 0.33 : 0.5
const snapLimit = Math.min(40, height * snapFactor)
const edgeLimits = [
Math.round(top + snapLimit),
Math.round(top + height - snapLimit),
]
const snapLimitV = Math.min(40, height * snapFactor)
const snapLimitH = Math.min(40, width * snapFactor)
if (mouseY <= edgeLimits[0]) {
// Determine all sies we are within snap range of
let sides = []
if (mouseY <= top + snapLimitV) {
sides.push(Sides.Top)
} else if (mouseY >= top + height - snapLimitV) {
sides.push(Sides.Bottom)
}
if (mouseX < left + snapLimitH) {
sides.push(Sides.Left)
} else if (mouseX > left + width - snapLimitH) {
sides.push(Sides.Right)
}
// When no edges match, drop inside if possible
if (!sides.length) {
dropInfo.mode = droppableInside ? "inside" : null
dropInfo.side = null
return
}
// When one edge matches, use that edge
if (sides.length === 1) {
dropInfo.side = sides[0]
if ([Sides.Top, Sides.Left].includes(sides[0])) {
dropInfo.mode = "above"
} else if (mouseY >= edgeLimits[1]) {
dropInfo.mode = "below"
} else if (droppableInside) {
dropInfo.mode = "inside"
} else {
dropInfo.mode = null
dropInfo.mode = "below"
}
return
}
// When 2 edges match, work out which is closer
const mousePoint = [mouseX, mouseY]
const edges = getEdges(bounds, mousePoint)
const edge1 = edges[sides[0]]
const delta1 = calculatePointDelta(mousePoint, edge1)
const edge2 = edges[sides[1]]
const delta2 = calculatePointDelta(mousePoint, edge2)
const edge = delta1 < delta2 ? sides[0] : sides[1]
dropInfo.side = edge
if ([Sides.Top, Sides.Left].includes(edge)) {
dropInfo.mode = "above"
} else {
dropInfo.mode = "below"
}
}

View File

@ -1,39 +1,53 @@
<script>
import Indicator from "./Indicator.svelte"
import { Sides } from "./DNDHandler.svelte"
export let dropInfo
export let zIndex
export let color
export let transition
$: dimensions = getDimensions(dropInfo?.bounds, dropInfo?.mode)
$: dimensions = getDimensions(dropInfo)
$: prefix = dropInfo?.mode === "above" ? "Before" : "After"
$: text = `${prefix} ${dropInfo?.name}`
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}`
const getDimensions = (bounds, mode) => {
if (!bounds || !mode) {
const getDimensions = info => {
const { bounds, side } = info ?? {}
if (!bounds || !side) {
return null
}
const { left, top, width, height } = bounds
if (side === Sides.Top || side === Sides.Bottom) {
return {
top: mode === "above" ? top - 4 : top + height,
top: side === Sides.Top ? top - 4 : top + height,
left: left - 2,
width: width + 4,
height: 0,
}
} else {
return {
top: top - 2,
left: side === Sides.Left ? left - 4 : left + width,
width: 0,
height: height + 4,
}
}
}
</script>
{#key `${dropInfo?.target}-${dropInfo?.mode}`}
{#key renderKey}
{#if dimensions && dropInfo?.mode !== "inside"}
<Indicator
left={dimensions.left}
top={dimensions.top}
left={Math.round(dimensions.left)}
top={Math.round(dimensions.top)}
width={dimensions.width}
height={0}
height={dimensions.height}
{text}
{zIndex}
{color}
{transition}
alignRight={dropInfo?.side === Sides.Right}
line
/>
{/if}

View File

@ -10,6 +10,7 @@
export let zIndex
export let transition = false
export let line = false
export let alignRight = false
$: flipped = top < 20
</script>
@ -26,7 +27,7 @@
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
>
{#if text}
<div class="text" class:flipped class:line>
<div class="text" class:flipped class:line class:right={alignRight}>
{text}
</div>
{/if}
@ -34,6 +35,7 @@
<style>
.indicator {
right: 0;
position: absolute;
z-index: var(--zIndex);
border: 2px solid var(--color);
@ -78,4 +80,8 @@
transform: translateY(-50%) !important;
border-radius: 4px !important;
}
.text.right {
right: -2px !important;
left: auto !important;
}
</style>