budibase/packages/client/src/components/preview/IndicatorSet.svelte

128 lines
3.1 KiB
Svelte

<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
export let prefix = null
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
if (prefix) {
text = `${prefix} ${text}`
}
}
// Batch reads to minimize reflow
const scrollX = window.scrollX
const scrollY = window.scrollY
// Extract valid children
// Sanity limit of 100 active indicators
const children = Array.from(parents)
.map(parent => parent?.childNodes?.[0])
.filter(node => node?.nodeType === 1)
.slice(0, 100)
// If there aren't any nodes then reset
if (!children.length) {
indicators = []
updating = false
}
const device = document.getElementById("app-root")
const deviceBounds = device.getBoundingClientRect()
children.forEach((child, idx) => {
const callback = createIntersectionCallback(idx)
const threshold = children.length > 1 ? 1 : 0
const observer = new IntersectionObserver(callback, {
threshold,
root: device,
})
observer.observe(child)
observers.push(observer)
const elBounds = child.getBoundingClientRect()
nextIndicators.push({
top: elBounds.top + scrollY - deviceBounds.top,
left: elBounds.left + scrollX - deviceBounds.left,
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}