pushfeedback 0.1.84 → 0.1.85
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/dist/cjs/canvas-editor_3.cjs.entry.js +393 -41
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/pushfeedback.cjs.js +1 -1
- package/dist/collection/components/canvas-editor/canvas-editor.js +173 -15
- package/dist/collection/components/feedback-button/feedback-button.css +45 -1
- package/dist/collection/components/feedback-button/feedback-button.js +58 -8
- package/dist/collection/components/feedback-modal/feedback-modal.css +6 -0
- package/dist/collection/components/feedback-modal/feedback-modal.js +272 -17
- package/dist/collection/utils/sanitize-html.js +90 -0
- package/dist/components/canvas-editor2.js +139 -15
- package/dist/components/feedback-button.js +24 -8
- package/dist/components/feedback-modal2.js +241 -18
- package/dist/esm/canvas-editor_3.entry.js +393 -41
- package/dist/esm/loader.js +1 -1
- package/dist/esm/pushfeedback.js +1 -1
- package/dist/pushfeedback/p-17acbd1c.entry.js +1 -0
- package/dist/pushfeedback/pushfeedback.esm.js +1 -1
- package/dist/types/components/canvas-editor/canvas-editor.d.ts +7 -0
- package/dist/types/components/feedback-button/feedback-button.d.ts +14 -0
- package/dist/types/components/feedback-modal/feedback-modal.d.ts +24 -0
- package/dist/types/components.d.ts +40 -0
- package/dist/types/utils/sanitize-html.d.ts +8 -0
- package/package.json +2 -1
- package/dist/pushfeedback/p-06bb93cb.entry.js +0 -1
|
@@ -1,8 +1,27 @@
|
|
|
1
1
|
import { h } from '@stencil/core';
|
|
2
|
+
import { sanitizeHtml } from '../../utils/sanitize-html';
|
|
2
3
|
export class FeedbackModal {
|
|
3
4
|
constructor() {
|
|
4
5
|
// Track the history entry pushed while the modal is open
|
|
5
6
|
this.historyEntryPushed = false;
|
|
7
|
+
// Element that had focus before the modal opened, so it can be restored on close
|
|
8
|
+
this.previouslyFocused = null;
|
|
9
|
+
this.handleKeyDown = (event) => {
|
|
10
|
+
// The canvas editor renders its own overlay and handles its own keys
|
|
11
|
+
if (!this.showModal || this.embedded || this.showCanvasEditor) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (event.key === 'Escape' && this.escapeClose) {
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
// Escape is an accidental-dismissal path like clicking outside, so keep
|
|
17
|
+
// whatever the user already typed
|
|
18
|
+
this.doClose(!this.formSuccess);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (event.key === 'Tab') {
|
|
22
|
+
this.trapFocus(event);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
6
25
|
this.handlePopState = () => {
|
|
7
26
|
// Back button pressed while the modal is open: close it and consume our entry
|
|
8
27
|
if (this.historyEntryPushed) {
|
|
@@ -46,12 +65,16 @@ export class FeedbackModal {
|
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
67
|
const body = Object.assign({ url: window.location.href, message: this.formMessage, email: this.formEmail, project: this.project, screenshot: this.encodedScreenshot, rating: this.selectedRating, ratingMode: this.ratingMode, metadata: this.metadata, verification: this.formVerification, session: localStorage.getItem('pushfeedback_sessionid') || '' }, (recaptchaToken && { recaptchaToken }));
|
|
49
|
-
const
|
|
68
|
+
const headers = {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
};
|
|
71
|
+
if (this.apiKey) {
|
|
72
|
+
headers['Authorization'] = `Api-Key ${this.apiKey}`;
|
|
73
|
+
}
|
|
74
|
+
const res = await fetch('https://app.pushfeedback.com/api/v1/feedback/', {
|
|
50
75
|
method: 'POST',
|
|
51
76
|
body: JSON.stringify(body),
|
|
52
|
-
headers
|
|
53
|
-
'Content-Type': 'application/json',
|
|
54
|
-
},
|
|
77
|
+
headers,
|
|
55
78
|
});
|
|
56
79
|
if (res.status === 201) {
|
|
57
80
|
const feedback_with_id = Object.assign(Object.assign({}, body), { id: await res.json() });
|
|
@@ -124,8 +147,15 @@ export class FeedbackModal {
|
|
|
124
147
|
this.encodedScreenshot = null;
|
|
125
148
|
this.formMessage = '';
|
|
126
149
|
this.formEmail = '';
|
|
150
|
+
// The rating and the privacy checkbox are part of the same in-progress
|
|
151
|
+
// input. Leaving the rating set meant reopening the modal showed the
|
|
152
|
+
// previous session's choice still highlighted, and submitting then sent
|
|
153
|
+
// that stale rating instead of one chosen for this submission.
|
|
154
|
+
this.selectedRating = this.initialRating();
|
|
155
|
+
this.isPrivacyChecked = false;
|
|
127
156
|
}
|
|
128
157
|
this.resetOverflow();
|
|
158
|
+
this.restoreFocus();
|
|
129
159
|
}, 200);
|
|
130
160
|
};
|
|
131
161
|
// Handle screenshot events from canvas editor
|
|
@@ -135,12 +165,16 @@ export class FeedbackModal {
|
|
|
135
165
|
this.takingScreenshot = false;
|
|
136
166
|
this.showCanvasEditor = false;
|
|
137
167
|
this.autoStartCapture = false;
|
|
168
|
+
this.focusModal();
|
|
138
169
|
};
|
|
139
170
|
this.handleScreenshotCancelled = () => {
|
|
140
171
|
this.showModal = true;
|
|
141
172
|
this.takingScreenshot = false;
|
|
142
173
|
this.showCanvasEditor = false;
|
|
143
174
|
this.autoStartCapture = false;
|
|
175
|
+
// Focus was inside the editor that just went away, so bring it back to the
|
|
176
|
+
// form rather than letting it fall to the top of the page
|
|
177
|
+
this.focusModal();
|
|
144
178
|
};
|
|
145
179
|
this.handleScreenshotError = (event) => {
|
|
146
180
|
console.error('Screenshot error:', event.detail.error);
|
|
@@ -152,6 +186,7 @@ export class FeedbackModal {
|
|
|
152
186
|
this.takingScreenshot = false;
|
|
153
187
|
this.showCanvasEditor = false;
|
|
154
188
|
this.autoStartCapture = false;
|
|
189
|
+
this.focusModal();
|
|
155
190
|
// Auto-hide error after 8 seconds
|
|
156
191
|
setTimeout(() => {
|
|
157
192
|
this.showScreenshotError = false;
|
|
@@ -193,6 +228,7 @@ export class FeedbackModal {
|
|
|
193
228
|
this.screenshotError = '';
|
|
194
229
|
this.showCanvasEditor = false;
|
|
195
230
|
this.autoStartCapture = false;
|
|
231
|
+
this.apiKey = '';
|
|
196
232
|
this.customFont = false;
|
|
197
233
|
this.emailAddress = '';
|
|
198
234
|
this.hideEmail = false;
|
|
@@ -212,8 +248,10 @@ export class FeedbackModal {
|
|
|
212
248
|
this.embedded = false;
|
|
213
249
|
this.clickOutsideClose = true;
|
|
214
250
|
this.historyClose = true;
|
|
251
|
+
this.escapeClose = true;
|
|
215
252
|
this.emailPlaceholder = 'Email address (optional)';
|
|
216
253
|
this.errorMessage = 'Please try again later.';
|
|
254
|
+
this.errorMessage401 = 'This project requires a valid API key to submit feedback. Check the api-key attribute.';
|
|
217
255
|
this.errorMessage403 = 'The request URL does not match the one defined in PushFeedback for this project.';
|
|
218
256
|
this.errorMessage404 = 'We could not find the provided project ID in PushFeedback.';
|
|
219
257
|
this.messagePlaceholder = 'Share your thoughts...';
|
|
@@ -226,6 +264,10 @@ export class FeedbackModal {
|
|
|
226
264
|
this.ratingPlaceholder = 'Was this page helpful?';
|
|
227
265
|
this.ratingStarsPlaceholder = 'How would you rate this page?';
|
|
228
266
|
this.sendButtonText = 'Send';
|
|
267
|
+
this.closeButtonLabel = 'Close feedback form';
|
|
268
|
+
this.ratingPositiveLabel = 'Yes';
|
|
269
|
+
this.ratingNegativeLabel = 'No';
|
|
270
|
+
this.ratingStarLabel = 'Rate {rating} out of 5';
|
|
229
271
|
this.successMessage = '';
|
|
230
272
|
this.recaptchaText = 'This form is protected by reCAPTCHA and the Google <a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a> and <a href="https://policies.google.com/terms" target="_blank" rel="noopener noreferrer">Terms of Service</a> apply.';
|
|
231
273
|
this.screenshotAttachedText = 'Screenshot attached';
|
|
@@ -248,20 +290,96 @@ export class FeedbackModal {
|
|
|
248
290
|
}
|
|
249
291
|
connectedCallback() {
|
|
250
292
|
window.addEventListener('popstate', this.handlePopState);
|
|
293
|
+
document.addEventListener('keydown', this.handleKeyDown);
|
|
251
294
|
}
|
|
252
295
|
disconnectedCallback() {
|
|
253
296
|
window.removeEventListener('popstate', this.handlePopState);
|
|
297
|
+
document.removeEventListener('keydown', this.handleKeyDown);
|
|
298
|
+
}
|
|
299
|
+
// Keeps Tab cycling inside the dialog while it is open
|
|
300
|
+
trapFocus(event) {
|
|
301
|
+
var _a, _b;
|
|
302
|
+
const focusable = this.getFocusableElements();
|
|
303
|
+
if (focusable.length === 0) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const first = focusable[0];
|
|
307
|
+
const last = focusable[focusable.length - 1];
|
|
308
|
+
const active = (_a = this.modalContent) === null || _a === void 0 ? void 0 : _a.getRootNode();
|
|
309
|
+
const current = active === null || active === void 0 ? void 0 : active.activeElement;
|
|
310
|
+
if (current === this.modalContent) {
|
|
311
|
+
// Focus is on the dialog container itself (where it lands on open).
|
|
312
|
+
// Tab falls through to the first control natively; Shift+Tab would
|
|
313
|
+
// escape the dialog, so wrap it to the last one.
|
|
314
|
+
if (event.shiftKey) {
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
last.focus();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (event.shiftKey && current === first) {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
last.focus();
|
|
322
|
+
}
|
|
323
|
+
else if (!event.shiftKey && current === last) {
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
first.focus();
|
|
326
|
+
}
|
|
327
|
+
else if (!current || !((_b = this.modalContent) === null || _b === void 0 ? void 0 : _b.contains(current))) {
|
|
328
|
+
// Focus escaped the dialog (or never entered it) - pull it back in
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
first.focus();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// offsetParent is not usable here: it is null for `position: fixed` elements
|
|
334
|
+
// even when they are perfectly visible, and it is non-null for
|
|
335
|
+
// `visibility: hidden` ones, which cannot be focused. getClientRects covers
|
|
336
|
+
// display:none and detached nodes, so visibility is the only extra check.
|
|
337
|
+
isFocusable(el) {
|
|
338
|
+
if (el.hidden || el.getClientRects().length === 0) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
return getComputedStyle(el).visibility !== 'hidden';
|
|
342
|
+
}
|
|
343
|
+
getFocusableElements() {
|
|
344
|
+
if (!this.modalContent) {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
const selector = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
348
|
+
return Array.from(this.modalContent.querySelectorAll(selector)).filter((el) => this.isFocusable(el));
|
|
349
|
+
}
|
|
350
|
+
// Moves focus to the dialog container once it has rendered, so screen readers
|
|
351
|
+
// announce the dialog title rather than jumping straight to the first control
|
|
352
|
+
focusModal() {
|
|
353
|
+
requestAnimationFrame(() => {
|
|
354
|
+
if (this.modalContent) {
|
|
355
|
+
this.modalContent.focus();
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
restoreFocus() {
|
|
360
|
+
const target = this.previouslyFocused;
|
|
361
|
+
this.previouslyFocused = null;
|
|
362
|
+
if (target && typeof target.focus === 'function' && target.isConnected) {
|
|
363
|
+
target.focus();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// The rating the widget starts from. Closing restores this rather than
|
|
367
|
+
// blanking to neutral, so a consumer's configured `rating` default survives
|
|
368
|
+
// the modal being closed and reopened.
|
|
369
|
+
initialRating() {
|
|
370
|
+
if (this.ratingMode == 'thumbs' && this.rating == 0) {
|
|
371
|
+
return 5;
|
|
372
|
+
}
|
|
373
|
+
if (this.rating) {
|
|
374
|
+
return this.rating;
|
|
375
|
+
}
|
|
376
|
+
return -1;
|
|
254
377
|
}
|
|
255
378
|
componentWillLoad() {
|
|
256
379
|
if (this.fetchData)
|
|
257
380
|
this.fetchProjectData();
|
|
258
381
|
this.formEmail = this.emailAddress;
|
|
259
|
-
|
|
260
|
-
this.selectedRating = this.rating;
|
|
261
|
-
}
|
|
262
|
-
if (this.ratingMode == 'thumbs' && this.rating == 0) {
|
|
263
|
-
this.selectedRating = 5;
|
|
264
|
-
}
|
|
382
|
+
this.selectedRating = this.initialRating();
|
|
265
383
|
// Automatically show modal when embedded
|
|
266
384
|
if (this.embedded) {
|
|
267
385
|
this.showModal = true;
|
|
@@ -269,7 +387,9 @@ export class FeedbackModal {
|
|
|
269
387
|
}
|
|
270
388
|
async fetchProjectData() {
|
|
271
389
|
try {
|
|
272
|
-
|
|
390
|
+
// Public config endpoint: never send the API key here, it is only
|
|
391
|
+
// needed (and only allowed to travel) on the feedback POST
|
|
392
|
+
const response = await fetch('https://app.pushfeedback.com/api/v1/projects/' + this.project + '/');
|
|
273
393
|
const data = await response.json();
|
|
274
394
|
this.whitelabel = data.whitelabel;
|
|
275
395
|
this.recaptchaEnabled = data.recaptcha_enabled || false;
|
|
@@ -358,28 +478,28 @@ export class FeedbackModal {
|
|
|
358
478
|
this.selectedRating = newRating;
|
|
359
479
|
}
|
|
360
480
|
render() {
|
|
361
|
-
return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''} ${this.embedded ? 'feedback-modal-wrapper--embedded' : ''}`, part: "wrapper" }, this.showCanvasEditor && (h("canvas-editor", { ref: (el) => (this.canvasEditorRef = el), exportparts: "overlay: canvas-editor-overlay, modal: canvas-editor-modal, header: canvas-editor-header, title: canvas-editor-title, toolbar: canvas-editor-toolbar, tool-button: canvas-editor-tool-button, cancel-button: canvas-editor-cancel-button, save-button: canvas-editor-save-button, content: canvas-editor-content, canvas: canvas-editor-canvas", "canvas-editor-title": this.screenshotEditorTitle, "canvas-editor-cancel-text": this.screenshotEditorCancelText, "canvas-editor-save-text": this.screenshotEditorSaveText, "screenshot-taking-text": this.screenshotTakingText, "screenshot-attached-text": this.screenshotAttachedText, "screenshot-button-text": this.screenshotButtonText, "auto-start-screenshot": this.autoStartCapture, "existing-screenshot": this.encodedScreenshot || '', "edit-text-button-text": this.screenshotEditTextButtonText, "size-label-text": this.screenshotSizeLabelText, "border-label-text": this.screenshotBorderLabelText, "edit-text-prompt-text": this.screenshotEditTextPromptText, "screenshot-error-general": this.screenshotErrorGeneral, "screenshot-error-permission": this.screenshotErrorPermission, "screenshot-error-not-supported": this.screenshotErrorNotSupported, "screenshot-error-not-found": this.screenshotErrorNotFound, "screenshot-error-cancelled": this.screenshotErrorCancelled, "screenshot-error-browser-not-supported": this.screenshotErrorBrowserNotSupported, "screenshot-error-unexpected": this.screenshotErrorUnexpected, onScreenshotReady: this.handleScreenshotReady, onScreenshotCancelled: this.handleScreenshotCancelled, onScreenshotFailed: this.handleScreenshotError })), this.showScreenshotError && (h("div", { class: "screenshot-error-notification" }, h("div", { class: "screenshot-error-content" }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("circle", { cx: "12", cy: "12", r: "10" }), h("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), h("line", { x1: "9", y1: "9", x2: "15", y2: "15" })), h("span", null, this.screenshotError), h("button", { class: "error-close-btn", onClick: () => (this.showScreenshotError = false), title: "Close" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))))), this.showModal && !this.embedded && (h("div", { class: `feedback-overlay ${this.isAnimating ? 'feedback-overlay--visible' : ''}`, part: "overlay", onClick: this.clickOutsideClose ? this.handleOverlayClick : undefined })), this.showModal && (h("div", { class: `feedback-modal-content feedback-modal-content--${this.modalPosition} ${this.isAnimating ? 'feedback-modal-content--open' : ''} ${this.embedded ? 'feedback-modal-content--embedded' : ''}`, part: "modal", ref: (el) => (this.modalContent = el) }, h("div", { class: `feedback-modal-header ${(this.formSuccess && !this.successMessage) || (this.formError && !this.errorMessage)
|
|
481
|
+
return (h("div", { class: `feedback-modal-wrapper ${this.customFont ? 'feedback-modal-wrapper--custom-font' : ''} ${this.embedded ? 'feedback-modal-wrapper--embedded' : ''}`, part: "wrapper" }, this.showCanvasEditor && (h("canvas-editor", { ref: (el) => (this.canvasEditorRef = el), exportparts: "overlay: canvas-editor-overlay, modal: canvas-editor-modal, header: canvas-editor-header, title: canvas-editor-title, toolbar: canvas-editor-toolbar, tool-button: canvas-editor-tool-button, cancel-button: canvas-editor-cancel-button, save-button: canvas-editor-save-button, content: canvas-editor-content, canvas: canvas-editor-canvas", "canvas-editor-title": this.screenshotEditorTitle, "canvas-editor-cancel-text": this.screenshotEditorCancelText, "canvas-editor-save-text": this.screenshotEditorSaveText, "screenshot-taking-text": this.screenshotTakingText, "screenshot-attached-text": this.screenshotAttachedText, "screenshot-button-text": this.screenshotButtonText, "escape-close": this.escapeClose ? 'true' : 'false', "click-outside-close": this.clickOutsideClose ? 'true' : 'false', "auto-start-screenshot": this.autoStartCapture, "existing-screenshot": this.encodedScreenshot || '', "edit-text-button-text": this.screenshotEditTextButtonText, "size-label-text": this.screenshotSizeLabelText, "border-label-text": this.screenshotBorderLabelText, "edit-text-prompt-text": this.screenshotEditTextPromptText, "screenshot-error-general": this.screenshotErrorGeneral, "screenshot-error-permission": this.screenshotErrorPermission, "screenshot-error-not-supported": this.screenshotErrorNotSupported, "screenshot-error-not-found": this.screenshotErrorNotFound, "screenshot-error-cancelled": this.screenshotErrorCancelled, "screenshot-error-browser-not-supported": this.screenshotErrorBrowserNotSupported, "screenshot-error-unexpected": this.screenshotErrorUnexpected, onScreenshotReady: this.handleScreenshotReady, onScreenshotCancelled: this.handleScreenshotCancelled, onScreenshotFailed: this.handleScreenshotError })), this.showScreenshotError && (h("div", { class: "screenshot-error-notification" }, h("div", { class: "screenshot-error-content" }, h("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("circle", { cx: "12", cy: "12", r: "10" }), h("line", { x1: "15", y1: "9", x2: "9", y2: "15" }), h("line", { x1: "9", y1: "9", x2: "15", y2: "15" })), h("span", null, this.screenshotError), h("button", { class: "error-close-btn", onClick: () => (this.showScreenshotError = false), title: "Close" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" })))))), this.showModal && !this.embedded && (h("div", { class: `feedback-overlay ${this.isAnimating ? 'feedback-overlay--visible' : ''}`, part: "overlay", "aria-hidden": "true", onClick: this.clickOutsideClose ? this.handleOverlayClick : undefined })), this.showModal && (h("div", { class: `feedback-modal-content feedback-modal-content--${this.modalPosition} ${this.isAnimating ? 'feedback-modal-content--open' : ''} ${this.embedded ? 'feedback-modal-content--embedded' : ''}`, part: "modal", role: this.embedded ? 'region' : 'dialog', "aria-modal": this.embedded ? undefined : 'true', "aria-labelledby": "feedback-modal-title", tabindex: "-1", ref: (el) => (this.modalContent = el) }, h("div", { class: `feedback-modal-header ${(this.formSuccess && !this.successMessage) || (this.formError && !this.errorMessage)
|
|
362
482
|
? 'feedback-modal-header--no-content'
|
|
363
|
-
: ''}`, part: "header" }, h("div", { class: "feedback-modal-header-content" }, !this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-header-text" }, h("span", { class: "feedback-modal-title", part: "title" }, this.modalTitle), !this.whitelabel && (h("span", { class: "feedback-modal-powered-by", part: "powered-by" }, "Powered by", ' ', h("a", { target: "_blank", href: "https://pushfeedback.com" }, "PushFeedback"))))) : this.formSuccess ? (h("span", { class: "feedback-modal-title", part: "title" }, this.modalTitleSuccess)) : (h("span", { class: "feedback-modal-title", part: "title" }, this.modalTitleError))), !this.embedded && (h("button", { class: "feedback-modal-close", part: "close-button", onClick: this.close }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))))), h("div", { class: "feedback-modal-body", part: "body" }, !this.formSuccess && !this.formError ? (h("form", { part: "form", onSubmit: this.handleSubmit }, !this.hideRating && (h("div", { class: "feedback-modal-rating", part: "rating" }, this.ratingMode === 'thumbs' ? (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--thumbs", part: "rating-buttons" }, h("button", { title: "
|
|
483
|
+
: ''}`, part: "header" }, h("div", { class: "feedback-modal-header-content" }, !this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-header-text" }, h("span", { id: "feedback-modal-title", class: "feedback-modal-title", part: "title" }, this.modalTitle), !this.whitelabel && (h("span", { class: "feedback-modal-powered-by", part: "powered-by" }, "Powered by", ' ', h("a", { target: "_blank", href: "https://pushfeedback.com" }, "PushFeedback"))))) : this.formSuccess ? (h("span", { id: "feedback-modal-title", class: "feedback-modal-title", part: "title" }, this.modalTitleSuccess)) : (h("span", { id: "feedback-modal-title", class: "feedback-modal-title", part: "title" }, this.modalTitleError))), !this.embedded && (h("button", { class: "feedback-modal-close", part: "close-button", type: "button", "aria-label": this.closeButtonLabel, onClick: this.close }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "#191919", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather feather-x" }, h("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), h("line", { x1: "6", y1: "6", x2: "18", y2: "18" }))))), h("div", { class: "feedback-modal-body", part: "body" }, !this.formSuccess && !this.formError ? (h("form", { part: "form", onSubmit: this.handleSubmit }, !this.hideRating && (h("div", { class: "feedback-modal-rating", part: "rating" }, this.ratingMode === 'thumbs' ? (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--thumbs", part: "rating-buttons" }, h("button", { type: "button", title: this.ratingPositiveLabel, "aria-label": this.ratingPositiveLabel, "aria-pressed": this.selectedRating === 1 ? 'true' : 'false', class: `feedback-modal-rating-button feedback-modal-rating-button--positive ${this.selectedRating === 1
|
|
364
484
|
? 'feedback-modal-rating-button--selected'
|
|
365
485
|
: ''}`, part: "rating-button rating-button-positive", onClick: (event) => {
|
|
366
486
|
event.preventDefault();
|
|
367
487
|
this.handleRatingChange(1);
|
|
368
|
-
} }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }))), h("button", { title: "
|
|
488
|
+
} }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" }))), h("button", { type: "button", title: this.ratingNegativeLabel, "aria-label": this.ratingNegativeLabel, "aria-pressed": this.selectedRating === 5 ? 'true' : 'false', class: `feedback-modal-rating-button feedback-modal-rating-button--negative ${this.selectedRating === 5
|
|
369
489
|
? 'feedback-modal-rating-button--selected'
|
|
370
490
|
: ''}`, part: "rating-button rating-button-negative", onClick: (event) => {
|
|
371
491
|
event.preventDefault();
|
|
372
492
|
this.handleRatingChange(5);
|
|
373
|
-
} }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" })))))) : (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingStarsPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--stars", part: "rating-buttons" }, [1, 2, 3, 4, 5].map((rating) => (h("button", { key: rating, class: `feedback-modal-rating-button ${this.selectedRating >= rating
|
|
493
|
+
} }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("path", { d: "M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" })))))) : (h("div", { class: "feedback-modal-rating-content" }, h("span", { class: "feedback-modal-input-heading", part: "rating-title" }, this.ratingStarsPlaceholder), h("div", { class: "feedback-modal-rating-buttons feedback-modal-rating-buttons--stars", part: "rating-buttons" }, [1, 2, 3, 4, 5].map((rating) => (h("button", { key: rating, type: "button", "aria-label": this.ratingStarLabel.replace('{rating}', String(rating)), "aria-pressed": this.selectedRating === rating ? 'true' : 'false', class: `feedback-modal-rating-button ${this.selectedRating >= rating
|
|
374
494
|
? 'feedback-modal-rating-button--selected'
|
|
375
495
|
: ''}`, part: "rating-button", onClick: (event) => {
|
|
376
496
|
event.preventDefault();
|
|
377
497
|
this.handleRatingChange(rating);
|
|
378
|
-
} }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })))))))))), h("div", { class: "feedback-modal-text" }, h("textarea", { part: "message-input", placeholder: this.messagePlaceholder, value: this.formMessage, onInput: (event) => this.handleMessageInput(event) })), !this.hideEmail && (h("div", { class: "feedback-modal-email" }, h("input", { part: "email-input", placeholder: this.emailPlaceholder, type: "email", onInput: (event) => this.handleEmailInput(event), value: this.formEmail, required: this.isEmailRequired }))), h("div", { class: "feedback-verification" }, h("input", { type: "text", name: "verification", style: { display: 'none' }, onInput: (event) => this.handleVerification(event), value: this.formVerification })), !this.hidePrivacyPolicy && (h("div", { class: "feedback-modal-privacy", part: "privacy" }, h("input", { type: "checkbox", id: "privacyPolicy", onChange: (ev) => this.handleCheckboxChange(ev), required: true }), h("span", { innerHTML: this.privacyPolicyText }))), h("div", { class: `feedback-modal-buttons ${this.hideScreenshotButton ? 'single' : ''}`, part: "buttons" }, !this.hideScreenshotButton && (h("button", { type: "button", class: `feedback-modal-button feedback-modal-button--screenshot ${this.encodedScreenshot ? 'feedback-modal-button--active' : ''}`, part: "screenshot-button", onClick: this.openScreenShot, disabled: this.sending || this.takingScreenshot }, this.encodedScreenshot && (h("div", { class: "screenshot-preview", onClick: this.openCanvasEditor }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview" }))), !this.encodedScreenshot && !this.takingScreenshot && (h("svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 -960 960 960", width: "24" }, h("path", { d: "M680-80v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-200h80v120h120v80H200Zm0-360v-200h200v80H280v120h-80Zm480 0v-120H560v-80h200v200h-80Z" }))), this.takingScreenshot && (h("div", { class: "screenshot-loading" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#666", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather-loader" }, h("line", { x1: "12", y1: "2", x2: "12", y2: "6" }), h("line", { x1: "12", y1: "18", x2: "12", y2: "22" }), h("line", { x1: "4.93", y1: "4.93", x2: "7.76", y2: "7.76" }), h("line", { x1: "16.24", y1: "16.24", x2: "19.07", y2: "19.07" }), h("line", { x1: "2", y1: "12", x2: "6", y2: "12" }), h("line", { x1: "18", y1: "12", x2: "22", y2: "12" }), h("line", { x1: "4.93", y1: "19.07", x2: "7.76", y2: "16.24" }), h("line", { x1: "16.24", y1: "7.76", x2: "19.07", y2: "4.93" })))), this.takingScreenshot
|
|
498
|
+
} }, h("svg", { xmlns: "http://www.w3.org/2000/svg", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "#5F6368", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round" }, h("polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" })))))))))), h("div", { class: "feedback-modal-text" }, h("textarea", { part: "message-input", placeholder: this.messagePlaceholder, value: this.formMessage, onInput: (event) => this.handleMessageInput(event) })), !this.hideEmail && (h("div", { class: "feedback-modal-email" }, h("input", { part: "email-input", placeholder: this.emailPlaceholder, type: "email", onInput: (event) => this.handleEmailInput(event), value: this.formEmail, required: this.isEmailRequired }))), h("div", { class: "feedback-verification" }, h("input", { type: "text", name: "verification", style: { display: 'none' }, onInput: (event) => this.handleVerification(event), value: this.formVerification })), !this.hidePrivacyPolicy && (h("div", { class: "feedback-modal-privacy", part: "privacy" }, h("input", { type: "checkbox", id: "privacyPolicy", onChange: (ev) => this.handleCheckboxChange(ev), required: true }), h("span", { innerHTML: sanitizeHtml(this.privacyPolicyText) }))), h("div", { class: `feedback-modal-buttons ${this.hideScreenshotButton ? 'single' : ''}`, part: "buttons" }, !this.hideScreenshotButton && (h("button", { type: "button", class: `feedback-modal-button feedback-modal-button--screenshot ${this.encodedScreenshot ? 'feedback-modal-button--active' : ''}`, part: "screenshot-button", onClick: this.openScreenShot, disabled: this.sending || this.takingScreenshot }, this.encodedScreenshot && (h("div", { class: "screenshot-preview", onClick: this.openCanvasEditor }, h("img", { src: this.encodedScreenshot, alt: "Screenshot Preview" }))), !this.encodedScreenshot && !this.takingScreenshot && (h("svg", { xmlns: "http://www.w3.org/2000/svg", height: "24", viewBox: "0 -960 960 960", width: "24" }, h("path", { d: "M680-80v-120H560v-80h120v-120h80v120h120v80H760v120h-80ZM200-200v-200h80v120h120v80H200Zm0-360v-200h200v80H280v120h-80Zm480 0v-120H560v-80h200v200h-80Z" }))), this.takingScreenshot && (h("div", { class: "screenshot-loading" }, h("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#666", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", class: "feather-loader" }, h("line", { x1: "12", y1: "2", x2: "12", y2: "6" }), h("line", { x1: "12", y1: "18", x2: "12", y2: "22" }), h("line", { x1: "4.93", y1: "4.93", x2: "7.76", y2: "7.76" }), h("line", { x1: "16.24", y1: "16.24", x2: "19.07", y2: "19.07" }), h("line", { x1: "2", y1: "12", x2: "6", y2: "12" }), h("line", { x1: "18", y1: "12", x2: "22", y2: "12" }), h("line", { x1: "4.93", y1: "19.07", x2: "7.76", y2: "16.24" }), h("line", { x1: "16.24", y1: "7.76", x2: "19.07", y2: "4.93" })))), this.takingScreenshot
|
|
379
499
|
? this.screenshotTakingText
|
|
380
500
|
: this.encodedScreenshot
|
|
381
501
|
? this.screenshotAttachedText
|
|
382
|
-
: this.screenshotButtonText)), h("button", { class: "feedback-modal-button feedback-modal-button--submit", part: "submit-button", type: "submit", disabled: this.sending }, this.sendButtonText)))) : this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-success" }, h("p", { class: "feedback-modal-message", part: "success-message" }, this.successMessage))) : this.formError && this.formErrorStatus == 404 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage404)) : this.formError && this.formErrorStatus == 403 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage403)) : this.formError ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage)) : (h("span", null))), !this.formSuccess && !this.formError && (this.recaptchaEnabled || this.footerText) && (h("div", { class: "feedback-modal-footer", part: "footer" }, h("div", { class: "feedback-footer-combined" }, this.recaptchaEnabled && h("span", { innerHTML: this.recaptchaText }), this.recaptchaEnabled && this.footerText && ' ', this.footerText && h("span", { innerHTML: this.footerText }))))))));
|
|
502
|
+
: this.screenshotButtonText)), h("button", { class: "feedback-modal-button feedback-modal-button--submit", part: "submit-button", type: "submit", disabled: this.sending }, this.sendButtonText)))) : this.formSuccess && !this.formError ? (h("div", { class: "feedback-modal-success" }, h("p", { class: "feedback-modal-message", part: "success-message" }, this.successMessage))) : this.formError && this.formErrorStatus == 404 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage404)) : this.formError && this.formErrorStatus == 401 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage401)) : this.formError && this.formErrorStatus == 403 ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage403)) : this.formError ? (h("p", { class: "feedback-modal-message", part: "error-message" }, this.errorMessage)) : (h("span", null))), !this.formSuccess && !this.formError && (this.recaptchaEnabled || this.footerText) && (h("div", { class: "feedback-modal-footer", part: "footer" }, h("div", { class: "feedback-footer-combined" }, this.recaptchaEnabled && (h("span", { innerHTML: sanitizeHtml(this.recaptchaText) })), this.recaptchaEnabled && this.footerText && ' ', this.footerText && h("span", { innerHTML: sanitizeHtml(this.footerText) }))))))));
|
|
383
503
|
}
|
|
384
504
|
componentDidRender() {
|
|
385
505
|
if (this.showModal) {
|
|
@@ -389,7 +509,13 @@ export class FeedbackModal {
|
|
|
389
509
|
}
|
|
390
510
|
}
|
|
391
511
|
async openModal() {
|
|
512
|
+
// Remember the trigger so focus can return there when the modal closes
|
|
513
|
+
if (!this.embedded) {
|
|
514
|
+
const active = document.activeElement;
|
|
515
|
+
this.previouslyFocused = active && active !== document.body ? active : null;
|
|
516
|
+
}
|
|
392
517
|
this.showModal = true;
|
|
518
|
+
this.focusModal();
|
|
393
519
|
if (this.historyClose && !this.embedded && !this.historyEntryPushed) {
|
|
394
520
|
history.pushState({ feedbackModal: true }, '');
|
|
395
521
|
this.historyEntryPushed = true;
|
|
@@ -414,6 +540,24 @@ export class FeedbackModal {
|
|
|
414
540
|
}
|
|
415
541
|
static get properties() {
|
|
416
542
|
return {
|
|
543
|
+
"apiKey": {
|
|
544
|
+
"type": "string",
|
|
545
|
+
"mutable": false,
|
|
546
|
+
"complexType": {
|
|
547
|
+
"original": "string",
|
|
548
|
+
"resolved": "string",
|
|
549
|
+
"references": {}
|
|
550
|
+
},
|
|
551
|
+
"required": false,
|
|
552
|
+
"optional": false,
|
|
553
|
+
"docs": {
|
|
554
|
+
"tags": [],
|
|
555
|
+
"text": "Optional API key sent with feedback submissions, for projects that\nrequire one. Sent as `Authorization: Api-Key <key>` on the feedback POST\nrequest only, never on the public config request. Only use keys scoped to\n`feedback:submit`: like any credential used by an embedded widget, the\nvalue is part of the page and readable by any script running on it, so it\nprovides server-side scoping and traceability rather than client-side\nsecrecy. Intended for internal or access-controlled pages."
|
|
556
|
+
},
|
|
557
|
+
"attribute": "api-key",
|
|
558
|
+
"reflect": false,
|
|
559
|
+
"defaultValue": "''"
|
|
560
|
+
},
|
|
417
561
|
"customFont": {
|
|
418
562
|
"type": "boolean",
|
|
419
563
|
"mutable": false,
|
|
@@ -754,6 +898,24 @@ export class FeedbackModal {
|
|
|
754
898
|
"reflect": false,
|
|
755
899
|
"defaultValue": "true"
|
|
756
900
|
},
|
|
901
|
+
"escapeClose": {
|
|
902
|
+
"type": "boolean",
|
|
903
|
+
"mutable": false,
|
|
904
|
+
"complexType": {
|
|
905
|
+
"original": "boolean",
|
|
906
|
+
"resolved": "boolean",
|
|
907
|
+
"references": {}
|
|
908
|
+
},
|
|
909
|
+
"required": false,
|
|
910
|
+
"optional": false,
|
|
911
|
+
"docs": {
|
|
912
|
+
"tags": [],
|
|
913
|
+
"text": ""
|
|
914
|
+
},
|
|
915
|
+
"attribute": "escape-close",
|
|
916
|
+
"reflect": false,
|
|
917
|
+
"defaultValue": "true"
|
|
918
|
+
},
|
|
757
919
|
"emailPlaceholder": {
|
|
758
920
|
"type": "string",
|
|
759
921
|
"mutable": false,
|
|
@@ -790,6 +952,24 @@ export class FeedbackModal {
|
|
|
790
952
|
"reflect": false,
|
|
791
953
|
"defaultValue": "'Please try again later.'"
|
|
792
954
|
},
|
|
955
|
+
"errorMessage401": {
|
|
956
|
+
"type": "string",
|
|
957
|
+
"mutable": false,
|
|
958
|
+
"complexType": {
|
|
959
|
+
"original": "string",
|
|
960
|
+
"resolved": "string",
|
|
961
|
+
"references": {}
|
|
962
|
+
},
|
|
963
|
+
"required": false,
|
|
964
|
+
"optional": false,
|
|
965
|
+
"docs": {
|
|
966
|
+
"tags": [],
|
|
967
|
+
"text": ""
|
|
968
|
+
},
|
|
969
|
+
"attribute": "error-message-4-0-1",
|
|
970
|
+
"reflect": false,
|
|
971
|
+
"defaultValue": "'This project requires a valid API key to submit feedback. Check the api-key attribute.'"
|
|
972
|
+
},
|
|
793
973
|
"errorMessage403": {
|
|
794
974
|
"type": "string",
|
|
795
975
|
"mutable": false,
|
|
@@ -1006,6 +1186,78 @@ export class FeedbackModal {
|
|
|
1006
1186
|
"reflect": false,
|
|
1007
1187
|
"defaultValue": "'Send'"
|
|
1008
1188
|
},
|
|
1189
|
+
"closeButtonLabel": {
|
|
1190
|
+
"type": "string",
|
|
1191
|
+
"mutable": false,
|
|
1192
|
+
"complexType": {
|
|
1193
|
+
"original": "string",
|
|
1194
|
+
"resolved": "string",
|
|
1195
|
+
"references": {}
|
|
1196
|
+
},
|
|
1197
|
+
"required": false,
|
|
1198
|
+
"optional": false,
|
|
1199
|
+
"docs": {
|
|
1200
|
+
"tags": [],
|
|
1201
|
+
"text": ""
|
|
1202
|
+
},
|
|
1203
|
+
"attribute": "close-button-label",
|
|
1204
|
+
"reflect": false,
|
|
1205
|
+
"defaultValue": "'Close feedback form'"
|
|
1206
|
+
},
|
|
1207
|
+
"ratingPositiveLabel": {
|
|
1208
|
+
"type": "string",
|
|
1209
|
+
"mutable": false,
|
|
1210
|
+
"complexType": {
|
|
1211
|
+
"original": "string",
|
|
1212
|
+
"resolved": "string",
|
|
1213
|
+
"references": {}
|
|
1214
|
+
},
|
|
1215
|
+
"required": false,
|
|
1216
|
+
"optional": false,
|
|
1217
|
+
"docs": {
|
|
1218
|
+
"tags": [],
|
|
1219
|
+
"text": ""
|
|
1220
|
+
},
|
|
1221
|
+
"attribute": "rating-positive-label",
|
|
1222
|
+
"reflect": false,
|
|
1223
|
+
"defaultValue": "'Yes'"
|
|
1224
|
+
},
|
|
1225
|
+
"ratingNegativeLabel": {
|
|
1226
|
+
"type": "string",
|
|
1227
|
+
"mutable": false,
|
|
1228
|
+
"complexType": {
|
|
1229
|
+
"original": "string",
|
|
1230
|
+
"resolved": "string",
|
|
1231
|
+
"references": {}
|
|
1232
|
+
},
|
|
1233
|
+
"required": false,
|
|
1234
|
+
"optional": false,
|
|
1235
|
+
"docs": {
|
|
1236
|
+
"tags": [],
|
|
1237
|
+
"text": ""
|
|
1238
|
+
},
|
|
1239
|
+
"attribute": "rating-negative-label",
|
|
1240
|
+
"reflect": false,
|
|
1241
|
+
"defaultValue": "'No'"
|
|
1242
|
+
},
|
|
1243
|
+
"ratingStarLabel": {
|
|
1244
|
+
"type": "string",
|
|
1245
|
+
"mutable": false,
|
|
1246
|
+
"complexType": {
|
|
1247
|
+
"original": "string",
|
|
1248
|
+
"resolved": "string",
|
|
1249
|
+
"references": {}
|
|
1250
|
+
},
|
|
1251
|
+
"required": false,
|
|
1252
|
+
"optional": false,
|
|
1253
|
+
"docs": {
|
|
1254
|
+
"tags": [],
|
|
1255
|
+
"text": ""
|
|
1256
|
+
},
|
|
1257
|
+
"attribute": "rating-star-label",
|
|
1258
|
+
"reflect": false,
|
|
1259
|
+
"defaultValue": "'Rate {rating} out of 5'"
|
|
1260
|
+
},
|
|
1009
1261
|
"successMessage": {
|
|
1010
1262
|
"type": "string",
|
|
1011
1263
|
"mutable": false,
|
|
@@ -1416,6 +1668,9 @@ export class FeedbackModal {
|
|
|
1416
1668
|
"references": {
|
|
1417
1669
|
"Promise": {
|
|
1418
1670
|
"location": "global"
|
|
1671
|
+
},
|
|
1672
|
+
"HTMLElement": {
|
|
1673
|
+
"location": "global"
|
|
1419
1674
|
}
|
|
1420
1675
|
},
|
|
1421
1676
|
"return": "Promise<void>"
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Tags allowed in the rich-text props (privacy policy, reCAPTCHA notice,
|
|
2
|
+
// footer). These only ever need links and light inline emphasis.
|
|
3
|
+
const ALLOWED_TAGS = new Set([
|
|
4
|
+
'A',
|
|
5
|
+
'B',
|
|
6
|
+
'BR',
|
|
7
|
+
'CODE',
|
|
8
|
+
'EM',
|
|
9
|
+
'I',
|
|
10
|
+
'P',
|
|
11
|
+
'SMALL',
|
|
12
|
+
'SPAN',
|
|
13
|
+
'STRONG',
|
|
14
|
+
'U',
|
|
15
|
+
]);
|
|
16
|
+
// No `class`: an injected class name cannot reach the host page's CSS (this
|
|
17
|
+
// renders inside a shadow root), but it could match the widget's own internal
|
|
18
|
+
// styles. The default prop values do not use classes, so dropping it is free.
|
|
19
|
+
const ALLOWED_ATTRS = new Set(['href', 'target', 'rel', 'title']);
|
|
20
|
+
// Schemes permitted in href. Anything else, notably javascript:, is dropped.
|
|
21
|
+
const SAFE_URL = /^(https?:|mailto:|tel:|#|\/|\.\/|\.\.\/)/i;
|
|
22
|
+
/**
|
|
23
|
+
* Removes anything that can execute from a fragment: script and style
|
|
24
|
+
* elements, event-handler attributes, javascript: URLs, and any tag not on the
|
|
25
|
+
* allowlist. Text inside a disallowed tag is kept, so stripping a wrapper does
|
|
26
|
+
* not silently delete the words inside it.
|
|
27
|
+
*/
|
|
28
|
+
function scrub(root) {
|
|
29
|
+
const elements = Array.from(root.querySelectorAll('*'));
|
|
30
|
+
for (const el of elements) {
|
|
31
|
+
// The node may already have been removed along with an ancestor
|
|
32
|
+
if (!root.contains(el)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const tag = el.tagName.toUpperCase();
|
|
36
|
+
if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'IFRAME' || tag === 'OBJECT') {
|
|
37
|
+
el.remove();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!ALLOWED_TAGS.has(tag)) {
|
|
41
|
+
// Unwrap: keep the text, drop the element
|
|
42
|
+
el.replaceWith(...Array.from(el.childNodes));
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
for (const attr of Array.from(el.attributes)) {
|
|
46
|
+
const name = attr.name.toLowerCase();
|
|
47
|
+
if (name.startsWith('on') || !ALLOWED_ATTRS.has(name)) {
|
|
48
|
+
el.removeAttribute(attr.name);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (name === 'href' && !SAFE_URL.test(attr.value.trim())) {
|
|
52
|
+
el.removeAttribute(attr.name);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Links opening a new tab must not hand the opener over
|
|
56
|
+
if (tag === 'A' && el.getAttribute('target') === '_blank') {
|
|
57
|
+
el.setAttribute('rel', 'noopener noreferrer');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Sanitizes a fragment of HTML for use with innerHTML.
|
|
63
|
+
*
|
|
64
|
+
* Prefers the browser's native Element.setHTML() where available, which parses
|
|
65
|
+
* without executing. Otherwise parses in an inert document (no scripts run, no
|
|
66
|
+
* images or other subresources are fetched) and scrubs the result.
|
|
67
|
+
*/
|
|
68
|
+
export function sanitizeHtml(html) {
|
|
69
|
+
if (!html) {
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
const template = document.createElement('template');
|
|
73
|
+
// Native sanitizer, when the browser has it
|
|
74
|
+
const withSetHtml = template;
|
|
75
|
+
if (typeof withSetHtml.setHTML === 'function') {
|
|
76
|
+
try {
|
|
77
|
+
withSetHtml.setHTML(html);
|
|
78
|
+
scrub(template.content);
|
|
79
|
+
return template.innerHTML;
|
|
80
|
+
}
|
|
81
|
+
catch (_a) {
|
|
82
|
+
// Fall through to the manual path
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Inert parsing: content inside <template> is not rendered, so scripts do not
|
|
86
|
+
// run and no subresource requests are made while we clean it up.
|
|
87
|
+
template.innerHTML = html;
|
|
88
|
+
scrub(template.content);
|
|
89
|
+
return template.innerHTML;
|
|
90
|
+
}
|