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

View File

@ -3,11 +3,11 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import IndicatorSet from "./IndicatorSet.svelte" import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore, componentStore } from "stores" import { builderStore, componentStore } from "stores"
import PlaceholderOverlay from "./PlaceholderOverlay.svelte" import DNDPlaceholderOverlay from "./DNDPlaceholderOverlay.svelte"
import { Utils } from "@budibase/frontend-core" import { Utils } from "@budibase/frontend-core"
import { findComponentById } from "utils/components.js" import { findComponentById } from "utils/components.js"
let sourceId let sourceInfo
let targetInfo let targetInfo
let dropInfo let dropInfo
@ -15,7 +15,8 @@
// the value of one of the properties actually changes // the value of one of the properties actually changes
$: parent = dropInfo?.parent $: parent = dropInfo?.parent
$: index = dropInfo?.index $: 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 // Util to get the inner DOM node by a component ID
const getDOMNode = id => { const getDOMNode = id => {
@ -37,14 +38,14 @@
// Callback when drag stops (whether dropped or not) // Callback when drag stops (whether dropped or not)
const stopDragging = () => { const stopDragging = () => {
// Reset state // Reset state
sourceId = null sourceInfo = null
targetInfo = null targetInfo = null
dropInfo = null dropInfo = null
builderStore.actions.setDragging(false) builderStore.actions.setDragging(false)
// Reset listener // Reset listener
if (sourceId) { if (sourceInfo) {
const component = document.getElementsByClassName(sourceId)[0] const component = document.getElementsByClassName(sourceInfo.id)[0]
if (component) { if (component) {
component.removeEventListener("dragend", stopDragging) component.removeEventListener("dragend", stopDragging)
} }
@ -65,14 +66,33 @@
component.addEventListener("dragend", stopDragging) component.addEventListener("dragend", stopDragging)
// Update state // Update state
sourceId = component.dataset.id const parentId = component.dataset.parent
builderStore.actions.selectComponent(sourceId) 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) builderStore.actions.setDragging(true)
// Execute this asynchronously so we don't kill the drag event by hiding // Set initial drop info to show placeholder exactly where the dragged
// the component in the same handler as starting the drag event // 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(() => { setTimeout(() => {
onDragEnter(e) dropInfo = {
parent: parentId,
index,
}
}, 0) }, 0)
} }
@ -182,7 +202,7 @@
// Callback when on top of a component // Callback when on top of a component
const onDragOver = e => { const onDragOver = e => {
if (!sourceId || !targetInfo) { if (!sourceInfo || !targetInfo) {
return return
} }
handleEvent(e) handleEvent(e)
@ -190,14 +210,14 @@
// Callback when entering a potential drop target // Callback when entering a potential drop target
const onDragEnter = e => { const onDragEnter = e => {
if (!sourceId) { if (!sourceInfo) {
return return
} }
// Find the next valid component to consider dropping over, ignoring nested // Find the next valid component to consider dropping over, ignoring nested
// block components // block components
const component = e.target?.closest?.( const component = e.target?.closest?.(
`.component:not(.block):not(.${sourceId})` `.component:not(.block):not(.${sourceInfo.id})`
) )
if (component && component.classList.contains("droppable")) { if (component && component.classList.contains("droppable")) {
targetInfo = { targetInfo = {
@ -216,7 +236,7 @@
let target, mode let target, mode
// Convert parent + index into 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( const parent = findComponentById(
get(componentStore).currentAsset?.props, get(componentStore).currentAsset?.props,
dropInfo.parent dropInfo.parent
@ -225,9 +245,17 @@
return 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 // Filter out source component and placeholder from consideration
const children = parent._children?.filter( 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 // Use inside if no existing children
@ -244,7 +272,7 @@
} }
if (target && mode) { if (target && mode) {
builderStore.actions.moveComponent(sourceId, target, mode) builderStore.actions.moveComponent(sourceInfo.id, target, mode)
} }
} }
@ -278,5 +306,5 @@
/> />
{#if $builderStore.isDragging} {#if $builderStore.isDragging}
<PlaceholderOverlay /> <DNDPlaceholderOverlay />
{/if} {/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> <script>
import { onMount } from "svelte" import { onMount } from "svelte"
import { DNDPlaceholderID } from "constants"
let left, top, height, width let left, top, height, width
onMount(() => { onMount(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
const node = document.getElementById("placeholder") const node = document.getElementById(DNDPlaceholderID)
if (!node) { if (!node) {
height = 0 height = 0
width = 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", ClearForm: "ClearForm",
ChangeFormStep: "ChangeFormStep", 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 { writable, get } from "svelte/store"
import { API } from "api" import { API } from "api"
import { devToolsStore } from "./devTools.js" import { devToolsStore } from "./devTools.js"
import { findComponentPathById } from "../utils/components.js"
const dispatchEvent = (type, data = {}) => { const dispatchEvent = (type, data = {}) => {
window.parent.postMessage({ type, data }) window.parent.postMessage({ type, data })
@ -21,9 +20,9 @@ const createBuilderStore = () => {
navigation: null, navigation: null,
hiddenComponentIds: [], hiddenComponentIds: [],
usedPlugins: null, usedPlugins: null,
dndParent: null, dndParent: null,
dndIndex: null, dndIndex: null,
dndBounds: null,
// Legacy - allow the builder to specify a layout // Legacy - allow the builder to specify a layout
layout: null, layout: null,
@ -109,10 +108,11 @@ const createBuilderStore = () => {
// Notify the builder so we can reload component definitions // Notify the builder so we can reload component definitions
dispatchEvent("reload-plugin") dispatchEvent("reload-plugin")
}, },
updateDNDPlaceholder: (parent, index) => { updateDNDPlaceholder: (parent, index, bounds) => {
store.update(state => { store.update(state => {
state.dndParent = parent state.dndParent = parent
state.dndIndex = index state.dndIndex = index
state.dndBounds = bounds
return state return state
}) })
}, },

View File

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

View File

@ -5,6 +5,7 @@ import { appStore } from "./app"
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"
import { DNDPlaceholderID, DNDPlaceholderType } from "constants"
const createScreenStore = () => { const createScreenStore = () => {
const store = derived( const store = derived(
@ -58,8 +59,8 @@ const createScreenStore = () => {
// Insert placeholder // Insert placeholder
const placeholder = { const placeholder = {
_component: "placeholder", _component: DNDPlaceholderID,
_id: "placeholder", _id: DNDPlaceholderType,
static: true, static: true,
} }
let parent = findComponentById(activeScreen.props, dndParent) let parent = findComponentById(activeScreen.props, dndParent)