@product7/feedback-sdk 1.0.5 → 1.0.7

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.
@@ -11,12 +11,14 @@ export class BaseWidget {
11
11
  theme: this.sdk.config.theme,
12
12
  boardId: this.sdk.config.boardId,
13
13
  autoShow: false,
14
+ showBackdrop: true,
14
15
  customStyles: {},
15
16
  ...options,
16
17
  };
17
18
 
18
19
  this.element = null;
19
- this.modalElement = null;
20
+ this.panelElement = null;
21
+ this.backdropElement = null;
20
22
  this.mounted = false;
21
23
  this.destroyed = false;
22
24
 
@@ -74,23 +76,47 @@ export class BaseWidget {
74
76
  return this;
75
77
  }
76
78
 
77
- openModal() {
79
+ openPanel() {
78
80
  this.state.isOpen = true;
79
- this._renderModal();
81
+ this._renderPanel();
82
+
83
+ requestAnimationFrame(() => {
84
+ if (this.panelElement) {
85
+ this.panelElement.classList.add('open');
86
+ }
87
+ if (this.backdropElement) {
88
+ this.backdropElement.classList.add('show');
89
+ }
90
+ });
80
91
  }
81
92
 
82
- closeModal() {
83
- this.state.isOpen = false;
84
- if (this.modalElement) {
85
- this.modalElement.remove();
86
- this.modalElement = null;
93
+ closePanel() {
94
+ if (this.panelElement) {
95
+ this.panelElement.classList.remove('open');
87
96
  }
88
- this._resetForm();
97
+ if (this.backdropElement) {
98
+ this.backdropElement.classList.remove('show');
99
+ }
100
+
101
+ setTimeout(() => {
102
+ this.state.isOpen = false;
103
+ if (this.panelElement && this.panelElement.parentNode) {
104
+ this.panelElement.parentNode.removeChild(this.panelElement);
105
+ this.panelElement = null;
106
+ }
107
+ if (this.backdropElement && this.backdropElement.parentNode) {
108
+ this.backdropElement.parentNode.removeChild(this.backdropElement);
109
+ this.backdropElement = null;
110
+ }
111
+ this._resetForm();
112
+ }, 300);
89
113
  }
90
114
 
91
115
  async submitFeedback() {
92
116
  if (this.state.isSubmitting) return;
93
117
 
118
+ this._hideError();
119
+
94
120
  try {
95
121
  this.state.isSubmitting = true;
96
122
  this._updateSubmitButton();
@@ -111,7 +137,7 @@ export class BaseWidget {
111
137
  const response = await this.apiService.submitFeedback(payload);
112
138
 
113
139
  this._showSuccessMessage();
114
- this.closeModal();
140
+ this.closePanel();
115
141
 
116
142
  this.sdk.eventBus.emit('feedback:submitted', {
117
143
  widget: this,
@@ -137,7 +163,7 @@ export class BaseWidget {
137
163
  if (this.destroyed) return;
138
164
 
139
165
  this.onDestroy();
140
- this.closeModal();
166
+ this.closePanel();
141
167
 
142
168
  if (this.element && this.element.parentNode) {
143
169
  this.element.parentNode.removeChild(this.element);
@@ -160,38 +186,46 @@ export class BaseWidget {
160
186
  }
161
187
 
162
188
  _bindMethods() {
163
- this.openModal = this.openModal.bind(this);
164
- this.closeModal = this.closeModal.bind(this);
189
+ this.openPanel = this.openPanel.bind(this);
190
+ this.closePanel = this.closePanel.bind(this);
165
191
  this.submitFeedback = this.submitFeedback.bind(this);
166
192
  }
167
193
 
168
- _renderModal() {
169
- if (this.modalElement) return;
194
+ _renderPanel() {
195
+ if (this.panelElement) return;
196
+
197
+ if (this.options.showBackdrop) {
198
+ this.backdropElement = document.createElement('div');
199
+ this.backdropElement.className = 'feedback-panel-backdrop';
200
+ document.body.appendChild(this.backdropElement);
201
+
202
+ this.backdropElement.addEventListener('click', this.closePanel);
203
+ }
170
204
 
171
- this.modalElement = document.createElement('div');
172
- this.modalElement.className = `feedback-modal theme-${this.options.theme}`;
173
- this.modalElement.innerHTML = this._getModalHTML();
205
+ this.panelElement = document.createElement('div');
206
+ this.panelElement.className = `feedback-panel theme-${this.options.theme}`;
207
+ this.panelElement.innerHTML = this._getPanelHTML();
174
208
 
175
- document.body.appendChild(this.modalElement);
176
- this._attachModalEvents();
209
+ document.body.appendChild(this.panelElement);
210
+ this._attachPanelEvents();
177
211
 
178
- const firstInput = this.modalElement.querySelector('input, textarea');
212
+ const firstInput = this.panelElement.querySelector('input, textarea');
179
213
  if (firstInput) {
180
- setTimeout(() => firstInput.focus(), 100);
214
+ setTimeout(() => firstInput.focus(), 350);
181
215
  }
182
216
  }
183
217
 
184
- _getModalHTML() {
218
+ _getPanelHTML() {
185
219
  return `
186
- <div class="feedback-modal-overlay">
187
- <div class="feedback-modal-content">
188
- <div class="feedback-modal-header">
189
- <h3>Send Feedback</h3>
190
- <button class="feedback-modal-close" type="button">&times;</button>
191
- </div>
220
+ <div class="feedback-panel-content">
221
+ <div class="feedback-panel-header">
222
+ <h3>Send Feedback</h3>
223
+ <button class="feedback-panel-close" type="button" aria-label="Close">&times;</button>
224
+ </div>
225
+ <div class="feedback-panel-body">
192
226
  <form class="feedback-form">
193
227
  <div class="feedback-form-group">
194
- <label for="feedback-title-${this.id}">Title</label>
228
+ <label for="feedback-title-${this.id}">Title (optional)</label>
195
229
  <input
196
230
  type="text"
197
231
  id="feedback-title-${this.id}"
@@ -205,62 +239,59 @@ export class BaseWidget {
205
239
  <textarea
206
240
  id="feedback-content-${this.id}"
207
241
  name="content"
208
- placeholder="Tell us more about your feedback..."
242
+ placeholder="Tell us what you think..."
209
243
  required
210
244
  >${this.state.content}</textarea>
211
245
  </div>
246
+ <div class="feedback-error" role="alert"></div>
212
247
  <div class="feedback-form-actions">
213
- <button type="button" class="feedback-btn feedback-btn-cancel">Cancel</button>
214
248
  <button type="submit" class="feedback-btn feedback-btn-submit">
215
249
  ${this.state.isSubmitting ? 'Sending...' : 'Send Feedback'}
216
250
  </button>
217
251
  </div>
218
- <div class="feedback-error" style="display: none;"></div>
219
252
  </form>
220
253
  </div>
221
254
  </div>
222
255
  `;
223
256
  }
224
257
 
225
- _attachModalEvents() {
226
- const modal = this.modalElement;
227
-
228
- modal
229
- .querySelector('.feedback-modal-close')
230
- .addEventListener('click', this.closeModal);
231
- modal
232
- .querySelector('.feedback-btn-cancel')
233
- .addEventListener('click', this.closeModal);
234
- modal
235
- .querySelector('.feedback-modal-overlay')
236
- .addEventListener('click', (e) => {
237
- if (e.target === e.currentTarget) {
238
- this.closeModal();
239
- }
240
- });
258
+ _attachPanelEvents() {
259
+ const panel = this.panelElement;
241
260
 
242
- const form = modal.querySelector('.feedback-form');
261
+ panel
262
+ .querySelector('.feedback-panel-close')
263
+ .addEventListener('click', this.closePanel);
264
+
265
+ const form = panel.querySelector('.feedback-form');
243
266
  form.addEventListener('submit', (e) => {
244
267
  e.preventDefault();
245
268
  this.submitFeedback();
246
269
  });
247
270
 
248
- modal
271
+ panel
249
272
  .querySelector('input[name="title"]')
250
273
  .addEventListener('input', (e) => {
251
274
  this.state.title = e.target.value;
252
275
  });
253
276
 
254
- modal
277
+ panel
255
278
  .querySelector('textarea[name="content"]')
256
279
  .addEventListener('input', (e) => {
257
280
  this.state.content = e.target.value;
258
281
  });
282
+
283
+ const handleEscape = (e) => {
284
+ if (e.key === 'Escape') {
285
+ this.closePanel();
286
+ document.removeEventListener('keydown', handleEscape);
287
+ }
288
+ };
289
+ document.addEventListener('keydown', handleEscape);
259
290
  }
260
291
 
261
292
  _updateSubmitButton() {
262
- if (this.modalElement) {
263
- const submitBtn = this.modalElement.querySelector('.feedback-btn-submit');
293
+ if (this.panelElement) {
294
+ const submitBtn = this.panelElement.querySelector('.feedback-btn-submit');
264
295
  if (submitBtn) {
265
296
  submitBtn.textContent = this.state.isSubmitting
266
297
  ? 'Sending...'
@@ -271,15 +302,21 @@ export class BaseWidget {
271
302
  }
272
303
 
273
304
  _showError(message) {
274
- if (this.modalElement) {
275
- const errorElement = this.modalElement.querySelector('.feedback-error');
276
- errorElement.textContent = message;
277
- errorElement.style.display = 'block';
278
- setTimeout(() => {
279
- if (errorElement) {
280
- errorElement.style.display = 'none';
281
- }
282
- }, 5000);
305
+ if (this.panelElement) {
306
+ const errorElement = this.panelElement.querySelector('.feedback-error');
307
+ if (errorElement) {
308
+ errorElement.textContent = message;
309
+ errorElement.classList.add('show');
310
+ }
311
+ }
312
+ }
313
+
314
+ _hideError() {
315
+ if (this.panelElement) {
316
+ const errorElement = this.panelElement.querySelector('.feedback-error');
317
+ if (errorElement) {
318
+ errorElement.classList.remove('show');
319
+ }
283
320
  }
284
321
  }
285
322
 
@@ -288,26 +325,29 @@ export class BaseWidget {
288
325
  notification.className = 'feedback-success-notification';
289
326
  notification.innerHTML = `
290
327
  <div class="feedback-success-content">
291
- <span>✓ Feedback submitted successfully!</span>
292
- <button class="feedback-success-close">&times;</button>
328
+ <div class="feedback-success-icon">✓</div>
329
+ <span>Feedback submitted successfully!</span>
330
+ <button class="feedback-success-close" aria-label="Close">&times;</button>
293
331
  </div>
294
332
  `;
295
333
 
296
334
  document.body.appendChild(notification);
297
335
 
298
- setTimeout(() => {
336
+ const closeBtn = notification.querySelector('.feedback-success-close');
337
+ const closeNotification = () => {
299
338
  if (notification.parentNode) {
300
- notification.parentNode.removeChild(notification);
339
+ notification.style.opacity = '0';
340
+ setTimeout(() => {
341
+ if (notification.parentNode) {
342
+ notification.parentNode.removeChild(notification);
343
+ }
344
+ }, 300);
301
345
  }
302
- }, 3000);
303
-
304
- notification
305
- .querySelector('.feedback-success-close')
306
- .addEventListener('click', () => {
307
- if (notification.parentNode) {
308
- notification.parentNode.removeChild(notification);
309
- }
310
- });
346
+ };
347
+
348
+ closeBtn.addEventListener('click', closeNotification);
349
+
350
+ setTimeout(closeNotification, 4000);
311
351
  }
312
352
 
313
353
  _resetForm() {
@@ -324,11 +364,19 @@ export class BaseWidget {
324
364
  `theme-${this.options.theme}`
325
365
  );
326
366
  }
327
- if (this.modalElement) {
328
- this.modalElement.className = this.modalElement.className.replace(
367
+ if (this.panelElement) {
368
+ this.panelElement.className = this.panelElement.className.replace(
329
369
  /theme-\w+/,
330
370
  `theme-${this.options.theme}`
331
371
  );
332
372
  }
333
373
  }
334
- }
374
+
375
+ openModal() {
376
+ this.openPanel();
377
+ }
378
+
379
+ closeModal() {
380
+ this.closePanel();
381
+ }
382
+ }
@@ -27,7 +27,7 @@ export class ButtonWidget extends BaseWidget {
27
27
 
28
28
  _attachEvents() {
29
29
  const button = this.element.querySelector('.feedback-trigger-btn');
30
- button.addEventListener('click', this.openModal);
30
+ button.addEventListener('click', this.openPanel);
31
31
 
32
32
  button.addEventListener('mouseenter', () => {
33
33
  if (!this.state.isSubmitting) {
@@ -59,4 +59,4 @@ export class ButtonWidget extends BaseWidget {
59
59
  );
60
60
  }
61
61
  }
62
- }
62
+ }