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 6e0e89bbf7
commit 9d10e938a4
6 changed files with 107 additions and 43 deletions

View File

@ -57,8 +57,10 @@
} }
// Reset state // Reset state
if (!$dndStore.dropped) {
dndStore.actions.reset() dndStore.actions.reset()
} }
}
// 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 => {
@ -251,7 +253,7 @@
} }
// 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 = () => {
if (!source || !drop?.parent || drop?.index == null) { if (!source || !drop?.parent || drop?.index == null) {
return return
} }
@ -299,6 +301,7 @@
} }
if (legacyDropTarget && legacyDropMode) { if (legacyDropTarget && legacyDropMode) {
dndStore.actions.markDropped()
builderStore.actions.moveComponent( builderStore.actions.moveComponent(
source.id, source.id,
legacyDropTarget, legacyDropTarget,

View File

@ -24,10 +24,6 @@ loadSpectrumIcons()
let app let app
const loadBudibase = async () => { const loadBudibase = async () => {
if (get(builderStore).clearGridNextLoad) {
builderStore.actions.setDragging(false)
}
// Update builder store with any builder flags // Update builder store with any builder flags
builderStore.set({ builderStore.set({
...get(builderStore), ...get(builderStore),
@ -45,6 +41,11 @@ const loadBudibase = async () => {
location: window["##BUDIBASE_LOCATION##"], 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 // Set app ID - this window flag is set by both the preview and the real
// server rendered app HTML // server rendered app HTML
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"]) appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])

View File

@ -1,4 +1,5 @@
import { writable, derived } from "svelte/store" import { writable, derived } from "svelte/store"
import { computed } from "../utils/computed.js"
const createDndStore = () => { const createDndStore = () => {
const initialState = { const initialState = {
@ -10,6 +11,9 @@ const createDndStore = () => {
// Info about where the component would be dropped // Info about where the component would be dropped
drop: null, drop: null,
// Whether the current drop has been completed successfully
dropped: false,
} }
const store = writable(initialState) const store = writable(initialState)
@ -59,6 +63,13 @@ const createDndStore = () => {
store.set(initialState) store.set(initialState)
} }
const markDropped = () => {
store.update(state => {
state.dropped = true
return state
})
}
return { return {
subscribe: store.subscribe, subscribe: store.subscribe,
actions: { actions: {
@ -67,6 +78,7 @@ const createDndStore = () => {
updateTarget, updateTarget,
updateDrop, updateDrop,
reset, reset,
markDropped,
}, },
} }
} }
@ -77,14 +89,12 @@ export const dndStore = createDndStore()
// performance by deriving any state that needs to be externally observed. // performance by deriving any state that needs to be externally observed.
// By doing this and using primitives, we can avoid invalidating other stores // By doing this and using primitives, we can avoid invalidating other stores
// or components which depend on DND state unless values actually change. // or components which depend on DND state unless values actually change.
export const dndParent = derived(dndStore, $dndStore => $dndStore.drop?.parent) export const dndParent = computed(dndStore, x => x.drop?.parent)
export const dndIndex = derived(dndStore, $dndStore => $dndStore.drop?.index) export const dndIndex = computed(dndStore, x => x.drop?.index)
export const dndBounds = derived( 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 => $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, dndBounds,
dndIsNewComponent, dndIsNewComponent,
dndIsDragging, dndIsDragging,
dndDropped,
} from "./dnd" } from "./dnd"
// Context stores are layered and duplicated, so it is not a singleton // 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 { routeStore } from "./routes"
import { builderStore } from "./builder" import { builderStore } from "./builder"
import { appStore } from "./app" 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 { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js" import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui" import { Helpers } from "@budibase/bbui"
@ -18,6 +24,7 @@ const createScreenStore = () => {
dndIndex, dndIndex,
dndIsNewComponent, dndIsNewComponent,
dndBounds, dndBounds,
dndDropped,
], ],
([ ([
$appStore, $appStore,
@ -27,6 +34,7 @@ const createScreenStore = () => {
$dndIndex, $dndIndex,
$dndIsNewComponent, $dndIsNewComponent,
$dndBounds, $dndBounds,
$dndDropped,
]) => { ]) => {
let activeLayout, activeScreen let activeLayout, activeScreen
let screens let screens
@ -64,39 +72,54 @@ const createScreenStore = () => {
// Insert DND placeholder if required // Insert DND placeholder if required
if (activeScreen && $dndParent && $dndIndex != null) { if (activeScreen && $dndParent && $dndIndex != null) {
// Remove selected component from tree if we are moving an existing
// component
const { selectedComponentId } = $builderStore const { selectedComponentId } = $builderStore
if (!$dndIsNewComponent) {
// Extract and save the selected component as we need a reference to it
// later, and we may be removing it
let selectedParent = findComponentParent( let selectedParent = findComponentParent(
activeScreen.props, activeScreen.props,
selectedComponentId selectedComponentId
) )
if (selectedParent) { const selectedComponent = selectedParent?._children?.find(
x => x._id === selectedComponentId
)
// Remove selected component from tree if we are moving an existing
// component
if (!$dndIsNewComponent && selectedParent) {
selectedParent._children = selectedParent._children?.filter( selectedParent._children = selectedParent._children?.filter(
x => x._id !== selectedComponentId x => x._id !== selectedComponentId
) )
} }
}
// Insert placeholder component // Insert placeholder component if dragging, or artificially insert
const placeholder = { // the dropped component in the new location if the drop completed
let componentToInsert
if ($dndDropped && !$dndIsNewComponent) {
componentToInsert = selectedComponent
} else {
componentToInsert = {
_component: "@budibase/standard-components/container", _component: "@budibase/standard-components/container",
_id: DNDPlaceholderID, _id: DNDPlaceholderID,
_styles: { _styles: {
normal: { normal: {
width: `${$dndBounds?.width || 666}px`, width: `${$dndBounds?.width || 400}px`,
height: `${$dndBounds?.height || 666}px`, height: `${$dndBounds?.height || 200}px`,
opacity: 0, opacity: 0,
}, },
}, },
static: true, static: true,
} }
}
if (componentToInsert) {
let parent = findComponentById(activeScreen.props, $dndParent) let parent = findComponentById(activeScreen.props, $dndParent)
if (parent) {
if (!parent._children?.length) { if (!parent._children?.length) {
parent._children = [placeholder] parent._children = [componentToInsert]
} else { } else {
parent._children.splice($dndIndex, 0, placeholder) 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
}