Add WIP component DND indicator refactor
This commit is contained in:
parent
e11c210bf5
commit
692ecb4139
|
@ -106,7 +106,7 @@
|
||||||
"rollup": "^2.44.0",
|
"rollup": "^2.44.0",
|
||||||
"rollup-plugin-copy": "^3.4.0",
|
"rollup-plugin-copy": "^3.4.0",
|
||||||
"start-server-and-test": "^1.12.1",
|
"start-server-and-test": "^1.12.1",
|
||||||
"svelte": "^3.38.2",
|
"svelte": "^3.48.0",
|
||||||
"svelte-jester": "^1.3.2",
|
"svelte-jester": "^1.3.2",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.4.0",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^4.5.5",
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import Panel from "components/design/Panel.svelte"
|
import Panel from "components/design/Panel.svelte"
|
||||||
import ComponentTree from "./ComponentTree.svelte"
|
import ComponentTree from "./ComponentTree.svelte"
|
||||||
import instantiateStore from "./dragDropStore.js"
|
import createDNDStore from "./dndStore.js"
|
||||||
import { goto } from "@roxi/routify"
|
import { goto } from "@roxi/routify"
|
||||||
import { store, selectedScreen, selectedComponent } from "builderStore"
|
import { store, selectedScreen } from "builderStore"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
import ScreenslotDropdownMenu from "./ScreenslotDropdownMenu.svelte"
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
|
|
||||||
const dragDropStore = instantiateStore()
|
const dndStore = createDNDStore()
|
||||||
let scrollRef
|
let scrollRef
|
||||||
|
|
||||||
const scrollTo = bounds => {
|
const scrollTo = bounds => {
|
||||||
|
@ -84,8 +84,7 @@
|
||||||
<ComponentTree
|
<ComponentTree
|
||||||
level={0}
|
level={0}
|
||||||
components={$selectedScreen?.props._children}
|
components={$selectedScreen?.props._children}
|
||||||
currentComponent={$selectedComponent}
|
{dndStore}
|
||||||
{dragDropStore}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
import { DropEffect, DropPosition } from "./dndStore"
|
||||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||||
import NavItem from "components/common/NavItem.svelte"
|
import NavItem from "components/common/NavItem.svelte"
|
||||||
import { capitalise } from "helpers"
|
import { capitalise } from "helpers"
|
||||||
|
@ -14,40 +14,23 @@
|
||||||
import { get } from "svelte/store"
|
import { get } from "svelte/store"
|
||||||
|
|
||||||
export let components = []
|
export let components = []
|
||||||
export let currentComponent
|
|
||||||
export let level = 0
|
export let level = 0
|
||||||
export let dragDropStore
|
export let dndStore
|
||||||
|
|
||||||
let closedNodes = {}
|
let closedNodes = {}
|
||||||
let indicatorY = 0
|
let ref
|
||||||
|
|
||||||
const dragstart = component => e => {
|
const dragstart = component => e => {
|
||||||
e.dataTransfer.dropEffect = DropEffect.MOVE
|
dndStore.actions.dragstart(component)
|
||||||
dragDropStore.actions.dragstart(component)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dragover = (component, index) => e => {
|
const dragover = (component, index) => e => {
|
||||||
const definition = store.actions.components.getDefinition(
|
|
||||||
component._component
|
|
||||||
)
|
|
||||||
const canHaveChildren = definition?.hasChildren
|
|
||||||
const hasChildren = componentHasChildren(component)
|
|
||||||
|
|
||||||
e.dataTransfer.dropEffect = DropEffect.COPY
|
|
||||||
|
|
||||||
// how far down the mouse pointer is on the drop target
|
|
||||||
const mousePosition = e.offsetY / e.currentTarget.offsetHeight
|
const mousePosition = e.offsetY / e.currentTarget.offsetHeight
|
||||||
|
dndStore.actions.dragover({
|
||||||
indicatorY = e.currentTarget.offsetTop
|
|
||||||
|
|
||||||
dragDropStore.actions.dragover({
|
|
||||||
component,
|
component,
|
||||||
index,
|
index,
|
||||||
canHaveChildren,
|
|
||||||
hasChildren,
|
|
||||||
mousePosition,
|
mousePosition,
|
||||||
})
|
})
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +69,9 @@
|
||||||
|
|
||||||
const onDrop = async () => {
|
const onDrop = async () => {
|
||||||
try {
|
try {
|
||||||
await dragDropStore.actions.drop()
|
await dndStore.actions.drop()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
notifications.error("Error saving component")
|
notifications.error("Error saving component")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,50 +98,38 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{#each components || [] as component, index (component._id)}
|
{#each components || [] as component, index (component._id)}
|
||||||
|
{@const hasChildren = componentHasChildren(component)}
|
||||||
|
{@const opened = isOpen(component, $selectedComponentPath, closedNodes)}
|
||||||
<li
|
<li
|
||||||
|
bind:this={ref}
|
||||||
on:click|stopPropagation={() => {
|
on:click|stopPropagation={() => {
|
||||||
$store.selectedComponentId = component._id
|
$store.selectedComponentId = component._id
|
||||||
}}
|
}}
|
||||||
|
id={`nav-${component._id}`}
|
||||||
>
|
>
|
||||||
{#if dragDropStore && $dragDropStore?.targetComponent === component}
|
|
||||||
<div
|
|
||||||
class:above={$dragDropStore.dropPosition === DropPosition.ABOVE}
|
|
||||||
class:below={$dragDropStore.dropPosition === DropPosition.BELOW}
|
|
||||||
class:inside={$dragDropStore.dropPosition === DropPosition.INSIDE}
|
|
||||||
class:hasChildren={componentHasChildren(component)}
|
|
||||||
on:drop={onDrop}
|
|
||||||
ondragover="return false"
|
|
||||||
ondragenter="return false"
|
|
||||||
class="drop-item"
|
|
||||||
style="--indicatorX: {(level + 2) *
|
|
||||||
14}px; --indicatorY:{indicatorY}px;"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<NavItem
|
<NavItem
|
||||||
scrollable
|
scrollable
|
||||||
draggable
|
draggable
|
||||||
on:dragend={dragDropStore.actions.reset}
|
on:dragend={dndStore.actions.reset}
|
||||||
on:dragstart={dragstart(component)}
|
on:dragstart={dragstart(component)}
|
||||||
on:dragover={dragover(component, index)}
|
on:dragover={dragover(component, index)}
|
||||||
on:iconClick={() => toggleNodeOpen(component._id)}
|
on:iconClick={() => toggleNodeOpen(component._id)}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}
|
||||||
text={getComponentText(component)}
|
text={getComponentText(component)}
|
||||||
icon={getComponentIcon(component)}
|
icon={getComponentIcon(component)}
|
||||||
withArrow={componentHasChildren(component)}
|
withArrow={hasChildren}
|
||||||
indentLevel={level + 1}
|
indentLevel={level + 1}
|
||||||
selected={$store.selectedComponentId === component._id}
|
selected={$store.selectedComponentId === component._id}
|
||||||
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
{opened}
|
||||||
highlighted={isChildOfSelectedComponent(component)}
|
highlighted={isChildOfSelectedComponent(component)}
|
||||||
>
|
>
|
||||||
<ComponentDropdownMenu {component} />
|
<ComponentDropdownMenu {component} />
|
||||||
</NavItem>
|
</NavItem>
|
||||||
|
|
||||||
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
{#if opened}
|
||||||
<svelte:self
|
<svelte:self
|
||||||
components={component._children}
|
components={component._children}
|
||||||
{currentComponent}
|
{dndStore}
|
||||||
{dragDropStore}
|
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -182,27 +154,7 @@
|
||||||
li {
|
li {
|
||||||
min-width: max-content;
|
min-width: max-content;
|
||||||
}
|
}
|
||||||
|
li {
|
||||||
.drop-item {
|
position: relative;
|
||||||
height: 2px;
|
|
||||||
background: var(--spectrum-global-color-static-green-500);
|
|
||||||
z-index: 999;
|
|
||||||
position: absolute;
|
|
||||||
top: calc(var(--indicatorY) - 1px);
|
|
||||||
left: var(--indicatorX);
|
|
||||||
width: calc(100% - var(--indicatorX));
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.drop-item.above {
|
|
||||||
}
|
|
||||||
.drop-item.below {
|
|
||||||
margin-top: 32px;
|
|
||||||
}
|
|
||||||
.drop-item.inside {
|
|
||||||
background: transparent;
|
|
||||||
border: 2px solid var(--spectrum-global-color-static-green-500);
|
|
||||||
height: 29px;
|
|
||||||
pointer-events: none;
|
|
||||||
width: calc(100% - var(--indicatorX) - 4px);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $dndStore.targetParent?._id === component._id && $dndStore.dropPosition !== DropPosition.INSIDE}
|
||||||
|
<div
|
||||||
|
class="inside drop-item"
|
||||||
|
style="--indicatorX: {indicatorX}px; --indicatorY:{indicatorY}px;"
|
||||||
|
/>
|
||||||
|
{/if}
|
|
@ -0,0 +1,47 @@
|
||||||
|
<script>
|
||||||
|
import { level } from "./ComponentTree.svelte"
|
||||||
|
|
||||||
|
export let componentId
|
||||||
|
export let dndStore
|
||||||
|
|
||||||
|
const indicatorX = (level + 2) * 14
|
||||||
|
let indicatorY = 0
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $dndStore.dragging && $dndStore.valid}
|
||||||
|
{#if $dndStore?.target?._id === componentId}
|
||||||
|
<div
|
||||||
|
class:above={$dndStore.dropPosition === DropPosition.ABOVE}
|
||||||
|
class:below={$dndStore.dropPosition === DropPosition.BELOW}
|
||||||
|
class:inside={$dndStore.dropPosition === DropPosition.INSIDE}
|
||||||
|
class="drop-item"
|
||||||
|
style="--indicatorX: {indicatorX}px; --indicatorY:{indicatorY}px;"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
]
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.drop-item {
|
||||||
|
height: 2px;
|
||||||
|
background: var(--spectrum-global-color-static-green-500);
|
||||||
|
z-index: 999;
|
||||||
|
position: absolute;
|
||||||
|
left: var(--indicatorX);
|
||||||
|
width: calc(100% - var(--indicatorX));
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.drop-item.above {
|
||||||
|
}
|
||||||
|
.drop-item.below {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
.drop-item.inside {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid var(--spectrum-global-color-static-green-500);
|
||||||
|
height: 29px;
|
||||||
|
pointer-events: none;
|
||||||
|
width: calc(100% - var(--indicatorX) - 4px);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { writable, get } from "svelte/store"
|
||||||
|
import { store as frontendStore, selectedScreen } from "builderStore"
|
||||||
|
import {
|
||||||
|
findComponentParent,
|
||||||
|
findComponentPath,
|
||||||
|
} from "builderStore/componentUtils"
|
||||||
|
|
||||||
|
export const DropPosition = {
|
||||||
|
ABOVE: "above",
|
||||||
|
BELOW: "below",
|
||||||
|
INSIDE: "inside",
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
source: null,
|
||||||
|
target: null,
|
||||||
|
targetParent: null,
|
||||||
|
dropPosition: null,
|
||||||
|
dragging: false,
|
||||||
|
valid: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const store = writable(initialState)
|
||||||
|
const actions = {
|
||||||
|
dragstart: component => {
|
||||||
|
if (!component) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
store.set({
|
||||||
|
source: component,
|
||||||
|
target: null,
|
||||||
|
dropPosition: null,
|
||||||
|
dragging: true,
|
||||||
|
valid: false,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
dragover: ({ component, mousePosition }) => {
|
||||||
|
const definition = frontendStore.actions.components.getDefinition(
|
||||||
|
component._component
|
||||||
|
)
|
||||||
|
const canHaveChildren = definition?.hasChildren
|
||||||
|
const hasChildren = component._children?.length > 0
|
||||||
|
|
||||||
|
let dropPosition
|
||||||
|
let target
|
||||||
|
let targetParent
|
||||||
|
|
||||||
|
// If the component has children, it cannot be dropped below
|
||||||
|
if (hasChildren) {
|
||||||
|
if (mousePosition <= 0.33) {
|
||||||
|
dropPosition = DropPosition.ABOVE
|
||||||
|
} else {
|
||||||
|
dropPosition = DropPosition.INSIDE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it can have children then it can be any position
|
||||||
|
else if (canHaveChildren) {
|
||||||
|
if (mousePosition <= 0.33) {
|
||||||
|
dropPosition = DropPosition.ABOVE
|
||||||
|
} else if (mousePosition >= 0.66) {
|
||||||
|
dropPosition = DropPosition.BELOW
|
||||||
|
} else {
|
||||||
|
dropPosition = DropPosition.INSIDE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise drop either above or below
|
||||||
|
else {
|
||||||
|
dropPosition =
|
||||||
|
mousePosition > 0.5 ? DropPosition.BELOW : DropPosition.ABOVE
|
||||||
|
}
|
||||||
|
|
||||||
|
// If hovering over a component with children and attempting to drop
|
||||||
|
// below, we need to change this to be above the first child instead
|
||||||
|
if (dropPosition === DropPosition.BELOW && hasChildren) {
|
||||||
|
target = component._children[0]
|
||||||
|
dropPosition = DropPosition.ABOVE
|
||||||
|
} else {
|
||||||
|
target = component
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the parent of the target component
|
||||||
|
if (target) {
|
||||||
|
targetParent = findComponentParent(
|
||||||
|
get(selectedScreen).props,
|
||||||
|
target._id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
store.update(state => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
dropPosition,
|
||||||
|
target,
|
||||||
|
targetParent,
|
||||||
|
|
||||||
|
/// Mark as invalid if the target is a child of the source
|
||||||
|
valid: findComponentPath(state.source, target._id)?.length === 0,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
reset: () => {
|
||||||
|
store.set(initialState)
|
||||||
|
},
|
||||||
|
drop: async () => {
|
||||||
|
const state = get(store)
|
||||||
|
if (!state.valid || !state.source || !state.target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
actions.reset()
|
||||||
|
|
||||||
|
// Cut and paste the component
|
||||||
|
frontendStore.actions.components.copy(state.source, true, false)
|
||||||
|
await frontendStore.actions.components.paste(
|
||||||
|
state.target,
|
||||||
|
state.dropPosition
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe: store.subscribe,
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,90 +0,0 @@
|
||||||
import { writable, get } from "svelte/store"
|
|
||||||
import { store as frontendStore } from "builderStore"
|
|
||||||
import { findComponentPath } from "builderStore/componentUtils"
|
|
||||||
|
|
||||||
export const DropEffect = {
|
|
||||||
MOVE: "move",
|
|
||||||
COPY: "copy",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DropPosition = {
|
|
||||||
ABOVE: "above",
|
|
||||||
BELOW: "below",
|
|
||||||
INSIDE: "inside",
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const store = writable({})
|
|
||||||
|
|
||||||
store.actions = {
|
|
||||||
dragstart: component => {
|
|
||||||
store.update(state => {
|
|
||||||
state.dragged = component
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
dragover: ({ component, canHaveChildren, hasChildren, mousePosition }) => {
|
|
||||||
store.update(state => {
|
|
||||||
if (canHaveChildren) {
|
|
||||||
if (mousePosition <= 0.33) {
|
|
||||||
state.dropPosition = DropPosition.ABOVE
|
|
||||||
} else if (mousePosition >= 0.66) {
|
|
||||||
state.dropPosition = DropPosition.BELOW
|
|
||||||
} else {
|
|
||||||
state.dropPosition = DropPosition.INSIDE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.dropPosition =
|
|
||||||
mousePosition > 0.5 ? DropPosition.BELOW : DropPosition.ABOVE
|
|
||||||
}
|
|
||||||
|
|
||||||
// If hovering over a component with children and attempting to drop
|
|
||||||
// below, we need to change this to be above the first child instead
|
|
||||||
if (state.dropPosition === DropPosition.BELOW && hasChildren) {
|
|
||||||
state.targetComponent = component._children[0]
|
|
||||||
state.dropPosition = DropPosition.ABOVE
|
|
||||||
} else {
|
|
||||||
state.targetComponent = component
|
|
||||||
}
|
|
||||||
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
reset: () => {
|
|
||||||
store.update(state => {
|
|
||||||
state.dropPosition = ""
|
|
||||||
state.targetComponent = null
|
|
||||||
state.dragged = null
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
},
|
|
||||||
drop: async () => {
|
|
||||||
const state = get(store)
|
|
||||||
|
|
||||||
// Stop if the target and source are the same
|
|
||||||
if (state.targetComponent === state.dragged) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Stop if the target or source are null
|
|
||||||
if (!state.targetComponent || !state.dragged) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Stop if the target is a child of source
|
|
||||||
const path = findComponentPath(state.dragged, state.targetComponent._id)
|
|
||||||
const ids = path.map(component => component._id)
|
|
||||||
if (ids.includes(state.targetComponent._id)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cut and paste the component
|
|
||||||
frontendStore.actions.components.copy(state.dragged, true, false)
|
|
||||||
await frontendStore.actions.components.paste(
|
|
||||||
state.targetComponent,
|
|
||||||
state.dropPosition
|
|
||||||
)
|
|
||||||
store.actions.reset()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return store
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue