forked from tsai/budibase
14 changed files with 313 additions and 404 deletions
@ -0,0 +1,114 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import { store } from "builderStore" |
|||
import { last } from "lodash/fp" |
|||
import { DropEffect, DropPosition } from "./dragDropStore" |
|||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte" |
|||
import NavItem from "components/common/NavItem.svelte" |
|||
import { getComponentDefinition } from "builderStore/storeUtils" |
|||
|
|||
export let components = [] |
|||
export let currentComponent |
|||
export let onSelect = () => {} |
|||
export let level = 0 |
|||
|
|||
export let dragDropStore |
|||
|
|||
const isScreenslot = name => name === "##builtin/screenslot" |
|||
|
|||
const selectComponent = component => { |
|||
// Set current component |
|||
store.actions.components.select(component) |
|||
|
|||
// Get ID path |
|||
const path = store.actions.components.findRoute(component) |
|||
|
|||
// Go to correct URL |
|||
$goto(`./:page/:screen/${path}`) |
|||
} |
|||
|
|||
const dragstart = component => e => { |
|||
e.dataTransfer.dropEffect = DropEffect.MOVE |
|||
dragDropStore.actions.dragstart(component) |
|||
} |
|||
|
|||
const dragover = (component, index) => e => { |
|||
const canHaveChildrenButIsEmpty = |
|||
getComponentDefinition($store, component._component).children && |
|||
component._children.length === 0 |
|||
|
|||
e.dataTransfer.dropEffect = DropEffect.COPY |
|||
|
|||
// how far down the mouse pointer is on the drop target |
|||
const mousePosition = e.offsetY / e.currentTarget.offsetHeight |
|||
|
|||
dragDropStore.actions.dragover({ |
|||
component, |
|||
index, |
|||
canHaveChildrenButIsEmpty, |
|||
mousePosition, |
|||
}) |
|||
|
|||
return false |
|||
} |
|||
</script> |
|||
|
|||
<ul> |
|||
{#each components as component, index (component._id)} |
|||
<li on:click|stopPropagation={() => selectComponent(component)}> |
|||
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE} |
|||
<div |
|||
on:drop={dragDropStore.actions.drop} |
|||
ondragover="return false" |
|||
ondragenter="return false" |
|||
class="drop-item" |
|||
style="margin-left: {(level + 1) * 16}px" /> |
|||
{/if} |
|||
|
|||
<NavItem |
|||
draggable |
|||
on:dragend={dragDropStore.actions.reset} |
|||
on:dragstart={dragstart(component)} |
|||
on:dragover={dragover(component, index)} |
|||
on:drop={dragDropStore.actions.drop} |
|||
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName} |
|||
withArrow |
|||
indentLevel={level + 3} |
|||
selected={currentComponent === component}> |
|||
<ComponentDropdownMenu {component} /> |
|||
</NavItem> |
|||
|
|||
{#if component._children} |
|||
<svelte:self |
|||
components={component._children} |
|||
{currentComponent} |
|||
{onSelect} |
|||
{dragDropStore} |
|||
level={level + 1} /> |
|||
{/if} |
|||
|
|||
{#if $dragDropStore && $dragDropStore.targetComponent === component && ($dragDropStore.dropPosition === DropPosition.INSIDE || $dragDropStore.dropPosition === DropPosition.BELOW)} |
|||
<div |
|||
on:drop={dragDropStore.actions.drop} |
|||
ondragover="return false" |
|||
ondragenter="return false" |
|||
class="drop-item" |
|||
style="margin-left: {(level + ($dragDropStore.dropPosition === DropPosition.INSIDE ? 3 : 1)) * 16}px" /> |
|||
{/if} |
|||
</li> |
|||
{/each} |
|||
</ul> |
|||
|
|||
<style> |
|||
ul { |
|||
list-style: none; |
|||
padding-left: 0; |
|||
margin: 0; |
|||
} |
|||
|
|||
.drop-item { |
|||
border-radius: var(--border-radius-m); |
|||
height: 32px; |
|||
background: var(--grey-3); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,50 @@ |
|||
<script> |
|||
import { writable } from "svelte/store" |
|||
import { goto } from "@sveltech/routify" |
|||
import instantiateStore from "./dragDropStore" |
|||
import ComponentsTree from "./ComponentTree.svelte" |
|||
import NavItem from "components/common/NavItem.svelte" |
|||
import { store } from "builderStore" |
|||
|
|||
const dragDropStore = instantiateStore() |
|||
|
|||
export let route |
|||
export let indent |
|||
|
|||
$: console.log(route) |
|||
$: console.log("preview", $store.currentPreviewItem) |
|||
$: selectedScreen = $store.currentPreviewItem |
|||
|
|||
const changeScreen = screenId => { |
|||
// select the route |
|||
store.actions.screens.select(screenId) |
|||
$goto(`./:page/${screenId}`) |
|||
} |
|||
</script> |
|||
|
|||
<NavItem |
|||
icon="ri-route-line" |
|||
text={route.fullpath} |
|||
withArrow={route.subpaths} |
|||
on:click={() => console.log(route)} /> |
|||
{#each Object.keys(route.screens) as screen} |
|||
<NavItem |
|||
icon="ri-artboard-2-line" |
|||
indentLevel={indent || 1} |
|||
selected={$store.currentComponentInfo._id === route.screens[screen]} |
|||
opened={$store.currentPreviewItem._id === route.screens[screen]} |
|||
text={screen || 'DEFAULT'} |
|||
withArrow={route.subpaths} |
|||
on:click={() => changeScreen(route.screens[screen])}> |
|||
<!-- <ScreenDropdownMenu screen={route.screens[screen]} /> --> |
|||
</NavItem> |
|||
{#if selectedScreen?._id === route.screens[screen]} |
|||
<ComponentsTree |
|||
components={selectedScreen.props._children} |
|||
currentComponent={$store.currentComponentInfo} |
|||
{dragDropStore} /> |
|||
{/if} |
|||
{/each} |
|||
{#each Object.keys(route.subpaths) as subpath} |
|||
<svelte:self route={route.subpaths[subpath]} indent={2} /> |
|||
{/each} |
|||
@ -0,0 +1,92 @@ |
|||
import { writable } from "svelte/store" |
|||
import { store as frontendStore } from "builderStore" |
|||
|
|||
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, |
|||
index, |
|||
canHaveChildrenButIsEmpty, |
|||
mousePosition, |
|||
}) => { |
|||
store.update(state => { |
|||
state.targetComponent = component |
|||
// only allow dropping inside when container is empty
|
|||
// if container has children, drag over them
|
|||
|
|||
if (canHaveChildrenButIsEmpty && index === 0) { |
|||
// hovered above center of target
|
|||
if (mousePosition < 0.4) { |
|||
state.dropPosition = DropPosition.ABOVE |
|||
} |
|||
|
|||
// hovered around bottom of target
|
|||
if (mousePosition > 0.8) { |
|||
state.dropPosition = DropPosition.BELOW |
|||
} |
|||
|
|||
// hovered in center of target
|
|||
if (mousePosition > 0.4 && mousePosition < 0.8) { |
|||
state.dropPosition = DropPosition.BELOW |
|||
} |
|||
return |
|||
} |
|||
|
|||
// bottom half
|
|||
if (mousePosition > 0.5) { |
|||
state.dropPosition = DropPosition.BELOW |
|||
} else { |
|||
state.dropPosition = canHaveChildrenButIsEmpty |
|||
? DropPosition.INSIDE |
|||
: DropPosition.ABOVE |
|||
} |
|||
|
|||
return state |
|||
}) |
|||
}, |
|||
reset: () => { |
|||
store.update(state => { |
|||
state.dropPosition = "" |
|||
state.targetComponent = null |
|||
state.dragged = null |
|||
return state |
|||
}) |
|||
}, |
|||
drop: () => { |
|||
store.update(state => { |
|||
if (state.targetComponent !== state.dragged) { |
|||
frontendStore.actions.components.copy(state.dragged, true) |
|||
frontendStore.actions.components.paste( |
|||
state.targetComponent, |
|||
state.dropPosition |
|||
) |
|||
} |
|||
|
|||
store.actions.reset() |
|||
|
|||
return state |
|||
}) |
|||
}, |
|||
} |
|||
|
|||
return store |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<script> |
|||
import { goto } from "@sveltech/routify" |
|||
import ComponentTree from "./ComponentTree.svelte" |
|||
import { store } from "builderStore" |
|||
import ScreenDropdownMenu from "../ScreenDropdownMenu.svelte" |
|||
import { writable } from "svelte/store" |
|||
import NavItem from "components/common/NavItem.svelte" |
|||
import PathTree from "./PathTree.svelte" |
|||
|
|||
export let routes = {} |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
{#each Object.keys(routes) as path} |
|||
<PathTree route={routes[path]} /> |
|||
{/each} |
|||
</div> |
|||
Loading…
Reference in new issue