@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,145 @@
|
|
|
1
|
+
import { BaseWidget } from './BaseWidget.js';
|
|
2
|
+
|
|
3
|
+
export class InlineWidget extends BaseWidget {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({ ...options, type: 'inline' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
_render() {
|
|
9
|
+
const widget = document.createElement('div');
|
|
10
|
+
widget.className = `feedback-widget feedback-widget-inline theme-${this.options.theme}`;
|
|
11
|
+
widget.innerHTML = `
|
|
12
|
+
<div class="feedback-inline-content">
|
|
13
|
+
<h3>Send us your feedback</h3>
|
|
14
|
+
<form class="feedback-inline-form">
|
|
15
|
+
<div class="feedback-form-group">
|
|
16
|
+
<input
|
|
17
|
+
type="text"
|
|
18
|
+
name="title"
|
|
19
|
+
placeholder="Title (optional)"
|
|
20
|
+
value="${this.state.title}"
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
<div class="feedback-form-group">
|
|
24
|
+
<textarea
|
|
25
|
+
name="content"
|
|
26
|
+
placeholder="Your feedback..."
|
|
27
|
+
required
|
|
28
|
+
>${this.state.content}</textarea>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="feedback-form-group">
|
|
31
|
+
<input
|
|
32
|
+
type="email"
|
|
33
|
+
name="email"
|
|
34
|
+
placeholder="Email (optional)"
|
|
35
|
+
value="${this.state.email}"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
<button type="submit" class="feedback-btn feedback-btn-submit">
|
|
39
|
+
Send Feedback
|
|
40
|
+
</button>
|
|
41
|
+
<div class="feedback-error" style="display: none;"></div>
|
|
42
|
+
</form>
|
|
43
|
+
</div>
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
if (this.options.customStyles) {
|
|
47
|
+
Object.assign(widget.style, this.options.customStyles);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return widget;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_attachEvents() {
|
|
54
|
+
const form = this.element.querySelector('.feedback-inline-form');
|
|
55
|
+
|
|
56
|
+
form.addEventListener('submit', (e) => {
|
|
57
|
+
e.preventDefault();
|
|
58
|
+
this.submitFeedback();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
form.querySelector('input[name="title"]').addEventListener('input', (e) => {
|
|
62
|
+
this.state.title = e.target.value;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
form
|
|
66
|
+
.querySelector('textarea[name="content"]')
|
|
67
|
+
.addEventListener('input', (e) => {
|
|
68
|
+
this.state.content = e.target.value;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
form.querySelector('input[name="email"]').addEventListener('input', (e) => {
|
|
72
|
+
this.state.email = e.target.value;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
openModal() {
|
|
77
|
+
const textarea = this.element.querySelector('textarea[name="content"]');
|
|
78
|
+
if (textarea) {
|
|
79
|
+
textarea.focus();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
closeModal() {
|
|
84
|
+
// Inline widget doesn't use modal
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_showSuccessMessage() {
|
|
88
|
+
const widget = this.element.querySelector('.feedback-inline-content');
|
|
89
|
+
const originalContent = widget.innerHTML;
|
|
90
|
+
|
|
91
|
+
widget.innerHTML = `
|
|
92
|
+
<div class="feedback-success">
|
|
93
|
+
<div class="feedback-success-icon">✓</div>
|
|
94
|
+
<h3>Thank you!</h3>
|
|
95
|
+
<p>Your feedback has been submitted successfully.</p>
|
|
96
|
+
<button class="feedback-btn feedback-btn-reset">Send Another</button>
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
const resetBtn = widget.querySelector('.feedback-btn-reset');
|
|
101
|
+
resetBtn.addEventListener('click', () => {
|
|
102
|
+
widget.innerHTML = originalContent;
|
|
103
|
+
this._attachEvents();
|
|
104
|
+
this._resetForm();
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
_showError(message) {
|
|
109
|
+
const errorElement = this.element.querySelector('.feedback-error');
|
|
110
|
+
if (errorElement) {
|
|
111
|
+
errorElement.textContent = message;
|
|
112
|
+
errorElement.style.display = 'block';
|
|
113
|
+
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
if (errorElement) {
|
|
116
|
+
errorElement.style.display = 'none';
|
|
117
|
+
}
|
|
118
|
+
}, 5000);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
_updateSubmitButton() {
|
|
123
|
+
const submitBtn = this.element.querySelector('.feedback-btn-submit');
|
|
124
|
+
if (submitBtn) {
|
|
125
|
+
submitBtn.textContent = this.state.isSubmitting
|
|
126
|
+
? 'Sending...'
|
|
127
|
+
: 'Send Feedback';
|
|
128
|
+
submitBtn.disabled = this.state.isSubmitting;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
updateTitle(title) {
|
|
133
|
+
const titleElement = this.element?.querySelector('h3');
|
|
134
|
+
if (titleElement) {
|
|
135
|
+
titleElement.textContent = title;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setPlaceholder(field, placeholder) {
|
|
140
|
+
const input = this.element?.querySelector(`[name="${field}"]`);
|
|
141
|
+
if (input) {
|
|
142
|
+
input.placeholder = placeholder;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { BaseWidget } from './BaseWidget.js';
|
|
2
|
+
|
|
3
|
+
export class TabWidget extends BaseWidget {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
super({ ...options, type: 'tab' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
_render() {
|
|
9
|
+
const tab = document.createElement('div');
|
|
10
|
+
tab.className = `feedback-widget feedback-widget-tab theme-${this.options.theme} position-${this.options.position}`;
|
|
11
|
+
tab.innerHTML = `
|
|
12
|
+
<div class="feedback-tab-trigger">
|
|
13
|
+
<span class="feedback-tab-text">Feedback</span>
|
|
14
|
+
</div>
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
if (this.options.customStyles) {
|
|
18
|
+
Object.assign(tab.style, this.options.customStyles);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return tab;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_attachEvents() {
|
|
25
|
+
const tab = this.element.querySelector('.feedback-tab-trigger');
|
|
26
|
+
tab.addEventListener('click', this.openModal);
|
|
27
|
+
|
|
28
|
+
tab.addEventListener('mouseenter', () => {
|
|
29
|
+
if (!this.state.isSubmitting) {
|
|
30
|
+
tab.style.transform = this._getHoverTransform();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
tab.addEventListener('mouseleave', () => {
|
|
35
|
+
tab.style.transform = 'none';
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_getHoverTransform() {
|
|
40
|
+
const position = this.options.position;
|
|
41
|
+
if (position.includes('right')) {
|
|
42
|
+
return 'translateX(-5px)';
|
|
43
|
+
} else if (position.includes('left')) {
|
|
44
|
+
return 'translateX(5px)';
|
|
45
|
+
}
|
|
46
|
+
return 'none';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
updateText(text) {
|
|
50
|
+
const textElement = this.element?.querySelector('.feedback-tab-text');
|
|
51
|
+
if (textElement) {
|
|
52
|
+
textElement.textContent = text;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
updatePosition(position) {
|
|
57
|
+
this.options.position = position;
|
|
58
|
+
if (this.element) {
|
|
59
|
+
this.element.className = this.element.className.replace(
|
|
60
|
+
/position-\w+-\w+/,
|
|
61
|
+
`position-${position}`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { SDKError } from '../utils/errors.js';
|
|
2
|
+
import { ButtonWidget } from './ButtonWidget.js';
|
|
3
|
+
import { InlineWidget } from './InlineWidget.js';
|
|
4
|
+
import { TabWidget } from './TabWidget.js';
|
|
5
|
+
|
|
6
|
+
export class WidgetFactory {
|
|
7
|
+
static widgets = new Map([
|
|
8
|
+
['button', ButtonWidget],
|
|
9
|
+
['tab', TabWidget],
|
|
10
|
+
['inline', InlineWidget],
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
static register(type, WidgetClass) {
|
|
14
|
+
if (typeof type !== 'string' || !type.trim()) {
|
|
15
|
+
throw new SDKError('Widget type must be a non-empty string');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof WidgetClass !== 'function') {
|
|
19
|
+
throw new SDKError('Widget class must be a constructor function');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.widgets.set(type, WidgetClass);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static create(type, options = {}) {
|
|
26
|
+
const WidgetClass = this.widgets.get(type);
|
|
27
|
+
|
|
28
|
+
if (!WidgetClass) {
|
|
29
|
+
const availableTypes = Array.from(this.widgets.keys()).join(', ');
|
|
30
|
+
throw new SDKError(
|
|
31
|
+
`Unknown widget type: ${type}. Available types: ${availableTypes}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return new WidgetClass(options);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw new SDKError(
|
|
39
|
+
`Failed to create widget of type '${type}': ${error.message}`,
|
|
40
|
+
error
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static getAvailableTypes() {
|
|
46
|
+
return Array.from(this.widgets.keys());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static isTypeRegistered(type) {
|
|
50
|
+
return this.widgets.has(type);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static unregister(type) {
|
|
54
|
+
return this.widgets.delete(type);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static clear() {
|
|
58
|
+
this.widgets.clear();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static getWidgetClass(type) {
|
|
62
|
+
return this.widgets.get(type);
|
|
63
|
+
}
|
|
64
|
+
}
|