184 lines
4.7 KiB
Svelte
184 lines
4.7 KiB
Svelte
<script>
|
|
import { onMount, onDestroy } from "svelte"
|
|
import Indicator from "./Indicator.svelte"
|
|
import { builderStore } from "stores"
|
|
import { memo } from "@budibase/frontend-core"
|
|
|
|
export let componentId = null
|
|
export let color = null
|
|
export let zIndex = 900
|
|
export let prefix = null
|
|
export let allowResizeAnchors = false
|
|
|
|
const errorColor = "var(--spectrum-global-color-static-red-600)"
|
|
const defaultState = () => ({
|
|
// Cached props
|
|
componentId,
|
|
color,
|
|
zIndex,
|
|
prefix,
|
|
allowResizeAnchors,
|
|
|
|
// Computed state
|
|
indicators: [],
|
|
text: null,
|
|
icon: null,
|
|
insideGrid: false,
|
|
error: false,
|
|
})
|
|
const config = memo($$props)
|
|
|
|
let interval
|
|
let state = defaultState()
|
|
let nextState = null
|
|
let updating = false
|
|
let observers = []
|
|
let callbackCount = 0
|
|
|
|
$: visibleIndicators = state.indicators.filter(x => x.visible)
|
|
$: offset = $builderStore.inBuilder ? 0 : 2
|
|
$: config.set({
|
|
componentId,
|
|
color,
|
|
zIndex,
|
|
prefix,
|
|
allowResizeAnchors,
|
|
})
|
|
|
|
const checkInsideGrid = id => {
|
|
const component = document.getElementsByClassName(id)[0]
|
|
const domNode = component?.children[0]
|
|
|
|
// Ignore grid itself
|
|
if (domNode?.classList?.contains("grid")) {
|
|
return false
|
|
}
|
|
|
|
return component?.parentNode
|
|
?.closest?.(".component")
|
|
?.childNodes[0]?.classList?.contains("grid")
|
|
}
|
|
|
|
const createIntersectionCallback = idx => entries => {
|
|
if (callbackCount >= observers.length) {
|
|
return
|
|
}
|
|
nextState.indicators[idx].visible =
|
|
nextState.indicators[idx].insideModal ||
|
|
nextState.indicators[idx].insideSidePanel ||
|
|
entries[0].isIntersecting
|
|
if (++callbackCount === observers.length) {
|
|
state = nextState
|
|
updating = false
|
|
}
|
|
}
|
|
|
|
const updatePosition = () => {
|
|
if (updating) {
|
|
return
|
|
}
|
|
|
|
// Sanity check
|
|
if (!componentId) {
|
|
state = defaultState()
|
|
return
|
|
}
|
|
|
|
// Reset state
|
|
updating = true
|
|
callbackCount = 0
|
|
observers.forEach(o => o.disconnect())
|
|
observers = []
|
|
nextState = defaultState()
|
|
|
|
// Check if we're inside a grid
|
|
if (allowResizeAnchors) {
|
|
nextState.insideGrid = checkInsideGrid(componentId)
|
|
}
|
|
|
|
// Determine next set of indicators
|
|
const parents = document.getElementsByClassName(componentId)
|
|
if (parents.length) {
|
|
nextState.text = parents[0].dataset.name
|
|
if (nextState.prefix) {
|
|
nextState.text = `${nextState.prefix} ${nextState.text}`
|
|
}
|
|
if (parents[0].dataset.icon) {
|
|
nextState.icon = parents[0].dataset.icon
|
|
}
|
|
}
|
|
nextState.error = parents?.[0]?.classList.contains("error")
|
|
|
|
// 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(
|
|
document.getElementsByClassName(`${componentId}-dom`)
|
|
)
|
|
.filter(x => x != null)
|
|
.slice(0, 100)
|
|
|
|
// If there aren't any nodes then reset
|
|
if (!children.length) {
|
|
state = defaultState()
|
|
updating = false
|
|
return
|
|
}
|
|
|
|
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()
|
|
nextState.indicators.push({
|
|
top: elBounds.top + scrollY - deviceBounds.top - offset,
|
|
left: elBounds.left + scrollX - deviceBounds.left - offset,
|
|
width: elBounds.width + 4,
|
|
height: elBounds.height + 4,
|
|
visible: false,
|
|
insideSidePanel: !!child.closest(".side-panel"),
|
|
insideModal: !!child.closest(".modal-content"),
|
|
})
|
|
})
|
|
}
|
|
|
|
onMount(() => {
|
|
updatePosition()
|
|
config.subscribe(updatePosition)
|
|
interval = setInterval(updatePosition, 100)
|
|
document.addEventListener("scroll", updatePosition, true)
|
|
})
|
|
|
|
onDestroy(() => {
|
|
clearInterval(interval)
|
|
document.removeEventListener("scroll", updatePosition, true)
|
|
observers.forEach(o => o.disconnect())
|
|
})
|
|
</script>
|
|
|
|
{#each visibleIndicators as indicator, idx}
|
|
<Indicator
|
|
top={indicator.top}
|
|
left={indicator.left}
|
|
width={indicator.width}
|
|
height={indicator.height}
|
|
text={idx === 0 ? state.text : null}
|
|
icon={idx === 0 ? state.icon : null}
|
|
showResizeAnchors={state.allowResizeAnchors && state.insideGrid}
|
|
color={state.error ? errorColor : state.color}
|
|
componentId={state.componentId}
|
|
zIndex={state.zIndex}
|
|
/>
|
|
{/each}
|