import { idNodeMap, MaskInputFn, MaskInputOptions, nodeMetaMap, IMirror, serializedNodeWithId, } from './types'; export function isElement(n: Node): n is Element { return n.nodeType === n.ELEMENT_NODE; } export function isShadowRoot(n: Node): n is ShadowRoot { const host: Element | null = (n as ShadowRoot)?.host; return Boolean(host?.shadowRoot === n); } /** * To fix the issue https://github.com/rrweb-io/rrweb/issues/933. * Some websites use polyfilled shadow dom and this function is used to detect this situation. */ export function isNativeShadowDom(shadowRoot: ShadowRoot) { return Object.prototype.toString.call(shadowRoot) === '[object ShadowRoot]'; } export function getCssRulesString(s: CSSStyleSheet): string | null { try { const rules = s.rules || s.cssRules; return rules ? Array.from(rules).map(getCssRuleString).join('') : null; } catch (error) { return null; } } export function getCssRuleString(rule: CSSRule): string { let cssStringified = rule.cssText; if (isCSSImportRule(rule)) { try { cssStringified = getCssRulesString(rule.styleSheet) || cssStringified; } catch { // ignore } } return cssStringified; } export function isCSSImportRule(rule: CSSRule): rule is CSSImportRule { return 'styleSheet' in rule; } export class Mirror implements IMirror { private idNodeMap: idNodeMap = new Map(); private nodeMetaMap: nodeMetaMap = new WeakMap(); getId(n: Node | undefined | null): number { if (!n) return -1; const id = this.getMeta(n)?.id; // if n is not a serialized Node, use -1 as its id. return id ?? -1; } getNode(id: number): Node | null { return this.idNodeMap.get(id) || null; } getIds(): number[] { return Array.from(this.idNodeMap.keys()); } getMeta(n: Node): serializedNodeWithId | null { return this.nodeMetaMap.get(n) || null; } // removes the node from idNodeMap // doesn't remove the node from nodeMetaMap removeNodeFromMap(n: Node) { const id = this.getId(n); this.idNodeMap.delete(id); if (n.childNodes) { n.childNodes.forEach((childNode) => this.removeNodeFromMap((childNode as unknown) as Node), ); } } has(id: number): boolean { return this.idNodeMap.has(id); } hasNode(node: Node): boolean { return this.nodeMetaMap.has(node); } add(n: Node, meta: serializedNodeWithId) { const id = meta.id; this.idNodeMap.set(id, n); this.nodeMetaMap.set(n, meta); } replace(id: number, n: Node) { const oldNode = this.getNode(id); if (oldNode) { const meta = this.nodeMetaMap.get(oldNode); if (meta) this.nodeMetaMap.set(n, meta); } this.idNodeMap.set(id, n); } reset() { this.idNodeMap = new Map(); this.nodeMetaMap = new WeakMap(); } } export function createMirror(): Mirror { return new Mirror(); } export function maskInputValue({ maskInputOptions, tagName, type, value, maskInputFn, }: { maskInputOptions: MaskInputOptions; tagName: string; type: string | number | boolean | null; value: string | null; maskInputFn?: MaskInputFn; }): string { let text = value || ''; if ( maskInputOptions[tagName.toLowerCase() as keyof MaskInputOptions] || maskInputOptions[type as keyof MaskInputOptions] ) { if (maskInputFn) { text = maskInputFn(text); } else { text = '*'.repeat(text.length); } } return text; } const ORIGINAL_ATTRIBUTE_NAME = '__rrweb_original__'; type PatchedGetImageData = { [ORIGINAL_ATTRIBUTE_NAME]: CanvasImageData['getImageData']; } & CanvasImageData['getImageData']; export function is2DCanvasBlank(canvas: HTMLCanvasElement): boolean { const ctx = canvas.getContext('2d'); if (!ctx) return true; const chunkSize = 50; // get chunks of the canvas and check if it is blank for (let x = 0; x < canvas.width; x += chunkSize) { for (let y = 0; y < canvas.height; y += chunkSize) { // eslint-disable-next-line @typescript-eslint/unbound-method const getImageData = ctx.getImageData as PatchedGetImageData; const originalGetImageData = ORIGINAL_ATTRIBUTE_NAME in getImageData ? getImageData[ORIGINAL_ATTRIBUTE_NAME] : getImageData; // by getting the canvas in chunks we avoid an expensive // `getImageData` call that retrieves everything // even if we can already tell from the first chunk(s) that // the canvas isn't blank const pixelBuffer = new Uint32Array( // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access originalGetImageData.call( ctx, x, y, Math.min(chunkSize, canvas.width - x), Math.min(chunkSize, canvas.height - y), ).data.buffer, ); if (pixelBuffer.some((pixel) => pixel !== 0)) return false; } } return true; }