devx-web-widget 1.0.0 → 1.1.0
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/README.md +30 -91
- package/dist/feedback-widget.js +309 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +737 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +6 -0
- package/package.json +1 -1
- package/src/feedback-widget.js +0 -496
package/dist/index.js
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Feedback Capture Widget - TypeScript Implementation
|
|
3
|
+
* Class-based, configurable widget with style isolation via Shadow DOM
|
|
4
|
+
* Supports multiple widget types (default, chatbot) and positioning (left, right)
|
|
5
|
+
*/
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Default Configuration
|
|
8
|
+
// ============================================================================
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
buttonLabel: 'Feedback',
|
|
11
|
+
backgroundColor: '#111827',
|
|
12
|
+
textColor: '#ffffff',
|
|
13
|
+
accentColor: '#2F6FED',
|
|
14
|
+
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
15
|
+
position: 'right',
|
|
16
|
+
widgetType: 'default'
|
|
17
|
+
};
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// FeedbackWidget Class
|
|
20
|
+
// ============================================================================
|
|
21
|
+
class FeedbackWidget {
|
|
22
|
+
config;
|
|
23
|
+
host;
|
|
24
|
+
root;
|
|
25
|
+
elements;
|
|
26
|
+
state;
|
|
27
|
+
showItemInForm;
|
|
28
|
+
selectedSelector;
|
|
29
|
+
selectedEl;
|
|
30
|
+
hoverRafId;
|
|
31
|
+
hostId;
|
|
32
|
+
apiKey;
|
|
33
|
+
endPoint;
|
|
34
|
+
constructor(apiKey = null, config = {}) {
|
|
35
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
36
|
+
this.hostId = `feedback-widget-host-${Math.random().toString(36).substr(2, 9)}`;
|
|
37
|
+
this.state = 'idle';
|
|
38
|
+
this.showItemInForm = true;
|
|
39
|
+
this.selectedSelector = null;
|
|
40
|
+
this.selectedEl = null;
|
|
41
|
+
this.hoverRafId = null;
|
|
42
|
+
this.apiKey = apiKey || '';
|
|
43
|
+
this.endPoint = "https://devx.today/v1/widget/ingest";
|
|
44
|
+
this.initialize();
|
|
45
|
+
}
|
|
46
|
+
initialize() {
|
|
47
|
+
this.createHostAndShadow();
|
|
48
|
+
this.injectStyles();
|
|
49
|
+
this.createMarkup();
|
|
50
|
+
this.cacheElements();
|
|
51
|
+
this.attachEventListeners();
|
|
52
|
+
}
|
|
53
|
+
// -------------------------------------------------------------------------
|
|
54
|
+
// Shadow DOM Setup
|
|
55
|
+
// -------------------------------------------------------------------------
|
|
56
|
+
createHostAndShadow() {
|
|
57
|
+
this.host = document.createElement('div');
|
|
58
|
+
this.host.id = this.hostId;
|
|
59
|
+
document.documentElement.appendChild(this.host);
|
|
60
|
+
this.root = this.host.attachShadow({ mode: 'open' });
|
|
61
|
+
}
|
|
62
|
+
// -------------------------------------------------------------------------
|
|
63
|
+
// CSS Generation (Dynamic based on config)
|
|
64
|
+
// -------------------------------------------------------------------------
|
|
65
|
+
injectStyles() {
|
|
66
|
+
const css = this.generateCSS();
|
|
67
|
+
const styleEl = document.createElement('style');
|
|
68
|
+
styleEl.textContent = css;
|
|
69
|
+
this.root.appendChild(styleEl);
|
|
70
|
+
}
|
|
71
|
+
generateCSS() {
|
|
72
|
+
const { backgroundColor, textColor, accentColor, font, position } = this.config;
|
|
73
|
+
const accentRgba = this.hexToRgba(accentColor, 0.22);
|
|
74
|
+
const accentFocusRgba = this.hexToRgba(accentColor, 0.15);
|
|
75
|
+
// Position-specific styles
|
|
76
|
+
const tabTransform = position === 'right'
|
|
77
|
+
? 'translateY(-50%) rotate(180deg)'
|
|
78
|
+
: 'translateY(-50%)';
|
|
79
|
+
const tabBorderRadius = position === 'right'
|
|
80
|
+
? '8px 0 0 8px'
|
|
81
|
+
: '0 8px 8px 0';
|
|
82
|
+
const tabRight = position === 'right' ? '0' : 'auto';
|
|
83
|
+
const tabLeft = position === 'left' ? '0' : 'auto';
|
|
84
|
+
const menuRight = position === 'right' ? '46px' : 'auto';
|
|
85
|
+
const menuLeft = position === 'left' ? '46px' : 'auto';
|
|
86
|
+
return `
|
|
87
|
+
:host { all: initial; }
|
|
88
|
+
* { box-sizing: border-box; font-family: ${font}; }
|
|
89
|
+
|
|
90
|
+
.fbw-tab {
|
|
91
|
+
position: fixed;
|
|
92
|
+
top: 50%;
|
|
93
|
+
${position === 'right' ? 'right' : 'left'}: 0;
|
|
94
|
+
transform: ${tabTransform};
|
|
95
|
+
background: ${backgroundColor};
|
|
96
|
+
color: ${textColor};
|
|
97
|
+
writing-mode: vertical-rl;
|
|
98
|
+
text-orientation: mixed;
|
|
99
|
+
padding: 14px 9px;
|
|
100
|
+
border-radius: ${tabBorderRadius};
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-size: 13px;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
letter-spacing: 0.03em;
|
|
105
|
+
box-shadow: ${position === 'right' ? '-2px' : '2px'} 0 10px rgba(0,0,0,0.18);
|
|
106
|
+
z-index: 2147483000;
|
|
107
|
+
border: none;
|
|
108
|
+
user-select: none;
|
|
109
|
+
transition: padding 0.12s ease, background 0.12s ease;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.fbw-tab:hover { padding-right: 13px; background: #1f2937; }
|
|
113
|
+
.fbw-tab:focus-visible { outline: 2px solid ${accentColor}; outline-offset: 2px; }
|
|
114
|
+
|
|
115
|
+
.fbw-menu {
|
|
116
|
+
position: fixed;
|
|
117
|
+
top: 50%;
|
|
118
|
+
right: ${menuRight};
|
|
119
|
+
left: ${menuLeft};
|
|
120
|
+
transform: translateY(-50%);
|
|
121
|
+
background: #fff;
|
|
122
|
+
border-radius: 10px;
|
|
123
|
+
box-shadow: 0 8px 30px rgba(0,0,0,0.22);
|
|
124
|
+
padding: 6px;
|
|
125
|
+
min-width: 220px;
|
|
126
|
+
z-index: 2147483001;
|
|
127
|
+
display: none;
|
|
128
|
+
border: 1px solid #e5e7eb;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.fbw-menu.fbw-open { display: block; }
|
|
132
|
+
|
|
133
|
+
.fbw-menu button {
|
|
134
|
+
display: block;
|
|
135
|
+
width: 100%;
|
|
136
|
+
text-align: left;
|
|
137
|
+
background: none;
|
|
138
|
+
border: none;
|
|
139
|
+
padding: 10px 12px;
|
|
140
|
+
font-size: 13.5px;
|
|
141
|
+
color: #111827;
|
|
142
|
+
border-radius: 6px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.fbw-menu button:hover { background: #f3f4f6; }
|
|
147
|
+
.fbw-menu button small { display: block; color: #6b7280; font-weight: 400; margin-top: 2px; font-size: 11.5px; }
|
|
148
|
+
|
|
149
|
+
.fbw-banner {
|
|
150
|
+
position: fixed;
|
|
151
|
+
top: 18px;
|
|
152
|
+
left: 50%;
|
|
153
|
+
transform: translateX(-50%);
|
|
154
|
+
background: ${backgroundColor};
|
|
155
|
+
color: ${textColor};
|
|
156
|
+
padding: 10px 10px 10px 16px;
|
|
157
|
+
border-radius: 999px;
|
|
158
|
+
font-size: 13px;
|
|
159
|
+
display: none;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 10px;
|
|
162
|
+
z-index: 2147483002;
|
|
163
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.fbw-banner.fbw-open { display: flex; }
|
|
167
|
+
|
|
168
|
+
.fbw-banner button {
|
|
169
|
+
background: rgba(255,255,255,0.12);
|
|
170
|
+
color: ${textColor};
|
|
171
|
+
border: none;
|
|
172
|
+
padding: 6px 12px;
|
|
173
|
+
border-radius: 999px;
|
|
174
|
+
font-size: 12px;
|
|
175
|
+
cursor: pointer;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.fbw-banner button:hover { background: rgba(255,255,255,0.22); }
|
|
179
|
+
|
|
180
|
+
.fbw-highlight {
|
|
181
|
+
position: fixed;
|
|
182
|
+
pointer-events: none;
|
|
183
|
+
border: 2px solid ${accentColor};
|
|
184
|
+
box-shadow: 0 0 0 4px ${accentRgba};
|
|
185
|
+
border-radius: 4px;
|
|
186
|
+
z-index: 2147483000;
|
|
187
|
+
display: none;
|
|
188
|
+
transition: top 0.06s ease, left 0.06s ease, width 0.06s ease, height 0.06s ease;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.fbw-overlay {
|
|
192
|
+
position: fixed;
|
|
193
|
+
inset: 0;
|
|
194
|
+
background: rgba(17,24,39,0.5);
|
|
195
|
+
display: none;
|
|
196
|
+
align-items: center;
|
|
197
|
+
justify-content: center;
|
|
198
|
+
z-index: 2147483003;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.fbw-overlay.fbw-open { display: flex; }
|
|
202
|
+
|
|
203
|
+
.fbw-modal {
|
|
204
|
+
background: #fff;
|
|
205
|
+
border-radius: 14px;
|
|
206
|
+
width: 420px;
|
|
207
|
+
max-width: 92vw;
|
|
208
|
+
max-height: 88vh;
|
|
209
|
+
overflow-y: auto;
|
|
210
|
+
padding: 22px;
|
|
211
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.fbw-modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
|
215
|
+
.fbw-modal-head h2 { font-size: 16px; margin: 0; color: #111827; font-weight: 700; }
|
|
216
|
+
.fbw-close { background: none; border: none; font-size: 18px; color: #6b7280; cursor: pointer; line-height: 1; padding: 4px; }
|
|
217
|
+
.fbw-close:hover { color: #111827; }
|
|
218
|
+
|
|
219
|
+
.fbw-field { margin-bottom: 14px; }
|
|
220
|
+
.fbw-field label { display: block; font-size: 12.5px; font-weight: 600; color: #374151; margin-bottom: 5px; }
|
|
221
|
+
|
|
222
|
+
.fbw-item-box {
|
|
223
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
224
|
+
font-size: 11.5px;
|
|
225
|
+
color: #1f2937;
|
|
226
|
+
background: #f3f4f6;
|
|
227
|
+
border: 1px solid #e5e7eb;
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
padding: 8px 10px;
|
|
230
|
+
word-break: break-all;
|
|
231
|
+
max-height: 60px;
|
|
232
|
+
overflow-y: auto;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.fbw-field textarea, .fbw-field input[type="email"], .fbw-field input[type="text"] {
|
|
236
|
+
width: 100%;
|
|
237
|
+
border: 1px solid #d1d5db;
|
|
238
|
+
border-radius: 8px;
|
|
239
|
+
padding: 9px 10px;
|
|
240
|
+
font-size: 13.5px;
|
|
241
|
+
color: #111827;
|
|
242
|
+
resize: vertical;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.fbw-field textarea:focus, .fbw-field input:focus {
|
|
246
|
+
outline: none;
|
|
247
|
+
border-color: ${accentColor};
|
|
248
|
+
box-shadow: 0 0 0 3px ${accentFocusRgba};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.fbw-error { color: #dc2626; font-size: 12px; margin: -6px 0 12px; display: none; }
|
|
252
|
+
.fbw-error.fbw-show { display: block; }
|
|
253
|
+
|
|
254
|
+
.fbw-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
|
|
255
|
+
|
|
256
|
+
.fbw-btn {
|
|
257
|
+
border: none;
|
|
258
|
+
border-radius: 8px;
|
|
259
|
+
padding: 9px 16px;
|
|
260
|
+
font-size: 13.5px;
|
|
261
|
+
font-weight: 600;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.fbw-btn-primary { background: ${accentColor}; color: #fff; }
|
|
266
|
+
.fbw-btn-primary:hover { filter: brightness(1.08); }
|
|
267
|
+
.fbw-btn-ghost { background: none; color: #4b5563; }
|
|
268
|
+
.fbw-btn-ghost:hover { background: #f3f4f6; }
|
|
269
|
+
|
|
270
|
+
.fbw-toast {
|
|
271
|
+
position: fixed;
|
|
272
|
+
bottom: 22px;
|
|
273
|
+
right: 22px;
|
|
274
|
+
background: ${backgroundColor};
|
|
275
|
+
color: ${textColor};
|
|
276
|
+
padding: 11px 16px;
|
|
277
|
+
border-radius: 10px;
|
|
278
|
+
font-size: 13px;
|
|
279
|
+
z-index: 2147483004;
|
|
280
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
|
281
|
+
opacity: 0;
|
|
282
|
+
transform: translateY(6px);
|
|
283
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
284
|
+
pointer-events: none;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.fbw-toast.fbw-show { opacity: 1; transform: translateY(0); }
|
|
288
|
+
.fbw-hidden { display: none !important; }
|
|
289
|
+
`;
|
|
290
|
+
}
|
|
291
|
+
hexToRgba(hex, alpha) {
|
|
292
|
+
const h = hex.replace('#', '');
|
|
293
|
+
const expanded = h.length === 3
|
|
294
|
+
? h.split('').map((c) => c + c).join('')
|
|
295
|
+
: h;
|
|
296
|
+
const r = parseInt(expanded.substring(0, 2), 16);
|
|
297
|
+
const g = parseInt(expanded.substring(2, 4), 16);
|
|
298
|
+
const b = parseInt(expanded.substring(4, 6), 16);
|
|
299
|
+
return `rgba(${r},${g},${b},${alpha})`;
|
|
300
|
+
}
|
|
301
|
+
// -------------------------------------------------------------------------
|
|
302
|
+
// Markup Creation
|
|
303
|
+
// -------------------------------------------------------------------------
|
|
304
|
+
createMarkup() {
|
|
305
|
+
const wrap = document.createElement('div');
|
|
306
|
+
wrap.innerHTML = this.getMarkup();
|
|
307
|
+
this.root.appendChild(wrap);
|
|
308
|
+
}
|
|
309
|
+
getMarkup() {
|
|
310
|
+
const { buttonLabel, widgetType } = this.config;
|
|
311
|
+
if (widgetType === 'chatbot') {
|
|
312
|
+
return this.getChatbotMarkup(buttonLabel);
|
|
313
|
+
}
|
|
314
|
+
return this.getDefaultMarkup(buttonLabel);
|
|
315
|
+
}
|
|
316
|
+
getDefaultMarkup(buttonLabel) {
|
|
317
|
+
return `
|
|
318
|
+
<button class="fbw-tab" type="button" aria-haspopup="true" aria-expanded="false">${this.escapeHtml(buttonLabel)}</button>
|
|
319
|
+
<div class="fbw-menu" role="menu">
|
|
320
|
+
<button type="button" data-action="pick-visible" role="menuitem">
|
|
321
|
+
Feedback on this page<small>Select any element you have feedback on</small>
|
|
322
|
+
</button>
|
|
323
|
+
<button type="button" data-action="pick-hidden" role="menuitem">
|
|
324
|
+
Quick feedback<small>General feedback on us</small>
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="fbw-banner">
|
|
328
|
+
<span>Click an element to select it · Esc to cancel</span>
|
|
329
|
+
<button type="button" data-action="cancel-pick">Cancel</button>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="fbw-highlight"></div>
|
|
332
|
+
<div class="fbw-overlay">
|
|
333
|
+
<div class="fbw-modal" role="dialog" aria-modal="true" aria-labelledby="fbw-modal-title">
|
|
334
|
+
<div class="fbw-modal-head">
|
|
335
|
+
<h2 id="fbw-modal-title">Share feedback</h2>
|
|
336
|
+
<button class="fbw-close" type="button" data-action="close-modal" aria-label="Close">×</button>
|
|
337
|
+
</div>
|
|
338
|
+
<form data-role="form">
|
|
339
|
+
<div class="fbw-field" data-role="item-field">
|
|
340
|
+
<label>Selected element</label>
|
|
341
|
+
<div class="fbw-item-box" data-role="item-box"></div>
|
|
342
|
+
</div>
|
|
343
|
+
<div class="fbw-field">
|
|
344
|
+
<label for="fbw-title">Title</label>
|
|
345
|
+
<input id="fbw-title" data-role="title" type="text" placeholder="Brief summary of your feedback" required />
|
|
346
|
+
</div>
|
|
347
|
+
<div class="fbw-field">
|
|
348
|
+
<label for="fbw-feedback">What's going on?</label>
|
|
349
|
+
<textarea id="fbw-feedback" data-role="feedback" rows="4" placeholder="Tell us what you noticed..."></textarea>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="fbw-field">
|
|
352
|
+
<label for="fbw-email">Email</label>
|
|
353
|
+
<input id="fbw-email" data-role="email" type="email" placeholder="you@example.com" required />
|
|
354
|
+
</div>
|
|
355
|
+
<div class="fbw-error" data-role="error"></div>
|
|
356
|
+
<div class="fbw-actions">
|
|
357
|
+
<button type="button" class="fbw-btn fbw-btn-ghost" data-action="close-modal">Cancel</button>
|
|
358
|
+
<button type="submit" class="fbw-btn fbw-btn-primary">Submit</button>
|
|
359
|
+
</div>
|
|
360
|
+
</form>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
<div class="fbw-toast" data-role="toast"></div>
|
|
364
|
+
`;
|
|
365
|
+
}
|
|
366
|
+
getChatbotMarkup(buttonLabel) {
|
|
367
|
+
// Placeholder for chatbot interface - can be expanded later
|
|
368
|
+
return `
|
|
369
|
+
<button class="fbw-tab" type="button" aria-haspopup="true" aria-expanded="false">${this.escapeHtml(buttonLabel)}</button>
|
|
370
|
+
<div class="fbw-menu" role="menu">
|
|
371
|
+
<button type="button" data-action="pick-visible" role="menuitem">
|
|
372
|
+
Feedback on this page<small>Select any element you have feedback on</small>
|
|
373
|
+
</button>
|
|
374
|
+
<button type="button" data-action="pick-hidden" role="menuitem">
|
|
375
|
+
Quick feedback<small>General feedback on us</small>
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
378
|
+
<div class="fbw-banner">
|
|
379
|
+
<span>Click an element to select it · Esc to cancel</span>
|
|
380
|
+
<button type="button" data-action="cancel-pick">Cancel</button>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="fbw-highlight"></div>
|
|
383
|
+
<div class="fbw-overlay">
|
|
384
|
+
<div class="fbw-modal" role="dialog" aria-modal="true" aria-labelledby="fbw-modal-title">
|
|
385
|
+
<div class="fbw-modal-head">
|
|
386
|
+
<h2 id="fbw-modal-title">Share feedback</h2>
|
|
387
|
+
<button class="fbw-close" type="button" data-action="close-modal" aria-label="Close">×</button>
|
|
388
|
+
</div>
|
|
389
|
+
<form data-role="form">
|
|
390
|
+
<div class="fbw-field" data-role="item-field">
|
|
391
|
+
<label>Selected element</label>
|
|
392
|
+
<div class="fbw-item-box" data-role="item-box"></div>
|
|
393
|
+
</div>
|
|
394
|
+
<div class="fbw-field">
|
|
395
|
+
<label for="fbw-title">Title</label>
|
|
396
|
+
<input id="fbw-title" data-role="title" type="text" placeholder="Brief summary of your feedback" required />
|
|
397
|
+
</div>
|
|
398
|
+
<div class="fbw-field">
|
|
399
|
+
<label for="fbw-feedback">What's going on?</label>
|
|
400
|
+
<textarea id="fbw-feedback" data-role="feedback" rows="4" placeholder="Tell us what you noticed..."></textarea>
|
|
401
|
+
</div>
|
|
402
|
+
<div class="fbw-field">
|
|
403
|
+
<label for="fbw-email">Email</label>
|
|
404
|
+
<input id="fbw-email" data-role="email" type="email" placeholder="you@example.com" required />
|
|
405
|
+
</div>
|
|
406
|
+
<div class="fbw-error" data-role="error"></div>
|
|
407
|
+
<div class="fbw-actions">
|
|
408
|
+
<button type="button" class="fbw-btn fbw-btn-ghost" data-action="close-modal">Cancel</button>
|
|
409
|
+
<button type="submit" class="fbw-btn fbw-btn-primary">Submit</button>
|
|
410
|
+
</div>
|
|
411
|
+
</form>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
<div class="fbw-toast" data-role="toast"></div>
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
escapeHtml(s) {
|
|
418
|
+
const d = document.createElement('div');
|
|
419
|
+
d.textContent = s;
|
|
420
|
+
return d.innerHTML;
|
|
421
|
+
}
|
|
422
|
+
// -------------------------------------------------------------------------
|
|
423
|
+
// Element Caching
|
|
424
|
+
// -------------------------------------------------------------------------
|
|
425
|
+
cacheElements() {
|
|
426
|
+
this.elements = {
|
|
427
|
+
tab: this.root.querySelector('.fbw-tab'),
|
|
428
|
+
menu: this.root.querySelector('.fbw-menu'),
|
|
429
|
+
banner: this.root.querySelector('.fbw-banner'),
|
|
430
|
+
highlight: this.root.querySelector('.fbw-highlight'),
|
|
431
|
+
overlay: this.root.querySelector('.fbw-overlay'),
|
|
432
|
+
modal: this.root.querySelector('.fbw-modal'),
|
|
433
|
+
form: this.root.querySelector('[data-role="form"]'),
|
|
434
|
+
itemField: this.root.querySelector('[data-role="item-field"]'),
|
|
435
|
+
itemBox: this.root.querySelector('[data-role="item-box"]'),
|
|
436
|
+
title: this.root.querySelector('[data-role="title"]'),
|
|
437
|
+
feedback: this.root.querySelector('[data-role="feedback"]'),
|
|
438
|
+
email: this.root.querySelector('[data-role="email"]'),
|
|
439
|
+
error: this.root.querySelector('[data-role="error"]'),
|
|
440
|
+
toast: this.root.querySelector('[data-role="toast"]')
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
// -------------------------------------------------------------------------
|
|
444
|
+
// Event Listeners
|
|
445
|
+
// -------------------------------------------------------------------------
|
|
446
|
+
attachEventListeners() {
|
|
447
|
+
this.elements.tab.addEventListener('click', () => this.onTabClick());
|
|
448
|
+
this.elements.menu.addEventListener('click', (e) => this.onMenuClick(e));
|
|
449
|
+
this.elements.banner.addEventListener('click', (e) => this.onBannerClick(e));
|
|
450
|
+
this.elements.overlay.addEventListener('click', (e) => this.onOverlayClick(e));
|
|
451
|
+
this.elements.form.addEventListener('submit', (e) => this.onSubmit(e));
|
|
452
|
+
}
|
|
453
|
+
// -------------------------------------------------------------------------
|
|
454
|
+
// Menu Handlers
|
|
455
|
+
// -------------------------------------------------------------------------
|
|
456
|
+
onTabClick() {
|
|
457
|
+
this.state === 'menu' ? this.closeMenu() : this.openMenu();
|
|
458
|
+
}
|
|
459
|
+
openMenu() {
|
|
460
|
+
this.state = 'menu';
|
|
461
|
+
this.elements.menu.classList.add('fbw-open');
|
|
462
|
+
this.elements.tab.setAttribute('aria-expanded', 'true');
|
|
463
|
+
document.addEventListener('click', this.onOutsideMenuClick, true);
|
|
464
|
+
}
|
|
465
|
+
closeMenu() {
|
|
466
|
+
if (this.state === 'menu')
|
|
467
|
+
this.state = 'idle';
|
|
468
|
+
this.elements.menu.classList.remove('fbw-open');
|
|
469
|
+
this.elements.tab.setAttribute('aria-expanded', 'false');
|
|
470
|
+
document.removeEventListener('click', this.onOutsideMenuClick, true);
|
|
471
|
+
}
|
|
472
|
+
onOutsideMenuClick = (e) => {
|
|
473
|
+
if (!this.isInsideWidget(e.target))
|
|
474
|
+
this.closeMenu();
|
|
475
|
+
};
|
|
476
|
+
onMenuClick(e) {
|
|
477
|
+
const btn = e.target.closest('button[data-action]');
|
|
478
|
+
if (!btn)
|
|
479
|
+
return;
|
|
480
|
+
const action = btn.getAttribute('data-action');
|
|
481
|
+
if (action === 'pick-visible')
|
|
482
|
+
this.startPicking(true);
|
|
483
|
+
if (action === 'pick-hidden') {
|
|
484
|
+
this.showItemInForm = false;
|
|
485
|
+
this.closeMenu();
|
|
486
|
+
this.openModal();
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
// Picking Mode
|
|
491
|
+
// -------------------------------------------------------------------------
|
|
492
|
+
startPicking(showItem) {
|
|
493
|
+
this.showItemInForm = showItem;
|
|
494
|
+
this.closeMenu();
|
|
495
|
+
this.state = 'picking';
|
|
496
|
+
this.elements.banner.classList.add('fbw-open');
|
|
497
|
+
document.addEventListener('mousemove', this.onHoverMove, true);
|
|
498
|
+
document.addEventListener('click', this.onDocumentClickCapture, true);
|
|
499
|
+
document.addEventListener('keydown', this.onKeyDownCapture, true);
|
|
500
|
+
document.addEventListener('scroll', this.onScrollOrResize, true);
|
|
501
|
+
window.addEventListener('resize', this.onScrollOrResize, true);
|
|
502
|
+
}
|
|
503
|
+
stopPicking() {
|
|
504
|
+
this.elements.banner.classList.remove('fbw-open');
|
|
505
|
+
document.removeEventListener('mousemove', this.onHoverMove, true);
|
|
506
|
+
document.removeEventListener('scroll', this.onScrollOrResize, true);
|
|
507
|
+
window.removeEventListener('resize', this.onScrollOrResize, true);
|
|
508
|
+
}
|
|
509
|
+
onBannerClick(e) {
|
|
510
|
+
if (e.target.closest('[data-action="cancel-pick"]')) {
|
|
511
|
+
this.cancelAll();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
onHoverMove = (e) => {
|
|
515
|
+
if (this.isInsideWidget(e.target))
|
|
516
|
+
return;
|
|
517
|
+
if (this.hoverRafId)
|
|
518
|
+
cancelAnimationFrame(this.hoverRafId);
|
|
519
|
+
this.hoverRafId = requestAnimationFrame(() => {
|
|
520
|
+
this.positionHighlight(e.target);
|
|
521
|
+
});
|
|
522
|
+
};
|
|
523
|
+
positionHighlight(target) {
|
|
524
|
+
const r = target.getBoundingClientRect();
|
|
525
|
+
this.elements.highlight.style.display = 'block';
|
|
526
|
+
this.elements.highlight.style.top = `${r.top}px`;
|
|
527
|
+
this.elements.highlight.style.left = `${r.left}px`;
|
|
528
|
+
this.elements.highlight.style.width = `${r.width}px`;
|
|
529
|
+
this.elements.highlight.style.height = `${r.height}px`;
|
|
530
|
+
}
|
|
531
|
+
onScrollOrResize = () => {
|
|
532
|
+
if (this.selectedEl && this.state === 'review') {
|
|
533
|
+
this.positionHighlight(this.selectedEl);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
onDocumentClickCapture = (e) => {
|
|
537
|
+
if (this.isInsideWidget(e.target))
|
|
538
|
+
return;
|
|
539
|
+
e.preventDefault();
|
|
540
|
+
e.stopPropagation();
|
|
541
|
+
if (typeof e.stopImmediatePropagation === 'function') {
|
|
542
|
+
e.stopImmediatePropagation();
|
|
543
|
+
}
|
|
544
|
+
if (this.state === 'picking') {
|
|
545
|
+
this.finalizeSelection(e.target);
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
onKeyDownCapture = (e) => {
|
|
549
|
+
if (e.key === 'Escape') {
|
|
550
|
+
e.preventDefault();
|
|
551
|
+
this.cancelAll();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
finalizeSelection(target) {
|
|
555
|
+
this.selectedEl = target;
|
|
556
|
+
this.selectedSelector = this.getUniqueSelector(target);
|
|
557
|
+
this.stopPicking();
|
|
558
|
+
this.positionHighlight(target);
|
|
559
|
+
this.openModal();
|
|
560
|
+
}
|
|
561
|
+
// -------------------------------------------------------------------------
|
|
562
|
+
// Unique Selector Generation
|
|
563
|
+
// -------------------------------------------------------------------------
|
|
564
|
+
getUniqueSelector(node) {
|
|
565
|
+
if (!(node instanceof Element))
|
|
566
|
+
return null;
|
|
567
|
+
if (node.id) {
|
|
568
|
+
const idSel = `#${this.cssEscape(node.id)}`;
|
|
569
|
+
if (this.safeQueryCount(idSel) === 1)
|
|
570
|
+
return idSel;
|
|
571
|
+
}
|
|
572
|
+
const parts = [];
|
|
573
|
+
let current = node;
|
|
574
|
+
while (current && current.nodeType === 1 && current !== document.documentElement) {
|
|
575
|
+
let part = current.tagName.toLowerCase();
|
|
576
|
+
if (current.id) {
|
|
577
|
+
part = `#${this.cssEscape(current.id)}`;
|
|
578
|
+
parts.unshift(part);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
const classes = Array.from(current.classList).filter((c) => !!c);
|
|
582
|
+
if (classes.length) {
|
|
583
|
+
part += '.' + classes.map(this.cssEscape).join('.');
|
|
584
|
+
}
|
|
585
|
+
const parent = current.parentElement;
|
|
586
|
+
if (parent) {
|
|
587
|
+
const sameTagSiblings = Array.from(parent.children).filter((s) => s.tagName === current.tagName);
|
|
588
|
+
if (sameTagSiblings.length > 1) {
|
|
589
|
+
const idx = sameTagSiblings.indexOf(current) + 1;
|
|
590
|
+
part += `:nth-of-type(${idx})`;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
parts.unshift(part);
|
|
594
|
+
const testSelector = parts.join(' > ');
|
|
595
|
+
if (this.safeQueryCount(testSelector) === 1)
|
|
596
|
+
return testSelector;
|
|
597
|
+
current = parent || null;
|
|
598
|
+
}
|
|
599
|
+
return parts.join(' > ');
|
|
600
|
+
}
|
|
601
|
+
cssEscape(str) {
|
|
602
|
+
if (window.CSS && window.CSS.escape) {
|
|
603
|
+
return window.CSS.escape(str);
|
|
604
|
+
}
|
|
605
|
+
return String(str).replace(/([ #.;?%&,.+*~':"!^$[\]()=>|/])/g, '\\$1');
|
|
606
|
+
}
|
|
607
|
+
safeQueryCount(selector) {
|
|
608
|
+
try {
|
|
609
|
+
return document.querySelectorAll(selector).length;
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
return -1;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
// -------------------------------------------------------------------------
|
|
616
|
+
// Modal Handlers
|
|
617
|
+
// -------------------------------------------------------------------------
|
|
618
|
+
openModal() {
|
|
619
|
+
this.state = 'review';
|
|
620
|
+
this.elements.error.classList.remove('fbw-show');
|
|
621
|
+
this.elements.form.reset();
|
|
622
|
+
if (this.showItemInForm) {
|
|
623
|
+
this.elements.itemField.classList.remove('fbw-hidden');
|
|
624
|
+
this.elements.itemBox.textContent = this.selectedSelector || '(none)';
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
this.elements.itemField.classList.add('fbw-hidden');
|
|
628
|
+
}
|
|
629
|
+
this.elements.overlay.classList.add('fbw-open');
|
|
630
|
+
setTimeout(() => this.elements.feedback.focus(), 30);
|
|
631
|
+
}
|
|
632
|
+
closeModal() {
|
|
633
|
+
this.elements.overlay.classList.remove('fbw-open');
|
|
634
|
+
}
|
|
635
|
+
cancelAll() {
|
|
636
|
+
this.stopPicking();
|
|
637
|
+
this.closeModal();
|
|
638
|
+
this.elements.highlight.style.display = 'none';
|
|
639
|
+
document.removeEventListener('click', this.onDocumentClickCapture, true);
|
|
640
|
+
document.removeEventListener('keydown', this.onKeyDownCapture, true);
|
|
641
|
+
this.selectedEl = null;
|
|
642
|
+
this.selectedSelector = null;
|
|
643
|
+
this.state = 'idle';
|
|
644
|
+
}
|
|
645
|
+
onOverlayClick(e) {
|
|
646
|
+
const actionBtn = e.target.closest('[data-action="close-modal"]');
|
|
647
|
+
if (actionBtn)
|
|
648
|
+
this.cancelAll();
|
|
649
|
+
}
|
|
650
|
+
// -------------------------------------------------------------------------
|
|
651
|
+
// Form Submission
|
|
652
|
+
// -------------------------------------------------------------------------
|
|
653
|
+
onSubmit(e) {
|
|
654
|
+
e.preventDefault();
|
|
655
|
+
const title = this.elements.title.value.trim();
|
|
656
|
+
const feedback = this.elements.feedback.value.trim();
|
|
657
|
+
const email = this.elements.email.value.trim();
|
|
658
|
+
const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
659
|
+
if (!title || !emailOk) {
|
|
660
|
+
this.elements.error.textContent = !title
|
|
661
|
+
? 'Please add a title before submitting.'
|
|
662
|
+
: 'Please enter a valid email address.';
|
|
663
|
+
this.elements.error.classList.add('fbw-show');
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const payload = {
|
|
667
|
+
optionType: this.showItemInForm ? 'page_item_visible' : 'page_item_hidden',
|
|
668
|
+
elementSelector: this.selectedSelector,
|
|
669
|
+
pageUrl: window.location.href,
|
|
670
|
+
pageTitle: document.title,
|
|
671
|
+
title,
|
|
672
|
+
feedback,
|
|
673
|
+
email,
|
|
674
|
+
submittedAt: new Date().toISOString()
|
|
675
|
+
};
|
|
676
|
+
this.submitFeedback(payload);
|
|
677
|
+
}
|
|
678
|
+
prepareMessageBody(payload) {
|
|
679
|
+
const user_handle = payload.email;
|
|
680
|
+
const feedbackTitle = payload.title;
|
|
681
|
+
const feedbackBody = payload.feedback || '(No additional details provided)';
|
|
682
|
+
const pageTitle = payload.pageTitle;
|
|
683
|
+
const message = `${feedbackBody}\n\nPage: ${pageTitle}\nURL: ${payload.pageUrl}\nSelector: ${payload.elementSelector}\nOption: ${payload.optionType}\nSubmitted: ${payload.submittedAt}`;
|
|
684
|
+
return {
|
|
685
|
+
title: feedbackTitle,
|
|
686
|
+
body: message,
|
|
687
|
+
user_handle: user_handle
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
submitFeedback(payload) {
|
|
691
|
+
window.dispatchEvent(new CustomEvent('feedbackwidget:submit', { detail: payload }));
|
|
692
|
+
if (this.apiKey) {
|
|
693
|
+
const messageBody = this.prepareMessageBody(payload);
|
|
694
|
+
fetch(this.endPoint, {
|
|
695
|
+
method: 'POST',
|
|
696
|
+
headers: {
|
|
697
|
+
'Content-Type': 'application/json',
|
|
698
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
699
|
+
},
|
|
700
|
+
body: JSON.stringify(messageBody)
|
|
701
|
+
}).catch(() => {
|
|
702
|
+
// Swallow network errors, UX already confirmed below
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
this.cancelAll();
|
|
706
|
+
this.showToast('Thanks for the feedback!');
|
|
707
|
+
}
|
|
708
|
+
showToast(message) {
|
|
709
|
+
this.elements.toast.textContent = message;
|
|
710
|
+
this.elements.toast.classList.add('fbw-show');
|
|
711
|
+
setTimeout(() => this.elements.toast.classList.remove('fbw-show'), 2400);
|
|
712
|
+
}
|
|
713
|
+
// -------------------------------------------------------------------------
|
|
714
|
+
// Helper Methods
|
|
715
|
+
// -------------------------------------------------------------------------
|
|
716
|
+
isInsideWidget(target) {
|
|
717
|
+
return target === this.host || this.host.contains(target);
|
|
718
|
+
}
|
|
719
|
+
// -------------------------------------------------------------------------
|
|
720
|
+
// Public API
|
|
721
|
+
// -------------------------------------------------------------------------
|
|
722
|
+
open(mode = 'visible') {
|
|
723
|
+
this.startPicking(mode === 'visible');
|
|
724
|
+
}
|
|
725
|
+
close() {
|
|
726
|
+
this.cancelAll();
|
|
727
|
+
}
|
|
728
|
+
destroy() {
|
|
729
|
+
this.cancelAll();
|
|
730
|
+
this.host.remove();
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// ============================================================================
|
|
734
|
+
// Export
|
|
735
|
+
// ============================================================================
|
|
736
|
+
export { FeedbackWidget };
|
|
737
|
+
//# sourceMappingURL=index.js.map
|