tracker: handle emotion-js style population
This commit is contained in:
parent
b109dd559a
commit
4a9a082896
4 changed files with 123 additions and 50 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "16.2.1",
|
||||
"version": "16.2.2-beta.19",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export type Options = Partial<
|
|||
}
|
||||
// dev only
|
||||
__DISABLE_SECURE_MODE?: boolean
|
||||
checkCssInterval?: number
|
||||
}
|
||||
|
||||
const DOCS_SETUP = '/en/sdk'
|
||||
|
|
@ -192,7 +193,7 @@ export default class API {
|
|||
Mouse(app, options.mouse)
|
||||
// inside iframe, we ignore viewport scroll
|
||||
Scroll(app, this.crossdomainMode)
|
||||
CSSRules(app)
|
||||
CSSRules(app, options)
|
||||
ConstructedStyleSheets(app)
|
||||
Console(app, options)
|
||||
Exception(app, options)
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@ export default function (app: App | null) {
|
|||
}
|
||||
adoptedStyleSheetsOwnings.set(nodeID, nowOwning)
|
||||
}, 20) // Mysterious bug:
|
||||
/* On the page https://explore.fast.design/components/fast-accordion
|
||||
/* On the page https://explore.fast.design/components/fast-accordion
|
||||
the only rule inside the only adoptedStyleSheet of the iframe-s document
|
||||
gets changed during first milliseconds after the load.
|
||||
gets changed during first milliseconds after the load.
|
||||
However, none of the documented methods (replace, insertRule) is triggered.
|
||||
The rule is not substituted (remains the same object), however the text gets changed.
|
||||
*/
|
||||
|
|
@ -127,6 +127,7 @@ export default function (app: App | null) {
|
|||
return replace.call(this, text).then((sheet: CSSStyleSheet) => {
|
||||
const sheetID = styleSheetIDMap.get(this)
|
||||
if (sheetID) {
|
||||
console.log('replace')
|
||||
app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref()))
|
||||
}
|
||||
return sheet
|
||||
|
|
@ -136,6 +137,7 @@ export default function (app: App | null) {
|
|||
context.CSSStyleSheet.prototype.replaceSync = function (text: string) {
|
||||
const sheetID = styleSheetIDMap.get(this)
|
||||
if (sheetID) {
|
||||
console.log('replaceSync')
|
||||
app.send(AdoptedSSReplaceURLBased(sheetID, text, app.getBaseHref()))
|
||||
}
|
||||
return replaceSync.call(this, text)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type App from '../app/index.js'
|
||||
import {
|
||||
AdoptedSSInsertRuleURLBased, // TODO: rename to common StyleSheet names
|
||||
AdoptedSSInsertRuleURLBased,
|
||||
AdoptedSSDeleteRule,
|
||||
AdoptedSSAddOwner,
|
||||
TechnicalInfo,
|
||||
|
|
@ -8,104 +8,174 @@ import {
|
|||
import { hasTag } from '../app/guards.js'
|
||||
import { nextID, styleSheetIDMap } from './constructedStyleSheets.js'
|
||||
|
||||
export default function (app: App | null) {
|
||||
if (app === null) {
|
||||
return
|
||||
}
|
||||
export default function (app: App, opts: { checkCssInterval?: number }) {
|
||||
if (app === null) return
|
||||
if (!window.CSSStyleSheet) {
|
||||
app.send(TechnicalInfo('no_stylesheet_prototype_in_window', ''))
|
||||
return
|
||||
}
|
||||
|
||||
// Track CSS rule snapshots by sheetID:index
|
||||
const ruleSnapshots = new Map<string, string>()
|
||||
let checkInterval: number | null = null
|
||||
const checkIntervalMs = opts.checkCssInterval || 200 // Check every 200ms
|
||||
|
||||
// Check all rules for changes
|
||||
function checkRuleChanges() {
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
try {
|
||||
const sheet = document.styleSheets[i]
|
||||
const sheetID = styleSheetIDMap.get(sheet)
|
||||
if (!sheetID) continue
|
||||
|
||||
// Check each rule in the sheet
|
||||
for (let j = 0; j < sheet.cssRules.length; j++) {
|
||||
try {
|
||||
const rule = sheet.cssRules[j]
|
||||
const key = `${sheetID}:${j}`
|
||||
const newText = rule.cssText
|
||||
const oldText = ruleSnapshots.get(key)
|
||||
|
||||
if (oldText !== newText) {
|
||||
// Rule is new or changed
|
||||
if (oldText !== undefined) {
|
||||
// Rule changed - send update
|
||||
app.send(AdoptedSSDeleteRule(sheetID, j))
|
||||
app.send(AdoptedSSInsertRuleURLBased(sheetID, newText, j, app.getBaseHref()))
|
||||
} else {
|
||||
// Rule added - send insert
|
||||
app.send(AdoptedSSInsertRuleURLBased(sheetID, newText, j, app.getBaseHref()))
|
||||
}
|
||||
ruleSnapshots.set(key, newText)
|
||||
}
|
||||
} catch (e) {
|
||||
/* Skip inaccessible rules */
|
||||
}
|
||||
}
|
||||
|
||||
// Check for deleted rules
|
||||
const keysToCheck = Array.from(ruleSnapshots.keys()).filter((key) =>
|
||||
key.startsWith(`${sheetID}:`),
|
||||
)
|
||||
|
||||
for (const key of keysToCheck) {
|
||||
const index = parseInt(key.split(':')[1], 10)
|
||||
if (index >= sheet.cssRules.length) {
|
||||
ruleSnapshots.delete(key)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
/* Skip inaccessible sheets */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Standard API hooks
|
||||
const sendInsertDeleteRule = app.safe((sheet: CSSStyleSheet, index: number, rule?: string) => {
|
||||
const sheetID = styleSheetIDMap.get(sheet)
|
||||
if (!sheetID) {
|
||||
// OK-case. Sheet haven't been registered yet. Rules will be sent on registration.
|
||||
return
|
||||
}
|
||||
if (!sheetID) return
|
||||
|
||||
if (typeof rule === 'string') {
|
||||
app.send(AdoptedSSInsertRuleURLBased(sheetID, rule, index, app.getBaseHref()))
|
||||
ruleSnapshots.set(`${sheetID}:${index}`, rule)
|
||||
} else {
|
||||
app.send(AdoptedSSDeleteRule(sheetID, index))
|
||||
ruleSnapshots.delete(`${sheetID}:${index}`)
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: proper rule insertion/removal (how?)
|
||||
const sendReplaceGroupingRule = app.safe((rule: CSSGroupingRule) => {
|
||||
let topmostRule: CSSRule = rule
|
||||
while (topmostRule.parentRule) {
|
||||
topmostRule = topmostRule.parentRule
|
||||
}
|
||||
while (topmostRule.parentRule) topmostRule = topmostRule.parentRule
|
||||
|
||||
const sheet = topmostRule.parentStyleSheet
|
||||
if (!sheet) {
|
||||
app.debug.warn('No parent StyleSheet found for', topmostRule, rule)
|
||||
return
|
||||
}
|
||||
if (!sheet) return
|
||||
|
||||
const sheetID = styleSheetIDMap.get(sheet)
|
||||
if (!sheetID) {
|
||||
app.debug.warn('No sheedID found for', sheet, styleSheetIDMap)
|
||||
return
|
||||
}
|
||||
if (!sheetID) return
|
||||
|
||||
const cssText = topmostRule.cssText
|
||||
const ruleList = sheet.cssRules
|
||||
const idx = Array.from(ruleList).indexOf(topmostRule)
|
||||
const idx = Array.from(sheet.cssRules).indexOf(topmostRule)
|
||||
|
||||
if (idx >= 0) {
|
||||
app.send(AdoptedSSInsertRuleURLBased(sheetID, cssText, idx, app.getBaseHref()))
|
||||
app.send(AdoptedSSDeleteRule(sheetID, idx + 1)) // Remove previous clone
|
||||
} else {
|
||||
app.debug.warn('Rule index not found in', sheet, topmostRule)
|
||||
app.send(AdoptedSSDeleteRule(sheetID, idx + 1))
|
||||
ruleSnapshots.set(`${sheetID}:${idx}`, cssText)
|
||||
}
|
||||
})
|
||||
|
||||
// Patch prototype methods
|
||||
const patchContext = app.safe((context: typeof globalThis) => {
|
||||
if ((context as any).__css_tracking_patched__) return
|
||||
;(context as any).__css_tracking_patched__ = true
|
||||
|
||||
const { insertRule, deleteRule } = context.CSSStyleSheet.prototype
|
||||
const { insertRule: groupInsertRule, deleteRule: groupDeleteRule } =
|
||||
context.CSSGroupingRule.prototype
|
||||
|
||||
context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0): number {
|
||||
sendInsertDeleteRule(this, index, rule)
|
||||
return insertRule.call(this, rule, index)
|
||||
context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
|
||||
const result = insertRule.call(this, rule, index)
|
||||
sendInsertDeleteRule(this, result, rule)
|
||||
return result
|
||||
}
|
||||
context.CSSStyleSheet.prototype.deleteRule = function (index: number): void {
|
||||
|
||||
context.CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||
sendInsertDeleteRule(this, index)
|
||||
return deleteRule.call(this, index)
|
||||
}
|
||||
|
||||
context.CSSGroupingRule.prototype.insertRule = function (rule: string, index = 0): number {
|
||||
const result = groupInsertRule.call(this, rule, index) as number
|
||||
context.CSSGroupingRule.prototype.insertRule = function (rule: string, index = 0) {
|
||||
const result = groupInsertRule.call(this, rule, index)
|
||||
sendReplaceGroupingRule(this)
|
||||
return result
|
||||
}
|
||||
context.CSSGroupingRule.prototype.deleteRule = function (index = 0): void {
|
||||
const result = groupDeleteRule.call(this, index) as void
|
||||
|
||||
context.CSSGroupingRule.prototype.deleteRule = function (index: number) {
|
||||
const result = groupDeleteRule.call(this, index)
|
||||
sendReplaceGroupingRule(this)
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
// Apply patches
|
||||
patchContext(window)
|
||||
app.observer.attachContextCallback(patchContext)
|
||||
|
||||
// Track style nodes
|
||||
app.nodes.attachNodeCallback((node: Node): void => {
|
||||
if (!hasTag(node, 'style') || !node.sheet) {
|
||||
return
|
||||
}
|
||||
if (node.textContent !== null && node.textContent.trim().length > 0) {
|
||||
return // Non-virtual styles captured by the observer as a text
|
||||
}
|
||||
if (!hasTag(node, 'style') || !node.sheet) return
|
||||
if (node.textContent !== null && node.textContent.trim().length > 0) return
|
||||
|
||||
const nodeID = app.nodes.getID(node)
|
||||
if (!nodeID) {
|
||||
return
|
||||
}
|
||||
if (!nodeID) return
|
||||
|
||||
const sheet = node.sheet
|
||||
const sheetID = nextID()
|
||||
styleSheetIDMap.set(sheet, sheetID)
|
||||
app.send(AdoptedSSAddOwner(sheetID, nodeID))
|
||||
|
||||
const rules = sheet.cssRules
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
sendInsertDeleteRule(sheet, i, rules[i].cssText)
|
||||
for (let i = 0; i < sheet.cssRules.length; i++) {
|
||||
try {
|
||||
sendInsertDeleteRule(sheet, i, sheet.cssRules[i].cssText)
|
||||
} catch (e) {
|
||||
// Skip inaccessible rules
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Start checking and setup cleanup
|
||||
function startChecking() {
|
||||
if (checkInterval) return
|
||||
checkInterval = window.setInterval(checkRuleChanges, checkIntervalMs)
|
||||
}
|
||||
|
||||
setTimeout(startChecking, 50)
|
||||
|
||||
app.attachStopCallback(() => {
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval)
|
||||
checkInterval = null
|
||||
}
|
||||
ruleSnapshots.clear()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue