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:
parent
6e0e89bbf7
commit
9d10e938a4
|
@ -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,
|
||||||
|
|
|
@ -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##"])
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue