sidakram-bippy
Version:
hack into react internals
370 lines (366 loc) • 11.9 kB
JavaScript
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 };