Improve DND experience, use correct size of drop placeholder and don't drop if the position is unchanged

This commit is contained in:
Andrew Kingston 2022-10-11 16:02:09 +01:00
parent f2d1730139
commit b5d50a71d7
9 changed files with 100 additions and 42 deletions

View File

@ -22,6 +22,7 @@
import Placeholder from "components/app/Placeholder.svelte"
import ScreenPlaceholder from "components/app/ScreenPlaceholder.svelte"
import ComponentPlaceholder from "components/app/ComponentPlaceholder.svelte"
import { DNDPlaceholderID } from "constants"
export let instance = {}
export let isLayout = false
@ -458,7 +459,7 @@
class:editing
class:block={isBlock}
class:explode={interactive && hasChildren && inDndPath}
class:placeholder={id === "placeholder"}
class:placeholder={id === DNDPlaceholderID}
data-id={id}
data-name={name}
data-icon={icon}

View File

@ -3,11 +3,11 @@
import { get } from "svelte/store"
import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, componentStore } from "stores"
import PlaceholderOverlay from "./PlaceholderOverlay.svelte"
import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js"
let sourceId
let sourceInfo
let targetInfo
let dropInfo
@ -15,7 +15,8 @@
// the value of one of the properties actually changes
$: parent = dropInfo?.parent
$: index = dropInfo?.index
$: builderStore.actions.updateDNDPlaceholder(parent, index)
$: bounds = sourceInfo?.bounds
$: builderStore.actions.updateDNDPlaceholder(parent, index, bounds)
// Util to get the inner DOM node by a component ID
const getDOMNode = id => {
@ -37,14 +38,14 @@
// Callback when drag stops (whether dropped or not)
const stopDragging = () => {
// Reset state
sourceId = null
sourceInfo = null
targetInfo = null
dropInfo = null
builderStore.actions.setDragging(false)
// Reset listener
if (sourceId) {
const component = document.getElementsByClassName(sourceId)[0]
if (sourceInfo) {
const component = document.getElementsByClassName(sourceInfo.id)[0]
if (component) {
component.removeEventListener("dragend", stopDragging)
}
@ -65,14 +66,33 @@
component.addEventListener("dragend", stopDragging)
// Update state
sourceId = component.dataset.id
builderStore.actions.selectComponent(sourceId)
const parentId = component.dataset.parent
const parent = findComponentById(
get(componentStore).currentAsset.props,
parentId
)
const index = parent._children.findIndex(
x => x._id === component.dataset.id
)
sourceInfo = {
id: component.dataset.id,
bounds: component.children[0].getBoundingClientRect(),
parent: parentId,
index,
}
builderStore.actions.selectComponent(sourceInfo.id)
builderStore.actions.setDragging(true)
// Execute this asynchronously so we don't kill the drag event by hiding
// the component in the same handler as starting the drag event
// Set initial drop info to show placeholder exactly where the dragged
// component is.
// Execute this asynchronously to prevent bugs caused by updating state in
// the same handler as selecting a new component (which causes a client
// re-initialisation).
setTimeout(() => {
onDragEnter(e)
dropInfo = {
parent: parentId,
index,
}
}, 0)
}
@ -182,7 +202,7 @@
// Callback when on top of a component
const onDragOver = e => {
if (!sourceId || !targetInfo) {
if (!sourceInfo || !targetInfo) {
return
}
handleEvent(e)
@ -190,14 +210,14 @@
// Callback when entering a potential drop target
const onDragEnter = e => {
if (!sourceId) {
if (!sourceInfo) {
return
}
// Find the next valid component to consider dropping over, ignoring nested
// block components
const component = e.target?.closest?.(
`.component:not(.block):not(.${sourceId})`
`.component:not(.block):not(.${sourceInfo.id})`
)
if (component && component.classList.contains("droppable")) {
targetInfo = {
@ -216,7 +236,7 @@
let target, mode
// Convert parent + index into target + mode
if (sourceId && dropInfo?.parent && dropInfo.index != null) {
if (sourceInfo && dropInfo?.parent && dropInfo.index != null) {
const parent = findComponentById(
get(componentStore).currentAsset?.props,
dropInfo.parent
@ -225,9 +245,17 @@
return
}
// Do nothing if we didn't change the location
if (
sourceInfo.parent === dropInfo.parent &&
sourceInfo.index === dropInfo.index
) {
return
}
// Filter out source component and placeholder from consideration
const children = parent._children?.filter(
x => x._id !== "placeholder" && x._id !== sourceId
x => x._id !== "placeholder" && x._id !== sourceInfo.id
)
// Use inside if no existing children
@ -244,7 +272,7 @@
}
if (target && mode) {
builderStore.actions.moveComponent(sourceId, target, mode)
builderStore.actions.moveComponent(sourceInfo.id, target, mode)
}
}
@ -278,5 +306,5 @@
/>
{#if $builderStore.isDragging}
<PlaceholderOverlay />
<DNDPlaceholderOverlay />
{/if}

View File

@ -0,0 +1,33 @@
<script>
import { builderStore } from "stores"
import { DNDPlaceholderID } from "constants"
$: style = getStyle($builderStore.dndBounds)
const getStyle = bounds => {
if (!bounds) {
return null
}
return `--height: ${bounds.height}px; --width: ${bounds.width}px;`
}
</script>
{#if style}
<div class="wrapper">
<div class="placeholder" id={DNDPlaceholderID} {style} />
</div>
{/if}
<style>
.wrapper {
overflow: hidden;
}
.placeholder {
display: block;
height: var(--height);
width: var(--width);
max-height: 100%;
max-width: 100%;
opacity: 0;
}
</style>

View File

@ -1,11 +1,12 @@
<script>
import { onMount } from "svelte"
import { DNDPlaceholderID } from "constants"
let left, top, height, width
onMount(() => {
const interval = setInterval(() => {
const node = document.getElementById("placeholder")
const node = document.getElementById(DNDPlaceholderID)
if (!node) {
height = 0
width = 0

View File

@ -1,11 +0,0 @@
<div id="placeholder" class="placeholder" />
<style>
.placeholder {
display: block;
min-height: 64px;
min-width: 64px;
flex: 0 0 64px;
opacity: 0;
}
</style>

View File

@ -29,3 +29,7 @@ export const ActionTypes = {
ClearForm: "ClearForm",
ChangeFormStep: "ChangeFormStep",
}
export const DNDPlaceholderID = "dnd-placeholder"
export const DNDPlaceholderType = "dnd-placeholder"
export const ScreenslotType = "screenslot"

View File

@ -1,7 +1,6 @@
import { writable, get } from "svelte/store"
import { API } from "api"
import { devToolsStore } from "./devTools.js"
import { findComponentPathById } from "../utils/components.js"
const dispatchEvent = (type, data = {}) => {
window.parent.postMessage({ type, data })
@ -21,9 +20,9 @@ const createBuilderStore = () => {
navigation: null,
hiddenComponentIds: [],
usedPlugins: null,
dndParent: null,
dndIndex: null,
dndBounds: null,
// Legacy - allow the builder to specify a layout
layout: null,
@ -109,10 +108,11 @@ const createBuilderStore = () => {
// Notify the builder so we can reload component definitions
dispatchEvent("reload-plugin")
},
updateDNDPlaceholder: (parent, index) => {
updateDNDPlaceholder: (parent, index, bounds) => {
store.update(state => {
state.dndParent = parent
state.dndIndex = index
state.dndBounds = bounds
return state
})
},

View File

@ -5,8 +5,9 @@ import { devToolsStore } from "./devTools"
import { screenStore } from "./screens"
import { builderStore } from "./builder"
import Router from "../components/Router.svelte"
import Placeholder from "../components/preview/Placeholder.svelte"
import DNDPlaceholder from "../components/preview/DNDPlaceholder.svelte"
import * as AppComponents from "../components/app/index.js"
import { DNDPlaceholderType, ScreenslotType } from "../constants.js"
const budibasePrefix = "@budibase/standard-components/"
@ -109,9 +110,9 @@ const createComponentStore = () => {
}
// Screenslot is an edge case
if (type === "screenslot") {
if (type === ScreenslotType) {
type = `${budibasePrefix}${type}`
} else if (type === "placeholder") {
} else if (type === DNDPlaceholderType) {
return {}
}
@ -130,10 +131,10 @@ const createComponentStore = () => {
if (!type) {
return null
}
if (type === "screenslot") {
if (type === ScreenslotType) {
return Router
} else if (type === "placeholder") {
return Placeholder
} else if (type === DNDPlaceholderType) {
return DNDPlaceholder
}
// Handle budibase components

View File

@ -5,6 +5,7 @@ import { appStore } from "./app"
import { RoleUtils } from "@budibase/frontend-core"
import { findComponentById, findComponentParent } from "../utils/components.js"
import { Helpers } from "@budibase/bbui"
import { DNDPlaceholderID, DNDPlaceholderType } from "constants"
const createScreenStore = () => {
const store = derived(
@ -58,8 +59,8 @@ const createScreenStore = () => {
// Insert placeholder
const placeholder = {
_component: "placeholder",
_id: "placeholder",
_component: DNDPlaceholderID,
_id: DNDPlaceholderType,
static: true,
}
let parent = findComponentById(activeScreen.props, dndParent)