From 885562ec94b4eac8e338d4ea77427200ac4a51c4 Mon Sep 17 00:00:00 2001 From: Alex Kaminskii Date: Sun, 2 Apr 2023 22:44:36 +0200 Subject: [PATCH 1/5] feat(player): lazy JS DOM node creation; (need fixes for reaching full potential) --- .../app/player/web/managers/DOM/DOMManager.ts | 272 +++++++++--------- .../app/player/web/managers/DOM/VirtualDOM.ts | 193 +++++++------ .../web/messages/rewriter/rewriteMessage.ts | 12 + 3 files changed, 244 insertions(+), 233 deletions(-) diff --git a/frontend/app/player/web/managers/DOM/DOMManager.ts b/frontend/app/player/web/managers/DOM/DOMManager.ts index 8e0fb8b6b..c07b6ea73 100644 --- a/frontend/app/player/web/managers/DOM/DOMManager.ts +++ b/frontend/app/player/web/managers/DOM/DOMManager.ts @@ -12,9 +12,9 @@ import { PostponedStyleSheet, VDocument, VElement, + VHTMLElement, VNode, VShadowRoot, - VStyleElement, VText, } from './VirtualDOM'; import { deleteRule, insertRule } from './safeCSSRules'; @@ -24,23 +24,14 @@ type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectE const IGNORED_ATTRS = [ "autocomplete" ]; 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 readonly vTexts: Map = new Map() // map vs object here? private readonly vElements: Map = new Map() private readonly vRoots: Map = new Map() private styleSheets: Map = new Map() private ppStyleSheets: Map = new Map() + /** @depreacted since tracker 4.0.2 Mapping by nodeID */ + private ppStyleSheetsDeprecated: Map = new Map() private stringDict: Record = {} private attrsBacktrack: Message[] = [] @@ -91,21 +82,20 @@ export default class DOMManager extends ListWalker { super.append(m) } - private removeBodyScroll(id: number, vn: VElement): void { + private removeBodyScroll(id: number, vElem: VElement): void { if (this.isMobile && this.upperBodyId === id) { // Need more type safety! - (vn.node as HTMLBodyElement).style.overflow = "hidden" + (vElem.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 + private removeAutocomplete(vElem: VElement): boolean { + const tag = vElem.tagName if ([ "FORM", "TEXTAREA", "SELECT" ].includes(tag)) { - node.setAttribute("autocomplete", "off"); + vElem.setAttribute("autocomplete", "off"); return true; } if (tag === "INPUT") { - node.setAttribute("autocomplete", "new-password"); + vElem.setAttribute("autocomplete", "new-password"); return true; } return false; @@ -123,7 +113,7 @@ export default class DOMManager extends ListWalker { return; } - const pNode = parent.node + const pNode = parent.node // TODOTODO if ((pNode instanceof HTMLStyleElement) && // TODO: correct ordering OR filter in tracker pNode.sheet && pNode.sheet.cssRules && @@ -143,11 +133,11 @@ export default class DOMManager extends ListWalker { const vn = this.vElements.get(msg.id) if (!vn) { logger.error("SetNodeAttribute: Node not found", msg); return } - if (vn.node.tagName === "INPUT" && name === "name") { + if (vn.tagName === "INPUT" && name === "name") { // Otherwise binds local autocomplete values (maybe should ignore on the tracker level) return } - if (name === "href" && vn.node.tagName === "LINK") { + if (name === "href" && vn.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"); @@ -161,7 +151,7 @@ export default class DOMManager extends ListWalker { // TODO: check if node actually exists on the page, not just in memory this.stylesManager.setStyleHandlers(vn.node as HTMLLinkElement, value); } - if (vn.node.namespaceURI === 'http://www.w3.org/2000/svg' && value.startsWith("url(")) { + if (vn.isSVG && value.startsWith("url(")) { value = "url(#" + (value.split("#")[1] ||")") } vn.setAttribute(name, value) @@ -169,12 +159,9 @@ export default class DOMManager extends ListWalker { } private applyMessage = (msg: Message): Promise | undefined => { - let vn: VNode | undefined - let doc: Document | null - let styleSheet: CSSStyleSheet | PostponedStyleSheet | undefined switch (msg.tp) { - case MType.CreateDocument: - doc = this.screen.document; + case MType.CreateDocument: { + const doc = this.screen.document; if (!doc) { logger.error("No root iframe document found", msg, this.screen) return; @@ -185,11 +172,11 @@ export default class DOMManager extends ListWalker { const fRoot = doc.documentElement; fRoot.innerText = ''; - vn = new VElement(fRoot) + const vHTMLElement = new VHTMLElement(fRoot) this.vElements.clear() - this.vElements.set(0, vn) - const vDoc = new VDocument(doc) - vDoc.insertChildAt(vn, 0) + this.vElements.set(0, vHTMLElement) + const vDoc = new VDocument(() => doc as Document) + vDoc.insertChildAt(vHTMLElement, 0) this.vRoots.clear() this.vRoots.set(0, vDoc) // watchout: id==0 for both Document and documentElement // this is done for the AdoptedCSS logic @@ -198,42 +185,36 @@ export default class DOMManager extends ListWalker { this.stylesManager.reset() this.stringDict = {} return - case MType.CreateTextNode: - vn = new VText() - this.vTexts.set(msg.id, vn) + } + case MType.CreateTextNode: { + const vText = new VText() + this.vTexts.set(msg.id, vText) this.insertNode(msg) return - case MType.CreateElementNode: - 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) + } + case MType.CreateElementNode: { + const vElem = new VElement(msg.tag, msg.svg) + this.vElements.set(msg.id, vElem) this.insertNode(msg) - this.removeBodyScroll(msg.id, vn) - this.removeAutocomplete(element) + this.removeBodyScroll(msg.id, vElem) + this.removeAutocomplete(vElem) if (['STYLE', 'style', 'LINK'].includes(msg.tag)) { // Styles in priority - vn.enforceInsertion() + vElem.enforceInsertion() } return + } case MType.MoveNode: - this.insertNode(msg); + this.insertNode(msg) return - case MType.RemoveNode: - vn = this.vElements.get(msg.id) || this.vTexts.get(msg.id) - if (!vn) { logger.error("RemoveNode: Node not found", msg); return } - if (!vn.parentNode) { logger.error("RemoveNode: Parent node not found", msg); return } - vn.parentNode.removeChild(vn) + case MType.RemoveNode: { + const vChild = this.vElements.get(msg.id) || this.vTexts.get(msg.id) + if (!vChild) { logger.error("RemoveNode: Node not found", msg); return } + if (!vChild.parentNode) { logger.error("RemoveNode: Parent node not found", msg); return } + vChild.parentNode.removeChild(vChild) this.vElements.delete(msg.id) this.vTexts.delete(msg.id) return + } case MType.SetNodeAttribute: if (msg.name === 'href') this.attrsBacktrack.push(msg) else this.setNodeAttribute(msg) @@ -254,15 +235,16 @@ export default class DOMManager extends ListWalker { }) } return - case MType.RemoveNodeAttribute: - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("RemoveNodeAttribute: Node not found", msg); return } - vn.removeAttribute(msg.name) + case MType.RemoveNodeAttribute: { + const vElem = this.vElements.get(msg.id) + if (!vElem) { logger.error("RemoveNodeAttribute: Node not found", msg); return } + vElem.removeAttribute(msg.name) return - case MType.SetInputValue: - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("SetInoputValue: Node not found", msg); return } - const nodeWithValue = vn.node + } + case MType.SetInputValue: { + const vElem = this.vElements.get(msg.id) + if (!vElem) { logger.error("SetInoputValue: Node not found", msg); return } + const nodeWithValue = vElem.node if (!(nodeWithValue instanceof HTMLInputElement || nodeWithValue instanceof HTMLTextAreaElement || nodeWithValue instanceof HTMLSelectElement) @@ -271,55 +253,60 @@ export default class DOMManager extends ListWalker { return } const val = msg.mask > 0 ? '*'.repeat(msg.mask) : msg.value - doc = this.screen.document + const doc = this.screen.document if (doc && nodeWithValue === doc.activeElement) { // For the case of Remote Control nodeWithValue.onblur = () => { nodeWithValue.value = val } return } - nodeWithValue.value = val + nodeWithValue.value = val // Maybe make special VInputValueElement type for lazy value update return - case MType.SetInputChecked: - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("SetInputChecked: Node not found", msg); return } - (vn.node as HTMLInputElement).checked = msg.checked + } + case MType.SetInputChecked: { + const vElem = this.vElements.get(msg.id) + if (!vElem) { logger.error("SetInputChecked: Node not found", msg); return } + (vElem.node as HTMLInputElement).checked = msg.checked // Maybe make special VCheckableElement type for lazy checking return + } case MType.SetNodeData: - case MType.SetCssData: // mbtodo: remove css transitions when timeflow is not natural (on jumps) - vn = this.vTexts.get(msg.id) - if (!vn) { logger.error("SetCssData: Node not found", msg); return } - vn.setData(msg.data) - if (msg.tp === MType.SetCssData) { // Styles in priority (do we need inlines as well?) - vn.applyChanges() + case MType.SetCssData: { + const vText = this.vTexts.get(msg.id) + if (!vText) { logger.error("SetCssData: Node not found", msg); return } + vText.setData(msg.data) + + if (msg.tp === MType.SetCssData) { //TODOTODO + vText.applyChanges() // Styles in priority (do we need inlines as well?) } return + } - // @deprecated since 4.0.2 in favor of adopted_ss_insert/delete_rule + add_owner as being common case for StyleSheets - case MType.CssInsertRule: - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("CssInsertRule: 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 + /** @deprecated + * since 4.0.2 in favor of AdoptedSsInsertRule/DeleteRule + AdoptedSsAddOwner as a common case for StyleSheets + */ + case MType.CssInsertRule: { + let styleSheet = this.ppStyleSheetsDeprecated.get(msg.id) + if (!styleSheet) { + const vElem = this.vElements.get(msg.id) + if (!vElem) { logger.error("CssInsertRule: Node not found", msg); return } + if (vElem.tagName.toLowerCase() !== "style") { logger.error("CssInsertRule: Non-style elemtn", msg); return } + styleSheet = new PostponedStyleSheet(vElem.node as StyleElement) + this.ppStyleSheetsDeprecated.set(msg.id, styleSheet) } - vn.onStyleSheet(sheet => insertRule(sheet, msg)) + styleSheet.insertRule(msg.rule, msg.index) return - case MType.CssDeleteRule: - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("CssDeleteRule: 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 => deleteRule(sheet, msg)) + } + case MType.CssDeleteRule: { + const styleSheet = this.ppStyleSheetsDeprecated.get(msg.id) + if (!styleSheet) { logger.error("CssDeleteRule: StyleSheet was not created", msg); return } + styleSheet.deleteRule(msg.index) return - // end @deprecated - - case MType.CreateIFrameDocument: - vn = this.vElements.get(msg.frameID) - if (!vn) { logger.error("CreateIFrameDocument: Node not found", msg); return } - vn.enforceInsertion() - const host = vn.node + } + /* end @deprecated */ + case MType.CreateIFrameDocument: { + const vElem = this.vElements.get(msg.frameID) + if (!vElem) { logger.error("CreateIFrameDocument: Node not found", msg); return } + vElem.enforceInsertion() //TODOTODO + const host = vElem.node if (host instanceof HTMLIFrameElement) { const doc = host.contentDocument if (!doc) { @@ -327,14 +314,14 @@ export default class DOMManager extends ListWalker { return } - const vDoc = new VDocument(doc) + const vDoc = new VDocument(() => doc) this.vRoots.set(msg.id, vDoc) return; } else if (host instanceof Element) { // shadow DOM try { const shadowRoot = host.attachShadow({ mode: 'open' }) - vn = new VShadowRoot(shadowRoot) - this.vRoots.set(msg.id, vn) + const vRoot = new VShadowRoot(() => shadowRoot) + this.vRoots.set(msg.id, vRoot) } catch(e) { logger.warn("Can not attach shadow dom", e, msg) } @@ -342,25 +329,27 @@ export default class DOMManager extends ListWalker { logger.warn("Context message host is not Element", msg) } return - case MType.AdoptedSsInsertRule: - styleSheet = this.styleSheets.get(msg.sheetID) || this.ppStyleSheets.get(msg.sheetID) + } + case MType.AdoptedSsInsertRule: { + const styleSheet = this.styleSheets.get(msg.sheetID) || this.ppStyleSheets.get(msg.sheetID) if (!styleSheet) { logger.warn("No stylesheet was created for ", msg) return } insertRule(styleSheet, msg) return - case MType.AdoptedSsDeleteRule: - styleSheet = this.styleSheets.get(msg.sheetID) || this.ppStyleSheets.get(msg.sheetID) + } + case MType.AdoptedSsDeleteRule: { + const styleSheet = this.styleSheets.get(msg.sheetID) || this.ppStyleSheets.get(msg.sheetID) if (!styleSheet) { logger.warn("No stylesheet was created for ", msg) return } deleteRule(styleSheet, msg) return - - case MType.AdoptedSsReplace: - styleSheet = this.styleSheets.get(msg.sheetID) + } + case MType.AdoptedSsReplace: { + const styleSheet = this.styleSheets.get(msg.sheetID) if (!styleSheet) { logger.warn("No stylesheet was created for ", msg) return @@ -368,47 +357,51 @@ export default class DOMManager extends ListWalker { // @ts-ignore styleSheet.replaceSync(msg.text) return - case MType.AdoptedSsAddOwner: - vn = this.vRoots.get(msg.id) - if (!vn) { - // non-constructed case - vn = this.vElements.get(msg.id) - if (!vn) { logger.error("AdoptedSsAddOwner: Node not found", msg); return } - if (!(vn instanceof VStyleElement)) { logger.error("Non-style owner", msg); return } - this.ppStyleSheets.set(msg.sheetID, new PostponedStyleSheet(vn.node)) + } + case MType.AdoptedSsAddOwner: { + const vRoot = this.vRoots.get(msg.id) + if (!vRoot) { + /*