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:
Andrew Kingston 2021-06-08 14:19:03 +01:00
parent a88eeb7de3
commit ba1908f7f9
9 changed files with 217 additions and 44 deletions

View File

@ -101,12 +101,12 @@
overflow: hidden;
margin: auto;
height: 100%;
background-color: white;
}
.component-container iframe {
border: 0;
left: 0;
top: 0;
width: 100%;
background-color: transparent;
}
</style>

View File

@ -14,10 +14,13 @@ export default `
<style>
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
body {
padding: 2px;
}
*,

View File

@ -15,6 +15,8 @@
} from "../store"
import { TableNames, ActionTypes } from "../constants"
import SettingsBar from "./SettingsBar.svelte"
import SelectionIndicator from "./SelectionIndicator.svelte"
import HoverIndicator from "./HoverIndicator.svelte"
// Provide contexts
setContext("sdk", SDK)
@ -64,11 +66,13 @@
<Provider key="user" data={$authStore} {actions}>
<Component definition={$screenStore.activeLayout.props} />
<NotificationDisplay />
{#if $builderStore.inBuilder && $builderStore.selectedComponent}
{#key $builderStore.selectedComponentId}
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder && $builderStore.selectedComponent}
<SettingsBar />
{/key}
{/if}
<SelectionIndicator />
<HoverIndicator />
{/if}
{/key}
</Provider>
</div>
{/if}

View File

@ -32,6 +32,7 @@
$: constructor = getComponentConstructor(definition._component)
$: children = definition._children || []
$: id = definition._id
$: name = definition._instanceName
$: updateComponentProps(definition, $context)
$: styles = definition._styles
$: transition = definition._transition
@ -99,9 +100,9 @@
}
</script>
{#if constructor && componentProps}
<div class={id} data-type="component" data-id={id} data-name={name}>
{#key propsHash}
<div class={id}>
{#if constructor && componentProps}
<svelte:component this={constructor} {...componentProps}>
{#if children.length}
{#each children as child (child._id)}
@ -109,9 +110,9 @@
{/each}
{/if}
</svelte:component>
</div>
{/if}
{/key}
{/if}
</div>
<style>
div {

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -3,11 +3,15 @@
import SettingsButton from "./SettingsButton.svelte"
import { builderStore } from "../store"
const verticalOffset = 28
const horizontalOffset = 2
let top = 0
let left = 0
let interval
let self
let measured = false
$: definition = $builderStore.selectedComponentDefinition
$: showBar = definition?.showSettingsBar
$: settings = definition?.settings?.filter(setting => setting.showInBar) ?? []
@ -20,32 +24,35 @@
const parent = document.getElementsByClassName(id)?.[0]
const element = parent?.childNodes?.[0]
if (element && self) {
// Batch reads to minimize reflow
const elBounds = element.getBoundingClientRect()
const width = self.offsetWidth
const height = self.offsetHeight
const { scrollX, scrollY, innerWidth } = window
// 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) {
newTop = elBounds.top + window.scrollY + 10
newTop = elBounds.top + scrollY + verticalOffset
}
// Horizontally, try to center first.
// Failing that, render to left edge of component.
// Failing that, render to right edge of component,
// 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
if (newLeft < 0 || newLeft + width > window.innerWidth) {
newLeft = elBounds.left + window.scrollX
if (newLeft < 0 || newLeft + width > window.innerWidth) {
newLeft = elBounds.left + window.scrollX + elBounds.width - width
if (newLeft < 0 || newLeft + width > window.innerWidth) {
if (newLeft < 0 || newLeft + width > innerWidth) {
newLeft = elBounds.left + scrollX - horizontalOffset
if (newLeft < 0 || newLeft + width > innerWidth) {
newLeft = innerWidth - width - 20
if (newLeft < 0 || newLeft + width > innerWidth) {
newLeft = 0
}
}
}
// Only update state when things changes to minimize renders
if (Math.round(newTop) !== Math.round(top)) {
top = newTop
}
@ -62,9 +69,7 @@
})
onDestroy(() => {
if (interval) {
clearInterval(interval)
}
clearInterval(interval)
})
</script>
@ -97,8 +102,8 @@
.bar {
display: flex;
position: absolute;
z-index: 999;
padding: 6px 10px;
z-index: 920;
padding: 6px 8px;
opacity: 0;
flex-direction: row;
background: var(--background);

View File

@ -14,25 +14,6 @@ const buildStyleString = (styleObject, 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.
* 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
const applyStyles = styleString => {
node.style = addBuilderPreviewStyles(node, styleString, componentId)
node.style = styleString
node.dataset.componentId = componentId
}