Finish new component DND experience
This commit is contained in:
parent
b9dd1068f2
commit
05d627b846
|
@ -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;
|
||||||
|
|
|
@ -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,24 +69,43 @@
|
||||||
borderRight
|
borderRight
|
||||||
>
|
>
|
||||||
<div class="nav-items-container" bind:this={scrollRef}>
|
<div class="nav-items-container" bind:this={scrollRef}>
|
||||||
<NavItem
|
<ul>
|
||||||
text="Screen"
|
<li
|
||||||
indentLevel={0}
|
on:click={() => {
|
||||||
selected={$store.selectedComponentId === $selectedScreen?.props._id}
|
$store.selectedComponentId = $selectedScreen?.props._id
|
||||||
opened
|
}}
|
||||||
scrollable
|
id={`component-${$selectedScreen?.props._id}`}
|
||||||
icon="WebPage"
|
>
|
||||||
on:click={() => {
|
<NavItem
|
||||||
$store.selectedComponentId = $selectedScreen?.props._id
|
text="Screen"
|
||||||
}}
|
indentLevel={0}
|
||||||
>
|
selected={$store.selectedComponentId === $selectedScreen?.props._id}
|
||||||
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
opened
|
||||||
</NavItem>
|
scrollable
|
||||||
<ComponentTree
|
icon="WebPage"
|
||||||
level={0}
|
>
|
||||||
components={$selectedScreen?.props._children}
|
<ScreenslotDropdownMenu component={$selectedScreen?.props} />
|
||||||
{dndStore}
|
</NavItem>
|
||||||
/>
|
<ComponentTree
|
||||||
|
level={0}
|
||||||
|
components={$selectedScreen?.props._children}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
|
|
@ -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={position === DropPosition.ABOVE}
|
||||||
class:above={$dndStore.dropPosition === DropPosition.ABOVE}
|
class:below={position === DropPosition.BELOW}
|
||||||
class:below={$dndStore.dropPosition === DropPosition.BELOW}
|
class:inside={position === DropPosition.INSIDE}
|
||||||
class:inside={$dndStore.dropPosition === DropPosition.INSIDE}
|
class="indicator"
|
||||||
class="drop-item"
|
style="--x:{x}px; --y:{y}px; --width:{width}px; --height:{height}px"
|
||||||
style="--indicatorX: {indicatorX}px; --indicatorY:{indicatorY}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>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue