2025-01-25 04:21:30 +01:00
|
|
|
import {
|
|
|
|
DeesElement,
|
|
|
|
html,
|
|
|
|
css,
|
|
|
|
customElement,
|
|
|
|
property,
|
|
|
|
query,
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
import * as rrwebMod from 'rrweb';
|
|
|
|
import rrwebPlayerMod from 'rrweb-player';
|
|
|
|
|
|
|
|
const rrweb: any = rrwebMod;
|
|
|
|
const rrwebPlayer: typeof rrwebPlayerMod.default = rrwebPlayerMod as any;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use rrweb's eventWithTime if you like strict typing:
|
|
|
|
*
|
|
|
|
* import { eventWithTime } from 'rrweb';
|
|
|
|
* export interface IRecordingEvent extends eventWithTime {}
|
|
|
|
*
|
|
|
|
* Here, for brevity, we define an empty interface
|
|
|
|
* and cast all events to any.
|
|
|
|
*/
|
|
|
|
export interface IRecordingEvent {}
|
|
|
|
|
|
|
|
@customElement('sio-recorder')
|
|
|
|
export class SioRecorder extends DeesElement {
|
|
|
|
public static demo = () => html`<sio-recorder></sio-recorder>`;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Holds all recorded events from rrweb.
|
|
|
|
*/
|
|
|
|
private events: IRecordingEvent[] = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A reference to rrweb's stop recording function.
|
|
|
|
* We'll store it when we begin a record session so we can call it later.
|
|
|
|
*/
|
|
|
|
private stopFn: (() => void) | null = null;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Query for the div in our template that will be used for playback.
|
|
|
|
*/
|
|
|
|
@query('#playback')
|
|
|
|
private playbackDiv!: HTMLDivElement;
|
|
|
|
|
|
|
|
static styles = css`
|
|
|
|
:host {
|
|
|
|
display: block;
|
|
|
|
}
|
|
|
|
/* The playback container: set a fixed size for demonstration. */
|
|
|
|
#playback {
|
|
|
|
width: 800px;
|
|
|
|
height: 600px;
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
background-color: #fff;
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
render() {
|
|
|
|
return html`
|
|
|
|
<div id="playback"></div>
|
|
|
|
`;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lifecycle: Called when the element is inserted into the DOM.
|
|
|
|
*/
|
|
|
|
async connectedCallback(): Promise<void> {
|
|
|
|
super.connectedCallback();
|
|
|
|
console.log('sio-recorder connectedCallback');
|
|
|
|
|
|
|
|
// Start recording immediately
|
|
|
|
this.startRecording();
|
|
|
|
|
|
|
|
// Use the domtools-based approach you have (Ctrl+H):
|
|
|
|
const domtools = await this.domtoolsPromise;
|
|
|
|
domtools.keyboard
|
|
|
|
.on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.H])
|
|
|
|
.subscribe(this.handleKeydown);
|
|
|
|
await this.domtoolsPromise;
|
|
|
|
this.domtools.convenience.smartdelay.delayFor(2000).then(() => {
|
|
|
|
// this.handleKeydown();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lifecycle: Called when the element is removed from the DOM.
|
|
|
|
*/
|
|
|
|
public async disconnectedCallback(): Promise<void> {
|
|
|
|
super.disconnectedCallback();
|
|
|
|
|
|
|
|
// Stop recording if it's still running
|
|
|
|
this.stopRecording();
|
|
|
|
|
|
|
|
// Clean up your subscription or any global listeners if needed
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Starts an rrweb recording session that tracks the entire DOM,
|
|
|
|
* including canvases and cross-origin iframes (if permissible).
|
|
|
|
*/
|
|
|
|
private startRecording(): void {
|
|
|
|
this.events = [];
|
|
|
|
|
|
|
|
// For capturing "everything," enable advanced flags:
|
|
|
|
this.stopFn = rrweb.record({
|
|
|
|
emit: (event: any) => {
|
|
|
|
// If you have a stricter type:
|
|
|
|
// this.events.push(event as IRecordingEvent);
|
|
|
|
// else store as any:
|
|
|
|
this.events.push(event);
|
|
|
|
},
|
|
|
|
// Some recommended settings to capture the "complete" page:
|
|
|
|
recordCanvas: true, // record canvas elements
|
|
|
|
recordCrossOriginIframes: true, // attempt capturing cross-origin iframes
|
2025-01-25 13:38:06 +01:00
|
|
|
|
2025-01-25 04:21:30 +01:00
|
|
|
// checkoutEveryNms: 1000, // check every N milliseconds
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log('Recording has started...');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the rrweb recording session
|
|
|
|
*/
|
|
|
|
private stopRecording(): void {
|
|
|
|
if (this.stopFn) {
|
|
|
|
this.stopFn();
|
|
|
|
this.stopFn = null;
|
|
|
|
console.log('Recording has stopped.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Plays back the recorded events in the `playbackDiv`
|
|
|
|
*/
|
|
|
|
private async playRecording(): Promise<void> {
|
|
|
|
await this.domtoolsPromise;
|
|
|
|
if (!this.playbackDiv) return;
|
|
|
|
const replayer =new rrwebPlayer({
|
|
|
|
target: this.playbackDiv, // customizable root element
|
|
|
|
props: {
|
|
|
|
events: this.events as any,
|
|
|
|
root: this.playbackDiv,
|
|
|
|
showController: false,
|
|
|
|
width: this.playbackDiv.offsetWidth,
|
|
|
|
height: this.playbackDiv.offsetHeight,
|
|
|
|
|
|
|
|
},
|
|
|
|
});
|
|
|
|
this.domtools.convenience.smartdelay.delayFor(0).then(async () => {
|
|
|
|
while (true) {
|
|
|
|
await this.domtools.convenience.smartdelay.delayFor(30000);
|
|
|
|
await replayer.play();
|
|
|
|
await this.domtools.convenience.smartdelay.delayFor(0);
|
|
|
|
// this.fixPosition();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.fixPosition();
|
|
|
|
}
|
|
|
|
|
|
|
|
public async fixPosition() {
|
|
|
|
await this.domtoolsPromise;
|
|
|
|
await this.domtools.convenience.smartdelay.delayFor(0);
|
2025-01-25 13:38:06 +01:00
|
|
|
const playbackDiv = this.shadowRoot.querySelector('#playback') as HTMLElement;
|
2025-01-25 04:21:30 +01:00
|
|
|
const replayerWrapper = this.shadowRoot.querySelector('.replayer-wrapper') as HTMLElement;
|
|
|
|
const replayerMouse = this.shadowRoot.querySelector('.replayer-mouse') as HTMLElement;
|
|
|
|
const replayerMouseTail = this.shadowRoot.querySelector('.replayer-mouse-tail') as HTMLElement;
|
2025-01-25 13:38:06 +01:00
|
|
|
const iframe = this.shadowRoot.querySelector('iframe');
|
2025-01-25 04:21:30 +01:00
|
|
|
replayerWrapper.style.position = 'absolute';
|
|
|
|
replayerWrapper.style.top = '0px';
|
|
|
|
replayerWrapper.style.left = '0px';
|
|
|
|
replayerWrapper.style.transformOrigin = 'center center';
|
|
|
|
replayerMouse.style.position = 'absolute';
|
|
|
|
replayerMouseTail.style.position = 'absolute';
|
|
|
|
iframe.style.position = 'absolute';
|
|
|
|
iframe.style.top = '0px';
|
|
|
|
iframe.style.left = '0px';
|
2025-01-25 13:38:06 +01:00
|
|
|
iframe.style.border = 'none';
|
|
|
|
|
|
|
|
// set z-index
|
|
|
|
replayerWrapper.style.zIndex = '1000';
|
|
|
|
iframe.style.zIndex = '1000';
|
|
|
|
replayerMouse.style.zIndex = '1002';
|
|
|
|
replayerMouseTail.style.zIndex = '1001';
|
|
|
|
|
|
|
|
// lets show a mouse cursor
|
|
|
|
replayerMouse.style.width = '10px';
|
|
|
|
replayerMouse.style.height = '10px';
|
|
|
|
replayerMouse.style.background = 'green';
|
|
|
|
replayerMouse.style.transform = 'translate(-50%, -50%)';
|
|
|
|
replayerMouse.style.borderRadius = '50%';
|
|
|
|
replayerMouse.style.border = '1px solid white';
|
2025-01-25 04:21:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Keydown handler. If Ctrl + H is pressed, stop the recording and replay the session
|
|
|
|
*/
|
|
|
|
private handleKeydown = (): void => {
|
|
|
|
this.stopRecording();
|
|
|
|
this.playRecording();
|
|
|
|
};
|
|
|
|
}
|