commit
17acae87c0
|
@ -165,7 +165,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
||||||
.its("body")
|
.its("body")
|
||||||
.should("not.be.null")
|
.should("not.be.null")
|
||||||
.then(cy.wrap)
|
.then(cy.wrap)
|
||||||
.find(`[data-component-id=${componentId}]`)
|
.find(`[data-id=${componentId}]`)
|
||||||
})
|
})
|
||||||
|
|
||||||
Cypress.Commands.add("navigateToFrontend", () => {
|
Cypress.Commands.add("navigateToFrontend", () => {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { get } from "svelte/store"
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
import { store, currentAsset } from "builderStore"
|
import { store, currentAsset } from "builderStore"
|
||||||
import iframeTemplate from "./iframeTemplate"
|
import iframeTemplate from "./iframeTemplate"
|
||||||
|
@ -7,6 +8,7 @@
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
|
import { ProgressCircle, Layout, Heading, Body } from "@budibase/bbui"
|
||||||
import ErrorSVG from "assets/error.svg?raw"
|
import ErrorSVG from "assets/error.svg?raw"
|
||||||
|
import { findComponent, findComponentPath } from "builderStore/storeUtils"
|
||||||
|
|
||||||
let iframe
|
let iframe
|
||||||
let layout
|
let layout
|
||||||
|
@ -102,7 +104,7 @@
|
||||||
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
iframe.contentWindow.addEventListener("keydown", handleKeydownEvent)
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove all iframe event listeners on component destroy
|
// Remove all iframe event listeners on component destroy
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (iframe.contentWindow) {
|
if (iframe.contentWindow) {
|
||||||
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
iframe.contentWindow.removeEventListener("bb-event", handleBudibaseEvent)
|
||||||
|
@ -122,6 +124,26 @@
|
||||||
// Wait for this event to show the client library if intelligent
|
// Wait for this event to show the client library if intelligent
|
||||||
// loading is supported
|
// loading is supported
|
||||||
loading = false
|
loading = false
|
||||||
|
} else if (type === "move-component") {
|
||||||
|
const { componentId, destinationComponentId } = data
|
||||||
|
const rootComponent = get(currentAsset).props
|
||||||
|
|
||||||
|
// Get source and destination components
|
||||||
|
const source = findComponent(rootComponent, componentId)
|
||||||
|
const destination = findComponent(rootComponent, destinationComponentId)
|
||||||
|
|
||||||
|
// Stop if the target is a child of source
|
||||||
|
const path = findComponentPath(source, destinationComponentId)
|
||||||
|
const ids = path.map(component => component._id)
|
||||||
|
if (ids.includes(data.destinationComponentId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cut and paste the component to the new destination
|
||||||
|
if (source && destination) {
|
||||||
|
store.actions.components.copy(source, true)
|
||||||
|
store.actions.components.paste(destination, data.mode)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warning(`Client sent unknown event type: ${type}`)
|
console.warning(`Client sent unknown event type: ${type}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
import SelectionIndicator from "components/preview/SelectionIndicator.svelte"
|
||||||
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
import HoverIndicator from "components/preview/HoverIndicator.svelte"
|
||||||
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
import CustomThemeWrapper from "./CustomThemeWrapper.svelte"
|
||||||
|
import DNDHandler from "components/preview/DNDHandler.svelte"
|
||||||
import ErrorSVG from "builder/assets/error.svg"
|
import ErrorSVG from "builder/assets/error.svg"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
|
@ -104,7 +105,10 @@
|
||||||
<div id="app-root">
|
<div id="app-root">
|
||||||
<CustomThemeWrapper>
|
<CustomThemeWrapper>
|
||||||
{#key $screenStore.activeLayout._id}
|
{#key $screenStore.activeLayout._id}
|
||||||
<Component instance={$screenStore.activeLayout.props} />
|
<Component
|
||||||
|
isLayout
|
||||||
|
instance={$screenStore.activeLayout.props}
|
||||||
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
||||||
<!-- Layers on top of app -->
|
<!-- Layers on top of app -->
|
||||||
|
@ -122,6 +126,7 @@
|
||||||
{#if $builderStore.inBuilder}
|
{#if $builderStore.inBuilder}
|
||||||
<SelectionIndicator />
|
<SelectionIndicator />
|
||||||
<HoverIndicator />
|
<HoverIndicator />
|
||||||
|
<DNDHandler />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</StateBindingsProvider>
|
</StateBindingsProvider>
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
import Placeholder from "components/app/Placeholder.svelte"
|
import Placeholder from "components/app/Placeholder.svelte"
|
||||||
|
|
||||||
export let instance = {}
|
export let instance = {}
|
||||||
|
export let isLayout = false
|
||||||
|
export let isScreen = false
|
||||||
|
|
||||||
// The enriched component settings
|
// The enriched component settings
|
||||||
let enrichedSettings
|
let enrichedSettings
|
||||||
|
@ -49,11 +51,11 @@
|
||||||
$: children = instance._children || []
|
$: children = instance._children || []
|
||||||
$: id = instance._id
|
$: id = instance._id
|
||||||
$: name = instance._instanceName
|
$: name = instance._instanceName
|
||||||
$: empty =
|
$: interactive =
|
||||||
!children.length &&
|
$builderStore.inBuilder &&
|
||||||
definition?.hasChildren &&
|
($builderStore.previewType === "layout" || insideScreenslot)
|
||||||
definition?.showEmptyState !== false &&
|
$: empty = interactive && !children.length && definition?.hasChildren
|
||||||
$builderStore.inBuilder
|
$: emptyState = empty && definition?.showEmptyState !== false
|
||||||
$: rawProps = getRawProps(instance)
|
$: rawProps = getRawProps(instance)
|
||||||
$: instanceKey = JSON.stringify(rawProps)
|
$: instanceKey = JSON.stringify(rawProps)
|
||||||
$: updateComponentProps(rawProps, instanceKey, $context)
|
$: updateComponentProps(rawProps, instanceKey, $context)
|
||||||
|
@ -61,16 +63,16 @@
|
||||||
$builderStore.inBuilder &&
|
$builderStore.inBuilder &&
|
||||||
$builderStore.selectedComponentId === instance._id
|
$builderStore.selectedComponentId === instance._id
|
||||||
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
$: inSelectedPath = $builderStore.selectedComponentPath?.includes(id)
|
||||||
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
|
|
||||||
$: evaluateConditions(enrichedSettings?._conditions)
|
$: evaluateConditions(enrichedSettings?._conditions)
|
||||||
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
$: componentSettings = { ...enrichedSettings, ...conditionalSettings }
|
||||||
|
$: renderKey = `${propsHash}-${emptyState}`
|
||||||
|
|
||||||
// Update component context
|
// Update component context
|
||||||
$: componentStore.set({
|
$: componentStore.set({
|
||||||
id,
|
id,
|
||||||
children: children.length,
|
children: children.length,
|
||||||
styles: { ...instance._styles, id, empty, interactive },
|
styles: { ...instance._styles, id, empty: emptyState, interactive },
|
||||||
empty,
|
empty: emptyState,
|
||||||
selected,
|
selected,
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
|
@ -169,13 +171,22 @@
|
||||||
conditionalSettings = result.settingUpdates
|
conditionalSettings = result.settingUpdates
|
||||||
visible = nextVisible
|
visible = nextVisible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drag and drop helper tags
|
||||||
|
$: draggable = interactive && !isLayout && !isScreen
|
||||||
|
$: droppable = interactive && !isLayout && !isScreen
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key propsHash}
|
{#key renderKey}
|
||||||
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
{#if constructor && componentSettings && (visible || inSelectedPath)}
|
||||||
|
<!-- The ID is used as a class because getElementsByClassName is O(1) -->
|
||||||
|
<!-- and the performance matters for the selection indicators -->
|
||||||
<div
|
<div
|
||||||
class={`component ${id}`}
|
class={`component ${id}`}
|
||||||
data-type={interactive ? "component" : ""}
|
class:draggable
|
||||||
|
class:droppable
|
||||||
|
class:empty
|
||||||
|
class:interactive
|
||||||
data-id={id}
|
data-id={id}
|
||||||
data-name={name}
|
data-name={name}
|
||||||
>
|
>
|
||||||
|
@ -184,7 +195,7 @@
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
<svelte:self instance={child} />
|
<svelte:self instance={child} />
|
||||||
{/each}
|
{/each}
|
||||||
{:else if empty}
|
{:else if emptyState}
|
||||||
<Placeholder />
|
<Placeholder />
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
|
@ -196,4 +207,10 @@
|
||||||
.component {
|
.component {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
.interactive :global(*:hover) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.draggable :global(*:hover) {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -22,6 +22,6 @@
|
||||||
<!-- Ensure to fully remount when screen changes -->
|
<!-- Ensure to fully remount when screen changes -->
|
||||||
{#key screenDefinition?._id}
|
{#key screenDefinition?._id}
|
||||||
<Provider key="url" data={params}>
|
<Provider key="url" data={params}>
|
||||||
<Component instance={screenDefinition} />
|
<Component isScreen instance={screenDefinition} />
|
||||||
</Provider>
|
</Provider>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
@ -31,4 +31,7 @@
|
||||||
.spectrum-Button--overBackground:hover {
|
.spectrum-Button--overBackground:hover {
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
.spectrum-Button::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.valid-container :global([data-type="component"] > *) {
|
.valid-container :global(.component > *) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.direction-row {
|
.direction-row {
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
/* Grow containers inside a row need 0 width 0 so that they ignore content */
|
||||||
/* The nested selector for data-type is the wrapper around all components */
|
/* The nested selector for data-type is the wrapper around all components */
|
||||||
.direction-row :global(> [data-type="component"] > .size-grow) {
|
.direction-row :global(> .component > .size-grow) {
|
||||||
width: 0;
|
width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,240 @@
|
||||||
|
<script context="module">
|
||||||
|
export const Sides = {
|
||||||
|
Top: "Top",
|
||||||
|
Right: "Right",
|
||||||
|
Bottom: "Bottom",
|
||||||
|
Left: "Left",
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
import { get } from "svelte/store"
|
||||||
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
|
import DNDPositionIndicator from "./DNDPositionIndicator.svelte"
|
||||||
|
import { builderStore } from "stores"
|
||||||
|
|
||||||
|
let dragInfo
|
||||||
|
let dropInfo
|
||||||
|
|
||||||
|
const getEdges = (bounds, mousePoint) => {
|
||||||
|
const { width, height, top, left } = bounds
|
||||||
|
return {
|
||||||
|
[Sides.Top]: [mousePoint[0], top],
|
||||||
|
[Sides.Right]: [left + width, mousePoint[1]],
|
||||||
|
[Sides.Bottom]: [mousePoint[0], top + height],
|
||||||
|
[Sides.Left]: [left, mousePoint[1]],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculatePointDelta = (point1, point2) => {
|
||||||
|
const deltaX = Math.abs(point1[0] - point2[0])
|
||||||
|
const deltaY = Math.abs(point1[1] - point2[1])
|
||||||
|
return Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDOMNodeForComponent = component => {
|
||||||
|
const parent = component.closest(".component")
|
||||||
|
const children = Array.from(parent.childNodes)
|
||||||
|
return children?.find(node => node?.nodeType === 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when initially starting a drag on a draggable component
|
||||||
|
const onDragStart = e => {
|
||||||
|
const parent = e.target.closest(".component")
|
||||||
|
if (!parent?.classList.contains("draggable")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
dragInfo = {
|
||||||
|
target: parent.dataset.id,
|
||||||
|
parent: parent.dataset.parent,
|
||||||
|
}
|
||||||
|
builderStore.actions.selectComponent(dragInfo.target)
|
||||||
|
builderStore.actions.setDragging(true)
|
||||||
|
|
||||||
|
// Highlight being dragged by setting opacity
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
if (child) {
|
||||||
|
child.style.opacity = "0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when drag stops (whether dropped or not)
|
||||||
|
const onDragEnd = e => {
|
||||||
|
// Reset opacity style
|
||||||
|
if (dragInfo) {
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
if (child) {
|
||||||
|
child.style.opacity = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state and styles
|
||||||
|
dragInfo = null
|
||||||
|
dropInfo = null
|
||||||
|
builderStore.actions.setDragging(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when on top of a component
|
||||||
|
const onDragOver = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo || !dropInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault()
|
||||||
|
const { droppableInside, bounds } = dropInfo
|
||||||
|
const { top, left, height, width } = bounds
|
||||||
|
const mouseY = e.clientY
|
||||||
|
const mouseX = e.clientX
|
||||||
|
const snapFactor = droppableInside ? 0.33 : 0.5
|
||||||
|
const snapLimitV = Math.min(40, height * snapFactor)
|
||||||
|
const snapLimitH = Math.min(40, width * snapFactor)
|
||||||
|
|
||||||
|
// Determine all sies we are within snap range of
|
||||||
|
let sides = []
|
||||||
|
if (mouseY <= top + snapLimitV) {
|
||||||
|
sides.push(Sides.Top)
|
||||||
|
} else if (mouseY >= top + height - snapLimitV) {
|
||||||
|
sides.push(Sides.Bottom)
|
||||||
|
}
|
||||||
|
if (mouseX < left + snapLimitH) {
|
||||||
|
sides.push(Sides.Left)
|
||||||
|
} else if (mouseX > left + width - snapLimitH) {
|
||||||
|
sides.push(Sides.Right)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When no edges match, drop inside if possible
|
||||||
|
if (!sides.length) {
|
||||||
|
dropInfo.mode = droppableInside ? "inside" : null
|
||||||
|
dropInfo.side = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When one edge matches, use that edge
|
||||||
|
if (sides.length === 1) {
|
||||||
|
dropInfo.side = sides[0]
|
||||||
|
if ([Sides.Top, Sides.Left].includes(sides[0])) {
|
||||||
|
dropInfo.mode = "above"
|
||||||
|
} else {
|
||||||
|
dropInfo.mode = "below"
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When 2 edges match, work out which is closer
|
||||||
|
const mousePoint = [mouseX, mouseY]
|
||||||
|
const edges = getEdges(bounds, mousePoint)
|
||||||
|
const edge1 = edges[sides[0]]
|
||||||
|
const delta1 = calculatePointDelta(mousePoint, edge1)
|
||||||
|
const edge2 = edges[sides[1]]
|
||||||
|
const delta2 = calculatePointDelta(mousePoint, edge2)
|
||||||
|
const edge = delta1 < delta2 ? sides[0] : sides[1]
|
||||||
|
dropInfo.side = edge
|
||||||
|
if ([Sides.Top, Sides.Left].includes(edge)) {
|
||||||
|
dropInfo.mode = "above"
|
||||||
|
} else {
|
||||||
|
dropInfo.mode = "below"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when entering a potential drop target
|
||||||
|
const onDragEnter = e => {
|
||||||
|
// Skip if we aren't validly dragging currently
|
||||||
|
if (!dragInfo) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = e.target.closest(".component")
|
||||||
|
if (
|
||||||
|
element &&
|
||||||
|
element.classList.contains("droppable") &&
|
||||||
|
element.dataset.id !== dragInfo.target
|
||||||
|
) {
|
||||||
|
// Do nothing if this is the same target
|
||||||
|
if (element.dataset.id === dropInfo?.target) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the dragging flag is always set.
|
||||||
|
// There's a bit of a race condition between the app reinitialisation
|
||||||
|
// after selecting the DND component and setting this the first time
|
||||||
|
if (!get(builderStore).isDragging) {
|
||||||
|
builderStore.actions.setDragging(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store target ID
|
||||||
|
const target = element.dataset.id
|
||||||
|
|
||||||
|
// Precompute and store some info to avoid recalculating everything in
|
||||||
|
// dragOver
|
||||||
|
const child = getDOMNodeForComponent(e.target)
|
||||||
|
const bounds = child.getBoundingClientRect()
|
||||||
|
dropInfo = {
|
||||||
|
target,
|
||||||
|
name: element.dataset.name,
|
||||||
|
droppableInside: element.classList.contains("empty"),
|
||||||
|
bounds,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dropInfo = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback when leaving a potential drop target.
|
||||||
|
// Since we don't style our targets, we don't need to unset anything.
|
||||||
|
const onDragLeave = () => {}
|
||||||
|
|
||||||
|
// Callback when dropping a drag on top of some component
|
||||||
|
const onDrop = e => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (dropInfo?.mode) {
|
||||||
|
builderStore.actions.moveComponent(
|
||||||
|
dragInfo.target,
|
||||||
|
dropInfo.target,
|
||||||
|
dropInfo.mode
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
// Events fired on the draggable target
|
||||||
|
document.addEventListener("dragstart", onDragStart, false)
|
||||||
|
document.addEventListener("dragend", onDragEnd, false)
|
||||||
|
|
||||||
|
// Events fired on the drop targets
|
||||||
|
document.addEventListener("dragover", onDragOver, false)
|
||||||
|
document.addEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.addEventListener("dragleave", onDragLeave, false)
|
||||||
|
document.addEventListener("drop", onDrop, false)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// Events fired on the draggable target
|
||||||
|
document.removeEventListener("dragstart", onDragStart, false)
|
||||||
|
document.removeEventListener("dragend", onDragEnd, false)
|
||||||
|
|
||||||
|
// Events fired on the drop targets
|
||||||
|
document.removeEventListener("dragover", onDragOver, false)
|
||||||
|
document.removeEventListener("dragenter", onDragEnter, false)
|
||||||
|
document.removeEventListener("dragleave", onDragLeave, false)
|
||||||
|
document.removeEventListener("drop", onDrop, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<IndicatorSet
|
||||||
|
componentId={dropInfo?.mode === "inside" ? dropInfo.target : null}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex="930"
|
||||||
|
transition
|
||||||
|
prefix="Inside"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DNDPositionIndicator
|
||||||
|
{dropInfo}
|
||||||
|
color="var(--spectrum-global-color-static-green-500)"
|
||||||
|
zIndex="940"
|
||||||
|
transition
|
||||||
|
/>
|
|
@ -0,0 +1,54 @@
|
||||||
|
<script>
|
||||||
|
import Indicator from "./Indicator.svelte"
|
||||||
|
import { Sides } from "./DNDHandler.svelte"
|
||||||
|
|
||||||
|
export let dropInfo
|
||||||
|
export let zIndex
|
||||||
|
export let color
|
||||||
|
export let transition
|
||||||
|
|
||||||
|
$: dimensions = getDimensions(dropInfo)
|
||||||
|
$: prefix = dropInfo?.mode === "above" ? "Before" : "After"
|
||||||
|
$: text = `${prefix} ${dropInfo?.name}`
|
||||||
|
$: renderKey = `${dropInfo?.target}-${dropInfo?.side}`
|
||||||
|
|
||||||
|
const getDimensions = info => {
|
||||||
|
const { bounds, side } = info ?? {}
|
||||||
|
if (!bounds || !side) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const { left, top, width, height } = bounds
|
||||||
|
if (side === Sides.Top || side === Sides.Bottom) {
|
||||||
|
return {
|
||||||
|
top: side === Sides.Top ? top - 4 : top + height,
|
||||||
|
left: left - 2,
|
||||||
|
width: width + 4,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
top: top - 2,
|
||||||
|
left: side === Sides.Left ? left - 4 : left + width,
|
||||||
|
width: 0,
|
||||||
|
height: height + 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key renderKey}
|
||||||
|
{#if dimensions && dropInfo?.mode !== "inside"}
|
||||||
|
<Indicator
|
||||||
|
left={Math.round(dimensions.left)}
|
||||||
|
top={Math.round(dimensions.top)}
|
||||||
|
width={dimensions.width}
|
||||||
|
height={dimensions.height}
|
||||||
|
{text}
|
||||||
|
{zIndex}
|
||||||
|
{color}
|
||||||
|
{transition}
|
||||||
|
alignRight={dropInfo?.side === Sides.Right}
|
||||||
|
line
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
|
@ -7,7 +7,7 @@
|
||||||
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
const element = e.target.closest("[data-type='component']")
|
const element = e.target.closest(".interactive.component")
|
||||||
const newId = element?.dataset?.id
|
const newId = element?.dataset?.id
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
componentId = newId
|
||||||
|
@ -30,7 +30,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IndicatorSet
|
<IndicatorSet
|
||||||
{componentId}
|
componentId={$builderStore.isDragging ? null : componentId}
|
||||||
color="var(--spectrum-global-color-static-blue-200)"
|
color="var(--spectrum-global-color-static-blue-200)"
|
||||||
transition
|
transition
|
||||||
{zIndex}
|
{zIndex}
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
export let color
|
export let color
|
||||||
export let zIndex
|
export let zIndex
|
||||||
export let transition = false
|
export let transition = false
|
||||||
|
export let line = false
|
||||||
|
export let alignRight = false
|
||||||
|
|
||||||
|
$: flipped = top < 20
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -18,11 +22,12 @@
|
||||||
}}
|
}}
|
||||||
out:fade={{ duration: transition ? 130 : 0 }}
|
out:fade={{ duration: transition ? 130 : 0 }}
|
||||||
class="indicator"
|
class="indicator"
|
||||||
class:flipped={top < 20}
|
class:flipped
|
||||||
|
class:line
|
||||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color}; --zIndex: {zIndex};"
|
||||||
>
|
>
|
||||||
{#if text}
|
{#if text}
|
||||||
<div class="text" class:flipped={top < 20}>
|
<div class="text" class:flipped class:line class:right={alignRight}>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -30,6 +35,7 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.indicator {
|
.indicator {
|
||||||
|
right: 0;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: var(--zIndex);
|
z-index: var(--zIndex);
|
||||||
border: 2px solid var(--color);
|
border: 2px solid var(--color);
|
||||||
|
@ -42,6 +48,9 @@
|
||||||
.indicator.flipped {
|
.indicator.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
}
|
}
|
||||||
|
.indicator.line {
|
||||||
|
border-radius: 4px !important;
|
||||||
|
}
|
||||||
.text {
|
.text {
|
||||||
background-color: var(--color);
|
background-color: var(--color);
|
||||||
color: white;
|
color: white;
|
||||||
|
@ -61,9 +70,18 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
.text.line {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
.text.flipped {
|
.text.flipped {
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
transform: translateY(0%);
|
transform: translateY(0%);
|
||||||
top: -2px;
|
top: -2px;
|
||||||
}
|
}
|
||||||
|
.text.right {
|
||||||
|
right: -2px;
|
||||||
|
left: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
export let color
|
export let color
|
||||||
export let transition
|
export let transition
|
||||||
export let zIndex
|
export let zIndex
|
||||||
|
export let prefix = null
|
||||||
|
|
||||||
let indicators = []
|
let indicators = []
|
||||||
let interval
|
let interval
|
||||||
|
@ -51,6 +52,9 @@
|
||||||
const parents = document.getElementsByClassName(componentId)
|
const parents = document.getElementsByClassName(componentId)
|
||||||
if (parents.length) {
|
if (parents.length) {
|
||||||
text = parents[0].dataset.name
|
text = parents[0].dataset.name
|
||||||
|
if (prefix) {
|
||||||
|
text = `${prefix} ${text}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch reads to minimize reflow
|
// Batch reads to minimize reflow
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
let measured = false
|
let measured = false
|
||||||
|
|
||||||
$: definition = $builderStore.selectedComponentDefinition
|
$: definition = $builderStore.selectedComponentDefinition
|
||||||
$: showBar = definition?.showSettingsBar
|
$: showBar = definition?.showSettingsBar && !$builderStore.isDragging
|
||||||
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
||||||
|
|
||||||
const updatePosition = () => {
|
const updatePosition = () => {
|
||||||
|
|
|
@ -23,6 +23,7 @@ const createBuilderStore = () => {
|
||||||
theme: null,
|
theme: null,
|
||||||
customTheme: null,
|
customTheme: null,
|
||||||
previewDevice: "desktop",
|
previewDevice: "desktop",
|
||||||
|
isDragging: false,
|
||||||
}
|
}
|
||||||
const writableStore = writable(initialState)
|
const writableStore = writable(initialState)
|
||||||
const derivedStore = derived(writableStore, $state => {
|
const derivedStore = derived(writableStore, $state => {
|
||||||
|
@ -64,13 +65,24 @@ const createBuilderStore = () => {
|
||||||
dispatchEvent("preview-loaded")
|
dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
setSelectedPath: path => {
|
setSelectedPath: path => {
|
||||||
console.log("set to ")
|
|
||||||
console.log(path)
|
|
||||||
writableStore.update(state => {
|
writableStore.update(state => {
|
||||||
state.selectedPath = path
|
state.selectedPath = path
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
moveComponent: (componentId, destinationComponentId, mode) => {
|
||||||
|
dispatchEvent("move-component", {
|
||||||
|
componentId,
|
||||||
|
destinationComponentId,
|
||||||
|
mode,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setDragging: dragging => {
|
||||||
|
writableStore.update(state => {
|
||||||
|
state.isDragging = dragging
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
},
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...writableStore,
|
...writableStore,
|
||||||
|
|
|
@ -23,10 +23,14 @@ export const styleable = (node, styles = {}) => {
|
||||||
let applyHoverStyles
|
let applyHoverStyles
|
||||||
let selectComponent
|
let selectComponent
|
||||||
|
|
||||||
|
// Allow dragging if required
|
||||||
|
const parent = node.closest(".component")
|
||||||
|
if (parent && parent.classList.contains("draggable")) {
|
||||||
|
node.setAttribute("draggable", true)
|
||||||
|
}
|
||||||
|
|
||||||
// Creates event listeners and applies initial styles
|
// Creates event listeners and applies initial styles
|
||||||
const setupStyles = (newStyles = {}) => {
|
const setupStyles = (newStyles = {}) => {
|
||||||
// Use empty state styles as base styles if required, but let them, get
|
|
||||||
// overridden by any user specified styles
|
|
||||||
let baseStyles = {}
|
let baseStyles = {}
|
||||||
if (newStyles.empty) {
|
if (newStyles.empty) {
|
||||||
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
baseStyles.border = "2px dashed var(--spectrum-global-color-gray-600)"
|
||||||
|
@ -45,7 +49,6 @@ export const styleable = (node, styles = {}) => {
|
||||||
// Applies a style string to a DOM node
|
// Applies a style string to a DOM node
|
||||||
const applyStyles = styleString => {
|
const applyStyles = styleString => {
|
||||||
node.style = styleString
|
node.style = styleString
|
||||||
node.dataset.componentId = componentId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Applies the "normal" style definition
|
// Applies the "normal" style definition
|
||||||
|
|
Loading…
Reference in New Issue