Use IntersectionObservers to determine visibility of selected and hovered components
This commit is contained in:
parent
1b2d5b6e47
commit
da72a079ae
|
@ -68,13 +68,20 @@
|
||||||
<Component definition={$screenStore.activeLayout.props} />
|
<Component definition={$screenStore.activeLayout.props} />
|
||||||
</div>
|
</div>
|
||||||
<NotificationDisplay />
|
<NotificationDisplay />
|
||||||
|
<!-- Key block needs to be outside the if statement or it breaks -->
|
||||||
{#key $builderStore.selectedComponentId}
|
{#key $builderStore.selectedComponentId}
|
||||||
{#if $builderStore.inBuilder && $builderStore.selectedComponent}
|
{#if $builderStore.inBuilder}
|
||||||
<SettingsBar />
|
<SettingsBar />
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
<!--
|
||||||
|
We don't want to key these by componentID as they control their own
|
||||||
|
re-mounting to avoid flashes.
|
||||||
|
-->
|
||||||
|
{#if $builderStore.inBuilder}
|
||||||
<SelectionIndicator />
|
<SelectionIndicator />
|
||||||
<HoverIndicator />
|
<HoverIndicator />
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
|
||||||
</Provider>
|
</Provider>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -1,86 +1,32 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte"
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
import { builderStore } from "../store"
|
import { builderStore } from "../store"
|
||||||
import Indicator from "./Indicator.svelte"
|
|
||||||
import { domDebounce } from "../utils/domDebounce"
|
|
||||||
|
|
||||||
let indicators = []
|
|
||||||
let interval
|
|
||||||
let componentId
|
let componentId
|
||||||
let componentName
|
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
|
||||||
|
|
||||||
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 - 2,
|
|
||||||
left: elBounds.left + scrollX - 2,
|
|
||||||
width: elBounds.width + 4,
|
|
||||||
height: elBounds.height + 4,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
indicators = newIndicators
|
|
||||||
}
|
|
||||||
const debouncedUpdate = domDebounce(updatePosition)
|
|
||||||
|
|
||||||
const onMouseOver = e => {
|
const onMouseOver = e => {
|
||||||
const element = e.target.closest("[data-type='component']")
|
const element = e.target.closest("[data-type='component']")
|
||||||
const newId = element?.dataset?.id
|
const newId = element?.dataset?.id
|
||||||
const newName = element?.dataset?.name
|
|
||||||
if (newId !== componentId) {
|
if (newId !== componentId) {
|
||||||
componentId = newId
|
componentId = newId
|
||||||
componentName = newName
|
|
||||||
debouncedUpdate()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMouseLeave = () => {
|
const onMouseLeave = () => {
|
||||||
componentId = null
|
componentId = null
|
||||||
componentName = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
debouncedUpdate()
|
|
||||||
interval = setInterval(debouncedUpdate, 100)
|
|
||||||
document.addEventListener("mouseover", onMouseOver)
|
document.addEventListener("mouseover", onMouseOver)
|
||||||
document.addEventListener("mouseleave", onMouseLeave)
|
document.addEventListener("mouseleave", onMouseLeave)
|
||||||
document.addEventListener("scroll", debouncedUpdate, true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
clearInterval(interval)
|
|
||||||
document.removeEventListener("mouseover", onMouseOver)
|
document.removeEventListener("mouseover", onMouseOver)
|
||||||
document.removeEventListener("mouseleave", onMouseLeave)
|
document.removeEventListener("mouseleave", onMouseLeave)
|
||||||
document.removeEventListener("scroll", debouncedUpdate, true)
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key componentId}
|
<IndicatorSet {componentId} color="rgb(120, 170, 244)" transition {zIndex} />
|
||||||
{#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)"
|
|
||||||
transition
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
|
|
|
@ -7,13 +7,18 @@
|
||||||
export let height
|
export let height
|
||||||
export let text
|
export let text
|
||||||
export let color
|
export let color
|
||||||
|
export let zIndex
|
||||||
export let transition = false
|
export let transition = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
in:fade={{ delay: transition ? 65 : 0, duration: transition ? 130 : 0 }}
|
in:fade={{
|
||||||
|
delay: transition ? 50 : 0,
|
||||||
|
duration: transition ? 130 : 0,
|
||||||
|
}}
|
||||||
|
out:fade={{ duration: transition ? 130 : 0 }}
|
||||||
class="indicator"
|
class="indicator"
|
||||||
style="top: {top}px; left: {left}px; width: {width}px; height: {height}px; --color: {color};"
|
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 < 22}>
|
<div class="text" class:flipped={top < 22}>
|
||||||
|
@ -24,8 +29,8 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.indicator {
|
.indicator {
|
||||||
position: absolute;
|
position: fixed;
|
||||||
z-index: 910;
|
z-index: var(--zIndex);
|
||||||
border: 2px solid var(--color);
|
border: 2px solid var(--color);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, onDestroy } from "svelte"
|
||||||
|
import Indicator from "./Indicator.svelte"
|
||||||
|
import { domDebounce } from "../utils/domDebounce"
|
||||||
|
|
||||||
|
export let componentId
|
||||||
|
export let color
|
||||||
|
export let transition
|
||||||
|
export let zIndex
|
||||||
|
|
||||||
|
let indicators = []
|
||||||
|
let interval
|
||||||
|
let text
|
||||||
|
$: visibleIndicators = indicators.filter(x => x.visible)
|
||||||
|
|
||||||
|
let updating = false
|
||||||
|
let observers = []
|
||||||
|
let callbackCount = 0
|
||||||
|
let nextIndicators = []
|
||||||
|
|
||||||
|
const createIntersectionCallback = idx => entries => {
|
||||||
|
if (callbackCount >= observers.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nextIndicators[idx].visible = entries[0].isIntersecting
|
||||||
|
if (++callbackCount === observers.length) {
|
||||||
|
indicators = nextIndicators
|
||||||
|
updating = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePosition = () => {
|
||||||
|
if (updating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (!componentId) {
|
||||||
|
indicators = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
updating = true
|
||||||
|
callbackCount = 0
|
||||||
|
observers.forEach(o => o.disconnect())
|
||||||
|
observers = []
|
||||||
|
nextIndicators = []
|
||||||
|
|
||||||
|
// Determine next set of indicators
|
||||||
|
const parents = document.getElementsByClassName(componentId)
|
||||||
|
if (parents.length) {
|
||||||
|
text = parents[0].dataset.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch reads to minimize reflow
|
||||||
|
const scrollX = window.scrollX
|
||||||
|
const scrollY = window.scrollY
|
||||||
|
|
||||||
|
// Extract valid children
|
||||||
|
const children = Array.from(parents)
|
||||||
|
.map(parent => parent?.childNodes?.[0])
|
||||||
|
.filter(child => child != null)
|
||||||
|
|
||||||
|
children.forEach((child, idx) => {
|
||||||
|
const callback = createIntersectionCallback(idx)
|
||||||
|
const threshold = children.length > 1 ? 1 : 0
|
||||||
|
const observer = new IntersectionObserver(callback, { threshold })
|
||||||
|
observer.observe(child)
|
||||||
|
observers.push(observer)
|
||||||
|
|
||||||
|
const elBounds = child.getBoundingClientRect()
|
||||||
|
nextIndicators.push({
|
||||||
|
top: elBounds.top + scrollY - 2,
|
||||||
|
left: elBounds.left + scrollX - 2,
|
||||||
|
width: elBounds.width + 4,
|
||||||
|
height: elBounds.height + 4,
|
||||||
|
visible: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const debouncedUpdate = domDebounce(updatePosition)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
debouncedUpdate()
|
||||||
|
interval = setInterval(debouncedUpdate, 100)
|
||||||
|
document.addEventListener("scroll", debouncedUpdate, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
clearInterval(interval)
|
||||||
|
document.removeEventListener("scroll", debouncedUpdate, true)
|
||||||
|
observers.forEach(o => o.disconnect())
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#key componentId}
|
||||||
|
{#each visibleIndicators as indicator, idx}
|
||||||
|
<Indicator
|
||||||
|
top={indicator.top}
|
||||||
|
left={indicator.left}
|
||||||
|
width={indicator.width}
|
||||||
|
height={indicator.height}
|
||||||
|
text={idx === 0 ? text : null}
|
||||||
|
{transition}
|
||||||
|
{zIndex}
|
||||||
|
{color}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
{/key}
|
|
@ -1,56 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { onMount, onDestroy } from "svelte"
|
|
||||||
import { builderStore } from "../store"
|
import { builderStore } from "../store"
|
||||||
import Indicator from "./Indicator.svelte"
|
import IndicatorSet from "./IndicatorSet.svelte"
|
||||||
import { domDebounce } from "../utils/domDebounce"
|
|
||||||
|
|
||||||
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 - 2,
|
|
||||||
left: elBounds.left + scrollX - 2,
|
|
||||||
width: elBounds.width + 4,
|
|
||||||
height: elBounds.height + 4,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
indicators = newIndicators
|
|
||||||
}
|
|
||||||
const debouncedUpdate = domDebounce(updatePosition)
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
debouncedUpdate()
|
|
||||||
interval = setInterval(debouncedUpdate, 100)
|
|
||||||
document.addEventListener("scroll", debouncedUpdate, true)
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
clearInterval(interval)
|
|
||||||
document.removeEventListener("scroll", debouncedUpdate, true)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each indicators as indicator, idx}
|
<IndicatorSet
|
||||||
<Indicator
|
componentId={$builderStore.selectedComponentId}
|
||||||
top={indicator.top}
|
|
||||||
left={indicator.left}
|
|
||||||
width={indicator.width}
|
|
||||||
height={indicator.height}
|
|
||||||
text={idx === 0 ? $builderStore.selectedComponent._instanceName : null}
|
|
||||||
color="rgb(66, 133, 244)"
|
color="rgb(66, 133, 244)"
|
||||||
/>
|
zIndex="910"
|
||||||
{/each}
|
transition
|
||||||
|
/>
|
||||||
|
|
Loading…
Reference in New Issue