Render builder preview selection indicators via top level component instead of via styleable healper. Add indiciator on hover and add name to indicator
This commit is contained in:
parent
a88eeb7de3
commit
ba1908f7f9
|
@ -101,12 +101,12 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: white;
|
|
||||||
}
|
}
|
||||||
.component-container iframe {
|
.component-container iframe {
|
||||||
border: 0;
|
border: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -14,10 +14,13 @@ export default `
|
||||||
<style>
|
<style>
|
||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
*,
|
*,
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
} from "../store"
|
} from "../store"
|
||||||
import { TableNames, ActionTypes } from "../constants"
|
import { TableNames, ActionTypes } from "../constants"
|
||||||
import SettingsBar from "./SettingsBar.svelte"
|
import SettingsBar from "./SettingsBar.svelte"
|
||||||
|
import SelectionIndicator from "./SelectionIndicator.svelte"
|
||||||
|
import HoverIndicator from "./HoverIndicator.svelte"
|
||||||
|
|
||||||
// Provide contexts
|
// Provide contexts
|
||||||
setContext("sdk", SDK)
|
setContext("sdk", SDK)
|
||||||
|
@ -64,11 +66,13 @@
|
||||||
<Provider key="user" data={$authStore} {actions}>
|
<Provider key="user" data={$authStore} {actions}>
|
||||||
<Component definition={$screenStore.activeLayout.props} />
|
<Component definition={$screenStore.activeLayout.props} />
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
{#if $builderStore.inBuilder && $builderStore.selectedComponent}
|
|
||||||
{#key $builderStore.selectedComponentId}
|
{#key $builderStore.selectedComponentId}
|
||||||
|
{#if $builderStore.inBuilder && $builderStore.selectedComponent}
|
||||||
<SettingsBar />
|
<SettingsBar />
|
||||||
{/key}
|
<SelectionIndicator />
|
||||||
|
<HoverIndicator />
|
||||||
{/if}
|
{/if}
|
||||||
|
{/key}
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
$: constructor = getComponentConstructor(definition._component)
|
$: constructor = getComponentConstructor(definition._component)
|
||||||
$: children = definition._children || []
|
$: children = definition._children || []
|
||||||
$: id = definition._id
|
$: id = definition._id
|
||||||
|
$: name = definition._instanceName
|
||||||
$: updateComponentProps(definition, $context)
|
$: updateComponentProps(definition, $context)
|
||||||
$: styles = definition._styles
|
$: styles = definition._styles
|
||||||
$: transition = definition._transition
|
$: transition = definition._transition
|
||||||
|
@ -99,9 +100,9 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if constructor && componentProps}
|
<div class={id} data-type="component" data-id={id} data-name={name}>
|
||||||
{#key propsHash}
|
{#key propsHash}
|
||||||
<div class={id}>
|
{#if constructor && componentProps}
|
||||||
<svelte:component this={constructor} {...componentProps}>
|
<svelte:component this={constructor} {...componentProps}>
|
||||||
{#if children.length}
|
{#if children.length}
|
||||||
{#each children as child (child._id)}
|
{#each children as child (child._id)}
|
||||||
|
@ -109,9 +110,9 @@
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
</div>
|
|
||||||
{/key}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
div {
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { builderStore } from "../store"
|
||||||
|
import Indicator from "./Indicator.svelte"
|
||||||
|
|
||||||
|
const offset = 2
|
||||||
|
let indicators = []
|
||||||
|
let interval
|
||||||
|
let componentId
|
||||||
|
let componentName
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
let newIndicators = []
|
||||||
|
|
||||||
|
if (componentId) {
|
||||||
|
const parents = document.getElementsByClassName(componentId)
|
||||||
|
|
||||||
|
// Batch reads to minimize reflow
|
||||||
|
const scrollX = window.scrollX
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
|
||||||
|
for (let i = 0; i < parents.length; i++) {
|
||||||
|
const child = parents[i]?.childNodes?.[0]
|
||||||
|
if (child) {
|
||||||
|
const elBounds = child.getBoundingClientRect()
|
||||||
|
newIndicators.push({
|
||||||
|
top: elBounds.top + scrollY - offset * 2,
|
||||||
|
left: elBounds.left + scrollX - offset * 2,
|
||||||
|
width: elBounds.width + offset * 2,
|
||||||
|
height: elBounds.height + offset * 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
indicators = newIndicators
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseOver = e => {
|
||||||
|
const element = e.target.closest("[data-type='component']")
|
||||||
|
componentId = element?.dataset?.id
|
||||||
|
componentName = element?.dataset?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
componentId = null
|
||||||
|
componentName = null
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
interval = setInterval(updatePosition, 100)
|
||||||
|
window.addEventListener("mouseover", onMouseOver)
|
||||||
|
document.documentElement.addEventListener("mouseleave", onMouseLeave)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(interval)
|
||||||
|
window.removeEventListener("mouseover", onMouseOver)
|
||||||
|
document.documentElement.removeEventListener("mouseleave", onMouseLeave)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if componentId !== $builderStore.selectedComponentId}
|
||||||
|
{#each indicators as indicator, idx}
|
||||||
|
<Indicator
|
||||||
|
top={indicator.top}
|
||||||
|
left={indicator.left}
|
||||||
|
width={indicator.width}
|
||||||
|
height={indicator.height}
|
||||||
|
text={idx === 0 ? componentName : null}
|
||||||
|
color="rgb(120, 170, 244)"
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script>
|
||||||
|
export let top
|
||||||
|
export let left
|
||||||
|
export let width
|
||||||
|
export let height
|
||||||
|
export let text
|
||||||
|
export let color
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="indicator"
|
||||||
|
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color};"
|
||||||
|
>
|
||||||
|
{#if text}
|
||||||
|
<div class="text" class:flipped={top < 22}>
|
||||||
|
{text}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.indicator {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 910;
|
||||||
|
border: 2px solid var(--color);
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
background-color: var(--color);
|
||||||
|
color: white;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -2px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 8px 2px 8px;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
font-size: 11px;
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
white-space: nowrap;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.flipped {
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
transform: translateY(0%);
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,52 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import { builderStore } from "../store"
|
||||||
|
import Indicator from "./Indicator.svelte"
|
||||||
|
|
||||||
|
const offset = 2
|
||||||
|
let indicators = []
|
||||||
|
let interval
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
const id = $builderStore.selectedComponentId
|
||||||
|
const parents = document.getElementsByClassName(id)
|
||||||
|
|
||||||
|
// Batch reads to minimize reflow
|
||||||
|
const scrollX = window.scrollX
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
|
||||||
|
let newIndicators = []
|
||||||
|
for (let i = 0; i < parents.length; i++) {
|
||||||
|
const child = parents[i]?.childNodes?.[0]
|
||||||
|
if (child) {
|
||||||
|
const elBounds = child.getBoundingClientRect()
|
||||||
|
newIndicators.push({
|
||||||
|
top: elBounds.top + scrollY - offset * 2,
|
||||||
|
left: elBounds.left + scrollX - offset * 2,
|
||||||
|
width: elBounds.width + offset * 2,
|
||||||
|
height: elBounds.height + offset * 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
indicators = newIndicators
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
interval = setInterval(updatePosition, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(interval)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each indicators as indicator, idx}
|
||||||
|
<Indicator
|
||||||
|
top={indicator.top}
|
||||||
|
left={indicator.left}
|
||||||
|
width={indicator.width}
|
||||||
|
height={indicator.height}
|
||||||
|
text={idx === 0 ? $builderStore.selectedComponent._instanceName : null}
|
||||||
|
color="rgb(66, 133, 244)"
|
||||||
|
/>
|
||||||
|
{/each}
|
|
@ -3,11 +3,15 @@
|
||||||
import SettingsButton from "./SettingsButton.svelte"
|
import SettingsButton from "./SettingsButton.svelte"
|
||||||
import { builderStore } from "../store"
|
import { builderStore } from "../store"
|
||||||
|
|
||||||
|
const verticalOffset = 28
|
||||||
|
const horizontalOffset = 2
|
||||||
|
|
||||||
let top = 0
|
let top = 0
|
||||||
let left = 0
|
let left = 0
|
||||||
let interval
|
let interval
|
||||||
let self
|
let self
|
||||||
let measured = false
|
let measured = false
|
||||||
|
|
||||||
$: definition = $builderStore.selectedComponentDefinition
|
$: definition = $builderStore.selectedComponentDefinition
|
||||||
$: showBar = definition?.showSettingsBar
|
$: showBar = definition?.showSettingsBar
|
||||||
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
|
||||||
|
@ -20,32 +24,35 @@
|
||||||
const parent = document.getElementsByClassName(id)?.[0]
|
const parent = document.getElementsByClassName(id)?.[0]
|
||||||
const element = parent?.childNodes?.[0]
|
const element = parent?.childNodes?.[0]
|
||||||
if (element && self) {
|
if (element && self) {
|
||||||
|
// Batch reads to minimize reflow
|
||||||
const elBounds = element.getBoundingClientRect()
|
const elBounds = element.getBoundingClientRect()
|
||||||
const width = self.offsetWidth
|
const width = self.offsetWidth
|
||||||
const height = self.offsetHeight
|
const height = self.offsetHeight
|
||||||
|
const { scrollX, scrollY, innerWidth } = window
|
||||||
|
|
||||||
// Vertically, always render above unless no room, then render inside
|
// Vertically, always render above unless no room, then render inside
|
||||||
let newTop = elBounds.top + window.scrollY - 10 - height
|
let newTop = elBounds.top + scrollY - verticalOffset - height
|
||||||
if (newTop < 0) {
|
if (newTop < 0) {
|
||||||
newTop = elBounds.top + window.scrollY + 10
|
newTop = elBounds.top + scrollY + verticalOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
// Horizontally, try to center first.
|
// Horizontally, try to center first.
|
||||||
// Failing that, render to left edge of component.
|
// Failing that, render to left edge of component.
|
||||||
// Failing that, render to right edge of component,
|
// Failing that, render to right edge of component,
|
||||||
// Failing that, render to window left edge and accept defeat.
|
// Failing that, render to window left edge and accept defeat.
|
||||||
let elCenter = elBounds.left + window.scrollX + elBounds.width / 2
|
let elCenter = elBounds.left + scrollX + elBounds.width / 2
|
||||||
let newLeft = elCenter - width / 2
|
let newLeft = elCenter - width / 2
|
||||||
if (newLeft < 0 || newLeft + width > window.innerWidth) {
|
if (newLeft < 0 || newLeft + width > innerWidth) {
|
||||||
newLeft = elBounds.left + window.scrollX
|
newLeft = elBounds.left + scrollX - horizontalOffset
|
||||||
if (newLeft < 0 || newLeft + width > window.innerWidth) {
|
if (newLeft < 0 || newLeft + width > innerWidth) {
|
||||||
newLeft = elBounds.left + window.scrollX + elBounds.width - width
|
newLeft = innerWidth - width - 20
|
||||||
if (newLeft < 0 || newLeft + width > window.innerWidth) {
|
if (newLeft < 0 || newLeft + width > innerWidth) {
|
||||||
newLeft = 0
|
newLeft = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only update state when things changes to minimize renders
|
||||||
if (Math.round(newTop) !== Math.round(top)) {
|
if (Math.round(newTop) !== Math.round(top)) {
|
||||||
top = newTop
|
top = newTop
|
||||||
}
|
}
|
||||||
|
@ -62,9 +69,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -97,8 +102,8 @@
|
||||||
.bar {
|
.bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999;
|
z-index: 920;
|
||||||
padding: 6px 10px;
|
padding: 6px 8px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
|
|
|
@ -14,25 +14,6 @@ const buildStyleString = (styleObject, customStyles) => {
|
||||||
return str + (customStyles || "")
|
return str + (customStyles || "")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies styles to enrich the builder preview.
|
|
||||||
* Applies styles to highlight the selected component, and allows pointer
|
|
||||||
* events for any selectable components (overriding the blanket ban on pointer
|
|
||||||
* events in the iframe HTML).
|
|
||||||
*/
|
|
||||||
const addBuilderPreviewStyles = (node, styleString, componentId) => {
|
|
||||||
if (componentId === get(builderStore).selectedComponentId) {
|
|
||||||
const style = window.getComputedStyle(node)
|
|
||||||
const property = style?.display === "table-row" ? "outline" : "border"
|
|
||||||
return (
|
|
||||||
styleString +
|
|
||||||
`;${property}: 2px solid #4285f4 !important; border-radius: 4px !important;`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return styleString
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Svelte action to apply correct component styles.
|
* Svelte action to apply correct component styles.
|
||||||
* This also applies handlers for selecting components from the builder preview.
|
* This also applies handlers for selecting components from the builder preview.
|
||||||
|
@ -54,7 +35,7 @@ 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 = addBuilderPreviewStyles(node, styleString, componentId)
|
node.style = styleString
|
||||||
node.dataset.componentId = componentId
|
node.dataset.componentId = componentId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue