Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTML dialog recording and replay #22

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/rrweb-snapshot/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atlasinc/rrweb-snapshot",
"version": "1.1.18",
"version": "1.1.19",
"description": "rrweb's component to take a snapshot of DOM, aka DOM serializer",
"scripts": {
"prepare": "npm run prepack",
Expand Down
5 changes: 5 additions & 0 deletions packages/rrweb-snapshot/src/rebuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ function buildNode(
break;
default:
}
} else if (name === 'rr_open_mode') {
(node as HTMLDialogElement).setAttribute(
'rr_open_mode',
value as string,
); // keep this attribute for rrweb to trigger showModal
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions packages/rrweb-snapshot/src/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MaskInputFn,
KeepIframeSrcFn,
MaskImageFn,
DialogAttributes,
} from './types';
import {
isElement,
Expand Down Expand Up @@ -542,6 +543,16 @@ function serializeNode(
delete attributes.selected;
}
}

if (tagName === 'dialog' && (n as HTMLDialogElement).open) {
// register what type of dialog is this
// `modal` or `non-modal`
// this is used to trigger `showModal()` or `show()` on replay (outside of rrweb-snapshot, in rrweb)
(attributes as DialogAttributes).rr_open_mode = (n as HTMLDialogElement).matches('dialog:modal')
? 'modal'
: 'non-modal';
}

// canvas image data
if (tagName === 'canvas' && recordCanvas) {
attributes.rr_dataURL = (n as HTMLCanvasElement).toDataURL();
Expand Down
17 changes: 17 additions & 0 deletions packages/rrweb-snapshot/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@ export type tagMap = {
[key: string]: string;
};

export type DialogAttributes = {
open: string;
/**
* Represents the dialog's open mode.
* `modal` means the dialog is opened with `showModal()`.
* `non-modal` means the dialog is opened with `show()` or
* by adding an `open` attribute.
*/
rr_open_mode: 'modal' | 'non-modal';
/**
* Currently unimplemented, but in future can be used to:
* Represents the order of which of the dialog was opened.
* This is useful for replaying the dialog `.showModal()` in the correct order.
*/
// rr_open_mode_index?: number;
};

export interface INode extends Node {
__sn_atlas: serializedNodeWithId;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/rrweb-snapshot/typings/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export declare type serializedNodeWithId = serializedNode & {
export declare type tagMap = {
[key: string]: string;
};
export declare type DialogAttributes = {
open: string;
rr_open_mode: 'modal' | 'non-modal';
};
export interface INode extends Node {
__sn_atlas: serializedNodeWithId;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/rrweb/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@atlasinc/rrweb",
"version": "1.1.20",
"version": "1.1.21",
"description": "record and replay the web",
"scripts": {
"prepare": "npm run prepack",
Expand Down
8 changes: 8 additions & 0 deletions packages/rrweb/src/record/mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,14 @@ export default class MutationBuffer {
);
}

if (m.attributeName === 'open' && tagName === 'dialog') {
if (target.matches('dialog:modal')) {
item.attributes.rr_open_mode = 'modal';
} else {
item.attributes.rr_open_mode = 'non-modal';
}
}

if (tagName === 'img') {
const needsMasking = needMaskingText(
m.target,
Expand Down
80 changes: 80 additions & 0 deletions packages/rrweb/src/replay/dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { attributeMutation } from '../types';

/**
* Checks if the dialog is a top level dialog and applies the dialog to the top level
* @param node - potential dialog element to apply top level `showModal()` to, or other node (which will be ignored)
* @param attributeMutation - the attribute mutation used to change the dialog (optional)
* @returns void
*/
export function applyDialogToTopLevel(
node: HTMLDialogElement | Node,
attributeMutation?: attributeMutation,
): void {
if (node.nodeName !== 'DIALOG') {
return;
}
const dialog = node as HTMLDialogElement;
const oldIsOpen = dialog.open;
const oldIsModalState = oldIsOpen && dialog.matches('dialog:modal');
const rrOpenMode = dialog.getAttribute('rr_open_mode');

const newIsOpen =
typeof attributeMutation?.attributes.open === 'string' ||
typeof dialog.getAttribute('open') === 'string';
const newIsModalState = rrOpenMode === 'modal';
const newIsNonModalState = rrOpenMode === 'non-modal';

const modalStateChanged =
(oldIsModalState && newIsNonModalState) ||
(!oldIsModalState && newIsModalState);

if (oldIsOpen && !modalStateChanged) {
return;
}

// complain if dialog is not attached to the dom
if (!dialog.isConnected) {
console.warn('dialog is not attached to the dom', dialog);
return;
}

if (oldIsOpen) {
dialog.close();
}
if (!newIsOpen) {
return;
}

if (newIsModalState) {
dialog.showModal();
} else {
dialog.show();
}
}

/**
* Check if the dialog is a top level dialog and removes the dialog from the top level if necessary
* @param node - potential dialog element to remove from top level, or other node (which will be ignored)
* @param attributeMutation - the attribute mutation used to change the dialog
* @returns void
*/
export function removeDialogFromTopLevel(
node: HTMLDialogElement | Node,
attributeMutation: attributeMutation,
): void {
if (node.nodeName !== 'DIALOG') {
return;
}
const dialog = node as HTMLDialogElement;

// complain if dialog is not attached to the dom
if (!dialog.isConnected) {
console.warn('dialog is not attached to the dom', dialog);
return;
}

if (attributeMutation.attributes.open === null) {
dialog.removeAttribute('open');
dialog.removeAttribute('rr_open_mode');
}
}
Loading