feat(tracker): maintain mouse actions, cssrules in iframes; console with same API
This commit is contained in:
parent
5c3fc505ab
commit
c31f878ef6
5 changed files with 100 additions and 74 deletions
|
|
@ -14,6 +14,16 @@ export interface Options {
|
|||
|
||||
type ContextCallback = (context: Window & typeof globalThis) => void
|
||||
|
||||
// Le truc - for defining an absolute offset for (nested) iframe documents. (To track mouse movments)
|
||||
type Offset = { top: number; left: number }
|
||||
type PatchedDocument = Document & {
|
||||
__openreplay__getOffset: () => Offset
|
||||
}
|
||||
function isPatchedDocument(doc: Document): doc is PatchedDocument {
|
||||
// @ts-ignore
|
||||
return typeof doc.__openreplay__getOffset === 'function'
|
||||
}
|
||||
|
||||
const attachShadowNativeFn = IN_BROWSER ? Element.prototype.attachShadow : () => new ShadowRoot()
|
||||
|
||||
export default class TopObserver extends Observer {
|
||||
|
|
@ -53,6 +63,14 @@ export default class TopObserver extends Observer {
|
|||
this.contextCallbacks.push(cb)
|
||||
}
|
||||
|
||||
// Le truc
|
||||
getDocumentOffset(doc: Document): Offset {
|
||||
if (isPatchedDocument(doc)) {
|
||||
return doc.__openreplay__getOffset()
|
||||
}
|
||||
return { top: 0, left: 0 }
|
||||
}
|
||||
|
||||
private iframeObservers: IFrameObserver[] = []
|
||||
private handleIframe(iframe: HTMLIFrameElement): void {
|
||||
let doc: Document | null = null
|
||||
|
|
@ -70,6 +88,15 @@ export default class TopObserver extends Observer {
|
|||
this.iframeObservers.push(observer)
|
||||
observer.observe(iframe)
|
||||
doc = currentDoc
|
||||
|
||||
// Le truc
|
||||
;(doc as PatchedDocument).__openreplay__getOffset = () => {
|
||||
const { top, left } = this.getDocumentOffset(iframe.ownerDocument)
|
||||
return {
|
||||
top: iframe.offsetTop + top,
|
||||
left: iframe.offsetLeft + left,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentWin && currentWin !== win) {
|
||||
//@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
const patchConsole = (console: Console) =>
|
||||
options.consoleMethods!.forEach((method) => {
|
||||
if (consoleMethods.indexOf(method) === -1) {
|
||||
console.error(`OpenReplay: unsupported console method "${method}"`)
|
||||
app.debug.error(`OpenReplay: unsupported console method "${method}"`)
|
||||
return
|
||||
}
|
||||
const fn = (console as any)[method]
|
||||
|
|
@ -134,23 +134,8 @@ export default function (app: App, opts: Partial<Options>): void {
|
|||
sendConsoleLog(method, args)
|
||||
}
|
||||
})
|
||||
patchConsole(window.console)
|
||||
const patchContext = app.safe((context: typeof globalThis) => patchConsole(context.console))
|
||||
|
||||
app.nodes.attachNodeCallback(
|
||||
app.safe((node) => {
|
||||
if (hasTag(node, 'IFRAME')) {
|
||||
// TODO: newContextCallback
|
||||
let context = node.contentWindow
|
||||
if (context) {
|
||||
patchConsole((context as Window & typeof globalThis).console)
|
||||
}
|
||||
app.attachEventListener(node, 'load', () => {
|
||||
if (node.contentWindow !== context) {
|
||||
context = node.contentWindow
|
||||
patchConsole((context as Window & typeof globalThis).console)
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
)
|
||||
patchContext(window)
|
||||
app.observer.attachContextCallback(patchContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,16 +27,20 @@ export default function (app: App | null) {
|
|||
} // else error?
|
||||
})
|
||||
|
||||
const { insertRule, deleteRule } = CSSStyleSheet.prototype
|
||||
const patchContext = (context: typeof globalThis) => {
|
||||
const { insertRule, deleteRule } = context.CSSStyleSheet.prototype
|
||||
context.CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
|
||||
processOperation(this, index, rule)
|
||||
return insertRule.call(this, rule, index)
|
||||
}
|
||||
context.CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||
processOperation(this, index)
|
||||
return deleteRule.call(this, index)
|
||||
}
|
||||
}
|
||||
|
||||
CSSStyleSheet.prototype.insertRule = function (rule: string, index = 0) {
|
||||
processOperation(this, index, rule)
|
||||
return insertRule.call(this, rule, index)
|
||||
}
|
||||
CSSStyleSheet.prototype.deleteRule = function (index: number) {
|
||||
processOperation(this, index)
|
||||
return deleteRule.call(this, index)
|
||||
}
|
||||
patchContext(window)
|
||||
app.observer.attachContextCallback(patchContext)
|
||||
|
||||
app.nodes.attachNodeCallback((node: Node): void => {
|
||||
if (!hasTag(node, 'STYLE') || !node.sheet) {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const labelElementFor: (element: TextEditableElement) => HTMLLabelElement | unde
|
|||
}
|
||||
const id = node.id
|
||||
if (id) {
|
||||
const labels = document.querySelectorAll('label[for="' + id + '"]')
|
||||
const labels = node.ownerDocument.querySelectorAll('label[for="' + id + '"]')
|
||||
if (labels !== null && labels.length === 1) {
|
||||
return labels[0] as HTMLLabelElement
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type App from '../app/index.js'
|
||||
import { hasTag, isSVGElement } from '../app/guards.js'
|
||||
import { hasTag, isSVGElement, isDocument } from '../app/guards.js'
|
||||
import { normSpaces, hasOpenreplayAttribute, getLabelAttribute } from '../utils.js'
|
||||
import { MouseMove, MouseClick } from '../app/messages.gen.js'
|
||||
import { getInputLabel } from './input.js'
|
||||
|
||||
function _getSelector(target: Element): string {
|
||||
function _getSelector(target: Element, document: Document): string {
|
||||
let el: Element | null = target
|
||||
let selector: string | null = null
|
||||
do {
|
||||
|
|
@ -37,18 +37,18 @@ function isClickable(element: Element): boolean {
|
|||
element.getAttribute('role') === 'button'
|
||||
)
|
||||
//|| element.className.includes("btn")
|
||||
// MBTODO: intersect addEventListener
|
||||
// MBTODO: intersept addEventListener
|
||||
}
|
||||
|
||||
//TODO: fix (typescript doesn't allow work when the guard is inside the function)
|
||||
function getTarget(target: EventTarget | null): Element | null {
|
||||
//TODO: fix (typescript is not sure about target variable after assignation of svg)
|
||||
function getTarget(target: EventTarget | null, document: Document): Element | null {
|
||||
if (target instanceof Element) {
|
||||
return _getTarget(target)
|
||||
return _getTarget(target, document)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function _getTarget(target: Element): Element | null {
|
||||
function _getTarget(target: Element, document: Document): Element | null {
|
||||
let element: Element | null = target
|
||||
while (element !== null && element !== document.documentElement) {
|
||||
if (hasOpenreplayAttribute(element, 'masked')) {
|
||||
|
|
@ -120,48 +120,58 @@ export default function (app: App): void {
|
|||
}
|
||||
}
|
||||
|
||||
const selectorMap: { [id: number]: string } = {}
|
||||
function getSelector(id: number, target: Element): string {
|
||||
return (selectorMap[id] = selectorMap[id] || _getSelector(target))
|
||||
const patchDocument = (document: Document) => {
|
||||
const selectorMap: { [id: number]: string } = {}
|
||||
function getSelector(id: number, target: Element): string {
|
||||
return (selectorMap[id] = selectorMap[id] || _getSelector(target, document))
|
||||
}
|
||||
|
||||
app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => {
|
||||
const target = getTarget(e.target, document)
|
||||
if (target !== mouseTarget) {
|
||||
mouseTarget = target
|
||||
mouseTargetTime = performance.now()
|
||||
}
|
||||
})
|
||||
app.attachEventListener(
|
||||
document,
|
||||
'mousemove',
|
||||
(e: MouseEvent): void => {
|
||||
const { top, left } = app.observer.getDocumentOffset(document)
|
||||
mousePositionX = e.clientX + left
|
||||
mousePositionY = e.clientY + top
|
||||
mousePositionChanged = true
|
||||
},
|
||||
false,
|
||||
)
|
||||
app.attachEventListener(document, 'click', (e: MouseEvent): void => {
|
||||
const target = getTarget(e.target, document)
|
||||
if ((!e.clientX && !e.clientY) || target === null) {
|
||||
return
|
||||
}
|
||||
const id = app.nodes.getID(target)
|
||||
if (id !== undefined) {
|
||||
sendMouseMove()
|
||||
app.send(
|
||||
MouseClick(
|
||||
id,
|
||||
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
|
||||
getTargetLabel(target),
|
||||
getSelector(id, target),
|
||||
),
|
||||
true,
|
||||
)
|
||||
}
|
||||
mouseTarget = null
|
||||
})
|
||||
}
|
||||
|
||||
app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => {
|
||||
const target = getTarget(e.target)
|
||||
if (target !== mouseTarget) {
|
||||
mouseTarget = target
|
||||
mouseTargetTime = performance.now()
|
||||
app.nodes.attachNodeCallback((node) => {
|
||||
if (isDocument(node)) {
|
||||
patchDocument(node)
|
||||
}
|
||||
})
|
||||
app.attachEventListener(
|
||||
document,
|
||||
'mousemove',
|
||||
(e: MouseEvent): void => {
|
||||
mousePositionX = e.clientX
|
||||
mousePositionY = e.clientY
|
||||
mousePositionChanged = true
|
||||
},
|
||||
false,
|
||||
)
|
||||
app.attachEventListener(document, 'click', (e: MouseEvent): void => {
|
||||
const target = getTarget(e.target)
|
||||
if ((!e.clientX && !e.clientY) || target === null) {
|
||||
return
|
||||
}
|
||||
const id = app.nodes.getID(target)
|
||||
if (id !== undefined) {
|
||||
sendMouseMove()
|
||||
app.send(
|
||||
MouseClick(
|
||||
id,
|
||||
mouseTarget === target ? Math.round(performance.now() - mouseTargetTime) : 0,
|
||||
getTargetLabel(target),
|
||||
getSelector(id, target),
|
||||
),
|
||||
true,
|
||||
)
|
||||
}
|
||||
mouseTarget = null
|
||||
})
|
||||
patchDocument(document)
|
||||
|
||||
app.ticker.attach(sendMouseMove, 10)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue