Allow blocking elements by selector (#50)

* Extract method (isElementBlocked) and add tests

* Add blockSelector argument to snapshot

If blockSelector is passed, it will be matched against the element.

Reasoning: Mutating class names can get messy, so providing another hook
helps keep code clean by using data-attributes instead.
This commit is contained in:
Karl-Aksel Puulmann
2026-04-01 12:00:00 +08:00
committed by GitHub
parent 65671be739
commit 0c62d31002
4 changed files with 61 additions and 13 deletions

View File

@@ -34,12 +34,14 @@
"homepage": "https://github.com/rrweb-io/rrweb-snapshot#readme",
"devDependencies": {
"@types/chai": "^4.1.4",
"@types/jsdom": "^16.2.4",
"@types/mocha": "^5.2.5",
"@types/node": "^10.11.3",
"@types/puppeteer": "^1.12.4",
"chai": "^4.1.2",
"cross-env": "^5.2.0",
"jest-snapshot": "^23.6.0",
"jsdom": "^16.4.0",
"mocha": "^5.2.0",
"puppeteer": "^1.15.0",
"rollup": "^0.66.4",

View File

@@ -162,10 +162,34 @@ export function transformAttribute(
}
}
export function _isBlockedElement(
element: HTMLElement,
blockClass: string | RegExp,
blockSelector: string | null,
): boolean {
if (typeof blockClass === 'string') {
if (element.classList.contains(blockClass)) {
return true;
}
} else {
element.classList.forEach((className) => {
if (blockClass.test(className)) {
return true;
}
});
}
if (blockSelector) {
return element.matches(blockSelector)
}
return false;
}
function serializeNode(
n: Node,
doc: Document,
blockClass: string | RegExp,
blockSelector: string | null,
inlineStylesheet: boolean,
maskInputOptions: MaskInputOptions = {},
recordCanvas: boolean,
@@ -184,16 +208,7 @@ function serializeNode(
systemId: (n as DocumentType).systemId,
};
case n.ELEMENT_NODE:
let needBlock = false;
if (typeof blockClass === 'string') {
needBlock = (n as HTMLElement).classList.contains(blockClass);
} else {
(n as HTMLElement).classList.forEach((className) => {
if (blockClass.test(className)) {
needBlock = true;
}
});
}
const needBlock = _isBlockedElement(n as HTMLElement, blockClass, blockSelector);
const tagName = getValidTagName((n as HTMLElement).tagName);
let attributes: attributes = {};
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
@@ -406,6 +421,7 @@ export function serializeNodeWithId(
doc: Document,
map: idNodeMap,
blockClass: string | RegExp,
blockSelector: string | null,
skipChild = false,
inlineStylesheet = true,
maskInputOptions?: MaskInputOptions,
@@ -417,6 +433,7 @@ export function serializeNodeWithId(
n,
doc,
blockClass,
blockSelector,
inlineStylesheet,
maskInputOptions,
recordCanvas || false,
@@ -472,6 +489,7 @@ export function serializeNodeWithId(
doc,
map,
blockClass,
blockSelector,
skipChild,
inlineStylesheet,
maskInputOptions,
@@ -494,6 +512,7 @@ function snapshot(
maskAllInputsOrOptions: boolean | MaskInputOptions,
slimDOMSensibleOrOptions: boolean | SlimDOMOptions,
recordCanvas?: boolean,
blockSelector: string | null = null,
): [serializedNodeWithId | null, idNodeMap] {
const idNodeMap: idNodeMap = {};
const maskInputOptions: MaskInputOptions =
@@ -543,6 +562,7 @@ function snapshot(
n,
idNodeMap,
blockClass,
blockSelector,
false,
inlineStylesheet,
maskInputOptions,

View File

@@ -1,6 +1,7 @@
import 'mocha';
import { JSDOM } from 'jsdom';
import { expect } from 'chai';
import { absoluteToStylesheet } from '../src/snapshot';
import { absoluteToStylesheet, _isBlockedElement } from '../src/snapshot';
describe('absolute url to stylesheet', () => {
const href = 'http://localhost/css/style.css';
@@ -83,3 +84,27 @@ describe('absolute url to stylesheet', () => {
expect(absoluteToStylesheet(`url('')`, href)).to.equal(`url('')`);
});
});
describe('isBlockedElement()', () => {
const subject = (html: string, opt: any = {}) =>
_isBlockedElement(render(html), 'rr-block', opt.blockSelector)
const render = (html: string): HTMLElement =>
JSDOM.fragment(html).querySelector('div')!
it('can handle empty elements', () => {
expect(subject('<div />')).to.equal(false)
})
it('blocks prohibited className', () => {
expect(subject('<div class="foo rr-block bar" />')).to.equal(true)
})
it('does not block random data selector', () => {
expect(subject('<div data-rr-block />')).to.equal(false)
})
it('blocks blocked selector', () => {
expect(subject('<div data-rr-block />', { blockSelector: '[data-rr-block]' })).to.equal(true)
})
})

View File

@@ -3,8 +3,9 @@ export declare const IGNORED_NODE = -2;
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
export declare function absoluteToDoc(doc: Document, attributeValue: string): string;
export declare function transformAttribute(doc: Document, name: string, value: string): string;
export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null;
declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean): [serializedNodeWithId | null, idNodeMap];
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean;
export declare function serializeNodeWithId(n: Node | INode, doc: Document, map: idNodeMap, blockClass: string | RegExp, blockSelector: string | null, skipChild?: boolean, inlineStylesheet?: boolean, maskInputOptions?: MaskInputOptions, slimDOMOptions?: SlimDOMOptions, recordCanvas?: boolean, preserveWhiteSpace?: boolean): serializedNodeWithId | null;
declare function snapshot(n: Document, blockClass: string | RegExp | undefined, inlineStylesheet: boolean | undefined, maskAllInputsOrOptions: boolean | MaskInputOptions, slimDOMSensibleOrOptions: boolean | SlimDOMOptions, recordCanvas?: boolean, blockSelector?: string | null): [serializedNodeWithId | null, idNodeMap];
export declare function visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
export declare function cleanupSnapshot(): void;
export default snapshot;