diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOMManager.ts index 1d723a923..cd5e77b02 100644 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.ts +++ b/frontend/app/player/MessageDistributor/managers/DOMManager.ts @@ -3,7 +3,7 @@ import type { Message, SetNodeScroll, CreateElementNode } from '../messages'; import type { TimedMessage } from '../Timed'; import logger from 'App/logger'; -import StylesManager from './StylesManager'; +import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; import ListWalker from './ListWalker'; import type { Timed }from '../Timed'; @@ -147,6 +147,7 @@ export default class DOMManager extends ListWalker { this.insertNode(msg); break; case "create_element_node": + // console.log('elementnode', msg) if (msg.svg) { this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag); } else { @@ -207,9 +208,14 @@ export default class DOMManager extends ListWalker { break; case "set_node_data": case "set_css_data": - if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); break; } + node = this.nl[ msg.id ] + if (!node) { logger.error("Node not found", msg); break; } // @ts-ignore - this.nl[ msg.id ].data = msg.data; + node.data = msg.data; + if (node instanceof HTMLStyleElement) { + const doc = this.screen.document + doc && rewriteNodeStyleSheet(doc, node) + } break; case "css_insert_rule": node = this.nl[ msg.id ]; @@ -239,6 +245,23 @@ export default class DOMManager extends ListWalker { } catch (e) { logger.warn(e, msg) } + break; + case "create_i_frame_document": + // console.log('ifr', msg) + node = this.nl[ msg.frameID ]; + if (!(node instanceof HTMLIFrameElement)) { + logger.warn("create_i_frame_document message. Node is not iframe") + return; + } + console.log("iframe", msg) + // await new Promise(resolve => { node.onload = resolve }) + + const doc = node.contentDocument; + if (!doc) { + logger.warn("No iframe doc", msg, node, node.contentDocument); + return; + } + this.nl[ msg.id ] = doc.documentElement break; //not sure what to do with this one //case "disconnected": diff --git a/frontend/app/player/MessageDistributor/managers/StylesManager.ts b/frontend/app/player/MessageDistributor/managers/StylesManager.ts new file mode 100644 index 000000000..389533368 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/StylesManager.ts @@ -0,0 +1,109 @@ + +import type StatedScreen from '../StatedScreen'; +import type { CssInsertRule, CssDeleteRule } from '../messages'; +import type { Timed } from '../Timed'; + +type CSSRuleMessage = CssInsertRule | CssDeleteRule; +type TimedCSSRuleMessage = Timed & CSSRuleMessage; + +import logger from 'App/logger'; +import ListWalker from './ListWalker'; + + +const HOVER_CN = "-openreplay-hover"; +const HOVER_SELECTOR = `.${HOVER_CN}`; + +// Doesn't work with css files (hasOwnProperty) +export function rewriteNodeStyleSheet(doc: Document, node: HTMLLinkElement | HTMLStyleElement) { + const ss = Object.values(doc.styleSheets).find(s => s.ownerNode === node); + if (!ss || !ss.hasOwnProperty('rules')) { return; } + for(let i = 0; i < ss.rules.length; i++){ + const r = ss.rules[i] + if (r instanceof CSSStyleRule) { + r.selectorText = r.selectorText.replace('/\:hover/g', HOVER_SELECTOR) + } + } +} + +export default class StylesManager extends ListWalker { + private linkLoadingCount: number = 0; + private linkLoadPromises: Array> = []; + private skipCSSLinks: Array = []; // should be common for all pages + + constructor(private readonly screen: StatedScreen) { + super(); + } + + reset():void { + super.reset(); + this.linkLoadingCount = 0; + this.linkLoadPromises = []; + + //cancel all promises? tothinkaboutit + } + + setStyleHandlers(node: HTMLLinkElement, value: string): void { + let timeoutId; + const promise = new Promise((resolve) => { + if (this.skipCSSLinks.includes(value)) resolve(null); + this.linkLoadingCount++; + this.screen.setCSSLoading(true); + const addSkipAndResolve = () => { + this.skipCSSLinks.push(value); // watch out + resolve(null); + } + timeoutId = setTimeout(addSkipAndResolve, 4000); + + node.onload = () => { + const doc = this.screen.document; + doc && rewriteNodeStyleSheet(doc, node); + resolve(null); + } + node.onerror = addSkipAndResolve; + }).then(() => { + node.onload = null; + node.onerror = null; + clearTimeout(timeoutId); + this.linkLoadingCount--; + if (this.linkLoadingCount === 0) { + this.screen.setCSSLoading(false); + } + }); + this.linkLoadPromises.push(promise); + } + + private manageRule = (msg: CSSRuleMessage):void => { + // if (msg.tp === "css_insert_rule") { + // let styleSheet = this.#screen.document.styleSheets[ msg.stylesheetID ]; + // if (!styleSheet) { + // logger.log("No stylesheet with corresponding ID found: ", msg) + // styleSheet = this.#screen.document.styleSheets[0]; + // if (!styleSheet) { + // return; + // } + // } + // try { + // styleSheet.insertRule(msg.rule, msg.index); + // } catch (e) { + // logger.log(e, msg) + // //const index = Math.min(msg.index, styleSheet.cssRules.length); + // styleSheet.insertRule(msg.rule, styleSheet.cssRules.length); + // //styleSheet.ownerNode.innerHTML += msg.rule; + // } + // } + // if (msg.tp === "css_delete_rule") { + // // console.warn('Warning: STYLESHEET_DELETE_RULE msg') + // const styleSheet = this.#screen.document.styleSheets[msg.stylesheetID]; + // if (!styleSheet) { + // logger.log("No stylesheet with corresponding ID found: ", msg) + // return; + // } + // styleSheet.deleteRule(msg.index); + // } + } + + moveReady(t: number): Promise { + return Promise.all(this.linkLoadPromises) + .then(() => this.moveApply(t, this.manageRule)); + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/messages.ts b/frontend/app/player/MessageDistributor/messages.ts index 92c7e60fb..6d9f3244c 100644 --- a/frontend/app/player/MessageDistributor/messages.ts +++ b/frontend/app/player/MessageDistributor/messages.ts @@ -36,6 +36,8 @@ export const ID_TP_MAP = { 54: "connection_information", 55: "set_page_visibility", 59: "long_task", + 69: "mouse_click", + 70: "create_i_frame_document", } as const; @@ -255,11 +257,26 @@ export interface LongTask { containerName: string, } +export interface MouseClick { + tp: "mouse_click", + id: number, + hesitationTime: number, + label: string, + selector: string, +} -export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask; +export interface CreateIFrameDocument { + tp: "create_i_frame_document", + frameID: number, + id: number, +} + + +export type Message = Timestamp | SetPageLocation | SetViewportSize | SetViewportScroll | CreateDocument | CreateElementNode | CreateTextNode | MoveNode | RemoveNode | SetNodeAttribute | RemoveNodeAttribute | SetNodeData | SetCssData | SetNodeScroll | SetInputValue | SetInputChecked | MouseMove | ConsoleLog | CssInsertRule | CssDeleteRule | Fetch | Profiler | OTable | Redux | Vuex | MobX | NgRx | GraphQl | PerformanceTrack | ConnectionInformation | SetPageVisibility | LongTask | MouseClick | CreateIFrameDocument; export default function (r: PrimitiveReader): Message | null { - switch (r.readUint()) { + const ui= r.readUint() + switch (ui) { case 0: return { @@ -509,7 +526,24 @@ export default function (r: PrimitiveReader): Message | null { containerName: r.readString(), }; + case 69: + return { + tp: ID_TP_MAP[69], + id: r.readUint(), + hesitationTime: r.readUint(), + label: r.readString(), + selector: r.readString(), + }; + + case 70: + return { + tp: ID_TP_MAP[70], + frameID: r.readUint(), + id: r.readUint(), + }; + default: + console.log("wtf is this", ui) r.readUint(); // IOS skip timestamp r.skip(r.readUint()); return null;