dnd component nav

This commit is contained in:
Michael Shanks 2020-08-12 16:28:19 +01:00
parent 6e49506348
commit 2db40c6d2c
5 changed files with 185 additions and 52 deletions

View File

@ -1,4 +1,4 @@
import { values } from "lodash/fp" import { values, cloneDeep } from "lodash/fp"
import { get_capitalised_name } from "../../helpers" import { get_capitalised_name } from "../../helpers"
import { backendUiStore } from "builderStore" import { backendUiStore } from "builderStore"
import * as backendStoreActions from "./backend" import * as backendStoreActions from "./backend"
@ -24,8 +24,8 @@ import {
saveCurrentPreviewItem as _saveCurrentPreviewItem, saveCurrentPreviewItem as _saveCurrentPreviewItem,
saveScreenApi as _saveScreenApi, saveScreenApi as _saveScreenApi,
regenerateCssForCurrentScreen, regenerateCssForCurrentScreen,
generateNewIdsForComponent,
} from "../storeUtils" } from "../storeUtils"
export const getStore = () => { export const getStore = () => {
const initial = { const initial = {
apps: [], apps: [],
@ -70,6 +70,8 @@ export const getStore = () => {
store.addTemplatedComponent = addTemplatedComponent(store) store.addTemplatedComponent = addTemplatedComponent(store)
store.setMetadataProp = setMetadataProp(store) store.setMetadataProp = setMetadataProp(store)
store.editPageOrScreen = editPageOrScreen(store) store.editPageOrScreen = editPageOrScreen(store)
store.pasteComponent = pasteComponent(store)
store.storeComponentForCopy = storeComponentForCopy(store)
return store return store
} }
@ -454,3 +456,50 @@ const setMetadataProp = store => (name, prop) => {
return s return s
}) })
} }
const storeComponentForCopy = store => (component, cut = false) => {
store.update(s => {
const copiedComponent = cloneDeep(component)
s.componentToPaste = copiedComponent
s.componentToPaste.isCut = cut
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
}
const pasteComponent = store => (targetComponent, mode) => {
store.update(s => {
if (!s.componentToPaste) return s
const componentToPaste = cloneDeep(s.componentToPaste)
// retain the same ids as things may be referencing this component
if (componentToPaste.isCut) {
// in case we paste a second time
s.componentToPaste.isCut = false
} else {
generateNewIdsForComponent(componentToPaste)
}
delete componentToPaste.isCut
if (mode === "inside") {
targetComponent._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, targetComponent)
const targetIndex = parent._children.indexOf(targetComponent)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
regenerateCssForCurrentScreen(s)
_saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
}

View File

@ -1,6 +1,7 @@
import { makePropsSafe } from "components/userInterface/pagesParsing/createProps" import { makePropsSafe } from "components/userInterface/pagesParsing/createProps"
import api from "./api" import api from "./api"
import { generate_screen_css } from "./generate_css" import { generate_screen_css } from "./generate_css"
import { uuid } from "./uuid"
export const selectComponent = (state, component) => { export const selectComponent = (state, component) => {
const componentDef = component._component.startsWith("##") const componentDef = component._component.startsWith("##")
@ -79,3 +80,8 @@ export const regenerateCssForCurrentScreen = state => {
]) ])
return state return state
} }
export const generateNewIdsForComponent = c =>
walkProps(c, p => {
p._id = uuid()
})

View File

@ -28,8 +28,7 @@
}) })
$: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false)) $: dropdown && UIkit.util.on(dropdown, "shown", () => (hidden = false))
$: noChildrenAllowed = $: noChildrenAllowed =
!component || !component || !getComponentDefinition($store, component._component).children
getComponentDefinition($store, component._component).children === false
$: noPaste = !$store.componentToPaste $: noPaste = !$store.componentToPaste
const lastPartOfName = c => (c ? last(c._component.split("/")) : "") const lastPartOfName = c => (c ? last(c._component.split("/")) : "")
@ -105,49 +104,14 @@
}) })
} }
const generateNewIdsForComponent = c =>
walkProps(c, p => {
p._id = uuid()
})
const storeComponentForCopy = (cut = false) => { const storeComponentForCopy = (cut = false) => {
store.update(s => { // lives in store - also used by drag drop
const copiedComponent = cloneDeep(component) store.storeComponentForCopy(component, cut)
s.componentToPaste = copiedComponent
if (cut) {
const parent = getParent(s.currentPreviewItem.props, component._id)
parent._children = parent._children.filter(c => c._id !== component._id)
selectComponent(s, parent)
}
return s
})
} }
const pasteComponent = mode => { const pasteComponent = mode => {
store.update(s => { // lives in store - also used by drag drop
if (!s.componentToPaste) return s store.pasteComponent(component, mode)
const componentToPaste = cloneDeep(s.componentToPaste)
generateNewIdsForComponent(componentToPaste)
delete componentToPaste._cutId
if (mode === "inside") {
component._children.push(componentToPaste)
return s
}
const parent = getParent(s.currentPreviewItem.props, component)
const targetIndex = parent._children.indexOf(component)
const index = mode === "above" ? targetIndex : targetIndex + 1
parent._children.splice(index, 0, cloneDeep(componentToPaste))
regenerateCssForCurrentScreen(s)
saveCurrentPreviewItem(s)
selectComponent(s, componentToPaste)
return s
})
} }
</script> </script>

View File

@ -7,6 +7,7 @@
import { store } from "builderStore" import { store } from "builderStore"
import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/" import { ArrowDownIcon, ShapeIcon } from "components/common/Icons/"
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
import { writable } from "svelte/store"
export let screens = [] export let screens = []
@ -16,12 +17,15 @@
const joinPath = join("/") const joinPath = join("/")
const normalizedName = name => const normalizedName = name =>
pipe(name, [ pipe(
name,
[
trimCharsStart("./"), trimCharsStart("./"),
trimCharsStart("~/"), trimCharsStart("~/"),
trimCharsStart("../"), trimCharsStart("../"),
trimChars(" "), trimChars(" "),
]) ]
)
const changeScreen = screen => { const changeScreen = screen => {
store.setCurrentScreen(screen.props._instanceName) store.setCurrentScreen(screen.props._instanceName)
@ -57,7 +61,8 @@
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children} {#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children}
<ComponentsHierarchyChildren <ComponentsHierarchyChildren
components={screen.props._children} components={screen.props._children}
currentComponent={$store.currentComponentInfo} /> currentComponent={$store.currentComponentInfo}
dragDropStore={writable({})} />
{/if} {/if}
{/each} {/each}

View File

@ -15,11 +15,19 @@
export let currentComponent export let currentComponent
export let onSelect = () => {} export let onSelect = () => {}
export let level = 0 export let level = 0
export let dragDropStore
let dropUnderComponent
let componentToDrop
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => (!s ? "" : last(s.split("/"))) const get_name = s => (!s ? "" : last(s.split("/")))
const get_capitalised_name = name => pipe(name, [get_name, capitalise]) const get_capitalised_name = name =>
pipe(
name,
[get_name, capitalise]
)
const isScreenslot = name => name === "##builtin/screenslot" const isScreenslot = name => name === "##builtin/screenslot"
const selectComponent = component => { const selectComponent = component => {
@ -32,15 +40,92 @@
// Go to correct URL // Go to correct URL
$goto(`./:page/:screen/${path}`) $goto(`./:page/:screen/${path}`)
} }
const dragstart = component => e => {
e.dataTransfer.dropEffect = "move"
dragDropStore.update(s => {
s.componentToDrop = component
return s
})
}
const dragover = (component, index) => e => {
const canHaveChildrenButIsEmpty =
$store.components[component._component].children &&
component._children.length === 0
e.dataTransfer.dropEffect = "copy"
dragDropStore.update(s => {
const isBottomHalf = e.offsetY > e.currentTarget.offsetHeight / 2
s.targetComponent = component
// only allow dropping inside when container type
// is empty. If it has children, the user can drag over
// it's existing children
if (canHaveChildrenButIsEmpty) {
if (index === 0) {
// when its the first component in the screen,
// we divide into 3, so we can paste above, inside or below
const pos = e.offsetY / e.currentTarget.offsetHeight
if (pos < 0.4) {
s.dropPosition = "above"
} else if (pos > 0.8) {
// purposely giving this the least space as it is often covered
// by the component below's "above" space
s.dropPosition = "below"
} else {
s.dropPosition = "inside"
}
} else {
s.dropPosition = isBottomHalf ? "below" : "inside"
}
} else {
s.dropPosition = isBottomHalf ? "below" : "above"
}
return s
})
return false
}
const drop = () => {
if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) {
store.storeComponentForCopy($dragDropStore.componentToDrop, true)
store.pasteComponent(
$dragDropStore.targetComponent,
$dragDropStore.dropPosition
)
}
dragDropStore.update(s => {
s.dropPosition = ""
s.targetComponent = null
s.componentToDrop = null
return s
})
}
</script> </script>
<ul> <ul>
{#each components as component, index (component._id)} {#each components as component, index (component._id)}
<li on:click|stopPropagation={() => selectComponent(component)}> <li on:click|stopPropagation={() => selectComponent(component)}>
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition == 'above'}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {level * 20 + 40}px" />
{/if}
<div <div
class="budibase__nav-item item" class="budibase__nav-item item"
class:selected={currentComponent === component} class:selected={currentComponent === component}
style="padding-left: {level * 20 + 40}px"> style="padding-left: {level * 20 + 40}px"
draggable={true}
on:dragstart={dragstart(component)}
on:dragover={dragover(component, index)}
on:drop={drop}
ondragover="return false"
ondragenter="return false">
<div class="nav-item"> <div class="nav-item">
<i class="icon ri-arrow-right-circle-line" /> <i class="icon ri-arrow-right-circle-line" />
{isScreenslot(component._component) ? 'Screenslot' : component._instanceName} {isScreenslot(component._component) ? 'Screenslot' : component._instanceName}
@ -55,8 +140,27 @@
components={component._children} components={component._children}
{currentComponent} {currentComponent}
{onSelect} {onSelect}
{dragDropStore}
level={level + 1} /> level={level + 1} />
{/if} {/if}
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition == 'inside'}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {(level + 2) * 20 + 40}px" />
{/if}
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition == 'below'}
<div
on:drop={drop}
ondragover="return false"
ondragenter="return false"
class="budibase__nav-item item drop-item"
style="margin-left: {level * 20 + 40}px" />
{/if}
</li> </li>
{/each} {/each}
</ul> </ul>
@ -78,6 +182,11 @@
align-items: center; align-items: center;
} }
.drop-item {
background: var(--blue-light);
height: 36px;
}
.actions { .actions {
display: none; display: none;
height: 24px; height: 24px;