@miso.ai/client-sdk-ui 1.11.5-beta.0 → 1.11.5-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.
Files changed (67) hide show
  1. package/dist/cjs/actor/data.js +4 -2
  2. package/dist/cjs/actor/view.js +3 -2
  3. package/dist/cjs/asset/svgs.js +5 -2
  4. package/dist/cjs/constants.js +2 -1
  5. package/dist/cjs/defaults/hybrid-search/answer-box.js +13 -2
  6. package/dist/cjs/defaults/hybrid-search/controls.js +2 -1
  7. package/dist/cjs/defaults/hybrid-search/templates.js +9 -0
  8. package/dist/cjs/layout/affiliation/affiliation.js +6 -7
  9. package/dist/cjs/layout/container/container.js +5 -1
  10. package/dist/cjs/layout/index.js +7 -0
  11. package/dist/cjs/layout/input/option-list.js +2 -1
  12. package/dist/cjs/layout/input/search-box.js +2 -1
  13. package/dist/cjs/layout/list/carousel.js +2 -2
  14. package/dist/cjs/layout/list/collection.js +7 -4
  15. package/dist/cjs/layout/list/gallery.js +270 -0
  16. package/dist/cjs/layout/mixin/swipeable.js +14 -15
  17. package/dist/cjs/layout/raf.js +2 -0
  18. package/dist/cjs/layout/templates.js +19 -8
  19. package/dist/cjs/util/dom.js +101 -0
  20. package/dist/cjs/version.js +1 -1
  21. package/dist/cjs/workflow/answer-based.js +49 -77
  22. package/dist/cjs/workflow/ask.js +5 -3
  23. package/dist/cjs/workflow/base.js +4 -2
  24. package/dist/cjs/workflow/explore.js +17 -7
  25. package/dist/cjs/workflow/hybrid-search-answer.js +13 -0
  26. package/dist/cjs/workflow/hybrid-search-results.js +16 -0
  27. package/dist/cjs/workflow/hybrid-search.js +3 -8
  28. package/dist/cjs/workflow/options/auto-query.js +62 -0
  29. package/dist/cjs/workflow/options/index.js +11 -0
  30. package/dist/cjs/workflow/processors.js +73 -0
  31. package/dist/cjs/workflow/search-based.js +4 -0
  32. package/dist/cjs/workflow/search.js +0 -25
  33. package/dist/cjs/workflow/use-link.js +9 -2
  34. package/package.json +2 -2
  35. package/src/actor/data.js +4 -2
  36. package/src/actor/view.js +3 -2
  37. package/src/asset/svgs.js +5 -1
  38. package/src/constants.js +1 -0
  39. package/src/defaults/hybrid-search/answer-box.js +9 -2
  40. package/src/defaults/hybrid-search/controls.js +2 -2
  41. package/src/defaults/hybrid-search/templates.js +9 -0
  42. package/src/layout/affiliation/affiliation.js +5 -7
  43. package/src/layout/container/container.js +4 -1
  44. package/src/layout/index.js +1 -0
  45. package/src/layout/input/option-list.js +2 -1
  46. package/src/layout/input/search-box.js +2 -1
  47. package/src/layout/list/carousel.js +3 -3
  48. package/src/layout/list/collection.js +6 -4
  49. package/src/layout/list/gallery.js +229 -0
  50. package/src/layout/mixin/swipeable.js +12 -16
  51. package/src/layout/raf.js +1 -0
  52. package/src/layout/templates.js +15 -3
  53. package/src/util/dom.js +103 -0
  54. package/src/version.js +1 -1
  55. package/src/workflow/answer-based.js +42 -42
  56. package/src/workflow/ask.js +6 -4
  57. package/src/workflow/base.js +4 -2
  58. package/src/workflow/explore.js +15 -4
  59. package/src/workflow/hybrid-search-answer.js +15 -1
  60. package/src/workflow/hybrid-search-results.js +17 -0
  61. package/src/workflow/hybrid-search.js +3 -6
  62. package/src/workflow/options/auto-query.js +29 -0
  63. package/src/workflow/options/index.js +1 -0
  64. package/src/workflow/processors.js +60 -0
  65. package/src/workflow/search-based.js +5 -1
  66. package/src/workflow/search.js +0 -17
  67. package/src/workflow/use-link.js +2 -2
@@ -0,0 +1,229 @@
1
+ import { trimObj } from '@miso.ai/commons';
2
+ import CollectionLayout from './collection.js';
3
+ import { addOrRemoveClass, resolveCssLength, cssAspectRatio } from '../../util/dom.js';
4
+
5
+ function calculateItemSizes(itemCount, columnCount) {
6
+ const totalSpaces = columnCount * 2;
7
+ let remainingSpaces = Math.max(totalSpaces - itemCount, 0);
8
+
9
+ let small = itemCount;
10
+ // turning a small block into a large block takes 3 extra spaces
11
+ const large = Math.min(Math.floor(remainingSpaces / 3), small);
12
+ small -= large;
13
+ remainingSpaces -= large * 3;
14
+ // turning a small block into a medium block takes 1 extra space
15
+ const medium = Math.min(remainingSpaces, small);
16
+ small -= medium;
17
+ remainingSpaces -= medium;
18
+
19
+ return [large, medium, small];
20
+ }
21
+
22
+ function item(layout, state, value, index) {
23
+ const { className, templates, options } = layout;
24
+ const { itemType } = options;
25
+ const body = templates[itemType](layout, state, value, { index });
26
+ const sizeClass = value._size ? ` ${className}__item-${value._size}` : '';
27
+ return `<li class="${className}__item${sizeClass}">${body}</li>`;
28
+ }
29
+
30
+ const DEFAULT_HEIGHT = '16rem';
31
+
32
+ const DEFAULT_TEMPLATES = Object.freeze({
33
+ ...CollectionLayout.defaultTemplates,
34
+ item,
35
+ });
36
+
37
+ const TYPE = 'gallery';
38
+ const DEFAULT_CLASSNAME = 'miso-gallery';
39
+
40
+ export default class GalleryLayout extends CollectionLayout {
41
+
42
+ static get type() {
43
+ return TYPE;
44
+ }
45
+
46
+ static get defaultTemplates() {
47
+ return DEFAULT_TEMPLATES;
48
+ }
49
+
50
+ static get defaultClassName() {
51
+ return DEFAULT_CLASSNAME;
52
+ }
53
+
54
+ constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
55
+ super({
56
+ className,
57
+ templates: { ...DEFAULT_TEMPLATES, ...templates },
58
+ ...options,
59
+ });
60
+ this._galleryContext = {
61
+ itemAspectRatio: undefined,
62
+ itemCount: undefined,
63
+ columnCount: undefined,
64
+ };
65
+ }
66
+
67
+ syncSize() {
68
+ const { element } = this._view;
69
+ const listElement = element && this._getListElement(element);
70
+ if (!listElement) {
71
+ return;
72
+ }
73
+ this._syncListAspectRatio(listElement);
74
+ this._syncItemSizes(listElement);
75
+ }
76
+
77
+ _preprocess({ state, rendered }) {
78
+ const incremental = this._shallRenderIncrementally(state, rendered);
79
+ const bodyHtml = incremental ? undefined : this._bodyHtml(state);
80
+ const itemsHtml = this._itemsHtml(state, rendered);
81
+ return {
82
+ ...state,
83
+ ...trimObj({
84
+ incremental,
85
+ bodyHtml,
86
+ itemsHtml,
87
+ }),
88
+ };
89
+ }
90
+
91
+ _bodyHtml(state) {
92
+ return this.templates.root(this, { ...state, value: [] });
93
+ }
94
+
95
+ _itemsHtml(state, rendered) {
96
+ const offset = (rendered && rendered.value && rendered.value.length) || 0;
97
+ const values = (this._getItems(state) || []).slice(offset);
98
+ return values.length > 0 ? this.templates.items(this, state, values, { offset }) : '';
99
+ }
100
+
101
+ _render(element, states, controls) {
102
+ this._syncItemCount(states);
103
+ this._syncCssVariables();
104
+ const listElement = this._renderListElement(element, states, controls);
105
+ if (!listElement) {
106
+ return;
107
+ }
108
+ this._syncListAspectRatio(listElement);
109
+ this._renderItems(listElement, states, controls);
110
+ }
111
+
112
+ _renderListElement(element, { state }) {
113
+ const { incremental, bodyHtml } = state;
114
+ if (!incremental) {
115
+ element.innerHTML = bodyHtml;
116
+ }
117
+ return this._getListElement(element);
118
+ }
119
+
120
+ _renderItems(listElement, states) {
121
+ if (states.state.incremental) {
122
+ this._appendItems(listElement, states);
123
+ } else {
124
+ this._updateItems(listElement, states);
125
+ }
126
+ }
127
+
128
+ _updateItems(listElement, { state }) {
129
+ const { itemSizes } = this._galleryContext;
130
+ const values = this._writeItemSizes(this._getItems(state) || [], itemSizes);
131
+ listElement.innerHTML = this.templates.items(this, state, values);
132
+ }
133
+
134
+ _appendItems(listElement, { state }) {
135
+ const { itemSizes } = this._galleryContext;
136
+ // update existing items
137
+ this._syncItemSizes(listElement);
138
+ // append new items
139
+ const offset = listElement.children.length;
140
+ const values = this._writeItemSizes((this._getItems(state) || []).slice(offset), itemSizes, offset);
141
+ listElement.insertAdjacentHTML('beforeend', this.templates.items(this, state, values, { offset }));
142
+ }
143
+
144
+ _writeItemSizes(values, [large = 0, medium = 0] = [], offset = 0) {
145
+ let omitted = 0;
146
+ if (offset > 0) {
147
+ omitted = Math.min(offset, large);
148
+ offset -= omitted;
149
+ large -= omitted;
150
+ }
151
+ if (offset > 0) {
152
+ omitted = Math.min(offset, medium);
153
+ offset -= omitted;
154
+ medium -= omitted;
155
+ }
156
+ const results = [];
157
+ for (const value of values) {
158
+ let _size = 'small';
159
+ if (large > 0) {
160
+ _size = 'large';
161
+ large--;
162
+ } else if (medium > 0) {
163
+ _size = 'medium';
164
+ medium--;
165
+ }
166
+ results.push({ ...value, _size });
167
+ }
168
+ return results;
169
+ }
170
+
171
+ _syncCssVariables() {
172
+ const context = this._galleryContext;
173
+ let { itemAspectRatio = 1 } = this.options;
174
+ itemAspectRatio = context.itemAspectRatio = cssAspectRatio(itemAspectRatio);
175
+ const { element } = this._view;
176
+ if (!element) {
177
+ return;
178
+ }
179
+ // TODO: skip if equal to default values
180
+ element.style.setProperty('--miso-gallery-item-1x1-aspect-ratio', `${itemAspectRatio}`);
181
+ element.style.setProperty('--miso-gallery-item-1x2-aspect-ratio', `${itemAspectRatio.scaleBy([1, 2])}`);
182
+ }
183
+
184
+ _syncListAspectRatio(listElement) {
185
+ const context = this._galleryContext;
186
+ const { itemCount, itemAspectRatio } = context;
187
+ const { height = DEFAULT_HEIGHT } = this.options;
188
+ const resolvedHeight = resolveCssLength(height);
189
+ const width = listElement.clientWidth;
190
+ if (width === 0) {
191
+ context.columnCount = undefined;
192
+ context.itemSizes = undefined;
193
+ return; // probably not visible yet
194
+ }
195
+ const columnCount = context.columnCount = Math.round((width * itemAspectRatio.width * 2) / (resolvedHeight * itemAspectRatio.height));
196
+ context.itemSizes = calculateItemSizes(itemCount, columnCount);
197
+
198
+ // update DOM
199
+ listElement.style.aspectRatio = `${columnCount} / 2`;
200
+ listElement.style.height = itemCount > 0 ? 'auto' : '0';
201
+ }
202
+
203
+ _syncItemCount({ state } = {}) {
204
+ const items = this._getItems(state);
205
+ this._galleryContext.itemCount = items ? items.length : undefined;
206
+ }
207
+
208
+ _syncItemSizes(listElement) {
209
+ const { itemSizes = [] } = this._galleryContext;
210
+ let [large = 0, medium = 0] = itemSizes;
211
+ const { className } = this;
212
+
213
+ // update DOM
214
+ for (const itemElement of listElement.children) {
215
+ let lg = false, md = false;
216
+ if (large > 0) {
217
+ lg = true;
218
+ large--;
219
+ } else if (medium > 0) {
220
+ md = true;
221
+ medium--;
222
+ }
223
+ addOrRemoveClass(itemElement, `${className}__item-large`, lg);
224
+ addOrRemoveClass(itemElement, `${className}__item-medium`, md);
225
+ addOrRemoveClass(itemElement, `${className}__item-small`, !lg && !md);
226
+ }
227
+ }
228
+
229
+ }
@@ -1,4 +1,5 @@
1
1
  import { mixin, requestAnimationFrame as raf } from '@miso.ai/commons';
2
+ import { setOrRemoveAttribute, addOrRemoveClass } from '../../util/dom.js';
2
3
 
3
4
  export function makeSwipeable(prototype) {
4
5
  mixin(prototype, SwipeableMixin.prototype);
@@ -33,7 +34,7 @@ export class SwipeableMixin {
33
34
  index = 0;
34
35
  }
35
36
  context.itemIndex = index;
36
- raf(() => this._syncSwipeableItemIndex());
37
+ raf(() => this._syncDisplayedSwipeableItemIndex());
37
38
  }
38
39
 
39
40
  next() {
@@ -52,7 +53,8 @@ export class SwipeableMixin {
52
53
 
53
54
  _renderSwipeable(element, states, controls) {
54
55
  this._syncSwipeableItemCount(states);
55
- this._syncSwipeableItemIndex();
56
+ this._syncDisplayedSwipeableItemCount();
57
+ this._syncDisplayedSwipeableItemIndex();
56
58
  }
57
59
 
58
60
  _afterRenderSwipeable() {
@@ -74,8 +76,6 @@ export class SwipeableMixin {
74
76
  } else if (oldItemCount === undefined) {
75
77
  context.itemIndex = 0;
76
78
  }
77
-
78
- this._syncDisplayedSwipeableItemCount();
79
79
  }
80
80
 
81
81
  _syncDisplayedSwipeableItemCount() {
@@ -89,24 +89,20 @@ export class SwipeableMixin {
89
89
  if (!element) {
90
90
  return;
91
91
  }
92
+ setOrRemoveAttribute(element, 'data-item-count', itemCount);
92
93
  context.displayedCount = itemCount;
93
- if (itemCount === undefined) {
94
- element.removeAttribute('data-item-count');
95
- } else {
96
- element.setAttribute('data-item-count', itemCount);
97
- }
98
94
  }
99
95
 
100
- _syncSwipeableItemIndex() {
96
+ _syncDisplayedSwipeableItemIndex() {
101
97
  const context = this._swipeableContext;
102
- const index = context.itemIndex;
103
- if (index === context.displayedIndex) {
98
+ const { itemIndex } = context;
99
+ if (itemIndex === context.displayedIndex) {
104
100
  return;
105
101
  }
106
- context.displayedIndex = index;
102
+ context.displayedIndex = itemIndex;
107
103
 
108
- this._syncSwipeableListIndex(index);
109
- this._syncSwipeableIndicatorIndex(index);
104
+ this._syncSwipeableListIndex(itemIndex);
105
+ this._syncSwipeableIndicatorIndex(itemIndex);
110
106
  }
111
107
 
112
108
  _syncSwipeableListIndex(index) {
@@ -126,7 +122,7 @@ export class SwipeableMixin {
126
122
  }
127
123
  let i = 0;
128
124
  for (const item of indicatorElement.children) {
129
- item.classList[ i === index ? 'add' : 'remove' ]('active');
125
+ addOrRemoveClass(item, 'active', i === index);
130
126
  i++;
131
127
  }
132
128
  }
package/src/layout/raf.js CHANGED
@@ -89,6 +89,7 @@ export default class RafLayout {
89
89
 
90
90
  notifyUpdate && notifyUpdate(...argsForNotifyUpdate.value);
91
91
 
92
+ // TODO: should the signature be aligned with _render?
92
93
  this._afterRender(this._element, state);
93
94
  }
94
95
 
@@ -187,15 +187,27 @@ export function cta(layout, data, meta) {
187
187
 
188
188
  // helpers //
189
189
  function tagPair({ className, options = {} }, { product_id, url }, { classSuffix = 'item-body', role = 'item', ...meta } = {}) {
190
- const { link = {} } = options;
191
- const { target = '_blank', rel = 'noopener' } = link; // TODO: other properties
192
190
  const tag = meta.link !== false && url ? 'a' : 'div';
193
- const urlAttrs = meta.link !== false && url ? `href="${url}" target="${target}" rel="${rel}"` : '';
191
+ const urlAttrs = meta.link !== false && url ? `href="${url}" ${linkAttrs(options.link)}` : '';
194
192
  const productAttrs = product_id ? `${ATTR_DATA_MISO_PRODUCT_ID}="${product_id}"` : '';
195
193
  const roleAttrs = role ? `data-role="${role}"` : '';
196
194
  return [`<${tag} class="${className}__${classSuffix}" ${roleAttrs} ${productAttrs} ${urlAttrs}>`, `</${tag}>`];
197
195
  }
198
196
 
197
+ function linkAttrs(link = {}) {
198
+ const { target = '_blank', rel = 'noopener', open = true } = link; // TODO: other properties
199
+ let attrs = '';
200
+ if (open) {
201
+ if (target) {
202
+ attrs += ` target="${target}"`;
203
+ }
204
+ if (rel) {
205
+ attrs += ` rel="${rel}"`;
206
+ }
207
+ }
208
+ return attrs;
209
+ }
210
+
199
211
  const DEFAULT_DATE_OPTIONS = Object.freeze({ locale: 'en-US', year: 'numeric', month: 'short', day: 'numeric' });
200
212
  const DEFAULT_NUMBER_OPTIONS = 'en-US';
201
213
 
@@ -0,0 +1,103 @@
1
+ export function setOrRemoveAttribute(element, name, value) {
2
+ if (value === undefined) {
3
+ element.removeAttribute(name);
4
+ } else {
5
+ element.setAttribute(name, value);
6
+ }
7
+ }
8
+
9
+ export function addOrRemoveClass(element, name, add) {
10
+ if (add) {
11
+ element.classList.add(name);
12
+ } else {
13
+ element.classList.remove(name);
14
+ }
15
+ }
16
+
17
+ export function resolveCssLength(str) {
18
+ if (typeof str === 'number') {
19
+ return str;
20
+ }
21
+ const [value, unit] = parseLength(str);
22
+ switch (unit) {
23
+ case '':
24
+ case 'px':
25
+ return value;
26
+ case 'rem':
27
+ return remToPx(value);
28
+ default:
29
+ throw new Error(`Unsupported unit: ${unit}`);
30
+ }
31
+ }
32
+
33
+ function parseLength(str) {
34
+ const match = str.match(/^([+-]?(?:\d+|\d*\.\d+))([a-z]*)$/);
35
+ if (!match) {
36
+ throw new Error(`Invalid length: ${str}`);
37
+ }
38
+ const value = parseFloat(match[1]);
39
+ const unit = match[2] || '';
40
+ return [value, unit];
41
+ }
42
+
43
+ function remToPx(value = 1) {
44
+ return value * parseFloat(getComputedStyle(document.documentElement).fontSize);
45
+ }
46
+
47
+ export function cssAspectRatio(value) {
48
+ return (value === undefined || value instanceof CssAspectRatio) ? value : new CssAspectRatio(value);
49
+ }
50
+
51
+ class CssAspectRatio {
52
+
53
+ constructor(value) {
54
+ this._width = NaN;
55
+ this._height = NaN;
56
+ switch (typeof value) {
57
+ case 'number':
58
+ this._width = value;
59
+ this._height = 1;
60
+ break;
61
+ case 'object':
62
+ if (Array.isArray(value)) {
63
+ this._width = parseFloat(value[0]);
64
+ this._height = parseFloat(value[1]);
65
+ }
66
+ break;
67
+ case 'string':
68
+ const i = value.indexOf('/');
69
+ if (i < 0) {
70
+ this._width = parseFloat(value);
71
+ this._height = 1;
72
+ } else {
73
+ this._width = parseFloat(value.slice(0, i).trim());
74
+ this._height = parseFloat(value.slice(i + 1).trim());
75
+ }
76
+ break;
77
+ }
78
+ if (this._width === NaN || this._height === NaN) {
79
+ throw new Error(`Invalid aspect ratio value: ${value}`);
80
+ }
81
+ }
82
+
83
+ get width() {
84
+ return this._width;
85
+ }
86
+
87
+ get height() {
88
+ return this._height;
89
+ }
90
+
91
+ get floatValue() {
92
+ return this._width / this._height;
93
+ }
94
+
95
+ scaleBy(factor) {
96
+ factor = cssAspectRatio(factor);
97
+ return cssAspectRatio([factor._width * this._width, factor._height * this._height]);
98
+ }
99
+
100
+ toString() {
101
+ return this._height === 1 ? `${this._width}` : `${this._width} / ${this._height}`;
102
+ }
103
+ }
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.11.5-beta.0';
1
+ export default '1.11.5-beta.10';
@@ -2,10 +2,10 @@ import { API } from '@miso.ai/commons';
2
2
  import Workflow from './base.js';
3
3
  import { fields, FeedbackActor } from '../actor/index.js';
4
4
  import { ROLE, STATUS, ORGANIC_QUESTION_SOURCE } from '../constants.js';
5
- import { SearchBoxLayout, TextLayout, ListLayout, CarouselLayout, TypewriterLayout, FeedbackLayout, AffiliationLayout } from '../layout/index.js';
5
+ import { SearchBoxLayout, TextLayout, ListLayout, GalleryLayout, TypewriterLayout, FeedbackLayout, AffiliationLayout } from '../layout/index.js';
6
6
  import { processData as processAffiliationData } from '../affiliation/index.js';
7
- import { mergeRolesOptions, DEFAULT_TRACKER_OPTIONS } from './options/index.js';
8
- import { writeAnswerStageToMeta, mergeInteraction } from './processors.js';
7
+ import { mergeRolesOptions, autoQuery as autoQueryFn, DEFAULT_AUTO_QUERY_PARAM, DEFAULT_TRACKER_OPTIONS } from './options/index.js';
8
+ import { writeAnswerStageToMeta, writeEventTargetToInteraction, mergeInteraction } from './processors.js';
9
9
  import { enableUseLink } from './use-link.js';
10
10
  import { isTracked, markAsTracked } from '../util/trackers.js';
11
11
 
@@ -35,7 +35,7 @@ const DEFAULT_LAYOUTS = Object.freeze({
35
35
  [ROLE.QUESTION]: [TextLayout.type, { tag: 'h2' }],
36
36
  [ROLE.ANSWER]: TypewriterLayout.type,
37
37
  [ROLE.FEEDBACK]: FeedbackLayout.type,
38
- [ROLE.IMAGES]: [CarouselLayout.type, { incremental: true, itemType: 'image' }],
38
+ [ROLE.IMAGES]: [GalleryLayout.type, { incremental: true, itemType: 'image' }],
39
39
  [ROLE.SOURCES]: [ListLayout.type, { incremental: true, itemType: 'article', templates: { ordered: true } }],
40
40
  [ROLE.AFFILIATION]: [AffiliationLayout.type, { incremental: true, itemType: 'affiliation', link: { rel: 'noopener nofollow' } }],
41
41
  });
@@ -67,10 +67,6 @@ const ROLES_OPTIONS = mergeRolesOptions(Workflow.ROLES_OPTIONS, {
67
67
  members: Object.keys(DEFAULT_LAYOUTS),
68
68
  });
69
69
 
70
- function normalizeAutoQueryOptions({ setValue = true, focus = true, param = 'q', sourceParam = 'qs', ...options } = {}) {
71
- return { setValue, focus, param, sourceParam, ...options };
72
- }
73
-
74
70
  export default class AnswerBasedWorkflow extends Workflow {
75
71
 
76
72
  // constructor //
@@ -146,22 +142,7 @@ export default class AnswerBasedWorkflow extends Workflow {
146
142
 
147
143
  // query //
148
144
  autoQuery(options = {}) {
149
- const { setValue, focus, param, sourceParam } = this._autoQuery = normalizeAutoQueryOptions(options);
150
- const searchParams = new URLSearchParams(window.location.search);
151
- const q = searchParams.get(param);
152
- const qs = searchParams.get(sourceParam);
153
- const { layout } = this._views.get(ROLE.QUERY);
154
- if (layout) {
155
- if (q && setValue) {
156
- layout.value = q;
157
- }
158
- if (!q && focus) {
159
- layout.focus();
160
- }
161
- }
162
- if (q) {
163
- this.query({ q, qs });
164
- }
145
+ autoQueryFn.call(this, options);
165
146
  }
166
147
 
167
148
  query(args) {
@@ -196,8 +177,21 @@ export default class AnswerBasedWorkflow extends Workflow {
196
177
  throw new Error(`Not implemented`);
197
178
  }
198
179
 
180
+ _writeWikiLinkTemplateToPayload(payload) {
181
+ if (!this._autoQuery || !this._autoQuery.param || this._autoQuery.param === DEFAULT_AUTO_QUERY_PARAM) {
182
+ return payload;
183
+ }
184
+ return {
185
+ ...payload,
186
+ wiki_link_template: `?${this._autoQuery.param}=%s`,
187
+ };
188
+ }
189
+
199
190
  // data //
200
191
  _updateData(data) {
192
+ // capture question ID and register at context
193
+ this._writeQuestionIdFromData(data);
194
+
201
195
  // if it's the head response, write question id and return
202
196
  if (data.value && !data.value.answer_stage) {
203
197
  this._handleHeadResponse(data);
@@ -209,10 +203,7 @@ export default class AnswerBasedWorkflow extends Workflow {
209
203
  this._updateUrlIfNecessary(data);
210
204
  }
211
205
 
212
- _handleHeadResponse(data) {
213
- // capture question ID and register at context
214
- this._writeQuestionIdFromData(data);
215
- }
206
+ _handleHeadResponse(data) {}
216
207
 
217
208
  _defaultProcessData(data) {
218
209
  data = super._defaultProcessData(data);
@@ -222,28 +213,34 @@ export default class AnswerBasedWorkflow extends Workflow {
222
213
  }
223
214
 
224
215
  _writeQuestionIdFromData({ value }) {
225
- if (!value) {
226
- return;
227
- }
228
- const { question_id: questionId } = value;
229
- this._writeQuestionId(questionId);
216
+ value && this._writeQuestionId(value.question_id);
230
217
  }
231
218
 
232
219
  _updateUrlIfNecessary({ value, request }) {
233
- if (!value || !this._autoQuery || this._autoQuery.updateUrl === false) { // explicitly false
220
+ if (!this._autoQuery || !this._autoQuery.updateUrl) {
234
221
  return;
235
222
  }
236
- const { param } = this._autoQuery;
237
- const { question } = value;
238
- const organic = request && request.payload && request.payload._meta && request.payload._meta.question_source === ORGANIC_QUESTION_SOURCE;
223
+ const { param, sourceParam } = this._autoQuery;
224
+ const question = (value && value.question) || (request && request.payload && request.payload.question);
225
+ const questionSource = request && request.payload && request.payload._meta && request.payload._meta.question_source;
226
+
227
+ if (!question) {
228
+ return; // at initial phase
229
+ }
239
230
 
240
231
  const url = new URL(window.location);
241
232
  const currentQuestion = url.searchParams.get(param);
242
- // TODO: review this, handle &qs=
243
- if (question !== currentQuestion && (!organic || this._autoQuery.updateUrl === true)) {
244
- url.searchParams.set(param, question);
245
- window.history.replaceState({}, '', url);
233
+ const currentQuestionSource = url.searchParams.get(sourceParam) || ORGANIC_QUESTION_SOURCE;
234
+ if (question === currentQuestion && questionSource === currentQuestionSource) {
235
+ return;
236
+ }
237
+ url.searchParams.set(param, question);
238
+ if (questionSource === ORGANIC_QUESTION_SOURCE) {
239
+ url.searchParams.delete(sourceParam);
240
+ } else {
241
+ url.searchParams.set(sourceParam, questionSource);
246
242
  }
243
+ window.history.replaceState({}, '', url);
247
244
  }
248
245
 
249
246
  // interactions //
@@ -251,6 +248,7 @@ export default class AnswerBasedWorkflow extends Workflow {
251
248
  payload = super._defaultProcessInteraction(payload, args);
252
249
  payload = this._writeAskPropertiesToInteraction(payload, args);
253
250
  payload = this._writeAnswerClickInteraction(payload, args);
251
+ payload = writeEventTargetToInteraction(payload, args);
254
252
  return payload;
255
253
  }
256
254
 
@@ -304,7 +302,9 @@ export default class AnswerBasedWorkflow extends Workflow {
304
302
  }
305
303
  // TODO: should we track impression as well?
306
304
  markAsTracked(event);
307
- this._views.trackers.sources.click([source.product_id]);
305
+ // distinguish from regular sources element click
306
+ // TODO
307
+ this._views.trackers.sources.click([source.product_id], { event_target: 'citation-link' });
308
308
  }
309
309
 
310
310
  _onAnswerLinkClick({ event, ...item }) {
@@ -115,17 +115,19 @@ export default class Ask extends AnswerBasedWorkflow {
115
115
  }
116
116
 
117
117
  // query //
118
- _buildPayload({ q, qs, ...payload } = {}) {
118
+ _buildPayload({ q, qs, ...options } = {}) {
119
119
  const { parentQuestionId } = this;
120
- return trimObj({
121
- ...payload,
120
+ let payload = trimObj({
121
+ ...options,
122
122
  question: q, // question, not q
123
123
  parent_question_id: parentQuestionId,
124
124
  _meta: {
125
- ...payload._meta,
125
+ ...options._meta,
126
126
  question_source: qs || ORGANIC_QUESTION_SOURCE, // might be null, not undefined
127
127
  },
128
128
  });
129
+ payload = this._writeWikiLinkTemplateToPayload(payload);
130
+ return payload;
129
131
  }
130
132
 
131
133
  // interactions //
@@ -10,14 +10,16 @@ import { getRevision, writeDataStatus, writeMisoIdToMeta, buildBaseInteraction,
10
10
  function injectLogger(hub, callback) {
11
11
  const { update, trigger } = hub;
12
12
  hub.update = (name, state, options) => {
13
+ const result = update.call(hub, name, state, options);
13
14
  if (!options || !options.silent) {
14
15
  callback('update', name, state, options);
15
16
  }
16
- return update.call(hub, name, state, options);
17
+ return result;
17
18
  }
18
19
  hub.trigger = (...args) => {
20
+ const result = trigger.apply(hub, args);
19
21
  callback('trigger', ...args);
20
- return trigger.apply(hub, args);
22
+ return result;
21
23
  }
22
24
  return hub;
23
25
  }
@@ -87,6 +87,17 @@ export default class Explore extends Workflow {
87
87
  return super.useApi(options);
88
88
  }
89
89
 
90
+ useLink(fn, options) {
91
+ UseLinkMixin.prototype.useLink.call(this, fn, options);
92
+ // also put options to layouts
93
+ if (options) {
94
+ this.useLayouts({
95
+ [ROLE.RELATED_QUESTIONS]: { link: options },
96
+ });
97
+ }
98
+ return this;
99
+ }
100
+
90
101
  // lifecycle //
91
102
  start({ relatedQuestions = true } = {}) {
92
103
  if (this._linkFn === undefined) {
@@ -121,7 +132,7 @@ export default class Explore extends Workflow {
121
132
  return data;
122
133
  }
123
134
  const linkFn = this._linkFn && this._linkFn[0];
124
- const related_questions = value.related_questions.map(linkFn ? (text => ({ text, url: this._getSubmitUrl({ q: text }, { generated: true }) })) : (text => ({ text })));
135
+ const related_questions = value.related_questions.map(linkFn ? (text => ({ text, url: this._getSubmitUrl({ q: text, generated: true }) })) : (text => ({ text })));
125
136
  return {
126
137
  ...data,
127
138
  value: {
@@ -143,12 +154,12 @@ export default class Explore extends Workflow {
143
154
  }
144
155
 
145
156
  _query(args) {
146
- this._submitToPage(args);
157
+ this._submitToPage({ ...args, generated: false });
147
158
  }
148
159
 
149
- _getSubmitUrl(args, { generated = false } = {}) {
160
+ _getSubmitUrl(args) {
150
161
  let url = UseLinkMixin.prototype._getSubmitUrl.call(this, args);
151
- if (!generated || !this._productId) {
162
+ if (!args.generated || !this._productId) {
152
163
  return url;
153
164
  }
154
165
  return `${url}&qs=${encodeURIComponent(this._productId)}`;