feat(player): lazy JS DOM node creation; (need fixes for reaching full potential)

This commit is contained in:
Alex Kaminskii 2023-04-02 22:44:36 +02:00
parent 07fc2a1212
commit 885562ec94
3 changed files with 244 additions and 233 deletions

View file

@ -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<Message> {
private readonly vTexts: Map<number, VText> = new Map() // map vs object here?
private readonly vElements: Map<number, VElement> = new Map()
private readonly vRoots: Map<number, VShadowRoot | VDocument> = new Map()
private styleSheets: Map<number, CSSStyleSheet> = new Map()
private ppStyleSheets: Map<number, PostponedStyleSheet> = new Map()
/** @depreacted since tracker 4.0.2 Mapping by nodeID */
private ppStyleSheetsDeprecated: Map<number, PostponedStyleSheet> = new Map()
private stringDict: Record<number,string> = {}
private attrsBacktrack: Message[] = []
@ -91,21 +82,20 @@ export default class DOMManager extends ListWalker<Message> {
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<Message> {
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<Message> {
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<Message> {
// 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<Message> {
}
private applyMessage = (msg: Message): Promise<any> | 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<Message> {
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<Message> {
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<Message> {
})
}
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<Message> {
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<Message> {
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<Message> {
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<Message> {
// @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) {
/* <style> tag case */
const vElem = this.vElements.get(msg.id)
if (!vElem) { logger.error("AdoptedSsAddOwner: Node not found", msg); return }
if (vElem.tagName.toLowerCase() !== "style") { logger.error("Non-style owner", msg); return }
this.ppStyleSheets.set(msg.sheetID, new PostponedStyleSheet(vElem.node as StyleElement))
return
}
styleSheet = this.styleSheets.get(msg.sheetID)
/* Constructed StyleSheet case */
let styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
let context: typeof globalThis
const rootNode = vn.node
if (rootNode.nodeType === Node.DOCUMENT_NODE) {
context = (rootNode as Document).defaultView
let context: typeof globalThis | null
if (vRoot instanceof VDocument) {
context = vRoot.node.defaultView
} else {
context = (rootNode as ShadowRoot).ownerDocument.defaultView
context = vRoot.node.ownerDocument.defaultView
}
styleSheet = new context.CSSStyleSheet()
if (!context) { logger.error("AdoptedSsAddOwner: Root node default view not found", msg); return }
styleSheet = new context.CSSStyleSheet() /* a StyleSheet from another Window context won't work */
this.styleSheets.set(msg.sheetID, styleSheet)
}
//@ts-ignore
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets, styleSheet]
// @ts-ignore
vRoot.node.adoptedStyleSheets = [...vRoot.node.adoptedStyleSheets, styleSheet]
return
case MType.AdoptedSsRemoveOwner:
styleSheet = this.styleSheets.get(msg.sheetID)
}
case MType.AdoptedSsRemoveOwner: {
const styleSheet = this.styleSheets.get(msg.sheetID)
if (!styleSheet) {
logger.warn("No stylesheet was created for ", msg)
return
}
vn = this.vRoots.get(msg.id)
if (!vn) { logger.error("AdoptedSsRemoveOwner: Node not found", msg); return }
const vRoot = this.vRoots.get(msg.id)
if (!vRoot) { logger.error("AdoptedSsRemoveOwner: Node not found", msg); return }
//@ts-ignore
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets].filter(s => s !== styleSheet)
vRoot.node.adoptedStyleSheets = [...vRoot.node.adoptedStyleSheets].filter(s => s !== styleSheet)
return
case MType.LoadFontFace:
vn = this.vRoots.get(msg.parentID)
if (!vn) { logger.error("LoadFontFace: Node not found", msg); return }
if (vn instanceof VShadowRoot) { logger.error(`Node ${vn} expected to be a Document`, msg); return }
let descr: Object
}
case MType.LoadFontFace: {
const vRoot = this.vRoots.get(msg.parentID)
if (!vRoot) { logger.error("LoadFontFace: Node not found", msg); return }
if (vRoot instanceof VShadowRoot) { logger.error(`Node ${vRoot} expected to be a Document`, msg); return }
let descr: Object | undefined
try {
descr = JSON.parse(msg.descriptors)
descr = typeof descr === 'object' ? descr : undefined
@ -416,8 +409,9 @@ export default class DOMManager extends ListWalker<Message> {
logger.warn("Can't parse font-face descriptors: ", msg)
}
const ff = new FontFace(msg.family, msg.source, descr)
vn.node.fonts.add(ff)
vRoot.node.fonts.add(ff)
return ff.load()
}
}
}
@ -481,7 +475,7 @@ export default class DOMManager extends ListWalker<Message> {
this.nodeScrollManagers.forEach(manager => {
const msg = manager.moveGetLast(t)
if (msg) {
let vNode: VNode
let vNode: VElement | VDocument | VShadowRoot | undefined
if (vNode = this.vElements.get(msg.id)) {
vNode.node.scrollLeft = msg.x
vNode.node.scrollTop = msg.y

View file

@ -1,11 +1,45 @@
type VChild = VElement | VText
export type VNode = VDocument | VShadowRoot | VElement | VText
import { insertRule, deleteRule } from './safeCSSRules';
abstract class VParent {
abstract node: Node | null
type Callback<T> = (o: T) => void
/**
* Virtual Node base class.
* Implements common abstract methods and lazy node creation logic.
*
* @privateRemarks
* Would be better to export type-only, but didn't find a nice way to do that.
*/
export abstract class VNode<T extends Node = Node> {
protected abstract createNode(): T
private _node: T | null
/**
* JS DOM Node getter with lazy node creation
*
* @returns underneath JS DOM Node
* @remarks should not be called unless the real node is required since creation might be expensive
*/
get node(): T {
if (!this._node) {
const node = this._node = this.createNode()
this.nodeCallbacks.forEach(cb => cb(node))
this.nodeCallbacks = []
}
return this._node
}
private nodeCallbacks: Callback<T>[] = []
onNode(callback: Callback<T>) {
if (this._node) {
callback(this._node)
return
}
this.nodeCallbacks.push(callback)
}
public abstract applyChanges(): void
}
type VChild = VElement | VText
abstract class VParent<T extends Node = Node> extends VNode<T>{
protected children: VChild[] = []
private insertedChildren: Set<VChild> = new Set()
@ -26,12 +60,7 @@ abstract class VParent {
applyChanges() {
const node = this.node
if (!node) {
// log err
console.error("No node found", this)
return
}
// inserting
/* Inserting */
for (let i = this.children.length-1; i >= 0; i--) {
const child = this.children[i]
child.applyChanges()
@ -41,29 +70,25 @@ abstract class VParent {
}
}
this.insertedChildren.clear()
// removing
/* Removing in-between */
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
/* Removing tail */
while(realChildren.length > this.children.length) {
node.removeChild(node.lastChild)
node.removeChild(node.lastChild as Node) /* realChildren.length > this.children.length >= 0 */
}
}
}
export class VDocument extends VParent {
constructor(public readonly node: Document) { super() }
export class VDocument extends VParent<Document> {
constructor(protected readonly createNode: () => Document) { super() }
applyChanges() {
if (this.children.length > 1) {
// log err
}
if (!this.node) {
// iframe not mounted yet
return
console.error("VDocument expected to have a single child.", this)
}
const child = this.children[0]
if (!child) { return }
@ -75,14 +100,20 @@ export class VDocument extends VParent {
}
}
export class VShadowRoot extends VParent {
constructor(public readonly node: ShadowRoot) { super() }
export class VShadowRoot extends VParent<ShadowRoot> {
constructor(protected readonly createNode: () => ShadowRoot) { super() }
}
export class VElement extends VParent {
export class VElement extends VParent<Element> {
parentNode: VParent | null = null
private newAttributes: Map<string, string | false> = new Map()
constructor(public readonly node: Element) { super() }
constructor(readonly tagName: string, readonly isSVG = false) { super() }
protected createNode(){
return this.isSVG
? document.createElementNS('http://www.w3.org/2000/svg', this.tagName)
: document.createElement(this.tagName)
}
setAttribute(name: string, value: string) {
this.newAttributes.set(name, value)
}
@ -90,17 +121,6 @@ export class VElement extends VParent {
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) {
@ -116,51 +136,54 @@ export class VElement extends VParent {
this.newAttributes.clear()
super.applyChanges()
}
// TODO: 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()
}
}
export class VHTMLElement extends VElement {
constructor(node: HTMLElement) {
super("HTML", false)
this.createNode = () => node
}
}
export class VText extends VNode<Text> {
parentNode: VParent | null = null
protected createNode() {
return 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
}
}
}
type StyleSheetCallback = (s: CSSStyleSheet) => void
export type StyleElement = HTMLStyleElement | SVGStyleElement
// @deprecated TODO: remove in favor of PostponedStyleSheet
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))
this.stylesheetCallbacks = []
} else {
// console.warn("Style onload: sheet is null") ?
// sometimes logs sheet ton of errors for some reason
}
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 PostponedStyleSheet {
private loaded = false
private stylesheetCallbacks: StyleSheetCallback[] = []
private stylesheetCallbacks: Callback<CSSStyleSheet>[] = []
constructor(private readonly node: StyleElement) {
node.onload = () => {
constructor(private readonly node: StyleElement) { //TODO: use virtual DOM + onNode callback for better lazy node init
node.addEventListener("load", () => {
const sheet = node.sheet
if (sheet) {
this.stylesheetCallbacks.forEach(cb => cb(sheet))
@ -169,10 +192,10 @@ export class PostponedStyleSheet {
console.warn("Style node onload: sheet is null")
}
this.loaded = true
}
})
}
private applyCallback(cb: StyleSheetCallback) {
private applyCallback(cb: Callback<CSSStyleSheet>) {
if (this.loaded) {
if (!this.node.sheet) {
console.warn("Style tag is loaded, but sheet is null")
@ -192,21 +215,3 @@ export class PostponedStyleSheet {
this.applyCallback(s => deleteRule(s, { index }))
}
}
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
}
}
}

View file

@ -15,6 +15,18 @@ import { MType } from '../raw.gen'
import { resolveURL, resolveCSS } from './urlResolve'
import { HOVER_CLASSNAME, FOCUS_CLASSNAME } from './constants'
/* maybetodo: filter out non-relevant prefixes in CSS-rules.
They might cause an error in console, but not sure if it breaks the replay.
(research required)
*/
// function replaceCSSPrefixes(css: string) {
// return css
// .replace(/\-ms\-/g, "")
// .replace(/\-webkit\-/g, "")
// .replace(/\-moz\-/g, "")
// .replace(/\-webkit\-/g, "")
// }
const HOVER_SELECTOR = `.${HOVER_CLASSNAME}`
const FOCUS_SELECTOR = `.${FOCUS_CLASSNAME}`
export function replaceCSSPseudoclasses(cssText: string): string {