UNPKG

@sanity/visual-editing

Version:

[![npm stat](https://img.shields.io/npm/dm/@sanity/visual-editing.svg?style=flat-square)](https://npm-stat.com/charts.html?package=@sanity/visual-editing) [![npm version](https://img.shields.io/npm/v/@sanity/visual-editing.svg?style=flat-square)](https://

1,119 lines (1,118 loc) 45.9 kB
import { c } from "react-compiler-runtime"; import { getPublishedId } from "@sanity/client/csm"; import { useState, useEffect, startTransition, createContext } from "react"; import { useEffectEvent } from "use-effect-event"; import { isEmptyActor } from "./context.js"; import { useOptimisticActor } from "./mutations.js"; import { decodeSanityNodeData } from "@sanity/visual-editing-csm"; import { VERCEL_STEGA_REGEX, vercelStegaDecode } from "@vercel/stega"; function useOptimistic(passthrough, reducer) { const $ = c(21), [pristine, setPristine] = useState(!0), [optimistic, setOptimistic] = useState(passthrough), [lastEvent, setLastEvent] = useState(null), [lastPassthrough, setLastPassthrough] = useState(passthrough), actor = useOptimisticActor(); let t0; $[0] !== reducer ? (t0 = (action, prevState) => (Array.isArray(reducer) ? reducer : [reducer]).reduce((acc, reducer_0) => reducer_0(acc, { document: action.document, id: getPublishedId(action.id), originalId: action.id, type: action.type }), prevState), $[0] = reducer, $[1] = t0) : t0 = $[1]; const reduceStateFromAction = useEffectEvent(t0); let t1; $[2] !== passthrough ? (t1 = () => setLastPassthrough(passthrough), $[2] = passthrough, $[3] = t1) : t1 = $[3]; const updateLastPassthrough = useEffectEvent(t1); let t2; $[4] !== actor || $[5] !== reduceStateFromAction || $[6] !== updateLastPassthrough ? (t2 = () => { if (isEmptyActor(actor)) return; let pristineTimeout; const rebasedSub = actor.on("rebased.local", (_event) => { const event = { document: _event.document, id: _event.id, originalId: getPublishedId(_event.id), type: "mutate" }; setOptimistic((prevState_0) => reduceStateFromAction(event, prevState_0)), setLastEvent(event), updateLastPassthrough(), setPristine(!1), clearTimeout(pristineTimeout); }), pristineSub = actor.on("pristine", () => { pristineTimeout = setTimeout(() => { startTransition(() => setPristine(!0)); }, 15e3); }); return () => { rebasedSub.unsubscribe(), pristineSub.unsubscribe(); }; }, $[4] = actor, $[5] = reduceStateFromAction, $[6] = updateLastPassthrough, $[7] = t2) : t2 = $[7]; let t3; $[8] !== actor ? (t3 = [actor], $[8] = actor, $[9] = t3) : t3 = $[9], useEffect(t2, t3); let t4; $[10] !== lastEvent || $[11] !== lastPassthrough || $[12] !== passthrough || $[13] !== pristine || $[14] !== reduceStateFromAction ? (t4 = () => { if (!pristine) { if (!lastEvent) throw new Error("No last event found when syncing passthrough"); lastPassthrough !== passthrough && startTransition(() => { setOptimistic(reduceStateFromAction(lastEvent, passthrough)), setLastPassthrough(passthrough); }); } }, $[10] = lastEvent, $[11] = lastPassthrough, $[12] = passthrough, $[13] = pristine, $[14] = reduceStateFromAction, $[15] = t4) : t4 = $[15]; let t5; return $[16] !== lastEvent || $[17] !== lastPassthrough || $[18] !== passthrough || $[19] !== pristine ? (t5 = [lastEvent, lastPassthrough, passthrough, pristine], $[16] = lastEvent, $[17] = lastPassthrough, $[18] = passthrough, $[19] = pristine, $[20] = t5) : t5 = $[20], useEffect(t4, t5), pristine ? passthrough : optimistic; } const byteToHex = []; for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1)); function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } let getRandomValues; const rnds8 = new Uint8Array(16); function rng() { if (!getRandomValues) { if (typeof crypto > "u" || !crypto.getRandomValues) throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported"); getRandomValues = crypto.getRandomValues.bind(crypto); } return getRandomValues(rnds8); } const randomUUID = typeof crypto < "u" && crypto.randomUUID && crypto.randomUUID.bind(crypto); var native = { randomUUID }; function v4(options, buf, offset) { if (native.randomUUID && !options) return native.randomUUID(); options = options || {}; const rnds = options.random ?? options.rng?.() ?? rng(); if (rnds.length < 16) throw new Error("Random bytes length must be >= 16"); return rnds[6] = rnds[6] & 15 | 64, rnds[8] = rnds[8] & 63 | 128, unsafeStringify(rnds); } function getRect(element) { const domRect = element.getBoundingClientRect(); return { x: domRect.x + scrollX, y: domRect.y + scrollY, w: domRect.width, h: domRect.height }; } function offsetRect(rect, px, axis) { return axis === "x" ? { x: rect.x + px, y: rect.y, w: rect.w - 2 * px, h: rect.h } : { x: rect.x, y: rect.y + px, w: rect.w, h: rect.h - 2 * px }; } function rayIntersect(l1, l2) { const { x1, y1, x2, y2 } = l1, { x1: x3, y1: y3, x2: x4, y2: y4 } = l2; if (x1 === x2 && y1 === y2 || x3 === x4 && y3 === y4) return !1; const denominator = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if (denominator === 0) return !1; const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator, ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denominator; if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return !1; const x = x1 + ua * (x2 - x1), y = y1 + ua * (y2 - y1); return { x, y }; } function rectEqual(r1, r2) { return r1.x === r2.x && r1.y === r2.y && r1.w === r2.w && r1.h === r2.h; } function rayRectIntersections(line, rect) { const rectLines = [{ x1: rect.x, y1: rect.y, x2: rect.x + rect.w, y2: rect.y }, { x1: rect.x + rect.w, y1: rect.y, x2: rect.x + rect.w, y2: rect.y + rect.h }, { x1: rect.x + rect.w, y1: rect.y + rect.h, x2: rect.x, y2: rect.y + rect.h }, { x1: rect.x, y1: rect.y + rect.h, x2: rect.x, y2: rect.y }], intersections = []; for (let i = 0; i < rectLines.length; i++) { const intersection = rayIntersect(line, rectLines[i]); if (intersection) { let isDuplicate = !1; for (let j = 0; j < intersections.length; j++) intersections[j].x === intersection.x && intersections[j].y === intersection.y && (isDuplicate = !0); isDuplicate || intersections.push(intersection); } } return intersections.length === 0 ? !1 : intersections.sort((a, b) => pointDist(a, { x: line.x1, y: line.y1 }) - pointDist(b, { x: line.x1, y: line.y1 })); } function pointDist(p1, p2) { const a = p1.x - p2.x, b = p1.y - p2.y; return Math.sqrt(a * a + b * b); } function pointInBounds(point, bounds) { const withinX = point.x >= bounds.x && point.x <= bounds.x + bounds.w, withinY = point.y >= bounds.y && point.y <= bounds.y + bounds.h; return withinX && withinY; } function findClosestIntersection(ray, targets, flow) { const rayOrigin = { x: ray.x1, y: ray.y1 }; if (targets.some((t) => pointInBounds(rayOrigin, offsetRect(t, Math.min(t.w, t.h) / 10, flow === "horizontal" ? "x" : "y")))) return null; let closestIntersection, closestRect; for (const target of targets) { const intersections = rayRectIntersections(ray, offsetRect(target, Math.min(target.w, target.h) / 10, flow === "horizontal" ? "x" : "y")); if (intersections) { const firstIntersection = intersections[0]; closestIntersection ? pointDist(rayOrigin, firstIntersection) < pointDist(rayOrigin, closestIntersection) && (closestIntersection = firstIntersection, closestRect = target) : (closestIntersection = firstIntersection, closestRect = target); } } return closestRect || null; } function scaleRect(rect, scale, origin) { const { x, y, w, h } = rect, { x: originX, y: originY } = origin, newX = originX + (x - originX) * scale, newY = originY + (y - originY) * scale, newWidth = w * scale, newHeight = h * scale; return { x: newX, y: newY, w: newWidth, h: newHeight }; } function getRectGroupXExtent(rects) { const minGroupX = Math.max(0, Math.min(...rects.map((r) => r.x))), maxGroupX = Math.min(document.body.offsetWidth, Math.max(...rects.map((r) => r.x + r.w))); return { min: minGroupX, max: maxGroupX, width: maxGroupX - minGroupX }; } function getRectGroupYExtent(rects) { const minGroupY = Math.max(0, Math.min(...rects.map((r) => r.y))), maxGroupY = Math.min(document.body.scrollHeight, Math.max(...rects.map((r) => r.y + r.h))); return { min: minGroupY, max: maxGroupY, height: maxGroupY - minGroupY }; } function calcTargetFlow(targets) { return targets.some((t1) => targets.filter((t2) => !rectEqual(t1, t2)).some((t2) => t1.y === t2.y)) ? "horizontal" : "vertical"; } function calcInsertPosition(origin, targets, flow) { if (flow === "horizontal") { const rayLeft = { x1: origin.x, y1: origin.y, x2: origin.x - 1e8, y2: origin.y }, rayRight = { x1: origin.x, y1: origin.y, x2: origin.x + 1e8, y2: origin.y }; return { left: findClosestIntersection(rayLeft, targets, flow), right: findClosestIntersection(rayRight, targets, flow) }; } else { const rayTop = { x1: origin.x, y1: origin.y, x2: origin.x, y2: origin.y - 1e8 }, rayBottom = { x1: origin.x, y1: origin.y, x2: origin.x, y2: origin.y + 1e8 }; return { top: findClosestIntersection(rayTop, targets, flow), bottom: findClosestIntersection(rayBottom, targets, flow) }; } } function findRectSanityData(rect, overlayGroup) { return overlayGroup.find((e) => rectEqual(getRect(e.elements.element), rect))?.sanity; } function resolveInsertPosition(overlayGroup, insertPosition, flow) { return Object.values(insertPosition).every((v) => v === null) ? null : flow === "horizontal" ? { left: insertPosition.left ? { rect: insertPosition.left, sanity: findRectSanityData(insertPosition.left, overlayGroup) } : null, right: insertPosition.right ? { rect: insertPosition.right, sanity: findRectSanityData(insertPosition.right, overlayGroup) } : null } : { top: insertPosition.top ? { rect: insertPosition.top, sanity: findRectSanityData(insertPosition.top, overlayGroup) } : null, bottom: insertPosition.bottom ? { rect: insertPosition.bottom, sanity: findRectSanityData(insertPosition.bottom, overlayGroup) } : null }; } function calcMousePos(e) { const bodyBounds = document.body.getBoundingClientRect(); return { x: Math.max(bodyBounds.x, Math.min(e.clientX, bodyBounds.x + bodyBounds.width)), y: e.clientY + window.scrollY }; } function calcMousePosInverseTransform(mousePos2) { const body = document.body, transform = window.getComputedStyle(body).transform; if (transform === "none") return { x: mousePos2.x, y: mousePos2.y }; const inverseMatrix = new DOMMatrix(transform).inverse(), transformedPoint = new DOMPoint(mousePos2.x, mousePos2.y).matrixTransform(inverseMatrix); return { x: transformedPoint.x, y: transformedPoint.y }; } function buildPreviewSkeleton(mousePos2, element, scaleFactor) { const bounds = getRect(element), children = [...element.querySelectorAll(":where(h1, h2, h3, h4, p, a, img, span, button):not(:has(*))")]; mousePos2.x <= bounds.x && (mousePos2.x = bounds.x), mousePos2.x >= bounds.x + bounds.w && (mousePos2.x = bounds.x + bounds.w), mousePos2.y >= bounds.y + bounds.h && (mousePos2.y = bounds.y + bounds.h), mousePos2.y <= bounds.y && (mousePos2.y = bounds.y); const childRects = children.map((child) => { const rect = scaleRect(getRect(child), scaleFactor, { x: bounds.x, y: bounds.y }); return { x: rect.x - bounds.x, y: rect.y - bounds.y, w: rect.w, h: rect.h, tagName: child.tagName }; }); return { offsetX: (bounds.x - mousePos2.x) * scaleFactor, offsetY: (bounds.y - mousePos2.y) * scaleFactor, w: bounds.w * scaleFactor, h: bounds.h * scaleFactor, maxWidth: bounds.w * scaleFactor * 0.75, childRects }; } const minDragDelta = 4; async function applyMinimapWrapperTransform(target, scaleFactor, minYScaled, handler, rectUpdateFrequency) { return new Promise((resolve) => { target.addEventListener("transitionend", () => { setTimeout(() => { handler({ type: "overlay/dragEndMinimapTransition" }); }, rectUpdateFrequency * 2), resolve(); }, { once: !0 }), handler({ type: "overlay/dragStartMinimapTransition" }), handler({ type: "overlay/dragToggleMinimap", display: !0 }), document.body.style.overflow = "hidden", document.body.style.height = "100%", document.documentElement.style.overflow = "initial", document.documentElement.style.height = "100%", setTimeout(() => { target.style.transformOrigin = "50% 0px", target.style.transition = "transform 150ms ease", target.style.transform = `translate3d(0px, ${-minYScaled + scrollY}px, 0px) scale(${scaleFactor})`; }, 25); }); } function calcMinimapTransformValues(rects, groupHeightOverride) { let groupHeight = groupHeightOverride || getRectGroupYExtent(rects).height; const padding = 100; groupHeight += padding * 2; const scaleFactor = groupHeight > window.innerHeight ? window.innerHeight / groupHeight : 1, scaledRects = rects.map((r) => scaleRect(r, scaleFactor, { x: window.innerWidth / 2, y: 0 })), { min: minYScaled } = getRectGroupYExtent(scaledRects); return { scaleFactor, minYScaled: minYScaled - padding * scaleFactor }; } function calcGroupBoundsPreview(rects) { const groupBoundsX = getRectGroupXExtent(rects), groupBoundsY = getRectGroupYExtent(rects), offsetDist = 8, canOffsetX = groupBoundsX.min > offsetDist && groupBoundsX.min + groupBoundsX.width <= window.innerWidth - offsetDist, canOffsetY = groupBoundsY.min > offsetDist && groupBoundsY.min + groupBoundsY.height <= document.body.scrollHeight - offsetDist, canOffset = canOffsetX && canOffsetY; return { x: canOffset ? groupBoundsX.min - offsetDist : groupBoundsX.min, y: canOffset ? groupBoundsY.min - offsetDist : groupBoundsY.min, w: canOffset ? groupBoundsX.width + offsetDist * 2 : groupBoundsX.width, h: canOffset ? groupBoundsY.height + offsetDist * 2 : groupBoundsY.height }; } async function resetMinimapWrapperTransform(endYOrigin, target, prescaleHeight2, handler, rectUpdateFrequency, previousRootStyleValues2) { return new Promise((resolve) => { const transform = window.getComputedStyle(target).transform; if (new DOMMatrix(transform).a === 1) return; const maxScroll = prescaleHeight2 - window.innerHeight, prevScrollY = scrollY; endYOrigin -= window.innerHeight / 2, endYOrigin < 0 && (endYOrigin = 0), target.addEventListener("transitionend", () => { target.style.transition = "none", target.style.transform = "none", scrollTo({ top: endYOrigin, behavior: "instant" }), setTimeout(() => { handler({ type: "overlay/dragEndMinimapTransition" }), handler({ type: "overlay/dragToggleMinimap", display: !1 }); }, rectUpdateFrequency * 2), resolve(); }, { once: !0 }), handler({ type: "overlay/dragStartMinimapTransition" }), target.style.transform = `translateY(${Math.max(prevScrollY - endYOrigin, -maxScroll + prevScrollY)}px) scale(1)`, previousRootStyleValues2 && (document.body.style.overflow = previousRootStyleValues2.body.overflow, document.body.style.height = previousRootStyleValues2.body.height, document.documentElement.style.overflow = previousRootStyleValues2.documentElement.overflow, document.documentElement.style.height = previousRootStyleValues2.documentElement.height); }); } let minimapScaleApplied = !1, mousePosInverseTransform = { y: 0 }, mousePos = { x: 0, y: 0 }, prescaleHeight = typeof document > "u" ? 0 : document.documentElement.scrollHeight, previousRootStyleValues = null; function handleOverlayDrag(opts) { const { mouseEvent, element, overlayGroup, handler, target, onSequenceStart, onSequenceEnd } = opts; if (mouseEvent.button !== 0) return; window.focus(); const rectUpdateFrequency = 150; let rects = overlayGroup.map((e) => getRect(e.elements.element)); const flow = element.getAttribute("data-sanity-drag-flow") || calcTargetFlow(rects), dragGroup = element.getAttribute("data-sanity-drag-group"), disableMinimap = !!element.getAttribute("data-sanity-drag-minimap-disable"), preventInsertDefault = !!element.getAttribute("data-sanity-drag-prevent-default"), documentHeightOverride = element.getAttribute("data-unstable_sanity-drag-document-height"), groupHeightOverride = element.getAttribute("data-unstable_sanity-drag-group-height"); let insertPosition = null; const initialMousePos = calcMousePos(mouseEvent), scaleTarget = document.body, { minYScaled, scaleFactor } = calcMinimapTransformValues(rects, groupHeightOverride ? ~~groupHeightOverride : null); let sequenceStarted = !1, minimapPromptShown = !1, mousedown = !0; minimapScaleApplied || (previousRootStyleValues = { body: { overflow: window.getComputedStyle(document.body).overflow, height: window.getComputedStyle(document.body).height }, documentElement: { overflow: window.getComputedStyle(document.documentElement).overflow, height: window.getComputedStyle(document.documentElement).height } }, prescaleHeight = documentHeightOverride ? ~~documentHeightOverride : document.documentElement.scrollHeight); const rectsInterval = setInterval(() => { rects = overlayGroup.map((e) => getRect(e.elements.element)); }, rectUpdateFrequency), applyMinimap = () => { if (scaleFactor >= 1) return; const skeleton = buildPreviewSkeleton(mousePos, element, scaleFactor); handler({ type: "overlay/dragUpdateSkeleton", skeleton }), handler({ type: "overlay/dragToggleMinimapPrompt", display: !1 }), applyMinimapWrapperTransform(scaleTarget, scaleFactor, minYScaled, handler, rectUpdateFrequency).then(() => { setTimeout(() => { handler({ type: "overlay/dragUpdateGroupRect", groupRect: calcGroupBoundsPreview(rects) }); }, rectUpdateFrequency * 2); }); }, handleScroll = (e) => { Math.abs(e.deltaY) >= 10 && scaleFactor < 1 && !minimapScaleApplied && !minimapPromptShown && !disableMinimap && mousedown && (handler({ type: "overlay/dragToggleMinimapPrompt", display: !0 }), minimapPromptShown = !0), e.shiftKey && !minimapScaleApplied && !disableMinimap && (window.dispatchEvent(new CustomEvent("unstable_sanity/dragApplyMinimap")), minimapScaleApplied = !0, setTimeout(() => { applyMinimap(); }, 50)); }, handleMouseMove = (e) => { if (e.preventDefault(), mousePos = calcMousePos(e), mousePosInverseTransform = calcMousePosInverseTransform(mousePos), Math.abs(pointDist(mousePos, initialMousePos)) < minDragDelta) return; if (!sequenceStarted) { const groupRect = calcGroupBoundsPreview(rects), skeleton = buildPreviewSkeleton(mousePos, element, 1); handler({ type: "overlay/dragStart", flow }), handler({ type: "overlay/dragUpdateSkeleton", skeleton }), handler({ type: "overlay/dragUpdateGroupRect", groupRect }), sequenceStarted = !0, onSequenceStart(); } handler({ type: "overlay/dragUpdateCursorPosition", x: mousePos.x, y: mousePos.y }), e.shiftKey && !minimapScaleApplied && !disableMinimap && (window.dispatchEvent(new CustomEvent("unstable_sanity/dragApplyMinimap")), minimapScaleApplied = !0, setTimeout(() => { applyMinimap(); }, 50)); const newInsertPosition = calcInsertPosition(mousePos, rects, flow); JSON.stringify(insertPosition) !== JSON.stringify(newInsertPosition) && (insertPosition = newInsertPosition, handler({ type: "overlay/dragUpdateInsertPosition", insertPosition: resolveInsertPosition(overlayGroup, insertPosition, flow) })); }, handleMouseUp = () => { mousedown = !1, handler({ type: "overlay/dragEnd", target, insertPosition: insertPosition ? resolveInsertPosition(overlayGroup, insertPosition, flow) : null, dragGroup, flow, preventInsertDefault }), minimapPromptShown && handler({ type: "overlay/dragToggleMinimapPrompt", display: !1 }), minimapScaleApplied || (clearInterval(rectsInterval), onSequenceEnd(), removeFrameListeners(), removeKeyListeners()), removeMouseListeners(); }, handleKeyup = (e) => { if (e.key === "Shift" && minimapScaleApplied) { minimapScaleApplied = !1; const skeleton = buildPreviewSkeleton(mousePos, element, 1 / scaleFactor); handler({ type: "overlay/dragUpdateSkeleton", skeleton }), window.dispatchEvent(new CustomEvent("unstable_sanity/dragResetMinimap")), setTimeout(() => { resetMinimapWrapperTransform(mousePosInverseTransform.y, scaleTarget, prescaleHeight, handler, rectUpdateFrequency, previousRootStyleValues); }, 50), handler({ type: "overlay/dragUpdateGroupRect", groupRect: null }), mousedown || (clearInterval(rectsInterval), removeMouseListeners(), removeFrameListeners(), removeKeyListeners(), onSequenceEnd()); } }, handleBlur = () => { handler({ type: "overlay/dragUpdateGroupRect", groupRect: null }), window.dispatchEvent(new CustomEvent("unstable_sanity/dragResetMinimap")), setTimeout(() => { resetMinimapWrapperTransform(mousePosInverseTransform.y, scaleTarget, prescaleHeight, handler, rectUpdateFrequency, previousRootStyleValues).then(() => { minimapScaleApplied = !1; }); }, 50), clearInterval(rectsInterval), removeMouseListeners(), removeFrameListeners(), removeKeyListeners(), onSequenceEnd(); }, removeMouseListeners = () => { window.removeEventListener("mousemove", handleMouseMove), window.removeEventListener("wheel", handleScroll), window.removeEventListener("mouseup", handleMouseUp); }, removeKeyListeners = () => { window.removeEventListener("keyup", handleKeyup); }, removeFrameListeners = () => { window.removeEventListener("blur", handleBlur); }; window.addEventListener("blur", handleBlur), window.addEventListener("keyup", handleKeyup), window.addEventListener("wheel", handleScroll), window.addEventListener("mousemove", handleMouseMove), window.addEventListener("mouseup", handleMouseUp); } const isElementNode$1 = (target) => target instanceof HTMLElement || target instanceof SVGElement; function findNonInlineElement(element) { const { display } = window.getComputedStyle(element); if (display !== "inline") return element; const parent = element.parentElement; return parent ? findNonInlineElement(parent) : null; } const findOverlayElement = (el) => !el || !isElementNode$1(el) ? null : el.dataset?.sanityOverlayElement ? el : findOverlayElement(el.parentElement); function testVercelStegaRegex(input) { return VERCEL_STEGA_REGEX.lastIndex = 0, VERCEL_STEGA_REGEX.test(input); } function decodeStega(str, isAltText = !1) { try { const decoded = vercelStegaDecode(str); return !decoded || decoded.origin !== "sanity.io" ? null : (isAltText && (decoded.href = decoded.href?.replace(".alt", "")), decoded); } catch (err) { return console.error("Failed to decode stega for string: ", str, "with the original error: ", err), null; } } function testAndDecodeStega(str, isAltText = !1) { return testVercelStegaRegex(str) ? decodeStega(str, isAltText) : null; } const isElementNode = (node) => node.nodeType === Node.ELEMENT_NODE, isImgElement = (el) => el.tagName === "IMG", isTimeElement = (el) => el.tagName === "TIME", isSvgRootElement = (el) => el.tagName.toUpperCase() === "SVG"; function isSanityNode(node) { return "path" in node; } function findCommonPath(first, second) { let firstParts = first.split("."), secondParts = second.split("."); const maxLength = Math.min(firstParts.length, secondParts.length); return firstParts = firstParts.slice(0, maxLength).reverse(), secondParts = secondParts.slice(0, maxLength).reverse(), firstParts.reduce((parts, part, i) => part === secondParts[i] ? [...parts, part] : [], []).reverse().join("."); } function findCommonSanityData(nodes) { if (!nodes.length || !nodes.map((n) => isSanityNode(n)).every((n, _i, arr) => n === arr[0])) return; if (!isSanityNode(nodes[0])) return nodes[0]; const sanityNodes = nodes.filter(isSanityNode); let common = nodes[0]; const consistentValueKeys = ["projectId", "dataset", "id", "baseUrl", "workspace", "tool"]; for (let i = 1; i < sanityNodes.length; i++) { const node = sanityNodes[i]; if (consistentValueKeys.some((key) => node[key] !== common?.[key])) { common = void 0; break; } common = { ...common, path: findCommonPath(common.path, node.path) }; } return common; } function findSanityNodes(el) { const mainResults = []; function createResolvedElement(element, data, reason, preventGrouping) { const sanity = decodeSanityNodeData(data); if (!sanity) return; const measureElement = findNonInlineElement(element); if (measureElement) return { elements: { element, measureElement }, sanity, reason, preventGrouping }; } function resolveNode(node) { const { nodeType, parentElement, textContent } = node; if (isElementNode(node) && node.dataset?.sanityEditTarget !== void 0) { const nodesInTarget = findSanityNodes(node), commonData = findCommonSanityData(nodesInTarget.map((node2) => node2.type === "element" ? node2.commonSanity : void 0).filter((n) => n !== void 0)); if (commonData) return { reason: "edit-target", elements: { element: node, measureElement: node }, sanity: commonData }; } else if (nodeType === Node.TEXT_NODE && parentElement && textContent) { const data = testAndDecodeStega(textContent); return data ? createResolvedElement(parentElement, data, "stega-text", !0) : void 0; } else if (isElementNode(node)) { if (node.tagName === "SCRIPT" || node.tagName === "SANITY-VISUAL-EDITING") return; if (node.dataset?.sanity) return createResolvedElement(node, node.dataset.sanity, "data-attribute", !!(node.textContent && testVercelStegaRegex(node.textContent))); if (node.dataset?.sanityEditInfo) return createResolvedElement(node, node.dataset.sanityEditInfo, "data-attribute", !!(node.textContent && testVercelStegaRegex(node.textContent))); if (isImgElement(node)) { const data = testAndDecodeStega(node.alt, !0); return data ? createResolvedElement(node, data, "stega-attribute") : void 0; } else if (isTimeElement(node)) { const data = testAndDecodeStega(node.dateTime, !0); return data ? createResolvedElement(node, data, "stega-attribute") : void 0; } else if (isSvgRootElement(node)) { if (!node.ariaLabel) return; const data = testAndDecodeStega(node.ariaLabel, !0); return data ? createResolvedElement(node, data, "stega-attribute") : void 0; } } } function processNode(node, _parentGroup) { const resolvedElement = resolveNode(node); let parentGroup = _parentGroup; if (isElementNode(node) && node.dataset?.sanityEditGroup !== void 0 && (parentGroup = { type: "group", elements: { element: node, measureElement: node }, targets: [] }, mainResults.push(parentGroup)), resolvedElement) { const target = { elements: resolvedElement.elements, sanity: resolvedElement.sanity, reason: resolvedElement.reason }; parentGroup && !resolvedElement.preventGrouping ? parentGroup.targets.push(target) : mainResults.push({ elements: resolvedElement.elements, type: "element", targets: [target] }); } if (isElementNode(node) && !isImgElement(node) && !(node.tagName === "SCRIPT" || node.tagName === "SANITY-VISUAL-EDITING")) for (const childNode of node.childNodes) processNode(childNode, parentGroup); } if (el) for (const node of el.childNodes) processNode(node, void 0); return mainResults.map((node) => { if (node.targets.length === 0 && node.type === "group") return { ...node, commonSanity: void 0 }; const commonSanity = node.targets.length === 1 ? node.targets[0].sanity : findCommonSanityData(node.targets.map(({ sanity }) => sanity).filter((n) => n !== void 0)) || node.targets[0].sanity; return commonSanity ? { ...node, commonSanity } : null; }).filter((node) => node !== null); } function isSanityArrayPath(path) { const lastDotIndex = path.lastIndexOf("."); return path.substring(lastDotIndex, path.length).includes("["); } function getSanityNodeArrayPath(path) { if (!isSanityArrayPath(path)) return null; const split = path.split("."); return split[split.length - 1] = split[split.length - 1].replace(/\[.*?\]/g, "[]"), split.join("."); } function sanityNodesExistInSameArray(sanityNode1, sanityNode2) { return !isSanityArrayPath(sanityNode1.path) || !isSanityArrayPath(sanityNode2.path) ? !1 : getSanityNodeArrayPath(sanityNode1.path) === getSanityNodeArrayPath(sanityNode2.path); } function resolveDragAndDropGroup(element, sanity, elementSet, elementsMap) { if (!element.getAttribute("data-sanity") || element.getAttribute("data-sanity-drag-disable") || !sanity || !isSanityNode(sanity) || !isSanityArrayPath(sanity.path)) return null; const targetDragGroup = element.getAttribute("data-sanity-drag-group"), group = [...elementSet].reduce((acc, el) => { const elData = elementsMap.get(el), elDragDisabled = el.getAttribute("data-sanity-drag-disable"), elDragGroup = el.getAttribute("data-sanity-drag-group"), elHasSanityAttribution = el.getAttribute("data-sanity") !== null, sharedDragGroup = targetDragGroup !== null ? targetDragGroup === elDragGroup : !0; return elData?.sanity && !elDragDisabled && isSanityNode(elData.sanity) && sanityNodesExistInSameArray(sanity, elData.sanity) && sharedDragGroup && elHasSanityAttribution && acc.push(elData), acc; }, []); return group.length <= 1 ? null : group; } function createOverlayController({ handler, overlayElement, inFrame, inPopUp, optimisticActorReady }) { let activated = !1; const elementIdMap = /* @__PURE__ */ new Map(), elementsMap = /* @__PURE__ */ new WeakMap(), elementSet = /* @__PURE__ */ new Set(), measureElements = /* @__PURE__ */ new WeakMap(), cursorMap = /* @__PURE__ */ new WeakMap(); let ro, io, mo, activeDragSequence = !1, hoverStack = []; const getHoveredElement = () => hoverStack[hoverStack.length - 1]; function addEventHandlers(el, handlers) { el.addEventListener("click", handlers.click, { capture: !0 }), el.addEventListener("contextmenu", handlers.contextmenu, { capture: !0 }), el.addEventListener("mousemove", handlers.mousemove, { once: !0, capture: !0 }), el.addEventListener("mousedown", handlers.mousedown, { capture: !0 }); } function removeEventHandlers(el, handlers) { el.removeEventListener("click", handlers.click, { capture: !0 }), el.removeEventListener("contextmenu", handlers.contextmenu, { capture: !0 }), el.removeEventListener("mousemove", handlers.mousemove, { capture: !0 }), el.removeEventListener("mousedown", handlers.mousedown, { capture: !0 }), el.removeEventListener("mouseenter", handlers.mouseenter), el.removeEventListener("mouseleave", handlers.mouseleave); } function activateElement({ id, elements, handlers }) { const { element, measureElement } = elements; addEventHandlers(element, handlers), ro.observe(measureElement), handler({ type: "element/activate", id }); } function deactivateElement({ id, elements, handlers }) { const { element, measureElement } = elements; removeEventHandlers(element, handlers), ro.unobserve(measureElement), hoverStack = hoverStack.filter((el) => el !== element), handler({ type: "element/deactivate", id }); } function setOverlayCursor(element) { if (!(!inFrame && !inPopUp || !optimisticActorReady)) for (const hoverstackElement of hoverStack) { if (element === hoverstackElement) { const targetSanityData = elementsMap.get(element)?.sanity; if (!targetSanityData || !isSanityNode(targetSanityData)) return; if (resolveDragAndDropGroup(element, targetSanityData, elementSet, elementsMap)) { const existingCursor = element.style.cursor; existingCursor && cursorMap.set(element, existingCursor), handler({ type: "overlay/setCursor", element, cursor: "move" }); continue; } } restoreOverlayCursor(hoverstackElement); } } function restoreOverlayCursor(element) { const previousCursor = cursorMap.get(element); handler({ type: "overlay/setCursor", element, cursor: previousCursor }); } function registerElement({ type, elements, commonSanity, targets }) { const { element, measureElement } = elements, eventHandlers = { click(event) { const target = event.target; if (element === getHoveredElement() && element.contains(target)) { inFrame && (event.preventDefault(), event.stopPropagation()); const sanity = elementsMap.get(element)?.sanity; sanity && !activeDragSequence && handler({ type: "element/click", id, sanity }); } }, contextmenu(event) { if (!("path" in commonSanity) || !inFrame && !inPopUp || !optimisticActorReady || !commonSanity.path.split(".").pop()?.includes("[_key==")) return; const target = event.target; element === getHoveredElement() && element.contains(target) && ((inFrame || inPopUp) && (event.preventDefault(), event.stopPropagation()), handler({ type: "element/contextmenu", id, position: { x: event.clientX, y: event.clientY }, sanity: commonSanity })); }, mousedown(event) { if (event.preventDefault(), event.currentTarget !== hoverStack.at(-1) || element.getAttribute("data-sanity-drag-disable") || !inFrame && !inPopUp || !optimisticActorReady) return; const targetSanityData = elementsMap.get(element)?.sanity; if (!targetSanityData || !isSanityNode(targetSanityData) || !isSanityArrayPath(targetSanityData.path)) return; const dragGroup = resolveDragAndDropGroup(element, commonSanity, elementSet, elementsMap); dragGroup && handleOverlayDrag({ element, handler, mouseEvent: event, overlayGroup: dragGroup, target: targetSanityData, onSequenceStart: () => { activeDragSequence = !0; }, onSequenceEnd: () => { setTimeout(() => { activeDragSequence = !1; }, 250); } }); }, mousemove(event) { eventHandlers.mouseenter(event); const el = event.currentTarget; el && (el.addEventListener("mouseenter", eventHandlers.mouseenter), el.addEventListener("mouseleave", eventHandlers.mouseleave)); }, mouseenter() { document.querySelector("vercel-live-feedback") && element.closest("[data-vercel-edit-info]") || element.closest("[data-vercel-edit-target]") || (hoverStack.push(element), handler({ type: "element/mouseenter", id, rect: getRect(element) }), setOverlayCursor(element)); }, mouseleave(e) { function leave() { hoverStack.pop(); const hoveredElement = getHoveredElement(); if (handler({ type: "element/mouseleave", id }), hoveredElement) { setOverlayCursor(hoveredElement); const overlayElement2 = elementsMap.get(hoveredElement); overlayElement2 && handler({ type: "element/mouseenter", id: overlayElement2.id, rect: getRect(hoveredElement) }); } restoreOverlayCursor(element); } function addDeferredLeave(el) { const deferredLeave = (e2) => { const { relatedTarget: relatedTarget2 } = e2; findOverlayElement(relatedTarget2) ? relatedTarget2 && isElementNode$1(relatedTarget2) && (el.removeEventListener("mouseleave", deferredLeave), addDeferredLeave(relatedTarget2)) : (el.removeEventListener("mouseleave", deferredLeave), leave()); }; el.addEventListener("mouseleave", deferredLeave); } const { relatedTarget } = e, container = findOverlayElement(relatedTarget), isInteractiveOverlayElement = overlayElement.contains(container); if (isElementNode$1(container) && isInteractiveOverlayElement) return addDeferredLeave(container); leave(); } }, id = v4(), sanityNode = { type, id, elements, sanity: commonSanity, handlers: eventHandlers }; elementSet.add(element), measureElements.set(measureElement, element), elementIdMap.set(id, element), elementsMap.set(element, sanityNode), io?.observe(element), handler({ type: "element/register", elementType: type, id, element, rect: getRect(element), sanity: commonSanity, dragDisabled: !!element.getAttribute("data-sanity-drag-disable"), targets: targets.map((target) => ({ sanity: target.sanity, element: target.elements.element })) }), activated && activateElement(sanityNode); } function updateElement(resolvedElement) { const { element } = resolvedElement.elements, overlayElement2 = elementsMap.get(element); overlayElement2 && (elementsMap.set(element, { ...overlayElement2, sanity: resolvedElement.commonSanity }), handler({ type: "element/update", elementType: overlayElement2.type, id: overlayElement2.id, rect: getRect(element), sanity: resolvedElement.commonSanity, targets: resolvedElement.targets.map((target) => ({ sanity: target.sanity, element: target.elements.element })) })); } function parseElements(node) { const sanityNodes = findSanityNodes(node); for (const sanityNode of sanityNodes) { if (sanityNode.type === "group") { for (const target of sanityNode.targets) { const overlayElement2 = elementsMap.get(target.elements.element); overlayElement2 && overlayElement2.type === "element" && unregisterElement(target.elements.element); } sanityNode.targets.length === 0 && unregisterElement(sanityNode.elements.element); } if (!sanityNode.commonSanity) continue; const { element } = sanityNode.elements; elementsMap.has(element) ? updateElement(sanityNode) : registerElement(sanityNode); } } function unregisterElement(element) { const overlayElement2 = elementsMap.get(element); if (overlayElement2) { const { id, handlers } = overlayElement2; removeEventHandlers(element, handlers), ro.unobserve(element), elementsMap.delete(element), elementSet.delete(element), elementIdMap.delete(id), handler({ type: "element/unregister", id }); } } function handleMutation(mutations) { let mutationWasInScope = !1; for (const mutation of mutations) { const { target, type } = mutation, node = type === "characterData" ? target.parentElement : target; if (!(node === overlayElement || overlayElement.contains(node)) && (mutationWasInScope = !0, isElementNode$1(node))) { const possibleGroupParent = node.parentElement?.closest("[data-sanity-edit-group]") || null, updateNodeTarget = isElementNode$1(possibleGroupParent) ? possibleGroupParent : node; parseElements({ childNodes: [updateNodeTarget] }); } } if (mutationWasInScope) for (const element of elementSet) element.isConnected || unregisterElement(element), elementsMap.get(element)?.type === "group" && !element.hasAttribute("data-sanity-edit-group") && unregisterElement(element); } function updateRect(el) { const overlayElement2 = elementsMap.get(el); overlayElement2 && handler({ type: "element/updateRect", id: overlayElement2.id, rect: getRect(el) }); } function handleResize(entries) { for (const entry of entries) { const target = entry.target; if (isElementNode$1(target)) { const element = measureElements.get(target); if (!element) return; updateRect(element); } } } function handleIntersection(entries) { if (activated) for (const entry of entries) { const { target } = entry, match = isElementNode$1(target) && elementsMap.get(target); match && (entry.isIntersecting ? activateElement(match) : deactivateElement(match)); } } function handleBlur(event) { const element = findOverlayElement(event.target); if (element) { element.dataset.sanityOverlayElement === "capture" && (event.preventDefault(), event.stopPropagation()); return; } hoverStack = [], handler({ type: "overlay/blur" }); } function handleExclusivePluginClosed() { hoverStack = [], handler({ type: "overlay/reset-mouse-state" }); } function handleWindowResize() { for (const element of elementSet) updateRect(element); } function handleKeydown(event) { event.key === "Escape" && (hoverStack = [], handler({ type: "overlay/blur" })); } function handleWindowScroll(event) { const { target } = event; if (!(target === window.document || !isElementNode$1(target))) for (const element of elementSet) target.contains(element) && updateRect(element); } function activate() { activated || (io = new IntersectionObserver(handleIntersection, { threshold: 0.3 }), elementSet.forEach((element) => io.observe(element)), handler({ type: "overlay/activate" }), activated = !0); } function deactivate() { activated && (io?.disconnect(), elementSet.forEach((element) => { const overlayElement2 = elementsMap.get(element); overlayElement2 && deactivateElement(overlayElement2); }), handler({ type: "overlay/deactivate" }), activated = !1); } function handleHeaderClick(event) { const { id } = event.detail, element = elementIdMap.get(id); if (!element) return; const sanity = elementsMap.get(element)?.sanity; sanity && handler({ type: "element/click", id, sanity }); } function destroy() { window.removeEventListener("click", handleBlur), window.removeEventListener("contextmenu", handleBlur), window.removeEventListener("sanity-overlay/exclusive-plugin-closed", handleExclusivePluginClosed), window.removeEventListener("sanity-overlay/label-click", handleHeaderClick), window.removeEventListener("keydown", handleKeydown), window.removeEventListener("resize", handleWindowResize), window.removeEventListener("scroll", handleWindowScroll), mo.disconnect(), ro.disconnect(), elementSet.forEach((element) => { unregisterElement(element); }), elementIdMap.clear(), elementSet.clear(), hoverStack = [], deactivate(); } function create() { window.addEventListener("click", handleBlur), window.addEventListener("contextmenu", handleBlur), window.addEventListener("sanity-overlay/exclusive-plugin-closed", handleExclusivePluginClosed), window.addEventListener("sanity-overlay/label-click", handleHeaderClick), window.addEventListener("keydown", handleKeydown), window.addEventListener("resize", handleWindowResize), window.addEventListener("scroll", handleWindowScroll, { capture: !0, passive: !0 }), ro = new ResizeObserver(handleResize), mo = new MutationObserver(handleMutation), mo.observe(document.body, { attributes: !0, characterData: !0, childList: !0, subtree: !0 }), parseElements(document.body), activate(); } return window.document.fonts.ready.then(() => { for (const element of elementSet) updateRect(element); }), create(), { activate, deactivate, destroy }; } const SharedStateContext = createContext(null); export { SharedStateContext, createOverlayController, sanityNodesExistInSameArray, useOptimistic, v4 }; //# sourceMappingURL=SharedStateContext.js.map