UNPKG

sidakram-bippy

Version:

hack into react internals

370 lines (366 loc) 11.9 kB
import '../chunk-XX7FLNAQ.js'; import { getFiberFromHostInstance, instrument, traverseFiber, isHostFiber, __async } from '../chunk-7ROEST76.js'; /** * @license bippy * * Copyright (c) Aiden Bai, Million Software, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ // src/experiments/shrinkwrap.ts var getDpr = () => { return Math.min(window.devicePixelRatio || 1, 2); }; var CANVAS_HTML_STR = `<canvas style="position:fixed;top:0;left:0;pointer-events:none;z-index:2147483646" aria-hidden="true"></canvas>`; var COLORS = [ [255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 165, 0], [128, 0, 128], [0, 128, 128], [255, 105, 180], [75, 0, 130], [255, 69, 0], [46, 139, 87], [220, 20, 60], [70, 130, 180] ]; var interactiveElements = [ "a", "button", "details", "embed", "input", "label", "menu", "menuitem", "object", "select", "textarea", "summary" ]; var interactiveRoles = [ "button", "menu", "menuitem", "link", "checkbox", "radio", "slider", "tab", "tabpanel", "textbox", "combobox", "grid", "listbox", "option", "progressbar", "scrollbar", "searchbox", "switch", "tree", "treeitem", "spinbutton", "tooltip", "a-button-inner", "a-dropdown-button", "click", "menuitemcheckbox", "menuitemradio", "a-button-text", "button-text", "button-icon", "button-icon-only", "button-text-icon-only", "dropdown", "combobox" ]; var interactiveEvents = [ "click", "mousedown", "mouseup", "touchstart", "touchend", "keydown", "keyup", "focus", "blur" ]; var isScrollable = (element) => { const isScrollable2 = element.hasAttribute("aria-scrollable") || element.hasAttribute("scrollable") || "style" in element && (element.style.overflow === "auto" || element.style.overflow === "scroll" || element.style.overflowY === "auto" || element.style.overflowY === "scroll" || element.style.overflowX === "auto" || element.style.overflowX === "scroll"); return isScrollable2; }; var isInteractive = (element) => { const fiber = getFiberFromHostInstance(element); if ((fiber == null ? void 0 : fiber.stateNode) instanceof Element) { for (const propName of Object.keys(fiber.memoizedProps || {})) { if (!propName.startsWith("on")) continue; const event = propName.slice(2).toLowerCase().replace(/capture$/, ""); if (!interactiveEvents.includes(event)) continue; if (fiber.memoizedProps[propName]) { return true; } } } for (const event of interactiveEvents) { const dotOnHandler = element[`on${event}`]; const explicitOnHandler = element.hasAttribute(`on${event}`); const ngClick = element.hasAttribute(`ng-${event}`); const atClick = element.hasAttribute(`@${event}`); const vOnClick = element.hasAttribute(`v-on:${event}`); if (dotOnHandler || explicitOnHandler || ngClick || atClick || vOnClick) { return true; } } const tagName = element.tagName.toLowerCase(); const role = element.getAttribute("role"); const ariaRole = element.getAttribute("aria-role"); const tabIndex = element.getAttribute("tabindex"); const hasInteractiveRole = interactiveElements.includes(tagName) || role && interactiveRoles.includes(role) || ariaRole && interactiveRoles.includes(ariaRole) || tabIndex !== null && tabIndex !== "-1"; const hasAriaProps = element.hasAttribute("aria-expanded") || element.hasAttribute("aria-pressed") || element.hasAttribute("aria-selected") || element.hasAttribute("aria-checked"); const isFormRelated = "form" in element && element.form !== void 0 || element.hasAttribute("contenteditable"); const isDraggable = "draggable" in element && element.draggable || element.getAttribute("draggable") === "true"; return hasInteractiveRole || isFormRelated || isDraggable || hasAriaProps; }; var isElementVisible = (element) => { const style = window.getComputedStyle(element); return element.offsetWidth > 0 && element.offsetHeight > 0 && style.visibility !== "hidden" && style.display !== "none"; }; var isTopElement = (element) => { const doc = element.ownerDocument; if (doc !== window.document) { return true; } const shadowRoot = element.getRootNode(); if (shadowRoot instanceof ShadowRoot) { const rect2 = element.getBoundingClientRect(); const point = { x: rect2.left + rect2.width / 2, y: rect2.top + rect2.height / 2 }; try { const topEl = shadowRoot.elementFromPoint(point.x, point.y); if (!topEl) return false; let current = topEl; while (current && current !== shadowRoot) { if (current === element) return true; current = current.parentElement; } return false; } catch (e) { return true; } } const rect = element.getBoundingClientRect(); const scrollX = window.scrollX; const scrollY = window.scrollY; const viewportTop = scrollY; const viewportLeft = scrollX; const viewportBottom = window.innerHeight + scrollY; const viewportRight = window.innerWidth + scrollX; const absTop = rect.top + scrollY; const absLeft = rect.left + scrollX; const absBottom = rect.bottom + scrollY; const absRight = rect.right + scrollX; if (absBottom < viewportTop || absTop > viewportBottom || absRight < viewportLeft || absLeft > viewportRight) { return false; } try { const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; const point = { x: centerX, y: centerY }; if (point.x < 0 || point.x >= window.innerWidth || point.y < 0 || point.y >= window.innerHeight) { return true; } const topEl = document.elementFromPoint(point.x, point.y); if (!topEl) return false; let current = topEl; while (current && current !== document.documentElement) { if (current === element) return true; current = current.parentElement; } return false; } catch (e) { return true; } }; var createShrinkwrap = () => { const draw = (elements) => __async(void 0, null, function* () { if (!ctx) return; const rectMap = yield getRectMap(elements); clear(); const drawnLabelBounds = []; let visibleCount = 0; const visibleIndices = /* @__PURE__ */ new Map(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; const COVERAGE_THRESHOLD = 0.97; for (let i = 0, len = elements.length; i < len; i++) { const element = elements[i]; const rect = rectMap.get(element); if (!rect) continue; const { width: width2, height: height2 } = rect; const x = rect.x; const y = rect.y; if (width2 / viewportWidth > COVERAGE_THRESHOLD && height2 / viewportHeight > COVERAGE_THRESHOLD) continue; const text = `${visibleCount + 1}`; const textSize = 16; ctx.textRendering = "optimizeSpeed"; ctx.font = `${textSize}px monospace`; const { width: textWidth } = ctx.measureText(text); let labelY = y - textSize - 4; if (labelY < 0) { labelY = 0; } const labelBounds = { x, y: labelY, width: textWidth + 4, height: textSize + 4 }; const hasCollision = drawnLabelBounds.some( (bound) => labelBounds.x < bound.x + bound.width && labelBounds.x + labelBounds.width > bound.x && labelBounds.y < bound.y + bound.height && labelBounds.y + labelBounds.height > bound.y ); if (!hasCollision) { drawnLabelBounds.push(labelBounds); visibleCount++; visibleIndices.set(element, visibleCount); ctx.beginPath(); ctx.rect(x, y, width2, height2); const color = COLORS[i % COLORS.length].join(","); ctx.fillStyle = `rgba(${color},0.1)`; ctx.strokeStyle = `rgba(${color})`; ctx.fill(); ctx.stroke(); ctx.fillStyle = `rgba(${color})`; ctx.fillRect(x, labelY, textWidth + 4, textSize + 4); ctx.fillStyle = "rgba(255,255,255)"; ctx.fillText(text, x + 2, labelY + textSize); } } return visibleIndices; }); const clear = () => { if (!ctx) return; ctx.clearRect(0, 0, canvas.width / dpr, canvas.height / dpr); }; const host = document.createElement("div"); host.setAttribute("data-react-scan", "true"); const root = host.attachShadow({ mode: "open" }); root.innerHTML = CANVAS_HTML_STR; const canvas = root.firstChild; let dpr = Math.min(window.devicePixelRatio || 1, 2); const { innerWidth, innerHeight } = window; canvas.style.width = `${innerWidth}px`; canvas.style.height = `${innerHeight}px`; const width = innerWidth * dpr; const height = innerHeight * dpr; canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d", { alpha: true }); if (ctx) { ctx.scale(dpr, dpr); } root.appendChild(canvas); document.documentElement.appendChild(host); let isResizeScheduled = false; const resizeHandler = () => { if (!isResizeScheduled) { isResizeScheduled = true; setTimeout(() => { const width2 = window.innerWidth; const height2 = window.innerHeight; dpr = getDpr(); canvas.style.width = `${width2}px`; canvas.style.height = `${height2}px`; canvas.width = width2 * dpr; canvas.height = height2 * dpr; if (ctx) { ctx.resetTransform(); ctx.scale(dpr, dpr); } shrinkwrap.trackInteractive(); isResizeScheduled = false; }); } }; let isScrollScheduled = false; const scrollHandler = () => { if (isScrollScheduled) return; isScrollScheduled = true; setTimeout(() => { requestAnimationFrame(() => { shrinkwrap.trackInteractive(); }); isScrollScheduled = false; }, 8); }; const fiberRoots = /* @__PURE__ */ new Set(); const shrinkwrap = { draw(elements) { draw(elements).then((visibleIndices) => { if (!visibleIndices) return; const elementMap = {}; visibleIndices.forEach((index, element) => { elementMap[index] = element; }); window.shrinkwrap = { elementMap }; }); }, trackInteractive() { instrument({ onCommitFiberRoot(_, root2) { fiberRoots.add(root2); const elements = []; for (const fiberRoot of fiberRoots) { traverseFiber(fiberRoot.current, (fiber) => { if (isHostFiber(fiber) && isInteractive(fiber.stateNode) && isElementVisible(fiber.stateNode) && isTopElement(fiber.stateNode)) { elements.push(fiber.stateNode); } }); } shrinkwrap.draw(elements); } }); }, cleanup() { fiberRoots.clear(); window.removeEventListener("scroll", scrollHandler); window.removeEventListener("resize", resizeHandler); document.documentElement.removeChild(host); } }; window.addEventListener("scroll", scrollHandler); window.addEventListener("resize", resizeHandler); return shrinkwrap; }; var getRectMap = (elements) => { return new Promise((resolve) => { const rects = /* @__PURE__ */ new Map(); const observer = new IntersectionObserver((entries) => { for (let i = 0, len = entries.length; i < len; i++) { const entry = entries[i]; const element = entry.target; const rect = entry.boundingClientRect; if (entry.isIntersecting && rect.width && rect.height) { rects.set(element, rect); } } observer.disconnect(); resolve(rects); }); for (let i = 0, len = elements.length; i < len; i++) { const element = elements[i]; observer.observe(element); } }); }; export { createShrinkwrap, getRectMap, isInteractive, isScrollable };