Use IntersectionObservers to determine visibility of selected and hovered components

This commit is contained in:
Andrew Kingston 2021-06-10 15:13:51 +01:00
parent 1b2d5b6e47
commit da72a079ae
5 changed files with 139 additions and 116 deletions

View File

@ -68,13 +68,20 @@
<Component definition={$screenStore.activeLayout.props} />
</div>
<NotificationDisplay />
<!-- Key block needs to be outside the if statement or it breaks -->
{#key $builderStore.selectedComponentId}
{#if $builderStore.inBuilder && $builderStore.selectedComponent}
{#if $builderStore.inBuilder}
<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 />
<HoverIndicator />
{/if}
{/key}
</Provider>
</div>
{/if}

View File

@ -1,86 +1,32 @@
<script>
import { onMount, onDestroy } from "svelte"
import IndicatorSet from "./IndicatorSet.svelte"
import { builderStore } from "../store"
import Indicator from "./Indicator.svelte"
import { domDebounce } from "../utils/domDebounce"
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 - 2,
left: elBounds.left + scrollX - 2,
width: elBounds.width + 4,
height: elBounds.height + 4,
})
}
}
}
indicators = newIndicators
}
const debouncedUpdate = domDebounce(updatePosition)
$: zIndex = componentId === $builderStore.selectedComponentId ? 900 : 920
const onMouseOver = e => {
const element = e.target.closest("[data-type='component']")
const newId = element?.dataset?.id
const newName = element?.dataset?.name
if (newId !== componentId) {
componentId = newId
componentName = newName
debouncedUpdate()
}
}
const onMouseLeave = () => {
componentId = null
componentName = null
}
onMount(() => {
debouncedUpdate()
interval = setInterval(debouncedUpdate, 100)
document.addEventListener("mouseover", onMouseOver)
document.addEventListener("mouseleave", onMouseLeave)
document.addEventListener("scroll", debouncedUpdate, true)
})
onDestroy(() => {
clearInterval(interval)
document.removeEventListener("mouseover", onMouseOver)
document.removeEventListener("mouseleave", onMouseLeave)
document.removeEventListener("scroll", debouncedUpdate, true)
})
</script>
{#key componentId}
{#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}
<IndicatorSet {componentId} color="rgb(120, 170, 244)" transition {zIndex} />

View File

@ -7,13 +7,18 @@
export let height
export let text
export let color
export let zIndex
export let transition = false
</script>
<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"
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}
<div class="text" class:flipped={top < 22}>
@ -24,8 +29,8 @@
<style>
.indicator {
position: absolute;
z-index: 910;
position: fixed;
z-index: var(--zIndex);
border: 2px solid var(--color);
pointer-events: none;
border-radius: 4px;

View File

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

View File

@ -1,56 +1,11 @@
<script>
import { onMount, onDestroy } from "svelte"
import { builderStore } from "../store"
import Indicator from "./Indicator.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)
})
import IndicatorSet from "./IndicatorSet.svelte"
</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}
<IndicatorSet
componentId={$builderStore.selectedComponentId}
color="rgb(66, 133, 244)"
/>
{/each}
zIndex="910"
transition
/>