highlightNode.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import type {Replayer} from 'rrweb';
  2. const DEFAULT_HIGHLIGHT_COLOR = 'rgba(168, 196, 236, 0.75)';
  3. const highlightsByNodeId: Map<number, {canvas: HTMLCanvasElement}> = new Map();
  4. interface AddHighlightParams {
  5. nodeId: number;
  6. replayer: Replayer;
  7. annotation?: string;
  8. color?: string;
  9. }
  10. interface RemoveHighlightParams {
  11. nodeId: number;
  12. replayer: Replayer;
  13. }
  14. interface ClearAllHighlightsParams {
  15. replayer: Replayer;
  16. }
  17. export function clearAllHighlights({replayer}: ClearAllHighlightsParams) {
  18. for (const nodeId of highlightsByNodeId.keys()) {
  19. removeHighlightedNode({replayer, nodeId});
  20. }
  21. }
  22. /**
  23. * Remove the canvas that has the highlight for a node.
  24. *
  25. * XXX: This is potentially not good if we have a lot of highlights, as we
  26. * are creating a new canvas PER highlight.
  27. */
  28. export function removeHighlightedNode({replayer, nodeId}: RemoveHighlightParams) {
  29. if (!highlightsByNodeId.has(nodeId)) {
  30. return false;
  31. }
  32. const highlightObj = highlightsByNodeId.get(nodeId);
  33. if (!highlightObj || !replayer.wrapper.contains(highlightObj.canvas)) {
  34. return false;
  35. }
  36. replayer.wrapper.removeChild(highlightObj.canvas);
  37. highlightsByNodeId.delete(nodeId);
  38. return true;
  39. }
  40. /**
  41. * Attempt to highlight the node inside of a replay recording
  42. */
  43. export function highlightNode({
  44. replayer,
  45. nodeId,
  46. annotation = '',
  47. color,
  48. }: AddHighlightParams) {
  49. // @ts-expect-error mouseTail is private
  50. const {mouseTail, wrapper} = replayer;
  51. const mirror = replayer.getMirror();
  52. const node = mirror.getNode(nodeId);
  53. // TODO(replays): There is some sort of race condition here when you "rewind" a replay,
  54. // mirror will be empty and highlight does not get added because node is null
  55. if (
  56. !node ||
  57. !('getBoundingClientRect' in node) ||
  58. !replayer.iframe.contentDocument?.body?.contains(node)
  59. ) {
  60. return null;
  61. }
  62. // @ts-ignore This builds locally, but fails in CI -- ignoring for now
  63. const {top, left, width, height} = node.getBoundingClientRect();
  64. const highlightColor = color ?? DEFAULT_HIGHLIGHT_COLOR;
  65. // Clone the mouseTail canvas as it has the dimensions and position that we
  66. // want on top of the replay. We may need to revisit this strategy as we
  67. // create a new canvas for every highlight. See additional notes in
  68. // removeHighlight() method.
  69. const canvas = mouseTail.cloneNode();
  70. const ctx = canvas.getContext('2d');
  71. if (!ctx) {
  72. return null;
  73. }
  74. // TODO(replays): Does not account for scrolling (should we attempt to keep highlight visible, or does it disappear)
  75. // Draw a rectangle to highlight element
  76. ctx.fillStyle = highlightColor;
  77. ctx.fillRect(left, top, width, height);
  78. // Draw a dashed border around highlight
  79. ctx.beginPath();
  80. ctx.setLineDash([5, 5]);
  81. ctx.moveTo(left, top);
  82. ctx.lineTo(left + width, top);
  83. ctx.lineTo(left + width, top + height);
  84. ctx.lineTo(left, top + height);
  85. ctx.closePath();
  86. ctx.stroke();
  87. ctx.font = '24px Rubik';
  88. ctx.textAlign = 'right';
  89. ctx.textBaseline = 'bottom';
  90. const textWidth = ctx.measureText(annotation).width;
  91. // Draw rect around text
  92. ctx.fillStyle = 'rgba(30, 30, 30, 0.75)';
  93. ctx.fillRect(left + width - textWidth, top + height - 30, textWidth, 30);
  94. // Draw text
  95. ctx.fillStyle = 'white';
  96. ctx.fillText(annotation, left + width, top + height);
  97. highlightsByNodeId.set(nodeId, {
  98. canvas,
  99. });
  100. wrapper.insertBefore(canvas, mouseTail);
  101. return {
  102. canvas,
  103. };
  104. }