dnd component nav
This commit is contained in:
parent
6e49506348
commit
2db40c6d2c
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue