Reduce jank by computing symmetrical component breakpoint whens considering DND candidate positions and ignoring the hidden selected component
This commit is contained in:
parent
eb1dbc7302
commit
09f2cc1f72
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue