commit
5b70ff35fe
4 changed files with 97 additions and 36 deletions
68
tracker/tracker/src/main/app/observer/iframe_offsets.ts
Normal file
68
tracker/tracker/src/main/app/observer/iframe_offsets.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Le truc - for defining an absolute offset for (nested) iframe documents. (To track mouse movments)
|
||||
|
||||
export type Offset = [/*left:*/ number, /*top: */ number]
|
||||
|
||||
type OffsetState = {
|
||||
offset: Offset | null
|
||||
parent: OffsetState | null
|
||||
iFrame: HTMLIFrameElement
|
||||
clear: () => void
|
||||
}
|
||||
|
||||
export default class IFrameOffsets {
|
||||
private readonly states: Map<Document, OffsetState> = new Map()
|
||||
|
||||
private calcOffset(state: OffsetState): Offset {
|
||||
let parLeft = 0,
|
||||
parTop = 0
|
||||
if (state.parent) {
|
||||
;[parLeft, parTop] = this.calcOffset(state.parent)
|
||||
}
|
||||
if (!state.offset) {
|
||||
const { left, top } = state.iFrame.getBoundingClientRect()
|
||||
state.offset = [left, top]
|
||||
}
|
||||
const [left, top] = state.offset
|
||||
return [parLeft + left, parTop + top] // TODO: store absolute sum, invalidate whole subtree. Otherwise it is summated on each mousemove
|
||||
}
|
||||
|
||||
getDocumentOffset(doc: Document): Offset {
|
||||
const state = this.states.get(doc)
|
||||
if (!state) {
|
||||
return [0, 0]
|
||||
} // topmost doc
|
||||
return this.calcOffset(state)
|
||||
}
|
||||
|
||||
observe(iFrame: HTMLIFrameElement): void {
|
||||
const doc = iFrame.contentDocument
|
||||
if (!doc) {
|
||||
return
|
||||
}
|
||||
const parentDoc = iFrame.ownerDocument
|
||||
const parentState = this.states.get(parentDoc)
|
||||
const state = {
|
||||
offset: null, // lazy calc
|
||||
iFrame,
|
||||
parent: parentState || null, // null when parentDoc is the topmost document
|
||||
clear: () => {
|
||||
parentDoc.removeEventListener('scroll', invalidateOffset)
|
||||
parentDoc.defaultView?.removeEventListener('resize', invalidateOffset)
|
||||
},
|
||||
}
|
||||
const invalidateOffset = () => {
|
||||
state.offset = null
|
||||
}
|
||||
|
||||
// anything more reliable? This does not cover all cases (layout changes are ignored, for ex.)
|
||||
parentDoc.addEventListener('scroll', invalidateOffset)
|
||||
parentDoc.defaultView?.addEventListener('resize', invalidateOffset)
|
||||
|
||||
this.states.set(doc, state)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.states.forEach((s) => s.clear())
|
||||
this.states.clear()
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { isElementNode, hasTag } from '../guards.js'
|
|||
|
||||
import IFrameObserver from './iframe_observer.js'
|
||||
import ShadowRootObserver from './shadow_root_observer.js'
|
||||
import IFrameOffsets, { Offset } from './iframe_offsets.js'
|
||||
|
||||
import { CreateDocument } from '../messages.gen.js'
|
||||
import App from '../index.js'
|
||||
|
|
@ -13,23 +14,14 @@ export interface Options {
|
|||
}
|
||||
|
||||
type Context = Window & typeof globalThis
|
||||
|
||||
type ContextCallback = (context: Context) => 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 {
|
||||
private readonly options: Options
|
||||
private readonly iframeOffsets: IFrameOffsets = new IFrameOffsets()
|
||||
|
||||
constructor(app: App, options: Partial<Options>) {
|
||||
super(app, true)
|
||||
this.options = Object.assign(
|
||||
|
|
@ -66,18 +58,13 @@ 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 }
|
||||
return this.iframeOffsets.getDocumentOffset(doc)
|
||||
}
|
||||
|
||||
private iframeObservers: IFrameObserver[] = []
|
||||
private handleIframe(iframe: HTMLIFrameElement): void {
|
||||
let doc: Document | null = null
|
||||
let win: Window | null = null
|
||||
// setTimeout is required. Otherwise some event listeners (scroll, mousemove) applied in modules
|
||||
// do not work on the iframe document when it 've been loaded dynamically ((why?))
|
||||
const handle = this.app.safe(() =>
|
||||
|
|
@ -95,25 +82,18 @@ export default class TopObserver extends Observer {
|
|||
observer.observe(iframe) // TODO: call unregisterNode for the previous doc if present (incapsulate: one iframe - one observer)
|
||||
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,
|
||||
}
|
||||
}
|
||||
this.iframeOffsets.observe(iframe)
|
||||
}
|
||||
if (
|
||||
currentWin &&
|
||||
// Sometimes currentWin.window is null (not in specification). Such window object is not functional
|
||||
currentWin === currentWin.window &&
|
||||
!this.contextsSet.has(currentWin) // for each context callbacks called once per Tracker (TopObserver) instance
|
||||
//TODO: more explicit logic
|
||||
) {
|
||||
this.contextsSet.add(currentWin)
|
||||
//@ts-ignore https://github.com/microsoft/TypeScript/issues/41684
|
||||
this.contextCallbacks.forEach((cb) => cb(currentWin))
|
||||
win = currentWin
|
||||
}
|
||||
}, 0),
|
||||
)
|
||||
|
|
@ -155,6 +135,7 @@ export default class TopObserver extends Observer {
|
|||
}
|
||||
|
||||
disconnect() {
|
||||
this.iframeOffsets.clear()
|
||||
Element.prototype.attachShadow = attachShadowNativeFn
|
||||
this.iframeObservers.forEach((o) => o.disconnect())
|
||||
this.iframeObservers = []
|
||||
|
|
|
|||
|
|
@ -105,12 +105,14 @@ export default function (app: App): void {
|
|||
let mousePositionChanged = false
|
||||
let mouseTarget: Element | null = null
|
||||
let mouseTargetTime = 0
|
||||
let selectorMap: { [id: number]: string } = {}
|
||||
|
||||
app.attachStopCallback(() => {
|
||||
mousePositionX = -1
|
||||
mousePositionY = -1
|
||||
mousePositionChanged = false
|
||||
mouseTarget = null
|
||||
selectorMap = {}
|
||||
})
|
||||
|
||||
const sendMouseMove = (): void => {
|
||||
|
|
@ -120,31 +122,34 @@ export default function (app: App): void {
|
|||
}
|
||||
}
|
||||
|
||||
const patchDocument = (document: Document) => {
|
||||
const selectorMap: { [id: number]: string } = {}
|
||||
const patchDocument = (document: Document, topframe = false) => {
|
||||
function getSelector(id: number, target: Element): string {
|
||||
return (selectorMap[id] = selectorMap[id] || _getSelector(target, document))
|
||||
}
|
||||
|
||||
app.attachEventListener(document.documentElement, 'mouseover', (e: MouseEvent): void => {
|
||||
const attachListener = topframe
|
||||
? app.attachEventListener.bind(app) // attached/removed on start/stop
|
||||
: app.nodes.attachNodeListener.bind(app.nodes) // attached/removed on node register/unregister
|
||||
|
||||
attachListener(document.documentElement, 'mouseover', (e: MouseEvent): void => {
|
||||
const target = getTarget(e.target, document)
|
||||
if (target !== mouseTarget) {
|
||||
mouseTarget = target
|
||||
mouseTargetTime = performance.now()
|
||||
}
|
||||
})
|
||||
app.attachEventListener(
|
||||
attachListener(
|
||||
document,
|
||||
'mousemove',
|
||||
(e: MouseEvent): void => {
|
||||
const { top, left } = app.observer.getDocumentOffset(document)
|
||||
const [left, top] = app.observer.getDocumentOffset(document) // MBTODO?: document-id related message
|
||||
mousePositionX = e.clientX + left
|
||||
mousePositionY = e.clientY + top
|
||||
mousePositionChanged = true
|
||||
},
|
||||
false,
|
||||
)
|
||||
app.attachEventListener(document, 'click', (e: MouseEvent): void => {
|
||||
attachListener(document, 'click', (e: MouseEvent): void => {
|
||||
const target = getTarget(e.target, document)
|
||||
if ((!e.clientX && !e.clientY) || target === null) {
|
||||
return
|
||||
|
|
@ -171,7 +176,7 @@ export default function (app: App): void {
|
|||
patchDocument(node)
|
||||
}
|
||||
})
|
||||
patchDocument(document)
|
||||
patchDocument(document, true)
|
||||
|
||||
app.ticker.attach(sendMouseMove, 10)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,9 +52,16 @@ export default function (app: App): void {
|
|||
|
||||
app.nodes.attachNodeCallback((node, isStart) => {
|
||||
// MBTODO: iterate over all the nodes on start instead of using isStart hack
|
||||
if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
|
||||
nodeScroll.set(node, [node.scrollLeft, node.scrollTop])
|
||||
} else if (isRootNode(node)) {
|
||||
if (isStart) {
|
||||
if (isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
|
||||
nodeScroll.set(node, [node.scrollLeft, node.scrollTop])
|
||||
} else if (isDocument(node)) {
|
||||
// DRY somehow?
|
||||
nodeScroll.set(node, getDocumentScroll(node))
|
||||
}
|
||||
}
|
||||
|
||||
if (isRootNode(node)) {
|
||||
// scroll is not-composed event (https://javascript.info/shadow-dom-events)
|
||||
app.nodes.attachNodeListener(node, 'scroll', (e: Event): void => {
|
||||
setNodeScroll(e.target)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue