@sanity/visual-editing
Version:
[](https://npm-stat.com/charts.html?package=@sanity/visual-editing) [](https://
1 lines • 110 kB
Source Map (JSON)
{"version":3,"file":"SharedStateContext.cjs","sources":["../../src/react/useOptimistic.ts","../../../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm-browser/stringify.js","../../../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm-browser/rng.js","../../../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm-browser/native.js","../../../../node_modules/.pnpm/uuid@11.1.0/node_modules/uuid/dist/esm-browser/v4.js","../../src/util/geometry.ts","../../src/util/dragAndDrop.ts","../../src/util/elements.ts","../../src/util/stega.ts","../../src/util/findSanityNodes.ts","../../src/controller.ts","../../src/ui/shared-state/SharedStateContext.ts"],"sourcesContent":["import {getPublishedId} from '@sanity/client/csm'\nimport type {SanityDocument} from '@sanity/types'\nimport {startTransition, useEffect, useState} from 'react'\nimport {useEffectEvent} from 'use-effect-event'\nimport {isEmptyActor} from '../optimistic/context'\nimport type {OptimisticReducer, OptimisticReducerAction} from '../optimistic/types'\nimport {useOptimisticActor} from './useOptimisticActor'\n\nexport function useOptimistic<T, U = SanityDocument>(\n passthrough: T,\n reducer: OptimisticReducer<T, U> | Array<OptimisticReducer<T, U>>,\n): T {\n const [pristine, setPristine] = useState(true)\n const [optimistic, setOptimistic] = useState<T>(passthrough)\n const [lastEvent, setLastEvent] = useState<OptimisticReducerAction<U> | null>(null)\n const [lastPassthrough, setLastPassthrough] = useState<T>(passthrough)\n\n const actor = useOptimisticActor()\n\n /**\n * This action is used in two `useEffect` hooks, it needs access to the provided `reducer`,\n * but doesn't want to cause re-renders if `reducer` changes identity.\n * The `useEffectEvent` hook ensures that the `reducer` value is never stale when used, and doesn't trigger setup and teardown of\n * `useEffect` deps to make it happen.\n */\n const reduceStateFromAction = useEffectEvent(\n (action: OptimisticReducerAction<U>, prevState: T) => {\n const reducers = Array.isArray(reducer) ? reducer : [reducer]\n return reducers.reduce(\n (acc, reducer) =>\n reducer(acc, {\n document: action.document,\n id: getPublishedId(action.id),\n originalId: action.id,\n type: action.type,\n }),\n prevState,\n )\n },\n )\n\n /**\n * Records the last passthrough value when reducers ran in response to a rebased event.\n * This allows us to later know when reducers should run should the passthrough change.\n */\n const updateLastPassthrough = useEffectEvent(() => setLastPassthrough(passthrough))\n\n /**\n * Handle rebase events, which runs the provided reducers,\n * caches the event that was used to produce the new state,\n * and marks the state as non-pristine.\n */\n useEffect(() => {\n // If the actor hasn't been set yet, we don't need to subscribe to mutations\n if (isEmptyActor(actor)) {\n return\n }\n\n /**\n * The pristine event fires much too soon, so the temporary workaround is that we greatly delay firing `setPristine(true)`,\n * and instead relying on re-running reducers with the last event whenever the passthrough changes, to preserve the optimistic state,\n * until we hopefully have eventual consistency on the passthrough.\n */\n let pristineTimeout: ReturnType<typeof setTimeout>\n\n const rebasedSub = actor.on('rebased.local', (_event) => {\n const event = {\n // @todo You shall not cast\n document: _event.document as U,\n id: _event.id,\n originalId: getPublishedId(_event.id),\n // @todo This should eventually be emitted by the state machine\n type: 'mutate' as const,\n }\n setOptimistic((prevState) => reduceStateFromAction(event, prevState))\n setLastEvent(event)\n updateLastPassthrough()\n setPristine(false)\n\n clearTimeout(pristineTimeout)\n })\n const pristineSub = actor.on('pristine', () => {\n pristineTimeout = setTimeout(() => {\n // Marking it in a startTransition allows react to interrupt the resulting render, should a new rebase happen and we're back to dirty\n startTransition(() => setPristine(true))\n }, 15000)\n })\n return () => {\n rebasedSub.unsubscribe()\n pristineSub.unsubscribe()\n }\n }, [actor])\n\n /**\n * If the passthrough changes, and we are in a dirty state, we rerun the reducers with the new passthrough but the previous event.\n * Marking it in a transition allows react to interrupt this render should a new action happen, or should we be back in a pristine state.\n */\n useEffect(() => {\n if (pristine) {\n // if we are pristine, then we will passthrough anyway\n return undefined\n }\n if (!lastEvent) {\n // If we don't have a lastEvent when we are pristine, it's a fatal error\n throw new Error('No last event found when syncing passthrough')\n }\n if (lastPassthrough === passthrough) {\n // If the passthrough hasn't changed, then we don't need to rerun the reducers\n return undefined\n }\n\n // Marking it in a startTransition allows react to interrupt the resulting render, should a new rebase happen\n startTransition(() => {\n setOptimistic(reduceStateFromAction(lastEvent, passthrough))\n setLastPassthrough(passthrough)\n })\n }, [lastEvent, lastPassthrough, passthrough, pristine])\n\n return pristine ? passthrough : optimistic\n}\n","import validate from './validate.js';\nconst byteToHex = [];\nfor (let i = 0; i < 256; ++i) {\n byteToHex.push((i + 0x100).toString(16).slice(1));\n}\nexport function unsafeStringify(arr, offset = 0) {\n return (byteToHex[arr[offset + 0]] +\n byteToHex[arr[offset + 1]] +\n byteToHex[arr[offset + 2]] +\n byteToHex[arr[offset + 3]] +\n '-' +\n byteToHex[arr[offset + 4]] +\n byteToHex[arr[offset + 5]] +\n '-' +\n byteToHex[arr[offset + 6]] +\n byteToHex[arr[offset + 7]] +\n '-' +\n byteToHex[arr[offset + 8]] +\n byteToHex[arr[offset + 9]] +\n '-' +\n byteToHex[arr[offset + 10]] +\n byteToHex[arr[offset + 11]] +\n byteToHex[arr[offset + 12]] +\n byteToHex[arr[offset + 13]] +\n byteToHex[arr[offset + 14]] +\n byteToHex[arr[offset + 15]]).toLowerCase();\n}\nfunction stringify(arr, offset = 0) {\n const uuid = unsafeStringify(arr, offset);\n if (!validate(uuid)) {\n throw TypeError('Stringified UUID is invalid');\n }\n return uuid;\n}\nexport default stringify;\n","let getRandomValues;\nconst rnds8 = new Uint8Array(16);\nexport default function rng() {\n if (!getRandomValues) {\n if (typeof crypto === 'undefined' || !crypto.getRandomValues) {\n throw new Error('crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported');\n }\n getRandomValues = crypto.getRandomValues.bind(crypto);\n }\n return getRandomValues(rnds8);\n}\n","const randomUUID = typeof crypto !== 'undefined' && crypto.randomUUID && crypto.randomUUID.bind(crypto);\nexport default { randomUUID };\n","import native from './native.js';\nimport rng from './rng.js';\nimport { unsafeStringify } from './stringify.js';\nfunction v4(options, buf, offset) {\n if (native.randomUUID && !buf && !options) {\n return native.randomUUID();\n }\n options = options || {};\n const rnds = options.random ?? options.rng?.() ?? rng();\n if (rnds.length < 16) {\n throw new Error('Random bytes length must be >= 16');\n }\n rnds[6] = (rnds[6] & 0x0f) | 0x40;\n rnds[8] = (rnds[8] & 0x3f) | 0x80;\n if (buf) {\n offset = offset || 0;\n if (offset < 0 || offset + 16 > buf.length) {\n throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`);\n }\n for (let i = 0; i < 16; ++i) {\n buf[offset + i] = rnds[i];\n }\n return buf;\n }\n return unsafeStringify(rnds);\n}\nexport default v4;\n","import type {OverlayRect, Point2D, Ray2D} from '../types'\n\nexport function getRect(element: Element): OverlayRect {\n const domRect = element.getBoundingClientRect()\n\n const rect = {\n x: domRect.x + scrollX,\n y: domRect.y + scrollY,\n w: domRect.width,\n h: domRect.height,\n }\n\n return rect\n}\n\nexport function offsetRect(rect: OverlayRect, px: number, axis: 'x' | 'y'): OverlayRect {\n if (axis === 'x') {\n return {\n x: rect.x + px,\n y: rect.y,\n w: rect.w - 2 * px,\n h: rect.h,\n }\n } else {\n return {\n x: rect.x,\n y: rect.y + px,\n w: rect.w,\n h: rect.h - 2 * px,\n }\n }\n}\n\n// Ref http://paulbourke.net/geometry/pointlineplane/\nexport function rayIntersect(l1: Ray2D, l2: Ray2D): Point2D | false {\n const {x1, y1, x2, y2} = l1\n const {x1: x3, y1: y3, x2: x4, y2: y4} = l2\n\n // Check if none of the lines are of length 0\n if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {\n return false\n }\n\n const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)\n\n // Lines are parallel\n if (denominator === 0) {\n return false\n }\n\n const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator\n const ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator\n\n // is the intersection along the segments\n if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {\n return false\n }\n\n const x = x1 + ua * (x2 - x1)\n const y = y1 + ua * (y2 - y1)\n\n return {x, y}\n}\n\nexport function rectEqual(r1: OverlayRect, r2: OverlayRect): boolean {\n return r1.x === r2.x && r1.y === r2.y && r1.w === r2.w && r1.h === r2.h\n}\n\nexport function rayRectIntersections(line: Ray2D, rect: OverlayRect): Array<Point2D> | false {\n const rectLines: Array<Ray2D> = [\n {x1: rect.x, y1: rect.y, x2: rect.x + rect.w, y2: rect.y},\n {\n x1: rect.x + rect.w,\n y1: rect.y,\n x2: rect.x + rect.w,\n y2: rect.y + rect.h,\n },\n {\n x1: rect.x + rect.w,\n y1: rect.y + rect.h,\n x2: rect.x,\n y2: rect.y + rect.h,\n },\n {\n x1: rect.x,\n y1: rect.y + rect.h,\n x2: rect.x,\n y2: rect.y,\n },\n ]\n\n const intersections: Array<Point2D> = []\n\n for (let i = 0; i < rectLines.length; i++) {\n const intersection = rayIntersect(line, rectLines[i])\n\n if (intersection) {\n let isDuplicate = false\n\n for (let j = 0; j < intersections.length; j++) {\n if (intersections[j].x === intersection.x && intersections[j].y === intersection.y) {\n isDuplicate = true\n }\n }\n\n if (!isDuplicate) intersections.push(intersection)\n }\n }\n\n if (intersections.length === 0) {\n return false\n }\n\n return intersections.sort(\n (a, b) => pointDist(a, {x: line.x1, y: line.y1}) - pointDist(b, {x: line.x1, y: line.y1}),\n )\n}\nexport function pointDist(p1: Point2D, p2: Point2D): number {\n const a = p1.x - p2.x\n const b = p1.y - p2.y\n\n return Math.sqrt(a * a + b * b)\n}\n\nexport function pointInBounds(point: Point2D, bounds: OverlayRect): boolean {\n const withinX = point.x >= bounds.x && point.x <= bounds.x + bounds.w\n const withinY = point.y >= bounds.y && point.y <= bounds.y + bounds.h\n\n return withinX && withinY\n}\n\nexport function findClosestIntersection(\n ray: Ray2D,\n targets: OverlayRect[],\n flow: string,\n): OverlayRect | null {\n const rayOrigin = {\n x: ray.x1,\n y: ray.y1,\n }\n\n // Offset rects to ensure raycasting works when siblings touch\n if (\n targets.some((t) =>\n pointInBounds(\n rayOrigin,\n offsetRect(t, Math.min(t.w, t.h) / 10, flow === 'horizontal' ? 'x' : 'y'),\n ),\n )\n )\n return null\n let closestIntersection\n let closestRect\n\n for (const target of targets) {\n const intersections = rayRectIntersections(\n ray,\n offsetRect(target, Math.min(target.w, target.h) / 10, flow === 'horizontal' ? 'x' : 'y'),\n )\n if (intersections) {\n const firstIntersection = intersections[0]\n\n if (closestIntersection) {\n if (pointDist(rayOrigin, firstIntersection) < pointDist(rayOrigin, closestIntersection)) {\n closestIntersection = firstIntersection\n closestRect = target\n }\n } else {\n closestIntersection = firstIntersection\n closestRect = target\n }\n }\n }\n\n if (closestRect) return closestRect\n\n return null\n}\n\nexport function scaleRect(\n rect: OverlayRect,\n scale: number,\n origin: {x: number; y: number},\n): OverlayRect {\n const {x, y, w, h} = rect\n const {x: originX, y: originY} = origin\n\n const newX = originX + (x - originX) * scale\n const newY = originY + (y - originY) * scale\n\n const newWidth = w * scale\n const newHeight = h * scale\n\n return {\n x: newX,\n y: newY,\n w: newWidth,\n h: newHeight,\n }\n}\n\nexport function getRectGroupXExtent(rects: OverlayRect[]): {\n min: number\n max: number\n width: number\n} {\n const minGroupX = Math.max(0, Math.min(...rects.map((r) => r.x)))\n const maxGroupX = Math.min(document.body.offsetWidth, Math.max(...rects.map((r) => r.x + r.w)))\n\n return {\n min: minGroupX,\n max: maxGroupX,\n width: maxGroupX - minGroupX,\n }\n}\n\nexport function getRectGroupYExtent(rects: OverlayRect[]): {\n min: number\n max: number\n height: number\n} {\n const minGroupY = Math.max(0, Math.min(...rects.map((r) => r.y)))\n const maxGroupY = Math.min(document.body.scrollHeight, Math.max(...rects.map((r) => r.y + r.h)))\n\n return {\n min: minGroupY,\n max: maxGroupY,\n height: maxGroupY - minGroupY,\n }\n}\n","import type {\n DragInsertPosition,\n DragInsertPositionRects,\n ElementNode,\n OverlayElement,\n OverlayEventHandler,\n OverlayRect,\n Point2D,\n SanityNode,\n} from '../types'\nimport {\n findClosestIntersection,\n getRect,\n getRectGroupXExtent,\n getRectGroupYExtent,\n pointDist,\n rectEqual,\n scaleRect,\n} from './geometry'\n\nfunction calcTargetFlow(targets: OverlayRect[]) {\n if (\n targets.some((t1) => {\n const others = targets.filter((t2) => !rectEqual(t1, t2))\n\n return others.some((t2) => {\n return t1.y === t2.y\n })\n })\n ) {\n return 'horizontal'\n } else {\n return 'vertical'\n }\n}\n\nfunction calcInsertPosition(origin: Point2D, targets: OverlayRect[], flow: string) {\n if (flow === 'horizontal') {\n const rayLeft = {\n x1: origin.x,\n y1: origin.y,\n x2: origin.x - 100_000_000,\n y2: origin.y,\n }\n\n const rayRight = {\n x1: origin.x,\n y1: origin.y,\n x2: origin.x + 100_000_000,\n y2: origin.y,\n }\n\n return {\n left: findClosestIntersection(rayLeft, targets, flow),\n right: findClosestIntersection(rayRight, targets, flow),\n }\n } else {\n const rayTop = {\n x1: origin.x,\n y1: origin.y,\n x2: origin.x,\n y2: origin.y - 100_000_000,\n }\n\n const rayBottom = {\n x1: origin.x,\n y1: origin.y,\n x2: origin.x,\n y2: origin.y + 100_000_000,\n }\n\n return {\n top: findClosestIntersection(rayTop, targets, flow),\n bottom: findClosestIntersection(rayBottom, targets, flow),\n }\n }\n}\n\nfunction findRectSanityData(rect: OverlayRect, overlayGroup: OverlayElement[]) {\n return overlayGroup.find((e) => rectEqual(getRect(e.elements.element), rect))\n ?.sanity as SanityNode\n}\n\nfunction resolveInsertPosition(\n overlayGroup: OverlayElement[],\n insertPosition: DragInsertPositionRects,\n flow: string,\n): DragInsertPosition {\n if (Object.values(insertPosition).every((v) => v === null)) return null\n\n if (flow === 'horizontal') {\n return {\n left: insertPosition.left\n ? {\n rect: insertPosition.left,\n sanity: findRectSanityData(insertPosition.left, overlayGroup),\n }\n : null,\n right: insertPosition.right\n ? {\n rect: insertPosition.right,\n sanity: findRectSanityData(insertPosition.right, overlayGroup),\n }\n : null,\n }\n } else {\n return {\n top: insertPosition.top\n ? {\n rect: insertPosition.top,\n sanity: findRectSanityData(insertPosition.top, overlayGroup),\n }\n : null,\n bottom: insertPosition.bottom\n ? {\n rect: insertPosition.bottom,\n sanity: findRectSanityData(insertPosition.bottom, overlayGroup),\n }\n : null,\n }\n }\n}\n\nfunction calcMousePos(e: MouseEvent) {\n const bodyBounds = document.body.getBoundingClientRect()\n\n return {\n x: Math.max(bodyBounds.x, Math.min(e.clientX, bodyBounds.x + bodyBounds.width)),\n y: e.clientY + window.scrollY,\n }\n}\n\nfunction calcMousePosInverseTransform(mousePos: Point2D) {\n const body = document.body\n const computedStyle = window.getComputedStyle(body)\n const transform = computedStyle.transform\n\n if (transform === 'none') {\n return {\n x: mousePos.x,\n y: mousePos.y,\n }\n }\n\n const matrix = new DOMMatrix(transform)\n const inverseMatrix = matrix.inverse()\n\n const point = new DOMPoint(mousePos.x, mousePos.y)\n const transformedPoint = point.matrixTransform(inverseMatrix)\n\n return {\n x: transformedPoint.x,\n y: transformedPoint.y,\n }\n}\n\nfunction buildPreviewSkeleton(mousePos: Point2D, element: ElementNode, scaleFactor: number) {\n const bounds = getRect(element)\n\n const children = [\n ...element.querySelectorAll(':where(h1, h2, h3, h4, p, a, img, span, button):not(:has(*))'),\n ]\n\n if (mousePos.x <= bounds.x) mousePos.x = bounds.x\n if (mousePos.x >= bounds.x + bounds.w) mousePos.x = bounds.x + bounds.w\n\n if (mousePos.y >= bounds.y + bounds.h) mousePos.y = bounds.y + bounds.h\n if (mousePos.y <= bounds.y) mousePos.y = bounds.y\n\n const childRects = children.map((child: Element) => {\n // offset to account for stroke in rendered rects\n const rect = scaleRect(getRect(child), scaleFactor, {\n x: bounds.x,\n y: bounds.y,\n })\n\n return {\n x: rect.x - bounds.x,\n y: rect.y - bounds.y,\n w: rect.w,\n h: rect.h,\n tagName: child.tagName,\n }\n })\n\n return {\n offsetX: (bounds.x - mousePos.x) * scaleFactor,\n offsetY: (bounds.y - mousePos.y) * scaleFactor,\n w: bounds.w * scaleFactor,\n h: bounds.h * scaleFactor,\n maxWidth: bounds.w * scaleFactor * 0.75,\n childRects,\n }\n}\n\nconst minDragDelta = 4\n\nasync function applyMinimapWrapperTransform(\n target: HTMLElement,\n scaleFactor: number,\n minYScaled: number,\n handler: OverlayEventHandler,\n rectUpdateFrequency: number,\n): Promise<void> {\n return new Promise((resolve) => {\n target.addEventListener(\n 'transitionend',\n () => {\n setTimeout(() => {\n handler({\n type: 'overlay/dragEndMinimapTransition',\n })\n }, rectUpdateFrequency * 2)\n\n resolve()\n },\n {once: true},\n )\n\n handler({\n type: 'overlay/dragStartMinimapTransition',\n })\n\n handler({\n type: 'overlay/dragToggleMinimap',\n display: true,\n })\n\n document.body.style.overflow = 'hidden'\n document.body.style.height = '100%'\n document.documentElement.style.overflow = 'initial'\n document.documentElement.style.height = '100%'\n\n // ensure overflow hidden has applied and scrolling stopped before applying transform, prevent minor y-position transform issues\n setTimeout(() => {\n target.style.transformOrigin = '50% 0px'\n target.style.transition = 'transform 150ms ease'\n target.style.transform = `translate3d(0px, ${-minYScaled + scrollY}px, 0px) scale(${scaleFactor})`\n }, 25)\n })\n}\n\nfunction calcMinimapTransformValues(rects: OverlayRect[], groupHeightOverride: number | null) {\n let groupHeight = groupHeightOverride || getRectGroupYExtent(rects).height\n\n const padding = 100 // px\n\n groupHeight += padding * 2\n\n const scaleFactor = groupHeight > window.innerHeight ? window.innerHeight / groupHeight : 1\n const scaledRects = rects.map((r) => scaleRect(r, scaleFactor, {x: window.innerWidth / 2, y: 0}))\n\n const {min: minYScaled} = getRectGroupYExtent(scaledRects)\n\n return {\n scaleFactor,\n minYScaled: minYScaled - padding * scaleFactor,\n }\n}\nfunction calcGroupBoundsPreview(rects: OverlayRect[]) {\n const groupBoundsX = getRectGroupXExtent(rects)\n const groupBoundsY = getRectGroupYExtent(rects)\n\n const offsetDist = 8\n\n const canOffsetX =\n groupBoundsX.min > offsetDist &&\n groupBoundsX.min + groupBoundsX.width <= window.innerWidth - offsetDist\n const canOffsetY =\n groupBoundsY.min > offsetDist &&\n groupBoundsY.min + groupBoundsY.height <= document.body.scrollHeight - offsetDist\n const canOffset = canOffsetX && canOffsetY\n\n const groupRect = {\n x: canOffset ? groupBoundsX.min - offsetDist : groupBoundsX.min,\n y: canOffset ? groupBoundsY.min - offsetDist : groupBoundsY.min,\n w: canOffset ? groupBoundsX.width + offsetDist * 2 : groupBoundsX.width,\n h: canOffset ? groupBoundsY.height + offsetDist * 2 : groupBoundsY.height,\n }\n\n return groupRect\n}\n\nasync function resetMinimapWrapperTransform(\n endYOrigin: number,\n target: HTMLElement,\n prescaleHeight: number,\n handler: OverlayEventHandler,\n rectUpdateFrequency: number,\n previousRootStyleValues: PreviousRootStyleValues | null,\n): Promise<void> {\n return new Promise((resolve) => {\n const computedStyle = window.getComputedStyle(target)\n const transform = computedStyle.transform\n\n const matrix = new DOMMatrix(transform)\n\n const scale = matrix.a\n\n if (scale === 1) return\n\n const maxScroll = prescaleHeight - window.innerHeight\n const prevScrollY = scrollY\n\n endYOrigin -= window.innerHeight / 2\n\n if (endYOrigin < 0) endYOrigin = 0\n\n target.addEventListener(\n 'transitionend',\n () => {\n target.style.transition = `none`\n target.style.transform = `none`\n\n scrollTo({\n top: endYOrigin,\n behavior: 'instant',\n })\n\n setTimeout(() => {\n handler({\n type: 'overlay/dragEndMinimapTransition',\n })\n\n handler({\n type: 'overlay/dragToggleMinimap',\n display: false,\n })\n }, rectUpdateFrequency * 2)\n\n resolve()\n },\n {once: true},\n )\n\n handler({\n type: 'overlay/dragStartMinimapTransition',\n })\n\n target.style.transform = `translateY(${Math.max(prevScrollY - endYOrigin, -maxScroll + prevScrollY)}px) scale(${1})`\n\n if (!previousRootStyleValues) return\n\n document.body.style.overflow = previousRootStyleValues.body.overflow\n document.body.style.height = previousRootStyleValues.body.height\n document.documentElement.style.overflow = previousRootStyleValues.documentElement.overflow\n document.documentElement.style.height = previousRootStyleValues.documentElement.height\n })\n}\n\ninterface PreviousRootStyleValues {\n body: {\n overflow: string\n height: string\n }\n documentElement: {\n overflow: string\n height: string\n }\n}\n\ninterface HandleOverlayDragOpts {\n mouseEvent: MouseEvent\n element: ElementNode\n overlayGroup: OverlayElement[]\n handler: OverlayEventHandler\n target: SanityNode\n onSequenceStart: () => void\n onSequenceEnd: () => void\n}\n\nlet minimapScaleApplied = false\n\nlet mousePosInverseTransform = {x: 0, y: 0}\nlet mousePos = {x: 0, y: 0}\n\nlet prescaleHeight = typeof document === 'undefined' ? 0 : document.documentElement.scrollHeight\n\nlet previousRootStyleValues: PreviousRootStyleValues | null = null\n\nexport function handleOverlayDrag(opts: HandleOverlayDragOpts): void {\n const {mouseEvent, element, overlayGroup, handler, target, onSequenceStart, onSequenceEnd} = opts\n\n // do not trigger drag sequence on anything other than \"main\" (0) click, ignore right click, etc\n if (mouseEvent.button !== 0) return\n\n // ensure keyboard events fire within frame context\n window.focus()\n\n const rectUpdateFrequency = 150\n let rects = overlayGroup.map((e) => getRect(e.elements.element))\n\n const flow = (element.getAttribute('data-sanity-drag-flow') || calcTargetFlow(rects)) as\n | 'horizontal'\n | 'vertical'\n\n const dragGroup = element.getAttribute('data-sanity-drag-group')\n\n const disableMinimap = !!element.getAttribute('data-sanity-drag-minimap-disable')\n\n const preventInsertDefault = !!element.getAttribute('data-sanity-drag-prevent-default')\n\n const documentHeightOverride = element.getAttribute('data-unstable_sanity-drag-document-height')\n const groupHeightOverride = element.getAttribute('data-unstable_sanity-drag-group-height')\n\n let insertPosition: DragInsertPositionRects | null = null\n\n const initialMousePos = calcMousePos(mouseEvent)\n\n const scaleTarget = document.body\n\n const {minYScaled, scaleFactor} = calcMinimapTransformValues(\n rects,\n groupHeightOverride ? ~~groupHeightOverride : null,\n )\n\n let sequenceStarted = false\n let minimapPromptShown = false\n\n let mousedown = true\n\n if (!minimapScaleApplied) {\n previousRootStyleValues = {\n body: {\n overflow: window.getComputedStyle(document.body).overflow,\n height: window.getComputedStyle(document.body).height,\n },\n documentElement: {\n overflow: window.getComputedStyle(document.documentElement).overflow,\n height: window.getComputedStyle(document.documentElement).height,\n },\n }\n\n prescaleHeight = documentHeightOverride\n ? ~~documentHeightOverride\n : document.documentElement.scrollHeight\n }\n\n const rectsInterval = setInterval(() => {\n rects = overlayGroup.map((e) => getRect(e.elements.element))\n }, rectUpdateFrequency)\n\n const applyMinimap = (): void => {\n if (scaleFactor >= 1) return\n\n const skeleton = buildPreviewSkeleton(mousePos, element, scaleFactor)\n\n handler({\n type: 'overlay/dragUpdateSkeleton',\n skeleton,\n })\n\n handler({\n type: 'overlay/dragToggleMinimapPrompt',\n display: false,\n })\n\n applyMinimapWrapperTransform(\n scaleTarget,\n scaleFactor,\n minYScaled,\n handler,\n rectUpdateFrequency,\n ).then(() => {\n setTimeout(() => {\n handler({\n type: 'overlay/dragUpdateGroupRect',\n groupRect: calcGroupBoundsPreview(rects),\n })\n }, rectUpdateFrequency * 2)\n })\n }\n\n const handleScroll = (e: WheelEvent) => {\n if (\n Math.abs(e.deltaY) >= 10 &&\n scaleFactor < 1 &&\n !minimapScaleApplied &&\n !minimapPromptShown &&\n !disableMinimap &&\n mousedown\n ) {\n handler({\n type: 'overlay/dragToggleMinimapPrompt',\n display: true,\n })\n\n minimapPromptShown = true\n }\n\n if (e.shiftKey && !minimapScaleApplied && !disableMinimap) {\n window.dispatchEvent(new CustomEvent('unstable_sanity/dragApplyMinimap'))\n\n minimapScaleApplied = true\n\n setTimeout(() => {\n applyMinimap()\n }, 50)\n }\n }\n\n const handleMouseMove = (e: MouseEvent): void => {\n e.preventDefault()\n\n mousePos = calcMousePos(e)\n mousePosInverseTransform = calcMousePosInverseTransform(mousePos)\n\n if (Math.abs(pointDist(mousePos, initialMousePos)) < minDragDelta) return\n\n if (!sequenceStarted) {\n const groupRect = calcGroupBoundsPreview(rects)\n\n const skeleton = buildPreviewSkeleton(mousePos, element, 1)\n\n handler({\n type: 'overlay/dragStart',\n flow,\n })\n\n handler({\n type: 'overlay/dragUpdateSkeleton',\n skeleton,\n })\n\n handler({\n type: 'overlay/dragUpdateGroupRect',\n groupRect,\n })\n\n sequenceStarted = true\n onSequenceStart()\n }\n\n handler({\n type: 'overlay/dragUpdateCursorPosition',\n x: mousePos.x,\n y: mousePos.y,\n })\n\n if (e.shiftKey && !minimapScaleApplied && !disableMinimap) {\n window.dispatchEvent(new CustomEvent('unstable_sanity/dragApplyMinimap'))\n\n minimapScaleApplied = true\n\n setTimeout(() => {\n applyMinimap()\n }, 50)\n }\n\n const newInsertPosition = calcInsertPosition(mousePos, rects, flow)\n\n if (JSON.stringify(insertPosition) !== JSON.stringify(newInsertPosition)) {\n insertPosition = newInsertPosition\n\n handler({\n type: 'overlay/dragUpdateInsertPosition',\n insertPosition: resolveInsertPosition(overlayGroup, insertPosition, flow),\n })\n }\n }\n\n const handleMouseUp = (): void => {\n mousedown = false\n\n handler({\n type: 'overlay/dragEnd',\n target,\n insertPosition: insertPosition\n ? resolveInsertPosition(overlayGroup, insertPosition, flow)\n : null,\n dragGroup,\n flow,\n preventInsertDefault,\n })\n\n if (minimapPromptShown) {\n handler({\n type: 'overlay/dragToggleMinimapPrompt',\n display: false,\n })\n }\n\n if (!minimapScaleApplied) {\n clearInterval(rectsInterval)\n onSequenceEnd()\n\n removeFrameListeners()\n removeKeyListeners()\n }\n\n removeMouseListeners()\n }\n\n const handleKeyup = (e: KeyboardEvent) => {\n if (e.key === 'Shift' && minimapScaleApplied) {\n minimapScaleApplied = false\n\n const skeleton = buildPreviewSkeleton(mousePos, element, 1 / scaleFactor)\n\n handler({\n type: 'overlay/dragUpdateSkeleton',\n skeleton,\n })\n\n window.dispatchEvent(new CustomEvent('unstable_sanity/dragResetMinimap'))\n\n setTimeout(() => {\n resetMinimapWrapperTransform(\n mousePosInverseTransform.y,\n scaleTarget,\n prescaleHeight,\n handler,\n rectUpdateFrequency,\n previousRootStyleValues,\n )\n }, 50)\n\n handler({\n type: 'overlay/dragUpdateGroupRect',\n groupRect: null,\n })\n\n // cleanup keyup after drag sequence is complete\n if (!mousedown) {\n clearInterval(rectsInterval)\n\n removeMouseListeners()\n removeFrameListeners()\n removeKeyListeners()\n\n onSequenceEnd()\n }\n }\n }\n\n const handleBlur = () => {\n handler({\n type: 'overlay/dragUpdateGroupRect',\n groupRect: null,\n })\n\n window.dispatchEvent(new CustomEvent('unstable_sanity/dragResetMinimap'))\n\n setTimeout(() => {\n resetMinimapWrapperTransform(\n mousePosInverseTransform.y,\n scaleTarget,\n prescaleHeight,\n handler,\n rectUpdateFrequency,\n previousRootStyleValues,\n ).then(() => {\n minimapScaleApplied = false\n })\n }, 50)\n\n clearInterval(rectsInterval)\n\n removeMouseListeners()\n removeFrameListeners()\n removeKeyListeners()\n\n onSequenceEnd()\n }\n\n const removeMouseListeners = () => {\n window.removeEventListener('mousemove', handleMouseMove)\n window.removeEventListener('wheel', handleScroll)\n window.removeEventListener('mouseup', handleMouseUp)\n }\n\n const removeKeyListeners = () => {\n window.removeEventListener('keyup', handleKeyup)\n }\n\n const removeFrameListeners = () => {\n window.removeEventListener('blur', handleBlur)\n }\n\n window.addEventListener('blur', handleBlur)\n window.addEventListener('keyup', handleKeyup)\n window.addEventListener('wheel', handleScroll)\n window.addEventListener('mousemove', handleMouseMove)\n window.addEventListener('mouseup', handleMouseUp)\n}\n","import type {ElementNode} from '../types'\n\nexport const isElementNode = (target: EventTarget | null): target is ElementNode => {\n return target instanceof HTMLElement || target instanceof SVGElement\n}\n\nexport function findNonInlineElement(element: ElementNode): ElementNode | null {\n const {display} = window.getComputedStyle(element)\n\n if (display !== 'inline') return element\n\n const parent = element.parentElement\n\n if (!parent) return null\n\n return findNonInlineElement(parent)\n}\n\nexport const findOverlayElement = (\n el: EventTarget | ElementNode | null | undefined,\n): ElementNode | null => {\n if (!el || !isElementNode(el)) {\n return null\n }\n\n if (el.dataset?.['sanityOverlayElement']) {\n return el\n }\n\n return findOverlayElement(el.parentElement)\n}\n","import type {SanityStegaNode} from '@sanity/presentation-comlink'\nimport {vercelStegaDecode} from '@vercel/stega'\nimport {VERCEL_STEGA_REGEX} from '../constants'\n\n/**\n * JavaScript regexps are stateful. Have to reset lastIndex between runs to ensure consistent behaviour for the same string\n * @param input\n */\nexport function testVercelStegaRegex(input: string): boolean {\n VERCEL_STEGA_REGEX.lastIndex = 0\n return VERCEL_STEGA_REGEX.test(input)\n}\n\nfunction decodeStega(str: string, isAltText = false): SanityStegaNode | null {\n try {\n const decoded = vercelStegaDecode<SanityStegaNode>(str)\n if (!decoded || decoded.origin !== 'sanity.io') {\n return null\n }\n if (isAltText) {\n decoded.href = decoded.href?.replace('.alt', '')\n }\n return decoded\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('Failed to decode stega for string: ', str, 'with the original error: ', err)\n return null\n }\n}\n\nexport function testAndDecodeStega(str: string, isAltText = false): SanityStegaNode | null {\n if (testVercelStegaRegex(str)) {\n return decodeStega(str, isAltText)\n }\n return null\n}\n","import {decodeSanityNodeData} from '@sanity/visual-editing-csm'\nimport type {\n ElementNode,\n OverlayElement,\n ResolvedElement,\n ResolvedElementReason,\n ResolvedElementTarget,\n ResolvingElement,\n SanityNode,\n SanityStegaNode,\n} from '../types'\nimport {findNonInlineElement} from './elements'\nimport {testAndDecodeStega, testVercelStegaRegex} from './stega'\n\nconst isElementNode = (node: ChildNode): node is ElementNode => node.nodeType === Node.ELEMENT_NODE\n\nconst isImgElement = (el: ElementNode): el is HTMLImageElement => el.tagName === 'IMG'\n\nconst isTimeElement = (el: ElementNode): el is HTMLTimeElement => el.tagName === 'TIME'\n\nconst isSvgRootElement = (el: ElementNode): el is SVGSVGElement =>\n el.tagName.toUpperCase() === 'SVG'\n\nexport function isSanityNode(node: SanityNode | SanityStegaNode): node is SanityNode {\n return 'path' in node\n}\n\n/**\n * Finds commonality between two document paths strings\n * @param first First path to compare\n * @param second Second path to compare\n * @returns A common path\n */\nexport function findCommonPath(first: string, second: string): string {\n let firstParts = first.split('.')\n let secondParts = second.split('.')\n const maxLength = Math.min(firstParts.length, secondParts.length)\n firstParts = firstParts.slice(0, maxLength).reverse()\n secondParts = secondParts.slice(0, maxLength).reverse()\n\n return firstParts\n .reduce((parts, part, i) => (part === secondParts[i] ? [...parts, part] : []), [] as string[])\n .reverse()\n .join('.')\n}\n\n/**\n * Returns common Sanity node data from multiple nodes\n * If document paths are present, tries to resolve a common path\n * @param nodes An array of Sanity nodes\n * @returns A single sanity node or undefined\n * @internal\n */\nexport function findCommonSanityData(\n nodes: (SanityNode | SanityStegaNode)[],\n): SanityNode | SanityStegaNode | undefined {\n // If there are no nodes, or inconsistent node types\n if (!nodes.length || !nodes.map((n) => isSanityNode(n)).every((n, _i, arr) => n === arr[0])) {\n return undefined\n }\n // If legacy nodes, return first match (no common pathfinding)\n if (!isSanityNode(nodes[0])) return nodes[0]\n\n const sanityNodes = nodes.filter(isSanityNode)\n let common: SanityNode | undefined = nodes[0]\n\n const consistentValueKeys: Array<keyof SanityNode> = [\n 'projectId',\n 'dataset',\n 'id',\n 'baseUrl',\n 'workspace',\n 'tool',\n ]\n for (let i = 1; i < sanityNodes.length; i++) {\n const node = sanityNodes[i]\n if (consistentValueKeys.some((key) => node[key] !== common?.[key])) {\n common = undefined\n break\n }\n\n common = {...common, path: findCommonPath(common.path, node.path)}\n }\n\n return common\n}\n\n/**\n * Finds nodes containing sanity specific data\n * @param el - A parent element to traverse\n * @returns An array of overlay targets\n * @internal\n */\nexport function findSanityNodes(\n el: ElementNode | ChildNode | {childNodes: Array<ElementNode>},\n): ResolvedElement[] {\n const mainResults: Omit<ResolvedElement, 'commonSanity'>[] = []\n\n function createResolvedElement(\n element: ElementNode,\n data: SanityStegaNode | string,\n reason: ResolvedElementReason,\n preventGrouping?: boolean,\n ): ResolvingElement | undefined {\n const sanity = decodeSanityNodeData(data)\n\n if (!sanity) {\n return\n }\n\n // resize observer does not fire for non-replaced inline elements https://drafts.csswg.org/resize-observer/#intro\n const measureElement = findNonInlineElement(element)\n if (!measureElement) {\n return\n }\n\n return {\n elements: {\n element,\n measureElement,\n },\n sanity,\n reason,\n preventGrouping,\n }\n }\n\n function resolveNode(node: ChildNode): ResolvingElement | undefined {\n const {nodeType, parentElement, textContent} = node\n // If an edit target is found, find common paths\n if (isElementNode(node) && node.dataset?.['sanityEditTarget'] !== undefined) {\n const nodesInTarget = findSanityNodes(node)\n const commonData = findCommonSanityData(\n nodesInTarget\n .map((node) => (node.type === 'element' ? node.commonSanity : undefined))\n .filter((n) => n !== undefined),\n )\n if (commonData) {\n return {\n reason: 'edit-target',\n elements: {\n element: node,\n measureElement: node,\n },\n sanity: commonData,\n }\n }\n\n // Check non-empty, child-only text nodes for stega strings\n } else if (nodeType === Node.TEXT_NODE && parentElement && textContent) {\n const data = testAndDecodeStega(textContent)\n if (!data) return\n return createResolvedElement(parentElement, data, 'stega-text', true)\n }\n // Check element nodes for data attributes, alt tags, etc\n else if (isElementNode(node)) {\n // Do not traverse script tags\n // Do not traverse the visual editing overlay\n if (node.tagName === 'SCRIPT' || node.tagName === 'SANITY-VISUAL-EDITING') {\n return\n }\n\n // Prefer elements with explicit data attributes\n if (node.dataset?.['sanity']) {\n return createResolvedElement(\n node,\n node.dataset['sanity'],\n 'data-attribute',\n Boolean(node.textContent && testVercelStegaRegex(node.textContent)),\n )\n }\n // Look for legacy sanity data attributes\n else if (node.dataset?.['sanityEditInfo']) {\n return createResolvedElement(\n node,\n node.dataset['sanityEditInfo'],\n 'data-attribute',\n Boolean(node.textContent && testVercelStegaRegex(node.textContent)),\n )\n } else if (isImgElement(node)) {\n const data = testAndDecodeStega(node.alt, true)\n if (!data) return\n return createResolvedElement(node, data, 'stega-attribute')\n } else if (isTimeElement(node)) {\n const data = testAndDecodeStega(node.dateTime, true)\n if (!data) return\n return createResolvedElement(node, data, 'stega-attribute')\n } else if (isSvgRootElement(node)) {\n if (!node.ariaLabel) return\n const data = testAndDecodeStega(node.ariaLabel, true)\n if (!data) return\n return createResolvedElement(node, data, 'stega-attribute')\n }\n }\n return\n }\n\n function processNode(\n node: ChildNode,\n _parentGroup: Omit<ResolvedElement, 'commonSanity'> | undefined,\n ): void {\n const resolvedElement = resolveNode(node)\n\n let parentGroup: Omit<ResolvedElement, 'commonSanity'> | undefined = _parentGroup\n\n if (isElementNode(node) && node.dataset?.['sanityEditGroup'] !== undefined) {\n parentGroup = {\n type: 'group',\n elements: {\n element: node,\n measureElement: node,\n },\n targets: [],\n }\n mainResults.push(parentGroup)\n }\n\n if (resolvedElement) {\n const target: ResolvedElementTarget = {\n elements: resolvedElement.elements,\n sanity: resolvedElement.sanity,\n reason: resolvedElement.reason,\n }\n if (parentGroup && !resolvedElement.preventGrouping) {\n parentGroup.targets.push(target)\n } else {\n mainResults.push({\n elements: resolvedElement.elements,\n type: 'element',\n targets: [target],\n })\n }\n }\n\n const shouldTraverseNode =\n isElementNode(node) &&\n !isImgElement(node) &&\n !(node.tagName === 'SCRIPT' || node.tagName === 'SANITY-VISUAL-EDITING')\n\n if (shouldTraverseNode) {\n for (const childNode of node.childNodes) {\n processNode(childNode, parentGroup)\n }\n }\n }\n\n if (el) {\n for (const node of el.childNodes) {\n processNode(node, undefined)\n }\n }\n\n return mainResults\n .map((node) => {\n if (node.targets.length === 0 && node.type === 'group') {\n // Always return empty groups so the controller can unregister them\n return {\n ...node,\n commonSanity: undefined,\n }\n }\n\n const commonSanity =\n node.targets.length === 1\n ? node.targets[0].sanity\n : findCommonSanityData(\n node.targets.map(({sanity}) => sanity).filter((n) => n !== undefined),\n ) || node.targets[0].sanity\n\n if (!commonSanity) return null\n\n return {\n ...node,\n commonSanity,\n }\n })\n .filter((node) => node !== null)\n}\n\nexport function isSanityArrayPath(path: string): boolean {\n const lastDotIndex = path.lastIndexOf('.')\n const lastPathItem = path.substring(lastDotIndex, path.length)\n\n return lastPathItem.includes('[')\n}\n\nexport function getSanityNodeArrayPath(path: string): string | null {\n if (!isSanityArrayPath(path)) return null\n\n const split = path.split('.')\n\n split[split.length - 1] = split[split.length - 1].replace(/\\[.*?\\]/g, '[]')\n\n return split.join('.')\n}\n\nexport function sanityNodesExistInSameArray(\n sanityNode1: SanityNode,\n sanityNode2: SanityNode,\n): boolean {\n if (!isSanityArrayPath(sanityNode1.path) || !isSanityArrayPath(sanityNode2.path)) return false\n\n return getSanityNodeArrayPath(sanityNode1.path) === getSanityNodeArrayPath(sanityNode2.path)\n}\n\nexport function resolveDragAndDropGroup(\n element: ElementNode,\n sanity: SanityNode | SanityStegaNode,\n elementSet: Set<ElementNode>,\n elementsMap: WeakMap<ElementNode, OverlayElement>,\n): null | OverlayElement[] {\n if (!element.getAttribute('data-sanity')) return null\n\n if (element.getAttribute('data-sanity-drag-disable')) return null\n\n if (!sanity || !isSanityNode(sanity) || !isSanityArrayPath(sanity.path)) return null\n\n const targetDragGroup = element.getAttribute('data-sanity-drag-group')\n\n const group = [...elementSet].reduce<OverlayElement[]>((acc, el) => {\n const elData = elementsMap.get(el)\n const elDragDisabled = el.getAttribute('data-sanity-drag-disable')\n const elDragGroup = el.getAttribute('data-sanity-drag-group')\n const elHasSanityAttribution = el.getAttribute('data-sanity') !== null\n\n const sharedDragGroup = targetDragGroup !== null ? targetDragGroup === elDragGroup : true\n\n if (\n elData?.sanity &&\n !elDragDisabled &&\n isSanityNode(elData.sanity) &&\n sanityNodesExistInSameArray(sanity, elData.sanity) &&\n sharedDragGroup &&\n elHasSanityAttribution\n ) {\n acc.push(elData)\n }\n\n return acc\n }, [])\n\n if (group.length <= 1) return null\n\n return group\n}\n","import {v4 as uuid} from 'uuid'\nimport type {\n ElementNode,\n EventHandlers,\n OverlayController,\n OverlayElement,\n OverlayOptions,\n ResolvedElement,\n} from './types'\nimport {handleOverlayDrag} from './util/dragAndDrop'\nimport {findOverlayElement, isElementNode} from './util/elements'\nimport {\n findSanityNodes,\n isSanityArrayPath,\n isSanityNode,\n resolveDragAndDropGroup,\n} from './util/findSanityNodes'\nimport {getRect} from './util/geometry'\n\n/**\n * Creates a controller which dispatches overlay related events\n *\n * @param handler - Dispatched event handler\n * @param overlayElement - Parent element containing rendered overlay elements\n * @public\n */\nexport function createOverlayController({\n handler,\n overlayElement,\n inFrame,\n inPopUp,\n optimisticActorReady,\n}: OverlayOptions): OverlayController {\n let activated = false\n // Map for getting element by ID\n const elementIdMap = new Map<string, ElementNode>()\n // WeakMap for getting data by element\n const elementsMap = new WeakMap<ElementNode, OverlayElement>()\n // Set for iterating over elements\n const elementSet = new Set<ElementNode>()\n // Weakmap keyed by measureElement to find associated element\n const measureElements = new WeakMap<ElementNode, ElementNode>()\n // Weakmap for storing user set cursor styles per element\n const cursorMap = new WeakMap<ElementNode, string | undefined>()\n\n let ro: ResizeObserver\n let io: IntersectionObserver | undefined\n let mo: MutationObserver\n\n let activeDragSequence = false\n\n // The `hoverStack` is used as a container for tracking which elements are hovered at any time.\n // The browser supports hovering multiple nested elements simultanously, but we only want to\n // highlight the \"outer most\" element.\n //\n // This is how it works:\n // - Whenever the mouse enters an element, we add it to the stack.\n // - Whenever the mouse leaves an element, we remove it from the stack.\n //\n // When we want to know which element is currently hovered, we take the element at the top of the\n // stack. Since JavaScript does not have a Stack type, we use an array and take the last element.\n let hoverStack: Array<ElementNode> = []\n const getHoveredElement = () => hoverStack[hoverStack.length - 1] as ElementNode | undefined\n\n function addEventHandlers(el: ElementNode, handlers: EventHandlers) {\n el.addEventListener('click', handlers.click as EventListener, {\n capture: true,\n })\n el.addEventListener('contextmenu', handlers.contextmenu as EventListener, {\n capture: true,\n })\n // We listen for the initial mousemove event, in case the overlay is enabled whilst the cursor is already over an element\n // mouseenter and mouseleave listeners are attached within this handler\n el.addEventListener('mousemove', handlers.mousemove as EventListener, {\n once: true,\n capture: true,\n })\n // Listen for mousedown in case we need to prevent default behavior\n el.addEventListener('mousedown', handlers.mousedown as EventListener, {\n capture: true,\n })\n }\n\n function removeEventHandlers(el: ElementNode, handlers: EventHandlers) {\n el.removeEventListener('click', handlers.click as EventListener, {\n capture: true,\n })\n el.removeEventListener('contextmenu', handlers.contextmenu as EventListener, {\n capture: true,\n })\n el.removeEventListener('mousemove', handlers.mousemove as EventListener, {\n capture: true,\n })\n el.removeEventListener('mousedown', handlers.mousedown as EventListener, {\n capture: true,\n })\n el.removeEventListener('mouseenter', handlers.mouseenter as EventListener)\n el.removeEventListener('mouseleave', handlers.mouseleave as EventListener)\n }\n\n /**\n * Executed when element enters the viewport\n * Enables an element’s event handlers\n */\n function activateElement({id, elements, handlers}: OverlayElement) {\n const {element, measureElement} = elements\n addEventHandlers(element, handlers)\n ro.observe(measureElement)\n handler({\n type: 'element/activate',\n id,\n })\n }\n\n /**\n * Executed when element leaves the viewport\n * Disables an element’s event handlers\n */\n function deactivateElement({id, elements, handlers}: OverlayElement) {\n const {element, measureElement} = elements\n removeEventHandlers(element, handlers)\n ro.unobserve(measureElement)\n // Scrolling from a hovered element will not trigger mouseleave event, so filter the stack\n hoverStack = hoverStack.filter((el) => el !== element)\n handler({\n type: 'element/deactivate',\n id,\n })\n }\n\n function setOverlayCursor(element: ElementNode) {\n // Don't set the cursor if mutations are unavailable\n if ((!inFrame && !inPopUp) || !optimisticActorReady) return\n\n // Loops through the entire hoverStack, trying to set the cursor if the\n // stack element matches the element passed to the function, otherwise\n // restoring the