@miso.ai/client-sdk-ui 1.10.0-beta.0 → 1.10.0-beta.10
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/dist/cjs/actor/autocomplete.js +157 -0
- package/dist/cjs/actor/data.js +4 -12
- package/dist/cjs/actor/fields.js +8 -0
- package/dist/cjs/actor/index.js +7 -0
- package/dist/cjs/actor/tracker.js +46 -70
- package/dist/cjs/actor/trackers.js +5 -1
- package/dist/cjs/actor/utils.js +16 -2
- package/dist/cjs/actor/view.js +14 -3
- package/dist/cjs/actor/views.js +9 -1
- package/dist/cjs/affiliation/index.js +126 -0
- package/dist/cjs/combo/ask/options.js +1 -1
- package/dist/cjs/combo/base.js +1 -1
- package/dist/cjs/constants.js +6 -3
- package/dist/cjs/defaults/ask/controls.js +1 -1
- package/dist/cjs/defaults/ask/phrases.js +1 -1
- package/dist/cjs/defaults/ask/templates.js +35 -4
- package/dist/cjs/element/role/index.js +7 -0
- package/dist/cjs/element/role/miso-affiliation.js +20 -0
- package/dist/cjs/layout/affiliation/affiliation.js +330 -0
- package/dist/cjs/layout/banner/banner.js +1 -1
- package/dist/cjs/layout/container/container.js +1 -1
- package/dist/cjs/layout/error/error.js +1 -1
- package/dist/cjs/layout/index.js +7 -0
- package/dist/cjs/layout/input/option-list.js +35 -50
- package/dist/cjs/layout/input/search-box.js +227 -129
- package/dist/cjs/layout/list/cards.js +1 -1
- package/dist/cjs/layout/list/carousel.js +5 -3
- package/dist/cjs/layout/list/collection.js +44 -49
- package/dist/cjs/layout/list/list.js +1 -1
- package/dist/cjs/layout/radio/feedback.js +1 -1
- package/dist/cjs/layout/radio/radio.js +1 -1
- package/dist/cjs/layout/raf.js +1 -1
- package/dist/cjs/layout/template.js +1 -1
- package/dist/cjs/layout/templates.js +172 -66
- package/dist/cjs/layout/text/text.js +1 -1
- package/dist/cjs/layout/text/typewriter/index.js +3 -3
- package/dist/cjs/layout/trackable.js +124 -0
- package/dist/cjs/plugin.js +1 -1
- package/dist/cjs/source/index.js +20 -8
- package/dist/cjs/util/bindings.js +58 -110
- package/dist/cjs/util/states.js +31 -24
- package/dist/cjs/util/trackers.js +75 -0
- package/dist/cjs/util/viewables.js +55 -0
- package/dist/cjs/version.js +1 -1
- package/dist/cjs/workflow/ask.js +60 -19
- package/dist/cjs/workflow/asks.js +1 -1
- package/dist/cjs/workflow/base.js +6 -7
- package/dist/cjs/workflow/explore.js +5 -2
- package/dist/cjs/workflow/options.js +60 -14
- package/dist/cjs/workflow/recommendation.js +2 -0
- package/dist/cjs/workflow/search.js +35 -7
- package/package.json +2 -2
- package/src/actor/autocomplete.js +131 -0
- package/src/actor/data.js +3 -10
- package/src/actor/fields.js +8 -0
- package/src/actor/index.js +1 -0
- package/src/actor/tracker.js +39 -62
- package/src/actor/trackers.js +3 -1
- package/src/actor/utils.js +14 -2
- package/src/actor/view.js +12 -1
- package/src/actor/views.js +11 -0
- package/src/affiliation/index.js +108 -0
- package/src/constants.js +10 -1
- package/src/defaults/ask/phrases.js +1 -1
- package/src/defaults/ask/templates.js +34 -2
- package/src/element/role/index.js +1 -0
- package/src/element/role/miso-affiliation.js +16 -0
- package/src/layout/affiliation/affiliation.js +295 -0
- package/src/layout/index.js +1 -0
- package/src/layout/input/option-list.js +35 -43
- package/src/layout/input/search-box.js +192 -95
- package/src/layout/list/carousel.js +4 -2
- package/src/layout/list/collection.js +42 -43
- package/src/layout/templates.js +127 -46
- package/src/layout/text/typewriter/index.js +2 -2
- package/src/layout/trackable.js +106 -0
- package/src/source/index.js +14 -1
- package/src/util/bindings.js +47 -103
- package/src/util/states.js +26 -20
- package/src/util/trackers.js +69 -0
- package/src/util/viewables.js +47 -0
- package/src/version.js +1 -1
- package/src/workflow/ask.js +37 -3
- package/src/workflow/base.js +2 -3
- package/src/workflow/explore.js +1 -1
- package/src/workflow/options.js +54 -13
- package/src/workflow/recommendation.js +1 -0
- package/src/workflow/search.js +33 -1
- package/dist/cjs/util/tracker.js +0 -348
- package/src/util/tracker.js +0 -318
|
@@ -1,53 +1,104 @@
|
|
|
1
|
-
import { escapeHtml } from '@miso.ai/commons';
|
|
2
|
-
import { LAYOUT_CATEGORY } from '../../constants.js';
|
|
1
|
+
import { defineValues, escapeHtml, findInAncestors, debounce } from '@miso.ai/commons';
|
|
2
|
+
import { LAYOUT_CATEGORY, STATUS } from '../../constants.js';
|
|
3
3
|
import { fields } from '../../actor/index.js';
|
|
4
4
|
import TemplateBasedLayout from '../template.js';
|
|
5
|
+
import { helpers, imageBlock, productInfoBlock } from '../templates.js';
|
|
5
6
|
import { SEND, SEARCH } from '../../asset/svgs.js';
|
|
6
7
|
|
|
7
8
|
const TYPE = 'search-box';
|
|
8
9
|
const DEFAULT_CLASSNAME = 'miso-search-box';
|
|
10
|
+
const DEFAULT_AUTOCOMPLETE_CLASSNAME = 'miso-autocomplete';
|
|
9
11
|
|
|
10
12
|
function root(layout) {
|
|
11
|
-
const { className, role,
|
|
12
|
-
const {
|
|
13
|
+
const { className, role, options, workflow, autocomplete } = layout;
|
|
14
|
+
const { placeholder, buttonText } = options;
|
|
13
15
|
const roleAttr = role ? `data-role="${role}"` : '';
|
|
14
16
|
const buttonContent = buttonText || workflow === 'search' ? SEARCH : SEND;
|
|
15
17
|
return `
|
|
16
18
|
<div class="${className}" ${roleAttr}>
|
|
17
19
|
<div class="${className}__input-group">
|
|
18
20
|
<input class="${className}__input" type="text" data-role="input" ${placeholder ? `placeholder="${placeholder}"` : ''}>
|
|
19
|
-
<button class="${className}__button" type="submit" data-role="
|
|
21
|
+
<button class="${className}__button" type="submit" data-role="submit">${buttonContent}</button>
|
|
20
22
|
</div>
|
|
21
|
-
${autocomplete
|
|
22
|
-
</div
|
|
23
|
-
`;
|
|
23
|
+
<div class="${autocomplete.className}" data-role="autocomplete"></div>
|
|
24
|
+
</div>`.trim();
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function
|
|
27
|
-
const {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
function completions(layout, items) {
|
|
28
|
+
const { templates, workflow, autocomplete } = layout;
|
|
29
|
+
// use options under layout.autocomplete from now on
|
|
30
|
+
layout = {
|
|
31
|
+
templates,
|
|
32
|
+
workflow,
|
|
33
|
+
...autocomplete,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const products = [], queries = [];
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
(item.product ? products : queries).push(item);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return templates.queries(layout, queries) + (queries.length > 0 && products.length > 0 ? `<hr>` : '') + templates.products(layout, products);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function queries(layout, items) {
|
|
45
|
+
const { templates } = layout;
|
|
46
|
+
return templates.completionList(layout, 'query', items);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function products(layout, items) {
|
|
50
|
+
const { templates } = layout;
|
|
51
|
+
return templates.completionList(layout, 'product', items);
|
|
33
52
|
}
|
|
34
53
|
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
return `<ol class="${className}__suggestion-list" data-role="suggestion-list"></ol>`;
|
|
54
|
+
function query(layout, { text, text_with_inverted_markups } = {}) {
|
|
55
|
+
return text_with_inverted_markups || escapeHtml(text);
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
function
|
|
41
|
-
const {
|
|
42
|
-
const
|
|
43
|
-
return
|
|
58
|
+
function product(layout, { product }) {
|
|
59
|
+
const { templates } = layout;
|
|
60
|
+
const [openTag, closeTag] = helpers.tagPair(layout, product, { role: false });
|
|
61
|
+
return [
|
|
62
|
+
openTag,
|
|
63
|
+
templates.imageBlock(layout, product),
|
|
64
|
+
templates.productInfoBlock(layout, product),
|
|
65
|
+
closeTag,
|
|
66
|
+
].join('');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function completionList(layout, type, items) {
|
|
70
|
+
const { className, templates } = layout;
|
|
71
|
+
return `
|
|
72
|
+
<ul class="${className}__${type}-list" data-role="${type}-list">
|
|
73
|
+
${items.map(item => templates.completionItem(layout, type, item)).join('')}
|
|
74
|
+
</ul>`.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function completionItem(layout, type, item) {
|
|
78
|
+
const { className, templates } = layout;
|
|
79
|
+
return `
|
|
80
|
+
<li class="${className}__${type}-item" data-role="${type}-item" data-index="${item._index}" tabindex="0">
|
|
81
|
+
${templates[type](layout, item)}
|
|
82
|
+
</li>`.trim();
|
|
44
83
|
}
|
|
45
84
|
|
|
46
85
|
const DEFAULT_TEMPLATES = Object.freeze({
|
|
47
86
|
root,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
completions,
|
|
88
|
+
completionList,
|
|
89
|
+
completionItem,
|
|
90
|
+
queries,
|
|
91
|
+
products,
|
|
92
|
+
query,
|
|
93
|
+
product,
|
|
94
|
+
imageBlock,
|
|
95
|
+
productInfoBlock,
|
|
96
|
+
helpers,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
|
|
100
|
+
className: DEFAULT_AUTOCOMPLETE_CLASSNAME,
|
|
101
|
+
debounce: 300,
|
|
51
102
|
});
|
|
52
103
|
|
|
53
104
|
export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
@@ -68,31 +119,36 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
68
119
|
return DEFAULT_CLASSNAME;
|
|
69
120
|
}
|
|
70
121
|
|
|
71
|
-
constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
|
|
122
|
+
constructor({ className = DEFAULT_CLASSNAME, templates, autocomplete, ...options } = {}) {
|
|
72
123
|
super({
|
|
73
124
|
className,
|
|
74
125
|
templates: { ...DEFAULT_TEMPLATES, ...templates },
|
|
75
126
|
...options,
|
|
76
127
|
});
|
|
128
|
+
defineValues(this, {
|
|
129
|
+
autocomplete: { ...DEFAULT_AUTOCOMPLETE_OPTIONS, ...autocomplete },
|
|
130
|
+
});
|
|
77
131
|
this._contexts = new WeakMap();
|
|
78
|
-
this.
|
|
132
|
+
this._prevInput = undefined;
|
|
133
|
+
this._debounce = debounce(this.autocomplete.debounce);
|
|
79
134
|
}
|
|
80
135
|
|
|
81
136
|
initialize(view) {
|
|
82
|
-
const { autocomplete } = this.options;
|
|
83
137
|
this._view = view;
|
|
84
138
|
const { proxyElement, hub } = view;
|
|
85
139
|
this._unsubscribes = [
|
|
86
140
|
...this._unsubscribes,
|
|
141
|
+
proxyElement.on('input', (e) => this._syncInput(e)),
|
|
87
142
|
proxyElement.on('keydown', (e) => this._handleKeyDown(e)),
|
|
88
143
|
proxyElement.on('click', (e) => this._handleClick(e)),
|
|
144
|
+
proxyElement.on('focusin', (e) => this._handleFocusIn(e)),
|
|
145
|
+
proxyElement.on('focusout', (e) => this._handleFocusOut(e)),
|
|
89
146
|
];
|
|
90
|
-
|
|
91
|
-
this._unsubscribes.push(hub.on(fields.suggestions(), () => this._handleInput()));
|
|
92
|
-
}
|
|
147
|
+
this._unsubscribes.push(hub.on(fields.completions(), () => view.refresh({ force: true })));
|
|
93
148
|
}
|
|
94
149
|
|
|
95
150
|
focus() {
|
|
151
|
+
// TODO: support blur as well
|
|
96
152
|
if (this._element) {
|
|
97
153
|
this._context().focus();
|
|
98
154
|
} else {
|
|
@@ -101,6 +157,14 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
101
157
|
}
|
|
102
158
|
}
|
|
103
159
|
|
|
160
|
+
open() {
|
|
161
|
+
this._element.classList.add('open');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
close() {
|
|
165
|
+
this._element.classList.remove('open');
|
|
166
|
+
}
|
|
167
|
+
|
|
104
168
|
set value(value) {
|
|
105
169
|
if (this._element) {
|
|
106
170
|
this._context().value = value;
|
|
@@ -111,12 +175,10 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
111
175
|
|
|
112
176
|
_preprocess({ state }) {
|
|
113
177
|
state = super._preprocess({ state });
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
const suggestions = this._view.hub.states[fields.suggestions()];
|
|
119
|
-
return { ...state, suggestions };
|
|
178
|
+
return {
|
|
179
|
+
...state,
|
|
180
|
+
completions: this._view.hub.states[fields.completions()],
|
|
181
|
+
};
|
|
120
182
|
}
|
|
121
183
|
|
|
122
184
|
_render(element, data, controls) {
|
|
@@ -124,42 +186,32 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
124
186
|
// only render the template once, and also skip if there are children already to allow customized DOM
|
|
125
187
|
super._render(element, data, controls);
|
|
126
188
|
}
|
|
127
|
-
this.
|
|
128
|
-
this._renderSuggestions(element, data, controls);
|
|
189
|
+
this._updateCompletions(element, data, controls);
|
|
129
190
|
this._fullfillFocusRequest(element);
|
|
130
191
|
this._fullfillValueRequest(element);
|
|
131
192
|
}
|
|
132
193
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!autocomplete) {
|
|
194
|
+
_updateCompletions(element, { state, rendered }, { writeToState }) {
|
|
195
|
+
if (!state.completions) {
|
|
136
196
|
return;
|
|
137
197
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
_renderSuggestions(element, { state }, { writeToState } = {}) {
|
|
142
|
-
const { autocomplete } = this.options;
|
|
143
|
-
if (!autocomplete) {
|
|
198
|
+
const { status = STATUS.INITIAL, index, value: completions } = state.completions;
|
|
199
|
+
if (rendered && rendered.completions.index === index && rendered.completions.status === status) {
|
|
144
200
|
return;
|
|
145
201
|
}
|
|
146
|
-
|
|
147
|
-
const {
|
|
148
|
-
if (!
|
|
149
|
-
// not actually rendered
|
|
150
|
-
writeToState && writeToState({ suggestions: { value: [] } });
|
|
202
|
+
const context = this._context(element);
|
|
203
|
+
const { autocompleteElement } = context;
|
|
204
|
+
if (!autocompleteElement) {
|
|
151
205
|
return;
|
|
152
206
|
}
|
|
153
207
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
autocompleteElement.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this._suggestionItems.set(itemElement, suggestionItems[i++]);
|
|
162
|
-
}
|
|
208
|
+
const items = context.trackItems(completions);
|
|
209
|
+
|
|
210
|
+
element.classList[items.length > 0 ? 'add' : 'remove']('nonempty');
|
|
211
|
+
autocompleteElement.setAttribute('data-status', status);
|
|
212
|
+
autocompleteElement.innerHTML = this.templates.completions(this, items);
|
|
213
|
+
|
|
214
|
+
writeToState({ completions: { index, status } });
|
|
163
215
|
}
|
|
164
216
|
|
|
165
217
|
_fullfillFocusRequest(element) {
|
|
@@ -190,39 +242,80 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
190
242
|
return context;
|
|
191
243
|
}
|
|
192
244
|
|
|
245
|
+
_syncInput() {
|
|
246
|
+
const { inputElement } = this._context();
|
|
247
|
+
if (!inputElement) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const value = inputElement.value.trim();
|
|
251
|
+
|
|
252
|
+
if (this._prevInput === value) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
this._prevInput = value;
|
|
256
|
+
|
|
257
|
+
const self = this;
|
|
258
|
+
this._debounce(() => self._view.hub.update(fields.input(), { value }));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_handleFocusIn({ target }) {
|
|
262
|
+
if (!this._element) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (target.matches('[data-role="input"]')) {
|
|
266
|
+
this._syncInput();
|
|
267
|
+
this.open();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
_handleFocusOut({ relatedTarget }) {
|
|
272
|
+
if (!this._element) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!relatedTarget || !findInAncestors(relatedTarget, element => element === this._element || undefined)) {
|
|
276
|
+
this.close();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
193
280
|
_handleKeyDown({ key, target, isComposing }) {
|
|
194
281
|
if (!isComposing && key === 'Enter' && target.matches('input')) {
|
|
195
282
|
this._submit(target.value);
|
|
196
283
|
target.blur();
|
|
197
284
|
}
|
|
285
|
+
// TODO: select completion items with arrow keys
|
|
198
286
|
}
|
|
199
287
|
|
|
200
|
-
_handleClick(
|
|
288
|
+
_handleClick(event) {
|
|
289
|
+
const { target } = event;
|
|
201
290
|
const { inputElement } = this._context();
|
|
202
|
-
for (let element = target; element && element !== this.
|
|
203
|
-
if (element.matches('[
|
|
291
|
+
for (let element = target; element && element !== this._element; element = element.parentElement) {
|
|
292
|
+
if (element.matches('[data-role="submit"]') || element.matches('[data-role="button"]') || element.matches('[type="submit"]')) {
|
|
204
293
|
if (inputElement) {
|
|
205
294
|
inputElement.blur();
|
|
206
295
|
this._submit(inputElement.value);
|
|
207
296
|
}
|
|
208
297
|
return;
|
|
209
298
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
299
|
+
if (element.matches('[data-role="query-item"]')) {
|
|
300
|
+
const index = element.getAttribute('data-index');
|
|
301
|
+
const item = this._context().data.get(parseInt(index));
|
|
302
|
+
const value = item && item.text;
|
|
303
|
+
if (value) {
|
|
304
|
+
if (inputElement) {
|
|
305
|
+
inputElement.value = value;
|
|
306
|
+
}
|
|
307
|
+
this._view._events.emit('select', Object.freeze({ value: item, index, domEvent: event }));
|
|
308
|
+
if (!event.defaultPrevented) {
|
|
309
|
+
this._submit(value);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
this.close();
|
|
313
|
+
return;
|
|
219
314
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
315
|
+
if (element.matches('[data-role="product-item"]')) {
|
|
316
|
+
this._view._events.emit('select', Object.freeze({ value: item, index, domEvent: event }));
|
|
317
|
+
return;
|
|
223
318
|
}
|
|
224
|
-
this._submit(value);
|
|
225
|
-
return;
|
|
226
319
|
}
|
|
227
320
|
}
|
|
228
321
|
|
|
@@ -230,6 +323,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
|
|
|
230
323
|
if (!value) {
|
|
231
324
|
return;
|
|
232
325
|
}
|
|
326
|
+
this.close();
|
|
233
327
|
// TODO: q -> value
|
|
234
328
|
this._view.hub.trigger(fields.query(), { q: value });
|
|
235
329
|
}
|
|
@@ -245,6 +339,7 @@ class Context {
|
|
|
245
339
|
constructor(element) {
|
|
246
340
|
this._element = element;
|
|
247
341
|
this._cache = {};
|
|
342
|
+
this._data = new Map();
|
|
248
343
|
}
|
|
249
344
|
|
|
250
345
|
get inputElement() {
|
|
@@ -255,25 +350,27 @@ class Context {
|
|
|
255
350
|
return this._get('autocomplete');
|
|
256
351
|
}
|
|
257
352
|
|
|
258
|
-
get
|
|
259
|
-
return this.
|
|
353
|
+
get data() {
|
|
354
|
+
return this._data;
|
|
260
355
|
}
|
|
261
356
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
357
|
+
trackItems(completions) {
|
|
358
|
+
let index = 0;
|
|
359
|
+
const data = this._data = new Map();
|
|
360
|
+
const items = [];
|
|
361
|
+
for (const field in completions) {
|
|
362
|
+
for (const item of completions[field]) {
|
|
363
|
+
if (item.product && !item.product.url) {
|
|
364
|
+
continue; // product without URL is not clickable
|
|
365
|
+
}
|
|
366
|
+
item._index = index;
|
|
367
|
+
item._field = field;
|
|
368
|
+
data.set(index, item);
|
|
369
|
+
items.push(item);
|
|
370
|
+
index++;
|
|
371
|
+
}
|
|
276
372
|
}
|
|
373
|
+
return items;
|
|
277
374
|
}
|
|
278
375
|
|
|
279
376
|
focus() {
|
|
@@ -17,7 +17,8 @@ const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
|
|
|
17
17
|
|
|
18
18
|
function ready(layout, state) {
|
|
19
19
|
const { templates } = layout;
|
|
20
|
-
const
|
|
20
|
+
const items = layout._getItems(state);
|
|
21
|
+
const nonEmpty = items && items.length > 0;
|
|
21
22
|
const html = Object.getPrototypeOf(layout.constructor).defaultTemplates.ready(layout, state);
|
|
22
23
|
return nonEmpty ? templates.carousel(layout, state, html) : html;
|
|
23
24
|
}
|
|
@@ -60,7 +61,8 @@ export default class CarouselLayout extends CollectionLayout {
|
|
|
60
61
|
_render(element, states, controls) {
|
|
61
62
|
super._render(element, states, controls);
|
|
62
63
|
const { state } = states;
|
|
63
|
-
|
|
64
|
+
const items = this._getItems(state);
|
|
65
|
+
this._itemCount = items ? items.length : undefined;
|
|
64
66
|
this.syncSize();
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { defineValues } from '@miso.ai/commons';
|
|
2
1
|
import { STATUS, LAYOUT_CATEGORY } from '../../constants.js';
|
|
3
2
|
import TemplateBasedLayout from '../template.js';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
const VALUE = Symbol.for('miso.value');
|
|
3
|
+
import { makeTrackable } from '../trackable.js';
|
|
4
|
+
import { product, article, question, productInfoBlock, articleInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock, helpers } from '../templates.js';
|
|
7
5
|
|
|
8
6
|
function root(layout, state) {
|
|
9
7
|
const { className, role, templates } = layout;
|
|
@@ -14,7 +12,7 @@ function root(layout, state) {
|
|
|
14
12
|
|
|
15
13
|
function ready(layout, state) {
|
|
16
14
|
const { templates } = layout;
|
|
17
|
-
const values = state
|
|
15
|
+
const values = layout._getItems(state);
|
|
18
16
|
|
|
19
17
|
// TODO: handle categories, attributes, etc. by introducing sublayout
|
|
20
18
|
if ((values && values.length > 0) || state.ongoing) {
|
|
@@ -28,7 +26,7 @@ function list(layout, state, values) {
|
|
|
28
26
|
const { className, templates, options } = layout;
|
|
29
27
|
const listTag = templates.ordered ? 'ol' : 'ul';
|
|
30
28
|
// TODO: support separator?
|
|
31
|
-
return `<${listTag} class="${className}__list" data-item-type="${options.itemType}">${templates.items(layout, state, values)}</${listTag}>`;
|
|
29
|
+
return `<${listTag} class="${className}__list" data-role="list" data-item-type="${options.itemType}">${templates.items(layout, state, values)}</${listTag}>`;
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
function items(layout, state, values, { offset = 0 } = {}) {
|
|
@@ -57,6 +55,19 @@ const DEFAULT_TEMPLATES = Object.freeze({
|
|
|
57
55
|
ordered: false,
|
|
58
56
|
items,
|
|
59
57
|
item,
|
|
58
|
+
productInfoBlock,
|
|
59
|
+
articleInfoBlock,
|
|
60
|
+
brandBlock,
|
|
61
|
+
titleBlock,
|
|
62
|
+
descriptionBlock,
|
|
63
|
+
dateBlock,
|
|
64
|
+
priceBlock,
|
|
65
|
+
discountRateText,
|
|
66
|
+
ctaBlock,
|
|
67
|
+
cta,
|
|
68
|
+
imageBlock,
|
|
69
|
+
indexBlock,
|
|
70
|
+
helpers,
|
|
60
71
|
});
|
|
61
72
|
|
|
62
73
|
export default class CollectionLayout extends TemplateBasedLayout {
|
|
@@ -75,18 +86,13 @@ export default class CollectionLayout extends TemplateBasedLayout {
|
|
|
75
86
|
itemType,
|
|
76
87
|
...options,
|
|
77
88
|
});
|
|
78
|
-
defineValues(this, {
|
|
79
|
-
bindings: Object.freeze({
|
|
80
|
-
list: this._listBindings.bind(this),
|
|
81
|
-
}),
|
|
82
|
-
});
|
|
83
89
|
this._view = undefined;
|
|
90
|
+
this._initTrackable();
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
initialize(view) {
|
|
87
94
|
this._view = view;
|
|
88
|
-
|
|
89
|
-
this._unsubscribes.push(proxyElement.on('click', this._onClick.bind(this)));
|
|
95
|
+
this._unsubscribes.push(view.proxyElement.on('click', this._onClick.bind(this)));
|
|
90
96
|
}
|
|
91
97
|
|
|
92
98
|
_preprocess({ state, rendered }) {
|
|
@@ -112,7 +118,7 @@ export default class CollectionLayout extends TemplateBasedLayout {
|
|
|
112
118
|
_html(state, rendered, incremental) {
|
|
113
119
|
if (incremental) {
|
|
114
120
|
const offset = rendered.value.length;
|
|
115
|
-
const values = state.
|
|
121
|
+
const values = (this._getItems(state) || []).slice(offset);
|
|
116
122
|
return values.length > 0 ? this.templates.items(this, state, values, { offset }) : '';
|
|
117
123
|
} else {
|
|
118
124
|
return this.templates.root(this, state);
|
|
@@ -131,42 +137,23 @@ export default class CollectionLayout extends TemplateBasedLayout {
|
|
|
131
137
|
} else {
|
|
132
138
|
element.innerHTML = html;
|
|
133
139
|
}
|
|
134
|
-
this.
|
|
140
|
+
this._syncBindings(element, state);
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const values = state.value || [];
|
|
142
|
-
let i = 0;
|
|
143
|
-
for (const itemElement of this._listItemElements(element)) {
|
|
144
|
-
itemElement[VALUE] = values[i++];
|
|
145
|
-
}
|
|
143
|
+
_unrender() {
|
|
144
|
+
this._clearBindings();
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
|
|
149
|
-
return
|
|
147
|
+
_getItems(state) {
|
|
148
|
+
return state.value;
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
return element
|
|
151
|
+
_getListElement(element) {
|
|
152
|
+
return element.querySelector('[data-role="list"]');
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
const bindings = [];
|
|
161
|
-
for (const element of this._listItemElements(rootElement)) {
|
|
162
|
-
const value = element[VALUE];
|
|
163
|
-
if (!value) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
const key = this.options.itemType === 'product' ? value.product_id : value;
|
|
167
|
-
bindings.push({ element, key, value });
|
|
168
|
-
}
|
|
169
|
-
return bindings;
|
|
155
|
+
_getItemElements(element) {
|
|
156
|
+
return element ? Array.from(element.querySelectorAll(`[data-role="item"]`)) : [];
|
|
170
157
|
}
|
|
171
158
|
|
|
172
159
|
_onClick(event) {
|
|
@@ -174,14 +161,26 @@ export default class CollectionLayout extends TemplateBasedLayout {
|
|
|
174
161
|
if (!element) {
|
|
175
162
|
return;
|
|
176
163
|
}
|
|
177
|
-
const
|
|
164
|
+
const binding = this._bindings.get(element);
|
|
165
|
+
if (!binding) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this._trackClick(event, binding);
|
|
169
|
+
this._emitClickEvent(event, binding);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_emitClickEvent(event, binding) {
|
|
178
173
|
const { session } = this._view._state;
|
|
174
|
+
const { value, element } = binding;
|
|
179
175
|
this._view._events.emit('click', { session, value, element, domEvent: event });
|
|
180
176
|
}
|
|
181
177
|
|
|
182
178
|
destroy() {
|
|
179
|
+
this._destroyTrackable();
|
|
183
180
|
this._view = undefined;
|
|
184
181
|
super.destroy();
|
|
185
182
|
}
|
|
186
183
|
|
|
187
184
|
}
|
|
185
|
+
|
|
186
|
+
makeTrackable(CollectionLayout.prototype);
|