@miso.ai/client-sdk-ui 1.10.3-beta.0 → 1.10.4-beta.0

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.
Files changed (99) hide show
  1. package/dist/cjs/actor/autocomplete.js +4 -1
  2. package/dist/cjs/actor/data.js +15 -6
  3. package/dist/cjs/actor/fields.js +10 -10
  4. package/dist/cjs/actor/filters.js +114 -0
  5. package/dist/cjs/actor/hub.js +2 -1
  6. package/dist/cjs/actor/sessions.js +3 -5
  7. package/dist/cjs/actor/view.js +19 -6
  8. package/dist/cjs/actor/views.js +32 -19
  9. package/dist/cjs/constants.js +9 -2
  10. package/dist/cjs/defaults/ask/constants.js +3 -2
  11. package/dist/cjs/defaults/ask/templates.js +112 -219
  12. package/dist/cjs/defaults/common/templates.js +234 -0
  13. package/dist/cjs/defaults/hybrid-search/answer-box.js +115 -0
  14. package/dist/cjs/defaults/hybrid-search/constants.js +8 -0
  15. package/dist/cjs/defaults/hybrid-search/controls.js +17 -0
  16. package/dist/cjs/defaults/hybrid-search/index.js +31 -0
  17. package/dist/cjs/defaults/hybrid-search/phrases.js +27 -0
  18. package/dist/cjs/defaults/hybrid-search/templates.js +127 -0
  19. package/dist/cjs/defaults/index.js +4 -2
  20. package/dist/cjs/element/container/index.js +7 -0
  21. package/dist/cjs/element/container/miso-hybrid-search.js +21 -0
  22. package/dist/cjs/element/role/index.js +21 -0
  23. package/dist/cjs/element/role/miso-facets.js +20 -0
  24. package/dist/cjs/element/role/miso-hits.js +20 -0
  25. package/dist/cjs/element/role/miso-keywords.js +20 -0
  26. package/dist/cjs/layout/index.js +7 -0
  27. package/dist/cjs/layout/input/facets.js +294 -0
  28. package/dist/cjs/layout/input/option-list.js +4 -4
  29. package/dist/cjs/layout/input/search-box.js +6 -8
  30. package/dist/cjs/layout/list/collection.js +1 -2
  31. package/dist/cjs/layout/raf.js +1 -0
  32. package/dist/cjs/layout/template.js +3 -1
  33. package/dist/cjs/layout/templates.js +15 -0
  34. package/dist/cjs/layout/text/text.js +38 -5
  35. package/dist/cjs/layout/trackable.js +2 -16
  36. package/dist/cjs/plugin.js +4 -1
  37. package/dist/cjs/{source/index.js → source.js} +7 -4
  38. package/dist/cjs/util/bindings.js +3 -0
  39. package/dist/cjs/version.js +1 -1
  40. package/dist/cjs/workflow/answer-based.js +335 -0
  41. package/dist/cjs/workflow/ask.js +98 -271
  42. package/dist/cjs/workflow/base.js +69 -35
  43. package/dist/cjs/workflow/explore.js +10 -0
  44. package/dist/cjs/workflow/hybrid-search.js +140 -0
  45. package/dist/cjs/workflow/index.js +7 -0
  46. package/dist/cjs/workflow/processors.js +113 -0
  47. package/dist/cjs/workflow/recommendation.js +12 -3
  48. package/dist/cjs/workflow/search.js +34 -12
  49. package/package.json +2 -2
  50. package/src/actor/autocomplete.js +4 -2
  51. package/src/actor/data.js +8 -2
  52. package/src/actor/fields.js +10 -10
  53. package/src/actor/filters.js +103 -0
  54. package/src/actor/hub.js +2 -1
  55. package/src/actor/sessions.js +3 -4
  56. package/src/actor/view.js +19 -2
  57. package/src/actor/views.js +30 -16
  58. package/src/constants.js +10 -0
  59. package/src/defaults/ask/constants.js +1 -0
  60. package/src/defaults/ask/templates.js +70 -179
  61. package/src/defaults/common/templates.js +173 -0
  62. package/src/defaults/hybrid-search/answer-box.js +118 -0
  63. package/src/defaults/hybrid-search/constants.js +2 -0
  64. package/src/defaults/hybrid-search/controls.js +11 -0
  65. package/src/defaults/hybrid-search/index.js +9 -0
  66. package/src/defaults/hybrid-search/phrases.js +22 -0
  67. package/src/defaults/hybrid-search/templates.js +83 -0
  68. package/src/defaults/index.js +2 -0
  69. package/src/element/container/index.js +1 -0
  70. package/src/element/container/miso-hybrid-search.js +19 -0
  71. package/src/element/role/index.js +3 -0
  72. package/src/element/role/miso-facets.js +16 -0
  73. package/src/element/role/miso-hits.js +16 -0
  74. package/src/element/role/miso-keywords.js +16 -0
  75. package/src/layout/index.js +1 -0
  76. package/src/layout/input/facets.js +242 -0
  77. package/src/layout/input/option-list.js +3 -4
  78. package/src/layout/input/search-box.js +6 -7
  79. package/src/layout/list/collection.js +1 -2
  80. package/src/layout/raf.js +1 -0
  81. package/src/layout/template.js +2 -1
  82. package/src/layout/templates.js +15 -0
  83. package/src/layout/text/text.js +28 -3
  84. package/src/layout/trackable.js +2 -15
  85. package/src/plugin.js +6 -2
  86. package/src/{source/index.js → source.js} +7 -4
  87. package/src/util/bindings.js +4 -0
  88. package/src/version.js +1 -1
  89. package/src/workflow/answer-based.js +280 -0
  90. package/src/workflow/ask.js +82 -204
  91. package/src/workflow/base.js +64 -27
  92. package/src/workflow/explore.js +10 -1
  93. package/src/workflow/hybrid-search.js +125 -0
  94. package/src/workflow/index.js +1 -0
  95. package/src/workflow/processors.js +95 -0
  96. package/src/workflow/recommendation.js +9 -2
  97. package/src/workflow/search.js +35 -7
  98. package/dist/cjs/source/api.js +0 -23
  99. package/src/source/api.js +0 -12
@@ -0,0 +1,2 @@
1
+ export const CLASS_PREFIX = 'miso-hybrid-search-combo';
2
+ export const CONAINER_TAG = 'miso-hybrid-search';
@@ -0,0 +1,11 @@
1
+ import AnswerBox from './answer-box.js';
2
+
3
+ export function wireAnswerBox(client, element) {
4
+ if (!client) {
5
+ throw new Error('client is required');
6
+ }
7
+ if (!element) {
8
+ return;
9
+ }
10
+ new AnswerBox(client, element);
11
+ }
@@ -0,0 +1,9 @@
1
+ import * as _constants from './constants.js';
2
+ import * as _phrases from './phrases.js';
3
+ import * as _templates from './templates.js';
4
+
5
+ export * from './controls.js';
6
+
7
+ export const constants = Object.freeze(_constants);
8
+ export const phrases = Object.freeze(_phrases);
9
+ export const templates = Object.freeze(_templates);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The phrase displayed right before <miso-question>
3
+ */
4
+ export const question = 'You asked...';
5
+
6
+ /**
7
+ * The phrase displayed right before <miso-sources>
8
+ */
9
+ export const sources = 'Sources';
10
+
11
+ /**
12
+ * The phrase to display <miso-keywords>
13
+ */
14
+ export const keywords = 'You searched for “${value}”';
15
+
16
+ /**
17
+ * The phrase to display <miso-hits>
18
+ */
19
+ export const hits = '${value} Results';
20
+
21
+ export const showMore = 'Show more';
22
+ export const showLess = 'Show less';
@@ -0,0 +1,83 @@
1
+ import { externalize } from '@miso.ai/commons';
2
+ import * as CONSTANTS from './constants.js';
3
+ import * as DEFAULT_PHRASES from './phrases.js';
4
+ import { Helpers, Sections as _Sections } from '../common/templates.js';
5
+
6
+ class Sections extends _Sections {
7
+
8
+ constructor() {
9
+ super({ helpers });
10
+ }
11
+
12
+ answerBox(options) {
13
+ const { element } = this._helpers;
14
+ const { classPrefix } = options;
15
+ return element('div', { classes: [`${classPrefix}__answer-box`] }, [
16
+ element('div', { classes: [`${classPrefix}__answer-box-inner`] }, this.answerGroup(options)),
17
+ this.answerBoxToggle(options),
18
+ ]);
19
+ }
20
+
21
+ answerBoxToggle(options) {
22
+ const { element } = this._helpers;
23
+ const { classPrefix } = options;
24
+ return element('div', { classes: [`${classPrefix}__answer-box-toggle-container`] }, [
25
+ element('div', { classes: [`${classPrefix}__answer-box-toggle`], 'data-role': 'answer-box-toggle' }),
26
+ ]);
27
+ }
28
+
29
+ searchResultsGroup(options) {
30
+ const { container } = this._helpers;
31
+ return container(options, { name: 'search-results', visibleWhen: 'ready' }, [
32
+ this.searchResultsInfoGroup(options),
33
+ this.searchResultsFilters(options),
34
+ '<miso-products></miso-products>',
35
+ ]);
36
+ }
37
+
38
+ searchResultsInfoGroup(options) {
39
+ const { element } = this._helpers;
40
+ const { classPrefix } = options;
41
+ return element('div', { classes: [`${classPrefix}__search-results-info`] }, [
42
+ this.keywords(options),
43
+ this.hits(options),
44
+ ]);
45
+ }
46
+
47
+ searchResultsFilters(options) {
48
+ const { element } = this._helpers;
49
+ const { classPrefix } = options;
50
+ return element('div', { classes: [`${classPrefix}__search-results-filters`] }, [
51
+ '<miso-facets></miso-facets>',
52
+ ]);
53
+ }
54
+
55
+ keywords(options) {
56
+ const { phrase } = this._helpers;
57
+ return phrase(options, { name: 'keywords', tag: 'div', inline: true }).replaceAll('${value}', '<miso-keywords></miso-keywords>');
58
+ }
59
+
60
+ hits(options) {
61
+ const { phrase } = this._helpers;
62
+ return phrase(options, { name: 'hits', tag: 'div', inline: true }).replaceAll('${value}', '<miso-hits></miso-hits>');
63
+ }
64
+
65
+ }
66
+
67
+ export const helpers = externalize(new Helpers({ ...CONSTANTS, DEFAULT_PHRASES }));
68
+ export const sections = externalize(new Sections({ helpers }));
69
+
70
+ export function root(options = {}) {
71
+ const { normalizeOptions, section, container } = helpers;
72
+ options = normalizeOptions(options);
73
+ const { answerBox } = sections;
74
+ return [
75
+ section(options, { name: 'question' }, [
76
+ container(options, { name: 'query' }, '<miso-query></miso-query>'),
77
+ ]),
78
+ section(options, { name: 'answer' }, [
79
+ answerBox(options),
80
+ ]),
81
+ section(options, { name: 'search-results' }, sections.searchResultsGroup(options)),
82
+ ].join('');
83
+ };
@@ -1,3 +1,5 @@
1
1
  import * as _ask from './ask/index.js';
2
+ import * as _hybridSearch from './hybrid-search/index.js';
2
3
 
3
4
  export const ask = Object.freeze(_ask);
5
+ export const hybridSearch = Object.freeze(_hybridSearch);
@@ -1,4 +1,5 @@
1
1
  export { default as MisoAskElement } from './miso-ask.js';
2
2
  export { default as MisoExploreElement } from './miso-explore.js';
3
+ export { default as MisoHybridSearchElement } from './miso-hybrid-search.js';
3
4
  export { default as MisoSearchElement } from './miso-search.js';
4
5
  export { default as MisoRecommendationElement } from './miso-recommendation.js';
@@ -0,0 +1,19 @@
1
+ import MisoContainerElement from './miso-container.js';
2
+
3
+ const TAG_NAME = 'miso-hybrid-search';
4
+
5
+ export default class MisoHybridSearchElement extends MisoContainerElement {
6
+
7
+ static get observedAttributes() {
8
+ return MisoContainerElement.observedAttributes;
9
+ }
10
+
11
+ static get tagName() {
12
+ return TAG_NAME;
13
+ }
14
+
15
+ _getWorkflow(client) {
16
+ return client.ui.hybridSearch;
17
+ }
18
+
19
+ }
@@ -10,4 +10,7 @@ export { default as MisoSourcesElement } from './miso-sources.js';
10
10
  export { default as MisoRelatedResourcesElement } from './miso-related-resources.js';
11
11
  export { default as MisoRelatedQuestionsElement } from './miso-related-questions.js';
12
12
  export { default as MisoSovrnAffiliationElement } from './miso-affiliation.js';
13
+ export { default as MisoKeywordsElement } from './miso-keywords.js';
14
+ export { default as MisoHitsElement } from './miso-hits.js';
15
+ export { default as MisoFacetsElement } from './miso-facets.js';
13
16
  export { default as MisoErrorElement } from './miso-error.js';
@@ -0,0 +1,16 @@
1
+ import MisoComponentElement from './miso-component.js';
2
+ import { ROLE } from '../../constants.js';
3
+
4
+ export default class MisoFacetsElement extends MisoComponentElement {
5
+
6
+ constructor() {
7
+ super({
8
+ role: ROLE.FACETS,
9
+ });
10
+ }
11
+
12
+ static get tagName() {
13
+ return 'miso-facets';
14
+ }
15
+
16
+ }
@@ -0,0 +1,16 @@
1
+ import MisoComponentElement from './miso-component.js';
2
+ import { ROLE } from '../../constants.js';
3
+
4
+ export default class MisoHitsElement extends MisoComponentElement {
5
+
6
+ constructor() {
7
+ super({
8
+ role: ROLE.HITS,
9
+ });
10
+ }
11
+
12
+ static get tagName() {
13
+ return 'miso-hits';
14
+ }
15
+
16
+ }
@@ -0,0 +1,16 @@
1
+ import MisoComponentElement from './miso-component.js';
2
+ import { ROLE } from '../../constants.js';
3
+
4
+ export default class MisoKeywordsElement extends MisoComponentElement {
5
+
6
+ constructor() {
7
+ super({
8
+ role: ROLE.KEYWORDS,
9
+ });
10
+ }
11
+
12
+ static get tagName() {
13
+ return 'miso-keywords';
14
+ }
15
+
16
+ }
@@ -12,3 +12,4 @@ export { default as FeedbackLayout } from './radio/feedback.js';
12
12
  export { default as TextLayout } from './text/text.js';
13
13
  export { default as TypewriterLayout } from './text/typewriter/index.js';
14
14
  export { default as AffiliationLayout } from './affiliation/affiliation.js';
15
+ export { default as FacetsLayout } from './input/facets.js';
@@ -0,0 +1,242 @@
1
+ import { escapeHtml } from '@miso.ai/commons';
2
+ import { LAYOUT_CATEGORY } from '../../constants.js';
3
+ import TemplateBasedLayout from '../template.js';
4
+ import Bindings from '../../util/bindings.js';
5
+
6
+ const TYPE = 'facets';
7
+ const DEFAULT_CLASSNAME = 'miso-facets';
8
+ const DEDAULT_FACET_CLASSNAME = 'miso-facet';
9
+ const CUSTOM_ATTRIBUTES_PREFIX = 'custom_attributes.';
10
+
11
+ function root(layout, state) {
12
+ const { className, role, templates } = layout;
13
+ const roleAttr = role ? `data-role="${role}"` : '';
14
+ return `<div class="${className}" ${roleAttr}>${templates.facets(layout, state)}</div>`;
15
+ }
16
+
17
+ function facets(layout, state) {
18
+ const { templates } = layout;
19
+ const { facet_fields = {} } = state.value || {};
20
+ return Object.keys(facet_fields).map(field => templates.facet(layout, { field, entries: facet_fields[field] }, state));
21
+ }
22
+
23
+ function facet(layout, facet, state) {
24
+ const { facetClassName = DEDAULT_FACET_CLASSNAME, templates } = layout;
25
+ const { field } = facet;
26
+ return `<div class="${facetClassName}" data-role="facet" data-field="${escapeHtml(field)}">${templates.header(layout, facet, state)}${templates.options(layout, facet, state)}</div>`;
27
+ }
28
+
29
+ function header(layout, facet, state) {
30
+ const { facetClassName = DEDAULT_FACET_CLASSNAME, templates } = layout;
31
+ return `<div class="${facetClassName}__header">${templates.title(layout, facet, state)}</div>`;
32
+ }
33
+
34
+ function title(layout, { field }) {
35
+ if (field.startsWith(CUSTOM_ATTRIBUTES_PREFIX)) {
36
+ field = field.substring(CUSTOM_ATTRIBUTES_PREFIX.length);
37
+ }
38
+ // TODO: to words
39
+ return escapeHtml(field);
40
+ }
41
+
42
+ function options(layout, { field, entries }, state) {
43
+ const { facetClassName = DEDAULT_FACET_CLASSNAME, templates } = layout;
44
+ return `<ul class="${facetClassName}__options" data-role="options">${entries.map(([value, count]) => templates.option(layout, { field, value, count }, state)).join('')}</ul>`;
45
+ }
46
+
47
+ function option(layout, entry, state) {
48
+ const { facetClassName = DEDAULT_FACET_CLASSNAME, templates } = layout;
49
+ return `
50
+ <li class="${facetClassName}__option" data-role="option" tabindex="0">
51
+ <span class="${facetClassName}__value">${templates.value(layout, entry, state)}</span>
52
+ <span class="${facetClassName}__count">${templates.count(layout, entry, state)}</span>
53
+ </li>`;
54
+ }
55
+
56
+
57
+ function value(layout, { value }, state) {
58
+ return escapeHtml(value);
59
+ }
60
+
61
+ function count(layout, { count }, state) {
62
+ return layout.templates.helpers.formatNumber(count);
63
+ }
64
+
65
+ const DEFAULT_TEMPLATES = Object.freeze({
66
+ root,
67
+ facets,
68
+ facet,
69
+ header,
70
+ title,
71
+ options,
72
+ option,
73
+ value,
74
+ count,
75
+ });
76
+
77
+ function getItemKey(field, value) {
78
+ return `${field}:::${value}`;
79
+ }
80
+
81
+ function getSelectedValues(facets) {
82
+ const selections = {};
83
+ for (const field in facets) {
84
+ selections[field] = new Set(facets[field]);
85
+ }
86
+ return selections;
87
+ }
88
+
89
+ export default class FacetsLayout extends TemplateBasedLayout {
90
+
91
+ static get category() {
92
+ return LAYOUT_CATEGORY.FILTERS;
93
+ }
94
+
95
+ static get type() {
96
+ return TYPE;
97
+ }
98
+
99
+ static get defaultTemplates() {
100
+ return DEFAULT_TEMPLATES;
101
+ }
102
+
103
+ static get defaultClassName() {
104
+ return DEFAULT_CLASSNAME;
105
+ }
106
+
107
+ constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
108
+ super({
109
+ className,
110
+ templates: { ...DEFAULT_TEMPLATES, ...templates },
111
+ ...options,
112
+ });
113
+ this._bindings = new Bindings();
114
+ }
115
+
116
+ initialize(view) {
117
+ const { proxyElement, hub, filters } = view;
118
+ this._unsubscribes = [
119
+ ...this._unsubscribes,
120
+ proxyElement.on('click', (e) => this._handleClick(e)),
121
+ filters.on('update', (e) => this._updateSelections(e)),
122
+ filters.on('reset', () => this._syncSelections()),
123
+ ];
124
+ }
125
+
126
+ _render(element, data, controls) {
127
+ super._render(element, data, controls);
128
+ this._syncSelections();
129
+ }
130
+
131
+ _afterRender(element, state) {
132
+ this._syncBindings(element, state);
133
+ }
134
+
135
+ _unrender() {
136
+ this._clearBindings();
137
+ }
138
+
139
+ _syncSelections() {
140
+ const selectedValues = getSelectedValues(this._view.filters.states.facets);
141
+ for (const { value: { field, value }, element } of this._bindings.entries) {
142
+ this._setSelected(element, selectedValues[field] && selectedValues[field].has(value));
143
+ }
144
+ }
145
+
146
+ _updateSelections({ updates }) {
147
+ const selectedValues = getSelectedValues(updates.facets);
148
+ for (const { value: { field, value }, element } of this._bindings.entries) {
149
+ if (!selectedValues[field]) {
150
+ continue;
151
+ }
152
+ this._setSelected(element, selectedValues[field].has(value));
153
+ }
154
+ }
155
+
156
+ _setSelected(element, selected) {
157
+ if (!element) {
158
+ return;
159
+ }
160
+ if (selected) {
161
+ element.classList.add('selected');
162
+ } else {
163
+ element.classList.remove('selected');
164
+ }
165
+ }
166
+
167
+ _syncBindings(element, state) {
168
+ if (!element || !state.value) {
169
+ return;
170
+ }
171
+ const items = this._getItems(state);
172
+ const keys = [];
173
+ const values = [];
174
+ const elements = [];
175
+ for (const facetElement of this._getFacetElements(element)) {
176
+ const field = facetElement.getAttribute('data-field');
177
+ const _values = items[field];
178
+ if (!_values) {
179
+ continue;
180
+ }
181
+ const itemElements = this._getItemElements(facetElement);
182
+ for (let i = 0; i < itemElements.length; i++) {
183
+ const itemElement = itemElements[i];
184
+ const value = _values[i];
185
+ if (!value) {
186
+ break;
187
+ }
188
+ keys.push(getItemKey(field, value));
189
+ values.push({ field, value });
190
+ elements.push(itemElement);
191
+ }
192
+ }
193
+ this._bindings.update(keys, values, elements);
194
+ }
195
+
196
+ _clearBindings() {
197
+ this._bindings.clear();
198
+ }
199
+
200
+ _getItems({ value } = {}) {
201
+ const { facet_fields = {} } = value || {};
202
+ const items = {};
203
+ for (const field in facet_fields) {
204
+ // the key needs to be matched against data-field attribute
205
+ items[escapeHtml(field)] = facet_fields[field].map(([value]) => value);
206
+ }
207
+ return items;
208
+ }
209
+
210
+ _getFacetElements(element) {
211
+ return Array.from(element.querySelectorAll(`[data-role="facet"]`));
212
+ }
213
+
214
+ _getFacetElement(element, field) {
215
+ return element.querySelector(`[data-role="facet"][data-field="${field}"]`);
216
+ }
217
+
218
+ _getItemElements(element) {
219
+ return Array.from(element.querySelectorAll(`[data-role="option"]`));
220
+ }
221
+
222
+ _handleClick(event) {
223
+ const element = event.target.closest(`[data-role="option"]`);
224
+ if (!element) {
225
+ return;
226
+ }
227
+ const binding = this._bindings.get(element);
228
+ const option = binding && binding.value;
229
+ if (!option) {
230
+ return;
231
+ }
232
+ const { field, value } = option;
233
+ this._view.filters.facets.toggle(field, value);
234
+ this._view.filters.apply(); // TODO: support autoApply = false
235
+ }
236
+
237
+ destroy() {
238
+ this._bindings.clear();
239
+ super.destroy();
240
+ }
241
+
242
+ }
@@ -71,13 +71,13 @@ export default class OptionListLayout extends TemplateBasedLayout {
71
71
  this._unsubscribes = [
72
72
  ...this._unsubscribes,
73
73
  proxyElement.on('click', (e) => this._handleClick(e)),
74
- hub.on(fields.suggestions(), () => this._handleInput()),
74
+ hub.on(fields.suggestions(), () => this._handleInput()), // TODO: use data mapping
75
75
  ];
76
76
  }
77
77
 
78
78
  _preprocess() {
79
79
  return super._preprocess({
80
- state: this._view.hub.states[fields.suggestions()],
80
+ state: this._view.hub.states[fields.suggestions()], // TODO: use data mapping
81
81
  });
82
82
  }
83
83
 
@@ -135,12 +135,11 @@ export default class OptionListLayout extends TemplateBasedLayout {
135
135
  return;
136
136
  }
137
137
  // TODO: q -> value
138
- this._view.hub.trigger(fields.query(), { q: value });
138
+ this._view.hub.update(fields.query(), { q: value });
139
139
  }
140
140
 
141
141
  destroy() {
142
142
  this._destroyTrackable();
143
- this._view = undefined;
144
143
  super.destroy();
145
144
  }
146
145
 
@@ -1,8 +1,8 @@
1
1
  import { defineValues, escapeHtml, findInAncestors, debounce } from '@miso.ai/commons';
2
- import { LAYOUT_CATEGORY, STATUS, EVENT_TYPE } from '../../constants.js';
2
+ import { LAYOUT_CATEGORY, STATUS, EVENT_TYPE, DATA_ASPECT } 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
+ import { imageBlock, productInfoBlock } from '../templates.js';
6
6
  import { SEND, SEARCH } from '../../asset/svgs.js';
7
7
 
8
8
  const TYPE = 'search-box';
@@ -57,7 +57,7 @@ function query(layout, { text, text_with_inverted_markups } = {}) {
57
57
 
58
58
  function product(layout, { product }) {
59
59
  const { templates } = layout;
60
- const [openTag, closeTag] = helpers.tagPair(layout, product, { role: false });
60
+ const [openTag, closeTag] = templates.helpers.tagPair(layout, product, { role: false });
61
61
  return [
62
62
  openTag,
63
63
  templates.imageBlock(layout, product),
@@ -93,7 +93,6 @@ const DEFAULT_TEMPLATES = Object.freeze({
93
93
  product,
94
94
  imageBlock,
95
95
  productInfoBlock,
96
- helpers,
97
96
  });
98
97
 
99
98
  const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
@@ -142,8 +141,8 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
142
141
  proxyElement.on('click', (e) => this._handleClick(e)),
143
142
  proxyElement.on('focusin', (e) => this._handleFocusIn(e)),
144
143
  proxyElement.on('focusout', (e) => this._handleFocusOut(e)),
144
+ hub.on(fields.data(DATA_ASPECT.AUTOCOMPLETE), () => view.refresh({ force: true })),
145
145
  ];
146
- this._unsubscribes.push(hub.on(fields.completions(), () => view.refresh({ force: true })));
147
146
  }
148
147
 
149
148
  focus() {
@@ -176,7 +175,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
176
175
  state = super._preprocess({ state });
177
176
  return {
178
177
  ...state,
179
- completions: this._view.hub.states[fields.completions()],
178
+ completions: this._view.hub.states[fields.data(DATA_ASPECT.AUTOCOMPLETE)],
180
179
  };
181
180
  }
182
181
 
@@ -324,7 +323,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
324
323
  }
325
324
  this.close();
326
325
  // TODO: q -> value
327
- this._view.hub.trigger(fields.query(), { q: value });
326
+ this._view.hub.update(fields.query(), { q: value });
328
327
  this._trackSubmit(value);
329
328
  }
330
329
 
@@ -1,7 +1,7 @@
1
1
  import { STATUS, LAYOUT_CATEGORY } from '../../constants.js';
2
2
  import TemplateBasedLayout from '../template.js';
3
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';
4
+ import { product, article, question, productInfoBlock, articleInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
5
5
 
6
6
  function root(layout, state) {
7
7
  const { className, role, templates } = layout;
@@ -67,7 +67,6 @@ const DEFAULT_TEMPLATES = Object.freeze({
67
67
  cta,
68
68
  imageBlock,
69
69
  indexBlock,
70
- helpers,
71
70
  });
72
71
 
73
72
  export default class CollectionLayout extends TemplateBasedLayout {
package/src/layout/raf.js CHANGED
@@ -103,6 +103,7 @@ export default class RafLayout {
103
103
  unsubscribe();
104
104
  }
105
105
  this._unsubscribes = [];
106
+ this._view = undefined;
106
107
  this._element = undefined;
107
108
  this._notifyUpdate = undefined;
108
109
  this._pending = undefined;
@@ -1,6 +1,6 @@
1
1
  import { defineValues } from '@miso.ai/commons';
2
2
  import RafLayout from './raf.js';
3
- import { requiresImplementation } from './templates.js';
3
+ import { requiresImplementation, helpers } from './templates.js';
4
4
 
5
5
  export default class TemplateBasedLayout extends RafLayout {
6
6
 
@@ -11,6 +11,7 @@ export default class TemplateBasedLayout extends RafLayout {
11
11
  templates: {
12
12
  ...requiresImplementation('root'),
13
13
  ...templates,
14
+ helpers,
14
15
  },
15
16
  });
16
17
  }
@@ -176,6 +176,7 @@ function tagPair({ className, options = {} }, { product_id, url }, { classSuffix
176
176
  }
177
177
 
178
178
  const DEFAULT_DATE_OPTIONS = Object.freeze({ locale: 'en-US', year: 'numeric', month: 'short', day: 'numeric' });
179
+ const DEFAULT_NUMBER_OPTIONS = 'en-US';
179
180
 
180
181
  function formatDate(date, fn = DEFAULT_DATE_OPTIONS) {
181
182
  switch (typeof fn) {
@@ -190,6 +191,19 @@ function formatDate(date, fn = DEFAULT_DATE_OPTIONS) {
190
191
  }
191
192
  }
192
193
 
194
+ function formatNumber(number, fn = DEFAULT_NUMBER_OPTIONS) {
195
+ switch (typeof fn) {
196
+ case 'function':
197
+ return fn(number);
198
+ case 'string':
199
+ return new Intl.NumberFormat(fn).format(number);
200
+ case 'object':
201
+ return new Intl.NumberFormat(fn.locale || DEFAULT_NUMBER_OPTIONS.locale, fn).format(number);
202
+ default:
203
+ throw new Error(`Invalid number format: ${fn}`);
204
+ }
205
+ }
206
+
193
207
  function asFunction(template = '') {
194
208
  return typeof template === 'function' ? template : () => template;
195
209
  }
@@ -197,6 +211,7 @@ function asFunction(template = '') {
197
211
  export const helpers = Object.freeze({
198
212
  tagPair,
199
213
  formatDate,
214
+ formatNumber,
200
215
  escapeHtml,
201
216
  asFunction,
202
217
  });
@@ -5,10 +5,35 @@ import TemplateBasedLayout from '../template.js';
5
5
  const TYPE = 'text';
6
6
  const DEFAULT_CLASSNAME = 'miso-text';
7
7
 
8
- function root(layout, state) {
9
- const { className, role, options: { tag } } = layout;
8
+ function root(layout, { value }) {
9
+ const { className, role, options: { raw = false, tag } } = layout;
10
10
  const roleAttr = role ? `data-role="${role}"` : '';
11
- return `<${tag} class="${className}" ${roleAttr}>${escapeHtml(state.value || '')}</${tag}>`;
11
+ const content = value !== undefined ? escapeHtml(getFormatFunction(layout)(value)) : '';
12
+ return raw ? content : `<${tag} class="${className}" ${roleAttr}>${content}</${tag}>`;
13
+ }
14
+
15
+ function getFormatFunction(layout) {
16
+ let { format } = layout.options;
17
+ const { formatDate, formatNumber } = layout.templates.helpers;
18
+ if (!format) {
19
+ return v => `${v}`;
20
+ }
21
+ switch (typeof format) {
22
+ case 'function':
23
+ return format;
24
+ case 'string':
25
+ format = [format];
26
+ break;
27
+ }
28
+ let [type, args] = format;
29
+ switch (type) {
30
+ case 'date':
31
+ return v => formatDate(v, args);
32
+ case 'number':
33
+ return v => formatNumber(v, args);
34
+ default:
35
+ return v => `${v}`;
36
+ }
12
37
  }
13
38
 
14
39
  const DEFAULT_TEMPLATES = Object.freeze({