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:
committed by
GitHub
parent
65671be739
commit
0c62d31002
@@ -34,12 +34,14 @@
|
|||||||
"homepage": "https://github.com/rrweb-io/rrweb-snapshot#readme",
|
"homepage": "https://github.com/rrweb-io/rrweb-snapshot#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.1.4",
|
"@types/chai": "^4.1.4",
|
||||||
|
"@types/jsdom": "^16.2.4",
|
||||||
"@types/mocha": "^5.2.5",
|
"@types/mocha": "^5.2.5",
|
||||||
"@types/node": "^10.11.3",
|
"@types/node": "^10.11.3",
|
||||||
"@types/puppeteer": "^1.12.4",
|
"@types/puppeteer": "^1.12.4",
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"jest-snapshot": "^23.6.0",
|
"jest-snapshot": "^23.6.0",
|
||||||
|
"jsdom": "^16.4.0",
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"puppeteer": "^1.15.0",
|
"puppeteer": "^1.15.0",
|
||||||
"rollup": "^0.66.4",
|
"rollup": "^0.66.4",
|
||||||
|
|||||||
@@ -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(
|
function serializeNode(
|
||||||
n: Node,
|
n: Node,
|
||||||
doc: Document,
|
doc: Document,
|
||||||
blockClass: string | RegExp,
|
blockClass: string | RegExp,
|
||||||
|
blockSelector: string | null,
|
||||||
inlineStylesheet: boolean,
|
inlineStylesheet: boolean,
|
||||||
maskInputOptions: MaskInputOptions = {},
|
maskInputOptions: MaskInputOptions = {},
|
||||||
recordCanvas: boolean,
|
recordCanvas: boolean,
|
||||||
@@ -184,16 +208,7 @@ function serializeNode(
|
|||||||
systemId: (n as DocumentType).systemId,
|
systemId: (n as DocumentType).systemId,
|
||||||
};
|
};
|
||||||
case n.ELEMENT_NODE:
|
case n.ELEMENT_NODE:
|
||||||
let needBlock = false;
|
const needBlock = _isBlockedElement(n as HTMLElement, blockClass, blockSelector);
|
||||||
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 tagName = getValidTagName((n as HTMLElement).tagName);
|
const tagName = getValidTagName((n as HTMLElement).tagName);
|
||||||
let attributes: attributes = {};
|
let attributes: attributes = {};
|
||||||
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
|
for (const { name, value } of Array.from((n as HTMLElement).attributes)) {
|
||||||
@@ -406,6 +421,7 @@ export function serializeNodeWithId(
|
|||||||
doc: Document,
|
doc: Document,
|
||||||
map: idNodeMap,
|
map: idNodeMap,
|
||||||
blockClass: string | RegExp,
|
blockClass: string | RegExp,
|
||||||
|
blockSelector: string | null,
|
||||||
skipChild = false,
|
skipChild = false,
|
||||||
inlineStylesheet = true,
|
inlineStylesheet = true,
|
||||||
maskInputOptions?: MaskInputOptions,
|
maskInputOptions?: MaskInputOptions,
|
||||||
@@ -417,6 +433,7 @@ export function serializeNodeWithId(
|
|||||||
n,
|
n,
|
||||||
doc,
|
doc,
|
||||||
blockClass,
|
blockClass,
|
||||||
|
blockSelector,
|
||||||
inlineStylesheet,
|
inlineStylesheet,
|
||||||
maskInputOptions,
|
maskInputOptions,
|
||||||
recordCanvas || false,
|
recordCanvas || false,
|
||||||
@@ -472,6 +489,7 @@ export function serializeNodeWithId(
|
|||||||
doc,
|
doc,
|
||||||
map,
|
map,
|
||||||
blockClass,
|
blockClass,
|
||||||
|
blockSelector,
|
||||||
skipChild,
|
skipChild,
|
||||||
inlineStylesheet,
|
inlineStylesheet,
|
||||||
maskInputOptions,
|
maskInputOptions,
|
||||||
@@ -494,6 +512,7 @@ function snapshot(
|
|||||||
maskAllInputsOrOptions: boolean | MaskInputOptions,
|
maskAllInputsOrOptions: boolean | MaskInputOptions,
|
||||||
slimDOMSensibleOrOptions: boolean | SlimDOMOptions,
|
slimDOMSensibleOrOptions: boolean | SlimDOMOptions,
|
||||||
recordCanvas?: boolean,
|
recordCanvas?: boolean,
|
||||||
|
blockSelector: string | null = null,
|
||||||
): [serializedNodeWithId | null, idNodeMap] {
|
): [serializedNodeWithId | null, idNodeMap] {
|
||||||
const idNodeMap: idNodeMap = {};
|
const idNodeMap: idNodeMap = {};
|
||||||
const maskInputOptions: MaskInputOptions =
|
const maskInputOptions: MaskInputOptions =
|
||||||
@@ -543,6 +562,7 @@ function snapshot(
|
|||||||
n,
|
n,
|
||||||
idNodeMap,
|
idNodeMap,
|
||||||
blockClass,
|
blockClass,
|
||||||
|
blockSelector,
|
||||||
false,
|
false,
|
||||||
inlineStylesheet,
|
inlineStylesheet,
|
||||||
maskInputOptions,
|
maskInputOptions,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'mocha';
|
import 'mocha';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
import { absoluteToStylesheet } from '../src/snapshot';
|
import { absoluteToStylesheet, _isBlockedElement } from '../src/snapshot';
|
||||||
|
|
||||||
describe('absolute url to stylesheet', () => {
|
describe('absolute url to stylesheet', () => {
|
||||||
const href = 'http://localhost/css/style.css';
|
const href = 'http://localhost/css/style.css';
|
||||||
@@ -83,3 +84,27 @@ describe('absolute url to stylesheet', () => {
|
|||||||
expect(absoluteToStylesheet(`url('')`, href)).to.equal(`url('')`);
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
5
typings/snapshot.d.ts
vendored
5
typings/snapshot.d.ts
vendored
@@ -3,8 +3,9 @@ export declare const IGNORED_NODE = -2;
|
|||||||
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
|
export declare function absoluteToStylesheet(cssText: string | null, href: string): string;
|
||||||
export declare function absoluteToDoc(doc: Document, attributeValue: 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 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;
|
export declare function _isBlockedElement(element: HTMLElement, blockClass: string | RegExp, blockSelector: string | null): boolean;
|
||||||
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 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 visitSnapshot(node: serializedNodeWithId, onVisit: (node: serializedNodeWithId) => unknown): void;
|
||||||
export declare function cleanupSnapshot(): void;
|
export declare function cleanupSnapshot(): void;
|
||||||
export default snapshot;
|
export default snapshot;
|
||||||
|
|||||||
Reference in New Issue
Block a user