Finish new component DND experience

This commit is contained in:
Andrew Kingston 2022-05-23 11:50:26 +01:00
parent b9dd1068f2
commit 05d627b846
6 changed files with 109 additions and 80 deletions

View File

@ -156,6 +156,7 @@
} }
.icon.arrow { .icon.arrow {
flex: 0 0 20px; flex: 0 0 20px;
pointer-events: all;
} }
.icon.arrow.absolute { .icon.arrow.absolute {
position: absolute; position: absolute;

View File

@ -1,14 +1,15 @@
<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 createDNDStore from "./dndStore.js" import { dndStore } from "./dndStore.js"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import { store, selectedScreen } 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"
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
import { DropPosition } from "./dndStore"
const dndStore = createDNDStore()
let scrollRef let scrollRef
const scrollTo = bounds => { const scrollTo = bounds => {
@ -68,6 +69,13 @@
borderRight borderRight
> >
<div class="nav-items-container" bind:this={scrollRef}> <div class="nav-items-container" bind:this={scrollRef}>
<ul>
<li
on:click={() => {
$store.selectedComponentId = $selectedScreen?.props._id
}}
id={`component-${$selectedScreen?.props._id}`}
>
<NavItem <NavItem
text="Screen" text="Screen"
indentLevel={0} indentLevel={0}
@ -75,17 +83,29 @@
opened opened
scrollable scrollable
icon="WebPage" icon="WebPage"
on:click={() => {
$store.selectedComponentId = $selectedScreen?.props._id
}}
> >
<ScreenslotDropdownMenu component={$selectedScreen?.props} /> <ScreenslotDropdownMenu component={$selectedScreen?.props} />
</NavItem> </NavItem>
<ComponentTree <ComponentTree
level={0} level={0}
components={$selectedScreen?.props._children} components={$selectedScreen?.props._children}
{dndStore}
/> />
<!-- Show drop indicators for the target and the parent -->
{#if $dndStore.dragging && $dndStore.valid}
<DNDPositionIndicator
component={$dndStore.target}
position={$dndStore.dropPosition}
/>
{#if $dndStore.dropPosition !== DropPosition.INSIDE}
<DNDPositionIndicator
component={$dndStore.targetParent}
position={DropPosition.INSIDE}
/>
{/if}
{/if}
</li>
</ul>
</div> </div>
</Panel> </Panel>
@ -95,6 +115,15 @@
flex: 1 1 auto; flex: 1 1 auto;
overflow: auto; overflow: auto;
height: 0; height: 0;
}
ul {
list-style: none;
padding-left: 0;
margin: 0;
position: relative; position: relative;
} }
ul,
li {
min-width: max-content;
}
</style> </style>

View File

@ -1,6 +1,5 @@
<script> <script>
import { store } from "builderStore" import { store } from "builderStore"
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"
@ -12,17 +11,12 @@
} from "builderStore" } from "builderStore"
import { findComponentPath } from "builderStore/componentUtils" import { findComponentPath } from "builderStore/componentUtils"
import { get } from "svelte/store" import { get } from "svelte/store"
import { dndStore } from "./dndStore"
export let components = [] export let components = []
export let level = 0 export let level = 0
export let dndStore
let closedNodes = {} let closedNodes = {}
let ref
const dragstart = component => e => {
dndStore.actions.dragstart(component)
}
const dragover = (component, index) => e => { const dragover = (component, index) => e => {
const mousePosition = e.offsetY / e.currentTarget.offsetHeight const mousePosition = e.offsetY / e.currentTarget.offsetHeight
@ -98,26 +92,24 @@
<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)} {@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}`} id={`component-${component._id}`}
> >
<NavItem <NavItem
scrollable scrollable
draggable draggable
on:dragend={dndStore.actions.reset} on:dragend={dndStore.actions.reset}
on:dragstart={dragstart(component)} on:dragstart={() => dndStore.actions.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={hasChildren} withArrow={componentHasChildren(component)}
indentLevel={level + 1} indentLevel={level + 1}
selected={$store.selectedComponentId === component._id} selected={$store.selectedComponentId === component._id}
{opened} {opened}
@ -125,7 +117,6 @@
> >
<ComponentDropdownMenu {component} /> <ComponentDropdownMenu {component} />
</NavItem> </NavItem>
{#if opened} {#if opened}
<svelte:self <svelte:self
components={component._children} components={component._children}
@ -154,7 +145,4 @@
li { li {
min-width: max-content; min-width: max-content;
} }
li {
position: relative;
}
</style> </style>

View File

@ -1,9 +0,0 @@
<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

@ -1,47 +1,65 @@
<script> <script>
import { level } from "./ComponentTree.svelte" import { DropPosition } from "./dndStore"
export let componentId export let component
export let dndStore export let position
const indicatorX = (level + 2) * 14 let x
let indicatorY = 0 let y
let width
let height
$: calculatePosition(component)
const calculatePosition = component => {
// Get root li element
const el = document.getElementById(`component-${component?._id}`)
// Get inner nav item content element
const child = el?.childNodes[0]?.childNodes[0]
if (!el) {
return
}
x = child.offsetLeft
y = child.offsetTop
width = child.clientWidth
height = child.clientHeight
}
</script> </script>
{#if $dndStore.dragging && $dndStore.valid} {#if component && position}
{#if $dndStore?.target?._id === componentId}
<div <div
class:above={$dndStore.dropPosition === DropPosition.ABOVE} class:above={position === DropPosition.ABOVE}
class:below={$dndStore.dropPosition === DropPosition.BELOW} class:below={position === DropPosition.BELOW}
class:inside={$dndStore.dropPosition === DropPosition.INSIDE} class:inside={position === DropPosition.INSIDE}
class="drop-item" class="indicator"
style="--indicatorX: {indicatorX}px; --indicatorY:{indicatorY}px;" style="--x:{x}px; --y:{y}px; --width:{width}px; --height:{height}px"
/> />
{/if}
{/if} {/if}
]
<style> <style>
.drop-item { .indicator {
height: 2px; height: 2px;
background: var(--spectrum-global-color-static-green-500); background: var(--spectrum-global-color-static-green-500);
z-index: 999; z-index: 999;
position: absolute; position: absolute;
left: var(--indicatorX); left: calc(var(--x) + 18px);
width: calc(100% - var(--indicatorX)); top: var(--y);
width: calc(100% - var(--x) - 18px);
border-radius: 4px; border-radius: 4px;
pointer-events: none; pointer-events: none;
} }
.drop-item.above { .indicator.above {
} }
.drop-item.below { .indicator.below {
margin-top: 32px; margin-top: 32px;
} }
.drop-item.inside { .indicator.inside {
background: transparent; background: transparent;
border: 2px solid var(--spectrum-global-color-static-green-500); border: 2px solid var(--spectrum-global-color-static-green-500);
height: 29px;
pointer-events: none; pointer-events: none;
width: calc(100% - var(--indicatorX) - 4px); width: calc(var(--width) - 34px);
height: calc(var(--height) - 9px);
left: calc(var(--x) + 13px);
top: calc(var(--y) + 2px);
} }
</style> </style>

View File

@ -20,7 +20,7 @@ const initialState = {
valid: false, valid: false,
} }
export default function () { const createDNDStore = () => {
const store = writable(initialState) const store = writable(initialState)
const actions = { const actions = {
dragstart: component => { dragstart: component => {
@ -46,17 +46,8 @@ export default function () {
let target let target
let targetParent 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 // If it can have children then it can be any position
else if (canHaveChildren) { if (canHaveChildren) {
if (mousePosition <= 0.33) { if (mousePosition <= 0.33) {
dropPosition = DropPosition.ABOVE dropPosition = DropPosition.ABOVE
} else if (mousePosition >= 0.66) { } else if (mousePosition >= 0.66) {
@ -81,6 +72,15 @@ export default function () {
target = component target = component
} }
// If drop position and target are the same then we can skip this update
const state = get(store)
if (
dropPosition === state.dropPosition &&
target?._id === state.target?._id
) {
return
}
// Find the parent of the target component // Find the parent of the target component
if (target) { if (target) {
targetParent = findComponentParent( targetParent = findComponentParent(
@ -125,3 +125,5 @@ export default function () {
actions, actions,
} }
} }
export const dndStore = createDNDStore()