Merge pull request #1169 from openreplay/player-virtual-dom-lazy-creation
Player virtual dom lazy creation
This commit is contained in:
commit
b9a124d332
5 changed files with 485 additions and 396 deletions
|
|
@ -105,11 +105,11 @@ export default class ListWalker<T extends Timed> {
|
|||
: null
|
||||
}
|
||||
|
||||
/*
|
||||
Returns last message with the time <= t.
|
||||
Assumed that the current message is already handled so
|
||||
if pointer doesn't cahnge <null> is returned.
|
||||
*/
|
||||
/**
|
||||
* @returns last message with the time <= t.
|
||||
* Assumed that the current message is already handled so
|
||||
* if pointer doesn't cahnge <null> is returned.
|
||||
*/
|
||||
moveGetLast(t: number, index?: number): T | null {
|
||||
let key: string = "time"; //TODO
|
||||
let val = t;
|
||||
|
|
@ -130,7 +130,13 @@ export default class ListWalker<T extends Timed> {
|
|||
return changed ? this.list[ this.p - 1 ] : null;
|
||||
}
|
||||
|
||||
async moveWait(t: number, callback: (msg: T) => Promise<any> | undefined): Promise<void> {
|
||||
/**
|
||||
* Moves over the messages starting from the current+1 to the last one with the time <= t
|
||||
* applying callback on each of them
|
||||
* @param t - max message time to move to; will move & apply callback while msg.time <= t
|
||||
* @param callback - a callback to apply on each message passing by while moving
|
||||
*/
|
||||
moveApply(t: number, callback: (msg: T) => void): void {
|
||||
// Applying only in increment order for now
|
||||
if (t < this.timeNow) {
|
||||
this.reset();
|
||||
|
|
@ -138,8 +144,7 @@ export default class ListWalker<T extends Timed> {
|
|||
|
||||
const list = this.list
|
||||
while (list[this.p] && list[this.p].time <= t) {
|
||||
const maybePromise = callback(this.list[ this.p++ ]);
|
||||
if (maybePromise) { await maybePromise }
|
||||
callback(this.list[ this.p++ ])
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Store } from './types'
|
||||
|
||||
// (not a type)
|
||||
export default class SimpleSore<G, S=G> implements Store<G, S> {
|
||||
constructor(private state: G){}
|
||||
get(): G {
|
||||
|
|
|
|||
|
|
@ -9,40 +9,36 @@ import FocusManager from './FocusManager';
|
|||
import SelectionManager from './SelectionManager';
|
||||
import type { StyleElement } from './VirtualDOM';
|
||||
import {
|
||||
PostponedStyleSheet,
|
||||
OnloadStyleSheet,
|
||||
VDocument,
|
||||
VElement,
|
||||
VHTMLElement,
|
||||
VNode,
|
||||
VShadowRoot,
|
||||
VStyleElement,
|
||||
VText,
|
||||
OnloadVRoot,
|
||||
} from './VirtualDOM';
|
||||
import { deleteRule, insertRule } from './safeCSSRules';
|
||||
|
||||
type HTMLElementWithValue = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
|
||||
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, "")
|
||||
// }
|
||||
function isStyleVElement(vElem: VElement): vElem is VElement & { node: StyleElement } {
|
||||
return vElem.tagName.toLowerCase() === "style"
|
||||
}
|
||||
|
||||
const IGNORED_ATTRS = [ "autocomplete" ]
|
||||
const ATTR_NAME_REGEXP = /([^\t\n\f \/>"'=]+)/
|
||||
|
||||
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()
|
||||
private readonly olVRoots: Map<number, OnloadVRoot> = new Map()
|
||||
/** Constructed StyleSheets https://developer.mozilla.org/en-US/docs/Web/API/Document/adoptedStyleSheets
|
||||
* as well as <style> tag owned StyleSheets
|
||||
*/
|
||||
private olStyleSheets: Map<number, OnloadStyleSheet> = new Map()
|
||||
/** @depreacted since tracker 4.0.2 Mapping by nodeID */
|
||||
private olStyleSheetsDeprecated: Map<number, OnloadStyleSheet> = new Map()
|
||||
private stringDict: Record<number,string> = {}
|
||||
private attrsBacktrack: Message[] = []
|
||||
|
||||
private upperBodyId: number = -1;
|
||||
private nodeScrollManagers: Map<number, ListWalker<SetNodeScroll>> = new Map()
|
||||
|
|
@ -91,21 +87,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;
|
||||
|
|
@ -117,22 +112,24 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
logger.error("Insert error. Node not found", id);
|
||||
return;
|
||||
}
|
||||
const parent = this.vElements.get(parentID) || this.vRoots.get(parentID)
|
||||
const parent = this.vElements.get(parentID) || this.olVRoots.get(parentID)
|
||||
if (!parent) {
|
||||
logger.error("Insert error. Parent node not found", parentID, this.vElements, this.vRoots);
|
||||
logger.error("Insert error. Parent vNode not found", parentID, this.vElements, this.olVRoots);
|
||||
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 &&
|
||||
pNode.innerText.trim().length === 0
|
||||
) {
|
||||
logger.log("Trying to insert child to a style tag with virtual rules: ", parent, child);
|
||||
return;
|
||||
if (parent instanceof VElement && isStyleVElement(parent)) {
|
||||
// TODO: if this ever happens? ; Maybe do not send empty TextNodes in tracker
|
||||
const styleNode = parent.node
|
||||
if (styleNode.sheet &&
|
||||
styleNode.sheet.cssRules &&
|
||||
styleNode.sheet.cssRules.length > 0 &&
|
||||
styleNode.textContent &&
|
||||
styleNode.textContent.trim().length === 0
|
||||
) {
|
||||
logger.log("Trying to insert child to a style tag with virtual rules: ", parent, child);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
parent.insertChildAt(child, index)
|
||||
|
|
@ -143,25 +140,27 @@ 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") {
|
||||
// Otherwise binds local autocomplete values (maybe should ignore on the tracker level)
|
||||
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");
|
||||
// }
|
||||
if (!value.startsWith("http")) {
|
||||
/* blob:... value can happen here for some reason.
|
||||
* which will result in that link being unable to load and having 4sec timeout in the below function.
|
||||
*/
|
||||
return
|
||||
}
|
||||
// blob:... value can happen here for some reason.
|
||||
// which will result in that link being unable to load and having 4sec timeout in the below function.
|
||||
|
||||
// TODO: check if node actually exists on the page, not just in memory
|
||||
// TODOTODO: 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(")) {
|
||||
/* SVG shape ID-s for masks etc. Sometimes referred with the full-page url, which we don't have in replay */
|
||||
value = "url(#" + (value.split("#")[1] ||")")
|
||||
}
|
||||
vn.setAttribute(name, value)
|
||||
|
|
@ -169,12 +168,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,58 +181,51 @@ 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.vRoots.clear()
|
||||
this.vRoots.set(0, vDoc) // watchout: id==0 for both Document and documentElement
|
||||
this.vElements.set(0, vHTMLElement)
|
||||
const vDoc = OnloadVRoot.fromDocumentNode(doc)
|
||||
vDoc.insertChildAt(vHTMLElement, 0)
|
||||
this.olVRoots.clear()
|
||||
this.olVRoots.set(0, vDoc) // watchout: id==0 for both Document and documentElement
|
||||
// this is done for the AdoptedCSS logic
|
||||
// todo: start from 0-node (sync logic with tracker)
|
||||
// Maybetodo: start Document as 0-node in tracker
|
||||
this.vTexts.clear()
|
||||
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)
|
||||
}
|
||||
case MType.CreateElementNode: {
|
||||
const vElem = new VElement(msg.tag, msg.svg)
|
||||
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) {
|
||||
vElem.prioritized = true
|
||||
}
|
||||
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.vElements.set(msg.id, vElem)
|
||||
this.insertNode(msg)
|
||||
this.removeBodyScroll(msg.id, vn)
|
||||
this.removeAutocomplete(element)
|
||||
if (['STYLE', 'style', 'LINK'].includes(msg.tag)) { // Styles in priority
|
||||
vn.enforceInsertion()
|
||||
}
|
||||
this.removeBodyScroll(msg.id, vElem)
|
||||
this.removeAutocomplete(vElem)
|
||||
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)
|
||||
this.setNodeAttribute(msg)
|
||||
return
|
||||
case MType.StringDict:
|
||||
this.stringDict[msg.key] = msg.value
|
||||
|
|
@ -245,24 +234,22 @@ export default class DOMManager extends ListWalker<Message> {
|
|||
this.stringDict[msg.nameKey] === undefined && logger.error("No dictionary key for msg 'name': ", msg)
|
||||
this.stringDict[msg.valueKey] === undefined && logger.error("No dictionary key for msg 'value': ", msg)
|
||||
if (this.stringDict[msg.nameKey] === undefined || this.stringDict[msg.valueKey] === undefined ) { return }
|
||||
if (this.stringDict[msg.nameKey] === 'href') this.attrsBacktrack.push(msg)
|
||||
else {
|
||||
this.setNodeAttribute({
|
||||
id: msg.id,
|
||||
name: this.stringDict[msg.nameKey],
|
||||
value: this.stringDict[msg.valueKey],
|
||||
})
|
||||
}
|
||||
this.setNodeAttribute({
|
||||
id: msg.id,
|
||||
name: this.stringDict[msg.nameKey],
|
||||
value: this.stringDict[msg.valueKey],
|
||||
})
|
||||
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,222 +258,183 @@ 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("SetNodeData/SetCssData: Node not found", msg); return }
|
||||
vText.setData(msg.data)
|
||||
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.olStyleSheetsDeprecated.get(msg.id)
|
||||
if (!styleSheet) {
|
||||
const vElem = this.vElements.get(msg.id)
|
||||
if (!vElem) { logger.error("CssInsertRule: Node not found", msg); return }
|
||||
if (!isStyleVElement(vElem)) { logger.error("CssInsertRule: Non-style element", msg); return }
|
||||
styleSheet = OnloadStyleSheet.fromStyleElement(vElem.node)
|
||||
this.olStyleSheetsDeprecated.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.olStyleSheetsDeprecated.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
|
||||
if (host instanceof HTMLIFrameElement) {
|
||||
const doc = host.contentDocument
|
||||
if (!doc) {
|
||||
logger.warn("No default iframe doc", msg, host)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
} catch(e) {
|
||||
logger.warn("Can not attach shadow dom", e, msg)
|
||||
}
|
||||
} else {
|
||||
logger.warn("Context message host is not Element", msg)
|
||||
}
|
||||
}
|
||||
/* end @deprecated */
|
||||
case MType.CreateIFrameDocument: {
|
||||
const vElem = this.vElements.get(msg.frameID)
|
||||
if (!vElem) { logger.error("CreateIFrameDocument: Node not found", msg); return }
|
||||
const vRoot = OnloadVRoot.fromVElement(vElem)
|
||||
vRoot.catch(e => logger.warn(e, msg))
|
||||
this.olVRoots.set(msg.id, vRoot)
|
||||
return
|
||||
case MType.AdoptedSsInsertRule:
|
||||
styleSheet = this.styleSheets.get(msg.sheetID) || this.ppStyleSheets.get(msg.sheetID)
|
||||
}
|
||||
case MType.AdoptedSsInsertRule: {
|
||||
const styleSheet = this.olStyleSheets.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.olStyleSheets.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.olStyleSheets.get(msg.sheetID)
|
||||
if (!styleSheet) {
|
||||
logger.warn("No stylesheet was created for ", msg)
|
||||
return
|
||||
}
|
||||
// @ts-ignore
|
||||
// @ts-ignore (configure ts with recent WebaAPI)
|
||||
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))
|
||||
return
|
||||
}
|
||||
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
|
||||
} else {
|
||||
context = (rootNode as ShadowRoot).ownerDocument.defaultView
|
||||
}
|
||||
styleSheet = new context.CSSStyleSheet()
|
||||
this.styleSheets.set(msg.sheetID, styleSheet)
|
||||
}
|
||||
//@ts-ignore
|
||||
vn.node.adoptedStyleSheets = [...vn.node.adoptedStyleSheets, styleSheet]
|
||||
return
|
||||
case MType.AdoptedSsRemoveOwner:
|
||||
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 }
|
||||
//@ts-ignore
|
||||
vn.node.adoptedStyleSheets = [...vn.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
|
||||
try {
|
||||
descr = JSON.parse(msg.descriptors)
|
||||
descr = typeof descr === 'object' ? descr : undefined
|
||||
} catch {
|
||||
logger.warn("Can't parse font-face descriptors: ", msg)
|
||||
}
|
||||
const ff = new FontFace(msg.family, msg.source, descr)
|
||||
vn.node.fonts.add(ff)
|
||||
return ff.load()
|
||||
}
|
||||
}
|
||||
|
||||
applyBacktrack(msg: Message) {
|
||||
// @ts-ignore
|
||||
const target = this.vElements.get(msg.id)
|
||||
if (!target) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (msg.tp) {
|
||||
case MType.SetNodeAttribute: {
|
||||
this.setNodeAttribute(msg)
|
||||
return
|
||||
}
|
||||
case MType.SetNodeAttributeDict: {
|
||||
this.stringDict[msg.nameKey] === undefined && logger.error("No dictionary key for msg 'name': ", msg)
|
||||
this.stringDict[msg.valueKey] === undefined && logger.error("No dictionary key for msg 'value': ", msg)
|
||||
if (this.stringDict[msg.nameKey] === undefined || this.stringDict[msg.valueKey] === undefined) {
|
||||
case MType.AdoptedSsAddOwner: {
|
||||
const vRoot = this.olVRoots.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 (!isStyleVElement(vElem)) { logger.error("Non-style owner", msg); return }
|
||||
this.olStyleSheets.set(msg.sheetID, OnloadStyleSheet.fromStyleElement(vElem.node))
|
||||
return
|
||||
}
|
||||
this.setNodeAttribute({
|
||||
id: msg.id,
|
||||
name: this.stringDict[msg.nameKey],
|
||||
value: this.stringDict[msg.valueKey],
|
||||
/* Constructed StyleSheet case */
|
||||
let olStyleSheet = this.olStyleSheets.get(msg.sheetID)
|
||||
if (!olStyleSheet) {
|
||||
olStyleSheet = OnloadStyleSheet.fromVRootContext(vRoot)
|
||||
this.olStyleSheets.set(msg.sheetID, olStyleSheet)
|
||||
}
|
||||
olStyleSheet.whenReady(styleSheet => {
|
||||
vRoot.onNode(node => {
|
||||
// @ts-ignore
|
||||
node.adoptedStyleSheets = [...node.adoptedStyleSheets, styleSheet]
|
||||
})
|
||||
})
|
||||
return;
|
||||
return
|
||||
}
|
||||
case MType.AdoptedSsRemoveOwner: {
|
||||
const olStyleSheet = this.olStyleSheets.get(msg.sheetID)
|
||||
if (!olStyleSheet) {
|
||||
logger.warn("AdoptedSsRemoveOwner: No stylesheet was created for ", msg)
|
||||
return
|
||||
}
|
||||
const vRoot = this.olVRoots.get(msg.id)
|
||||
if (!vRoot) { logger.error("AdoptedSsRemoveOwner: Owner node not found", msg); return }
|
||||
olStyleSheet.whenReady(styleSheet => {
|
||||
vRoot.onNode(node => {
|
||||
// @ts-ignore
|
||||
node.adoptedStyleSheets = [...vRoot.node.adoptedStyleSheets].filter(s => s !== styleSheet)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
case MType.LoadFontFace: {
|
||||
const vRoot = this.olVRoots.get(msg.parentID)
|
||||
if (!vRoot) { logger.error("LoadFontFace: Node not found", msg); return }
|
||||
vRoot.whenReady(vNode => {
|
||||
if (vNode instanceof VShadowRoot) { logger.error(`Node ${vNode} expected to be a Document`, msg); return }
|
||||
let descr: Object | undefined
|
||||
try {
|
||||
descr = JSON.parse(msg.descriptors)
|
||||
descr = typeof descr === 'object' ? descr : undefined
|
||||
} catch {
|
||||
logger.warn("Can't parse font-face descriptors: ", msg)
|
||||
}
|
||||
const ff = new FontFace(msg.family, msg.source, descr)
|
||||
vNode.node.fonts.add(ff)
|
||||
ff.load() // TODOTODO: wait for this one in StylesManager in a common way with styles
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves and applies all the messages from the current (or from the beginning, if t < current.time)
|
||||
* to the one with msg.time >= `t`
|
||||
*
|
||||
* This function autoresets pointer if necessary (better name?)
|
||||
*
|
||||
* @returns Promise that fulfulls when necessary changes get applied
|
||||
* (the async part exists mostly due to styles loading)
|
||||
*/
|
||||
async moveReady(t: number): Promise<void> {
|
||||
// MBTODO (back jump optimisation):
|
||||
// - store intemediate virtual dom state
|
||||
// - cancel previous moveReady tasks (is it possible?) if new timestamp is less
|
||||
// This function autoresets pointer if necessary (better name?)
|
||||
this.moveApply(t, this.applyMessage)
|
||||
|
||||
/**
|
||||
* Basically just skipping all set attribute with attrs being "href" if user is 'jumping'
|
||||
* to the other point of replay to save time on NOT downloading any resources before the dom tree changes
|
||||
* are applied, so it won't try to download and then cancel when node is created in msg N and removed in msg N+2
|
||||
* which produces weird bug when asset is cached (10-25ms delay)
|
||||
* */
|
||||
// http://0.0.0.0:3333/5/session/8452905874437457
|
||||
// 70 iframe, 8 create element - STYLE tag
|
||||
await this.moveWait(t, this.applyMessage)
|
||||
|
||||
this.attrsBacktrack.forEach(msg => {
|
||||
this.applyBacktrack(msg)
|
||||
})
|
||||
this.attrsBacktrack = []
|
||||
|
||||
this.vRoots.forEach(rt => rt.applyChanges()) // MBTODO (optimisation): affected set
|
||||
this.olVRoots.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 focus
|
||||
/* Waiting for styles to be applied first */
|
||||
/* Applying focus */
|
||||
this.focusManager.move(t)
|
||||
/* Applying text selections */
|
||||
this.selectionManager.move(t)
|
||||
// Apply all scrolls after the styles got applied
|
||||
/* Applying all scrolls */
|
||||
this.nodeScrollManagers.forEach(manager => {
|
||||
const msg = manager.moveGetLast(t)
|
||||
if (msg) {
|
||||
let vNode: VNode
|
||||
if (vNode = this.vElements.get(msg.id)) {
|
||||
vNode.node.scrollLeft = msg.x
|
||||
vNode.node.scrollTop = msg.y
|
||||
} else if ((vNode = this.vRoots.get(msg.id)) && vNode instanceof VDocument){
|
||||
vNode.node.defaultView?.scrollTo(msg.x, msg.y)
|
||||
let scrollVHost: VElement | OnloadVRoot | undefined
|
||||
if (scrollVHost = this.vElements.get(msg.id)) {
|
||||
scrollVHost.node.scrollLeft = msg.x
|
||||
scrollVHost.node.scrollTop = msg.y
|
||||
} else if ((scrollVHost = this.olVRoots.get(msg.id))) {
|
||||
scrollVHost.whenReady(vNode => {
|
||||
if (vNode instanceof VDocument) {
|
||||
vNode.node.defaultView?.scrollTo(msg.x, msg.y)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,69 +1,119 @@
|
|||
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
|
||||
* It is better to use `onNode` callback applicator unless in the `applyChanges` implementation
|
||||
*/
|
||||
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>[] = []
|
||||
/**
|
||||
* Lazy Node callback applicator
|
||||
*
|
||||
* @param callback - Callback that fires on existing JS DOM Node instantly if it exists
|
||||
* or whenever it gets created. Call sequence is concerned.
|
||||
*/
|
||||
onNode(callback: Callback<T>) {
|
||||
if (this._node) {
|
||||
callback(this._node)
|
||||
return
|
||||
}
|
||||
this.nodeCallbacks.push(callback)
|
||||
}
|
||||
/**
|
||||
* Abstract method, should be implemented by the actual classes
|
||||
* It is supposed to apply virtual changes into the actual DOM
|
||||
*/
|
||||
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()
|
||||
private childrenToMount: Set<VChild> = new Set()
|
||||
|
||||
insertChildAt(child: VChild, index: number) {
|
||||
if (child.parentNode) {
|
||||
child.parentNode.removeChild(child)
|
||||
}
|
||||
this.children.splice(index, 0, child)
|
||||
this.insertedChildren.add(child)
|
||||
this.childrenToMount.add(child)
|
||||
child.parentNode = this
|
||||
}
|
||||
|
||||
removeChild(child: VChild) {
|
||||
this.children = this.children.filter(ch => ch !== child)
|
||||
this.insertedChildren.delete(child)
|
||||
this.childrenToMount.delete(child)
|
||||
child.parentNode = null
|
||||
}
|
||||
|
||||
applyChanges() {
|
||||
const node = this.node
|
||||
if (!node) {
|
||||
// log err
|
||||
console.error("No node found", this)
|
||||
return
|
||||
}
|
||||
// inserting
|
||||
protected mountChildren(shouldInsert?: (child: VChild) => boolean) {
|
||||
let nextMounted: VChild | null = null
|
||||
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)
|
||||
if (this.childrenToMount.has(child) &&
|
||||
(!shouldInsert || shouldInsert(child)) // is there a better way of not-knowing about subclass logic on prioritized insertion?
|
||||
) {
|
||||
this.node.insertBefore(child.node, nextMounted ? nextMounted.node : null)
|
||||
this.childrenToMount.delete(child)
|
||||
}
|
||||
if (!this.childrenToMount.has(child)) {
|
||||
nextMounted = child
|
||||
}
|
||||
}
|
||||
this.insertedChildren.clear()
|
||||
// removing
|
||||
}
|
||||
|
||||
applyChanges() {
|
||||
/* Building a sub-trees first (in-memory for non-mounted children) */
|
||||
this.children.forEach(child => child.applyChanges())
|
||||
/* Inserting */
|
||||
this.mountChildren()
|
||||
if (this.childrenToMount.size !== 0) {
|
||||
console.error("VParent: Something went wrong with children insertion")
|
||||
}
|
||||
/* Removing in-between */
|
||||
const node = this.node
|
||||
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 so it is not null */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +125,22 @@ 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 {
|
||||
parentNode: VParent | null = null
|
||||
export type VRoot = VDocument | VShadowRoot
|
||||
|
||||
export class VElement extends VParent<Element> {
|
||||
parentNode: VParent | null = null /** Should be modified only by he parent itself */
|
||||
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,18 +148,7 @@ 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() {
|
||||
private applyAttributeChanges() { // "changes" -> "updates" ?
|
||||
this.newAttributes.forEach((value, key) => {
|
||||
if (value === false) {
|
||||
this.node.removeAttribute(key)
|
||||
|
|
@ -114,88 +161,57 @@ export class VElement extends VParent {
|
|||
}
|
||||
})
|
||||
this.newAttributes.clear()
|
||||
}
|
||||
|
||||
applyChanges() {
|
||||
this.prioritized && this.applyPrioritizedChanges()
|
||||
this.applyAttributeChanges()
|
||||
super.applyChanges()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
/** Insertion Prioritization
|
||||
* Made for styles that should be inserted as prior,
|
||||
* otherwise it will show visual styling lag if there is a transition CSS property)
|
||||
*/
|
||||
prioritized = false
|
||||
insertChildAt(child: VChild, index: number) {
|
||||
super.insertChildAt(child, index)
|
||||
/* Bubble prioritization */
|
||||
if (child instanceof VElement && child.prioritized) {
|
||||
let parent: VParent | null = this
|
||||
while (parent instanceof VElement && !parent.prioritized) {
|
||||
parent.prioritized = true
|
||||
parent = parent.parentNode
|
||||
}
|
||||
cb(this.node.sheet)
|
||||
} else {
|
||||
this.stylesheetCallbacks.push(cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class PostponedStyleSheet {
|
||||
private loaded = false
|
||||
private stylesheetCallbacks: StyleSheetCallback[] = []
|
||||
|
||||
constructor(private readonly node: StyleElement) {
|
||||
node.onload = () => {
|
||||
const sheet = node.sheet
|
||||
if (sheet) {
|
||||
this.stylesheetCallbacks.forEach(cb => cb(sheet))
|
||||
this.stylesheetCallbacks = []
|
||||
} else {
|
||||
console.warn("Style node onload: sheet is null")
|
||||
}
|
||||
this.loaded = true
|
||||
}
|
||||
}
|
||||
|
||||
private applyCallback(cb: StyleSheetCallback) {
|
||||
if (this.loaded) {
|
||||
if (!this.node.sheet) {
|
||||
console.warn("Style tag is loaded, but sheet is null")
|
||||
return
|
||||
private applyPrioritizedChanges() {
|
||||
this.children.forEach(child => {
|
||||
if (child instanceof VText) {
|
||||
child.applyChanges()
|
||||
} else if (child.prioritized) {
|
||||
/* Update prioritized VElement-s */
|
||||
child.applyPrioritizedChanges()
|
||||
child.applyAttributeChanges()
|
||||
}
|
||||
cb(this.node.sheet)
|
||||
} else {
|
||||
this.stylesheetCallbacks.push(cb)
|
||||
}
|
||||
}
|
||||
|
||||
insertRule(rule: string, index: number) {
|
||||
this.applyCallback(s => insertRule(s, { rule, index }))
|
||||
}
|
||||
|
||||
deleteRule(index: number) {
|
||||
this.applyCallback(s => deleteRule(s, { index }))
|
||||
})
|
||||
this.mountChildren(child => child instanceof VText || child.prioritized)
|
||||
}
|
||||
}
|
||||
|
||||
export class VText {
|
||||
export class VHTMLElement extends VElement {
|
||||
constructor(node: HTMLElement) {
|
||||
super("HTML", false)
|
||||
this.createNode = () => node
|
||||
}
|
||||
}
|
||||
|
||||
export class VText extends VNode<Text> {
|
||||
parentNode: VParent | null = null
|
||||
constructor(public readonly node: Text = new Text()) {}
|
||||
protected createNode() {
|
||||
return new Text()
|
||||
}
|
||||
|
||||
private data: string = ""
|
||||
private changed: boolean = false
|
||||
setData(data: string) {
|
||||
|
|
@ -210,3 +226,112 @@ export class VText {
|
|||
}
|
||||
}
|
||||
|
||||
class PromiseQueue<T> {
|
||||
constructor(private promise: Promise<T>) {}
|
||||
/**
|
||||
* Call sequence is concerned.
|
||||
*/
|
||||
// Doing this with callbacks list instead might be more efficient (but more wordy). TODO: research
|
||||
whenReady(cb: Callback<T>) {
|
||||
this.promise = this.promise.then(vRoot => {
|
||||
cb(vRoot)
|
||||
return vRoot
|
||||
})
|
||||
}
|
||||
catch(cb: Parameters<Promise<T>['catch']>[0]) {
|
||||
this.promise.catch(cb)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* VRoot wrapper that allows to defer all the API calls till the moment
|
||||
* when VNode CAN be created (for example, on <iframe> mount&load)
|
||||
*/
|
||||
export class OnloadVRoot extends PromiseQueue<VRoot> {
|
||||
static fromDocumentNode(doc: Document): OnloadVRoot {
|
||||
return new OnloadVRoot(Promise.resolve(new VDocument(() => doc)))
|
||||
}
|
||||
static fromVElement(vElem: VElement): OnloadVRoot {
|
||||
return new OnloadVRoot(new Promise((resolve, reject) => {
|
||||
vElem.onNode(host => {
|
||||
if (host instanceof HTMLIFrameElement) {
|
||||
/* IFrame case: creating Document */
|
||||
const doc = host.contentDocument
|
||||
if (doc) {
|
||||
resolve(new VDocument(() => doc))
|
||||
} else {
|
||||
host.addEventListener('load', () => {
|
||||
const doc = host.contentDocument
|
||||
if (doc) {
|
||||
resolve(new VDocument(() => doc))
|
||||
} else {
|
||||
reject("No default Document found on iframe load") // Send `host` for logging as well
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
/* ShadowDom case */
|
||||
try {
|
||||
const shadowRoot = host.attachShadow({ mode: 'open' })
|
||||
resolve(new VShadowRoot(() => shadowRoot))
|
||||
} catch(e) {
|
||||
reject(e) // "Can not attach shadow dom"
|
||||
}
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
onNode(cb: Callback<Document | ShadowRoot>) {
|
||||
this.whenReady(vRoot => vRoot.onNode(cb))
|
||||
}
|
||||
applyChanges() {
|
||||
this.whenReady(vRoot => vRoot.applyChanges())
|
||||
}
|
||||
insertChildAt(...args: Parameters<VParent['insertChildAt']>) {
|
||||
this.whenReady(vRoot => vRoot.insertChildAt(...args))
|
||||
}
|
||||
}
|
||||
|
||||
export type StyleElement = HTMLStyleElement | SVGStyleElement
|
||||
|
||||
/**
|
||||
* CSSStyleSheet wrapper that collects all the insertRule/deleteRule calls
|
||||
* and then applies them when the sheet is ready
|
||||
*/
|
||||
export class OnloadStyleSheet extends PromiseQueue<CSSStyleSheet> {
|
||||
static fromStyleElement(node: StyleElement) {
|
||||
return new OnloadStyleSheet(new Promise((resolve, reject) => {
|
||||
node.addEventListener("load", () => {
|
||||
const sheet = node.sheet
|
||||
if (sheet) {
|
||||
resolve(sheet)
|
||||
} else {
|
||||
reject("Style node onload: sheet is null")
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
static fromVRootContext(vRoot: OnloadVRoot) {
|
||||
return new OnloadStyleSheet(new Promise((resolve, reject) =>
|
||||
vRoot.onNode(node => {
|
||||
let context: typeof globalThis | null
|
||||
if (node instanceof Document) {
|
||||
context = node.defaultView
|
||||
} else {
|
||||
context = node.ownerDocument.defaultView
|
||||
}
|
||||
if (!context) { reject("Root node default view not found"); return }
|
||||
/* a StyleSheet from another Window context won't work */
|
||||
resolve(new context.CSSStyleSheet())
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
insertRule(rule: string, index: number) {
|
||||
this.whenReady(s => insertRule(s, { rule, index }))
|
||||
}
|
||||
|
||||
deleteRule(index: number) {
|
||||
this.whenReady(s => deleteRule(s, { index }))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue