diff --git a/frontend/app/api_middleware.js b/frontend/app/api_middleware.js index a29a22eb6..8f9965ec5 100644 --- a/frontend/app/api_middleware.js +++ b/frontend/app/api_middleware.js @@ -1,3 +1,4 @@ +import logger from 'App/logger'; import APIClient from './api_client'; import { UPDATE, DELETE } from './duck/jwt'; @@ -28,8 +29,9 @@ export default store => next => (action) => { next({ type: UPDATE, data: jwt }); } }) - .catch(() => { - return next({ type: FAILURE, errors: [ 'Connection error' ] }); + .catch((e) => { + logger.error("Error during API request. ", e) + return next({ type: FAILURE, errors: [ "Connection error", String(e) ] }); }); }; diff --git a/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts new file mode 100644 index 000000000..378021203 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts @@ -0,0 +1,328 @@ +import logger from 'App/logger'; + +import type StatedScreen from '../../StatedScreen'; +import type { Message, SetNodeScroll, CreateElementNode } from '../../messages'; + +import ListWalker from '../ListWalker'; +import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; +import { VElement, VText, VFragment, VDocument, VNode, VStyleElement } from './VirtualDOM'; +import type { StyleElement } from './VirtualDOM'; + + +type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + +const IGNORED_ATTRS = [ "autocomplete", "name" ]; +const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~ + + +// TODO: filter out non-relevant prefixes +// function replaceCSSPrefixes(css: string) { +// return css +// .replace(/\-ms\-/g, "") +// .replace(/\-webkit\-/g, "") +// .replace(/\-moz\-/g, "") +// .replace(/\-webkit\-/g, "") +// } + +export default class DOMManager extends ListWalker { + private vTexts: Map = new Map() // map vs object here? + private vElements: Map = new Map() + private vRoots: Map = new Map() + + + private upperBodyId: number = -1; + private nodeScrollManagers: Map> = new Map() + private stylesManager: StylesManager + + + constructor( + private readonly screen: StatedScreen, + private readonly isMobile: boolean, + public readonly time: number + ) { + super() + this.stylesManager = new StylesManager(screen) + } + + append(m: Message): void { + if (m.tp === "set_node_scroll") { + let scrollManager = this.nodeScrollManagers.get(m.id) + if (!scrollManager) { + scrollManager = new ListWalker() + this.nodeScrollManagers.set(m.id, scrollManager) + } + scrollManager.append(m) + return + } + if (m.tp === "create_element_node") { + if(m.tag === "BODY" && this.upperBodyId === -1) { + this.upperBodyId = m.id + } + } else if (m.tp === "set_node_attribute" && + (IGNORED_ATTRS.includes(m.name) || !ATTR_NAME_REGEXP.test(m.name))) { + logger.log("Ignorring message: ", m) + return; // Ignoring + } + super.append(m) + } + + private removeBodyScroll(id: number, vn: VElement): void { + if (this.isMobile && this.upperBodyId === id) { // Need more type safety! + (vn.node as HTMLBodyElement).style.overflow = "hidden" + } + } + + // May be make it as a message on message add? + private removeAutocomplete(node: Element): boolean { + const tag = node.tagName + if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) { + node.setAttribute("autocomplete", "off"); + return true; + } + if (tag === "INPUT") { + node.setAttribute("autocomplete", "new-password"); + return true; + } + return false; + } + + private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void { + const child = this.vElements.get(id) || this.vTexts.get(id) + if (!child) { + logger.error("Insert error. Node not found", id); + return; + } + const parent = this.vElements.get(parentID) || this.vRoots.get(parentID) + if (!parent) { + logger.error("Insert error. Parent node not found", parentID); + return; + } + + const pNode = parent.node + if ((pNode instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker + pNode.sheet && + pNode.sheet.cssRules && + pNode.sheet.cssRules.length > 0 && + pNode.innerText.trim().length === 0 + ) { + logger.log("Trying to insert child to a style tag with virtual rules: ", parent, child); + return; + } + + parent.insertChildAt(child, index) + } + + private applyMessage = (msg: Message): void => { + let node: Node | undefined + let vn: VNode | undefined + let doc: Document | null + switch (msg.tp) { + case "create_document": + doc = this.screen.document; + if (!doc) { + logger.error("No iframe document found", msg) + return; + } + doc.open(); + doc.write(""); + doc.close(); + const fRoot = doc.documentElement; + fRoot.innerText = ''; + + vn = new VElement(fRoot) + this.vElements = new Map([[0, vn]]) + const vDoc = new VDocument(doc) + vDoc.insertChildAt(vn, 0) + this.vRoots = new Map([[-1, vDoc]]) // todo: start from 0 (sync logic with tracker) + this.stylesManager.reset() + return + case "create_text_node": + vn = new VText() + this.vTexts.set(msg.id, vn) + this.insertNode(msg) + return + case "create_element_node": + let element: Element + if (msg.svg) { + element = document.createElementNS('http://www.w3.org/2000/svg', msg.tag) + } else { + element = document.createElement(msg.tag) + } + if (msg.tag === "STYLE" || msg.tag === "style") { + vn = new VStyleElement(element as StyleElement) + } else { + vn = new VElement(element) + } + this.vElements.set(msg.id, vn) + this.insertNode(msg) + this.removeBodyScroll(msg.id, vn) + this.removeAutocomplete(element) + if (['STYLE', 'style', 'LINK'].includes(msg.tag)) { // Styles in priority + vn.enforceInsertion() + } + return + case "move_node": + this.insertNode(msg); + return + case "remove_node": + vn = this.vElements.get(msg.id) || this.vTexts.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + if (!vn.parentNode) { logger.error("Parent node not found", msg); return } + vn.parentNode.removeChild(vn) + return + case "set_node_attribute": + let { name, value } = msg; + vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + if (name === "href" && vn.node.tagName === "LINK") { + // @ts-ignore ?global ENV type // It've been done on backend (remove after testing in saas) + // if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) { + // value = value.replace("?", "%3F"); + // } + if (!value.startsWith("http")) { return } + // blob:... value happened here. https://foss.openreplay.com/3/session/7013553567419137 + // that resulted in that link being unable to load and having 4sec timeout in the below function. + this.stylesManager.setStyleHandlers(vn.node as HTMLLinkElement, value); + } + if (vn.node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) { + value = "url(#" + (value.split("#")[1] ||")") + } + vn.setAttribute(name, value) + this.removeBodyScroll(msg.id, vn) + return + case "remove_node_attribute": + vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + vn.removeAttribute(msg.name) + return + case "set_input_value": + vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + const nodeWithValue = vn.node + if (!(nodeWithValue instanceof HTMLInputElement + || nodeWithValue instanceof HTMLTextAreaElement + || nodeWithValue instanceof HTMLSelectElement) + ) { + logger.error("Trying to set value of non-Input element", msg) + return + } + const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value + doc = this.screen.document + if (doc && nodeWithValue === doc.activeElement) { + // For the case of Remote Control + nodeWithValue.onblur = () => { nodeWithValue.value = val } + return + } + nodeWithValue.value = val + return + case "set_input_checked": + vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + (vn.node as HTMLInputElement).checked = msg.checked + return + case "set_node_data": + case "set_css_data": // mbtodo: remove css transitions when timeflow is not natural (on jumps) + vn = this.vTexts.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + vn.setData(msg.data) + if (vn.node instanceof HTMLStyleElement) { + doc = this.screen.document + // TODO: move to message parsing + doc && rewriteNodeStyleSheet(doc, vn.node) + } + if (msg.tp === "set_css_data") { // Styles in priority (do we need inlines as well?) + vn.applyChanges() + } + return + case "css_insert_rule": + vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + if (!(vn instanceof VStyleElement)) { + logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn); + return + } + vn.onStyleSheet(sheet => { + try { + sheet.insertRule(msg.rule, msg.index) + } catch (e) { + logger.warn(e, msg) + try { + sheet.insertRule(msg.rule) + } catch (e) { + logger.warn("Cannot insert rule.", e, msg) + } + } + }) + return + case "css_delete_rule": + vn = this.vElements.get(msg.id) + if (!vn) { logger.error("Node not found", msg); return } + if (!(vn instanceof VStyleElement)) { + logger.warn("Non-style node in CSS rules message (or sheet is null)", msg, vn); + return + } + vn.onStyleSheet(sheet => { + try { + sheet.deleteRule(msg.index) + } catch (e) { + logger.warn(e, msg) + } + }) + return + case "create_i_frame_document": + vn = this.vElements.get(msg.frameID) + if (!vn) { logger.error("Node not found", msg); return } + const host = vn.node + if (host instanceof HTMLIFrameElement) { + const vDoc = new VDocument() + this.vRoots.set(msg.id, vDoc) + host.onload = () => { + const doc = host.contentDocument + if (!doc) { + logger.warn("No iframe doc onload", msg, host) + return + } + vDoc.setDocument(doc) + vDoc.applyChanges() + } + return; + } else if (host instanceof Element) { // shadow DOM + try { + const shadowRoot = host.attachShadow({ mode: 'open' }) + vn = new VFragment(shadowRoot) + this.vRoots.set(msg.id, vn) + } catch(e) { + logger.warn("Can not attach shadow dom", e, msg) + } + } else { + logger.warn("Context message host is not Element", msg) + } + return + } + } + + moveReady(t: number): Promise { + // MBTODO (back jump optimisation): + // - store intemediate virtual dom state + // - cancel previous moveReady tasks (is it possible?) if new timestamp is less + this.moveApply(t, this.applyMessage) // This function autoresets pointer if necessary (better name?) + + this.vRoots.forEach(rt => rt.applyChanges()) // MBTODO (optimisation): affected set + + // Thinkabout (read): css preload + // What if we go back before it is ready? We'll have two handlres? + return this.stylesManager.moveReady(t).then(() => { + // Apply all scrolls after the styles got applied + this.nodeScrollManagers.forEach(manager => { + const msg = manager.moveGetLast(t) + if (msg) { + const vElm = this.vElements.get(msg.id) + if (vElm) { + vElm.node.scrollLeft = msg.x + vElm.node.scrollTop = msg.y + } + } + }) + }) + } +} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/StylesManager.ts b/frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts similarity index 89% rename from frontend/app/player/MessageDistributor/managers/StylesManager.ts rename to frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts index 3f5ee1b86..c6b674c4e 100644 --- a/frontend/app/player/MessageDistributor/managers/StylesManager.ts +++ b/frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts @@ -1,10 +1,10 @@ -import type StatedScreen from '../StatedScreen'; -import type { CssInsertRule, CssDeleteRule } from '../messages'; +import type StatedScreen from '../../StatedScreen'; +import type { CssInsertRule, CssDeleteRule } from '../../messages'; type CSSRuleMessage = CssInsertRule | CssDeleteRule; import logger from 'App/logger'; -import ListWalker from './ListWalker'; +import ListWalker from '../ListWalker'; const HOVER_CN = "-openreplay-hover"; @@ -40,21 +40,21 @@ export default class StylesManager extends ListWalker { } setStyleHandlers(node: HTMLLinkElement, value: string): void { - let timeoutId; - const promise = new Promise((resolve) => { - if (this.skipCSSLinks.includes(value)) resolve(null); + let timeoutId: ReturnType | undefined; + const promise = new Promise((resolve) => { + if (this.skipCSSLinks.includes(value)) resolve(); this.linkLoadingCount++; this.screen.setCSSLoading(true); const addSkipAndResolve = () => { this.skipCSSLinks.push(value); // watch out - resolve(null); + resolve() } timeoutId = setTimeout(addSkipAndResolve, 4000); node.onload = () => { const doc = this.screen.document; doc && rewriteNodeStyleSheet(doc, node); - resolve(null); + resolve(); } node.onerror = addSkipAndResolve; }).then(() => { diff --git a/frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts b/frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts new file mode 100644 index 000000000..9fa151e61 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts @@ -0,0 +1,169 @@ +type VChild = VElement | VText + +export type VNode = VDocument | VFragment | VElement | VText + +abstract class VParent { + abstract node: Node | null + protected children: VChild[] = [] + private insertedChildren: Set = new Set() + + insertChildAt(child: VChild, index: number) { + if (child.parentNode) { + child.parentNode.removeChild(child) + } + this.children.splice(index, 0, child) + this.insertedChildren.add(child) + child.parentNode = this + } + + removeChild(child: VChild) { + this.children = this.children.filter(ch => ch !== child) + this.insertedChildren.delete(child) + child.parentNode = null + } + + applyChanges() { + const node = this.node + if (!node) { + // log err + console.error("No node found", this) + return + } + // inserting + for (let i = this.children.length-1; i >= 0; i--) { + const child = this.children[i] + child.applyChanges() + if (this.insertedChildren.has(child)) { + const nextVSibling = this.children[i+1] + node.insertBefore(child.node, nextVSibling ? nextVSibling.node : null) + } + } + this.insertedChildren.clear() + // removing + const realChildren = node.childNodes + for(let j = 0; j < this.children.length; j++) { + while (realChildren[j] !== this.children[j].node) { + node.removeChild(realChildren[j]) + } + } + // removing rest + while(realChildren.length > this.children.length) { + node.removeChild(node.lastChild) + } + } +} + +export class VDocument extends VParent { + constructor(public node: Document | null = null) { super() } + setDocument(doc: Document) { + this.node = doc + } + applyChanges() { + if (this.children.length > 1) { + // log err + } + if (!this.node) { + // iframe not mounted yet + return + } + const child = this.children[0] + child.applyChanges() + const htmlNode = child.node + if (htmlNode.parentNode !== this.node) { + this.node.replaceChild(htmlNode, this.node.documentElement) + } + } +} + +export class VFragment extends VParent { + constructor(public readonly node: DocumentFragment) { super() } +} + +export class VElement extends VParent { + parentNode: VParent | null = null + private newAttributes: Map = new Map() + constructor(public readonly node: Element) { super() } + setAttribute(name: string, value: string) { + this.newAttributes.set(name, value) + } + removeAttribute(name: string) { + this.newAttributes.set(name, false) + } + + // mbtodo: priority insertion instead. + // rn this is for styles that should be inserted as prior, + // otherwise it will show visual styling lag if there is a transition CSS property) + enforceInsertion() { + let vNode: VElement = this + while (vNode.parentNode instanceof VElement) { + vNode = vNode.parentNode + } + (vNode.parentNode || vNode).applyChanges() + } + + applyChanges() { + this.newAttributes.forEach((value, key) => { + if (value === false) { + this.node.removeAttribute(key) + } else { + try { + this.node.setAttribute(key, value) + } catch { + // log err + } + } + }) + this.newAttributes.clear() + super.applyChanges() + } +} + + +type StyleSheetCallback = (s: CSSStyleSheet) => void +export type StyleElement = HTMLStyleElement | SVGStyleElement +export class VStyleElement extends VElement { + // private loaded = false + private stylesheetCallbacks: StyleSheetCallback[] = [] + constructor(public readonly node: StyleElement) { + super(node) // Is it compiled correctly or with 2 node assignments? + // node.onload = () => { + // const sheet = node.sheet + // if (sheet) { + // this.stylesheetCallbacks.forEach(cb => cb(sheet)) + // } else { + // console.warn("Style onload: sheet is null") + // } + // this.loaded = true + // } + } + + onStyleSheet(cb: StyleSheetCallback) { + // if (this.loaded) { + if (!this.node.sheet) { + console.warn("Style tag is loaded, but sheet is null") + return + } + cb(this.node.sheet) + // } else { + // this.stylesheetCallbacks.push(cb) + // } + } +} + +export class VText { + parentNode: VParent | null = null + constructor(public readonly node: Text = new Text()) {} + private data: string = "" + private changed: boolean = false + setData(data: string) { + this.data = data + this.changed = true + } + applyChanges() { + if (this.changed) { + this.node.data = this.data + this.changed = false + } + } +} + diff --git a/frontend/app/player/MessageDistributor/managers/DOMManager.ts b/frontend/app/player/MessageDistributor/managers/DOMManager.ts deleted file mode 100644 index 43c7a274c..000000000 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.ts +++ /dev/null @@ -1,322 +0,0 @@ -import type StatedScreen from '../StatedScreen'; -import type { Message, SetNodeScroll, CreateElementNode } from '../messages'; - -import logger from 'App/logger'; -import StylesManager, { rewriteNodeStyleSheet } from './StylesManager'; -import ListWalker from './ListWalker'; - -const IGNORED_ATTRS = [ "autocomplete", "name" ]; - -const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/; // regexp costs ~ - -export default class DOMManager extends ListWalker { - private isMobile: boolean; - private screen: StatedScreen; - private nl: Array = []; - private isLink: Array = []; // Optimisations - private bodyId: number = -1; - private postponedBodyMessage: CreateElementNode | null = null; - private nodeScrollManagers: Array> = []; - - private stylesManager: StylesManager; - - private startTime: number; - - constructor(screen: StatedScreen, isMobile: boolean, startTime: number) { - super(); - this.startTime = startTime; - this.isMobile = isMobile; - this.screen = screen; - this.stylesManager = new StylesManager(screen); - } - - get time(): number { - return this.startTime; - } - - append(m: Message): void { - switch (m.tp) { - case "set_node_scroll": - if (!this.nodeScrollManagers[ m.id ]) { - this.nodeScrollManagers[ m.id ] = new ListWalker(); - } - this.nodeScrollManagers[ m.id ].append(m); - return; - //case "css_insert_rule": // || //set_css_data ??? - //case "css_delete_rule": - // (m.tp === "set_node_attribute" && this.isLink[ m.id ] && m.key === "href")) { - // this.stylesManager.append(m); - // return; - default: - if (m.tp === "create_element_node") { - switch(m.tag) { - case "LINK": - this.isLink[ m.id ] = true; - break; - case "BODY": - this.bodyId = m.id; // Can be several body nodes at one document session? - break; - } - } else if (m.tp === "set_node_attribute" && - (IGNORED_ATTRS.includes(m.name) || !ATTR_NAME_REGEXP.test(m.name))) { - logger.log("Ignorring message: ", m) - return; // Ignoring... - } - super.append(m); - } - - } - - private removeBodyScroll(id: number): void { - if (this.isMobile && this.bodyId === id) { - (this.nl[ id ] as HTMLBodyElement).style.overflow = "hidden"; - } - } - - // May be make it as a message on message add? - private removeAutocomplete({ id, tag }: CreateElementNode): boolean { - const node = this.nl[ id ] as HTMLElement; - if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) { - node.setAttribute("autocomplete", "off"); - return true; - } - if (tag === "INPUT") { - node.setAttribute("autocomplete", "new-password"); - return true; - } - return false; - } - - // type = NodeMessage ? - private insertNode({ parentID, id, index }: { parentID: number, id: number, index: number }): void { - if (!this.nl[ id ]) { - logger.error("Insert error. Node not found", id); - return; - } - if (!this.nl[ parentID ]) { - logger.error("Insert error. Parent node not found", parentID); - return; - } - // WHAT if text info contains some rules and the ordering is just wrong??? - const el = this.nl[ parentID ] - if ((el instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker - el.sheet && - el.sheet.cssRules && - el.sheet.cssRules.length > 0 && - el.innerText.trim().length === 0) { - logger.log("Trying to insert child to a style tag with virtual rules: ", this.nl[ parentID ], this.nl[ id ]); - return; - } - - const childNodes = this.nl[ parentID ].childNodes; - if (!childNodes) { - logger.error("Node has no childNodes", this.nl[ parentID ]); - return; - } - - if (this.nl[ id ] instanceof HTMLHtmlElement) { - // What if some exotic cases? - this.nl[ parentID ].replaceChild(this.nl[ id ], childNodes[childNodes.length-1]) - return - } - - this.nl[ parentID ] - .insertBefore(this.nl[ id ], childNodes[ index ]) - } - - private applyMessage = (msg: Message): void => { - let node; - let doc: Document | null; - switch (msg.tp) { - case "create_document": - doc = this.screen.document; - if (!doc) { - logger.error("No iframe document found", msg) - return; - } - doc.open(); - doc.write(""); - doc.close(); - const fRoot = doc.documentElement; - fRoot.innerText = ''; - this.nl = [ fRoot ]; - - // the last load event I can control - //if (this.document.fonts) { - // this.document.fonts.onloadingerror = () => this.marker.redraw(); - // this.document.fonts.onloadingdone = () => this.marker.redraw(); - //} - - //this.screen.setDisconnected(false); - this.stylesManager.reset(); - return - case "create_text_node": - this.nl[ msg.id ] = document.createTextNode(''); - this.insertNode(msg); - return - case "create_element_node": - if (msg.svg) { - this.nl[ msg.id ] = document.createElementNS('http://www.w3.org/2000/svg', msg.tag); - } else { - this.nl[ msg.id ] = document.createElement(msg.tag); - } - if (this.bodyId === msg.id) { // there are several bodies in iframes TODO: optimise & cache prebuild - this.postponedBodyMessage = msg; - } else { - this.insertNode(msg); - } - this.removeBodyScroll(msg.id); - this.removeAutocomplete(msg); - return - case "move_node": - this.insertNode(msg); - return - case "remove_node": - node = this.nl[ msg.id ] - if (!node) { logger.error("Node not found", msg); return } - if (!node.parentElement) { logger.error("Parent node not found", msg); return } - node.parentElement.removeChild(node); - return - case "set_node_attribute": - let { id, name, value } = msg; - node = this.nl[ id ]; - if (!node) { logger.error("Node not found", msg); return } - if (this.isLink[ id ] && name === "href") { - // @ts-ignore TODO: global ENV type - if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) { // Hack for queries in rewrited urls - value = value.replace("?", "%3F"); - } - this.stylesManager.setStyleHandlers(node, value); - } - if (node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) { - value = "url(#" + (value.split("#")[1] ||")") - } - try { - node.setAttribute(name, value); - } catch(e) { - logger.error(e, msg); - } - this.removeBodyScroll(msg.id); - return - case "remove_node_attribute": - if (!this.nl[ msg.id ]) { logger.error("Node not found", msg); return } - try { - (this.nl[ msg.id ] as HTMLElement).removeAttribute(msg.name); - } catch(e) { - logger.error(e, msg); - } - return - case "set_input_value": - node = this.nl[ msg.id ] - if (!node) { logger.error("Node not found", msg); return } - if (!(node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement)) { - logger.error("Trying to set value of non-Input element", msg) - return - } - const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value - doc = this.screen.document - if (doc && node === doc.activeElement) { - // For the case of Remote Control - node.onblur = () => { node.value = val } - return - } - node.value = val - return - case "set_input_checked": - node = this.nl[ msg.id ]; - if (!node) { logger.error("Node not found", msg); return } - (node as HTMLInputElement).checked = msg.checked; - return - case "set_node_data": - case "set_css_data": - node = this.nl[ msg.id ] - if (!node) { logger.error("Node not found", msg); return } - // @ts-ignore - node.data = msg.data; - if (node instanceof HTMLStyleElement) { - doc = this.screen.document - doc && rewriteNodeStyleSheet(doc, node) - } - return - case "css_insert_rule": - node = this.nl[ msg.id ]; - if (!node) { logger.error("Node not found", msg); return } - if (!(node instanceof HTMLStyleElement) // link or null - || node.sheet == null) { - logger.warn("Non-style node in CSS rules message (or sheet is null)", msg); - return - } - try { - node.sheet.insertRule(msg.rule, msg.index) - } catch (e) { - logger.warn(e, msg) - try { - node.sheet.insertRule(msg.rule) - } catch (e) { - logger.warn("Cannot insert rule.", e, msg) - } - } - return - case "css_delete_rule": - node = this.nl[ msg.id ]; - if (!node) { logger.error("Node not found", msg); return } - if (!(node instanceof HTMLStyleElement) // link or null - || node.sheet == null) { - logger.warn("Non-style node in CSS rules message (or sheet is null)", msg); - return - } - try { - node.sheet.deleteRule(msg.index) - } catch (e) { - logger.warn(e, msg) - } - return - case "create_i_frame_document": - node = this.nl[ msg.frameID ]; - // console.log('ifr', msg, node) - - if (node instanceof HTMLIFrameElement) { - doc = node.contentDocument; - if (!doc) { - logger.warn("No iframe doc", msg, node, node.contentDocument); - return; - } - this.nl[ msg.id ] = doc.documentElement - return; - } else if (node instanceof Element) { // shadow DOM - try { - this.nl[ msg.id ] = node.attachShadow({ mode: 'open' }) - } catch(e) { - logger.warn("Can not attach shadow dom", e, msg) - } - } else { - logger.warn("Context message host is not Element", msg) - } - return - } - } - - moveReady(t: number): Promise { - this.moveApply(t, this.applyMessage) // This function autoresets pointer if necessary (better name?) - - /* Mount body as late as possible */ - if (this.postponedBodyMessage != null) { - this.insertNode(this.postponedBodyMessage) - this.postponedBodyMessage = null - } - - // Thinkabout (read): css preload - // What if we go back before it is ready? We'll have two handlres? - return this.stylesManager.moveReady(t).then(() => { - // Apply all scrolls after the styles got applied - this.nodeScrollManagers.forEach(manager => { - const msg = manager.moveGetLast(t) - if (!!msg && !!this.nl[msg.id]) { - const node = this.nl[msg.id] as HTMLElement - node.scrollLeft = msg.x - node.scrollTop = msg.y - } - }) - }) - } -} \ No newline at end of file diff --git a/frontend/app/player/MessageDistributor/managers/PagesManager.ts b/frontend/app/player/MessageDistributor/managers/PagesManager.ts index 0a463fe97..9a4398246 100644 --- a/frontend/app/player/MessageDistributor/managers/PagesManager.ts +++ b/frontend/app/player/MessageDistributor/managers/PagesManager.ts @@ -2,7 +2,7 @@ import type StatedScreen from '../StatedScreen'; import type { Message } from '../messages'; import ListWalker from './ListWalker'; -import DOMManager from './DOMManager'; +import DOMManager from './DOM/DOMManager'; export default class PagesManager extends ListWalker { diff --git a/frontend/app/types/session/issue.js b/frontend/app/types/session/issue.js index db220f8cc..673467fd2 100644 --- a/frontend/app/types/session/issue.js +++ b/frontend/app/types/session/issue.js @@ -10,11 +10,11 @@ export const issues_types = List([ { 'type': 'memory', 'visible': true, 'order': 4, 'name': 'High Memory', 'icon': 'funnel/sd-card' }, { 'type': 'vault', 'visible': true, 'order': 5, 'name': 'Vault', 'icon': 'safe' }, { 'type': 'bookmark', 'visible': true, 'order': 5, 'name': 'Bookmarks', 'icon': 'safe' }, - // { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, - // { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' }, - // { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' }, - // { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' }, - // { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' } + { 'type': 'bad_request', 'visible': true, 'order': 1, 'name': 'Bad Requests', 'icon': 'funnel/file-medical-alt' }, + { 'type': 'missing_resource', 'visible': true, 'order': 2, 'name': 'Missing Images', 'icon': 'funnel/image' }, + { 'type': 'dead_click', 'visible': true, 'order': 4, 'name': 'Dead Clicks', 'icon': 'funnel/dizzy' }, + { 'type': 'cpu', 'visible': true, 'order': 6, 'name': 'High CPU', 'icon': 'funnel/cpu' }, + { 'type': 'custom', 'visible': false, 'order': 8, 'name': 'Custom', 'icon': 'funnel/exclamation-circle' } ]).map(Watchdog) export const issues_types_map = {} @@ -40,7 +40,7 @@ export default Record({ fromJS: ({ type, ...rest }) => ({ ...rest, type, - icon: issues_types_map[type].icon, - name: issues_types_map[type].name, + icon: issues_types_map[type]?.icon, + name: issues_types_map[type]?.name, }), });