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..278e97ba4 --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/DOM/DOMManager.ts @@ -0,0 +1,305 @@ +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 ~ + +export default class DOMManager extends ListWalker { + private isMobile: boolean; + private screen: StatedScreen; + 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: Array> = [] + private stylesManager: StylesManager + + + constructor(screen: StatedScreen, isMobile: boolean, public readonly time: number) { + super(); + this.isMobile = isMobile; + this.screen = screen; + this.stylesManager = new StylesManager(screen); + } + + 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; + default: + 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 ]]) + 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) + 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 TODO: global ENV type // Hack for queries in rewrited urls (don't we do that in backend?) + if (value.startsWith(window.env.ASSETS_HOST || window.location.origin + '/assets')) { + value = value.replace("?", "%3F"); + } + 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": // TODO: 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) + } + 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, node.sheet); + 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, node.sheet); + 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 { + this.moveApply(t, this.applyMessage) // This function autoresets pointer if necessary (better name?) + + // @ts-ignore + this.vElements.get(0).applyChanges() + this.vRoots.forEach(rt => rt.applyChanges()) + + // 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 90% rename from frontend/app/player/MessageDistributor/managers/StylesManager.ts rename to frontend/app/player/MessageDistributor/managers/DOM/StylesManager.ts index 3f5ee1b86..139ebd95c 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,7 +40,7 @@ export default class StylesManager extends ListWalker { } setStyleHandlers(node: HTMLLinkElement, value: string): void { - let timeoutId; + let timeoutId: ReturnType | undefined; const promise = new Promise((resolve) => { if (this.skipCSSLinks.includes(value)) resolve(null); this.linkLoadingCount++; @@ -49,8 +49,8 @@ export default class StylesManager extends ListWalker { this.skipCSSLinks.push(value); // watch out resolve(null); } - timeoutId = setTimeout(addSkipAndResolve, 4000); - + timeoutId = setTimeout(addSkipAndResolve, 4000000); + console.log(node.getAttribute("href")) node.onload = () => { const doc = this.screen.document; doc && rewriteNodeStyleSheet(doc, node); 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..3df1b5a3e --- /dev/null +++ b/frontend/app/player/MessageDistributor/managers/DOM/VirtualDOM.ts @@ -0,0 +1,143 @@ +type VChild = VElement | VText + +export type VNode = VDocument | VFragment | VElement | VText + +abstract class VParent { + abstract node: Node | null + protected children: VChild[] = [] + insertChildAt(child: VChild, index: number) { + if (child.parentNode) { + child.parentNode.removeChild(child) + } + this.children.splice(index, 0, child) + child.parentNode = this + } + + removeChild(child: VChild) { + this.children = this.children.filter(ch => ch !== child) + child.parentNode = null + } + applyChanges() { + const node = this.node + if (!node) { + // log err + console.error("No node found", this) + return + } + const realChildren = node.childNodes + for (let i = 0; i < this.children.length; i++) { + const ch = this.children[i] + ch.applyChanges() + if (ch.node.parentNode !== node) { + const nextSibling = realChildren[i] + node.insertBefore(ch.node, nextSibling || null) + } + if (realChildren[i] !== ch.node) { + node.removeChild(realChildren[i]) + } + } + } +} + +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 htmlNode = this.children[0].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: Record = {} + //private props: Record + constructor(public readonly node: Element) { super() } + setAttribute(name: string, value: string) { + this.newAttributes[name] = value + } + removeAttribute(name: string) { + this.newAttributes[name] = false + } + + applyChanges() { + Object.entries(this.newAttributes).forEach(([key, value]) => { + if (value === false) { + this.node.removeAttribute(key) + } else { + try { + this.node.setAttribute(key, value) + } catch { + // log err + } + } + }) + this.newAttributes = {} + 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 37d24aa44..000000000 --- a/frontend/app/player/MessageDistributor/managers/DOMManager.ts +++ /dev/null @@ -1,325 +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 - || node 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 && 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 {