@gem-sdk/clarity-visualize 0.8.94

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -0
  3. package/build/clarity.visualize.js +2753 -0
  4. package/build/clarity.visualize.min.js +1 -0
  5. package/build/clarity.visualize.module.js +2748 -0
  6. package/package.json +45 -0
  7. package/rollup.config.ts +79 -0
  8. package/src/clarity.ts +3 -0
  9. package/src/custom/README.md +87 -0
  10. package/src/custom/canvas-layer.ts +355 -0
  11. package/src/custom/dialog.ts +242 -0
  12. package/src/custom/index.ts +7 -0
  13. package/src/data.ts +103 -0
  14. package/src/enrich.ts +80 -0
  15. package/src/global.ts +9 -0
  16. package/src/heatmap.ts +512 -0
  17. package/src/index.ts +4 -0
  18. package/src/interaction.ts +524 -0
  19. package/src/layout.ts +805 -0
  20. package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
  21. package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
  22. package/src/styles/blobUnavailable/dutch.svg +5 -0
  23. package/src/styles/blobUnavailable/english.svg +5 -0
  24. package/src/styles/blobUnavailable/french.svg +5 -0
  25. package/src/styles/blobUnavailable/german.svg +5 -0
  26. package/src/styles/blobUnavailable/iconOnly.svg +4 -0
  27. package/src/styles/blobUnavailable/italian.svg +5 -0
  28. package/src/styles/blobUnavailable/japanese.svg +5 -0
  29. package/src/styles/blobUnavailable/korean.svg +5 -0
  30. package/src/styles/blobUnavailable/portuguese.svg +5 -0
  31. package/src/styles/blobUnavailable/russian.svg +5 -0
  32. package/src/styles/blobUnavailable/spanish.svg +5 -0
  33. package/src/styles/blobUnavailable/turkish.svg +5 -0
  34. package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
  35. package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
  36. package/src/styles/iframeUnavailable/dutch.svg +5 -0
  37. package/src/styles/iframeUnavailable/english.svg +5 -0
  38. package/src/styles/iframeUnavailable/french.svg +5 -0
  39. package/src/styles/iframeUnavailable/german.svg +5 -0
  40. package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
  41. package/src/styles/iframeUnavailable/italian.svg +5 -0
  42. package/src/styles/iframeUnavailable/japanese.svg +5 -0
  43. package/src/styles/iframeUnavailable/korean.svg +5 -0
  44. package/src/styles/iframeUnavailable/portuguese.svg +5 -0
  45. package/src/styles/iframeUnavailable/russian.svg +5 -0
  46. package/src/styles/iframeUnavailable/spanish.svg +5 -0
  47. package/src/styles/iframeUnavailable/turkish.svg +5 -0
  48. package/src/styles/imageMasked/chineseSimplified.svg +5 -0
  49. package/src/styles/imageMasked/chineseTraditional.svg +5 -0
  50. package/src/styles/imageMasked/dutch.svg +5 -0
  51. package/src/styles/imageMasked/english.svg +5 -0
  52. package/src/styles/imageMasked/french.svg +5 -0
  53. package/src/styles/imageMasked/german.svg +5 -0
  54. package/src/styles/imageMasked/iconOnly.svg +4 -0
  55. package/src/styles/imageMasked/italian.svg +5 -0
  56. package/src/styles/imageMasked/japanese.svg +5 -0
  57. package/src/styles/imageMasked/korean.svg +5 -0
  58. package/src/styles/imageMasked/portuguese.svg +5 -0
  59. package/src/styles/imageMasked/russian.svg +5 -0
  60. package/src/styles/imageMasked/spanish.svg +5 -0
  61. package/src/styles/imageMasked/turkish.svg +5 -0
  62. package/src/styles/pointer/click.css +31 -0
  63. package/src/styles/pointer/pointerIcon.svg +18 -0
  64. package/src/styles/shared.css +6 -0
  65. package/src/visualizer.ts +308 -0
  66. package/tsconfig.json +13 -0
  67. package/tslint.json +33 -0
  68. package/types/heatmap.d.ts +18 -0
  69. package/types/index.d.ts +11 -0
  70. package/types/layout.d.ts +12 -0
  71. package/types/string-import.d.ts +9 -0
  72. package/types/visualize.d.ts +253 -0
package/src/layout.ts ADDED
@@ -0,0 +1,805 @@
1
+ import { Data, Layout } from "@gem-sdk/clarity-js";
2
+ import type { Layout as DecodedLayout } from "@gem-sdk/clarity-decode";
3
+ import { Asset, Constant, type LinkHandler, NodeType, type PlaybackState, Setting } from "@clarity-types/visualize";
4
+ import { StyleSheetOperation } from "@gem-sdk/clarity-js/types/layout";
5
+ import { AnimationOperation } from "@gem-sdk/clarity-js/types/layout";
6
+ import { Constant as LayoutConstants } from "@gem-sdk/clarity-js/types/layout";
7
+ import sharedStyle from "./styles/shared.css";
8
+ import * as dialogCustom from "./custom/dialog";
9
+
10
+ /* BEGIN blobUnavailableSvgs */
11
+ import blobUnavailableSvgEnglish from "./styles/blobUnavailable/english.svg";
12
+ import blobUnavailableSvgSmall from "./styles/blobUnavailable/iconOnly.svg";
13
+ import blobUnavailableSvgChineseSimplified from "./styles/blobUnavailable/chineseSimplified.svg";
14
+ import blobUnavailableSvgChineseTraditional from "./styles/blobUnavailable/chineseTraditional.svg";
15
+ import blobUnavailableSvgJapanese from "./styles/blobUnavailable/japanese.svg";
16
+ import blobUnavailableSvgKorean from "./styles/blobUnavailable/korean.svg";
17
+ import blobUnavailableSvgRussian from "./styles/blobUnavailable/russian.svg";
18
+ import blobUnavailableSvgSpanish from "./styles/blobUnavailable/spanish.svg";
19
+ import blobUnavailableSvgTurkish from "./styles/blobUnavailable/turkish.svg";
20
+ import blobUnavailableSvgDutch from "./styles/blobUnavailable/dutch.svg";
21
+ import blobUnavailableSvgFrench from "./styles/blobUnavailable/french.svg";
22
+ import blobUnavailableSvgGerman from "./styles/blobUnavailable/german.svg";
23
+ import blobUnavailableSvgItalian from "./styles/blobUnavailable/italian.svg";
24
+ import blobUnavailableSvgPortuguese from "./styles/blobUnavailable/portuguese.svg";
25
+ const blobUnavailableSvg = {
26
+ "de-de": blobUnavailableSvgGerman,
27
+ "en-gb": blobUnavailableSvgEnglish,
28
+ "en-us": blobUnavailableSvgEnglish,
29
+ "es-es": blobUnavailableSvgSpanish,
30
+ "fr-fr": blobUnavailableSvgFrench,
31
+ "it-it": blobUnavailableSvgItalian,
32
+ "ja-jp": blobUnavailableSvgJapanese,
33
+ "ko-kr": blobUnavailableSvgKorean,
34
+ "nl-nl": blobUnavailableSvgDutch,
35
+ "pt-br": blobUnavailableSvgPortuguese,
36
+ "ru-ru": blobUnavailableSvgRussian,
37
+ "tr-tr": blobUnavailableSvgTurkish,
38
+ "zh-hans": blobUnavailableSvgChineseSimplified,
39
+ "zh-hant": blobUnavailableSvgChineseTraditional,
40
+ }
41
+ /* END blobUnavailableSvgs */
42
+
43
+ /* BEGIN iframeUnavailableSvgs */
44
+ import iframeUnavailableSvgEnglish from "./styles/iframeUnavailable/english.svg";
45
+ import iframeUnavailableSvgSmall from "./styles/iframeUnavailable/iconOnly.svg";
46
+ import iframeUnavailableSvgChineseSimplified from "./styles/iframeUnavailable/chineseSimplified.svg";
47
+ import iframeUnavailableSvgChineseTraditional from "./styles/iframeUnavailable/chineseTraditional.svg";
48
+ import iframeUnavailableSvgJapanese from "./styles/iframeUnavailable/japanese.svg";
49
+ import iframeUnavailableSvgKorean from "./styles/iframeUnavailable/korean.svg";
50
+ import iframeUnavailableSvgRussian from "./styles/iframeUnavailable/russian.svg";
51
+ import iframeUnavailableSvgSpanish from "./styles/iframeUnavailable/spanish.svg";
52
+ import iframeUnavailableSvgTurkish from "./styles/iframeUnavailable/turkish.svg";
53
+ import iframeUnavailableSvgDutch from "./styles/iframeUnavailable/dutch.svg";
54
+ import iframeUnavailableSvgFrench from "./styles/iframeUnavailable/french.svg";
55
+ import iframeUnavailableSvgGerman from "./styles/iframeUnavailable/german.svg";
56
+ import iframeUnavailableSvgItalian from "./styles/iframeUnavailable/italian.svg";
57
+ import iframeUnavailableSvgPortuguese from "./styles/iframeUnavailable/portuguese.svg";
58
+ const iframeUnavailableSvg = {
59
+ "de-de": iframeUnavailableSvgGerman,
60
+ "en-gb": iframeUnavailableSvgEnglish,
61
+ "en-us": iframeUnavailableSvgEnglish,
62
+ "es-es": iframeUnavailableSvgSpanish,
63
+ "fr-fr": iframeUnavailableSvgFrench,
64
+ "it-it": iframeUnavailableSvgItalian,
65
+ "ja-jp": iframeUnavailableSvgJapanese,
66
+ "ko-kr": iframeUnavailableSvgKorean,
67
+ "nl-nl": iframeUnavailableSvgDutch,
68
+ "pt-br": iframeUnavailableSvgPortuguese,
69
+ "ru-ru": iframeUnavailableSvgRussian,
70
+ "tr-tr": iframeUnavailableSvgTurkish,
71
+ "zh-hans": iframeUnavailableSvgChineseSimplified,
72
+ "zh-hant": iframeUnavailableSvgChineseTraditional,
73
+ }
74
+ /* END iframeUnavailableSvgs */
75
+
76
+ /* BEGIN imageMaskedSvgs */
77
+ import imageMaskedSvgEnglish from "./styles/imageMasked/english.svg";
78
+ import imageMaskedSvgSmall from "./styles/imageMasked/iconOnly.svg";
79
+ import imageMaskedSvgChineseSimplified from "./styles/imageMasked/chineseSimplified.svg";
80
+ import imageMaskedSvgChineseTraditional from "./styles/imageMasked/chineseTraditional.svg";
81
+ import imageMaskedSvgJapanese from "./styles/imageMasked/japanese.svg";
82
+ import imageMaskedSvgKorean from "./styles/imageMasked/korean.svg";
83
+ import imageMaskedSvgRussian from "./styles/imageMasked/russian.svg";
84
+ import imageMaskedSvgSpanish from "./styles/imageMasked/spanish.svg";
85
+ import imageMaskedSvgTurkish from "./styles/imageMasked/turkish.svg";
86
+ import imageMaskedSvgDutch from "./styles/imageMasked/dutch.svg";
87
+ import imageMaskedSvgFrench from "./styles/imageMasked/french.svg";
88
+ import imageMaskedSvgGerman from "./styles/imageMasked/german.svg";
89
+ import imageMaskedSvgItalian from "./styles/imageMasked/italian.svg";
90
+ import imageMaskedSvgPortuguese from "./styles/imageMasked/portuguese.svg";
91
+ const imageMaskedSvg = {
92
+ "de-de": imageMaskedSvgGerman,
93
+ "en-gb": imageMaskedSvgEnglish,
94
+ "en-us": imageMaskedSvgEnglish,
95
+ "es-es": imageMaskedSvgSpanish,
96
+ "fr-fr": imageMaskedSvgFrench,
97
+ "it-it": imageMaskedSvgItalian,
98
+ "ja-jp": imageMaskedSvgJapanese,
99
+ "ko-kr": imageMaskedSvgKorean,
100
+ "nl-nl": imageMaskedSvgDutch,
101
+ "pt-br": imageMaskedSvgPortuguese,
102
+ "ru-ru": imageMaskedSvgRussian,
103
+ "tr-tr": imageMaskedSvgTurkish,
104
+ "zh-hans": imageMaskedSvgChineseSimplified,
105
+ "zh-hant": imageMaskedSvgChineseTraditional,
106
+ }
107
+ /* END imageMaskedSvgs */
108
+
109
+ export class LayoutHelper {
110
+ static TIMEOUT = 7000;
111
+
112
+ primaryHtmlNodeId: number | null = null;
113
+ isMobile: boolean;
114
+ stylesheets: Promise<void>[] = [];
115
+ fonts: Promise<void>[] = [];
116
+ nodes = {};
117
+ events = {};
118
+ hashMapAlpha = {};
119
+ hashMapBeta = {};
120
+ adoptedStyleSheets = {};
121
+ animations = {};
122
+ state: PlaybackState = null;
123
+ stylesToApply: { [id: string]: string[] } = {};
124
+ BackgroundImageEligibleElements = ['DIV', 'SECTION', 'ARTICLE', 'HEADER', 'FOOTER', 'ASIDE', 'NAV', 'SPAN', 'P', 'MAIN'];
125
+ MaskedBackgroundImageStyle = `#CCC no-repeat center url("${Asset.Hide}")`;
126
+ vNext: boolean;
127
+ locale: string;
128
+
129
+ constructor(state: PlaybackState, isMobile = false, vNext = false, locale = 'en-us') {
130
+ this.state = state;
131
+ this.isMobile = isMobile;
132
+ this.vNext = vNext;
133
+ this.locale = locale;
134
+ }
135
+
136
+ public reset = (): void => {
137
+ this.nodes = {};
138
+ this.stylesheets = [];
139
+ this.fonts = [];
140
+ this.events = {};
141
+ this.hashMapAlpha = {};
142
+ this.hashMapBeta = {};
143
+ this.primaryHtmlNodeId = null;
144
+
145
+ // Reset dialog render state for new render cycle
146
+ dialogCustom.resetDialogRenderState();
147
+ }
148
+
149
+ /** Rebuild internal node/hash maps from a pre-rendered document (used for HTML cache restore). */
150
+ public hydrate = (doc: Document): void => {
151
+ this.nodes = {};
152
+ this.hashMapAlpha = {};
153
+ this.hashMapBeta = {};
154
+ // querySelectorAll does not include documentElement itself
155
+ const htmlEl = doc.documentElement;
156
+ if (htmlEl?.hasAttribute(Constant.Id)) {
157
+ const id = parseInt(htmlEl.getAttribute(Constant.Id), 10);
158
+ if (!isNaN(id) && id > 0) { this.nodes[id] = htmlEl; }
159
+ }
160
+ const elements = doc.querySelectorAll(`[${Constant.Id}]`);
161
+ for (const el of Array.from(elements)) {
162
+ const id = parseInt(el.getAttribute(Constant.Id), 10);
163
+ if (!isNaN(id) && id > 0) {
164
+ this.nodes[id] = el;
165
+ const ha = el.getAttribute(Constant.HashAlpha);
166
+ const hb = el.getAttribute(Constant.HashBeta);
167
+ if (ha) { this.hashMapAlpha[ha] = el as HTMLElement; }
168
+ if (hb) { this.hashMapBeta[hb] = el as HTMLElement; }
169
+ }
170
+ }
171
+ }
172
+
173
+ public get = (hash) => {
174
+ if (hash in this.hashMapBeta && this.hashMapBeta[hash].isConnected) {
175
+ return this.hashMapBeta[hash];
176
+ } else if (hash in this.hashMapAlpha && this.hashMapAlpha[hash].isConnected) {
177
+ return this.hashMapAlpha[hash];
178
+ }
179
+ return null;
180
+ }
181
+
182
+ private addToHashMap = (data: DecodedLayout.DomData, parent: Node) => {
183
+ // In case of selector collision, prefer the first inserted node
184
+ this.hashMapAlpha[data.hashAlpha] = this.get(data.hashAlpha) || parent;
185
+ this.hashMapBeta[data.hashBeta] = this.get(data.hashBeta) || parent;
186
+ }
187
+
188
+ private resize = (el: HTMLElement, width: number, height: number): void => {
189
+ if (el && el.nodeType === NodeType.ELEMENT_NODE && width && height) {
190
+ el.style.width = width + Layout.Constant.Pixel;
191
+ el.style.height = height + Layout.Constant.Pixel;
192
+ el.style.boxSizing = Layout.Constant.BorderBox; // Reference: https://developer.mozilla.org/en-US/docs/Web/CSS/box-sizing
193
+ }
194
+ }
195
+
196
+ public element = (nodeId: number): Node | null => {
197
+ return nodeId !== null && nodeId > 0 && nodeId in this.nodes ? this.nodes[nodeId] : null;
198
+ }
199
+
200
+ public animateChange = (event: DecodedLayout.AnimationEvent): void => {
201
+ let animation: Animation = this.animations[event.data.id];
202
+ if (!animation && event.data.operation !== AnimationOperation.Create) {
203
+ // We didn't have a reference to this animation. This shouldn't happen, but returning here
204
+ // to ensure we don't throw any errors.
205
+ return;
206
+ }
207
+ switch (event.data.operation) {
208
+ case AnimationOperation.Create:
209
+ let target = this.element(event.data.targetId);
210
+ // only create the animation if we successfully found the target, an animation without a target will throw an error
211
+ if (target) {
212
+ this.animations[event.data.id] = (target as HTMLElement).animate(JSON.parse(event.data.keyFrames), JSON.parse(event.data.timing));
213
+ }
214
+ break;
215
+ case AnimationOperation.Cancel:
216
+ animation.cancel();
217
+ break;
218
+ case AnimationOperation.Finish:
219
+ animation.finish();
220
+ break;
221
+ case AnimationOperation.Pause:
222
+ animation.pause();
223
+ break;
224
+ case AnimationOperation.Play:
225
+ animation.play();
226
+ break;
227
+ case AnimationOperation.CommitStyles:
228
+ animation.commitStyles();
229
+ break;
230
+ }
231
+ }
232
+
233
+ public dom = async (event: DecodedLayout.DomEvent, useproxy?: LinkHandler): Promise<void> => {
234
+ if (event) {
235
+ // When setting up rendering for the first time, start off with hidden target window
236
+ // This ensures we do not show flickers to the end user
237
+ let doc = this.state.window.document;
238
+ if (doc && doc.documentElement) {
239
+ doc.documentElement.style.visibility = Constant.Hidden;
240
+ // Render all DOM events to reconstruct the page
241
+ this.markup(event, useproxy);
242
+ // Wait on all stylesheets and fonts to finish loading
243
+ await Promise.all(this.stylesheets.concat(this.fonts));
244
+ // Toggle back the visibility of target window
245
+ doc.documentElement.style.visibility = Constant.Visible;
246
+ }
247
+ }
248
+ }
249
+
250
+ public styleChange = (event: DecodedLayout.StyleSheetEvent): void => {
251
+ switch (event.event) {
252
+ case Data.Event.StyleSheetUpdate:
253
+ let styleSheet: CSSStyleSheet = this.adoptedStyleSheets[event.data.id];
254
+ if (!styleSheet && event.data.operation !== StyleSheetOperation.Create) {
255
+ return;
256
+ }
257
+ switch (event.data.operation) {
258
+ case StyleSheetOperation.Create:
259
+ this.adoptedStyleSheets[event.data.id] = new (this.state.window as any).CSSStyleSheet();
260
+ break;
261
+ case StyleSheetOperation.Replace:
262
+ styleSheet.replace(event.data.cssRules);
263
+ break;
264
+ case StyleSheetOperation.ReplaceSync:
265
+ styleSheet.replaceSync(event.data.cssRules);
266
+ break;
267
+ }
268
+ break;
269
+ case Data.Event.StyleSheetAdoption:
270
+ this.setDocumentStyles(event.data.id as number, event.data.newIds);
271
+ break;
272
+ }
273
+ }
274
+
275
+ public customElement = (event: DecodedLayout.CustomElementEvent): void => {
276
+ const tagName = event.data.name;
277
+ if (!this.state.window.customElements.get(tagName)) {
278
+ try {
279
+ // Use eval to create class in target window context (avoids ES5 transpilation issues)
280
+ const EmptyElement = (this.state.window as any).eval(
281
+ '(class extends HTMLElement { constructor() { super(); } })'
282
+ );
283
+ this.state.window.customElements.define(tagName, EmptyElement);
284
+ } catch (e) {
285
+ console.error(`Failed to define custom element ${tagName}:`, e);
286
+ }
287
+ }
288
+ }
289
+
290
+ private setDocumentStyles(documentId: number, styleIds: string[]) {
291
+ let targetDocument = documentId === -1 ? this.state.window.document : this.element(documentId) as Document;
292
+
293
+ if (!targetDocument) {
294
+ if (!this.stylesToApply[documentId]) {
295
+ this.stylesToApply[documentId] = [];
296
+ }
297
+ this.stylesToApply[documentId] = styleIds;
298
+ return;
299
+ }
300
+
301
+ let newSheets: CSSStyleSheet[] = [];
302
+ for (var styleId of styleIds) {
303
+ let styleSheet = this.adoptedStyleSheets[styleId];
304
+ if (styleSheet) {
305
+ newSheets.push(styleSheet);
306
+ }
307
+ }
308
+ targetDocument.adoptedStyleSheets = newSheets
309
+ }
310
+
311
+ public exists = (hash: string): boolean => {
312
+ if (hash) {
313
+ let match = this.get(hash);
314
+ if (match) {
315
+ let rectangle = match.getBoundingClientRect();
316
+ return rectangle && rectangle.width > 0 && rectangle.height > 0;
317
+ }
318
+ }
319
+ return false;
320
+ }
321
+
322
+ public markup = (event: DecodedLayout.DomEvent, useproxy?: LinkHandler): void => {
323
+ let data = event.data;
324
+ let type = event.event;
325
+ let doc = this.state.window.document;
326
+ let retryEvent: DecodedLayout.DomEvent = {
327
+ data: [],
328
+ time: event.time,
329
+ event: event.event
330
+ }
331
+ for (let node of data) {
332
+ let parent = this.element(node.parent);
333
+ let pivot = this.element(node.previous);
334
+ let insert = this.insertAfter;
335
+
336
+ let tag = node.tag;
337
+ if (tag && tag.indexOf(Layout.Constant.IFramePrefix) === 0) { tag = node.tag.substr(Layout.Constant.IFramePrefix.length); }
338
+ if (parent === null && node.parent !== null && node.parent > -1 && tag !== "HTML") {
339
+ // We are referencing a parent for this node that hasn't been created yet. Push it to a list of nodes to
340
+ // try once we are finished with other nodes within this event. Though we don't require HTML tags to
341
+ // have a parent as they are typically the root.
342
+ retryEvent.data.push(node);
343
+ continue;
344
+ }
345
+ switch (tag) {
346
+ case Layout.Constant.DocumentTag:
347
+ let tagDoc = tag !== node.tag ? (parent ? (parent as HTMLIFrameElement).contentDocument : null) : doc;
348
+ if (tagDoc && tagDoc === doc && type === Data.Event.Discover) { this.reset(); }
349
+ if (typeof XMLSerializer !== "undefined" && tagDoc) {
350
+ tagDoc.open();
351
+ tagDoc.write(new XMLSerializer().serializeToString(
352
+ tagDoc.implementation.createDocumentType(
353
+ node.attributes["name"],
354
+ node.attributes["publicId"],
355
+ node.attributes["systemId"]
356
+ )
357
+ ));
358
+ tagDoc.close();
359
+ }
360
+ break;
361
+ case Layout.Constant.PolyfillShadowDomTag:
362
+ // In case of polyfill, map shadow dom to it's parent for rendering purposes
363
+ // All its children should be inserted as regular children to the parent node.
364
+ this.nodes[node.id] = parent;
365
+ this.addToHashMap(node, parent);
366
+ break;
367
+ case Layout.Constant.ShadowDomTag:
368
+ if (parent) {
369
+ let shadowRoot = this.element(node.id);
370
+ shadowRoot = shadowRoot ? shadowRoot : (parent as HTMLElement).attachShadow({ mode: "open" });
371
+ this.nodes[node.id] = shadowRoot;
372
+ this.addToHashMap(node, shadowRoot);
373
+ this.addStyles(node.id);
374
+ }
375
+ break;
376
+ case Layout.Constant.TextTag:
377
+ let textElement = this.element(node.id);
378
+ textElement = textElement ? textElement : doc.createTextNode(null);
379
+ textElement.nodeValue = node.value;
380
+ insert(node, parent, textElement, pivot);
381
+ break;
382
+ case Layout.Constant.SuspendMutationTag:
383
+ let suspendedElement = this.element(node.id);
384
+ if (suspendedElement && suspendedElement.nodeType === Node.ELEMENT_NODE) {
385
+ (suspendedElement as HTMLElement).setAttribute(Constant.Suspend, Layout.Constant.Empty);
386
+ }
387
+ break;
388
+ case "HTML":
389
+ if (this.primaryHtmlNodeId === null) {
390
+ this.primaryHtmlNodeId = node.id;
391
+ }
392
+ let isIframe = tag !== node.tag;
393
+ // when we see multiple HTML nodes in the same document we should treat subsequent ones as child elements
394
+ // rather than redefining our visualization base on them. It's technically illegal HTML but enough sites have
395
+ // this structure that we are robust against it.
396
+ if (this.primaryHtmlNodeId !== node.id && !isIframe) {
397
+ this.insertDefaultElement(node, parent, pivot, doc, insert);
398
+ break;
399
+ }
400
+ let htmlDoc = isIframe ? (parent ? (parent as HTMLIFrameElement).contentDocument : null) : doc;
401
+ if (htmlDoc !== null) {
402
+ let docElement = this.element(node.id) as HTMLElement;
403
+ if (docElement === null) {
404
+ let newDoc = htmlDoc.implementation.createHTMLDocument(Layout.Constant.Empty);
405
+ docElement = newDoc.documentElement;
406
+ let p = htmlDoc.importNode(docElement, true);
407
+ htmlDoc.replaceChild(p, htmlDoc.documentElement);
408
+ if (htmlDoc.head) { htmlDoc.head.parentNode.removeChild(htmlDoc.head); }
409
+ if (htmlDoc.body) { htmlDoc.body.parentNode.removeChild(htmlDoc.body); }
410
+ }
411
+ this.setAttributes(htmlDoc.documentElement, node);
412
+ // If we are still processing discover events, keep the markup hidden until we are done
413
+ if (type === Data.Event.Discover && !parent) { htmlDoc.documentElement.style.visibility = Constant.Hidden; }
414
+ this.nodes[node.id] = htmlDoc.documentElement;
415
+ this.addToHashMap(node, htmlDoc.documentElement);
416
+ }
417
+ break;
418
+ case "HEAD":
419
+ let headElement = this.element(node.id);
420
+ if (headElement === null) {
421
+ headElement = doc.createElement(node.tag);
422
+ if (node.attributes && Layout.Constant.Base in node.attributes) {
423
+ let base = doc.createElement("base");
424
+ base.href = node.attributes[Layout.Constant.Base];
425
+ headElement.appendChild(base);
426
+ }
427
+
428
+ // Add custom styles to assist with visualization
429
+ let custom = doc.createElement("style");
430
+ custom.setAttribute(Constant.CustomStyleTag, "true");
431
+ custom.innerText = this.getCustomStyle();
432
+ headElement.appendChild(custom);
433
+ }
434
+ this.setAttributes(headElement as HTMLElement, node);
435
+ insert(node, parent, headElement, pivot);
436
+ break;
437
+ case "LINK":
438
+ let linkElement = this.element(node.id) as HTMLLinkElement;
439
+ linkElement = linkElement ? linkElement : this.createElement(doc, node.tag) as HTMLLinkElement;
440
+ if (!node.attributes) { node.attributes = {}; }
441
+ this.setAttributes(linkElement, node);
442
+ if ("rel" in node.attributes) {
443
+ if (node.attributes["rel"] === Constant.StyleSheet) {
444
+ this.stylesheets.push(new Promise((resolve: () => void): void => {
445
+ const proxy = useproxy ?? this.state.options.useproxy;
446
+ if (proxy) {
447
+ if (linkElement.integrity) {
448
+ linkElement.removeAttribute('integrity');
449
+ }
450
+
451
+ linkElement.href = proxy(linkElement.href, linkElement.id, Constant.StyleSheet);
452
+ }
453
+ linkElement.onload = linkElement.onerror = this.style.bind(this, linkElement, resolve);
454
+ setTimeout(resolve, LayoutHelper.TIMEOUT);
455
+ }));
456
+ } else if ((node.attributes["rel"].includes("preload") || node.attributes["rel"].includes("preconnect"))
457
+ && (node.attributes?.as === "style" || node.attributes?.as === "font")) {
458
+ this.fonts.push(new Promise((resolve: () => void): void => {
459
+ const proxy = useproxy ?? this.state.options.useproxy;
460
+ linkElement.href = proxy ? proxy(linkElement.href, linkElement.id, node.attributes.as) : linkElement.href;
461
+ linkElement.onload = linkElement.onerror = this.style.bind(this, linkElement, resolve);
462
+ setTimeout(resolve, LayoutHelper.TIMEOUT);
463
+ }));
464
+ }
465
+ }
466
+ insert(node, parent, linkElement, pivot);
467
+ break;
468
+ case Layout.Constant.ImageTag:
469
+ let imgElement = this.element(node.id) as HTMLImageElement ?? this.createElement(doc, node.tag) as HTMLImageElement;
470
+ const proxy = useproxy ?? this.state.options.useproxy;
471
+ if (proxy && !!node.attributes?.src) {
472
+ node.attributes.src = proxy(node.attributes.src, node.attributes.id, Layout.Constant.ImageTag);
473
+ }
474
+ this.setAttributes(imgElement, node);
475
+ this.resize(imgElement, node.width, node.height);
476
+ insert(node, parent, imgElement, pivot);
477
+ break;
478
+ case "STYLE":
479
+ let styleElement = this.element(node.id) as HTMLStyleElement ?? doc.createElement(node.tag) as HTMLStyleElement;
480
+ this.setAttributes(styleElement, node);
481
+ styleElement.textContent = node.value;
482
+ insert(node, parent, styleElement, pivot);
483
+ this.style(styleElement);
484
+ break;
485
+ case "IFRAME":
486
+ let iframeElement = this.element(node.id) as HTMLIFrameElement;
487
+ iframeElement = iframeElement ? iframeElement : this.createElement(doc, node.tag) as HTMLIFrameElement;
488
+ if (!node.attributes) { node.attributes = {}; }
489
+ this.setAttributes(iframeElement, node);
490
+ insert(node, parent, iframeElement, pivot);
491
+ break;
492
+ case "SCRIPT":
493
+ {
494
+ node.id = -1; // We want to ensure children of script tags are not processed
495
+ node.value = null; // We don't want to set any potential script content
496
+ this.insertDefaultElement(node, parent, pivot, doc, insert);
497
+ break;
498
+ }
499
+ case "DIALOG":
500
+ {
501
+ this.insertDefaultElement(node, parent, pivot, doc, insert);
502
+
503
+ const domElement = this.element(node.id) as HTMLDialogElement;
504
+ const renderOptions = dialogCustom.getDialogRenderOptions(node.attributes, domElement);
505
+ dialogCustom.renderDialog(
506
+ domElement,
507
+ renderOptions,
508
+ this.state.options.logerror
509
+ );
510
+ break;
511
+ }
512
+ default:
513
+ this.insertDefaultElement(node, parent, pivot, doc, insert);
514
+ break;
515
+ }
516
+ // Track state for this node
517
+ if (node.id) { this.events[node.id] = node; }
518
+ }
519
+ // only retry failed nodes if we are still making positive progress. If we have the same number of
520
+ // nodes we started with, then we would just be spinning on an orphaned subtree.
521
+ if (retryEvent.data.length > 0 && retryEvent.data.length !== event.data.length) {
522
+ this.markup(retryEvent, useproxy);
523
+ }
524
+ }
525
+
526
+ private insertDefaultElement = (node: DecodedLayout.DomData, parent: Node, pivot: Node, doc: Document, insert: (data: DecodedLayout.DomData, parent: Node, node: Node, previous: Node) => void): void => {
527
+ let domElement = this.element(node.id) as HTMLElement;
528
+ domElement = domElement ? domElement : this.createElement(doc, node.tag);
529
+ this.setAttributes(domElement as HTMLElement, node);
530
+ this.resize(domElement, node.width, node.height);
531
+ insert(node, parent, domElement, pivot);
532
+ }
533
+
534
+ private style = (node: HTMLLinkElement | HTMLStyleElement, resolve: () => void = null): void => {
535
+ // Firefox throws a SecurityError when trying to access cssRules of a stylesheet from a different domain
536
+ try {
537
+ const sheet = node.sheet as CSSStyleSheet;
538
+ let cssRules = sheet ? sheet.cssRules : [];
539
+ for (let i = 0; i < cssRules.length; i++) {
540
+ if (cssRules[i].cssText.indexOf(Constant.Hover) >= 0) {
541
+ let css = cssRules[i].cssText.replace(/:hover/g, `[${Constant.CustomHover}]`);
542
+ sheet.removeRule(i);
543
+ sheet.insertRule(css, i);
544
+ }
545
+ }
546
+ } catch (e) {
547
+ if (this.state.options.logerror) {
548
+ this.state.options.logerror(e);
549
+ }
550
+ }
551
+
552
+ if (resolve) { resolve(); }
553
+ }
554
+
555
+ private addStyles = (id: number): void => {
556
+ let adoptedStylesToAdd = this.stylesToApply[id];
557
+ if (adoptedStylesToAdd && adoptedStylesToAdd.length > 0) {
558
+ this.setDocumentStyles(id, this.stylesToApply[id]);
559
+ delete this.stylesToApply[id];
560
+ }
561
+ }
562
+
563
+ private createElement = (doc: Document, tag: string): HTMLElement => {
564
+ if (tag && tag.indexOf(Layout.Constant.SvgPrefix) === 0) {
565
+ return doc.createElementNS(Layout.Constant.SvgNamespace as string, tag.substr(Layout.Constant.SvgPrefix.length)) as HTMLElement;
566
+ }
567
+ try {
568
+ return doc.createElement(tag);
569
+ } catch (ex) {
570
+ // We log the warning on non-standard markup but continue with the visualization
571
+ console.warn(`Exception encountered while creating element ${tag}: ${ex}`);
572
+ return doc.createElement(Constant.UnknownTag);
573
+ };
574
+ }
575
+
576
+ private insertAfter = (data: DecodedLayout.DomData, parent: Node, node: Node, previous: Node): void => {
577
+ // Skip over no-op changes where parent and previous element is still the same
578
+ // In case of IFRAME, re-adding DOM at the exact same place will lead to loss of state and the markup inside will be destroyed
579
+ if (this.events[data.id] && this.events[data.id].parent === data.parent && this.events[data.id].previous === data.previous) { return; }
580
+ // In case parent is a Shadow DOM, previous.parentElement will return null but previous.parentNode will return a valid node
581
+ let next = previous && (previous.parentElement === parent || previous.parentNode === parent) ? previous.nextSibling : null;
582
+ next = previous === null && parent ? this.firstChild(parent) : next;
583
+ this.insertBefore(data, parent, node, next);
584
+ }
585
+
586
+ private firstChild = (node: Node): ChildNode => {
587
+ let child = node.firstChild;
588
+ // BASE tag should always be the first child to ensure resources with relative URLs are loaded correctly
589
+ if (child && child.nodeType === NodeType.ELEMENT_NODE && (child as HTMLElement).tagName === Layout.Constant.BaseTag) {
590
+ if ((child.nextSibling as HTMLElement)?.hasAttribute('clarity-custom-styles')) {
591
+ // Keep the custom style tag on top of the head to let client tags override its values.
592
+ return child.nextSibling.nextSibling;
593
+ }
594
+ return child.nextSibling;
595
+ }
596
+ return child;
597
+ }
598
+
599
+
600
+ // Mask images within a masked ancestor element in the node has a background image.
601
+ private mask = (node: HTMLElement) => {
602
+ if (node && this.BackgroundImageEligibleElements.includes(node.nodeName) && 'getComputedStyle' in window && 'closest' in node) {
603
+ const urlPattern = /url\(['"]?([^'")]+)['"]?\)/;
604
+ const computedStyles = window.getComputedStyle(node);
605
+ const hasBackgroundImage = computedStyles.backgroundImage?.match(urlPattern) || computedStyles.background?.match(urlPattern);
606
+ const masked = node.closest?.(`[${LayoutConstants.MaskData}]`);
607
+
608
+ if (hasBackgroundImage && masked) {
609
+ node.style.background = this.MaskedBackgroundImageStyle;
610
+ }
611
+ }
612
+ };
613
+
614
+ private insertBefore = (data: DecodedLayout.DomData, parent: Node, node: Node, next: Node): void => {
615
+ if (parent !== null) {
616
+ // Compare against both parentNode and parentElement to ensure visualization works correctly for shadow DOMs
617
+ next = next && (next.parentElement !== parent && next.parentNode !== parent) ? null : next;
618
+ try {
619
+ parent.insertBefore(node, next);
620
+ this.mask(node as HTMLElement);
621
+ } catch (ex) {
622
+ console.warn("Node: " + node + " | Parent: " + parent + " | Data: " + JSON.stringify(data));
623
+ console.warn("Exception encountered while inserting node: " + ex);
624
+ }
625
+ } else if (parent === null && node.parentElement !== null) {
626
+ node.parentElement.removeChild(node);
627
+ } else if (parent === null && node.parentNode !== null) {
628
+ node.parentNode.removeChild(node);
629
+ }
630
+ this.nodes[data.id] = node;
631
+ this.addToHashMap(data, node);
632
+ }
633
+
634
+ private setAttributes = (node: HTMLElement, data: DecodedLayout.DomData): void => {
635
+ let attributes = data.attributes || {};
636
+ let sameorigin = false;
637
+
638
+ // Clarity attributes
639
+ attributes[Constant.Id] = `${data.id}`;
640
+ attributes[Constant.HashAlpha] = `${data.hashAlpha}`;
641
+ attributes[Constant.HashBeta] = `${data.hashBeta}`;
642
+
643
+ let tag = node.nodeType === NodeType.ELEMENT_NODE ? node.tagName.toLowerCase() : null;
644
+ // First remove all its existing attributes
645
+ if (node.attributes) {
646
+ let length = node.attributes.length;
647
+ while (node.attributes && length > 0) {
648
+ // Do not remove "clarity-hover" attribute and let it be managed by interaction module
649
+ // This helps avoid flickers during visualization
650
+ if (node.attributes[0].name !== Constant.HoverAttribute) {
651
+ node.removeAttribute(node.attributes[0].name);
652
+ }
653
+ length--;
654
+ }
655
+ }
656
+
657
+ // Add new attributes
658
+ for (let attribute in attributes) {
659
+ if (attributes[attribute] !== undefined) {
660
+ try {
661
+ let v = attributes[attribute];
662
+ if (attribute.indexOf("xlink:") === 0) {
663
+ node.setAttributeNS("http://www.w3.org/1999/xlink", attribute, v);
664
+ } else if (attribute.indexOf(Layout.Constant.SameOrigin) === 0) {
665
+ sameorigin = true;
666
+ } else if (attribute.indexOf("*") === 0) {
667
+ // Do nothing if we encounter internal Clarity attributes
668
+ } else if (tag === Constant.IFrameTag && (attribute.indexOf("src") === 0 || attribute.indexOf("allow") === 0) || attribute === "sandbox") {
669
+ node.setAttribute(`data-clarity-${attribute}`, v);
670
+ } else if (tag === Constant.ImageTag && attribute.indexOf("src") === 0 && ((v === null || v.length === 0 || v?.startsWith('blob:')))) {
671
+ const srcValue = attributes["src"];
672
+ const srcIsValid = srcValue && srcValue.length > 0 && !srcValue.startsWith("blob:");
673
+ if (attribute === "srcset" && srcIsValid) {
674
+ // srcset is empty/blob but src is a valid URL — skip to avoid overwriting with transparent
675
+ } else if (this.vNext) {
676
+ if (v.startsWith('blob:')) {
677
+ if (data.width >= Setting.LargeSvg && data.height >= Setting.LargeSvg) {
678
+ node.setAttribute(Constant.BlobUnavailable, `${Constant.Large}${Constant.Beta}`);
679
+ } else {
680
+ node.setAttribute(Constant.BlobUnavailable, `${Constant.Small}${Constant.Beta}`);
681
+ }
682
+ } else {
683
+ if (data.width >= Setting.LargeSvg && data.height >= Setting.LargeSvg) {
684
+ node.setAttribute(Constant.Hide, `${Constant.Large}${Constant.Beta}`);
685
+ } else {
686
+ node.setAttribute(Constant.Hide, `${Constant.Small}${Constant.Beta}`);
687
+ }
688
+ }
689
+ } else {
690
+ node.setAttribute(attribute, Asset.Transparent);
691
+ let size = Constant.Large;
692
+ if (data.width) {
693
+ size = data.width <= Setting.Medium ? Constant.Medium : (data.width <= Setting.Small ? Constant.Small : size);
694
+ }
695
+ node.setAttribute(Constant.Hide, size);
696
+ }
697
+ } else {
698
+ node.setAttribute(attribute, this.isSuspiciousAttribute(attribute, v) ? Constant.Empty : v);
699
+ }
700
+ } catch (ex) {
701
+ console.warn("Node: " + node + " | " + JSON.stringify(attributes));
702
+ console.warn("Exception encountered while adding attributes: " + ex);
703
+ }
704
+ }
705
+ }
706
+
707
+ if (sameorigin === false && tag === Constant.IFrameTag && typeof node.setAttribute === Constant.Function) {
708
+ if (this.svgFitsText(node)) {
709
+ node.setAttribute(Constant.Unavailable, Layout.Constant.Empty);
710
+ } else {
711
+ node.setAttribute(Constant.UnavailableSmall, Layout.Constant.Empty);
712
+ }
713
+ }
714
+
715
+ // Add an empty ALT tag on all IMG elements
716
+ if (tag === Constant.ImageTag && !node.hasAttribute(Constant.AltAttribute)) { node.setAttribute(Constant.AltAttribute, Constant.Empty); }
717
+
718
+ // During visualization This will prevent the browser from auto filling form fields with saved details of user who is seeing the visualization
719
+ if (tag === Constant.FormTag || tag === Constant.InputTag) {
720
+ if (node.hasAttribute(Constant.AutoComplete)) {
721
+ node.removeAttribute(Constant.AutoComplete);
722
+ }
723
+ node.setAttribute(Constant.AutoComplete, Constant.NewPassword);
724
+ }
725
+ }
726
+
727
+ private isSuspiciousAttribute(name: string, value: string): boolean {
728
+ // Block event handlers entirely
729
+ if (name.startsWith('on')) {
730
+ return true;
731
+ }
732
+
733
+ // Check for JavaScript protocols and dangerous patterns
734
+ const dangerous = [
735
+ /^\s*javascript:/i,
736
+ /^\s*data:text\/html/i,
737
+ /^\s*vbscript:/i
738
+ ];
739
+
740
+ return dangerous.some(pattern => pattern.test(value));
741
+ }
742
+
743
+
744
+ private getMobileCustomStyle = (): string => {
745
+ if (this.isMobile) {
746
+ return `*{scrollbar-width: none; scrollbar-gutter: unset;};`
747
+ }
748
+
749
+ return '';
750
+ }
751
+
752
+ private getCustomStyle = (): string => {
753
+ return this.getImageHiddenCss() +
754
+ this.getIframeUnavailableCss() +
755
+ this.getBlobUnavailableCss() +
756
+ this.getBackgroundCss() +
757
+ `*[${Constant.Suspend}] { filter: grayscale(100%); }` +
758
+ `body { font-size: initial; }
759
+ ${this.getMobileCustomStyle()}`;
760
+ }
761
+
762
+ private svgFitsText = (inputElement: HTMLElement): boolean => {
763
+ var dimensions = inputElement.getBoundingClientRect();
764
+ if (dimensions.width >= Setting.LargeSvg && dimensions.height >= Setting.LargeSvg) {
765
+ return true;
766
+ }
767
+ return false;
768
+ }
769
+
770
+ private getIframeUnavailableCss = (): string => {
771
+ if (this.vNext) {
772
+ return `${Constant.IFrameTag}[${Constant.UnavailableSmall}] { ${iframeUnavailableSvgSmall} }` +
773
+ `${Constant.IFrameTag}[${Constant.Unavailable}] { ${iframeUnavailableSvg[this.locale]} }`;
774
+ } else {
775
+ return `${Constant.IFrameTag}[${Constant.Unavailable}] { background: url(${Asset.Unavailable}) no-repeat center center, url('${Asset.Cross}'); }`;
776
+ }
777
+ }
778
+
779
+ private getBlobUnavailableCss = (): string => {
780
+ if (this.vNext) {
781
+ return `${Constant.ImageTag}[${Constant.BlobUnavailable}=${Constant.Small}${Constant.Beta}] { ${blobUnavailableSvgSmall} }` +
782
+ `${Constant.ImageTag}[${Constant.BlobUnavailable}=${Constant.Large}${Constant.Beta}] { ${blobUnavailableSvg[this.locale]} }`;
783
+ }
784
+ return '';
785
+ }
786
+
787
+ private getImageHiddenCss = (): string => {
788
+ if (this.vNext) {
789
+ return `${Constant.ImageTag}[${Constant.Hide}=${Constant.Small}${Constant.Beta}] { ${imageMaskedSvgSmall} }` +
790
+ `${Constant.ImageTag}[${Constant.Hide}=${Constant.Large}${Constant.Beta}] { ${imageMaskedSvg[this.locale]} }`;
791
+ } else {
792
+ return `${Constant.ImageTag}[${Constant.Hide}] { background-color: #CCC; background-image: url(${Asset.Hide}); background-repeat:no-repeat; background-position: center; }` +
793
+ `${Constant.ImageTag}[${Constant.Hide}=${Constant.Small}] { background-size: 18px 18px; }` +
794
+ `${Constant.ImageTag}[${Constant.Hide}=${Constant.Medium}] { background-size: 24px 24px; }` +
795
+ `${Constant.ImageTag}[${Constant.Hide}=${Constant.Large}] { background-size: 36px 36px; }`;
796
+ }
797
+ }
798
+
799
+ private getBackgroundCss = (): string => {
800
+ if (this.vNext) {
801
+ return sharedStyle;
802
+ }
803
+ return '';
804
+ }
805
+ }