@product7/feedback-sdk 1.0.1
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 +227 -0
- package/dist/README.md +227 -0
- package/dist/feedback-sdk.js +2483 -0
- package/dist/feedback-sdk.js.map +1 -0
- package/dist/feedback-sdk.min.js +2 -0
- package/dist/feedback-sdk.min.js.map +1 -0
- package/package.json +111 -0
- package/src/core/APIService.js +338 -0
- package/src/core/EventBus.js +54 -0
- package/src/core/FeedbackSDK.js +285 -0
- package/src/docs/api.md +686 -0
- package/src/docs/example.md +823 -0
- package/src/docs/installation.md +264 -0
- package/src/index.js +281 -0
- package/src/styles/styles.js +557 -0
- package/src/types/index.d.ts +12 -0
- package/src/utils/errors.js +142 -0
- package/src/utils/helpers.js +219 -0
- package/src/widgets/BaseWidget.js +334 -0
- package/src/widgets/ButtonWidget.js +62 -0
- package/src/widgets/InlineWidget.js +145 -0
- package/src/widgets/TabWidget.js +65 -0
- package/src/widgets/WidgetFactory.js +64 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
export function generateId(prefix = 'feedback') {
|
|
2
|
+
const timestamp = Date.now();
|
|
3
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
4
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function deepMerge(target, source) {
|
|
8
|
+
const result = { ...target };
|
|
9
|
+
|
|
10
|
+
for (const key in source) {
|
|
11
|
+
if (source.hasOwnProperty(key)) {
|
|
12
|
+
if (
|
|
13
|
+
source[key] &&
|
|
14
|
+
typeof source[key] === 'object' &&
|
|
15
|
+
!Array.isArray(source[key])
|
|
16
|
+
) {
|
|
17
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
18
|
+
} else {
|
|
19
|
+
result[key] = source[key];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function debounce(func, wait) {
|
|
28
|
+
let timeout;
|
|
29
|
+
return function executedFunction(...args) {
|
|
30
|
+
const later = () => {
|
|
31
|
+
clearTimeout(timeout);
|
|
32
|
+
func(...args);
|
|
33
|
+
};
|
|
34
|
+
clearTimeout(timeout);
|
|
35
|
+
timeout = setTimeout(later, wait);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function throttle(func, limit) {
|
|
40
|
+
let lastFunc;
|
|
41
|
+
let lastRan;
|
|
42
|
+
return function (...args) {
|
|
43
|
+
if (!lastRan) {
|
|
44
|
+
func(...args);
|
|
45
|
+
lastRan = Date.now();
|
|
46
|
+
} else {
|
|
47
|
+
clearTimeout(lastFunc);
|
|
48
|
+
lastFunc = setTimeout(
|
|
49
|
+
() => {
|
|
50
|
+
if (Date.now() - lastRan >= limit) {
|
|
51
|
+
func(...args);
|
|
52
|
+
lastRan = Date.now();
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
limit - (Date.now() - lastRan)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isValidEmail(email) {
|
|
62
|
+
if (!email || typeof email !== 'string') return false;
|
|
63
|
+
|
|
64
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
65
|
+
return emailRegex.test(email.trim());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function sanitizeHTML(str) {
|
|
69
|
+
if (!str || typeof str !== 'string') return '';
|
|
70
|
+
|
|
71
|
+
const div = document.createElement('div');
|
|
72
|
+
div.textContent = str;
|
|
73
|
+
return div.innerHTML;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getCSSProperty(element, property, fallback = '') {
|
|
77
|
+
if (!element || !property) return fallback;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const style = window.getComputedStyle(element);
|
|
81
|
+
return style.getPropertyValue(property) || fallback;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return fallback;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isInViewport(element) {
|
|
88
|
+
if (!element) return false;
|
|
89
|
+
|
|
90
|
+
const rect = element.getBoundingClientRect();
|
|
91
|
+
return (
|
|
92
|
+
rect.top >= 0 &&
|
|
93
|
+
rect.left >= 0 &&
|
|
94
|
+
rect.bottom <=
|
|
95
|
+
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
96
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function scrollToElement(element, options = {}) {
|
|
101
|
+
if (!element) return;
|
|
102
|
+
|
|
103
|
+
const defaultOptions = {
|
|
104
|
+
behavior: 'smooth',
|
|
105
|
+
block: 'center',
|
|
106
|
+
inline: 'nearest',
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
element.scrollIntoView({ ...defaultOptions, ...options });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getBrowserInfo() {
|
|
113
|
+
const userAgent = navigator.userAgent;
|
|
114
|
+
const platform = navigator.platform;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
userAgent,
|
|
118
|
+
platform,
|
|
119
|
+
language: navigator.language || navigator.userLanguage,
|
|
120
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
121
|
+
screenResolution: `${screen.width}x${screen.height}`,
|
|
122
|
+
windowSize: `${window.innerWidth}x${window.innerHeight}`,
|
|
123
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function formatFileSize(bytes) {
|
|
128
|
+
if (bytes === 0) return '0 Bytes';
|
|
129
|
+
|
|
130
|
+
const k = 1024;
|
|
131
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
132
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
133
|
+
|
|
134
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function delay(ms) {
|
|
138
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function safeJsonParse(str, fallback = null) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(str);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return fallback;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function escapeRegex(string) {
|
|
150
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function getNestedProperty(obj, path, defaultValue = undefined) {
|
|
154
|
+
if (!obj || !path) return defaultValue;
|
|
155
|
+
|
|
156
|
+
const keys = path.split('.');
|
|
157
|
+
let current = obj;
|
|
158
|
+
|
|
159
|
+
for (const key of keys) {
|
|
160
|
+
if (current === null || current === undefined || !(key in current)) {
|
|
161
|
+
return defaultValue;
|
|
162
|
+
}
|
|
163
|
+
current = current[key];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return current;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function setNestedProperty(obj, path, value) {
|
|
170
|
+
if (!obj || !path) return obj;
|
|
171
|
+
|
|
172
|
+
const keys = path.split('.');
|
|
173
|
+
const lastKey = keys.pop();
|
|
174
|
+
let current = obj;
|
|
175
|
+
|
|
176
|
+
for (const key of keys) {
|
|
177
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
178
|
+
current[key] = {};
|
|
179
|
+
}
|
|
180
|
+
current = current[key];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
current[lastKey] = value;
|
|
184
|
+
return obj;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function isBrowser() {
|
|
188
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function isMobile() {
|
|
192
|
+
if (!isBrowser()) return false;
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
196
|
+
navigator.userAgent
|
|
197
|
+
) || window.innerWidth <= 768
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function getCurrentTimestamp() {
|
|
202
|
+
return new Date().toISOString();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function validateConfig(config, required = []) {
|
|
206
|
+
const missing = [];
|
|
207
|
+
|
|
208
|
+
for (const key of required) {
|
|
209
|
+
if (!config[key]) {
|
|
210
|
+
missing.push(key);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (missing.length > 0) {
|
|
215
|
+
throw new Error(`Missing required configuration: ${missing.join(', ')}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
export class BaseWidget {
|
|
2
|
+
constructor(options = {}) {
|
|
3
|
+
this.id = options.id;
|
|
4
|
+
this.sdk = options.sdk;
|
|
5
|
+
this.apiService = options.apiService;
|
|
6
|
+
this.type = options.type || 'base';
|
|
7
|
+
|
|
8
|
+
this.options = {
|
|
9
|
+
container: null,
|
|
10
|
+
position: this.sdk.config.position,
|
|
11
|
+
theme: this.sdk.config.theme,
|
|
12
|
+
boardId: this.sdk.config.boardId,
|
|
13
|
+
autoShow: false,
|
|
14
|
+
customStyles: {},
|
|
15
|
+
...options,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
this.element = null;
|
|
19
|
+
this.modalElement = null;
|
|
20
|
+
this.mounted = false;
|
|
21
|
+
this.destroyed = false;
|
|
22
|
+
|
|
23
|
+
this.state = {
|
|
24
|
+
isOpen: false,
|
|
25
|
+
isSubmitting: false,
|
|
26
|
+
title: '',
|
|
27
|
+
content: '',
|
|
28
|
+
email: '',
|
|
29
|
+
attachments: [],
|
|
30
|
+
errors: {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this._bindMethods();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
mount(container) {
|
|
37
|
+
if (this.mounted || this.destroyed) return this;
|
|
38
|
+
|
|
39
|
+
if (typeof container === 'string') {
|
|
40
|
+
container = document.querySelector(container);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!container) {
|
|
44
|
+
container = document.body;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.container = container;
|
|
48
|
+
this.element = this._render();
|
|
49
|
+
this.container.appendChild(this.element);
|
|
50
|
+
|
|
51
|
+
this.mounted = true;
|
|
52
|
+
this._attachEvents();
|
|
53
|
+
this.onMount();
|
|
54
|
+
|
|
55
|
+
if (this.options.autoShow) {
|
|
56
|
+
this.show();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this.sdk.eventBus.emit('widget:mounted', { widget: this });
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
show() {
|
|
64
|
+
if (this.element) {
|
|
65
|
+
this.element.style.display = 'block';
|
|
66
|
+
}
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
hide() {
|
|
71
|
+
if (this.element) {
|
|
72
|
+
this.element.style.display = 'none';
|
|
73
|
+
}
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
openModal() {
|
|
78
|
+
this.state.isOpen = true;
|
|
79
|
+
this._renderModal();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
closeModal() {
|
|
83
|
+
this.state.isOpen = false;
|
|
84
|
+
if (this.modalElement) {
|
|
85
|
+
this.modalElement.remove();
|
|
86
|
+
this.modalElement = null;
|
|
87
|
+
}
|
|
88
|
+
this._resetForm();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async submitFeedback() {
|
|
92
|
+
if (this.state.isSubmitting) return;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
this.state.isSubmitting = true;
|
|
96
|
+
this._updateSubmitButton();
|
|
97
|
+
|
|
98
|
+
const payload = {
|
|
99
|
+
title: this.state.title || 'Feedback',
|
|
100
|
+
content: this.state.content,
|
|
101
|
+
email: this.state.email,
|
|
102
|
+
board_id: this.options.boardId,
|
|
103
|
+
attachments: this.state.attachments,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (!this.state.content.trim()) {
|
|
107
|
+
this._showError('Please enter your feedback message.');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const response = await this.apiService.submitFeedback(payload);
|
|
112
|
+
|
|
113
|
+
this._showSuccessMessage();
|
|
114
|
+
this.closeModal();
|
|
115
|
+
|
|
116
|
+
this.sdk.eventBus.emit('feedback:submitted', {
|
|
117
|
+
widget: this,
|
|
118
|
+
feedback: response,
|
|
119
|
+
});
|
|
120
|
+
} catch (error) {
|
|
121
|
+
this._showError('Failed to submit feedback. Please try again.');
|
|
122
|
+
this.sdk.eventBus.emit('feedback:error', { widget: this, error });
|
|
123
|
+
} finally {
|
|
124
|
+
this.state.isSubmitting = false;
|
|
125
|
+
this._updateSubmitButton();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
handleConfigUpdate(newConfig) {
|
|
130
|
+
this.options.theme = newConfig.theme;
|
|
131
|
+
if (this.element) {
|
|
132
|
+
this._updateTheme();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
destroy() {
|
|
137
|
+
if (this.destroyed) return;
|
|
138
|
+
|
|
139
|
+
this.onDestroy();
|
|
140
|
+
this.closeModal();
|
|
141
|
+
|
|
142
|
+
if (this.element && this.element.parentNode) {
|
|
143
|
+
this.element.parentNode.removeChild(this.element);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.destroyed = true;
|
|
147
|
+
this.mounted = false;
|
|
148
|
+
this.sdk.eventBus.emit('widget:destroyed', { widget: this });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
onMount() {}
|
|
152
|
+
onDestroy() {}
|
|
153
|
+
|
|
154
|
+
_render() {
|
|
155
|
+
throw new Error('_render() must be implemented by concrete widget');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_attachEvents() {
|
|
159
|
+
// Override in concrete widgets
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_bindMethods() {
|
|
163
|
+
this.openModal = this.openModal.bind(this);
|
|
164
|
+
this.closeModal = this.closeModal.bind(this);
|
|
165
|
+
this.submitFeedback = this.submitFeedback.bind(this);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_renderModal() {
|
|
169
|
+
if (this.modalElement) return;
|
|
170
|
+
|
|
171
|
+
this.modalElement = document.createElement('div');
|
|
172
|
+
this.modalElement.className = `feedback-modal theme-${this.options.theme}`;
|
|
173
|
+
this.modalElement.innerHTML = this._getModalHTML();
|
|
174
|
+
|
|
175
|
+
document.body.appendChild(this.modalElement);
|
|
176
|
+
this._attachModalEvents();
|
|
177
|
+
|
|
178
|
+
const firstInput = this.modalElement.querySelector('input, textarea');
|
|
179
|
+
if (firstInput) {
|
|
180
|
+
setTimeout(() => firstInput.focus(), 100);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_getModalHTML() {
|
|
185
|
+
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">×</button>
|
|
191
|
+
</div>
|
|
192
|
+
<form class="feedback-form">
|
|
193
|
+
<div class="feedback-form-group">
|
|
194
|
+
<label for="feedback-title-${this.id}">Title</label>
|
|
195
|
+
<input
|
|
196
|
+
type="text"
|
|
197
|
+
id="feedback-title-${this.id}"
|
|
198
|
+
name="title"
|
|
199
|
+
placeholder="Brief description of your feedback"
|
|
200
|
+
value="${this.state.title}"
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
<div class="feedback-form-group">
|
|
204
|
+
<label for="feedback-content-${this.id}">Message *</label>
|
|
205
|
+
<textarea
|
|
206
|
+
id="feedback-content-${this.id}"
|
|
207
|
+
name="content"
|
|
208
|
+
placeholder="Tell us more about your feedback..."
|
|
209
|
+
required
|
|
210
|
+
>${this.state.content}</textarea>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="feedback-form-actions">
|
|
213
|
+
<button type="button" class="feedback-btn feedback-btn-cancel">Cancel</button>
|
|
214
|
+
<button type="submit" class="feedback-btn feedback-btn-submit">
|
|
215
|
+
${this.state.isSubmitting ? 'Sending...' : 'Send Feedback'}
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="feedback-error" style="display: none;"></div>
|
|
219
|
+
</form>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
|
|
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
|
+
});
|
|
241
|
+
|
|
242
|
+
const form = modal.querySelector('.feedback-form');
|
|
243
|
+
form.addEventListener('submit', (e) => {
|
|
244
|
+
e.preventDefault();
|
|
245
|
+
this.submitFeedback();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
modal
|
|
249
|
+
.querySelector('input[name="title"]')
|
|
250
|
+
.addEventListener('input', (e) => {
|
|
251
|
+
this.state.title = e.target.value;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
modal
|
|
255
|
+
.querySelector('textarea[name="content"]')
|
|
256
|
+
.addEventListener('input', (e) => {
|
|
257
|
+
this.state.content = e.target.value;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_updateSubmitButton() {
|
|
262
|
+
if (this.modalElement) {
|
|
263
|
+
const submitBtn = this.modalElement.querySelector('.feedback-btn-submit');
|
|
264
|
+
if (submitBtn) {
|
|
265
|
+
submitBtn.textContent = this.state.isSubmitting
|
|
266
|
+
? 'Sending...'
|
|
267
|
+
: 'Send Feedback';
|
|
268
|
+
submitBtn.disabled = this.state.isSubmitting;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_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);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_showSuccessMessage() {
|
|
287
|
+
const notification = document.createElement('div');
|
|
288
|
+
notification.className = 'feedback-success-notification';
|
|
289
|
+
notification.innerHTML = `
|
|
290
|
+
<div class="feedback-success-content">
|
|
291
|
+
<span>✓ Feedback submitted successfully!</span>
|
|
292
|
+
<button class="feedback-success-close">×</button>
|
|
293
|
+
</div>
|
|
294
|
+
`;
|
|
295
|
+
|
|
296
|
+
document.body.appendChild(notification);
|
|
297
|
+
|
|
298
|
+
setTimeout(() => {
|
|
299
|
+
if (notification.parentNode) {
|
|
300
|
+
notification.parentNode.removeChild(notification);
|
|
301
|
+
}
|
|
302
|
+
}, 3000);
|
|
303
|
+
|
|
304
|
+
notification
|
|
305
|
+
.querySelector('.feedback-success-close')
|
|
306
|
+
.addEventListener('click', () => {
|
|
307
|
+
if (notification.parentNode) {
|
|
308
|
+
notification.parentNode.removeChild(notification);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_resetForm() {
|
|
314
|
+
this.state.title = '';
|
|
315
|
+
this.state.content = '';
|
|
316
|
+
this.state.email = '';
|
|
317
|
+
this.state.errors = {};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
_updateTheme() {
|
|
321
|
+
if (this.element) {
|
|
322
|
+
this.element.className = this.element.className.replace(
|
|
323
|
+
/theme-\w+/,
|
|
324
|
+
`theme-${this.options.theme}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (this.modalElement) {
|
|
328
|
+
this.modalElement.className = this.modalElement.className.replace(
|
|
329
|
+
/theme-\w+/,
|
|
330
|
+
`theme-${this.options.theme}`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { BaseWidget } from './BaseWidget.js';
|
|
2
|
+
|
|
3
|
+
export class ButtonWidget extends BaseWidget {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({ ...options, type: 'button' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
_render() {
|
|
9
|
+
const button = document.createElement('div');
|
|
10
|
+
button.className = `feedback-widget feedback-widget-button theme-${this.options.theme} position-${this.options.position}`;
|
|
11
|
+
button.innerHTML = `
|
|
12
|
+
<button class="feedback-trigger-btn" type="button">
|
|
13
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
14
|
+
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
|
|
15
|
+
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
|
|
16
|
+
</svg>
|
|
17
|
+
Feedback
|
|
18
|
+
</button>
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
if (this.options.customStyles) {
|
|
22
|
+
Object.assign(button.style, this.options.customStyles);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return button;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_attachEvents() {
|
|
29
|
+
const button = this.element.querySelector('.feedback-trigger-btn');
|
|
30
|
+
button.addEventListener('click', this.openModal);
|
|
31
|
+
|
|
32
|
+
button.addEventListener('mouseenter', () => {
|
|
33
|
+
if (!this.state.isSubmitting) {
|
|
34
|
+
button.style.transform = 'translateY(-2px)';
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
button.addEventListener('mouseleave', () => {
|
|
39
|
+
button.style.transform = 'translateY(0)';
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
updateText(text) {
|
|
44
|
+
const button = this.element?.querySelector('.feedback-trigger-btn');
|
|
45
|
+
if (button) {
|
|
46
|
+
const textNode = button.childNodes[button.childNodes.length - 1];
|
|
47
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
48
|
+
textNode.textContent = text;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
updatePosition(position) {
|
|
54
|
+
this.options.position = position;
|
|
55
|
+
if (this.element) {
|
|
56
|
+
this.element.className = this.element.className.replace(
|
|
57
|
+
/position-\w+-\w+/,
|
|
58
|
+
`position-${position}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|