123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127 |
- import type {Replayer} from 'rrweb';
- const DEFAULT_HIGHLIGHT_COLOR = 'rgba(168, 196, 236, 0.75)';
- const highlightsByNodeId: Map<number, {canvas: HTMLCanvasElement}> = new Map();
- interface AddHighlightParams {
- nodeId: number;
- replayer: Replayer;
- annotation?: string;
- color?: string;
- }
- interface RemoveHighlightParams {
- nodeId: number;
- replayer: Replayer;
- }
- interface ClearAllHighlightsParams {
- replayer: Replayer;
- }
- export function clearAllHighlights({replayer}: ClearAllHighlightsParams) {
- for (const nodeId of highlightsByNodeId.keys()) {
- removeHighlightedNode({replayer, nodeId});
- }
- }
- /**
- * Remove the canvas that has the highlight for a node.
- *
- * XXX: This is potentially not good if we have a lot of highlights, as we
- * are creating a new canvas PER highlight.
- */
- export function removeHighlightedNode({replayer, nodeId}: RemoveHighlightParams) {
- if (!highlightsByNodeId.has(nodeId)) {
- return false;
- }
- const highlightObj = highlightsByNodeId.get(nodeId);
- if (!highlightObj || !replayer.wrapper.contains(highlightObj.canvas)) {
- return false;
- }
- replayer.wrapper.removeChild(highlightObj.canvas);
- highlightsByNodeId.delete(nodeId);
- return true;
- }
- /**
- * Attempt to highlight the node inside of a replay recording
- */
- export function highlightNode({
- replayer,
- nodeId,
- annotation = '',
- color,
- }: AddHighlightParams) {
- // @ts-expect-error mouseTail is private
- const {mouseTail, wrapper} = replayer;
- const mirror = replayer.getMirror();
- const node = mirror.getNode(nodeId);
- // TODO(replays): There is some sort of race condition here when you "rewind" a replay,
- // mirror will be empty and highlight does not get added because node is null
- if (!node || !replayer.iframe.contentDocument?.body?.contains(node)) {
- return null;
- }
- // @ts-ignore This builds locally, but fails in CI -- ignoring for now
- const {top, left, width, height} = node.getBoundingClientRect();
- const highlightColor = color ?? DEFAULT_HIGHLIGHT_COLOR;
- // Clone the mouseTail canvas as it has the dimensions and position that we
- // want on top of the replay. We may need to revisit this strategy as we
- // create a new canvas for every highlight. See additional notes in
- // removeHighlight() method.
- const canvas = mouseTail.cloneNode();
- const ctx = canvas.getContext('2d');
- if (!ctx) {
- return null;
- }
- // TODO(replays): Does not account for scrolling (should we attempt to keep highlight visible, or does it disappear)
- // Draw a rectangle to highlight element
- ctx.fillStyle = highlightColor;
- ctx.fillRect(left, top, width, height);
- // Draw a dashed border around highlight
- ctx.beginPath();
- ctx.setLineDash([5, 5]);
- ctx.moveTo(left, top);
- ctx.lineTo(left + width, top);
- ctx.lineTo(left + width, top + height);
- ctx.lineTo(left, top + height);
- ctx.closePath();
- ctx.stroke();
- ctx.font = '24px Rubik';
- ctx.textAlign = 'right';
- ctx.textBaseline = 'bottom';
- const textWidth = ctx.measureText(annotation).width;
- // Draw rect around text
- ctx.fillStyle = 'rgba(30, 30, 30, 0.75)';
- ctx.fillRect(left + width - textWidth, top + height - 30, textWidth, 30);
- // Draw text
- ctx.fillStyle = 'white';
- ctx.fillText(annotation, left + width, top + height);
- highlightsByNodeId.set(nodeId, {
- canvas,
- });
- wrapper.insertBefore(canvas, mouseTail);
- return {
- canvas,
- };
- }
|