devx-web-widget 1.0.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 +166 -0
- package/package.json +28 -0
- package/pnpm-workspace.yaml +2 -0
- package/src/feedback-widget.js +496 -0
- package/src/index.ts +864 -0
- package/tsconfig.json +44 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Feedback Capture Widget
|
|
3
|
+
* Drop-in, dependency-free widget: a side tab that opens two feedback flows.
|
|
4
|
+
* 1. "Feedback on this page" -> pick an element, item selector shown in form
|
|
5
|
+
* 2. "Quick feedback" -> pick an element, item selector hidden in form
|
|
6
|
+
* Both flows freeze the host page (preventDefault on all outside interactions)
|
|
7
|
+
* while an element is being picked and while the modal is open.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* <script src="feedback-widget.js"></script>
|
|
11
|
+
* <script>
|
|
12
|
+
* window.FeedbackWidgetConfig = {
|
|
13
|
+
* endpoint: 'https://your-api.example.com/feedback', // optional
|
|
14
|
+
* buttonLabel: 'Feedback', // optional
|
|
15
|
+
* accentColor: '#2F6FED' // optional
|
|
16
|
+
* };
|
|
17
|
+
* </script>
|
|
18
|
+
*
|
|
19
|
+
* Listen for submissions from anywhere on the page:
|
|
20
|
+
* window.addEventListener('feedbackwidget:submit', (e) => console.log(e.detail));
|
|
21
|
+
*/
|
|
22
|
+
(function () {
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
if (window.__feedbackWidgetLoaded) return;
|
|
26
|
+
window.__feedbackWidgetLoaded = true;
|
|
27
|
+
|
|
28
|
+
var config = window.FeedbackWidgetConfig || {};
|
|
29
|
+
var ENDPOINT = config.endpoint || null;
|
|
30
|
+
var BUTTON_LABEL = config.buttonLabel || 'Feedback';
|
|
31
|
+
var ACCENT = config.accentColor || '#2F6FED';
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------
|
|
34
|
+
// Host + Shadow root (style isolation from the page we're embedded in)
|
|
35
|
+
// ---------------------------------------------------------------------
|
|
36
|
+
var host = document.createElement('div');
|
|
37
|
+
host.id = 'feedback-widget-host';
|
|
38
|
+
document.documentElement.appendChild(host);
|
|
39
|
+
var root = host.attachShadow({ mode: 'open' });
|
|
40
|
+
|
|
41
|
+
var css = ''
|
|
42
|
+
+ ':host { all: initial; }'
|
|
43
|
+
+ '* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }'
|
|
44
|
+
+ '.fbw-tab {'
|
|
45
|
+
+ ' position: fixed; top: 50%; right: 0; transform: translateY(-50%);'
|
|
46
|
+
+ ' background: #111827; color: #fff; writing-mode: vertical-rl; text-orientation: mixed;'
|
|
47
|
+
+ ' transform: translateY(-50%) rotate(180deg);'
|
|
48
|
+
+ ' padding: 14px 9px; border-radius: 8px 0 0 8px; cursor: pointer;'
|
|
49
|
+
+ ' font-size: 13px; font-weight: 600; letter-spacing: 0.03em;'
|
|
50
|
+
+ ' box-shadow: -2px 0 10px rgba(0,0,0,0.18); z-index: 2147483000;'
|
|
51
|
+
+ ' border: none; user-select: none; transition: padding 0.12s ease, background 0.12s ease;'
|
|
52
|
+
+ '}'
|
|
53
|
+
+ '.fbw-tab:hover { padding-right: 13px; background: #1f2937; }'
|
|
54
|
+
+ '.fbw-tab:focus-visible { outline: 2px solid ' + ACCENT + '; outline-offset: 2px; }'
|
|
55
|
+
+ '.fbw-menu {'
|
|
56
|
+
+ ' position: fixed; top: 50%; right: 46px; transform: translateY(-50%);'
|
|
57
|
+
+ ' background: #fff; border-radius: 10px; box-shadow: 0 8px 30px rgba(0,0,0,0.22);'
|
|
58
|
+
+ ' padding: 6px; min-width: 220px; z-index: 2147483001; display: none;'
|
|
59
|
+
+ ' border: 1px solid #e5e7eb;'
|
|
60
|
+
+ '}'
|
|
61
|
+
+ '.fbw-menu.fbw-open { display: block; }'
|
|
62
|
+
+ '.fbw-menu button {'
|
|
63
|
+
+ ' display: block; width: 100%; text-align: left; background: none; border: none;'
|
|
64
|
+
+ ' padding: 10px 12px; font-size: 13.5px; color: #111827; border-radius: 6px; cursor: pointer;'
|
|
65
|
+
+ '}'
|
|
66
|
+
+ '.fbw-menu button:hover { background: #f3f4f6; }'
|
|
67
|
+
+ '.fbw-menu button small { display: block; color: #6b7280; font-weight: 400; margin-top: 2px; font-size: 11.5px; }'
|
|
68
|
+
+ '.fbw-banner {'
|
|
69
|
+
+ ' position: fixed; top: 18px; left: 50%; transform: translateX(-50%);'
|
|
70
|
+
+ ' background: #111827; color: #fff; padding: 10px 10px 10px 16px; border-radius: 999px;'
|
|
71
|
+
+ ' font-size: 13px; display: none; align-items: center; gap: 10px; z-index: 2147483002;'
|
|
72
|
+
+ ' box-shadow: 0 8px 24px rgba(0,0,0,0.25);'
|
|
73
|
+
+ '}'
|
|
74
|
+
+ '.fbw-banner.fbw-open { display: flex; }'
|
|
75
|
+
+ '.fbw-banner button {'
|
|
76
|
+
+ ' background: rgba(255,255,255,0.12); color: #fff; border: none; padding: 6px 12px;'
|
|
77
|
+
+ ' border-radius: 999px; font-size: 12px; cursor: pointer;'
|
|
78
|
+
+ '}'
|
|
79
|
+
+ '.fbw-banner button:hover { background: rgba(255,255,255,0.22); }'
|
|
80
|
+
+ '.fbw-highlight {'
|
|
81
|
+
+ ' position: fixed; pointer-events: none; border: 2px solid ' + ACCENT + ';'
|
|
82
|
+
+ ' box-shadow: 0 0 0 4px ' + hexToRgba(ACCENT, 0.22) + '; border-radius: 4px;'
|
|
83
|
+
+ ' z-index: 2147483000; display: none; transition: top 0.06s ease, left 0.06s ease, width 0.06s ease, height 0.06s ease;'
|
|
84
|
+
+ '}'
|
|
85
|
+
+ '.fbw-overlay {'
|
|
86
|
+
+ ' position: fixed; inset: 0; background: rgba(17,24,39,0.5);'
|
|
87
|
+
+ ' display: none; align-items: center; justify-content: center; z-index: 2147483003;'
|
|
88
|
+
+ '}'
|
|
89
|
+
+ '.fbw-overlay.fbw-open { display: flex; }'
|
|
90
|
+
+ '.fbw-modal {'
|
|
91
|
+
+ ' background: #fff; border-radius: 14px; width: 420px; max-width: 92vw;'
|
|
92
|
+
+ ' max-height: 88vh; overflow-y: auto; padding: 22px; box-shadow: 0 20px 60px rgba(0,0,0,0.35);'
|
|
93
|
+
+ '}'
|
|
94
|
+
+ '.fbw-modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }'
|
|
95
|
+
+ '.fbw-modal-head h2 { font-size: 16px; margin: 0; color: #111827; font-weight: 700; }'
|
|
96
|
+
+ '.fbw-close { background: none; border: none; font-size: 18px; color: #6b7280; cursor: pointer; line-height: 1; padding: 4px; }'
|
|
97
|
+
+ '.fbw-close:hover { color: #111827; }'
|
|
98
|
+
+ '.fbw-field { margin-bottom: 14px; }'
|
|
99
|
+
+ '.fbw-field label { display: block; font-size: 12.5px; font-weight: 600; color: #374151; margin-bottom: 5px; }'
|
|
100
|
+
+ '.fbw-item-box {'
|
|
101
|
+
+ ' font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; color: #1f2937;'
|
|
102
|
+
+ ' background: #f3f4f6; border: 1px solid #e5e7eb; border-radius: 6px; padding: 8px 10px;'
|
|
103
|
+
+ ' word-break: break-all; max-height: 60px; overflow-y: auto;'
|
|
104
|
+
+ '}'
|
|
105
|
+
+ '.fbw-field textarea, .fbw-field input[type="email"] {'
|
|
106
|
+
+ ' width: 100%; border: 1px solid #d1d5db; border-radius: 8px; padding: 9px 10px; font-size: 13.5px;'
|
|
107
|
+
+ ' color: #111827; resize: vertical;'
|
|
108
|
+
+ '}'
|
|
109
|
+
+ '.fbw-field textarea:focus, .fbw-field input:focus { outline: none; border-color: ' + ACCENT + '; box-shadow: 0 0 0 3px ' + hexToRgba(ACCENT, 0.15) + '; }'
|
|
110
|
+
+ '.fbw-error { color: #dc2626; font-size: 12px; margin: -6px 0 12px; display: none; }'
|
|
111
|
+
+ '.fbw-error.fbw-show { display: block; }'
|
|
112
|
+
+ '.fbw-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }'
|
|
113
|
+
+ '.fbw-btn { border: none; border-radius: 8px; padding: 9px 16px; font-size: 13.5px; font-weight: 600; cursor: pointer; }'
|
|
114
|
+
+ '.fbw-btn-primary { background: ' + ACCENT + '; color: #fff; }'
|
|
115
|
+
+ '.fbw-btn-primary:hover { filter: brightness(1.08); }'
|
|
116
|
+
+ '.fbw-btn-ghost { background: none; color: #4b5563; }'
|
|
117
|
+
+ '.fbw-btn-ghost:hover { background: #f3f4f6; }'
|
|
118
|
+
+ '.fbw-toast {'
|
|
119
|
+
+ ' position: fixed; bottom: 22px; right: 22px; background: #111827; color: #fff;'
|
|
120
|
+
+ ' padding: 11px 16px; border-radius: 10px; font-size: 13px; z-index: 2147483004;'
|
|
121
|
+
+ ' box-shadow: 0 8px 24px rgba(0,0,0,0.3); opacity: 0; transform: translateY(6px);'
|
|
122
|
+
+ ' transition: opacity 0.18s ease, transform 0.18s ease; pointer-events: none;'
|
|
123
|
+
+ '}'
|
|
124
|
+
+ '.fbw-toast.fbw-show { opacity: 1; transform: translateY(0); }'
|
|
125
|
+
+ '.fbw-hidden { display: none !important; }';
|
|
126
|
+
|
|
127
|
+
var styleEl = document.createElement('style');
|
|
128
|
+
styleEl.textContent = css;
|
|
129
|
+
root.appendChild(styleEl);
|
|
130
|
+
|
|
131
|
+
function hexToRgba(hex, alpha) {
|
|
132
|
+
var h = hex.replace('#', '');
|
|
133
|
+
if (h.length === 3) h = h.split('').map(function (c) { return c + c; }).join('');
|
|
134
|
+
var r = parseInt(h.substring(0, 2), 16);
|
|
135
|
+
var g = parseInt(h.substring(2, 4), 16);
|
|
136
|
+
var b = parseInt(h.substring(4, 6), 16);
|
|
137
|
+
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------
|
|
141
|
+
// Markup
|
|
142
|
+
// ---------------------------------------------------------------------
|
|
143
|
+
var wrap = document.createElement('div');
|
|
144
|
+
wrap.innerHTML = ''
|
|
145
|
+
+ '<button class="fbw-tab" type="button" aria-haspopup="true" aria-expanded="false">' + escapeHtml(BUTTON_LABEL) + '</button>'
|
|
146
|
+
+ '<div class="fbw-menu" role="menu">'
|
|
147
|
+
+ ' <button type="button" data-action="pick-visible" role="menuitem">'
|
|
148
|
+
+ ' Feedback on this page<small>Select any element you have feedback on</small>'
|
|
149
|
+
+ ' </button>'
|
|
150
|
+
+ ' <button type="button" data-action="pick-hidden" role="menuitem">'
|
|
151
|
+
+ ' Quick feedback<small>General feedback on us</small>'
|
|
152
|
+
+ ' </button>'
|
|
153
|
+
+ '</div>'
|
|
154
|
+
+ '<div class="fbw-banner">'
|
|
155
|
+
+ ' <span>Click an element to select it · Esc to cancel</span>'
|
|
156
|
+
+ ' <button type="button" data-action="cancel-pick">Cancel</button>'
|
|
157
|
+
+ '</div>'
|
|
158
|
+
+ '<div class="fbw-highlight"></div>'
|
|
159
|
+
+ '<div class="fbw-overlay">'
|
|
160
|
+
+ ' <div class="fbw-modal" role="dialog" aria-modal="true" aria-labelledby="fbw-modal-title">'
|
|
161
|
+
+ ' <div class="fbw-modal-head">'
|
|
162
|
+
+ ' <h2 id="fbw-modal-title">Share feedback</h2>'
|
|
163
|
+
+ ' <button class="fbw-close" type="button" data-action="close-modal" aria-label="Close">×</button>'
|
|
164
|
+
+ ' </div>'
|
|
165
|
+
+ ' <form data-role="form">'
|
|
166
|
+
+ ' <div class="fbw-field" data-role="item-field">'
|
|
167
|
+
+ ' <label>Selected element</label>'
|
|
168
|
+
+ ' <div class="fbw-item-box" data-role="item-box"></div>'
|
|
169
|
+
+ ' </div>'
|
|
170
|
+
+ ' <div class="fbw-field">'
|
|
171
|
+
+ ' <label for="fbw-feedback">What\'s going on?</label>'
|
|
172
|
+
+ ' <textarea id="fbw-feedback" data-role="feedback" rows="4" placeholder="Tell us what you noticed..." required></textarea>'
|
|
173
|
+
+ ' </div>'
|
|
174
|
+
+ ' <div class="fbw-field">'
|
|
175
|
+
+ ' <label for="fbw-email">Email</label>'
|
|
176
|
+
+ ' <input id="fbw-email" data-role="email" type="email" placeholder="you@example.com" required />'
|
|
177
|
+
+ ' </div>'
|
|
178
|
+
+ ' <div class="fbw-error" data-role="error"></div>'
|
|
179
|
+
+ ' <div class="fbw-actions">'
|
|
180
|
+
+ ' <button type="button" class="fbw-btn fbw-btn-ghost" data-action="close-modal">Cancel</button>'
|
|
181
|
+
+ ' <button type="submit" class="fbw-btn fbw-btn-primary">Submit</button>'
|
|
182
|
+
+ ' </div>'
|
|
183
|
+
+ ' </form>'
|
|
184
|
+
+ ' </div>'
|
|
185
|
+
+ '</div>'
|
|
186
|
+
+ '<div class="fbw-toast" data-role="toast"></div>';
|
|
187
|
+
root.appendChild(wrap);
|
|
188
|
+
|
|
189
|
+
function escapeHtml(s) {
|
|
190
|
+
var d = document.createElement('div');
|
|
191
|
+
d.textContent = s;
|
|
192
|
+
return d.innerHTML;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------
|
|
196
|
+
// Element refs
|
|
197
|
+
// ---------------------------------------------------------------------
|
|
198
|
+
var el = {
|
|
199
|
+
tab: root.querySelector('.fbw-tab'),
|
|
200
|
+
menu: root.querySelector('.fbw-menu'),
|
|
201
|
+
banner: root.querySelector('.fbw-banner'),
|
|
202
|
+
highlight: root.querySelector('.fbw-highlight'),
|
|
203
|
+
overlay: root.querySelector('.fbw-overlay'),
|
|
204
|
+
modal: root.querySelector('.fbw-modal'),
|
|
205
|
+
form: root.querySelector('[data-role="form"]'),
|
|
206
|
+
itemField: root.querySelector('[data-role="item-field"]'),
|
|
207
|
+
itemBox: root.querySelector('[data-role="item-box"]'),
|
|
208
|
+
feedback: root.querySelector('[data-role="feedback"]'),
|
|
209
|
+
email: root.querySelector('[data-role="email"]'),
|
|
210
|
+
error: root.querySelector('[data-role="error"]'),
|
|
211
|
+
toast: root.querySelector('[data-role="toast"]')
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------
|
|
215
|
+
// State
|
|
216
|
+
// ---------------------------------------------------------------------
|
|
217
|
+
var STATE = { IDLE: 'idle', MENU: 'menu', PICKING: 'picking', REVIEW: 'review' };
|
|
218
|
+
var state = STATE.IDLE;
|
|
219
|
+
var showItemInForm = true;
|
|
220
|
+
var selectedSelector = null;
|
|
221
|
+
var selectedEl = null;
|
|
222
|
+
var hoverRafId = null;
|
|
223
|
+
|
|
224
|
+
function isInsideWidget(target) {
|
|
225
|
+
return target === host || host.contains(target);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------
|
|
229
|
+
// Menu
|
|
230
|
+
// ---------------------------------------------------------------------
|
|
231
|
+
el.tab.addEventListener('click', function () {
|
|
232
|
+
state === STATE.MENU ? closeMenu() : openMenu();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
function openMenu() {
|
|
236
|
+
state = STATE.MENU;
|
|
237
|
+
el.menu.classList.add('fbw-open');
|
|
238
|
+
el.tab.setAttribute('aria-expanded', 'true');
|
|
239
|
+
document.addEventListener('click', onOutsideMenuClick, true);
|
|
240
|
+
}
|
|
241
|
+
function closeMenu() {
|
|
242
|
+
if (state === STATE.MENU) state = STATE.IDLE;
|
|
243
|
+
el.menu.classList.remove('fbw-open');
|
|
244
|
+
el.tab.setAttribute('aria-expanded', 'false');
|
|
245
|
+
document.removeEventListener('click', onOutsideMenuClick, true);
|
|
246
|
+
}
|
|
247
|
+
function onOutsideMenuClick(e) {
|
|
248
|
+
if (!isInsideWidget(e.target)) closeMenu();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
el.menu.addEventListener('click', function (e) {
|
|
252
|
+
var btn = e.target.closest('button[data-action]');
|
|
253
|
+
if (!btn) return;
|
|
254
|
+
if (btn.dataset.action === 'pick-visible') startPicking(true);
|
|
255
|
+
if (btn.dataset.action === 'pick-hidden') {
|
|
256
|
+
showItemInForm = false;
|
|
257
|
+
closeMenu();
|
|
258
|
+
openModal();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------
|
|
263
|
+
// Picking mode (freezes the page, tracks hover, captures selection)
|
|
264
|
+
// ---------------------------------------------------------------------
|
|
265
|
+
function startPicking(showItem) {
|
|
266
|
+
showItemInForm = showItem;
|
|
267
|
+
closeMenu();
|
|
268
|
+
state = STATE.PICKING;
|
|
269
|
+
el.banner.classList.add('fbw-open');
|
|
270
|
+
document.addEventListener('mousemove', onHoverMove, true);
|
|
271
|
+
document.addEventListener('click', onDocumentClickCapture, true);
|
|
272
|
+
document.addEventListener('keydown', onKeyDownCapture, true);
|
|
273
|
+
document.addEventListener('scroll', onScrollOrResize, true);
|
|
274
|
+
window.addEventListener('resize', onScrollOrResize, true);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function stopPicking() {
|
|
278
|
+
el.banner.classList.remove('fbw-open');
|
|
279
|
+
document.removeEventListener('mousemove', onHoverMove, true);
|
|
280
|
+
document.removeEventListener('scroll', onScrollOrResize, true);
|
|
281
|
+
window.removeEventListener('resize', onScrollOrResize, true);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
el.banner.addEventListener('click', function (e) {
|
|
285
|
+
if (e.target.closest('[data-action="cancel-pick"]')) cancelAll();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
function onHoverMove(e) {
|
|
289
|
+
if (isInsideWidget(e.target)) return;
|
|
290
|
+
if (hoverRafId) cancelAnimationFrame(hoverRafId);
|
|
291
|
+
hoverRafId = requestAnimationFrame(function () {
|
|
292
|
+
positionHighlight(e.target);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function positionHighlight(target) {
|
|
297
|
+
var r = target.getBoundingClientRect();
|
|
298
|
+
el.highlight.style.display = 'block';
|
|
299
|
+
el.highlight.style.top = r.top + 'px';
|
|
300
|
+
el.highlight.style.left = r.left + 'px';
|
|
301
|
+
el.highlight.style.width = r.width + 'px';
|
|
302
|
+
el.highlight.style.height = r.height + 'px';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function onScrollOrResize() {
|
|
306
|
+
if (selectedEl && state === STATE.REVIEW) positionHighlight(selectedEl);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function onDocumentClickCapture(e) {
|
|
310
|
+
if (isInsideWidget(e.target)) return; // let widget UI work normally
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
e.stopPropagation();
|
|
313
|
+
if (typeof e.stopImmediatePropagation === 'function') e.stopImmediatePropagation();
|
|
314
|
+
|
|
315
|
+
if (state === STATE.PICKING) {
|
|
316
|
+
finalizeSelection(e.target);
|
|
317
|
+
}
|
|
318
|
+
// if state === STATE.REVIEW, the outside click is simply swallowed (page stays frozen)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function onKeyDownCapture(e) {
|
|
322
|
+
if (e.key === 'Escape') {
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
cancelAll();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function finalizeSelection(target) {
|
|
329
|
+
selectedEl = target;
|
|
330
|
+
selectedSelector = getUniqueSelector(target);
|
|
331
|
+
stopPicking();
|
|
332
|
+
positionHighlight(target);
|
|
333
|
+
openModal();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------
|
|
337
|
+
// Unique selector generation
|
|
338
|
+
// ---------------------------------------------------------------------
|
|
339
|
+
function getUniqueSelector(node) {
|
|
340
|
+
if (!(node instanceof Element)) return null;
|
|
341
|
+
|
|
342
|
+
if (node.id) {
|
|
343
|
+
var idSel = '#' + cssEscape(node.id);
|
|
344
|
+
if (safeQueryCount(idSel) === 1) return idSel;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
var parts = [];
|
|
348
|
+
var current = node;
|
|
349
|
+
|
|
350
|
+
while (current && current.nodeType === 1 && current !== document.documentElement) {
|
|
351
|
+
var part = current.tagName.toLowerCase();
|
|
352
|
+
|
|
353
|
+
if (current.id) {
|
|
354
|
+
part = '#' + cssEscape(current.id);
|
|
355
|
+
parts.unshift(part);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
var classes = Array.prototype.filter.call(current.classList, function (c) { return !!c; });
|
|
360
|
+
if (classes.length) {
|
|
361
|
+
part += '.' + classes.map(cssEscape).join('.');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
var parent = current.parentElement;
|
|
365
|
+
if (parent) {
|
|
366
|
+
var sameTagSiblings = Array.prototype.filter.call(parent.children, function (s) {
|
|
367
|
+
return s.tagName === current.tagName;
|
|
368
|
+
});
|
|
369
|
+
if (sameTagSiblings.length > 1) {
|
|
370
|
+
var idx = sameTagSiblings.indexOf(current) + 1;
|
|
371
|
+
part += ':nth-of-type(' + idx + ')';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
parts.unshift(part);
|
|
376
|
+
|
|
377
|
+
var testSelector = parts.join(' > ');
|
|
378
|
+
if (safeQueryCount(testSelector) === 1) return testSelector;
|
|
379
|
+
|
|
380
|
+
current = parent;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return parts.join(' > ');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function cssEscape(str) {
|
|
387
|
+
if (window.CSS && window.CSS.escape) return window.CSS.escape(str);
|
|
388
|
+
return String(str).replace(/([ #.;?%&,.+*~':"!^$[\]()=>|/])/g, '\\$1');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function safeQueryCount(selector) {
|
|
392
|
+
try {
|
|
393
|
+
return document.querySelectorAll(selector).length;
|
|
394
|
+
} catch (err) {
|
|
395
|
+
return -1;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------------
|
|
400
|
+
// Modal (page stays frozen while this is open)
|
|
401
|
+
// ---------------------------------------------------------------------
|
|
402
|
+
function openModal() {
|
|
403
|
+
state = STATE.REVIEW;
|
|
404
|
+
el.error.classList.remove('fbw-show');
|
|
405
|
+
el.form.reset();
|
|
406
|
+
|
|
407
|
+
if (showItemInForm) {
|
|
408
|
+
el.itemField.classList.remove('fbw-hidden');
|
|
409
|
+
el.itemBox.textContent = selectedSelector || '(none)';
|
|
410
|
+
} else {
|
|
411
|
+
el.itemField.classList.add('fbw-hidden');
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
el.overlay.classList.add('fbw-open');
|
|
415
|
+
setTimeout(function () { el.feedback.focus(); }, 30);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function closeModal() {
|
|
419
|
+
el.overlay.classList.remove('fbw-open');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function cancelAll() {
|
|
423
|
+
stopPicking();
|
|
424
|
+
closeModal();
|
|
425
|
+
el.highlight.style.display = 'none';
|
|
426
|
+
document.removeEventListener('click', onDocumentClickCapture, true);
|
|
427
|
+
document.removeEventListener('keydown', onKeyDownCapture, true);
|
|
428
|
+
selectedEl = null;
|
|
429
|
+
selectedSelector = null;
|
|
430
|
+
state = STATE.IDLE;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
el.overlay.addEventListener('click', function (e) {
|
|
434
|
+
var actionBtn = e.target.closest('[data-action="close-modal"]');
|
|
435
|
+
if (actionBtn) cancelAll();
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ---------------------------------------------------------------------
|
|
439
|
+
// Submit
|
|
440
|
+
// ---------------------------------------------------------------------
|
|
441
|
+
el.form.addEventListener('submit', function (e) {
|
|
442
|
+
e.preventDefault();
|
|
443
|
+
var feedback = el.feedback.value.trim();
|
|
444
|
+
var email = el.email.value.trim();
|
|
445
|
+
var emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
446
|
+
|
|
447
|
+
if (!feedback || !emailOk) {
|
|
448
|
+
el.error.textContent = !feedback
|
|
449
|
+
? 'Please add some feedback before submitting.'
|
|
450
|
+
: 'Please enter a valid email address.';
|
|
451
|
+
el.error.classList.add('fbw-show');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
var payload = {
|
|
456
|
+
optionType: showItemInForm ? 'page_item_visible' : 'page_item_hidden',
|
|
457
|
+
elementSelector: selectedSelector,
|
|
458
|
+
pageUrl: window.location.href,
|
|
459
|
+
pageTitle: document.title,
|
|
460
|
+
feedback: feedback,
|
|
461
|
+
email: email,
|
|
462
|
+
submittedAt: new Date().toISOString()
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
submitFeedback(payload);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
function submitFeedback(payload) {
|
|
469
|
+
window.dispatchEvent(new CustomEvent('feedbackwidget:submit', { detail: payload }));
|
|
470
|
+
|
|
471
|
+
if (ENDPOINT) {
|
|
472
|
+
fetch(ENDPOINT, {
|
|
473
|
+
method: 'POST',
|
|
474
|
+
headers: { 'Content-Type': 'application/json' },
|
|
475
|
+
body: JSON.stringify(payload)
|
|
476
|
+
}).catch(function () { /* swallow network errors, UX already confirmed below */ });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
cancelAll();
|
|
480
|
+
showToast('Thanks for the feedback!');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function showToast(message) {
|
|
484
|
+
el.toast.textContent = message;
|
|
485
|
+
el.toast.classList.add('fbw-show');
|
|
486
|
+
setTimeout(function () { el.toast.classList.remove('fbw-show'); }, 2400);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ---------------------------------------------------------------------
|
|
490
|
+
// Minimal public API
|
|
491
|
+
// ---------------------------------------------------------------------
|
|
492
|
+
window.FeedbackWidget = {
|
|
493
|
+
open: function (mode) { startPicking(mode !== 'hidden'); },
|
|
494
|
+
close: cancelAll
|
|
495
|
+
};
|
|
496
|
+
})();
|