mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
14 changed files with 336 additions and 296 deletions
@ -0,0 +1,113 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@sveltech/routify" |
||||
|
import { store } from "builderStore" |
||||
|
import { getComponentDefinition } from "builderStore/storeUtils" |
||||
|
import { DropEffect, DropPosition } from "./dragDropStore" |
||||
|
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte" |
||||
|
import NavItem from "components/common/NavItem.svelte" |
||||
|
|
||||
|
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?.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?.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,51 @@ |
|||||
|
<script> |
||||
|
import { writable } from "svelte/store" |
||||
|
import { goto } from "@sveltech/routify" |
||||
|
import { store } from "builderStore" |
||||
|
import instantiateStore from "./dragDropStore" |
||||
|
|
||||
|
import ComponentsTree from "./ComponentTree.svelte" |
||||
|
import NavItem from "components/common/NavItem.svelte" |
||||
|
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" |
||||
|
|
||||
|
const dragDropStore = instantiateStore() |
||||
|
|
||||
|
export let route |
||||
|
export let path |
||||
|
export let indent |
||||
|
|
||||
|
$: selectedScreen = $store.currentPreviewItem |
||||
|
|
||||
|
const changeScreen = screenId => { |
||||
|
// select the route |
||||
|
store.actions.screens.select(screenId) |
||||
|
$goto(`./:page/${screenId}`) |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<NavItem |
||||
|
icon="ri-folder-line" |
||||
|
text={path} |
||||
|
opened={true} |
||||
|
withArrow={route.subpaths} /> |
||||
|
|
||||
|
{#each Object.entries(route.subpaths) as [url, subpath]} |
||||
|
{#each Object.values(subpath.screens) as screenId} |
||||
|
<NavItem |
||||
|
icon="ri-artboard-2-line" |
||||
|
indentLevel={indent || 1} |
||||
|
selected={$store.currentPreviewItem._id === screenId} |
||||
|
opened={$store.currentPreviewItem._id === screenId} |
||||
|
text={url === "/" ? "Home" : url} |
||||
|
withArrow={route.subpaths} |
||||
|
on:click={() => changeScreen(screenId)}> |
||||
|
<ScreenDropdownMenu screen={screenId} /> |
||||
|
</NavItem> |
||||
|
{#if selectedScreen?._id === screenId} |
||||
|
<ComponentsTree |
||||
|
components={selectedScreen.props._children} |
||||
|
currentComponent={$store.currentComponentInfo} |
||||
|
{dragDropStore} /> |
||||
|
{/if} |
||||
|
{/each} |
||||
|
{/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.INSIDE |
||||
|
} |
||||
|
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,11 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@sveltech/routify" |
||||
|
import { store } from "builderStore" |
||||
|
import PathTree from "./PathTree.svelte" |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
{#each Object.keys($store.routes) as path} |
||||
|
<PathTree {path} route={$store.routes[path]} /> |
||||
|
{/each} |
||||
|
</div> |
||||
@ -1,61 +0,0 @@ |
|||||
<script> |
|
||||
import { goto } from "@sveltech/routify" |
|
||||
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte" |
|
||||
import { trimCharsStart, trimChars } from "lodash/fp" |
|
||||
import { pipe } from "../../helpers" |
|
||||
import { store } from "builderStore" |
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte" |
|
||||
import { writable } from "svelte/store" |
|
||||
import NavItem from "components/common/NavItem.svelte" |
|
||||
|
|
||||
export let screens = [] |
|
||||
|
|
||||
$: sortedScreens = screens.sort((s1, s2) => { |
|
||||
const name1 = s1.props._instanceName?.toLowerCase() ?? "" |
|
||||
const name2 = s2.props._instanceName?.toLowerCase() ?? "" |
|
||||
return name1 > name2 ? 1 : -1 |
|
||||
}) |
|
||||
/* |
|
||||
Using a store here seems odd.... |
|
||||
have a look in the <ComponentsHierarchyChildren /> code file to find out why. |
|
||||
I have commented the dragDropStore parameter |
|
||||
*/ |
|
||||
const dragDropStore = writable({}) |
|
||||
|
|
||||
let confirmDeleteDialog |
|
||||
let componentToDelete = "" |
|
||||
|
|
||||
const normalizedName = name => |
|
||||
pipe(name, [ |
|
||||
trimCharsStart("./"), |
|
||||
trimCharsStart("~/"), |
|
||||
trimCharsStart("../"), |
|
||||
trimChars(" "), |
|
||||
]) |
|
||||
|
|
||||
const changeScreen = screen => { |
|
||||
store.actions.screens.select(screen.props._instanceName) |
|
||||
$goto(`./:page/${screen.props._instanceName}`) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="root"> |
|
||||
{#each sortedScreens as screen} |
|
||||
<NavItem |
|
||||
icon="ri-artboard-2-line" |
|
||||
text={screen.props._instanceName} |
|
||||
withArrow={screen.props._children.length} |
|
||||
selected={$store.currentComponentInfo._id === screen.props._id} |
|
||||
opened={$store.currentPreviewItem.name === screen.props._id} |
|
||||
on:click={() => changeScreen(screen)}> |
|
||||
<ScreenDropdownMenu {screen} /> |
|
||||
</NavItem> |
|
||||
|
|
||||
{#if $store.currentPreviewItem.props._instanceName && $store.currentPreviewItem.props._instanceName === screen.props._instanceName && screen.props._children} |
|
||||
<ComponentsHierarchyChildren |
|
||||
components={screen.props._children} |
|
||||
currentComponent={$store.currentComponentInfo} |
|
||||
{dragDropStore} /> |
|
||||
{/if} |
|
||||
{/each} |
|
||||
</div> |
|
||||
@ -1,181 +0,0 @@ |
|||||
<script> |
|
||||
import { goto } from "@sveltech/routify" |
|
||||
import { store } from "builderStore" |
|
||||
import { last } from "lodash/fp" |
|
||||
import { pipe } from "../../helpers" |
|
||||
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 |
|
||||
|
|
||||
/* |
|
||||
"dragDropStore" is a svelte store. |
|
||||
This component is recursive... a tree. |
|
||||
Using a single, shared store, all the nodes in the tree can subscribe to state that is changed by other nodes, in the same tree. |
|
||||
|
|
||||
e.g. Say i have the structure |
|
||||
- Heading 1 |
|
||||
- Container |
|
||||
- Heading 2 |
|
||||
- Heading 3 |
|
||||
- Heading 4 |
|
||||
|
|
||||
1. When I dragover "Heading 1", a placeholder drop-slot appears below it |
|
||||
2. I drag down a bit so the cursor is INSIDE the container (i.e. now in a child <ComponentsHierarchyChildren />) |
|
||||
3. Using store subscribes... the original drop-slot now knows that it should disappear, and a new one is created inside the container. |
|
||||
*/ |
|
||||
export let dragDropStore |
|
||||
|
|
||||
let dropUnderComponent |
|
||||
let componentToDrop |
|
||||
|
|
||||
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1) |
|
||||
const get_name = s => (!s ? "" : last(s.split("/"))) |
|
||||
const get_capitalised_name = name => pipe(name, [get_name, capitalise]) |
|
||||
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 = "move" |
|
||||
dragDropStore.update(s => { |
|
||||
s.componentToDrop = component |
|
||||
return s |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
const dragover = (component, index) => e => { |
|
||||
const canHaveChildrenButIsEmpty = |
|
||||
getComponentDefinition($store, component._component).children && |
|
||||
component._children.length === 0 |
|
||||
|
|
||||
e.dataTransfer.dropEffect = "copy" |
|
||||
dragDropStore.update(s => { |
|
||||
const isBottomHalf = e.offsetY > e.currentTarget.offsetHeight / 2 |
|
||||
s.targetComponent = component |
|
||||
// only allow dropping inside when container type |
|
||||
// is empty. If it has children, the user can drag over |
|
||||
// it's existing children |
|
||||
if (canHaveChildrenButIsEmpty) { |
|
||||
if (index === 0) { |
|
||||
// when its the first component in the screen, |
|
||||
// we divide into 3, so we can paste above, inside or below |
|
||||
const pos = e.offsetY / e.currentTarget.offsetHeight |
|
||||
if (pos < 0.4) { |
|
||||
s.dropPosition = "above" |
|
||||
} else if (pos > 0.8) { |
|
||||
// purposely giving this the least space as it is often covered |
|
||||
// by the component below's "above" space |
|
||||
s.dropPosition = "below" |
|
||||
} else { |
|
||||
s.dropPosition = "inside" |
|
||||
} |
|
||||
} else { |
|
||||
s.dropPosition = isBottomHalf ? "below" : "inside" |
|
||||
} |
|
||||
} else { |
|
||||
s.dropPosition = isBottomHalf ? "below" : "above" |
|
||||
} |
|
||||
return s |
|
||||
}) |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
const drop = () => { |
|
||||
if ($dragDropStore.targetComponent !== $dragDropStore.componentToDrop) { |
|
||||
store.actions.components.copy($dragDropStore.componentToDrop, true) |
|
||||
store.actions.components.paste( |
|
||||
$dragDropStore.targetComponent, |
|
||||
$dragDropStore.dropPosition |
|
||||
) |
|
||||
} |
|
||||
dragDropStore.update(s => { |
|
||||
s.dropPosition = "" |
|
||||
s.targetComponent = null |
|
||||
s.componentToDrop = null |
|
||||
return s |
|
||||
}) |
|
||||
} |
|
||||
|
|
||||
const dragend = () => { |
|
||||
dragDropStore.update(s => { |
|
||||
s.dropPosition = "" |
|
||||
s.targetComponent = null |
|
||||
s.componentToDrop = null |
|
||||
return s |
|
||||
}) |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<ul> |
|
||||
{#each components as component, index (component._id)} |
|
||||
<li on:click|stopPropagation={() => selectComponent(component)}> |
|
||||
{#if $dragDropStore && $dragDropStore.targetComponent === component && $dragDropStore.dropPosition === 'above'} |
|
||||
<div |
|
||||
on:drop={drop} |
|
||||
ondragover="return false" |
|
||||
ondragenter="return false" |
|
||||
class="drop-item" |
|
||||
style="margin-left: {(level + 1) * 16}px" /> |
|
||||
{/if} |
|
||||
|
|
||||
<NavItem |
|
||||
draggable |
|
||||
on:dragend={dragend} |
|
||||
on:dragstart={dragstart(component)} |
|
||||
on:dragover={dragover(component, index)} |
|
||||
on:drop={drop} |
|
||||
text={isScreenslot(component._component) ? 'Screenslot' : component._instanceName} |
|
||||
withArrow |
|
||||
indentLevel={level + 1} |
|
||||
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 === 'inside' || $dragDropStore.dropPosition === 'below')} |
|
||||
<div |
|
||||
on:drop={drop} |
|
||||
ondragover="return false" |
|
||||
ondragenter="return false" |
|
||||
class="drop-item" |
|
||||
style="margin-left: {(level + ($dragDropStore.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> |
|
||||
Loading…
Reference in new issue