Improve DND experience, use correct size of drop placeholder and don't drop if the position is unchanged
This commit is contained in:
parent
f2d1730139
commit
b5d50a71d7
|
@ -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}
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue