diff --git a/es/rrweb/packages/rrweb/src/replay/index.js b/es/rrweb/packages/rrweb/src/replay/index.js index 0d49411b1f6d31103bed898c0e81d1d74ab51234..0b2160ef08aa3ae5310f63c295abc0a560332b22 100644 --- a/es/rrweb/packages/rrweb/src/replay/index.js +++ b/es/rrweb/packages/rrweb/src/replay/index.js @@ -203,6 +203,10 @@ class Replayer { mouseTail: defaultMouseTailConfig, useVirtualDom: true, logger: console, + onError: (e) => { + // maintain the existing behaviour of throwing if no handler is provided + throw e; + }, }; this.config = Object.assign({}, defaultConfig, config); this.handleResize = this.handleResize.bind(this); @@ -755,255 +759,259 @@ class Replayer { applyIncremental(e, isSync) { var _a, _b, _c; const { data: d } = e; - switch (d.source) { - case IncrementalSource.Mutation: { - try { - this.applyMutation(d, isSync); - } - catch (error) { - this.warn(`Exception in mutation ${error.message || error}`, d); - } - break; - } - case IncrementalSource.Drag: - case IncrementalSource.TouchMove: - case IncrementalSource.MouseMove: - if (isSync) { - const lastPosition = d.positions[d.positions.length - 1]; - this.mousePos = { - x: lastPosition.x, - y: lastPosition.y, - id: lastPosition.id, - debugData: d, - }; + try { + switch (d.source) { + case IncrementalSource.Mutation: { + try { + this.applyMutation(d, isSync); + } + catch (error) { + this.warn(`Exception in mutation ${error.message || error}`, d); + } + break; } - else { - d.positions.forEach((p) => { - const action = { - doAction: () => { - this.moveAndHover(p.x, p.y, p.id, isSync, d); - }, - delay: p.timeOffset + - e.timestamp - - this.service.state.context.baselineTime, + case IncrementalSource.Drag: + case IncrementalSource.TouchMove: + case IncrementalSource.MouseMove: + if (isSync) { + const lastPosition = d.positions[d.positions.length - 1]; + this.mousePos = { + x: lastPosition.x, + y: lastPosition.y, + id: lastPosition.id, + debugData: d, }; - this.timer.addAction(action); - }); - this.timer.addAction({ - doAction() { - }, - delay: e.delay - ((_a = d.positions[0]) === null || _a === void 0 ? void 0 : _a.timeOffset), - }); - } - break; - case IncrementalSource.MouseInteraction: { - if (d.id === -1) { + } + else { + d.positions.forEach((p) => { + const action = { + doAction: () => { + this.moveAndHover(p.x, p.y, p.id, isSync, d); + }, + delay: p.timeOffset + + e.timestamp - + this.service.state.context.baselineTime, + }; + this.timer.addAction(action); + }); + this.timer.addAction({ + doAction() { + }, + delay: e.delay - ((_a = d.positions[0]) === null || _a === void 0 ? void 0 : _a.timeOffset), + }); + } break; - } - const event = new Event(toLowerCase(MouseInteractions[d.type])); - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - this.emitter.emit(ReplayerEvents.MouseInteraction, { - type: d.type, - target, - }); - const { triggerFocus } = this.config; - switch (d.type) { - case MouseInteractions.Blur: - if ('blur' in target) { - target.blur(); - } - break; - case MouseInteractions.Focus: - if (triggerFocus && target.focus) { - target.focus({ - preventScroll: true, - }); - } + case IncrementalSource.MouseInteraction: { + if (d.id === -1) { break; - case MouseInteractions.Click: - case MouseInteractions.TouchStart: - case MouseInteractions.TouchEnd: - case MouseInteractions.MouseDown: - case MouseInteractions.MouseUp: - if (isSync) { - if (d.type === MouseInteractions.TouchStart) { - this.touchActive = true; - } - else if (d.type === MouseInteractions.TouchEnd) { - this.touchActive = false; - } - if (d.type === MouseInteractions.MouseDown) { - this.lastMouseDownEvent = [target, event]; - } - else if (d.type === MouseInteractions.MouseUp) { - this.lastMouseDownEvent = null; + } + const event = new Event(toLowerCase(MouseInteractions[d.type])); + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + this.emitter.emit(ReplayerEvents.MouseInteraction, { + type: d.type, + target, + }); + const { triggerFocus } = this.config; + switch (d.type) { + case MouseInteractions.Blur: + if ('blur' in target) { + target.blur(); } - this.mousePos = { - x: d.x || 0, - y: d.y || 0, - id: d.id, - debugData: d, - }; - } - else { - if (d.type === MouseInteractions.TouchStart) { - this.tailPositions.length = 0; + break; + case MouseInteractions.Focus: + if (triggerFocus && target.focus) { + target.focus({ + preventScroll: true, + }); } - this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d); - if (d.type === MouseInteractions.Click) { - this.mouse.classList.remove('active'); - void this.mouse.offsetWidth; - this.mouse.classList.add('active'); + break; + case MouseInteractions.Click: + case MouseInteractions.TouchStart: + case MouseInteractions.TouchEnd: + case MouseInteractions.MouseDown: + case MouseInteractions.MouseUp: + if (isSync) { + if (d.type === MouseInteractions.TouchStart) { + this.touchActive = true; + } + else if (d.type === MouseInteractions.TouchEnd) { + this.touchActive = false; + } + if (d.type === MouseInteractions.MouseDown) { + this.lastMouseDownEvent = [target, event]; + } + else if (d.type === MouseInteractions.MouseUp) { + this.lastMouseDownEvent = null; + } + this.mousePos = { + x: d.x || 0, + y: d.y || 0, + id: d.id, + debugData: d, + }; } - else if (d.type === MouseInteractions.TouchStart) { - void this.mouse.offsetWidth; - this.mouse.classList.add('touch-active'); + else { + if (d.type === MouseInteractions.TouchStart) { + this.tailPositions.length = 0; + } + this.moveAndHover(d.x || 0, d.y || 0, d.id, isSync, d); + if (d.type === MouseInteractions.Click) { + this.mouse.classList.remove('active'); + void this.mouse.offsetWidth; + this.mouse.classList.add('active'); + } + else if (d.type === MouseInteractions.TouchStart) { + void this.mouse.offsetWidth; + this.mouse.classList.add('touch-active'); + } + else if (d.type === MouseInteractions.TouchEnd) { + this.mouse.classList.remove('touch-active'); + } + else { + target.dispatchEvent(event); + } } - else if (d.type === MouseInteractions.TouchEnd) { - this.mouse.classList.remove('touch-active'); + break; + case MouseInteractions.TouchCancel: + if (isSync) { + this.touchActive = false; } else { - target.dispatchEvent(event); + this.mouse.classList.remove('touch-active'); } - } + break; + default: + target.dispatchEvent(event); + } + break; + } + case IncrementalSource.Scroll: { + if (d.id === -1) { break; - case MouseInteractions.TouchCancel: - if (isSync) { - this.touchActive = false; - } - else { - this.mouse.classList.remove('touch-active'); + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); } + target.scrollData = d; break; - default: - target.dispatchEvent(event); - } - break; - } - case IncrementalSource.Scroll: { - if (d.id === -1) { - break; - } - if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); } - target.scrollData = d; + this.applyScroll(d, isSync); break; } - this.applyScroll(d, isSync); - break; - } - case IncrementalSource.ViewportResize: - this.emitter.emit(ReplayerEvents.Resize, { - width: d.width, - height: d.height, - }); - break; - case IncrementalSource.Input: { - if (d.id === -1) { + case IncrementalSource.ViewportResize: + this.emitter.emit(ReplayerEvents.Resize, { + width: d.width, + height: d.height, + }); break; - } - if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); + case IncrementalSource.Input: { + if (d.id === -1) { + break; } - target.inputData = d; + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.inputData = d; + break; + } + this.applyInput(d); break; } - this.applyInput(d); - break; - } - case IncrementalSource.MediaInteraction: { - const target = this.usingVirtualDom - ? this.virtualDom.mirror.getNode(d.id) - : this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); - } - const mediaEl = target; - const { events } = this.service.state.context; - this.mediaManager.mediaMutation({ - target: mediaEl, - timeOffset: e.timestamp - events[0].timestamp, - mutation: d, - }); - break; - } - case IncrementalSource.StyleSheetRule: - case IncrementalSource.StyleDeclaration: { - if (this.usingVirtualDom) { - if (d.styleId) - this.constructedStyleMutations.push(d); - else if (d.id) - (_b = this.virtualDom.mirror.getNode(d.id)) === null || _b === void 0 ? void 0 : _b.rules.push(d); - } - else - this.applyStyleSheetMutation(d); - break; - } - case IncrementalSource.CanvasMutation: { - if (!this.config.UNSAFE_replayCanvas) { - return; - } - if (this.usingVirtualDom) { - const target = this.virtualDom.mirror.getNode(d.id); + case IncrementalSource.MediaInteraction: { + const target = this.usingVirtualDom + ? this.virtualDom.mirror.getNode(d.id) + : this.mirror.getNode(d.id); if (!target) { return this.debugNodeNotFound(d, d.id); } - target.canvasMutations.push({ - event: e, + const mediaEl = target; + const { events } = this.service.state.context; + this.mediaManager.mediaMutation({ + target: mediaEl, + timeOffset: e.timestamp - events[0].timestamp, mutation: d, }); + break; } - else { - const target = this.mirror.getNode(d.id); - if (!target) { - return this.debugNodeNotFound(d, d.id); + case IncrementalSource.StyleSheetRule: + case IncrementalSource.StyleDeclaration: { + if (this.usingVirtualDom) { + if (d.styleId) + this.constructedStyleMutations.push(d); + else if (d.id) + (_b = this.virtualDom.mirror.getNode(d.id)) === null || _b === void 0 ? void 0 : _b.rules.push(d); } - void canvasMutation({ - event: e, - mutation: d, - target: target, - imageMap: this.imageMap, - canvasEventMap: this.canvasEventMap, - errorHandler: this.warnCanvasMutationFailed.bind(this), - }); + else + this.applyStyleSheetMutation(d); + break; } - break; - } - case IncrementalSource.Font: { - try { - const fontFace = new FontFace(d.family, d.buffer - ? new Uint8Array(JSON.parse(d.fontSource)) - : d.fontSource, d.descriptors); - (_c = this.iframe.contentDocument) === null || _c === void 0 ? void 0 : _c.fonts.add(fontFace); + case IncrementalSource.CanvasMutation: { + if (!this.config.UNSAFE_replayCanvas) { + return; + } + if (this.usingVirtualDom) { + const target = this.virtualDom.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + target.canvasMutations.push({ + event: e, + mutation: d, + }); + } + else { + const target = this.mirror.getNode(d.id); + if (!target) { + return this.debugNodeNotFound(d, d.id); + } + void canvasMutation({ + event: e, + mutation: d, + target: target, + imageMap: this.imageMap, + canvasEventMap: this.canvasEventMap, + errorHandler: this.warnCanvasMutationFailed.bind(this), + }); + } + break; } - catch (error) { - this.warn(error); + case IncrementalSource.Font: { + try { + const fontFace = new FontFace(d.family, d.buffer + ? new Uint8Array(JSON.parse(d.fontSource)) + : d.fontSource, d.descriptors); + (_c = this.iframe.contentDocument) === null || _c === void 0 ? void 0 : _c.fonts.add(fontFace); + } + catch (error) { + this.warn(error); + } + break; } - break; - } - case IncrementalSource.Selection: { - if (isSync) { - this.lastSelectionData = d; + case IncrementalSource.Selection: { + if (isSync) { + this.lastSelectionData = d; + break; + } + this.applySelection(d); + break; + } + case IncrementalSource.AdoptedStyleSheet: { + if (this.usingVirtualDom) + this.adoptedStyleSheets.push(d); + else + this.applyAdoptedStyleSheet(d); break; } - this.applySelection(d); - break; - } - case IncrementalSource.AdoptedStyleSheet: { - if (this.usingVirtualDom) - this.adoptedStyleSheets.push(d); - else - this.applyAdoptedStyleSheet(d); - break; } + } catch (error) { + this.config.onError(error); } } applyMutation(d, isSync) { diff --git a/es/rrweb/packages/rrweb/src/replay/media/index.js b/es/rrweb/packages/rrweb/src/replay/media/index.js index 22fee601e786c1d8dfb5c01d2e359c8bcbac7c42..6d80d33030c601fa74f1b95573cb1590c3a0116b 100644 --- a/es/rrweb/packages/rrweb/src/replay/media/index.js +++ b/es/rrweb/packages/rrweb/src/replay/media/index.js @@ -21,7 +21,7 @@ class MediaManager { this.mediaMap.forEach((mediaState, target) => { this.syncTargetWithState(target); if (options.pause) { - target.pause(); + target?.pause(); } }); } @@ -49,7 +49,7 @@ class MediaManager { target.currentTime = seekToTime; } else { - target.pause(); + target?.pause(); target.currentTime = mediaState.currentTimeAtLastInteraction; } } @@ -117,7 +117,7 @@ class MediaManager { void target.play(); } else { - target.pause(); + target?.pause(); } } catch (error) { @@ -141,7 +141,7 @@ class MediaManager { isPlaying = target.getAttribute('autoplay') !== null; } if (isPlaying && playerIsPaused) - target.pause(); + target?.pause(); let playbackRate = 1; if (typeof mediaAttributes.rr_mediaPlaybackRate === 'number') { playbackRate = mediaAttributes.rr_mediaPlaybackRate; @@ -180,6 +180,8 @@ class MediaManager { this.syncTargetWithState(target); } mediaMutation({ target, timeOffset, mutation, }) { + if (!['AUDIO', 'VIDEO'].includes(target.nodeName)) + return; this.mediaMap.set(target, this.getMediaStateFromMutation({ target, timeOffset, diff --git a/es/rrweb/packages/rrweb-snapshot/es/css.js b/es/rrweb/packages/rrweb-snapshot/es/css.js new file mode 100644 index 0000000000000000000000000000000000000000..0a6c5930c017852afd3d7f0170f6eca821619dd3 --- /dev/null +++ b/es/rrweb/packages/rrweb-snapshot/es/css.js @@ -0,0 +1,87 @@ +import postcss from '../../../../../../../../../postcss/lib/postcss.js' + +const MEDIA_SELECTOR = /(max|min)-device-(width|height)/ +const MEDIA_SELECTOR_GLOBAL = new RegExp(MEDIA_SELECTOR.source, 'g') + +export const mutate = (cssText) => { + const ast = postcss([mediaSelectorPlugin, pseudoClassPlugin]).process(cssText) + return ast.css +} + +const mediaSelectorPlugin = { + postcssPlugin: 'postcss-custom-selectors', + prepare() { + return { + postcssPlugin: 'postcss-custom-selectors', + AtRule: function (atrule) { + if (atrule.params.match(MEDIA_SELECTOR_GLOBAL)) { + atrule.params = atrule.params.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2') + } + }, + } + }, +} + +// Adapted from https://github.com/giuseppeg/postcss-pseudo-classes/blob/master/index.js +const pseudoClassPlugin = { + postcssPlugin: 'postcss-hover-classes', + prepare: function () { + const fixed = [] + return { + Rule: function (rule) { + if (fixed.indexOf(rule) !== -1) { + return + } + fixed.push(rule) + + rule.selectors.forEach(function (selector) { + if (!selector.includes(':')) { + return + } + + const selectorParts = selector.replace(/\n/g, ' ').split(' ') + const pseudoedSelectorParts = [] + + selectorParts.forEach(function (selectorPart) { + const pseudos = selectorPart.match(/::?([^:]+)/g) + + if (!pseudos) { + pseudoedSelectorParts.push(selectorPart) + return + } + + const baseSelector = selectorPart.substr(0, selectorPart.length - pseudos.join('').length) + + const classPseudos = pseudos.map(function (pseudo) { + const pseudoToCheck = pseudo.replace(/\(.*/g, '') + if (pseudoToCheck !== ':hover') { + return pseudo + } + + // Ignore pseudo-elements! + if (pseudo.match(/^::/)) { + return pseudo + } + + // Kill the colon + pseudo = pseudo.substr(1) + + // Replace left and right parens + pseudo = pseudo.replace(/\(/g, '\\(') + pseudo = pseudo.replace(/\)/g, '\\)') + + return '.' + '\\:' + pseudo + }) + + pseudoedSelectorParts.push(baseSelector + classPseudos.join('')) + }) + + const newSelector = pseudoedSelectorParts.join(' ') + if (newSelector && newSelector !== selector) { + rule.selector += ',\n' + newSelector + } + }) + }, + } + }, +} diff --git a/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js b/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js index 38a23aaae8d683fa584329eced277dd8de55d1ff..8aeee467a3bab9baeefb1a97f2b131bedbd0fa3c 100644 --- a/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js +++ b/es/rrweb/packages/rrweb-snapshot/es/rrweb-snapshot.js @@ -1,3 +1,5 @@ +import {mutate} from './css.js'; + var NodeType; (function (NodeType) { NodeType[NodeType["Document"] = 0] = "Document"; @@ -1255,54 +1257,19 @@ function parse(css, options = {}) { }); } function selector() { - whitespace(); - while (css[0] == '}') { - error('extra closing bracket'); - css = css.slice(1); - whitespace(); - } - const m = match(/^(("(?:\\"|[^"])*"|'(?:\\'|[^'])*'|[^{])+)/); + const m = match(/^([^{]+)/); if (!m) { return; } - const cleanedInput = m[0] - .trim() + return trim(m[0]) .replace(/\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*\/+/g, '') .replace(/"(?:\\"|[^"])*"|'(?:\\'|[^'])*'/g, (m) => { - return m.replace(/,/g, '\u200C'); - }); - return customSplit(cleanedInput).map((s) => s.replace(/\u200C/g, ',').trim()); - } - function customSplit(input) { - const result = []; - let currentSegment = ''; - let depthParentheses = 0; - let depthBrackets = 0; - for (const char of input) { - if (char === '(') { - depthParentheses++; - } - else if (char === ')') { - depthParentheses--; - } - else if (char === '[') { - depthBrackets++; - } - else if (char === ']') { - depthBrackets--; - } - if (char === ',' && depthParentheses === 0 && depthBrackets === 0) { - result.push(currentSegment); - currentSegment = ''; - } - else { - currentSegment += char; - } - } - if (currentSegment) { - result.push(currentSegment); - } - return result; + return m.replace(/,/g, '\u200C'); + }) + .split(/\s*(?![^(]*\)),\s*/) + .map((s) => { + return s.replace(/\u200C/g, ','); + }); } function declaration() { const pos = position(); @@ -1662,56 +1629,60 @@ function adaptCssForReplay(cssText, cache) { const cachedStyle = cache === null || cache === void 0 ? void 0 : cache.stylesWithHoverClass.get(cssText); if (cachedStyle) return cachedStyle; - const ast = parse(cssText, { - silent: true, - }); - if (!ast.stylesheet) { - return cssText; - } - const selectors = []; - const medias = []; - function getSelectors(rule) { - if ('selectors' in rule && rule.selectors) { - rule.selectors.forEach((selector) => { - if (HOVER_SELECTOR.test(selector)) { - selectors.push(selector); - } - }); + let result = cssText; + try { + result = mutate(cssText) + } catch (error) { + const ast = parse(cssText, { + silent: true, + }); + if (!ast.stylesheet) { + return cssText; + } + const selectors = []; + const medias = []; + function getSelectors(rule) { + if ('selectors' in rule && rule.selectors) { + rule.selectors.forEach((selector) => { + if (HOVER_SELECTOR.test(selector)) { + selectors.push(selector); + } + }); + } + if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) { + medias.push(rule.media); + } + if ('rules' in rule && rule.rules) { + rule.rules.forEach(getSelectors); + } } - if ('media' in rule && rule.media && MEDIA_SELECTOR.test(rule.media)) { - medias.push(rule.media); + getSelectors(ast.stylesheet); + if (selectors.length > 0) { + const selectorMatcher = new RegExp(selectors + .filter((selector, index) => selectors.indexOf(selector) === index) + .sort((a, b) => b.length - a.length) + .map((selector) => { + return escapeRegExp(selector); + }) + .join('|'), 'g'); + result = result.replace(selectorMatcher, (selector) => { + const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover'); + return `${selector}, ${newSelector}`; + }); } - if ('rules' in rule && rule.rules) { - rule.rules.forEach(getSelectors); + if (medias.length > 0) { + const mediaMatcher = new RegExp(medias + .filter((media, index) => medias.indexOf(media) === index) + .sort((a, b) => b.length - a.length) + .map((media) => { + return escapeRegExp(media); + }) + .join('|'), 'g'); + result = result.replace(mediaMatcher, (media) => { + return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2'); + }); } } - getSelectors(ast.stylesheet); - let result = cssText; - if (selectors.length > 0) { - const selectorMatcher = new RegExp(selectors - .filter((selector, index) => selectors.indexOf(selector) === index) - .sort((a, b) => b.length - a.length) - .map((selector) => { - return escapeRegExp(selector); - }) - .join('|'), 'g'); - result = result.replace(selectorMatcher, (selector) => { - const newSelector = selector.replace(HOVER_SELECTOR_GLOBAL, '$1.\\:hover'); - return `${selector}, ${newSelector}`; - }); - } - if (medias.length > 0) { - const mediaMatcher = new RegExp(medias - .filter((media, index) => medias.indexOf(media) === index) - .sort((a, b) => b.length - a.length) - .map((media) => { - return escapeRegExp(media); - }) - .join('|'), 'g'); - result = result.replace(mediaMatcher, (media) => { - return media.replace(MEDIA_SELECTOR_GLOBAL, '$1-$2'); - }); - } cache === null || cache === void 0 ? void 0 : cache.stylesWithHoverClass.set(cssText, result); return result; }