@subiz/widget-v4 4.1.218 → 4.1.222
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/assets/img/bar_ho_tro.svg +3 -0
- package/{dist/assets/img/bubble_default.6f6eec8.svg → assets/img/bubble_11.svg} +0 -1
- package/assets/less/form-page.less +58 -0
- package/assets/less/form.less +279 -3
- package/assets/less/widget.less +6 -0
- package/compiled_src/chatbox/ChatButton.js +22 -3
- package/compiled_src/chatbox/Widget.js +13 -18
- package/compiled_src/form/Form.js +270 -71
- package/compiled_src/form/FormPage.js +17 -4
- package/compiled_src/form/form-fields.js +244 -0
- package/compiled_src/form/index.js +2 -0
- package/compiled_src/form/rating-stars.js +48 -88
- package/compiled_src/plugin.js +10 -0
- package/compiled_src/store/api.js +5 -7
- package/compiled_src/store/api_fake.js +3 -1
- package/compiled_src/store/localize.js +33 -0
- package/compiled_src/store/store.js +57 -1
- package/compiled_src/store/widget-settings.js +67 -0
- package/compiled_src/util/common.js +1 -1
- package/package.json +3 -3
- package/src/chatbox/ChatButton.js +25 -4
- package/src/chatbox/Widget.js +21 -21
- package/src/form/Form.js +335 -97
- package/src/form/FormPage.js +21 -8
- package/src/form/form-fields.js +224 -0
- package/src/form/index.js +6 -1
- package/src/form/rating-stars.js +48 -64
- package/src/plugin.js +10 -0
- package/src/store/api.js +9 -7
- package/src/store/api_fake.js +3 -1
- package/src/store/localize.js +30 -0
- package/src/store/store.js +64 -1
- package/src/store/widget-settings.js +46 -0
- package/src/util/common.js +1 -1
- package/test.cjs +3 -0
- package/webpack.dev.js +2 -0
- package/dist/127.js +0 -273
- package/dist/132.js +0 -2817
- package/dist/185.js +0 -1917
- package/dist/1a12ed33347.127.js +0 -1
- package/dist/1a12ed33347.132.js +0 -1
- package/dist/1a12ed33347.185.js +0 -1
- package/dist/1a12ed33347.27.js +0 -1
- package/dist/1a12ed33347.278.js +0 -1
- package/dist/1a12ed33347.339.js +0 -1
- package/dist/1a12ed33347.459.js +0 -1
- package/dist/1a12ed33347.490.js +0 -1
- package/dist/1a12ed33347.app.js +0 -1
- package/dist/1a12ed33347.chat.html +0 -1
- package/dist/1a12ed33347.chat.js +0 -1
- package/dist/1a12ed33347.form.html +0 -1
- package/dist/1a12ed33347.form.js +0 -1
- package/dist/1a12ed33347.index.html +0 -72
- package/dist/1a12ed33347.library.html +0 -72
- package/dist/1a12ed33347.library.js +0 -1
- package/dist/1a12ed33347.preview.html +0 -759
- package/dist/1a12ed33347.preview.js +0 -1
- package/dist/1a12ed33347.webview.js +0 -1
- package/dist/27.js +0 -37
- package/dist/459.js +0 -37
- package/dist/490.js +0 -305
- package/dist/HASH +0 -1
- package/dist/assets/img/Facebook.64607ec.png +0 -0
- package/dist/assets/img/Zalo.6845707.png +0 -0
- package/dist/assets/img/award1a.1a58e10.png +0 -0
- package/dist/assets/img/award3a.3b74423.png +0 -0
- package/dist/assets/img/award4a.ed6ccd4.png +0 -0
- package/dist/assets/img/award5.39eb7ae.png +0 -0
- package/dist/assets/img/caret.998eb8f.svg +0 -1
- package/dist/assets/img/convos_empty.2353007.svg +0 -72
- package/dist/assets/img/default.06e6ee0.jpg +0 -0
- package/dist/assets/img/default_avatar.e449b04.png +0 -0
- package/dist/assets/img/default_logo.dd13a2b.svg +0 -5
- package/dist/assets/img/done_gray.d7e8203.svg +0 -4
- package/dist/assets/img/done_green.b817fd4.svg +0 -4
- package/dist/assets/img/dot_chat.335fd8a.svg +0 -3
- package/dist/assets/img/emoji/messenger/sub_23.529_58.824.d5faf14.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_23.529_60.784.1420241.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_25.490_0.000.1e6b2c0.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_25.490_45.098.3dfbc08.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_25.490_5.882.4b49e94.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_27.451_72.549.7114f58.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_27.451_84.314.231e2b7.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_27.451_96.078.ddbb008.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_29.412_5.882.61f3359.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_100.000.6df72bc.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_45.098.3d7a7ad.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_47.059.0f4d6e2.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_54.902.65724c1.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_64.706.9da3cba.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_72.549.edca5cf.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_78.431.f910f2a.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_58.824_88.235.def4e71.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_60.784_11.765.49c03d0.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_60.784_27.451.46b54e5.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_60.784_29.412.e36d284.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_60.784_3.922.bb191b0.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_60.784_35.294.af7b927.png +0 -0
- package/dist/assets/img/emoji/messenger/sub_60.784_7.843.98f2a99.png +0 -0
- package/dist/assets/img/empty.29bed2d.svg +0 -23
- package/dist/assets/img/empty_1.0b98af0.svg +0 -11
- package/dist/assets/img/examplte_logo.f86535f.svg +0 -1
- package/dist/assets/img/external-link.90577cc.svg +0 -1
- package/dist/assets/img/fb_inverse_circle.36c2cfc.png +0 -0
- package/dist/assets/img/ghn.070f8e3.svg +0 -17
- package/dist/assets/img/ghtk.326eac1.svg +0 -18
- package/dist/assets/img/image_default.be0698b.png +0 -0
- package/dist/assets/img/info.ebce7ea.svg +0 -1
- package/dist/assets/img/loader.37000a7.svg +0 -1
- package/dist/assets/img/loading_static.a27f0ac.svg +0 -10
- package/dist/assets/img/main-service1.d536cc2.png +0 -0
- package/dist/assets/img/main-service2.9b99549.png +0 -0
- package/dist/assets/img/not_found_search.2ddb8d1.png +0 -0
- package/dist/assets/img/package.8d37d0f.svg +0 -5
- package/dist/assets/img/package_fade.763c001.svg +0 -1
- package/dist/assets/img/rating--average.1ea492c.svg +0 -12
- package/dist/assets/img/rating--great.e3d712e.svg +0 -16
- package/dist/assets/img/rating--poor.2938a25.svg +0 -12
- package/dist/assets/img/search.f9705ff.svg +0 -1
- package/dist/assets/img/send.6db52a9.svg +0 -3
- package/dist/assets/img/shop.f7dc7d8.svg +0 -1
- package/dist/assets/img/shop_faded.df7845a.svg +0 -1
- package/dist/assets/img/stars_v2.79cf728.png +0 -0
- package/dist/assets/img/subiz-mono.8fbc067.svg +0 -3
- package/dist/assets/img/x.26fdfa3.svg +0 -5
- package/dist/malicious-app.js +0 -17
- package/dist/screenshoot.html +0 -7
- package/dist/screenshoot.js +0 -14454
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {h} from 'preact'
|
|
2
|
+
import sb from '../util/index.js'
|
|
3
|
+
|
|
4
|
+
// Helpers and small presentational pieces for the new Form block fields
|
|
5
|
+
// (rating stars/faces, linear_scale, checkbox chips, "Other" option).
|
|
6
|
+
|
|
7
|
+
export const OTHER_OPTION_VALUE = '__other__'
|
|
8
|
+
|
|
9
|
+
// Defaults for i18n keys that may not exist yet in the backend locale files.
|
|
10
|
+
// t(key) returns the key itself when the message is missing.
|
|
11
|
+
const DEFAULT_MESSAGES = {
|
|
12
|
+
vi: {
|
|
13
|
+
form_other_option: 'Khác…',
|
|
14
|
+
form_other_option_placeholder: 'Nhập câu trả lời của bạn',
|
|
15
|
+
form_other_option_label: 'Khác',
|
|
16
|
+
},
|
|
17
|
+
en: {
|
|
18
|
+
form_other_option: 'Other…',
|
|
19
|
+
form_other_option_placeholder: 'Type your answer',
|
|
20
|
+
form_other_option_label: 'Other',
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function translate(t, key) {
|
|
25
|
+
let msg = typeof t === 'function' ? t(key) : ''
|
|
26
|
+
if (msg && msg !== key) return msg
|
|
27
|
+
let locale = ''
|
|
28
|
+
try {
|
|
29
|
+
if (t && typeof t.locale === 'function') locale = String(t.locale() || '')
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
// unknown locale falls back to vi, same as the widget's fallback locale
|
|
32
|
+
let lang = locale && !locale.toLowerCase().startsWith('vi') ? 'en' : 'vi'
|
|
33
|
+
return DEFAULT_MESSAGES[lang][key] || key
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isChips(com) {
|
|
37
|
+
return String(sb.get(com, 'class') || '')
|
|
38
|
+
.split(/\s+/)
|
|
39
|
+
.includes('chips')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getPlaceholder(com) {
|
|
43
|
+
return sb.get(com, 'place_holder') || sb.get(com, 'placeholder') || ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDescription(com) {
|
|
47
|
+
return sb.get(com, 'description') || sb.get(com, 'label_description') || ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// numeric option values ("1".."N") are stored as numbers, same as the legacy rating field
|
|
51
|
+
export function toStoredValue(v) {
|
|
52
|
+
if (typeof v === 'number') return v
|
|
53
|
+
let s = String(v === undefined || v === null ? '' : v).trim()
|
|
54
|
+
if (s !== '' && /^-?\d+(\.\d+)?$/.test(s)) return Number(s)
|
|
55
|
+
return s
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sameValue(a, b) {
|
|
59
|
+
if (a === undefined || a === null || a === '') return false
|
|
60
|
+
return String(a) === String(b)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const FACES_5 = ['face_very_sad', 'face_sad', 'face_neutral', 'face_happy', 'face_very_happy']
|
|
64
|
+
const FACES_3 = ['face_sad', 'face_neutral', 'face_happy']
|
|
65
|
+
|
|
66
|
+
// returns {kind: 'star'|'face', levels: [{value, icon}]}
|
|
67
|
+
export function getRatingConfig(com) {
|
|
68
|
+
let options = sb.filter(sb.get(com, 'input_options') || [], (o) => o && o.value !== undefined && o.value !== '')
|
|
69
|
+
if (sb.size(options) > 0) {
|
|
70
|
+
let isFace = sb.find(options, (o) => String(o.icon || '').startsWith('face'))
|
|
71
|
+
let faces = options.length === 3 ? FACES_3 : FACES_5
|
|
72
|
+
let levels = sb.map(options, (o, i) => {
|
|
73
|
+
let icon = String(o.icon || '')
|
|
74
|
+
if (isFace && !icon.startsWith('face')) icon = faces[i] || 'face_neutral'
|
|
75
|
+
if (!isFace) icon = 'star'
|
|
76
|
+
return {value: String(o.value), icon}
|
|
77
|
+
})
|
|
78
|
+
return {kind: isFace ? 'face' : 'star', levels}
|
|
79
|
+
}
|
|
80
|
+
let max = parseInt(sb.get(com, 'scale_max'), 10)
|
|
81
|
+
if (max !== 3) max = 5
|
|
82
|
+
let levels = []
|
|
83
|
+
for (let i = 1; i <= max; i++) levels.push({value: String(i), icon: 'star'})
|
|
84
|
+
return {kind: 'star', levels}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function getScaleRange(com) {
|
|
88
|
+
let min = parseInt(sb.get(com, 'scale_min'), 10) === 0 ? 0 : 1
|
|
89
|
+
let max = parseInt(sb.get(com, 'scale_max'), 10)
|
|
90
|
+
if (!max || isNaN(max)) max = 5
|
|
91
|
+
if (max < 2) max = 2
|
|
92
|
+
if (max > 10) max = 10
|
|
93
|
+
let values = []
|
|
94
|
+
for (let i = min; i <= max; i++) values.push(i)
|
|
95
|
+
return {min, max, values}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// label shown for a stored option value; values not in the option list
|
|
99
|
+
// (old free text or an "Other" answer) are shown as-is
|
|
100
|
+
export function optionLabel(com, v) {
|
|
101
|
+
let option = sb.find(sb.get(com, 'input_options') || [], (o) => sameValue(o.value, v))
|
|
102
|
+
if (option) return option.label || option.value
|
|
103
|
+
return v
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isEmptyValue(val) {
|
|
107
|
+
if (val === undefined || val === null) return true
|
|
108
|
+
if (typeof val === 'string' && !val.trim()) return true
|
|
109
|
+
if (Array.isArray(val) && val.length === 0) return true
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const MOUTHS = {
|
|
114
|
+
face_very_sad: 'M7.5 17.5 Q12 11.5 16.5 17.5',
|
|
115
|
+
face_sad: 'M8.5 16.5 Q12 13.5 15.5 16.5',
|
|
116
|
+
face_neutral: 'M8.5 15.5 L15.5 15.5',
|
|
117
|
+
face_happy: 'M8.5 14 Q12 17.5 15.5 14',
|
|
118
|
+
face_very_happy: 'M7.5 13 Q12 20 16.5 13 Z',
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function FaceIcon({icon, size}) {
|
|
122
|
+
let mouth = MOUTHS[icon] || MOUTHS.face_neutral
|
|
123
|
+
let s = size || 28
|
|
124
|
+
return (
|
|
125
|
+
<svg
|
|
126
|
+
class='form-face-icon'
|
|
127
|
+
width={s}
|
|
128
|
+
height={s}
|
|
129
|
+
viewBox='0 0 24 24'
|
|
130
|
+
fill='none'
|
|
131
|
+
stroke='currentColor'
|
|
132
|
+
stroke-width='1.6'
|
|
133
|
+
stroke-linecap='round'
|
|
134
|
+
stroke-linejoin='round'
|
|
135
|
+
aria-hidden='true'
|
|
136
|
+
>
|
|
137
|
+
<circle cx='12' cy='12' r='10' />
|
|
138
|
+
<circle cx='8.8' cy='9.6' r='0.6' fill='currentColor' stroke-width='1.2' />
|
|
139
|
+
<circle cx='15.2' cy='9.6' r='0.6' fill='currentColor' stroke-width='1.2' />
|
|
140
|
+
<path d={mouth} fill={icon === 'face_very_happy' ? 'currentColor' : 'none'} fill-opacity='0.15' />
|
|
141
|
+
</svg>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function RatingFaces({levels, value, onChange, readonly}) {
|
|
146
|
+
return (
|
|
147
|
+
<div class={'form-rating-faces' + (readonly ? ' readonly' : '')} role={readonly ? undefined : 'radiogroup'}>
|
|
148
|
+
{sb.map(levels, (level) => {
|
|
149
|
+
let selected = sameValue(value, level.value)
|
|
150
|
+
let cls = 'form-rating-face'
|
|
151
|
+
if (selected) cls += ' selected'
|
|
152
|
+
if (readonly) {
|
|
153
|
+
return (
|
|
154
|
+
<span class={cls} key={level.value}>
|
|
155
|
+
<FaceIcon icon={level.icon} />
|
|
156
|
+
</span>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
return (
|
|
160
|
+
<button
|
|
161
|
+
type='button'
|
|
162
|
+
class={cls}
|
|
163
|
+
key={level.value}
|
|
164
|
+
role='radio'
|
|
165
|
+
aria-checked={selected ? 'true' : 'false'}
|
|
166
|
+
aria-label={level.value}
|
|
167
|
+
title={level.value}
|
|
168
|
+
onClick={() => onChange(selected ? '' : toStoredValue(level.value))}
|
|
169
|
+
>
|
|
170
|
+
<FaceIcon icon={level.icon} />
|
|
171
|
+
</button>
|
|
172
|
+
)
|
|
173
|
+
})}
|
|
174
|
+
</div>
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// numberedLabels: prefix min/max labels with their number ("1 · …"), used in the chat box only
|
|
179
|
+
export function LinearScale({com, value, onChange, numberedLabels}) {
|
|
180
|
+
let {min, max, values} = getScaleRange(com)
|
|
181
|
+
let cols = Math.ceil(values.length / 2)
|
|
182
|
+
let minLabel = sb.get(com, 'scale_min_label') || ''
|
|
183
|
+
let maxLabel = sb.get(com, 'scale_max_label') || ''
|
|
184
|
+
return (
|
|
185
|
+
<div
|
|
186
|
+
class={'form-linear-scale' + (values.length >= 8 ? ' many' : '')}
|
|
187
|
+
style={`--scale-count: ${values.length}; --scale-cols: ${cols}`}
|
|
188
|
+
>
|
|
189
|
+
<div class='form-linear-scale-items' role='radiogroup'>
|
|
190
|
+
{sb.map(values, (v) => {
|
|
191
|
+
let selected = sameValue(value, v)
|
|
192
|
+
// odd count on two rows (e.g. 0..10 -> 6 + 5): start the 2nd row one column
|
|
193
|
+
// later so the max value ends bottom-right, above the max label.
|
|
194
|
+
// A class, not an inline style: the chatbox has `* {all: unset !important}`.
|
|
195
|
+
let cls = 'form-linear-scale-item'
|
|
196
|
+
if (selected) cls += ' selected'
|
|
197
|
+
if (values.length % 2 && v === values[cols]) cls += ' row2-start'
|
|
198
|
+
return (
|
|
199
|
+
<button
|
|
200
|
+
type='button'
|
|
201
|
+
key={v}
|
|
202
|
+
class={cls}
|
|
203
|
+
role='radio'
|
|
204
|
+
aria-checked={selected ? 'true' : 'false'}
|
|
205
|
+
onClick={() => onChange(v)}
|
|
206
|
+
>
|
|
207
|
+
{v}
|
|
208
|
+
</button>
|
|
209
|
+
)
|
|
210
|
+
})}
|
|
211
|
+
</div>
|
|
212
|
+
{(minLabel || maxLabel) && (
|
|
213
|
+
<div class='form-linear-scale-labels'>
|
|
214
|
+
<span class='form-linear-scale-label min'>
|
|
215
|
+
{minLabel && numberedLabels ? `${min} · ${minLabel}` : minLabel}
|
|
216
|
+
</span>
|
|
217
|
+
<span class='form-linear-scale-label max'>
|
|
218
|
+
{maxLabel && numberedLabels ? `${max} · ${maxLabel}` : maxLabel}
|
|
219
|
+
</span>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
package/src/form/index.js
CHANGED
|
@@ -40,6 +40,8 @@ async function load() {
|
|
|
40
40
|
getUrlParameter('id') || getUrlParameter('accid') || getUrlParameter('account-id') || 'acqsulrowbxiugvginhw'
|
|
41
41
|
let mask = accid + '@' + 'abcd'
|
|
42
42
|
let token = getUrlParameter('token')
|
|
43
|
+
// public link: ?account-id=...&form-id=...
|
|
44
|
+
let formId = getUrlParameter('form-id') || getUrlParameter('formid')
|
|
43
45
|
let previewJson = getUrlParameter('preview')
|
|
44
46
|
//previewJson = PREVIEW_B64
|
|
45
47
|
await store.init(accid)
|
|
@@ -62,7 +64,10 @@ async function load() {
|
|
|
62
64
|
}
|
|
63
65
|
t.locale = () => _locale
|
|
64
66
|
common.downloadLanguage(store, _locale).then(() => {
|
|
65
|
-
render(
|
|
67
|
+
render(
|
|
68
|
+
<FormPage store={store} t={t} token={token} formId={formId} previewJson={previewJson} />,
|
|
69
|
+
document.getElementById('form'),
|
|
70
|
+
)
|
|
66
71
|
})
|
|
67
72
|
}
|
|
68
73
|
|
package/src/form/rating-stars.js
CHANGED
|
@@ -1,88 +1,72 @@
|
|
|
1
1
|
import {Com} from '../common.js'
|
|
2
|
-
import {h
|
|
3
|
-
import sb from '../util/index.js'
|
|
4
|
-
|
|
5
|
-
export default class Form2 extends Com {
|
|
6
|
-
state = {
|
|
7
|
-
hovering: 0,
|
|
8
|
-
selected: 0,
|
|
9
|
-
}
|
|
2
|
+
import {h} from 'preact'
|
|
10
3
|
|
|
4
|
+
// Star rating. `count` stars (default 5), selecting level N highlights stars 1..N.
|
|
5
|
+
// Clicking the selected star again clears the value (onChange(0)).
|
|
6
|
+
export default class RatingStars extends Com {
|
|
11
7
|
constructor(props) {
|
|
12
8
|
super(props)
|
|
9
|
+
this.state = {hovering: 0, selected: Number(props.value) || 0}
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
onMouseEnter = (v) => {
|
|
16
|
-
let {selected
|
|
17
|
-
|
|
18
|
-
this.setState({hovering: selected})
|
|
19
|
-
return
|
|
20
|
-
}
|
|
21
|
-
this.setState({hovering: v})
|
|
13
|
+
let {selected} = this.state
|
|
14
|
+
this.setState({hovering: v <= selected ? selected : v})
|
|
22
15
|
}
|
|
23
16
|
|
|
24
17
|
onMouseLeave = () => {
|
|
25
|
-
let {selected, hovering} = this.state
|
|
26
18
|
this.setState({hovering: 0})
|
|
27
19
|
}
|
|
28
20
|
|
|
29
21
|
onToggleValue(v) {
|
|
30
|
-
let
|
|
31
|
-
let newVal = v
|
|
32
|
-
if (v === selected) newVal = 0
|
|
22
|
+
let newVal = v === this.state.selected ? 0 : v
|
|
33
23
|
this.setState({selected: newVal, hovering: newVal})
|
|
34
|
-
this.props.onChange(newVal)
|
|
24
|
+
if (this.props.onChange) this.props.onChange(newVal)
|
|
35
25
|
}
|
|
36
26
|
|
|
37
27
|
render() {
|
|
38
28
|
let {selected, hovering} = this.state
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
29
|
+
let count = Number(this.props.count) || 5
|
|
30
|
+
let readonly = !!this.props.readonly
|
|
31
|
+
let $stars = []
|
|
32
|
+
for (let i = 1; i <= count; i++) {
|
|
33
|
+
let cls = 'star-wrapper'
|
|
34
|
+
if (selected >= i) cls += ' selected'
|
|
35
|
+
else if (hovering >= i) cls += ' hovering'
|
|
36
|
+
if (readonly) {
|
|
37
|
+
$stars.push(
|
|
38
|
+
<div class={cls} key={i}>
|
|
39
|
+
<div class='star' />
|
|
40
|
+
</div>,
|
|
41
|
+
)
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
$stars.push(
|
|
45
|
+
<div
|
|
46
|
+
class={cls}
|
|
47
|
+
key={i}
|
|
48
|
+
role='radio'
|
|
49
|
+
aria-checked={selected === i ? 'true' : 'false'}
|
|
50
|
+
aria-label={String(i)}
|
|
51
|
+
tabIndex={0}
|
|
52
|
+
onClick={() => this.onToggleValue(i)}
|
|
53
|
+
onKeyDown={(e) => {
|
|
54
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
55
|
+
e.preventDefault()
|
|
56
|
+
this.onToggleValue(i)
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
onMouseEnter={() => this.onMouseEnter(i)}
|
|
60
|
+
>
|
|
61
|
+
<div class='star' />
|
|
62
|
+
</div>,
|
|
63
|
+
)
|
|
68
64
|
}
|
|
65
|
+
let cls = 'form-ratings-wrapper'
|
|
66
|
+
if (readonly) cls += ' readonly'
|
|
69
67
|
return (
|
|
70
|
-
<div class='
|
|
71
|
-
|
|
72
|
-
<div class='star' />
|
|
73
|
-
</div>
|
|
74
|
-
<div class={star2Cls} onClick={() => this.onToggleValue(2)} onMouseEnter={() => this.onMouseEnter(2)}>
|
|
75
|
-
<div class='star' />
|
|
76
|
-
</div>
|
|
77
|
-
<div class={star3Cls} onClick={() => this.onToggleValue(3)} onMouseEnter={() => this.onMouseEnter(3)}>
|
|
78
|
-
<div class='star' />
|
|
79
|
-
</div>
|
|
80
|
-
<div class={star4Cls} onClick={() => this.onToggleValue(4)} onMouseEnter={() => this.onMouseEnter(4)}>
|
|
81
|
-
<div class='star' />
|
|
82
|
-
</div>
|
|
83
|
-
<div class={star5Cls} onClick={() => this.onToggleValue(5)} onMouseEnter={() => this.onMouseEnter(5)}>
|
|
84
|
-
<div class='star' />
|
|
85
|
-
</div>
|
|
68
|
+
<div class={cls} role={readonly ? undefined : 'radiogroup'} onMouseLeave={() => this.onMouseLeave()}>
|
|
69
|
+
{$stars}
|
|
86
70
|
</div>
|
|
87
71
|
)
|
|
88
72
|
}
|
package/src/plugin.js
CHANGED
|
@@ -4,6 +4,7 @@ import {h, render, createRef} from 'preact'
|
|
|
4
4
|
|
|
5
5
|
// const LoadChatbox = () => import(/*webpackChunkName: "chatbox" */ './chatbox/main.js')
|
|
6
6
|
import Chatbox from './chatbox/main.js'
|
|
7
|
+
import {widgetGreeting} from './chatbox/greeting.js'
|
|
7
8
|
const LoadPopup = () => import(/*webpackChunkName: "popup" */ './popup/popup.js')
|
|
8
9
|
const LoadNotif = () => import(/*webpackChunkName: "notif" */ './notif/notif.js')
|
|
9
10
|
|
|
@@ -172,6 +173,15 @@ function Plugin(plugin, store, $container, lib) {
|
|
|
172
173
|
_locale = common.getLocale(plugin, locale)
|
|
173
174
|
await common.downloadLanguage(store, _locale)
|
|
174
175
|
this.t = t
|
|
176
|
+
if (plugin.type === 'chatbox') {
|
|
177
|
+
store.relocalizeLightConvos(_locale, t.i18n, {
|
|
178
|
+
widgetGreeting: () => widgetGreeting(plugin.chatbox, t),
|
|
179
|
+
previewGreetingText: () => {
|
|
180
|
+
let greeting = sb.get(plugin, 'chatbox.greeting', {})
|
|
181
|
+
return t.i18n(sb.get(greeting, 'greeting', {})) || greeting.message
|
|
182
|
+
},
|
|
183
|
+
})
|
|
184
|
+
}
|
|
175
185
|
_render()
|
|
176
186
|
return _locale
|
|
177
187
|
}
|
package/src/store/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sb from '../util/index.js'
|
|
2
|
+
import {loadWidgetSettings} from './widget-settings.js'
|
|
2
3
|
const ajax = require('@subiz/ajax/src/ajax.js')
|
|
3
4
|
|
|
4
5
|
function jsonifyTextPlain(param, done) {
|
|
@@ -309,8 +310,9 @@ export default class {
|
|
|
309
310
|
})
|
|
310
311
|
}
|
|
311
312
|
|
|
313
|
+
// Embedded widgets can use a relative API base, such as /4.0/ on the dashboard.
|
|
312
314
|
attachmentRequest = () => this.apireq
|
|
313
|
-
.setBaseUrl(new URL('/4.1', this.ApiURL).href)
|
|
315
|
+
.setBaseUrl(new URL('/4.1', new URL(this.ApiURL, document.location.href)).href)
|
|
314
316
|
.addQuery('x-account-id', this.accid)
|
|
315
317
|
|
|
316
318
|
attachmentJSON = async (path, body, method = 'post') => {
|
|
@@ -387,12 +389,7 @@ export default class {
|
|
|
387
389
|
getUser = (_) => this.apireq.get(`/accounts/${this.accid}/users/${this.uid}`)
|
|
388
390
|
getAccountSetting = async (p) => {
|
|
389
391
|
let href = sb.get(document, 'location.href') // send full href, this could be used to select which bot (ai-agent) should be returned
|
|
390
|
-
|
|
391
|
-
if (p && p.bot_id) {
|
|
392
|
-
res = await this.apireq.addQuery('url', href).addQuery('bot_id', p.bot_id).get(`/accounts/${this.accid}/settings`)
|
|
393
|
-
} else {
|
|
394
|
-
res = await this.apireq.addQuery('url', href).get(`/accounts/${this.accid}/settings`)
|
|
395
|
-
}
|
|
392
|
+
const res = await loadWidgetSettings(this.apireq, this.ApiURL, this.accid, href, p && p.bot_id)
|
|
396
393
|
if (res.error) return res
|
|
397
394
|
|
|
398
395
|
// fix image error image url link in popup
|
|
@@ -441,6 +438,11 @@ export default class {
|
|
|
441
438
|
|
|
442
439
|
checkFormToken = (accid, token) => this.api41.addQuery('account-id', accid).get(`/submissions/${token}`)
|
|
443
440
|
submitForm2 = (accid, token, data) => this.api41.addQuery('account-id', accid).post(`/submissions/${token}`, data)
|
|
441
|
+
// public form (no token): submitter is the current visitor
|
|
442
|
+
getPublicForm = (formid) =>
|
|
443
|
+
this.websiteFormRequest().get(`/accounts/${this.accid}/public-forms/${encodeURIComponent(formid)}`)
|
|
444
|
+
submitPublicForm = (formid, data) =>
|
|
445
|
+
this.websiteFormRequest().post(`/accounts/${this.accid}/public-forms/${encodeURIComponent(formid)}/submissions`, data)
|
|
444
446
|
suggest_address = (keyword) => this.apireq.setQuery({keyword: keyword}).get(`/accounts/${this.accid}/addresses`)
|
|
445
447
|
create_ticket = (ticket) => this.apireq.post(`/accounts/${this.accid}/tickets`, ticket)
|
|
446
448
|
}
|
package/src/store/api_fake.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sb from '../util/index.js'
|
|
2
|
+
import {loadWidgetSettings} from './widget-settings.js'
|
|
2
3
|
|
|
3
4
|
const ajax = require('@subiz/ajax/src/ajax.js')
|
|
4
5
|
|
|
@@ -13,6 +14,7 @@ const OK200 = Promise.resolve({code: 200})
|
|
|
13
14
|
export default class {
|
|
14
15
|
constructor(ApiURL, FileURL) {
|
|
15
16
|
this.FileURL = FileURL
|
|
17
|
+
this.ApiURL = ApiURL
|
|
16
18
|
this.apireq = ajax.setBaseUrl(ApiURL).setContentType('text/plain').beforeHook(jsonifyTextPlain).setParser('json')
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -84,7 +86,7 @@ export default class {
|
|
|
84
86
|
],
|
|
85
87
|
},
|
|
86
88
|
})
|
|
87
|
-
getAccountSetting = () => this.apireq.
|
|
89
|
+
getAccountSetting = (p) => loadWidgetSettings(this.apireq, this.ApiURL, this.accid, document.location.href, p && p.bot_id)
|
|
88
90
|
|
|
89
91
|
sendImpression = (impression) => OK200
|
|
90
92
|
sendConversion = (conversion) => OK200
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sb from '../util/index.js'
|
|
2
|
+
|
|
3
|
+
// pick(i18nObject) returns the localized value or a falsy value when none matches.
|
|
4
|
+
export const pickLocale = (locale) => (i18n) => {
|
|
5
|
+
if (!i18n || !locale) return undefined
|
|
6
|
+
return i18n[locale.replace('-', '_')] || i18n[locale]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Rewrites the visible fields of a message from its i18n fields, in place.
|
|
10
|
+
// Fields without a matching translation keep their current value.
|
|
11
|
+
// Returns true when anything changed.
|
|
12
|
+
export function localizeMessage(message, pick) {
|
|
13
|
+
if (!message) return false
|
|
14
|
+
let changed = false
|
|
15
|
+
const block = message.i18n_block && pick(message.i18n_block)
|
|
16
|
+
if (block) {
|
|
17
|
+
message.block = sb.cloneDeep(block)
|
|
18
|
+
message.text = sb.blockToText(block)
|
|
19
|
+
changed = true
|
|
20
|
+
}
|
|
21
|
+
sb.map(message.attachments, (att) => {
|
|
22
|
+
sb.map(att && att.quick_replies, (qr) => {
|
|
23
|
+
const title = qr && qr.i18n_title && pick(qr.i18n_title)
|
|
24
|
+
if (!title) return
|
|
25
|
+
qr.title = title
|
|
26
|
+
changed = true
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
return changed
|
|
30
|
+
}
|
package/src/store/store.js
CHANGED
|
@@ -7,6 +7,7 @@ import KV from './kv.js'
|
|
|
7
7
|
import APIFake from './api_fake.js'
|
|
8
8
|
import RealtimeFake from './realtime_fake.js'
|
|
9
9
|
import {uploadAttachments} from './attachments.js'
|
|
10
|
+
import {localizeMessage, pickLocale} from './localize.js'
|
|
10
11
|
var Realtime = require('@subiz/wsclient')
|
|
11
12
|
var cookie = require('cookie_js')
|
|
12
13
|
cookie.defaults.path = '/'
|
|
@@ -637,6 +638,8 @@ export default class Store {
|
|
|
637
638
|
|
|
638
639
|
checkFormToken = (accid, token) => this.api.checkFormToken(accid, token)
|
|
639
640
|
submitForm2 = (accid, token, data) => this.api.submitForm2(accid, token, data)
|
|
641
|
+
getPublicForm = (formid) => this.api.getPublicForm(formid)
|
|
642
|
+
submitPublicForm = (formid, data) => this.api.submitPublicForm(formid, data)
|
|
640
643
|
sendPresence = sb.throttle(
|
|
641
644
|
async () => {
|
|
642
645
|
if (!this._track_page_view) return
|
|
@@ -1311,6 +1314,65 @@ export default class Store {
|
|
|
1311
1314
|
|
|
1312
1315
|
_typing = sb.throttle((convoid) => this.api.sendTyping(convoid), 3000, {trailing: false})
|
|
1313
1316
|
|
|
1317
|
+
// Re-translates stored light convos and the button preview after the chatbox locale changes.
|
|
1318
|
+
// pick(i18nObject) returns the localized value.
|
|
1319
|
+
// opts.widgetGreeting() rebuilds the widget greeting event,
|
|
1320
|
+
// opts.previewGreetingText() returns the chatbox greeting text shown in the button preview.
|
|
1321
|
+
relocalizeLightConvos(locale, pick, opts = {}) {
|
|
1322
|
+
let changed = []
|
|
1323
|
+
sb.map(this.lightConvos, (convo) => {
|
|
1324
|
+
if (!convo) return
|
|
1325
|
+
sb.map(convo.events, (ev) => {
|
|
1326
|
+
let message = sb.get(ev, 'data.message')
|
|
1327
|
+
if (!message) return
|
|
1328
|
+
let ok = false
|
|
1329
|
+
if (convo.light_type === 'widget' && sb.get(ev, 'id', '').startsWith('widget-greeting-')) {
|
|
1330
|
+
let text = opts.widgetGreeting && sb.get(opts.widgetGreeting(), 'data.message.text')
|
|
1331
|
+
if (text) {
|
|
1332
|
+
message.text = text
|
|
1333
|
+
ok = true
|
|
1334
|
+
}
|
|
1335
|
+
} else {
|
|
1336
|
+
ok = localizeMessage(message, pick)
|
|
1337
|
+
}
|
|
1338
|
+
if (!ok) return
|
|
1339
|
+
if (sb.get(convo, 'last_message_sent.id') === ev.id) convo.last_message_sent = sb.cloneDeep(ev)
|
|
1340
|
+
changed.push(ev)
|
|
1341
|
+
})
|
|
1342
|
+
convo.locale = locale
|
|
1343
|
+
})
|
|
1344
|
+
if (sb.size(this.lightConvos)) this.kv.put('light_convos', this.lightConvos)
|
|
1345
|
+
this.relocalizePreview(pick, opts.previewGreetingText)
|
|
1346
|
+
// re-render only: publishing 'events' would re-trigger greeting previews and subiz('on') callbacks
|
|
1347
|
+
if (changed.length) this.pubsub.publish('account')
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// lastview.previewEvent is a snapshot (persisted separately from light_convos), so it must be rebuilt.
|
|
1351
|
+
relocalizePreview(pick, previewGreetingText) {
|
|
1352
|
+
let previewEvent = sb.get(this.matchLastview(), 'previewEvent')
|
|
1353
|
+
if (!sb.size(previewEvent)) return
|
|
1354
|
+
let cid = sb.get(previewEvent, 'data.message.conversation_id')
|
|
1355
|
+
let next = sb.cloneDeep(previewEvent)
|
|
1356
|
+
let ok = false
|
|
1357
|
+
if (!cid) {
|
|
1358
|
+
// greeting built from chatbox setting (ChatButton.pickBestGreeting)
|
|
1359
|
+
let text = previewGreetingText && previewGreetingText()
|
|
1360
|
+
if (text) {
|
|
1361
|
+
next.data.message.text = text
|
|
1362
|
+
ok = true
|
|
1363
|
+
}
|
|
1364
|
+
} else {
|
|
1365
|
+
let ev = previewEvent.id && sb.find(sb.get(this.lightConvos, [cid, 'events']), (e) => e && e.id === previewEvent.id)
|
|
1366
|
+
if (ev) {
|
|
1367
|
+
next = sb.cloneDeep(ev)
|
|
1368
|
+
ok = true
|
|
1369
|
+
} else {
|
|
1370
|
+
ok = localizeMessage(sb.get(next, 'data.message'), pick)
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
if (ok) this.updateLastview({previewEvent: next})
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1314
1376
|
deleteLightConvo(id) {
|
|
1315
1377
|
delete this.lightConvos[id]
|
|
1316
1378
|
this.kv.put('light_convos', this.lightConvos)
|
|
@@ -1365,8 +1427,9 @@ export default class Store {
|
|
|
1365
1427
|
ev.id = 'evfake' + now + idx
|
|
1366
1428
|
ev.type = 'message_sent'
|
|
1367
1429
|
ev.by = {id: botid, type: 'bot'}
|
|
1368
|
-
ev.data = {message: message}
|
|
1430
|
+
ev.data = {message: sb.cloneDeep(message)}
|
|
1369
1431
|
ev.data.message.conversation_id = convoid
|
|
1432
|
+
localizeMessage(ev.data.message, pickLocale(locale))
|
|
1370
1433
|
lastev = ev
|
|
1371
1434
|
return ev
|
|
1372
1435
|
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const ajax = require('@subiz/ajax/src/ajax.js')
|
|
2
|
+
|
|
3
|
+
function failed(response) {
|
|
4
|
+
return !response || response.error || response.body?.error || response.code < 200 || response.code >= 300 || !response.body || typeof response.body !== 'object'
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// A separate client keeps visitor identity, authentication hooks and cookies
|
|
8
|
+
// out of the shared Cloudflare cache key.
|
|
9
|
+
async function readConfig(query) {
|
|
10
|
+
const request = ajax.withCredentials(false).setParser('json').setQuery(query)
|
|
11
|
+
let response
|
|
12
|
+
try {
|
|
13
|
+
response = await request.setBaseUrl('https://api2.sbz.vn/4.1').get('/widget-config')
|
|
14
|
+
if (!failed(response)) return response
|
|
15
|
+
} catch (_) {
|
|
16
|
+
// CSP and network failures can reject instead of returning an error envelope.
|
|
17
|
+
}
|
|
18
|
+
return request.setBaseUrl('https://api.sbz.vn/4.1').get('/widget-config')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadWidgetSettings(contextRequest, apiURL, accountId, pageURL, botId) {
|
|
22
|
+
const query = {'account-id': accountId, url: pageURL, v: '6'}
|
|
23
|
+
if (botId) query.bot_id = botId
|
|
24
|
+
const contextBase = new URL('/4.1', new URL(apiURL, document.location.href)).href
|
|
25
|
+
const [config, context] = await Promise.all([
|
|
26
|
+
readConfig(query),
|
|
27
|
+
contextRequest.setBaseUrl(contextBase).setQuery(query).get('/widget-context'),
|
|
28
|
+
])
|
|
29
|
+
// Preserve the API error body for the existing banned/locked-account handling.
|
|
30
|
+
if (failed(context)) return {...context, error: context?.error || true}
|
|
31
|
+
if (failed(config)) return {...config, error: config?.error || true}
|
|
32
|
+
return {...config, body: mergeWidgetSettings(config.body, context.body)}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function mergeWidgetSettings(config, context) {
|
|
36
|
+
const dynamicPlugins = new Map((context.plugins || []).map(plugin => [plugin.id, plugin]))
|
|
37
|
+
const plugins = (config.plugins || []).map(plugin => {
|
|
38
|
+
const dynamic = dynamicPlugins.get(plugin.id)
|
|
39
|
+
if (!dynamic) return plugin
|
|
40
|
+
const merged = {...plugin, ...dynamic}
|
|
41
|
+
if (dynamic.chatbox) merged.chatbox = {...plugin.chatbox, ...dynamic.chatbox}
|
|
42
|
+
if (dynamic.notification) merged.notification = {...plugin.notification, ...dynamic.notification}
|
|
43
|
+
return merged
|
|
44
|
+
})
|
|
45
|
+
return {...config, ...context, plugins}
|
|
46
|
+
}
|
package/src/util/common.js
CHANGED
|
@@ -836,7 +836,7 @@ export function setCss(id, $css, css) {
|
|
|
836
836
|
}
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
-
export const DEF_CHAT_BUTTON_BACKGROUND_IMAGE_URL = '
|
|
839
|
+
export const DEF_CHAT_BUTTON_BACKGROUND_IMAGE_URL = require('../../assets/img/bubble_11.svg?inline')
|
|
840
840
|
|
|
841
841
|
export const DEFVAL = {
|
|
842
842
|
primary_color: '#0265FF',
|
package/test.cjs
CHANGED
|
@@ -34,6 +34,9 @@ Module._extensions['.js'] = (module, filename) => {
|
|
|
34
34
|
const originalLoad = Module._load
|
|
35
35
|
Module._load = function(request, parent, isMain) {
|
|
36
36
|
if (request.startsWith('css-loader!')) return {default: {toString: () => ''}}
|
|
37
|
+
if (request.endsWith('.svg?inline')) {
|
|
38
|
+
return 'data:image/svg+xml;base64,' + fs.readFileSync(path.resolve(path.dirname(parent.filename), request.slice(0, -7))).toString('base64')
|
|
39
|
+
}
|
|
37
40
|
return originalLoad.call(this, request, parent, isMain)
|
|
38
41
|
}
|
|
39
42
|
for (const extension of ['.svg', '.png', '.jpg', '.gif', '.mp3', '.ogg']) {
|
package/webpack.dev.js
CHANGED
|
@@ -34,8 +34,10 @@ module.exports = {
|
|
|
34
34
|
module: {
|
|
35
35
|
rules: [
|
|
36
36
|
{test: /\.less$/, loader: 'less-loader'},
|
|
37
|
+
{test: /\.svg$/i, resourceQuery: /inline/, type: 'asset/inline'},
|
|
37
38
|
{
|
|
38
39
|
test: /\.(svg|png|jpe?g|gif|ogg|mp3)$/i,
|
|
40
|
+
resourceQuery: {not: [/inline/]},
|
|
39
41
|
use: [
|
|
40
42
|
{
|
|
41
43
|
loader: 'file-loader',
|