wontfix-sdk 0.0.1 → 0.0.2
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.
- package/fesm2022/wontfix-sdk.mjs +1286 -2
- package/fesm2022/wontfix-sdk.mjs.map +1 -1
- package/package.json +4 -2
- package/types/wontfix-sdk.d.ts +196 -1
package/fesm2022/wontfix-sdk.mjs
CHANGED
|
@@ -1,5 +1,1288 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Component } from '@angular/core';
|
|
2
|
+
import { EventEmitter, Output, Input, ChangeDetectionStrategy, Component, inject, ApplicationRef, Injector, createComponent, Injectable } from '@angular/core';
|
|
3
|
+
import { BehaviorSubject, firstValueFrom, filter, take } from 'rxjs';
|
|
4
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
5
|
+
import * as i1 from '@angular/common';
|
|
6
|
+
import { CommonModule } from '@angular/common';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Mock tooltip data for demo purposes
|
|
10
|
+
* Paths are in format: tag_index/tag_index/.../
|
|
11
|
+
* Index indicates which child with that tag name (0-based)
|
|
12
|
+
* templateId: 1 = product tour, 2 = tooltip
|
|
13
|
+
*/
|
|
14
|
+
const MOCK_TOOLTIPS = [
|
|
15
|
+
{
|
|
16
|
+
templateId: 2,
|
|
17
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_0/div_0/button_0/',
|
|
18
|
+
pageUrl: '*',
|
|
19
|
+
metadata: {
|
|
20
|
+
interaction: 'hover',
|
|
21
|
+
position: 'top',
|
|
22
|
+
text: '💾 Save your work! All changes will be stored securely.'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
templateId: 2,
|
|
27
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_0/div_0/button_1/',
|
|
28
|
+
pageUrl: '*',
|
|
29
|
+
metadata: {
|
|
30
|
+
interaction: 'hover',
|
|
31
|
+
position: 'top',
|
|
32
|
+
text: '⚠️ Warning: This action cannot be undone!'
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
templateId: 2,
|
|
37
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_0/div_0/button_2/',
|
|
38
|
+
pageUrl: '*',
|
|
39
|
+
metadata: {
|
|
40
|
+
interaction: 'hover',
|
|
41
|
+
position: 'top',
|
|
42
|
+
text: '🔄 Refresh the page to get latest updates'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
templateId: 2,
|
|
47
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_1/div_0/button_0/',
|
|
48
|
+
pageUrl: '*',
|
|
49
|
+
metadata: {
|
|
50
|
+
interaction: 'click',
|
|
51
|
+
position: 'right',
|
|
52
|
+
text: '❓ Need help? Click here to view our documentation and FAQs.'
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
templateId: 2,
|
|
57
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_1/div_0/button_1/',
|
|
58
|
+
pageUrl: '*',
|
|
59
|
+
metadata: {
|
|
60
|
+
interaction: 'click',
|
|
61
|
+
position: 'right',
|
|
62
|
+
text: '⚙️ Configure app settings and preferences'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
templateId: 2,
|
|
67
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_1/div_0/button_2/',
|
|
68
|
+
pageUrl: '*',
|
|
69
|
+
metadata: {
|
|
70
|
+
interaction: 'click',
|
|
71
|
+
position: 'right',
|
|
72
|
+
text: 'ℹ️ Learn more about Wontfix Tooltip SDK v1.0'
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
templateId: 2,
|
|
77
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_2/form_0/div_0/input_0/',
|
|
78
|
+
pageUrl: '*',
|
|
79
|
+
metadata: {
|
|
80
|
+
interaction: 'click',
|
|
81
|
+
position: 'right',
|
|
82
|
+
text: '📧 Enter a valid email address (e.g., user@example.com)'
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
templateId: 2,
|
|
87
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_2/form_0/div_1/input_0/',
|
|
88
|
+
pageUrl: '*',
|
|
89
|
+
metadata: {
|
|
90
|
+
interaction: 'click',
|
|
91
|
+
position: 'right',
|
|
92
|
+
text: '🔒 Use a strong password with at least 8 characters'
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
templateId: 2,
|
|
97
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_2/form_0/div_2/input_0/',
|
|
98
|
+
pageUrl: '*',
|
|
99
|
+
metadata: {
|
|
100
|
+
interaction: 'click',
|
|
101
|
+
position: 'right',
|
|
102
|
+
text: '✓ Must match the password above'
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
templateId: 2,
|
|
107
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_3/div_0/button_0/',
|
|
108
|
+
pageUrl: '*',
|
|
109
|
+
metadata: {
|
|
110
|
+
interaction: 'click',
|
|
111
|
+
position: 'top',
|
|
112
|
+
text: '⬆️ This tooltip appears at the TOP'
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
templateId: 2,
|
|
117
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_3/div_0/button_1/',
|
|
118
|
+
pageUrl: '*',
|
|
119
|
+
metadata: {
|
|
120
|
+
interaction: 'click',
|
|
121
|
+
position: 'right',
|
|
122
|
+
text: '➡️ This tooltip appears on the RIGHT'
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
templateId: 2,
|
|
127
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_3/div_0/button_2/',
|
|
128
|
+
pageUrl: '*',
|
|
129
|
+
metadata: {
|
|
130
|
+
interaction: 'click',
|
|
131
|
+
position: 'bottom',
|
|
132
|
+
text: '⬇️ This tooltip appears at the BOTTOM'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
templateId: 2,
|
|
137
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_3/div_0/button_3/',
|
|
138
|
+
pageUrl: '*',
|
|
139
|
+
metadata: {
|
|
140
|
+
interaction: 'click',
|
|
141
|
+
position: 'left',
|
|
142
|
+
text: '⬅️ This tooltip appears on the LEFT'
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
templateId: 1,
|
|
147
|
+
path: '*',
|
|
148
|
+
pageUrl: '/product-tour',
|
|
149
|
+
metadata: {
|
|
150
|
+
assetUrl: '/assets/product-tour.mp4',
|
|
151
|
+
header: 'Cool Product Tour',
|
|
152
|
+
buttonText: 'Awesome!'
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
templateId: 3,
|
|
157
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_2/div_0/button_0/',
|
|
158
|
+
pageUrl: '*',
|
|
159
|
+
metadata: {
|
|
160
|
+
position: 'left',
|
|
161
|
+
label: ''
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
templateId: 3,
|
|
166
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_2/div_0/button_1/',
|
|
167
|
+
pageUrl: '*',
|
|
168
|
+
metadata: {
|
|
169
|
+
position: 'right',
|
|
170
|
+
label: 'Hot'
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
templateId: 4,
|
|
175
|
+
path: '*',
|
|
176
|
+
pageUrl: '*',
|
|
177
|
+
metadata: {
|
|
178
|
+
title: 'Getting Started Tour',
|
|
179
|
+
steps: [
|
|
180
|
+
{
|
|
181
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_0/div_0/button_0/',
|
|
182
|
+
position: 'bottom',
|
|
183
|
+
title: 'Save Your Work',
|
|
184
|
+
text: 'Click the Save button to securely store all your changes.',
|
|
185
|
+
order: 1
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_0/div_0/button_1/',
|
|
189
|
+
position: 'bottom',
|
|
190
|
+
title: 'Delete Items',
|
|
191
|
+
text: 'Use this button to permanently delete items. This action cannot be undone!',
|
|
192
|
+
order: 2
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
path: 'html_0/body_0/app-root_0/app-tooltip-demo_0/div_0/section_1/div_0/button_0/',
|
|
196
|
+
position: 'right',
|
|
197
|
+
title: 'Get Help',
|
|
198
|
+
text: 'Need assistance? Click here to access our comprehensive help documentation.',
|
|
199
|
+
order: 3
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
templateId: 5,
|
|
206
|
+
path: '*',
|
|
207
|
+
pageUrl: '/popup-demo',
|
|
208
|
+
metadata: {
|
|
209
|
+
title: 'Welcome to Wontfix! 🎉',
|
|
210
|
+
text: 'We\'re excited to have you here. This popup demonstrates how you can show important announcements, feature updates, or onboarding messages to your users.',
|
|
211
|
+
buttonText: 'Got it'
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Path matcher utility for DOM element targeting
|
|
218
|
+
* Matches elements based on their path in the DOM tree
|
|
219
|
+
*
|
|
220
|
+
* Path format: "body_0/app-root_0/app-tooltip-demo_0/div_0/button_1/"
|
|
221
|
+
* Numbers indicate the child index (0-based) among siblings of the same tag name
|
|
222
|
+
*/
|
|
223
|
+
class PathMatcher {
|
|
224
|
+
/**
|
|
225
|
+
* Find DOM element by path
|
|
226
|
+
* @param path DOM path like "html_0/body_0/div_0/button_1/"
|
|
227
|
+
* @returns The matching element or null
|
|
228
|
+
*/
|
|
229
|
+
static findElementByPath(path) {
|
|
230
|
+
// Remove leading/trailing slashes and split
|
|
231
|
+
const segments = path.split('/').filter(s => s.length > 0);
|
|
232
|
+
if (segments.length === 0) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
// Start from the appropriate root
|
|
236
|
+
let currentElement = null;
|
|
237
|
+
let startIndex = 0;
|
|
238
|
+
// Check if first segment is the html element
|
|
239
|
+
if (segments[0].startsWith('html')) {
|
|
240
|
+
currentElement = document.documentElement;
|
|
241
|
+
startIndex = 1;
|
|
242
|
+
}
|
|
243
|
+
else if (segments[0].startsWith('body')) {
|
|
244
|
+
currentElement = document.body;
|
|
245
|
+
startIndex = 1;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
currentElement = document.documentElement;
|
|
249
|
+
}
|
|
250
|
+
// Process remaining segments
|
|
251
|
+
for (let i = startIndex; i < segments.length; i++) {
|
|
252
|
+
if (!currentElement) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const segment = segments[i];
|
|
256
|
+
const [tagName, indexStr] = this.parseSegment(segment);
|
|
257
|
+
const index = parseInt(indexStr, 10);
|
|
258
|
+
// Find all children with the same tag name
|
|
259
|
+
const childrenWithTag = Array.from(currentElement.children).filter((child) => {
|
|
260
|
+
const matches = child.tagName.toLowerCase() === tagName.toLowerCase();
|
|
261
|
+
return matches;
|
|
262
|
+
});
|
|
263
|
+
// Get the element at the specified index
|
|
264
|
+
if (index >= 0 && index < childrenWithTag.length) {
|
|
265
|
+
currentElement = childrenWithTag[index];
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
return null; // Index out of bounds
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return currentElement instanceof HTMLElement ? currentElement : null;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get the path for a given DOM element
|
|
275
|
+
* @param element The element to get the path for
|
|
276
|
+
* @returns Path string like "body_0/div_0/button_1/"
|
|
277
|
+
*/
|
|
278
|
+
static getPathForElement(element) {
|
|
279
|
+
const segments = [];
|
|
280
|
+
let currentElement = element;
|
|
281
|
+
while (currentElement && currentElement !== document.documentElement) {
|
|
282
|
+
const parent = currentElement.parentElement;
|
|
283
|
+
if (!parent)
|
|
284
|
+
break;
|
|
285
|
+
// Count siblings with same tag name that come before this element
|
|
286
|
+
const siblings = Array.from(parent.children).filter((child) => child.tagName === currentElement.tagName);
|
|
287
|
+
const index = siblings.indexOf(currentElement);
|
|
288
|
+
if (index >= 0) {
|
|
289
|
+
segments.unshift(`${currentElement.tagName.toLowerCase()}_${index}`);
|
|
290
|
+
}
|
|
291
|
+
currentElement = parent;
|
|
292
|
+
}
|
|
293
|
+
// Add root element
|
|
294
|
+
if (currentElement === document.documentElement) {
|
|
295
|
+
segments.unshift(`${document.documentElement.tagName.toLowerCase()}_0`);
|
|
296
|
+
}
|
|
297
|
+
return segments.join('/') + '/';
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Parse a path segment like "button_1" into ["button", "1"]
|
|
301
|
+
*/
|
|
302
|
+
static parseSegment(segment) {
|
|
303
|
+
const lastUnderscoreIndex = segment.lastIndexOf('_');
|
|
304
|
+
if (lastUnderscoreIndex === -1) {
|
|
305
|
+
return [segment, '0'];
|
|
306
|
+
}
|
|
307
|
+
return [
|
|
308
|
+
segment.substring(0, lastUnderscoreIndex),
|
|
309
|
+
segment.substring(lastUnderscoreIndex + 1)
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
class TooltipComponent {
|
|
315
|
+
tooltip;
|
|
316
|
+
close = new EventEmitter();
|
|
317
|
+
top = 0;
|
|
318
|
+
left = 0;
|
|
319
|
+
closeTooltip() {
|
|
320
|
+
this.close.emit();
|
|
321
|
+
}
|
|
322
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TooltipComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
323
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.8", type: TooltipComponent, isStandalone: true, selector: "app-tooltip-popover", inputs: { tooltip: "tooltip" }, outputs: { close: "close" }, ngImport: i0, template: `
|
|
324
|
+
<div
|
|
325
|
+
class="tooltip-container"
|
|
326
|
+
[ngClass]="'tooltip-' + tooltip.position"
|
|
327
|
+
[style.top.px]="top"
|
|
328
|
+
[style.left.px]="left"
|
|
329
|
+
>
|
|
330
|
+
<div class="tooltip-content">
|
|
331
|
+
{{ tooltip.text }}
|
|
332
|
+
</div>
|
|
333
|
+
<div class="tooltip-arrow"></div>
|
|
334
|
+
</div>
|
|
335
|
+
`, isInline: true, styles: [".tooltip-container{position:fixed;background-color:#333;color:#fff;padding:8px 12px;border-radius:4px;font-size:12px;z-index:9999;box-shadow:0 2px 8px #00000026;max-width:300px;word-wrap:break-word;pointer-events:none;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.tooltip-arrow{position:absolute;width:0;height:0;border-style:solid}.tooltip-top{transform:translate(-50%) translateY(-100%)}.tooltip-top .tooltip-arrow{bottom:-4px;left:50%;transform:translate(-50%);border-width:4px 4px 0 4px;border-color:#333 transparent transparent transparent}.tooltip-bottom{transform:translate(-50%) translateY(8px)}.tooltip-bottom .tooltip-arrow{top:-4px;left:50%;transform:translate(-50%);border-width:0 4px 4px 4px;border-color:transparent transparent #333 transparent}.tooltip-left{transform:translate(-8px) translateY(-50%)}.tooltip-left .tooltip-arrow{right:-4px;top:50%;transform:translateY(-50%);border-width:4px 0 4px 4px;border-color:transparent transparent transparent #333}.tooltip-right{transform:translate(8px) translateY(-50%)}.tooltip-right .tooltip-arrow{left:-4px;top:50%;transform:translateY(-50%);border-width:4px 4px 4px 0;border-color:transparent #333 transparent transparent}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
336
|
+
}
|
|
337
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TooltipComponent, decorators: [{
|
|
338
|
+
type: Component,
|
|
339
|
+
args: [{ selector: 'app-tooltip-popover', standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
340
|
+
<div
|
|
341
|
+
class="tooltip-container"
|
|
342
|
+
[ngClass]="'tooltip-' + tooltip.position"
|
|
343
|
+
[style.top.px]="top"
|
|
344
|
+
[style.left.px]="left"
|
|
345
|
+
>
|
|
346
|
+
<div class="tooltip-content">
|
|
347
|
+
{{ tooltip.text }}
|
|
348
|
+
</div>
|
|
349
|
+
<div class="tooltip-arrow"></div>
|
|
350
|
+
</div>
|
|
351
|
+
`, styles: [".tooltip-container{position:fixed;background-color:#333;color:#fff;padding:8px 12px;border-radius:4px;font-size:12px;z-index:9999;box-shadow:0 2px 8px #00000026;max-width:300px;word-wrap:break-word;pointer-events:none;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.tooltip-arrow{position:absolute;width:0;height:0;border-style:solid}.tooltip-top{transform:translate(-50%) translateY(-100%)}.tooltip-top .tooltip-arrow{bottom:-4px;left:50%;transform:translate(-50%);border-width:4px 4px 0 4px;border-color:#333 transparent transparent transparent}.tooltip-bottom{transform:translate(-50%) translateY(8px)}.tooltip-bottom .tooltip-arrow{top:-4px;left:50%;transform:translate(-50%);border-width:0 4px 4px 4px;border-color:transparent transparent #333 transparent}.tooltip-left{transform:translate(-8px) translateY(-50%)}.tooltip-left .tooltip-arrow{right:-4px;top:50%;transform:translateY(-50%);border-width:4px 0 4px 4px;border-color:transparent transparent transparent #333}.tooltip-right{transform:translate(8px) translateY(-50%)}.tooltip-right .tooltip-arrow{left:-4px;top:50%;transform:translateY(-50%);border-width:4px 4px 4px 0;border-color:transparent #333 transparent transparent}\n"] }]
|
|
352
|
+
}], propDecorators: { tooltip: [{
|
|
353
|
+
type: Input
|
|
354
|
+
}], close: [{
|
|
355
|
+
type: Output
|
|
356
|
+
}] } });
|
|
357
|
+
|
|
358
|
+
class BeaconComponent {
|
|
359
|
+
elementRef;
|
|
360
|
+
config;
|
|
361
|
+
constructor(elementRef) {
|
|
362
|
+
this.elementRef = elementRef;
|
|
363
|
+
}
|
|
364
|
+
ngOnInit() {
|
|
365
|
+
// Position beacon relative to its parent container
|
|
366
|
+
// The beacon container will be positioned at top-left, then adjusted via CSS classes
|
|
367
|
+
}
|
|
368
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BeaconComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
|
|
369
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BeaconComponent, isStandalone: true, selector: "app-beacon", inputs: { config: "config" }, ngImport: i0, template: `
|
|
370
|
+
<div class="beacon-container" [class]="'beacon-' + config.position">
|
|
371
|
+
<div class="beacon-pulse"></div>
|
|
372
|
+
<div class="beacon-dot"></div>
|
|
373
|
+
</div>
|
|
374
|
+
@if (config.label) {
|
|
375
|
+
<span class="beacon-label">{{ config.label }}</span>
|
|
376
|
+
}
|
|
377
|
+
`, isInline: true, styles: [".beacon-container{display:flex;justify-content:center;align-items:center;gap:6px;z-index:100}.beacon-pulse{position:absolute;width:18px;height:18px;border-radius:50%;background:#e74c3c4d;animation:pulse 2s infinite}@keyframes pulse{0%{transform:scale(.8);opacity:1}70%{transform:scale(1.8);opacity:0}to{transform:scale(2);opacity:0}}.beacon-dot{position:relative;width:10px;height:10px;border-radius:50%;background:#e74c3c;box-shadow:0 2px 6px #e74c3c80;z-index:1;flex-shrink:0}.beacon-label{display:block;margin-top:8px;font-size:10px;font-weight:700;color:#fff;background:#e74c3c;padding:2px 5px;border-radius:3px;white-space:nowrap;text-transform:uppercase;letter-spacing:.4px;flex-shrink:0}\n"] });
|
|
378
|
+
}
|
|
379
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BeaconComponent, decorators: [{
|
|
380
|
+
type: Component,
|
|
381
|
+
args: [{ selector: 'app-beacon', standalone: true, template: `
|
|
382
|
+
<div class="beacon-container" [class]="'beacon-' + config.position">
|
|
383
|
+
<div class="beacon-pulse"></div>
|
|
384
|
+
<div class="beacon-dot"></div>
|
|
385
|
+
</div>
|
|
386
|
+
@if (config.label) {
|
|
387
|
+
<span class="beacon-label">{{ config.label }}</span>
|
|
388
|
+
}
|
|
389
|
+
`, styles: [".beacon-container{display:flex;justify-content:center;align-items:center;gap:6px;z-index:100}.beacon-pulse{position:absolute;width:18px;height:18px;border-radius:50%;background:#e74c3c4d;animation:pulse 2s infinite}@keyframes pulse{0%{transform:scale(.8);opacity:1}70%{transform:scale(1.8);opacity:0}to{transform:scale(2);opacity:0}}.beacon-dot{position:relative;width:10px;height:10px;border-radius:50%;background:#e74c3c;box-shadow:0 2px 6px #e74c3c80;z-index:1;flex-shrink:0}.beacon-label{display:block;margin-top:8px;font-size:10px;font-weight:700;color:#fff;background:#e74c3c;padding:2px 5px;border-radius:3px;white-space:nowrap;text-transform:uppercase;letter-spacing:.4px;flex-shrink:0}\n"] }]
|
|
390
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { config: [{
|
|
391
|
+
type: Input
|
|
392
|
+
}] } });
|
|
393
|
+
|
|
394
|
+
class NavFlowComponent {
|
|
395
|
+
step;
|
|
396
|
+
currentStep;
|
|
397
|
+
totalSteps;
|
|
398
|
+
next = new EventEmitter();
|
|
399
|
+
previous = new EventEmitter();
|
|
400
|
+
close = new EventEmitter();
|
|
401
|
+
top = 0;
|
|
402
|
+
left = 0;
|
|
403
|
+
onNextClick() {
|
|
404
|
+
if (this.currentStep === this.totalSteps) {
|
|
405
|
+
this.close.emit();
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
this.next.emit();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NavFlowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
412
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.8", type: NavFlowComponent, isStandalone: true, selector: "app-nav-flow", inputs: { step: "step", currentStep: "currentStep", totalSteps: "totalSteps" }, outputs: { next: "next", previous: "previous", close: "close" }, ngImport: i0, template: `
|
|
413
|
+
<div
|
|
414
|
+
class="nav-flow-container"
|
|
415
|
+
[ngClass]="'nav-flow-' + step.position"
|
|
416
|
+
[style.top.px]="top"
|
|
417
|
+
[style.left.px]="left"
|
|
418
|
+
>
|
|
419
|
+
<div class="nav-flow-header">
|
|
420
|
+
<span class="step-indicator">{{ currentStep }} of {{ totalSteps }}</span>
|
|
421
|
+
<button class="close-btn" (click)="close.emit()">✕</button>
|
|
422
|
+
</div>
|
|
423
|
+
<h3 class="nav-flow-title">{{ step.title }}</h3>
|
|
424
|
+
<p class="nav-flow-text">{{ step.text }}</p>
|
|
425
|
+
<div class="nav-flow-footer">
|
|
426
|
+
<button
|
|
427
|
+
class="nav-btn"
|
|
428
|
+
[disabled]="currentStep === 1"
|
|
429
|
+
(click)="previous.emit()"
|
|
430
|
+
>
|
|
431
|
+
← Previous
|
|
432
|
+
</button>
|
|
433
|
+
<button
|
|
434
|
+
class="nav-btn primary"
|
|
435
|
+
(click)="onNextClick()"
|
|
436
|
+
>
|
|
437
|
+
{{ currentStep === totalSteps ? 'Finish' : 'Next →' }}
|
|
438
|
+
</button>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="nav-flow-arrow"></div>
|
|
441
|
+
</div>
|
|
442
|
+
`, isInline: true, styles: [".nav-flow-container{position:fixed;background:#fff;border-radius:8px;padding:20px;box-shadow:0 4px 20px #0003;max-width:350px;z-index:10001;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.nav-flow-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.step-indicator{font-size:12px;color:#7f8c8d;font-weight:600}.close-btn{background:transparent;border:none;font-size:20px;cursor:pointer;color:#7f8c8d;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.close-btn:hover{background-color:#f0f0f0}.nav-flow-title{margin:0 0 8px;font-size:18px;font-weight:700;color:#2c3e50}.nav-flow-text{margin:0 0 16px;font-size:14px;line-height:1.5;color:#34495e}.nav-flow-footer{display:flex;gap:8px;justify-content:flex-end}.nav-btn{padding:8px 16px;border:1px solid #ddd;background:#fff;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}.nav-btn:hover:not(:disabled){background:#f8f9fa}.nav-btn:disabled{opacity:.5;cursor:not-allowed}.nav-btn.primary{background:#3498db;color:#fff;border-color:#3498db}.nav-btn.primary:hover{background:#2980b9}.nav-flow-arrow{position:absolute;width:0;height:0;border-style:solid}.nav-flow-top .nav-flow-arrow{bottom:-8px;left:50%;transform:translate(-50%);border-width:8px 8px 0 8px;border-color:white transparent transparent transparent}.nav-flow-bottom .nav-flow-arrow{top:-8px;left:50%;transform:translate(-50%);border-width:0 8px 8px 8px;border-color:transparent transparent white transparent}.nav-flow-left .nav-flow-arrow{right:-8px;top:50%;transform:translateY(-50%);border-width:8px 0 8px 8px;border-color:transparent transparent transparent white}.nav-flow-right .nav-flow-arrow{left:-8px;top:50%;transform:translateY(-50%);border-width:8px 8px 8px 0;border-color:transparent white transparent transparent}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] });
|
|
443
|
+
}
|
|
444
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: NavFlowComponent, decorators: [{
|
|
445
|
+
type: Component,
|
|
446
|
+
args: [{ selector: 'app-nav-flow', standalone: true, imports: [CommonModule], template: `
|
|
447
|
+
<div
|
|
448
|
+
class="nav-flow-container"
|
|
449
|
+
[ngClass]="'nav-flow-' + step.position"
|
|
450
|
+
[style.top.px]="top"
|
|
451
|
+
[style.left.px]="left"
|
|
452
|
+
>
|
|
453
|
+
<div class="nav-flow-header">
|
|
454
|
+
<span class="step-indicator">{{ currentStep }} of {{ totalSteps }}</span>
|
|
455
|
+
<button class="close-btn" (click)="close.emit()">✕</button>
|
|
456
|
+
</div>
|
|
457
|
+
<h3 class="nav-flow-title">{{ step.title }}</h3>
|
|
458
|
+
<p class="nav-flow-text">{{ step.text }}</p>
|
|
459
|
+
<div class="nav-flow-footer">
|
|
460
|
+
<button
|
|
461
|
+
class="nav-btn"
|
|
462
|
+
[disabled]="currentStep === 1"
|
|
463
|
+
(click)="previous.emit()"
|
|
464
|
+
>
|
|
465
|
+
← Previous
|
|
466
|
+
</button>
|
|
467
|
+
<button
|
|
468
|
+
class="nav-btn primary"
|
|
469
|
+
(click)="onNextClick()"
|
|
470
|
+
>
|
|
471
|
+
{{ currentStep === totalSteps ? 'Finish' : 'Next →' }}
|
|
472
|
+
</button>
|
|
473
|
+
</div>
|
|
474
|
+
<div class="nav-flow-arrow"></div>
|
|
475
|
+
</div>
|
|
476
|
+
`, styles: [".nav-flow-container{position:fixed;background:#fff;border-radius:8px;padding:20px;box-shadow:0 4px 20px #0003;max-width:350px;z-index:10001;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif}.nav-flow-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.step-indicator{font-size:12px;color:#7f8c8d;font-weight:600}.close-btn{background:transparent;border:none;font-size:20px;cursor:pointer;color:#7f8c8d;padding:0;width:24px;height:24px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.close-btn:hover{background-color:#f0f0f0}.nav-flow-title{margin:0 0 8px;font-size:18px;font-weight:700;color:#2c3e50}.nav-flow-text{margin:0 0 16px;font-size:14px;line-height:1.5;color:#34495e}.nav-flow-footer{display:flex;gap:8px;justify-content:flex-end}.nav-btn{padding:8px 16px;border:1px solid #ddd;background:#fff;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s}.nav-btn:hover:not(:disabled){background:#f8f9fa}.nav-btn:disabled{opacity:.5;cursor:not-allowed}.nav-btn.primary{background:#3498db;color:#fff;border-color:#3498db}.nav-btn.primary:hover{background:#2980b9}.nav-flow-arrow{position:absolute;width:0;height:0;border-style:solid}.nav-flow-top .nav-flow-arrow{bottom:-8px;left:50%;transform:translate(-50%);border-width:8px 8px 0 8px;border-color:white transparent transparent transparent}.nav-flow-bottom .nav-flow-arrow{top:-8px;left:50%;transform:translate(-50%);border-width:0 8px 8px 8px;border-color:transparent transparent white transparent}.nav-flow-left .nav-flow-arrow{right:-8px;top:50%;transform:translateY(-50%);border-width:8px 0 8px 8px;border-color:transparent transparent transparent white}.nav-flow-right .nav-flow-arrow{left:-8px;top:50%;transform:translateY(-50%);border-width:8px 8px 8px 0;border-color:transparent white transparent transparent}\n"] }]
|
|
477
|
+
}], propDecorators: { step: [{
|
|
478
|
+
type: Input
|
|
479
|
+
}], currentStep: [{
|
|
480
|
+
type: Input
|
|
481
|
+
}], totalSteps: [{
|
|
482
|
+
type: Input
|
|
483
|
+
}], next: [{
|
|
484
|
+
type: Output
|
|
485
|
+
}], previous: [{
|
|
486
|
+
type: Output
|
|
487
|
+
}], close: [{
|
|
488
|
+
type: Output
|
|
489
|
+
}] } });
|
|
490
|
+
|
|
491
|
+
class TooltipService {
|
|
492
|
+
appRef = inject(ApplicationRef);
|
|
493
|
+
injector = inject(Injector);
|
|
494
|
+
router = inject(Router, { optional: true });
|
|
495
|
+
tooltipsSubject = new BehaviorSubject([]);
|
|
496
|
+
config = null;
|
|
497
|
+
guidanceCollections = [];
|
|
498
|
+
tooltipComponentRefs = new Map();
|
|
499
|
+
tooltipElements = new Map();
|
|
500
|
+
tooltipListenerCleanup = new Map();
|
|
501
|
+
beaconComponentRefs = new Map();
|
|
502
|
+
beaconElements = new Map();
|
|
503
|
+
navFlowComponentRef = null;
|
|
504
|
+
navFlowElement = null;
|
|
505
|
+
currentNavFlowStep = 0;
|
|
506
|
+
navFlowData = null;
|
|
507
|
+
popupComponentRef = null;
|
|
508
|
+
popupElement = null;
|
|
509
|
+
popupOpen = false;
|
|
510
|
+
routeSubscription = null;
|
|
511
|
+
windowRouteListenersAttached = false;
|
|
512
|
+
tooltips$ = this.tooltipsSubject.asObservable();
|
|
513
|
+
/**
|
|
514
|
+
* Initialize tooltips by fetching from API on app load
|
|
515
|
+
* Waits for Angular to stabilize before matching paths
|
|
516
|
+
*/
|
|
517
|
+
async initializeTooltips(config) {
|
|
518
|
+
this.config = config;
|
|
519
|
+
if (config.debug) {
|
|
520
|
+
console.log('[Wontfix] Initializing tooltips...');
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
// Wait for Angular to stabilize (all components rendered and change detection complete)
|
|
524
|
+
await firstValueFrom(this.appRef.isStable.pipe(filter(stable => stable === true), take(1)));
|
|
525
|
+
if (config.debug) {
|
|
526
|
+
console.log('[Wontfix] Angular stable, fetching tooltips');
|
|
527
|
+
}
|
|
528
|
+
await this.fetchTooltips();
|
|
529
|
+
// After initial load, keep reacting to host route changes.
|
|
530
|
+
this.setupRouteListener();
|
|
531
|
+
}
|
|
532
|
+
catch (error) {
|
|
533
|
+
console.error('[Wontfix] Failed to initialize tooltips:', error);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Fetch tooltips (currently using mock data)
|
|
538
|
+
*/
|
|
539
|
+
async fetchTooltips() {
|
|
540
|
+
if (!this.config)
|
|
541
|
+
return;
|
|
542
|
+
const collections = await this.fetchGuidanceCollections();
|
|
543
|
+
this.guidanceCollections = collections;
|
|
544
|
+
if (this.config.debug) {
|
|
545
|
+
console.log(`[Wontfix] Loaded ${collections.length} guidance collections`);
|
|
546
|
+
}
|
|
547
|
+
// Map new API contract -> existing TooltipInstance shape used by TooltipComponent.
|
|
548
|
+
const tooltips = collections
|
|
549
|
+
.filter((c) => c?.type === 'tooltip')
|
|
550
|
+
.reduce((accumulator, collection) => {
|
|
551
|
+
const pageUrl = this.normalizeUrlToPathPattern(collection.url);
|
|
552
|
+
const elements = Array.isArray(collection.elements) ? collection.elements : [];
|
|
553
|
+
for (const el of elements) {
|
|
554
|
+
const position = (el.position ?? 'top');
|
|
555
|
+
const interaction = (el.interaction ?? 'hover');
|
|
556
|
+
const text = el.content ?? '';
|
|
557
|
+
accumulator.push({
|
|
558
|
+
templateId: 2,
|
|
559
|
+
path: el.selector,
|
|
560
|
+
pageUrl,
|
|
561
|
+
metadata: {
|
|
562
|
+
interaction,
|
|
563
|
+
position,
|
|
564
|
+
text,
|
|
565
|
+
},
|
|
566
|
+
priority: el.order ?? undefined,
|
|
567
|
+
id: `tooltip-${collection.id}-${el.id}`,
|
|
568
|
+
isVisible: false,
|
|
569
|
+
interaction,
|
|
570
|
+
position,
|
|
571
|
+
text,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
return accumulator;
|
|
575
|
+
}, []);
|
|
576
|
+
this.tooltipsSubject.next(tooltips);
|
|
577
|
+
this.matchSelectorsToDOM();
|
|
578
|
+
// Also initialize beacons
|
|
579
|
+
this.initializeBeacons();
|
|
580
|
+
// Check for popups on current route
|
|
581
|
+
this.checkPopups();
|
|
582
|
+
// Check for product tours on current route
|
|
583
|
+
this.checkProductTours();
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Initialize beacons (templateId: 3)
|
|
587
|
+
*/
|
|
588
|
+
initializeBeacons() {
|
|
589
|
+
const currentUrl = window.location.pathname;
|
|
590
|
+
const beacons = this.guidanceCollections.filter((c) => c?.type === 'beacon' && this.urlMatches(c.url, currentUrl));
|
|
591
|
+
beacons.forEach((beacon, collectionIndex) => {
|
|
592
|
+
const elements = Array.isArray(beacon.elements) ? beacon.elements : [];
|
|
593
|
+
elements.forEach((beaconElement, elementIndex) => {
|
|
594
|
+
try {
|
|
595
|
+
const element = this.resolveTargetElement(beaconElement.selector);
|
|
596
|
+
if (element instanceof HTMLElement) {
|
|
597
|
+
this.attachBeaconToElement(element, beacon, beaconElement, collectionIndex, elementIndex);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
console.error(`[Wontfix] ERROR matching beacon selector "${beaconElement.selector}":`, error);
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Attach beacon to an element
|
|
608
|
+
*/
|
|
609
|
+
attachBeaconToElement(element, beacon, beaconElementConfig, collectionIndex, elementIndex) {
|
|
610
|
+
const beaconId = `beacon-${beacon.id}-${beaconElementConfig.id ?? `${collectionIndex}-${elementIndex}`}`;
|
|
611
|
+
const componentRef = createComponent(BeaconComponent, {
|
|
612
|
+
environmentInjector: this.appRef.injector,
|
|
613
|
+
});
|
|
614
|
+
componentRef.instance.config = {
|
|
615
|
+
position: (beaconElementConfig.position ?? 'top'),
|
|
616
|
+
label: beaconElementConfig.content
|
|
617
|
+
};
|
|
618
|
+
this.appRef.attachView(componentRef.hostView);
|
|
619
|
+
const beaconElement = componentRef.location.nativeElement;
|
|
620
|
+
// Get parent button group
|
|
621
|
+
const parent = element.parentElement;
|
|
622
|
+
if (parent) {
|
|
623
|
+
parent.style.position = 'relative';
|
|
624
|
+
// Calculate button position within parent
|
|
625
|
+
const buttonRect = element.getBoundingClientRect();
|
|
626
|
+
const parentRect = parent.getBoundingClientRect();
|
|
627
|
+
const relativeTop = buttonRect.top - parentRect.top;
|
|
628
|
+
const relativeLeft = buttonRect.left - parentRect.left;
|
|
629
|
+
const buttonWidth = buttonRect.width;
|
|
630
|
+
const buttonHeight = buttonRect.height;
|
|
631
|
+
// Set beacon position based on position prop
|
|
632
|
+
const position = beaconElementConfig.position;
|
|
633
|
+
let top = relativeTop;
|
|
634
|
+
let left = relativeLeft;
|
|
635
|
+
switch (position) {
|
|
636
|
+
case 'top':
|
|
637
|
+
top = relativeTop - 18;
|
|
638
|
+
left = relativeLeft + buttonWidth / 2 - 5;
|
|
639
|
+
break;
|
|
640
|
+
case 'bottom':
|
|
641
|
+
top = relativeTop + buttonHeight + 8;
|
|
642
|
+
left = relativeLeft + buttonWidth / 2 - 5;
|
|
643
|
+
break;
|
|
644
|
+
case 'left':
|
|
645
|
+
top = relativeTop + buttonHeight / 2 - 5;
|
|
646
|
+
left = relativeLeft - 18;
|
|
647
|
+
break;
|
|
648
|
+
case 'right':
|
|
649
|
+
top = relativeTop + buttonHeight / 2 - 5;
|
|
650
|
+
left = relativeLeft + buttonWidth + 8;
|
|
651
|
+
break;
|
|
652
|
+
default:
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
beaconElement.style.position = 'absolute';
|
|
656
|
+
beaconElement.style.top = top + 'px';
|
|
657
|
+
beaconElement.style.left = left + 'px';
|
|
658
|
+
parent.appendChild(beaconElement);
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
element.appendChild(beaconElement);
|
|
662
|
+
}
|
|
663
|
+
this.beaconElements.set(beaconId, beaconElement);
|
|
664
|
+
this.beaconComponentRefs.set(beaconId, componentRef);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Check if current route should show a product tour
|
|
668
|
+
*/
|
|
669
|
+
checkProductTours() {
|
|
670
|
+
const currentUrl = window.location.pathname;
|
|
671
|
+
const productTours = this.guidanceCollections.filter(item => item?.type === 'product-tour' && this.urlMatches(item.url, currentUrl));
|
|
672
|
+
if (productTours.length > 0 && this.config?.debug) {
|
|
673
|
+
console.log('[Wontfix] Product tour found for route:', currentUrl);
|
|
674
|
+
// Product tour will be handled by the ProductTourComponent on that route
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Get all tooltips for current page
|
|
679
|
+
*/
|
|
680
|
+
getTooltipsForCurrentPage() {
|
|
681
|
+
const currentUrl = window.location.pathname;
|
|
682
|
+
return this.tooltipsSubject.value.filter(tooltip => this.urlMatches(tooltip.pageUrl, currentUrl));
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Match tooltip paths to actual DOM elements
|
|
686
|
+
*/
|
|
687
|
+
matchSelectorsToDOM() {
|
|
688
|
+
const tooltips = this.tooltipsSubject.value;
|
|
689
|
+
const currentUrl = window.location.pathname;
|
|
690
|
+
tooltips.forEach((tooltip) => {
|
|
691
|
+
if (!this.urlMatches(tooltip.pageUrl, currentUrl)) {
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
const element = this.resolveTargetElement(tooltip.path);
|
|
696
|
+
if (element instanceof HTMLElement) {
|
|
697
|
+
tooltip.element = element;
|
|
698
|
+
this.attachTooltipToElement(tooltip);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
catch (error) {
|
|
702
|
+
console.error(`[Wontfix] ERROR matching path "${tooltip.path}":`, error);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
this.tooltipsSubject.next(tooltips);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Attach tooltip event listeners to an element
|
|
709
|
+
*/
|
|
710
|
+
attachTooltipToElement(tooltip) {
|
|
711
|
+
const element = tooltip.element;
|
|
712
|
+
if (!element) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const existingCleanup = this.tooltipListenerCleanup.get(tooltip.id);
|
|
716
|
+
if (existingCleanup) {
|
|
717
|
+
existingCleanup();
|
|
718
|
+
this.tooltipListenerCleanup.delete(tooltip.id);
|
|
719
|
+
}
|
|
720
|
+
if (tooltip.interaction === 'hover') {
|
|
721
|
+
const onEnter = () => this.showTooltip(tooltip.id);
|
|
722
|
+
const onLeave = () => this.hideTooltip(tooltip.id);
|
|
723
|
+
element.addEventListener('mouseenter', onEnter);
|
|
724
|
+
element.addEventListener('mouseleave', onLeave);
|
|
725
|
+
this.tooltipListenerCleanup.set(tooltip.id, () => {
|
|
726
|
+
element.removeEventListener('mouseenter', onEnter);
|
|
727
|
+
element.removeEventListener('mouseleave', onLeave);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
else if (tooltip.interaction === 'click') {
|
|
731
|
+
const onElementClick = (e) => {
|
|
732
|
+
e.stopPropagation();
|
|
733
|
+
const isVisible = this.tooltipsSubject.value.find(t => t.id === tooltip.id)?.isVisible;
|
|
734
|
+
if (isVisible) {
|
|
735
|
+
this.hideTooltip(tooltip.id);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
this.showTooltip(tooltip.id);
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
element.addEventListener('click', onElementClick);
|
|
742
|
+
// Close on outside click
|
|
743
|
+
const onDocumentClick = () => this.hideTooltip(tooltip.id);
|
|
744
|
+
document.addEventListener('click', onDocumentClick);
|
|
745
|
+
this.tooltipListenerCleanup.set(tooltip.id, () => {
|
|
746
|
+
element.removeEventListener('click', onElementClick);
|
|
747
|
+
document.removeEventListener('click', onDocumentClick);
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Show tooltip
|
|
753
|
+
*/
|
|
754
|
+
showTooltip(tooltipId) {
|
|
755
|
+
const tooltip = this.tooltipsSubject.value.find(t => t.id === tooltipId);
|
|
756
|
+
if (!tooltip || tooltip.isVisible)
|
|
757
|
+
return;
|
|
758
|
+
tooltip.isVisible = true;
|
|
759
|
+
this.createAndShowTooltip(tooltip);
|
|
760
|
+
this.tooltipsSubject.next(this.tooltipsSubject.value);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Create and show tooltip component
|
|
764
|
+
*/
|
|
765
|
+
createAndShowTooltip(tooltip) {
|
|
766
|
+
if (!tooltip.element) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// Remove existing tooltip
|
|
770
|
+
this.hideTooltip(tooltip.id);
|
|
771
|
+
// Create component
|
|
772
|
+
const componentRef = createComponent(TooltipComponent, {
|
|
773
|
+
environmentInjector: this.appRef.injector,
|
|
774
|
+
elementInjector: this.injector
|
|
775
|
+
});
|
|
776
|
+
componentRef.instance.tooltip = tooltip;
|
|
777
|
+
componentRef.instance.close.subscribe(() => {
|
|
778
|
+
this.hideTooltip(tooltip.id);
|
|
779
|
+
});
|
|
780
|
+
this.appRef.attachView(componentRef.hostView);
|
|
781
|
+
const tooltipElement = componentRef.location.nativeElement;
|
|
782
|
+
document.body.appendChild(tooltipElement);
|
|
783
|
+
this.tooltipElements.set(tooltip.id, tooltipElement);
|
|
784
|
+
this.tooltipComponentRefs.set(tooltip.id, componentRef);
|
|
785
|
+
// Position tooltip
|
|
786
|
+
this.positionTooltip(tooltip, tooltipElement);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Position tooltip relative to element
|
|
790
|
+
*/
|
|
791
|
+
positionTooltip(tooltip, tooltipElement) {
|
|
792
|
+
if (!tooltip.element)
|
|
793
|
+
return;
|
|
794
|
+
const rect = tooltip.element.getBoundingClientRect();
|
|
795
|
+
const tooltipRect = tooltipElement.getBoundingClientRect();
|
|
796
|
+
let top = 0;
|
|
797
|
+
let left = 0;
|
|
798
|
+
const offset = 8;
|
|
799
|
+
switch (tooltip.position) {
|
|
800
|
+
case 'top':
|
|
801
|
+
top = rect.top - tooltipRect.height - offset;
|
|
802
|
+
left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
|
803
|
+
break;
|
|
804
|
+
case 'bottom':
|
|
805
|
+
top = rect.bottom + offset;
|
|
806
|
+
left = rect.left + rect.width / 2 - tooltipRect.width / 2;
|
|
807
|
+
break;
|
|
808
|
+
case 'left':
|
|
809
|
+
top = rect.top + rect.height / 2 - tooltipRect.height / 2;
|
|
810
|
+
left = rect.left - tooltipRect.width - offset;
|
|
811
|
+
break;
|
|
812
|
+
case 'right':
|
|
813
|
+
top = rect.top + rect.height / 2 - tooltipRect.height / 2;
|
|
814
|
+
left = rect.right + offset;
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
// Don't add scroll offset for position: fixed elements
|
|
818
|
+
// Fixed positioning is relative to viewport, not document
|
|
819
|
+
// Set position on component instance
|
|
820
|
+
const componentInstance = this.tooltipComponentRefs.get(tooltip.id)?.instance;
|
|
821
|
+
if (componentInstance) {
|
|
822
|
+
componentInstance.top = top;
|
|
823
|
+
componentInstance.left = left;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Hide tooltip
|
|
828
|
+
*/
|
|
829
|
+
hideTooltip(tooltipId) {
|
|
830
|
+
const tooltip = this.tooltipsSubject.value.find(t => t.id === tooltipId);
|
|
831
|
+
if (tooltip) {
|
|
832
|
+
tooltip.isVisible = false;
|
|
833
|
+
}
|
|
834
|
+
const componentRef = this.tooltipComponentRefs.get(tooltipId);
|
|
835
|
+
const tooltipElement = this.tooltipElements.get(tooltipId);
|
|
836
|
+
if (componentRef) {
|
|
837
|
+
this.appRef.detachView(componentRef.hostView);
|
|
838
|
+
componentRef.destroy();
|
|
839
|
+
this.tooltipComponentRefs.delete(tooltipId);
|
|
840
|
+
}
|
|
841
|
+
if (tooltipElement && tooltipElement.parentNode) {
|
|
842
|
+
tooltipElement.parentNode.removeChild(tooltipElement);
|
|
843
|
+
this.tooltipElements.delete(tooltipId);
|
|
844
|
+
}
|
|
845
|
+
this.tooltipsSubject.next(this.tooltipsSubject.value);
|
|
846
|
+
}
|
|
847
|
+
closeAllTooltips() {
|
|
848
|
+
for (const tooltipId of Array.from(this.tooltipComponentRefs.keys())) {
|
|
849
|
+
const tooltip = this.tooltipsSubject.value.find(t => t.id === tooltipId);
|
|
850
|
+
if (tooltip) {
|
|
851
|
+
tooltip.isVisible = false;
|
|
852
|
+
}
|
|
853
|
+
const componentRef = this.tooltipComponentRefs.get(tooltipId);
|
|
854
|
+
const tooltipElement = this.tooltipElements.get(tooltipId);
|
|
855
|
+
if (componentRef) {
|
|
856
|
+
this.appRef.detachView(componentRef.hostView);
|
|
857
|
+
componentRef.destroy();
|
|
858
|
+
}
|
|
859
|
+
if (tooltipElement && tooltipElement.parentNode) {
|
|
860
|
+
tooltipElement.parentNode.removeChild(tooltipElement);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
this.tooltipComponentRefs.clear();
|
|
864
|
+
this.tooltipElements.clear();
|
|
865
|
+
this.tooltipsSubject.next(this.tooltipsSubject.value);
|
|
866
|
+
}
|
|
867
|
+
detachAllTooltipListeners() {
|
|
868
|
+
this.tooltipListenerCleanup.forEach((cleanup) => cleanup());
|
|
869
|
+
this.tooltipListenerCleanup.clear();
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Re-scan DOM for new elements (useful for dynamically added content)
|
|
873
|
+
*/
|
|
874
|
+
rescanDOM() {
|
|
875
|
+
this.matchSelectorsToDOM();
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Update tooltip visibility
|
|
879
|
+
*/
|
|
880
|
+
setTooltipVisible(tooltipId, visible) {
|
|
881
|
+
const tooltips = this.tooltipsSubject.value.map(t => t.id === tooltipId ? { ...t, isVisible: visible } : t);
|
|
882
|
+
this.tooltipsSubject.next(tooltips);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Check if page URL matches the tooltip's pageUrl pattern
|
|
886
|
+
* Supports:
|
|
887
|
+
* - Exact match: "/dashboard"
|
|
888
|
+
* - Wildcard: "/dashboard/*"
|
|
889
|
+
* - All pages: "*"
|
|
890
|
+
*/
|
|
891
|
+
urlMatches(pattern, currentUrl) {
|
|
892
|
+
const normalizedPattern = this.normalizeUrlToPathPattern(pattern);
|
|
893
|
+
const normalizedCurrent = this.normalizeUrlToPathPattern(currentUrl);
|
|
894
|
+
if (normalizedPattern === '*')
|
|
895
|
+
return true;
|
|
896
|
+
if (normalizedPattern === normalizedCurrent)
|
|
897
|
+
return true;
|
|
898
|
+
// Handle wildcard patterns like "/dashboard/*"
|
|
899
|
+
const regexPattern = normalizedPattern
|
|
900
|
+
.replace(/\//g, '\\/')
|
|
901
|
+
.replace(/\*/g, '.*');
|
|
902
|
+
try {
|
|
903
|
+
return new RegExp(`^${regexPattern}$`).test(currentUrl);
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Get tooltip by ID
|
|
911
|
+
*/
|
|
912
|
+
getTooltipById(id) {
|
|
913
|
+
return this.tooltipsSubject.value.find(t => t.id === id);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Get all available tooltips (for debugging)
|
|
917
|
+
*/
|
|
918
|
+
getAvailableTooltips() {
|
|
919
|
+
return this.tooltipsSubject.value.map(t => t.id);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Get product tour data for current route
|
|
923
|
+
*/
|
|
924
|
+
getProductTourForRoute(route) {
|
|
925
|
+
const productTour = this.guidanceCollections.find(item => item?.type === 'product-tour' && this.urlMatches(item.url, route));
|
|
926
|
+
return productTour;
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Reinitialize tooltips and beacons (useful when navigating back to a route)
|
|
930
|
+
*/
|
|
931
|
+
reinitializeTooltips() {
|
|
932
|
+
this.closeAllTooltips();
|
|
933
|
+
this.closeNavFlow();
|
|
934
|
+
this.closePopup();
|
|
935
|
+
this.detachAllTooltipListeners();
|
|
936
|
+
// Clear existing beacons
|
|
937
|
+
this.beaconComponentRefs.forEach(ref => {
|
|
938
|
+
this.appRef.detachView(ref.hostView);
|
|
939
|
+
ref.destroy();
|
|
940
|
+
});
|
|
941
|
+
this.beaconComponentRefs.clear();
|
|
942
|
+
this.beaconElements.forEach(el => {
|
|
943
|
+
if (el && el.parentNode) {
|
|
944
|
+
el.parentNode.removeChild(el);
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
this.beaconElements.clear();
|
|
948
|
+
// Reinitialize beacons
|
|
949
|
+
this.initializeBeacons();
|
|
950
|
+
// Rematch tooltips to DOM
|
|
951
|
+
this.matchSelectorsToDOM();
|
|
952
|
+
this.checkPopups();
|
|
953
|
+
this.checkProductTours();
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Start a nav-flow for the current page
|
|
957
|
+
*/
|
|
958
|
+
startNavFlow() {
|
|
959
|
+
const currentUrl = window.location.pathname;
|
|
960
|
+
const navFlow = this.guidanceCollections.find(item => item?.type === 'nav-flow' && this.urlMatches(item.url, currentUrl));
|
|
961
|
+
if (!navFlow || !navFlow.metadata.steps) {
|
|
962
|
+
console.warn('[Wontfix] No nav-flow found for current page');
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
this.navFlowData = navFlow;
|
|
966
|
+
this.currentNavFlowStep = 0;
|
|
967
|
+
this.showNavFlowStep(0);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Show a specific nav-flow step
|
|
971
|
+
*/
|
|
972
|
+
showNavFlowStep(stepIndex) {
|
|
973
|
+
if (!this.navFlowData)
|
|
974
|
+
return;
|
|
975
|
+
const steps = this.navFlowData.metadata.steps;
|
|
976
|
+
if (stepIndex < 0 || stepIndex >= steps.length)
|
|
977
|
+
return;
|
|
978
|
+
// Clean up existing nav-flow component without resetting state
|
|
979
|
+
if (this.navFlowComponentRef) {
|
|
980
|
+
this.appRef.detachView(this.navFlowComponentRef.hostView);
|
|
981
|
+
this.navFlowComponentRef.destroy();
|
|
982
|
+
this.navFlowComponentRef = null;
|
|
983
|
+
}
|
|
984
|
+
if (this.navFlowElement && this.navFlowElement.parentNode) {
|
|
985
|
+
this.navFlowElement.parentNode.removeChild(this.navFlowElement);
|
|
986
|
+
this.navFlowElement = null;
|
|
987
|
+
}
|
|
988
|
+
const step = steps[stepIndex];
|
|
989
|
+
this.currentNavFlowStep = stepIndex;
|
|
990
|
+
try {
|
|
991
|
+
const element = this.resolveTargetElement(step.path);
|
|
992
|
+
if (element instanceof HTMLElement) {
|
|
993
|
+
this.createAndShowNavFlow(element, step, stepIndex + 1, steps.length);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
catch (error) {
|
|
997
|
+
console.error(`[Wontfix] ERROR finding element for nav-flow step:`, error);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Create and show nav-flow component
|
|
1002
|
+
*/
|
|
1003
|
+
createAndShowNavFlow(element, step, currentStep, totalSteps) {
|
|
1004
|
+
const componentRef = createComponent(NavFlowComponent, {
|
|
1005
|
+
environmentInjector: this.appRef.injector,
|
|
1006
|
+
});
|
|
1007
|
+
componentRef.instance.step = step;
|
|
1008
|
+
componentRef.instance.currentStep = currentStep;
|
|
1009
|
+
componentRef.instance.totalSteps = totalSteps;
|
|
1010
|
+
this.appRef.attachView(componentRef.hostView);
|
|
1011
|
+
const navFlowElement = componentRef.location.nativeElement;
|
|
1012
|
+
document.body.appendChild(navFlowElement);
|
|
1013
|
+
this.navFlowElement = navFlowElement;
|
|
1014
|
+
this.navFlowComponentRef = componentRef;
|
|
1015
|
+
// Position nav-flow
|
|
1016
|
+
this.positionNavFlow(element, step, navFlowElement, componentRef);
|
|
1017
|
+
// Subscribe to events after setting refs
|
|
1018
|
+
componentRef.instance.next.subscribe(() => {
|
|
1019
|
+
this.nextNavFlowStep();
|
|
1020
|
+
});
|
|
1021
|
+
componentRef.instance.previous.subscribe(() => {
|
|
1022
|
+
this.previousNavFlowStep();
|
|
1023
|
+
});
|
|
1024
|
+
componentRef.instance.close.subscribe(() => {
|
|
1025
|
+
this.closeNavFlow();
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Position nav-flow relative to element
|
|
1030
|
+
*/
|
|
1031
|
+
positionNavFlow(element, step, navFlowElement, componentRef) {
|
|
1032
|
+
const rect = element.getBoundingClientRect();
|
|
1033
|
+
const navFlowRect = navFlowElement.getBoundingClientRect();
|
|
1034
|
+
let top = 0;
|
|
1035
|
+
let left = 0;
|
|
1036
|
+
const offset = 8;
|
|
1037
|
+
// Use actual dimensions or fallback if not rendered yet
|
|
1038
|
+
const navFlowWidth = navFlowRect.width || 350;
|
|
1039
|
+
const navFlowHeight = navFlowRect.height || 200; // Approximate height
|
|
1040
|
+
switch (step.position) {
|
|
1041
|
+
case 'top':
|
|
1042
|
+
top = rect.top - navFlowHeight - offset;
|
|
1043
|
+
left = rect.left + rect.width / 2 - navFlowWidth / 2;
|
|
1044
|
+
break;
|
|
1045
|
+
case 'bottom':
|
|
1046
|
+
top = rect.bottom + offset;
|
|
1047
|
+
left = rect.left + rect.width / 2 - navFlowWidth / 2;
|
|
1048
|
+
break;
|
|
1049
|
+
case 'left':
|
|
1050
|
+
top = rect.top + rect.height / 2 - navFlowHeight / 2;
|
|
1051
|
+
left = rect.left - navFlowWidth - offset;
|
|
1052
|
+
break;
|
|
1053
|
+
case 'right':
|
|
1054
|
+
top = rect.top + rect.height / 2 - navFlowHeight / 2;
|
|
1055
|
+
left = rect.right + offset;
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
componentRef.instance.top = top;
|
|
1059
|
+
componentRef.instance.left = left;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* Go to next nav-flow step
|
|
1063
|
+
*/
|
|
1064
|
+
nextNavFlowStep() {
|
|
1065
|
+
this.showNavFlowStep(this.currentNavFlowStep + 1);
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Go to previous nav-flow step
|
|
1069
|
+
*/
|
|
1070
|
+
previousNavFlowStep() {
|
|
1071
|
+
this.showNavFlowStep(this.currentNavFlowStep - 1);
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Close nav-flow
|
|
1075
|
+
*/
|
|
1076
|
+
closeNavFlow() {
|
|
1077
|
+
if (this.navFlowComponentRef) {
|
|
1078
|
+
this.appRef.detachView(this.navFlowComponentRef.hostView);
|
|
1079
|
+
this.navFlowComponentRef.destroy();
|
|
1080
|
+
this.navFlowComponentRef = null;
|
|
1081
|
+
}
|
|
1082
|
+
if (this.navFlowElement && this.navFlowElement.parentNode) {
|
|
1083
|
+
this.navFlowElement.parentNode.removeChild(this.navFlowElement);
|
|
1084
|
+
this.navFlowElement = null;
|
|
1085
|
+
}
|
|
1086
|
+
this.navFlowData = null;
|
|
1087
|
+
this.currentNavFlowStep = 0;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Check if current route should show a popup
|
|
1091
|
+
*/
|
|
1092
|
+
checkPopups() {
|
|
1093
|
+
const currentUrl = window.location.pathname;
|
|
1094
|
+
const popup = this.guidanceCollections.find(item => item?.type === 'popup' && this.urlMatches(item.url, currentUrl));
|
|
1095
|
+
if (popup && this.config?.debug) {
|
|
1096
|
+
console.log('[Wontfix] Popup found for route:', currentUrl);
|
|
1097
|
+
// Popup will be handled by the PopupComponent on that route
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Get popup data for current route
|
|
1102
|
+
*/
|
|
1103
|
+
getPopupForRoute(route) {
|
|
1104
|
+
const popup = this.guidanceCollections.find(item => item?.type === 'popup' && this.urlMatches(item.url, route));
|
|
1105
|
+
return popup;
|
|
1106
|
+
}
|
|
1107
|
+
setupRouteListener() {
|
|
1108
|
+
if (this.routeSubscription || this.windowRouteListenersAttached) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (this.router) {
|
|
1112
|
+
this.routeSubscription = this.router.events.subscribe((event) => {
|
|
1113
|
+
if (event instanceof NavigationEnd) {
|
|
1114
|
+
this.onRouteChanged();
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
window.addEventListener('popstate', this.onRouteChanged);
|
|
1120
|
+
window.addEventListener('hashchange', this.onRouteChanged);
|
|
1121
|
+
this.windowRouteListenersAttached = true;
|
|
1122
|
+
}
|
|
1123
|
+
onRouteChanged = async () => {
|
|
1124
|
+
try {
|
|
1125
|
+
await firstValueFrom(this.appRef.isStable.pipe(filter(stable => stable === true), take(1)));
|
|
1126
|
+
this.reinitializeTooltips();
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
if (this.config?.debug) {
|
|
1130
|
+
console.warn('[Wontfix] Route-change reinit failed:', error);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
async fetchGuidanceCollections() {
|
|
1135
|
+
const fallbackToMock = this.config?.fallbackToMock ?? true;
|
|
1136
|
+
try {
|
|
1137
|
+
if (!this.config?.apiUrl) {
|
|
1138
|
+
return fallbackToMock ? this.normalizeGuidanceCollections(MOCK_TOOLTIPS) : [];
|
|
1139
|
+
}
|
|
1140
|
+
const hasPayload = this.config.payload !== undefined;
|
|
1141
|
+
const response = await fetch(this.config.apiUrl, {
|
|
1142
|
+
method: hasPayload ? 'POST' : 'GET',
|
|
1143
|
+
headers: {
|
|
1144
|
+
'Content-Type': 'application/json',
|
|
1145
|
+
},
|
|
1146
|
+
body: hasPayload ? JSON.stringify(this.config.payload) : undefined,
|
|
1147
|
+
});
|
|
1148
|
+
if (!response.ok) {
|
|
1149
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1150
|
+
}
|
|
1151
|
+
const json = await response.json();
|
|
1152
|
+
const raw = Array.isArray(json)
|
|
1153
|
+
? json
|
|
1154
|
+
: Array.isArray(json?.data)
|
|
1155
|
+
? json.data
|
|
1156
|
+
: Array.isArray(json?.items)
|
|
1157
|
+
? json.items
|
|
1158
|
+
: [];
|
|
1159
|
+
return this.normalizeGuidanceCollections(raw);
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
console.error('[Wontfix] Failed to fetch guidance elements:', error);
|
|
1163
|
+
return fallbackToMock ? this.normalizeGuidanceCollections(MOCK_TOOLTIPS) : [];
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
normalizeGuidanceCollections(raw) {
|
|
1167
|
+
if (!Array.isArray(raw))
|
|
1168
|
+
return [];
|
|
1169
|
+
const result = [];
|
|
1170
|
+
raw.forEach((item, index) => {
|
|
1171
|
+
if (item && typeof item === 'object' && typeof item.type === 'string' && typeof item.url === 'string') {
|
|
1172
|
+
if (item.isActive === false)
|
|
1173
|
+
return;
|
|
1174
|
+
if (Array.isArray(item.elements)) {
|
|
1175
|
+
item.elements = item.elements.filter((el) => el?.isActive !== false);
|
|
1176
|
+
}
|
|
1177
|
+
result.push(item);
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
if (item && typeof item === 'object' && typeof item.templateId === 'number') {
|
|
1181
|
+
const templateId = item.templateId;
|
|
1182
|
+
const type = templateId === 2 ? 'tooltip' :
|
|
1183
|
+
templateId === 3 ? 'beacon' :
|
|
1184
|
+
templateId === 1 ? 'product-tour' :
|
|
1185
|
+
templateId === 4 ? 'nav-flow' :
|
|
1186
|
+
templateId === 5 ? 'popup' :
|
|
1187
|
+
'unknown';
|
|
1188
|
+
result.push({
|
|
1189
|
+
id: item.id ?? `legacy-${templateId}-${index}`,
|
|
1190
|
+
type,
|
|
1191
|
+
url: item.pageUrl ?? '*',
|
|
1192
|
+
elements: [
|
|
1193
|
+
{
|
|
1194
|
+
id: item.elements?.[0]?.id ?? `legacy-el-${index}`,
|
|
1195
|
+
selector: item.path ?? item.selector,
|
|
1196
|
+
position: item.metadata?.position,
|
|
1197
|
+
interaction: item.metadata?.interaction,
|
|
1198
|
+
content: item.metadata?.text,
|
|
1199
|
+
order: item.priority ?? item.order ?? item.elements?.[0]?.order,
|
|
1200
|
+
},
|
|
1201
|
+
],
|
|
1202
|
+
metadata: item.metadata,
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
return result;
|
|
1207
|
+
}
|
|
1208
|
+
resolveTargetElement(selectorOrPath) {
|
|
1209
|
+
if (!selectorOrPath)
|
|
1210
|
+
return null;
|
|
1211
|
+
if (selectorOrPath.includes('/')) {
|
|
1212
|
+
return PathMatcher.findElementByPath(selectorOrPath);
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const element = document.querySelector(selectorOrPath);
|
|
1216
|
+
return element instanceof HTMLElement ? element : null;
|
|
1217
|
+
}
|
|
1218
|
+
catch {
|
|
1219
|
+
return PathMatcher.findElementByPath(selectorOrPath);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
normalizeUrlToPathPattern(value) {
|
|
1223
|
+
if (!value)
|
|
1224
|
+
return '';
|
|
1225
|
+
if (value === '*')
|
|
1226
|
+
return '*';
|
|
1227
|
+
if (value.startsWith('/')) {
|
|
1228
|
+
return value;
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
const parsed = new URL(value);
|
|
1232
|
+
return parsed.pathname || '/';
|
|
1233
|
+
}
|
|
1234
|
+
catch {
|
|
1235
|
+
return value;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Close popup
|
|
1240
|
+
*/
|
|
1241
|
+
closePopup() {
|
|
1242
|
+
this.popupOpen = false;
|
|
1243
|
+
if (this.popupComponentRef) {
|
|
1244
|
+
this.appRef.detachView(this.popupComponentRef.hostView);
|
|
1245
|
+
this.popupComponentRef.destroy();
|
|
1246
|
+
this.popupComponentRef = null;
|
|
1247
|
+
}
|
|
1248
|
+
if (this.popupElement && this.popupElement.parentNode) {
|
|
1249
|
+
this.popupElement.parentNode.removeChild(this.popupElement);
|
|
1250
|
+
this.popupElement = null;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Check if popup is open
|
|
1255
|
+
*/
|
|
1256
|
+
isPopupOpen() {
|
|
1257
|
+
return this.popupOpen;
|
|
1258
|
+
}
|
|
1259
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TooltipService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1260
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TooltipService, providedIn: 'root' });
|
|
1261
|
+
}
|
|
1262
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: TooltipService, decorators: [{
|
|
1263
|
+
type: Injectable,
|
|
1264
|
+
args: [{
|
|
1265
|
+
providedIn: 'root'
|
|
1266
|
+
}]
|
|
1267
|
+
}] });
|
|
1268
|
+
|
|
1269
|
+
/*
|
|
1270
|
+
* Tooltips are fetched once on app load.
|
|
1271
|
+
* Updates are only available on next app reload.
|
|
1272
|
+
*/
|
|
1273
|
+
function wontfixInitializer(config) {
|
|
1274
|
+
return () => {
|
|
1275
|
+
const tooltipService = inject(TooltipService);
|
|
1276
|
+
return tooltipService.initializeTooltips(config);
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Wontfix Tooltip SDK - Public API
|
|
1282
|
+
*
|
|
1283
|
+
* This is the main entry point for applications using the Wontfix SDK.
|
|
1284
|
+
* Export only the public-facing APIs that consumers should use.
|
|
1285
|
+
*/
|
|
3
1286
|
|
|
4
1287
|
class WontfixSdk {
|
|
5
1288
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: WontfixSdk, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
@@ -21,10 +1304,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
|
|
|
21
1304
|
/*
|
|
22
1305
|
* Public API Surface of wontfix-sdk
|
|
23
1306
|
*/
|
|
1307
|
+
// Main SDK surface (service + initializer + types)
|
|
24
1308
|
|
|
25
1309
|
/**
|
|
26
1310
|
* Generated bundle index. Do not edit.
|
|
27
1311
|
*/
|
|
28
1312
|
|
|
29
|
-
export { WontfixSdk };
|
|
1313
|
+
export { TooltipService, WontfixSdk, wontfixInitializer };
|
|
30
1314
|
//# sourceMappingURL=wontfix-sdk.mjs.map
|