Make DND feel much smoother by persisting the end position of drops, and more performance by properly memoizing some state values

This commit is contained in:
Andrew Kingston 2022-10-21 16:54:34 +01:00
parent f1714ab2a5
commit 794db1a7db
6 changed files with 107 additions and 43 deletions

View File

@ -57,7 +57,9 @@
}
// Reset state
dndStore.actions.reset()
if (!$dndStore.dropped) {
dndStore.actions.reset()
}
}
// Callback when initially starting a drag on a draggable component
@ -251,7 +253,7 @@
}
// Callback when dropping a drag on top of some component
const onDrop = e => {
const onDrop = () => {
if (!source || !drop?.parent || drop?.index == null) {
return
}
@ -299,6 +301,7 @@
}
if (legacyDropTarget && legacyDropMode) {
dndStore.actions.markDropped()
builderStore.actions.moveComponent(
source.id,
legacyDropTarget,

View File

@ -24,10 +24,6 @@ loadSpectrumIcons()
let app
const loadBudibase = async () => {
if (get(builderStore).clearGridNextLoad) {
builderStore.actions.setDragging(false)
}
// Update builder store with any builder flags
builderStore.set({
...get(builderStore),
@ -45,6 +41,11 @@ const loadBudibase = async () => {
location: window["##BUDIBASE_LOCATION##"],
})
// Reset DND state if we completed a successful drop
if (get(dndStore).dropped) {
dndStore.actions.reset()
}
// Set app ID - this window flag is set by both the preview and the real
// server rendered app HTML
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])

View File

@ -1,4 +1,5 @@
import { writable, derived } from "svelte/store"
import { computed } from "../utils/computed.js"
const createDndStore = () => {
const initialState = {
@ -10,6 +11,9 @@ const createDndStore = () => {
// Info about where the component would be dropped
drop: null,
// Whether the current drop has been completed successfully
dropped: false,
}
const store = writable(initialState)
@ -59,6 +63,13 @@ const createDndStore = () => {
store.set(initialState)
}
const markDropped = () => {
store.update(state => {
state.dropped = true
return state
})
}
return {
subscribe: store.subscribe,
actions: {
@ -67,6 +78,7 @@ const createDndStore = () => {
updateTarget,
updateDrop,
reset,
markDropped,
},
}
}
@ -77,14 +89,12 @@ export const dndStore = createDndStore()
// performance by deriving any state that needs to be externally observed.
// By doing this and using primitives, we can avoid invalidating other stores
// or components which depend on DND state unless values actually change.
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent)
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index)
export const dndBounds = derived(
export const dndParent = computed(dndStore, x => x.drop?.parent)
export const dndIndex = computed(dndStore, x => x.drop?.index)
export const dndDropped = computed(dndStore, x => x.dropped)
export const dndBounds = computed(dndStore, x => x.source?.bounds)
export const dndIsDragging = computed(dndStore, x => !!x.source)
export const dndIsNewComponent = computed(
dndStore,
$dndStore => $dndStore.source?.bounds
x => x.source?.newComponentType != null
)
export const dndIsNewComponent = derived(
dndStore,
$dndStore => $dndStore.source?.newComponentType != null
)
export const dndIsDragging = derived(dndStore, $dndStore => !!$dndStore.source)

View File

@ -22,6 +22,7 @@ export {
dndBounds,
dndIsNewComponent,
dndIsDragging,
dndDropped,
} from "./dnd"
// Context stores are layered and duplicated, so it is not a singleton

View File

@ -2,7 +2,13 @@ import { derived } from "svelte/store"
import { routeStore } from "./routes"
import { builderStore } from "./builder"
import { appStore } from "./app"
import { dndIndex, dndParent, dndIsNewComponent, dndBounds } from "./dnd.js"
import {
dndIndex,
dndParent,
dndIsNewComponent,
dndBounds,
dndDropped,
} from "./dnd.js"
import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui"
@ -18,6 +24,7 @@ const createScreenStore = () => {
dndIndex,
dndIsNewComponent,
dndBounds,
dndDropped,
],
([
$appStore,
@ -27,6 +34,7 @@ const createScreenStore = () => {
$dndIndex,
$dndIsNewComponent,
$dndBounds,
$dndDropped,
]) => {
let activeLayout, activeScreen
let screens
@ -64,39 +72,54 @@ const createScreenStore = () => {
// Insert DND placeholder if required
if (activeScreen && $dndParent && $dndIndex != null) {
const { selectedComponentId } = $builderStore
// Extract and save the selected component as we need a reference to it
// later, and we may be removing it
let selectedParent = findComponentParent(
activeScreen.props,
selectedComponentId
)
const selectedComponent = selectedParent?._children?.find(
x => x._id === selectedComponentId
)
// Remove selected component from tree if we are moving an existing
// component
const { selectedComponentId } = $builderStore
if (!$dndIsNewComponent) {
let selectedParent = findComponentParent(
activeScreen.props,
selectedComponentId
if (!$dndIsNewComponent && selectedParent) {
selectedParent._children = selectedParent._children?.filter(
x => x._id !== selectedComponentId
)
if (selectedParent) {
selectedParent._children = selectedParent._children?.filter(
x => x._id !== selectedComponentId
)
}
}
// Insert placeholder component
const placeholder = {
_component: "@budibase/standard-components/container",
_id: DNDPlaceholderID,
_styles: {
normal: {
width: `${$dndBounds?.width || 666}px`,
height: `${$dndBounds?.height || 666}px`,
opacity: 0,
},
},
static: true,
}
let parent = findComponentById(activeScreen.props, $dndParent)
if (!parent._children?.length) {
parent._children = [placeholder]
// Insert placeholder component if dragging, or artificially insert
// the dropped component in the new location if the drop completed
let componentToInsert
if ($dndDropped && !$dndIsNewComponent) {
componentToInsert = selectedComponent
} else {
parent._children.splice($dndIndex, 0, placeholder)
componentToInsert = {
_component: "@budibase/standard-components/container",
_id: DNDPlaceholderID,
_styles: {
normal: {
width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 200}px`,
opacity: 0,
},
},
static: true,
}
}
if (componentToInsert) {
let parent = findComponentById(activeScreen.props, $dndParent)
if (parent) {
if (!parent._children?.length) {
parent._children = [componentToInsert]
} else {
parent._children.splice($dndIndex, 0, componentToInsert)
}
}
}
}

View File

@ -0,0 +1,26 @@
import { writable } from "svelte/store"
const getKey = value => {
if (value == null || typeof value !== "object") {
return value
} else {
return JSON.stringify(value)
}
}
export const computed = (store, getValue) => {
const initialValue = getValue(store)
const computedStore = writable(initialValue)
let lastKey = getKey(initialValue)
store.subscribe(state => {
const value = getValue(state)
const key = getKey(value)
if (key !== lastKey) {
lastKey = key
computedStore.set(value)
}
})
return computedStore
}