From d423d530e417e80943be330b74d76e55e3055f19 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 12 Aug 2024 14:45:17 +0100 Subject: [PATCH] Rewrite settings bar updates to improve performance --- .../src/components/app/GridBlock.svelte | 2 +- .../src/components/preview/SettingsBar.svelte | 204 ++++++++++-------- 2 files changed, 110 insertions(+), 96 deletions(-) diff --git a/packages/client/src/components/app/GridBlock.svelte b/packages/client/src/components/app/GridBlock.svelte index 357e9ff084..30a35b0713 100644 --- a/packages/client/src/components/app/GridBlock.svelte +++ b/packages/client/src/components/app/GridBlock.svelte @@ -198,7 +198,7 @@ overflow: hidden; height: 410px; } - div.in-builder { + div.in-builder :global(> *) { pointer-events: none !important; } diff --git a/packages/client/src/components/preview/SettingsBar.svelte b/packages/client/src/components/preview/SettingsBar.svelte index b0c8cfcc82..09706726bb 100644 --- a/packages/client/src/components/preview/SettingsBar.svelte +++ b/packages/client/src/components/preview/SettingsBar.svelte @@ -11,25 +11,24 @@ const context = getContext("context") const verticalOffset = 36 const horizontalOffset = 2 + const observer = new MutationObserver(() => debouncedUpdate()) let top = 0 let left = 0 let interval let self let measured = false - let observer - + let observing = false let insideGrid = false let gridHAlign let gridVAlign + let deviceEl - // TODO: respect dependsOn keys - - $: componentId = $builderStore.selectedComponentId - $: measured, observeComputedStyles(componentId) + $: id = $builderStore.selectedComponentId + $: id, reset() $: component = $componentStore.selectedComponent $: definition = $componentStore.selectedComponentDefinition - $: instance = componentStore.actions.getComponentInstance(componentId) + $: instance = componentStore.actions.getComponentInstance(id) $: state = $instance?.state $: showBar = definition?.showSettingsBar !== false && @@ -37,7 +36,7 @@ definition && !$state?.errorState $: settings = getBarSettings(component, definition) - $: isRoot = componentId === $builderStore.screen?.props?._id + $: isRoot = id === $builderStore.screen?.props?._id $: showGridStyles = insideGrid && (definition?.grid?.hAlign !== "stretch" || @@ -47,6 +46,21 @@ $: gridHAlignVar = getGridVar(device, GridParams.HAlign) $: gridVAlignVar = getGridVar(device, GridParams.VAlign) + const reset = () => { + observer.disconnect() + measured = false + observing = false + insideGrid = false + } + + const startObserving = domBoundary => { + observer.observe(domBoundary, { + attributes: true, + attributeFilter: ["style"], + }) + observing = true + } + const getBarSettings = (component, definition) => { let allSettings = [] definition?.settings?.forEach(setting => { @@ -68,102 +82,102 @@ if (!showBar) { return } - const id = $builderStore.selectedComponentId - let element = document.getElementsByClassName(id)?.[0] - // Check if we're inside a grid - insideGrid = element?.parentNode.classList.contains("grid") + // Find DOM boundary and ensure it is valid + let domBoundary = document.getElementsByClassName(id)[0] + if (!domBoundary) { + return reset() + } + + // If we're inside a grid, allow time for buttons to render + const nextInsideGrid = domBoundary.dataset.insideGrid === "true" + if (nextInsideGrid && !insideGrid) { + insideGrid = true + return + } else { + insideGrid = nextInsideGrid + } + + // Get the correct DOM boundary depending if we're inside a grid or not if (!insideGrid) { - element = element?.children?.[0] + domBoundary = + domBoundary.getElementsByClassName(`${id}-dom`)[0] || + domBoundary.children?.[0] + } + if (!domBoundary || !self) { + return reset() } - // The settings bar is higher in the dom tree than the selection indicators - // as we want to be able to render the settings bar wider than the screen, - // or outside the screen. - // Therefore we use the clip root rather than the app root to determine - // its position. - const device = document.getElementById("clip-root") - if (element && self) { - // Batch reads to minimize reflow - const deviceBounds = device.getBoundingClientRect() - const elBounds = element.getBoundingClientRect() - const width = self.offsetWidth - const height = self.offsetHeight - const { scrollX, scrollY, innerWidth } = window + // Start observing if required + if (!observing) { + startObserving(domBoundary) + } - // Read grid metadata from data attributes - if (insideGrid) { - if (mobile) { - gridHAlign = element.dataset.gridMobileHAlign - gridVAlign = element.dataset.gridMobileVAlign - } else { - gridHAlign = element.dataset.gridDesktopHAlign - gridVAlign = element.dataset.gridDesktopVAlign - } - } + // Batch reads to minimize reflow + const deviceBounds = deviceEl.getBoundingClientRect() + const elBounds = domBoundary.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 + scrollY - verticalOffset - height - if (newTop < deviceBounds.top - 50) { - newTop = deviceBounds.top - 50 - } - if (newTop < 0) { - newTop = 0 - } - const deviceBottom = deviceBounds.top + deviceBounds.height - if (newTop > deviceBottom - 44) { - newTop = deviceBottom - 44 + // Read grid metadata from data attributes + if (insideGrid) { + if (mobile) { + gridHAlign = domBoundary.dataset.gridMobileHAlign + gridVAlign = domBoundary.dataset.gridMobileVAlign + } else { + gridHAlign = domBoundary.dataset.gridDesktopHAlign + gridVAlign = domBoundary.dataset.gridDesktopVAlign } + } - //If element is at the very top of the screen, put the bar below the element - if (elBounds.top < elBounds.height && elBounds.height < 80) { - newTop = elBounds.bottom + verticalOffset - } + // Vertically, always render above unless no room, then render inside + let newTop = elBounds.top + scrollY - verticalOffset - height + if (newTop < deviceBounds.top - 50) { + newTop = deviceBounds.top - 50 + } + if (newTop < 0) { + newTop = 0 + } + const deviceBottom = deviceBounds.top + deviceBounds.height + if (newTop > deviceBottom - 44) { + newTop = deviceBottom - 44 + } - // 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 + scrollX + elBounds.width / 2 - let newLeft = elCenter - width / 2 + //If element is at the very top of the screen, put the bar below the element + if (elBounds.top < elBounds.height && elBounds.height < 80) { + newTop = elBounds.bottom + 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 + scrollX + elBounds.width / 2 + let newLeft = elCenter - width / 2 + if (newLeft < 0 || newLeft + width > innerWidth) { + newLeft = elBounds.left + scrollX - horizontalOffset if (newLeft < 0 || newLeft + width > innerWidth) { - newLeft = elBounds.left + scrollX - horizontalOffset + newLeft = elBounds.right + scrollX - width + horizontalOffset if (newLeft < 0 || newLeft + width > innerWidth) { - newLeft = elBounds.right + scrollX - width + horizontalOffset - if (newLeft < 0 || newLeft + width > innerWidth) { - newLeft = horizontalOffset - } + newLeft = horizontalOffset } } - - // Only update state when things changes to minimize renders - if (Math.round(newTop) !== Math.round(top)) { - top = newTop - } - if (Math.round(newLeft) !== Math.round(left)) { - left = newLeft - } - - measured = true } + + // Only update state when things changes to minimize renders + if (Math.round(newTop) !== Math.round(top)) { + top = newTop + } + if (Math.round(newLeft) !== Math.round(left)) { + left = newLeft + } + measured = true } const debouncedUpdate = Utils.domDebounce(updatePosition) - const observeComputedStyles = id => { - observer?.disconnect() - const node = document.getElementsByClassName(`${id}-dom`)[0]?.parentNode - if (node) { - observer = new MutationObserver(updatePosition) - observer.observe(node, { - attributes: true, - attributeFilter: ["style"], - childList: false, - subtree: false, - }) - } - } - onMount(() => { + deviceEl = document.getElementById("clip-root") debouncedUpdate() interval = setInterval(debouncedUpdate, 100) document.addEventListener("scroll", debouncedUpdate, true) @@ -172,7 +186,7 @@ onDestroy(() => { clearInterval(interval) document.removeEventListener("scroll", debouncedUpdate, true) - observer?.disconnect() + reset() }) @@ -190,7 +204,7 @@ icon="AlignLeft" title="Align left" active={gridHAlign === "start"} - {componentId} + componentId={id} />
{/if}