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

View File

@ -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;
} }
*, *,

View File

@ -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 />
{/if} <HoverIndicator />
{/if}
{/key}
</Provider> </Provider>
</div> </div>
{/if} {/if}

View File

@ -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> {/if}
{/key} {/key}
{/if} </div>
<style> <style>
div { 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 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);

View File

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