Reduce jank by computing symmetrical component breakpoint whens considering DND candidate positions and ignoring the hidden selected component

This commit is contained in:
Andrew Kingston 2022-10-07 20:00:25 +01:00
parent eb1dbc7302
commit 09f2cc1f72
3 changed files with 93 additions and 89 deletions

View File

@ -407,6 +407,7 @@
} }
const scrollIntoView = () => { const scrollIntoView = () => {
return
const node = document.getElementsByClassName(id)?.[0]?.children[0] const node = document.getElementsByClassName(id)?.[0]?.children[0]
if (!node) { if (!node) {
return return
@ -458,6 +459,9 @@
class:block={isBlock} class:block={isBlock}
class:explode={interactive && hasChildren && inDndPath} class:explode={interactive && hasChildren && inDndPath}
class:placeholder={id === "placeholder"} class:placeholder={id === "placeholder"}
class:screen={isScreen}
class:dragging={$builderStore.selectedComponentId === id &&
$builderStore.isDragging}
data-id={id} data-id={id}
data-name={name} data-name={name}
data-icon={icon} data-icon={icon}
@ -504,4 +508,7 @@
.editing :global(*:hover) { .editing :global(*:hover) {
cursor: auto; cursor: auto;
} }
.dragging {
pointer-events: none;
}
</style> </style>

View File

@ -1,12 +1,3 @@
<script context="module">
export const Sides = {
Top: "Top",
Right: "Right",
Bottom: "Bottom",
Left: "Left",
}
</script>
<script> <script>
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
@ -17,11 +8,46 @@
let dropInfo let dropInfo
let placeholderInfo let placeholderInfo
$: parent = placeholderInfo?.parent
$: index = placeholderInfo?.index
$: builderStore.actions.updateDNDPlaceholder(parent, index)
// Util to get the inner DOM node by a component ID
const getDOMNode = id => { const getDOMNode = id => {
const component = document.getElementsByClassName(id)[0] const component = document.getElementsByClassName(id)[0]
return [...component.children][0] return [...component.children][0]
} }
// Util to calculate the variance of a set of data
const variance = arr => {
const mean = arr.reduce((a, b) => a + b, 0) / arr.length
let squareSum = 0
arr.forEach(value => {
const delta = value - mean
squareSum += delta * delta
})
return squareSum / arr.length
}
// Callback when drag stops (whether dropped or not)
const stopDragging = () => {
console.log("END")
// Reset state
dragInfo = null
dropInfo = null
placeholderInfo = null
builderStore.actions.setDragging(false)
// Reset listener
if (dragInfo?.target) {
const component = document.getElementsByClassName(dragInfo.target)[0]
if (component) {
component.removeEventListener("dragend", stopDragging)
}
}
}
// Callback when initially starting a drag on a draggable component // Callback when initially starting a drag on a draggable component
const onDragStart = e => { const onDragStart = e => {
const component = e.target.closest(".component") const component = e.target.closest(".component")
@ -29,6 +55,12 @@
return return
} }
// Hide drag ghost image
e.dataTransfer.setDragImage(new Image(), 0, 0)
// Add event handler to clear all drag state when dragging ends
component.addEventListener("dragend", stopDragging)
// Update state // Update state
dragInfo = { dragInfo = {
target: component.dataset.id, target: component.dataset.id,
@ -36,37 +68,11 @@
builderStore.actions.selectComponent(dragInfo.target) builderStore.actions.selectComponent(dragInfo.target)
builderStore.actions.setDragging(true) builderStore.actions.setDragging(true)
// Highlight being dragged by setting opacity // Execute this asynchronously so we don't kill the drag event by hiding
const child = getDOMNode(component.dataset.id) // the component in the same handler as starting the drag event
if (child) { setTimeout(() => {
child.style.opacity = "0.5" onDragEnter(e)
} }, 0)
}
// Callback when drag stops (whether dropped or not)
const onDragEnd = () => {
// Reset opacity style
if (dragInfo) {
const child = getDOMNode(dragInfo.target)
if (child) {
child.style.opacity = ""
}
}
// Reset state and styles
dragInfo = null
dropInfo = null
builderStore.actions.setDragging(false)
}
const variance = arr => {
const mean = arr.reduce((a, b) => a + b, 0) / arr.length
let squareSum = 0
arr.forEach(value => {
const delta = value - mean
squareSum += delta * delta
})
return squareSum / arr.length
} }
const handleEvent = e => { const handleEvent = e => {
@ -106,7 +112,7 @@
const childCoords = [...(node.children || [])].map(node => { const childCoords = [...(node.children || [])].map(node => {
const bounds = node.children[0].getBoundingClientRect() const bounds = node.children[0].getBoundingClientRect()
return { return {
placeholder: node.classList.contains("placeholder"), // placeholder: node.classList.contains("placeholder"),
centerX: bounds.left + bounds.width / 2, centerX: bounds.left + bounds.width / 2,
centerY: bounds.top + bounds.height / 2, centerY: bounds.top + bounds.height / 2,
left: bounds.left, left: bounds.left,
@ -117,35 +123,45 @@
}) })
// Calculate the variance between each set of positions on the children // Calculate the variance between each set of positions on the children
const variances = Object.keys(childCoords[0]) const variances = Object.keys(childCoords[0]).map(key => {
.filter(x => x !== "placeholder") const coords = childCoords.map(x => x[key])
.map(key => { return {
const coords = childCoords.map(x => x[key]) variance: variance(coords),
return { side: key,
variance: variance(coords), }
side: key, })
}
})
// Sort by variance. The lowest variance position indicates whether we are // Sort by variance. The lowest variance position indicates whether we are
// in a row or column layout // in a row or column layout
variances.sort((a, b) => { variances.sort((a, b) => {
return a.variance < b.variance ? -1 : 1 return a.variance < b.variance ? -1 : 1
}) })
console.log(variances[0].side)
const column = ["centerX", "left", "right"].includes(variances[0].side) const column = ["centerX", "left", "right"].includes(variances[0].side)
console.log(column ? "COL" : "ROW") console.log(column ? "COL" : "ROW")
// Find the correct index to drop in based on the midpoints of each child // Calculate breakpoints between children
// in their primary axis. let midpoints = []
// Here we filter out the placeholder component as we do not want it to for (let i = 0; i < childCoords.length - 1; i++) {
// affect the determination of the new index. const child1 = childCoords[i]
const childPositions = column const child2 = childCoords[i + 1]
? childCoords.filter(x => !x.placeholder).map(x => x.centerY) let midpoint
: childCoords.filter(x => !x.placeholder).map(x => x.centerX) if (column) {
const top = Math.min(child1.top, child2.top)
const bottom = Math.max(child1.bottom, child2.bottom)
midpoint = (top + bottom) / 2
} else {
const left = Math.min(child1.left, child2.left)
const right = Math.max(child1.right, child2.right)
midpoint = (left + right) / 2
}
midpoints.push(midpoint)
}
// let midpoints = childCoords.map(x => (column ? x.centerY : x.centerX))
// Determine the index to drop the component in
const mousePosition = column ? mouseY : mouseX const mousePosition = column ? mouseY : mouseX
let idx = 0 let idx = 0
while (idx < childPositions.length && childPositions[idx] < mousePosition) { while (idx < midpoints.length && midpoints[idx] < mousePosition) {
idx++ idx++
} }
placeholderInfo = { placeholderInfo = {
@ -171,11 +187,7 @@
} }
const component = e.target.closest(".component:not(.block)") const component = e.target.closest(".component:not(.block)")
if ( if (component && component.classList.contains("droppable")) {
component &&
component.classList.contains("droppable") &&
component.dataset.id !== dragInfo.target
) {
// Do nothing if this is the same target // Do nothing if this is the same target
if (component.dataset.id === dropInfo?.target) { if (component.dataset.id === dropInfo?.target) {
return return
@ -195,51 +207,33 @@
} }
} }
// Callback when leaving a potential drop target.
// Since we don't style our targets, we don't need to unset anything.
const onDragLeave = () => {}
// Callback when dropping a drag on top of some component // Callback when dropping a drag on top of some component
const onDrop = e => { const onDrop = () => {
e.preventDefault() console.log("DROP")
dropInfo = null // builderStore.actions.moveComponent(
placeholderInfo = null // dragInfo.target,
dragInfo = null // dropInfo.target,
builderStore.actions.setDragging(false) // dropInfo.mode
if (dropInfo?.mode) { // )
// builderStore.actions.moveComponent(
// dragInfo.target,
// dropInfo.target,
// dropInfo.mode
// )
}
} }
$: parent = placeholderInfo?.parent
$: index = placeholderInfo?.index
$: builderStore.actions.updateDNDPlaceholder(parent, index)
onMount(() => { onMount(() => {
// Events fired on the draggable target // Events fired on the draggable target
document.addEventListener("dragstart", onDragStart, false) document.addEventListener("dragstart", onDragStart, false)
document.addEventListener("dragend", onDragEnd, false)
// Events fired on the drop targets // Events fired on the drop targets
document.addEventListener("dragover", onDragOver, false) document.addEventListener("dragover", onDragOver, false)
document.addEventListener("dragenter", onDragEnter, false) document.addEventListener("dragenter", onDragEnter, false)
document.addEventListener("dragleave", onDragLeave, false)
document.addEventListener("drop", onDrop, false) document.addEventListener("drop", onDrop, false)
}) })
onDestroy(() => { onDestroy(() => {
// Events fired on the draggable target // Events fired on the draggable target
document.removeEventListener("dragstart", onDragStart, false) document.removeEventListener("dragstart", onDragStart, false)
document.removeEventListener("dragend", onDragEnd, false)
// Events fired on the drop targets // Events fired on the drop targets
document.removeEventListener("dragover", onDragOver, false) document.removeEventListener("dragover", onDragOver, false)
document.removeEventListener("dragenter", onDragEnter, false) document.removeEventListener("dragenter", onDragEnter, false)
document.removeEventListener("dragleave", onDragLeave, false)
document.removeEventListener("drop", onDrop, false) document.removeEventListener("drop", onDrop, false)
}) })
</script> </script>

View File

@ -65,6 +65,9 @@ const createScreenStore = () => {
if (!parent._children?.length) { if (!parent._children?.length) {
parent._children = [placeholder] parent._children = [placeholder]
} else { } else {
parent._children = parent._children.filter(
x => x._id !== selectedComponentId
)
parent._children.splice(dndIndex, 0, placeholder) parent._children.splice(dndIndex, 0, placeholder)
} }
} }