releasebird-javascript-sdk 1.0.73 → 1.0.76

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.
@@ -0,0 +1,567 @@
1
+ import RbirdSessionManager from './RbirdSessionManager';
2
+
3
+ // API is defined globally via webpack.DefinePlugin
4
+ /* global API */
5
+
6
+ export class RbirdFormManager {
7
+ static instance;
8
+
9
+ constructor() {
10
+ this.displayedForms = new Set();
11
+ this.apiKey = null;
12
+ }
13
+
14
+ static getInstance() {
15
+ if (!RbirdFormManager.instance) {
16
+ RbirdFormManager.instance = new RbirdFormManager();
17
+ }
18
+ return RbirdFormManager.instance;
19
+ }
20
+
21
+ init(apiKey) {
22
+ if (typeof window === 'undefined') return;
23
+ this.apiKey = apiKey;
24
+ this.injectStyles();
25
+ }
26
+
27
+ /**
28
+ * Show a form by its ID
29
+ * @param {string} formId - The ID of the form to show
30
+ * @param {Object} options - Optional configuration
31
+ * @param {string} options.target - CSS selector for inline rendering (optional)
32
+ * @param {Function} options.onSubmit - Callback when form is submitted
33
+ * @param {Function} options.onClose - Callback when form is closed
34
+ */
35
+ showForm(formId, options = {}) {
36
+ if (typeof window === 'undefined') return;
37
+ if (!this.apiKey) {
38
+ console.error('[RbirdFormManager] SDK not initialized. Call Rbird.initialize() first.');
39
+ return;
40
+ }
41
+ if (!formId) {
42
+ console.error('[RbirdFormManager] Form ID is required.');
43
+ return;
44
+ }
45
+
46
+ // Check if form is already displayed
47
+ if (this.displayedForms.has(formId)) {
48
+ console.warn('[RbirdFormManager] Form is already displayed:', formId);
49
+ return;
50
+ }
51
+
52
+ const sessionManager = RbirdSessionManager.getInstance();
53
+ const state = sessionManager.getState();
54
+
55
+ const http = new XMLHttpRequest();
56
+ http.open("GET", `${API}/ewidget/forms/${formId}`);
57
+ http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
58
+ http.setRequestHeader("apiKey", this.apiKey);
59
+
60
+ if (state?.identify?.people) {
61
+ http.setRequestHeader("peopleId", state.identify.people);
62
+ }
63
+ if (sessionManager.anonymousIdentifier) {
64
+ http.setRequestHeader("anonymousId", sessionManager.anonymousIdentifier);
65
+ }
66
+
67
+ const self = this;
68
+ http.onreadystatechange = function () {
69
+ if (http.readyState === 4) {
70
+ if (http.status === 200) {
71
+ try {
72
+ const form = JSON.parse(http.responseText);
73
+ if (options.target) {
74
+ self.renderInlineForm(form, options);
75
+ } else {
76
+ self.renderModalForm(form, options);
77
+ }
78
+ } catch (exp) {
79
+ console.error('[RbirdFormManager] Error parsing form response:', exp);
80
+ }
81
+ } else {
82
+ console.error('[RbirdFormManager] Failed to fetch form. Status:', http.status);
83
+ }
84
+ }
85
+ };
86
+ http.send();
87
+ }
88
+
89
+ renderInlineForm(form, options) {
90
+ const targetElement = document.querySelector(options.target);
91
+ if (!targetElement) {
92
+ console.error('[RbirdFormManager] Target element not found:', options.target);
93
+ return;
94
+ }
95
+
96
+ const formElement = this.createFormElement(form, false, options);
97
+ targetElement.innerHTML = '';
98
+ targetElement.appendChild(formElement);
99
+ this.displayedForms.add(form.id);
100
+ }
101
+
102
+ renderModalForm(form, options) {
103
+ const overlay = document.createElement('div');
104
+ overlay.className = 'rbird-form-overlay';
105
+ overlay.id = `rbird-form-overlay-${form.id}`;
106
+
107
+ const modal = this.createFormElement(form, true, options);
108
+ overlay.appendChild(modal);
109
+
110
+ // Close on overlay click
111
+ overlay.addEventListener('click', (e) => {
112
+ if (e.target === overlay) {
113
+ this.closeForm(form.id, options.onClose);
114
+ }
115
+ });
116
+
117
+ document.body.appendChild(overlay);
118
+ this.displayedForms.add(form.id);
119
+
120
+ // Animate in
121
+ requestAnimationFrame(() => {
122
+ overlay.classList.add('rbird-form-overlay-visible');
123
+ });
124
+ }
125
+
126
+ createFormElement(form, isModal, options) {
127
+ const container = document.createElement('div');
128
+ container.className = isModal ? 'rbird-form-modal' : 'rbird-form-inline';
129
+ container.id = `rbird-form-${form.id}`;
130
+
131
+ const theme = form.theme || {};
132
+
133
+ // Build form HTML
134
+ let fieldsHtml = '';
135
+ (form.fields || []).forEach(field => {
136
+ fieldsHtml += this.createFieldHtml(field, theme);
137
+ });
138
+
139
+ container.innerHTML = `
140
+ <div class="rbird-form-content" style="${this.getFormStyles(theme, isModal)}">
141
+ ${isModal ? `
142
+ <button class="rbird-form-close" data-form-id="${form.id}" style="${this.getCloseButtonStyles()}">
143
+ ×
144
+ </button>
145
+ ` : ''}
146
+ <div class="rbird-form-body">
147
+ ${form.title ? `<h3 class="rbird-form-title" style="${this.getTitleStyles(theme)}">${this.escapeHtml(form.title)}</h3>` : ''}
148
+ ${form.description ? `<p class="rbird-form-description" style="${this.getDescriptionStyles()}">${this.escapeHtml(form.description)}</p>` : ''}
149
+ <form class="rbird-form-fields" data-form-id="${form.id}">
150
+ ${fieldsHtml}
151
+ <button type="submit" class="rbird-form-submit" style="${this.getSubmitButtonStyles(theme)}">
152
+ ${this.escapeHtml(form.submitButtonText || 'Submit')}
153
+ </button>
154
+ </form>
155
+ <div class="rbird-form-success" style="display: none;">
156
+ <p style="${this.getSuccessStyles()}">${this.escapeHtml(form.successMessage || 'Thank you for your submission!')}</p>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ `;
161
+
162
+ // Add event listeners
163
+ if (isModal) {
164
+ const closeButton = container.querySelector('.rbird-form-close');
165
+ if (closeButton) {
166
+ closeButton.addEventListener('click', (e) => {
167
+ e.preventDefault();
168
+ this.closeForm(form.id, options.onClose);
169
+ });
170
+ }
171
+ }
172
+
173
+ const formElement = container.querySelector('form');
174
+ if (formElement) {
175
+ formElement.addEventListener('submit', (e) => {
176
+ e.preventDefault();
177
+ this.handleSubmit(form.id, formElement, options);
178
+ });
179
+ }
180
+
181
+ return container;
182
+ }
183
+
184
+ createFieldHtml(field, theme) {
185
+ const required = field.required ? 'required' : '';
186
+ const requiredMark = field.required ? '<span style="color: #dc2626;">*</span>' : '';
187
+ const inputStyle = this.getInputStyles(theme);
188
+
189
+ let html = `<div class="rbird-form-field" style="margin-bottom: 16px;">`;
190
+
191
+ if (field.label) {
192
+ html += `<label style="${this.getLabelStyles()}">${this.escapeHtml(field.label)} ${requiredMark}</label>`;
193
+ }
194
+
195
+ switch (field.type) {
196
+ case 'TEXT':
197
+ case 'EMAIL':
198
+ case 'PHONE':
199
+ case 'NUMBER':
200
+ case 'DATE':
201
+ html += `<input type="${this.getInputType(field.type)}" name="${field.id}"
202
+ placeholder="${this.escapeHtml(field.placeholder || '')}"
203
+ ${required} style="${inputStyle}" />`;
204
+ break;
205
+ case 'TEXTAREA':
206
+ html += `<textarea name="${field.id}" rows="4"
207
+ placeholder="${this.escapeHtml(field.placeholder || '')}"
208
+ ${required} style="${inputStyle}"></textarea>`;
209
+ break;
210
+ case 'CHECKBOX':
211
+ if (field.options && field.options.length > 0) {
212
+ field.options.forEach((option, index) => {
213
+ html += `<label style="${this.getCheckboxLabelStyles()}">
214
+ <input type="checkbox" name="${field.id}" value="${this.escapeHtml(option)}" />
215
+ ${this.escapeHtml(option)}
216
+ </label>`;
217
+ });
218
+ } else {
219
+ html += `<label style="${this.getCheckboxLabelStyles()}">
220
+ <input type="checkbox" name="${field.id}" ${required} />
221
+ ${this.escapeHtml(field.label || '')}
222
+ </label>`;
223
+ }
224
+ break;
225
+ case 'RADIO':
226
+ (field.options || []).forEach((option, index) => {
227
+ html += `<label style="${this.getCheckboxLabelStyles()}">
228
+ <input type="radio" name="${field.id}" value="${this.escapeHtml(option)}" ${required && index === 0 ? required : ''} />
229
+ ${this.escapeHtml(option)}
230
+ </label>`;
231
+ });
232
+ break;
233
+ case 'DROPDOWN':
234
+ html += `<select name="${field.id}" ${required} style="${inputStyle}">
235
+ <option value="">${this.escapeHtml(field.placeholder || 'Select an option')}</option>
236
+ ${(field.options || []).map(opt => `<option value="${this.escapeHtml(opt)}">${this.escapeHtml(opt)}</option>`).join('')}
237
+ </select>`;
238
+ break;
239
+ case 'FILE':
240
+ html += `<input type="file" name="${field.id}" ${required} style="${inputStyle}" />`;
241
+ break;
242
+ default:
243
+ html += `<input type="text" name="${field.id}"
244
+ placeholder="${this.escapeHtml(field.placeholder || '')}"
245
+ ${required} style="${inputStyle}" />`;
246
+ }
247
+
248
+ if (field.helpText) {
249
+ html += `<small style="${this.getHelpTextStyles()}">${this.escapeHtml(field.helpText)}</small>`;
250
+ }
251
+
252
+ html += `</div>`;
253
+ return html;
254
+ }
255
+
256
+ getInputType(fieldType) {
257
+ const typeMap = {
258
+ 'TEXT': 'text',
259
+ 'EMAIL': 'email',
260
+ 'PHONE': 'tel',
261
+ 'NUMBER': 'number',
262
+ 'DATE': 'date'
263
+ };
264
+ return typeMap[fieldType] || 'text';
265
+ }
266
+
267
+ handleSubmit(formId, formElement, options) {
268
+ const formData = new FormData(formElement);
269
+ const data = {};
270
+
271
+ formData.forEach((value, key) => {
272
+ if (data[key]) {
273
+ // Handle multiple values (checkboxes)
274
+ if (Array.isArray(data[key])) {
275
+ data[key].push(value);
276
+ } else {
277
+ data[key] = [data[key], value];
278
+ }
279
+ } else {
280
+ data[key] = value;
281
+ }
282
+ });
283
+
284
+ const sessionManager = RbirdSessionManager.getInstance();
285
+ const state = sessionManager.getState();
286
+
287
+ const http = new XMLHttpRequest();
288
+ http.open("POST", `${API}/ewidget/forms/${formId}/submit`);
289
+ http.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
290
+ http.setRequestHeader("apiKey", this.apiKey);
291
+
292
+ if (state?.identify?.people) {
293
+ http.setRequestHeader("peopleId", state.identify.people);
294
+ }
295
+ if (sessionManager.anonymousIdentifier) {
296
+ http.setRequestHeader("anonymousId", sessionManager.anonymousIdentifier);
297
+ }
298
+
299
+ // Send browser language
300
+ const browserLang = navigator.language ? navigator.language.split('-')[0] : 'en';
301
+ http.setRequestHeader("languageCode", browserLang);
302
+
303
+ const self = this;
304
+ const submitButton = formElement.querySelector('button[type="submit"]');
305
+ if (submitButton) {
306
+ submitButton.disabled = true;
307
+ submitButton.textContent = 'Sending...';
308
+ }
309
+
310
+ http.onreadystatechange = function () {
311
+ if (http.readyState === 4) {
312
+ if (http.status === 200 || http.status === 201) {
313
+ // Show success message
314
+ const container = document.getElementById(`rbird-form-${formId}`);
315
+ if (container) {
316
+ const form = container.querySelector('form');
317
+ const successMessage = container.querySelector('.rbird-form-success');
318
+ if (form) form.style.display = 'none';
319
+ if (successMessage) successMessage.style.display = 'block';
320
+ }
321
+
322
+ if (options.onSubmit) {
323
+ options.onSubmit(data);
324
+ }
325
+ } else {
326
+ console.error('[RbirdFormManager] Failed to submit form. Status:', http.status);
327
+ if (submitButton) {
328
+ submitButton.disabled = false;
329
+ submitButton.textContent = 'Submit';
330
+ }
331
+ alert('Failed to submit form. Please try again.');
332
+ }
333
+ }
334
+ };
335
+
336
+ http.send(JSON.stringify({
337
+ data: data,
338
+ userEmail: state?.identify?.email || null,
339
+ userName: state?.identify?.name || null
340
+ }));
341
+ }
342
+
343
+ closeForm(formId, onClose) {
344
+ const overlay = document.getElementById(`rbird-form-overlay-${formId}`);
345
+ if (overlay) {
346
+ overlay.classList.remove('rbird-form-overlay-visible');
347
+ setTimeout(() => {
348
+ overlay.remove();
349
+ }, 300);
350
+ }
351
+
352
+ const inlineForm = document.getElementById(`rbird-form-${formId}`);
353
+ if (inlineForm && !overlay) {
354
+ inlineForm.remove();
355
+ }
356
+
357
+ this.displayedForms.delete(formId);
358
+
359
+ if (onClose) {
360
+ onClose();
361
+ }
362
+ }
363
+
364
+ // Styles
365
+ getFormStyles(theme, isModal) {
366
+ return `
367
+ background-color: ${theme.backgroundColor || '#ffffff'};
368
+ font-family: ${theme.fontFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"};
369
+ border-radius: ${theme.borderRadius || '12px'};
370
+ padding: 24px;
371
+ min-width: min(400px, calc(100vw - 40px));
372
+ ${isModal ? 'max-width: 500px; width: 100%; max-height: 90vh; overflow-y: auto;' : 'width: 100%;'}
373
+ box-sizing: border-box;
374
+ `;
375
+ }
376
+
377
+ getCloseButtonStyles() {
378
+ return `
379
+ position: absolute;
380
+ top: 12px;
381
+ right: 12px;
382
+ background: transparent;
383
+ border: none;
384
+ font-size: 24px;
385
+ line-height: 1;
386
+ cursor: pointer;
387
+ color: #6b7280;
388
+ opacity: 0.6;
389
+ padding: 0;
390
+ width: 32px;
391
+ height: 32px;
392
+ display: flex;
393
+ align-items: center;
394
+ justify-content: center;
395
+ transition: opacity 0.2s;
396
+ `;
397
+ }
398
+
399
+ getTitleStyles(theme) {
400
+ return `
401
+ margin: 0 0 8px 0;
402
+ font-size: 20px;
403
+ font-weight: 600;
404
+ color: ${theme.primaryColor || '#1f2937'};
405
+ `;
406
+ }
407
+
408
+ getDescriptionStyles() {
409
+ return `
410
+ margin: 0 0 20px 0;
411
+ font-size: 14px;
412
+ color: #6b7280;
413
+ line-height: 1.5;
414
+ `;
415
+ }
416
+
417
+ getLabelStyles() {
418
+ return `
419
+ display: block;
420
+ margin-bottom: 6px;
421
+ font-size: 14px;
422
+ font-weight: 500;
423
+ color: #374151;
424
+ `;
425
+ }
426
+
427
+ getInputStyles(theme) {
428
+ return `
429
+ width: 100%;
430
+ padding: 10px 12px;
431
+ border: 1px solid ${theme.inputBorderColor || '#d1d5db'};
432
+ border-radius: ${theme.borderRadius || '8px'};
433
+ font-size: 14px;
434
+ transition: border-color 0.2s, box-shadow 0.2s;
435
+ box-sizing: border-box;
436
+ outline: none;
437
+ `;
438
+ }
439
+
440
+ getCheckboxLabelStyles() {
441
+ return `
442
+ display: flex;
443
+ align-items: center;
444
+ gap: 8px;
445
+ margin-bottom: 8px;
446
+ font-size: 14px;
447
+ color: #374151;
448
+ cursor: pointer;
449
+ `;
450
+ }
451
+
452
+ getHelpTextStyles() {
453
+ return `
454
+ display: block;
455
+ margin-top: 4px;
456
+ font-size: 12px;
457
+ color: #6b7280;
458
+ `;
459
+ }
460
+
461
+ getSubmitButtonStyles(theme) {
462
+ return `
463
+ width: 100%;
464
+ padding: 12px 20px;
465
+ background-color: ${theme.buttonColor || '#667eea'};
466
+ color: ${theme.buttonTextColor || '#ffffff'};
467
+ border: none;
468
+ border-radius: ${theme.borderRadius || '8px'};
469
+ font-size: 14px;
470
+ font-weight: 600;
471
+ cursor: pointer;
472
+ transition: opacity 0.2s, transform 0.2s;
473
+ margin-top: 8px;
474
+ `;
475
+ }
476
+
477
+ getSuccessStyles() {
478
+ return `
479
+ text-align: center;
480
+ padding: 40px 20px;
481
+ font-size: 16px;
482
+ color: #10b981;
483
+ `;
484
+ }
485
+
486
+ escapeHtml(text) {
487
+ if (!text) return '';
488
+ const div = document.createElement('div');
489
+ div.textContent = text;
490
+ return div.innerHTML;
491
+ }
492
+
493
+ injectStyles() {
494
+ if (typeof window === 'undefined' || document.getElementById('rbird-form-styles')) return;
495
+
496
+ const style = document.createElement('style');
497
+ style.id = 'rbird-form-styles';
498
+ style.textContent = `
499
+ .rbird-form-overlay {
500
+ position: fixed;
501
+ top: 0;
502
+ left: 0;
503
+ right: 0;
504
+ bottom: 0;
505
+ background-color: rgba(0, 0, 0, 0);
506
+ display: flex;
507
+ align-items: center;
508
+ justify-content: center;
509
+ z-index: 10001;
510
+ padding: 20px;
511
+ opacity: 0;
512
+ transition: background-color 0.3s, opacity 0.3s;
513
+ }
514
+
515
+ .rbird-form-overlay-visible {
516
+ background-color: rgba(0, 0, 0, 0.5);
517
+ opacity: 1;
518
+ }
519
+
520
+ .rbird-form-modal {
521
+ position: relative;
522
+ animation: rbirdFormSlideIn 0.3s ease-out;
523
+ }
524
+
525
+ .rbird-form-inline {
526
+ width: 100%;
527
+ }
528
+
529
+ .rbird-form-content {
530
+ position: relative;
531
+ }
532
+
533
+ .rbird-form-close:hover {
534
+ opacity: 1 !important;
535
+ }
536
+
537
+ .rbird-form-submit:hover {
538
+ opacity: 0.9;
539
+ transform: translateY(-1px);
540
+ }
541
+
542
+ .rbird-form-submit:disabled {
543
+ opacity: 0.6;
544
+ cursor: not-allowed;
545
+ }
546
+
547
+ .rbird-form-fields input:focus,
548
+ .rbird-form-fields textarea:focus,
549
+ .rbird-form-fields select:focus {
550
+ border-color: #667eea;
551
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
552
+ }
553
+
554
+ @keyframes rbirdFormSlideIn {
555
+ from {
556
+ opacity: 0;
557
+ transform: translateY(-20px) scale(0.95);
558
+ }
559
+ to {
560
+ opacity: 1;
561
+ transform: translateY(0) scale(1);
562
+ }
563
+ }
564
+ `;
565
+ document.head.appendChild(style);
566
+ }
567
+ }