Add WIP component DND indicator refactor

This commit is contained in:
Andrew Kingston 2022-05-23 09:42:55 +01:00
parent e11c210bf5
commit 692ecb4139
8 changed files with 217 additions and 1105 deletions

View File

@ -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",

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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,
}
}

View File

@ -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