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
c41f57861c
commit
65e7453fbf
|
@ -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>
|
||||
|
|
|
@ -14,10 +14,13 @@ export default `
|
|||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
body {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
*,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 { 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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue