@miso.ai/client-sdk-ui 1.11.4-beta.5 → 1.11.5-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 (39) hide show
  1. package/dist/cjs/actor/views.js +1 -0
  2. package/dist/cjs/asset/svgs.js +10 -4
  3. package/dist/cjs/constants.js +3 -2
  4. package/dist/cjs/element/role/index.js +7 -0
  5. package/dist/cjs/element/role/miso-images.js +20 -0
  6. package/dist/cjs/layout/input/option-list.js +1 -1
  7. package/dist/cjs/layout/input/select.js +1 -1
  8. package/dist/cjs/layout/list/carousel.js +43 -84
  9. package/dist/cjs/layout/list/collection.js +11 -5
  10. package/dist/cjs/layout/mixin/swipeable.js +165 -0
  11. package/dist/cjs/layout/{trackable.js → mixin/trackable.js} +4 -3
  12. package/dist/cjs/layout/{triggerable.js → mixin/triggerable.js} +2 -2
  13. package/dist/cjs/layout/radio/radio.js +11 -7
  14. package/dist/cjs/layout/templates.js +24 -5
  15. package/dist/cjs/layout/text/typewriter/index.js +2 -0
  16. package/dist/cjs/util/trackers.js +18 -5
  17. package/dist/cjs/version.js +1 -1
  18. package/dist/cjs/workflow/answer-based.js +23 -4
  19. package/dist/cjs/workflow/search.js +0 -1
  20. package/package.json +2 -2
  21. package/src/actor/views.js +1 -0
  22. package/src/asset/svgs.js +9 -1
  23. package/src/constants.js +3 -0
  24. package/src/element/role/index.js +1 -0
  25. package/src/element/role/miso-images.js +16 -0
  26. package/src/layout/input/option-list.js +1 -1
  27. package/src/layout/input/select.js +1 -1
  28. package/src/layout/list/carousel.js +39 -86
  29. package/src/layout/list/collection.js +13 -6
  30. package/src/layout/mixin/swipeable.js +162 -0
  31. package/src/layout/{trackable.js → mixin/trackable.js} +4 -3
  32. package/src/layout/{triggerable.js → mixin/triggerable.js} +2 -2
  33. package/src/layout/radio/radio.js +7 -7
  34. package/src/layout/templates.js +26 -5
  35. package/src/layout/text/typewriter/index.js +2 -1
  36. package/src/util/trackers.js +17 -4
  37. package/src/version.js +1 -1
  38. package/src/workflow/answer-based.js +14 -4
  39. package/src/workflow/search.js +0 -1
@@ -3,13 +3,20 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.isTracked = isTracked;
7
+ exports.markAsTracked = markAsTracked;
6
8
  exports.normalizeTrackerOptions = normalizeTrackerOptions;
7
9
  exports.validateClick = validateClick;
8
10
  var _commons = require("@miso.ai/commons");
9
11
  const _excluded = ["impression", "viewable", "click"];
10
12
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
11
- // TODO: think about how to incorporate this with workflow options
12
-
13
+ const TRACKED = Symbol.for('miso:tracked');
14
+ function isTracked(event) {
15
+ return !!event[TRACKED];
16
+ }
17
+ function markAsTracked(event) {
18
+ event[TRACKED] = true;
19
+ }
13
20
  const DEFAULT_TRACKER_OPTIONS = Object.freeze({
14
21
  impression: Object.freeze({}),
15
22
  viewable: Object.freeze({
@@ -24,6 +31,8 @@ function mergeOptions(def, opt) {
24
31
  // if opt is falsy then return false, merge otherwise
25
32
  return !!opt && Object.assign({}, def, opt);
26
33
  }
34
+
35
+ // TODO: don't need this anymore
27
36
  function normalizeTrackerOptions(options) {
28
37
  // TODO: make this { active: false } to keep it an object
29
38
  if (options === false) {
@@ -61,15 +70,19 @@ function validateClick() {
61
70
  }
62
71
 
63
72
  // standard criteria
64
- // 1. it must be a left click
73
+ // 1. each event can only be tracked once
74
+ if (isTracked(event)) {
75
+ return false;
76
+ }
77
+ // 2. it must be a left click
65
78
  if (event.button !== 0) {
66
79
  return false;
67
80
  }
68
- // 2. event default must not be prevented
81
+ // 3. event default must not be prevented
69
82
  if (event.defaultPrevented) {
70
83
  return false;
71
84
  }
72
- // 3. must go through a real link
85
+ // 4. must go through a real link
73
86
  if (!(0, _commons.findInAncestors)(event.target, element => element.tagName === 'A' && element.href || undefined)) {
74
87
  return false;
75
88
  }
@@ -4,4 +4,4 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = void 0;
7
- var _default = exports.default = '1.11.4-beta.5';
7
+ var _default = exports.default = '1.11.5-beta.0';
@@ -13,7 +13,9 @@ var _index3 = require("../affiliation/index.js");
13
13
  var _index4 = require("./options/index.js");
14
14
  var _processors = require("./processors.js");
15
15
  var _useLink = require("./use-link.js");
16
- const _excluded = ["setValue", "focus", "param", "sourceParam"];
16
+ var _trackers = require("../util/trackers.js");
17
+ const _excluded = ["setValue", "focus", "param", "sourceParam"],
18
+ _excluded2 = ["event"];
17
19
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
18
20
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
19
21
  const DEFAULT_API_OPTIONS = Object.freeze(Object.assign({}, _base.default.DEFAULT_API_OPTIONS, {
@@ -41,6 +43,10 @@ const DEFAULT_LAYOUTS = Object.freeze(Object.assign({}, _base.default.DEFAULT_LA
41
43
  }],
42
44
  [_constants.ROLE.ANSWER]: _index2.TypewriterLayout.type,
43
45
  [_constants.ROLE.FEEDBACK]: _index2.FeedbackLayout.type,
46
+ [_constants.ROLE.IMAGES]: [_index2.CarouselLayout.type, {
47
+ incremental: true,
48
+ itemType: 'image'
49
+ }],
44
50
  [_constants.ROLE.SOURCES]: [_index2.ListLayout.type, {
45
51
  incremental: true,
46
52
  itemType: 'article',
@@ -63,6 +69,7 @@ const DEFAULT_TRACKERS = Object.freeze(Object.assign({}, _base.default.DEFAULT_T
63
69
  deduplicated: false,
64
70
  click: _index4.DEFAULT_TRACKER_OPTIONS.click // click only
65
71
  },
72
+ [_constants.ROLE.IMAGES]: _index4.DEFAULT_TRACKER_OPTIONS,
66
73
  [_constants.ROLE.SOURCES]: _index4.DEFAULT_TRACKER_OPTIONS,
67
74
  [_constants.ROLE.AFFILIATION]: _index4.DEFAULT_TRACKER_OPTIONS,
68
75
  [_constants.ROLE.PRODUCTS]: _index4.DEFAULT_TRACKER_OPTIONS
@@ -306,7 +313,6 @@ class AnswerBasedWorkflow extends _base.default {
306
313
  if (args.role !== _constants.ROLE.ANSWER) {
307
314
  return payload;
308
315
  }
309
- console.log('writeAnswerClickInteraction', payload, args);
310
316
  const {
311
317
  items = []
312
318
  } = args;
@@ -334,8 +340,12 @@ class AnswerBasedWorkflow extends _base.default {
334
340
  // handlers //
335
341
  _onCitationClick(_ref4) {
336
342
  let {
337
- index
343
+ index,
344
+ event
338
345
  } = _ref4;
346
+ if ((0, _trackers.isTracked)(event)) {
347
+ return;
348
+ }
339
349
  const {
340
350
  value
341
351
  } = this.states[_index.fields.data()] || {};
@@ -348,9 +358,18 @@ class AnswerBasedWorkflow extends _base.default {
348
358
  return;
349
359
  }
350
360
  // TODO: should we track impression as well?
361
+ (0, _trackers.markAsTracked)(event);
351
362
  this._views.trackers.sources.click([source.product_id]);
352
363
  }
353
- _onAnswerLinkClick(item) {
364
+ _onAnswerLinkClick(_ref5) {
365
+ let {
366
+ event
367
+ } = _ref5,
368
+ item = _objectWithoutPropertiesLoose(_ref5, _excluded2);
369
+ if ((0, _trackers.isTracked)(event)) {
370
+ return;
371
+ }
372
+ (0, _trackers.markAsTracked)(event);
354
373
  // put everything into args and let _defaultProcessInteraction() handles it
355
374
  // for it's hard to make it a standard item
356
375
  this._views.trackers.answer.click([], {
@@ -85,7 +85,6 @@ class Search extends _searchBased.default {
85
85
 
86
86
  // destroy //
87
87
  _destroy(options) {
88
- this._feedback._destroy();
89
88
  this._destroyAutocomplete();
90
89
  super._destroy(options);
91
90
  }
package/package.json CHANGED
@@ -26,7 +26,7 @@
26
26
  "url": "https://github.com/MisoAI/miso-client-js-sdk/issues"
27
27
  },
28
28
  "dependencies": {
29
- "@miso.ai/commons": "1.11.4-beta.5"
29
+ "@miso.ai/commons": "1.11.5-beta.0"
30
30
  },
31
- "version": "1.11.4-beta.5"
31
+ "version": "1.11.5-beta.0"
32
32
  }
@@ -213,6 +213,7 @@ export default class ViewsActor {
213
213
 
214
214
  _getTrackerOptions(role) {
215
215
  const allTrackerOptions = this._options.resolved.trackers;
216
+ // TODO: don't need this anymore
216
217
  return normalizeTrackerOptions((allTrackerOptions && allTrackerOptions[role]) || false);
217
218
  }
218
219
 
package/src/asset/svgs.js CHANGED
@@ -14,6 +14,8 @@ const ICON_NAME = {
14
14
  SEND: 'send',
15
15
  SEARCH: 'search',
16
16
  TRIANGLE: 'triangle',
17
+ TRIANGLE_EQ: 'triangle-eq',
18
+ CHEVRON_NEGATIVE: 'chevron-negative',
17
19
  };
18
20
 
19
21
  // CC0 SVGs
@@ -23,7 +25,11 @@ const ICON_SEARCH = `<symbol id="${PREFIX}-${ICON_NAME.SEARCH}" viewBox="0 0 24
23
25
  // Custom SVGs
24
26
  const ICON_TRIANGLE = `<symbol id="${PREFIX}-${ICON_NAME.TRIANGLE}" viewBox="-5 -5 105 105"><path d="${trianglePath(5)}"/></symbol>`;
25
27
 
26
- const ICONS = [ICON_SEND, ICON_SEARCH, ICON_TRIANGLE];
28
+ const ICON_TRIANGLE_EQ = `<symbol id="${PREFIX}-${ICON_NAME.TRIANGLE_EQ}" viewBox="-100 -100 200 200"><path d="M12.99 -77.5A15 15,0,0,0,-12.99 -77.5L-73.61 27.5A15 15,0,0,0,-60.62 50L60.62 50A15 15,0,0,0,73.61 27.5L12.99 -77.5Z"/></symbol>`;
29
+
30
+ const ICON_CHEVRON_NEGATIVE = `<symbol id="${PREFIX}-${ICON_NAME.CHEVRON_NEGATIVE}" viewBox="-100 -100 200 200"><path d="M100 100V-200H-200V200H200ZM-10.61 -43.94A15 15,0,0,1,10.61 -43.94L60.61 6.06A15 15,0,0,1,39.39 27.27L0 -12.12L-39.39 27.27A15 15,0,0,1,-60.61 6.06L-10.61 -43.94Z"/></symbol>`;
31
+
32
+ const ICONS = [ICON_SEND, ICON_SEARCH, ICON_TRIANGLE, ICON_TRIANGLE_EQ, ICON_CHEVRON_NEGATIVE];
27
33
 
28
34
  const SPRITES_SVG = `<svg id="${ID}" style="display:none;">${ICONS.join('')}</svg>`;
29
35
 
@@ -52,3 +58,5 @@ export function getIcon(name) {
52
58
  export const SEND = _icon(ICON_NAME.SEND);
53
59
  export const SEARCH = _icon(ICON_NAME.SEARCH);
54
60
  export const TRIANGLE = _icon(ICON_NAME.TRIANGLE);
61
+ export const TRIANGLE_EQ = _icon(ICON_NAME.TRIANGLE_EQ);
62
+ export const CHEVRON_NEGATIVE = _icon(ICON_NAME.CHEVRON_NEGATIVE);
package/src/constants.js CHANGED
@@ -15,6 +15,7 @@ export const ROLE = Object.freeze({
15
15
  ITEMS: 'items',
16
16
  QUESTION: 'question',
17
17
  ANSWER: 'answer',
18
+ IMAGES: 'images',
18
19
  SOURCES: 'sources',
19
20
  RELATED_RESOURCES: 'related_resources',
20
21
  QUERY_SUGGESTIONS: 'query_suggestions',
@@ -36,6 +37,7 @@ const DATA_ROLE_SET = new Set([
36
37
  ROLE.ITEMS,
37
38
  ROLE.QUESTION,
38
39
  ROLE.ANSWER,
40
+ ROLE.IMAGES,
39
41
  ROLE.SOURCES,
40
42
  ROLE.RELATED_RESOURCES,
41
43
  ROLE.QUERY_SUGGESTIONS,
@@ -51,6 +53,7 @@ export function isDataRole(role) {
51
53
 
52
54
  const PRODUCT_ROLE_SET = new Set([
53
55
  ROLE.PRODUCTS,
56
+ ROLE.IMAGES,
54
57
  ROLE.SOURCES,
55
58
  ROLE.RELATED_RESOURCES,
56
59
  ]);
@@ -7,6 +7,7 @@ export { default as MisoProductsElement } from './miso-products.js';
7
7
  export { default as MisoQuestionElement } from './miso-question.js';
8
8
  export { default as MisoAnswerElement } from './miso-answer.js';
9
9
  export { default as MisoSourcesElement } from './miso-sources.js';
10
+ export { default as MisoImagesElement } from './miso-images.js';
10
11
  export { default as MisoRelatedResourcesElement } from './miso-related-resources.js';
11
12
  export { default as MisoRelatedQuestionsElement } from './miso-related-questions.js';
12
13
  export { default as MisoSovrnAffiliationElement } from './miso-affiliation.js';
@@ -0,0 +1,16 @@
1
+ import MisoComponentElement from './miso-component.js';
2
+ import { ROLE } from '../../constants.js';
3
+
4
+ export default class MisoImagesElement extends MisoComponentElement {
5
+
6
+ constructor() {
7
+ super({
8
+ role: ROLE.IMAGES,
9
+ });
10
+ }
11
+
12
+ static get tagName() {
13
+ return 'miso-images';
14
+ }
15
+
16
+ }
@@ -1,7 +1,7 @@
1
1
  import { escapeHtml } from '@miso.ai/commons';
2
2
  import { fields } from '../../actor/index.js';
3
3
  import TemplateBasedLayout from '../template.js';
4
- import { makeTrackable } from '../trackable.js';
4
+ import { makeTrackable } from '../mixin/trackable.js';
5
5
 
6
6
  const TYPE = 'option-list';
7
7
  const DEFAULT_CLASSNAME = 'miso-option-list';
@@ -1,6 +1,6 @@
1
1
  import { escapeHtml, kebabOrSnakeToHuman } from '@miso.ai/commons';
2
2
  import TemplateBasedLayout from '../template.js';
3
- import { makeTrackable } from '../trackable.js';
3
+ import { makeTrackable } from '../mixin/trackable.js';
4
4
 
5
5
  const TYPE = 'select';
6
6
  const DEFAULT_CLASSNAME = 'miso-select';
@@ -1,13 +1,19 @@
1
1
  import CollectionLayout from './collection.js';
2
+ import { makeSwipeable } from '../mixin/swipeable.js';
3
+ import { CHEVRON_NEGATIVE } from '../../asset/svgs.js';
2
4
 
3
5
  const TYPE = 'carousel';
4
6
  const DEFAULT_CLASSNAME = 'miso-carousel';
5
7
 
6
8
  const DEFAULT_TEMPLATES = Object.freeze({
7
- ready,
8
- carousel,
9
- previous: chevron,
10
- next: chevron,
9
+ body,
10
+ controls,
11
+ controlContainer,
12
+ control,
13
+ controlIcon,
14
+ indexIndicator,
15
+ indexIndicatorItem: () => ``,
16
+ trigger: () => ``, // no trigger element
11
17
  });
12
18
 
13
19
  const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
@@ -15,22 +21,33 @@ const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
15
21
  ...DEFAULT_TEMPLATES,
16
22
  });
17
23
 
18
- function ready(layout, state) {
24
+ function body(layout, state, values) {
25
+ const { className, templates } = layout;
26
+ return `<div class="${className}__viewport"><div class="${className}__viewport-inner">${templates.list(layout, state, values)}</div></div>${templates.controls(layout, state, values)}`;
27
+ }
28
+
29
+ function controls(layout, state, values) {
19
30
  const { templates } = layout;
20
- const items = layout._getItems(state);
21
- const nonEmpty = items && items.length > 0;
22
- const html = Object.getPrototypeOf(layout.constructor).defaultTemplates.ready(layout, state);
23
- return nonEmpty ? templates.carousel(layout, state, html) : html;
31
+ return `${templates.controlContainer(layout, state, 'previous')}${templates.controlContainer(layout, state, 'next')}${templates.indexIndicator(layout, state, values)}`;
32
+ }
33
+
34
+ function controlContainer(layout, state, direction) {
35
+ const { className, templates } = layout;
36
+ return `<div class="${className}__control-container-${direction}">${templates.control(layout, state, direction)}</div>`;
24
37
  }
25
38
 
26
- function carousel(layout, state, content) {
39
+ function control(layout, state, direction) {
27
40
  const { className, templates } = layout;
28
- return `<div class="${className}__control-previous" data-role="previous">${templates.previous(layout, state)}</div><div class="${className}__viewport">${content}</div><div class="${className}__control-next" data-role="next">${templates.next(layout, state)}</div>`;
41
+ return `<div class="${className}__control-${direction}" data-role="${direction}">${templates.controlIcon(layout, state, direction)}</div>`;
29
42
  }
30
43
 
31
- function chevron(layout) {
32
- const { className } = layout;
33
- return `<svg class="${className}__control-button" shape-rendering="geometricPrecision" stroke="currentColor" stroke-width="24" fill="#fff" viewBox="-30 -30 358 572"><path d="M285.77 441c16.24 16.17 16.32 42.46.15 58.7-16.16 16.24-42.45 16.32-58.69.16l-215-214.47c-16.24-16.16-16.32-42.45-.15-58.69L227.23 12.08c16.24-16.17 42.53-16.09 58.69.15 16.17 16.24 16.09 42.54-.15 58.7l-185.5 185.04L285.77 441z"/></svg>`;
44
+ function controlIcon(layout, state, direction) {
45
+ return CHEVRON_NEGATIVE;
46
+ }
47
+
48
+ function indexIndicator(layout, state, values) {
49
+ const { className, templates } = layout;
50
+ return `<ul class="${className}__index-indicator" data-role="index-indicator">${values.map(value => `<li class="${className}__index-indicator-item">${templates.indexIndicatorItem(layout, state, value)}</li>`).join('')}</ul>`;
34
51
  }
35
52
 
36
53
  export default class CarouselLayout extends CollectionLayout {
@@ -49,88 +66,24 @@ export default class CarouselLayout extends CollectionLayout {
49
66
 
50
67
  constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
51
68
  super({ className, templates: { ...DEFAULT_TEMPLATES, ...templates }, ...options });
52
- window.addEventListener('resize', this.syncSize = this.syncSize.bind(this));
53
- this._page = 0;
54
- this._unsubscribes = [];
69
+ this._initSwipeable();
55
70
  }
56
71
 
57
72
  _render(element, states, controls) {
58
73
  super._render(element, states, controls);
59
- const { state } = states;
60
- const items = this._getItems(state);
61
- this._itemCount = items ? items.length : undefined;
62
- this.syncSize();
63
- }
64
-
65
- get page() {
66
- return this._page;
67
- }
68
-
69
- get pageSize() {
70
- return this._pageSize;
71
- }
72
-
73
- next() {
74
- this._setPage(this._page + 1);
75
- }
76
-
77
- previous() {
78
- this._setPage(this._page - 1);
79
- }
80
-
81
- syncSize() {
82
- if (this._itemCount === undefined) {
83
- return; // data not ready yet
84
- }
85
- const grid = this._findGridElement();
86
- if (!grid) {
87
- return;
88
- }
89
- // update page size, page count and new page index
90
- const firstShownItemIndex = this._pageSize === undefined ? 0 : this._page * this._pageSize;
91
- const pageSize = this._pageSize = window.getComputedStyle(grid).getPropertyValue('grid-template-columns').split(' ').length;
92
- this._pageCount = Math.ceil(this._itemCount / this._pageSize);
93
- this._setPage(Math.floor(firstShownItemIndex / pageSize));
94
- }
95
-
96
- _setPage(page) {
97
- if (page < 0) {
98
- page = this._pageCount - 1;
99
- } else if (page >= this._pageCount) {
100
- page = 0;
101
- }
102
- if (page === this._page) {
103
- return;
104
- }
105
- this._page = page;
106
- const grid = this._findGridElement();
107
- if (grid) {
108
- const offset = page === 0 ? '' : `repeat(${page}, 0)`;
109
- grid.style.setProperty('grid-template-rows', `${offset} 1fr repeat(10, 0)`);
110
- }
74
+ this._renderSwipeable(element, states, controls);
111
75
  }
112
76
 
113
- _findGridElement() {
114
- const { element } = this._view;
115
- return element && element.querySelector(`.${this.className}__list`);
77
+ _afterRender(element, state) {
78
+ super._afterRender(element, state);
79
+ this._afterRenderSwipeable(element, state);
116
80
  }
117
81
 
118
82
  _onClick(event) {
83
+ this._onClickSwipeable(event);
119
84
  super._onClick(event);
120
- const element = event.target;
121
- if (element.closest(`[data-role="previous"]`)) {
122
- this.previous();
123
- return;
124
- }
125
- if (element.closest(`[data-role="next"]`)) {
126
- this.next();
127
- return;
128
- }
129
- }
130
-
131
- destroy() {
132
- window.removeEventListener('resize', this.syncSize);
133
- super.destroy();
134
85
  }
135
86
 
136
87
  }
88
+
89
+ makeSwipeable(CarouselLayout.prototype);
@@ -1,8 +1,8 @@
1
1
  import { STATUS } from '../../constants.js';
2
2
  import TemplateBasedLayout from '../template.js';
3
- import { makeTrackable } from '../trackable.js';
4
- import { makeTriggerable } from '../triggerable.js';
5
- import { product, article, question, productInfoBlock, articleInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
3
+ import { makeTrackable } from '../mixin/trackable.js';
4
+ import { makeTriggerable } from '../mixin/triggerable.js';
5
+ import { product, article, image, question, productInfoBlock, articleInfoBlock, imageInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock } from '../templates.js';
6
6
 
7
7
  function root(layout, state) {
8
8
  const { className, role, templates } = layout;
@@ -13,16 +13,20 @@ function root(layout, state) {
13
13
 
14
14
  function ready(layout, state) {
15
15
  const { templates } = layout;
16
- const values = layout._getItems(state);
16
+ const values = layout._getItems(state) || [];
17
17
 
18
18
  // TODO: handle categories, attributes, etc. by introducing sublayout
19
- if ((values && values.length > 0) || state.ongoing) { // TODO: ad-hoc ongoing?
20
- return templates.list(layout, state, values);
19
+ if (values.length > 0 || state.ongoing) { // TODO: ad-hoc ongoing?
20
+ return templates.body(layout, state, values);
21
21
  } else {
22
22
  return templates.empty(layout, state);
23
23
  }
24
24
  }
25
25
 
26
+ function body(layout, state, values) {
27
+ return layout.templates.list(layout, state, values);
28
+ }
29
+
26
30
  function list(layout, state, values) {
27
31
  const { className, templates, options } = layout;
28
32
  const listTag = templates.ordered ? 'ol' : 'ul';
@@ -57,16 +61,19 @@ function loading() {
57
61
  const DEFAULT_TEMPLATES = Object.freeze({
58
62
  product,
59
63
  article,
64
+ image,
60
65
  question,
61
66
  root,
62
67
  [STATUS.READY]: ready,
63
68
  empty: () => ``,
69
+ body,
64
70
  list,
65
71
  ordered: false,
66
72
  items,
67
73
  item,
68
74
  productInfoBlock,
69
75
  articleInfoBlock,
76
+ imageInfoBlock,
70
77
  brandBlock,
71
78
  titleBlock,
72
79
  descriptionBlock,
@@ -0,0 +1,162 @@
1
+ import { mixin, requestAnimationFrame as raf } from '@miso.ai/commons';
2
+
3
+ export function makeSwipeable(prototype) {
4
+ mixin(prototype, SwipeableMixin.prototype);
5
+ }
6
+
7
+ export class SwipeableMixin {
8
+
9
+ _initSwipeable() {
10
+ this._swipeableContext = {
11
+ itemCount: undefined,
12
+ itemIndex: undefined,
13
+ displayedCount: undefined,
14
+ displayedIndex: undefined,
15
+ displayEventIndex: undefined,
16
+ };
17
+ }
18
+
19
+ // TODO: autoplay
20
+
21
+ get itemIndex() {
22
+ return this._swipeableContext.itemIndex;
23
+ }
24
+
25
+ set itemIndex(index) {
26
+ const context = this._swipeableContext;
27
+ if (context.itemCount === undefined) {
28
+ return;
29
+ }
30
+ if (index < 0) {
31
+ index = context.itemCount - 1;
32
+ } else if (index >= context.itemCount) {
33
+ index = 0;
34
+ }
35
+ context.itemIndex = index;
36
+ raf(() => this._syncSwipeableItemIndex());
37
+ }
38
+
39
+ next() {
40
+ if (this._swipeableContext.itemCount === undefined) {
41
+ return;
42
+ }
43
+ this.itemIndex++;
44
+ }
45
+
46
+ previous() {
47
+ if (this._swipeableContext.itemCount === undefined) {
48
+ return;
49
+ }
50
+ this.itemIndex--;
51
+ }
52
+
53
+ _renderSwipeable(element, states, controls) {
54
+ this._syncSwipeableItemCount(states);
55
+ this._syncSwipeableItemIndex();
56
+ }
57
+
58
+ _afterRenderSwipeable() {
59
+ this._emitSwipeableDisplayEvent();
60
+ }
61
+
62
+ _syncSwipeableItemCount({ state } = {}) {
63
+ const context = this._swipeableContext;
64
+ const items = this._getItems(state);
65
+ const oldItemCount = context.itemCount;
66
+ const itemCount = context.itemCount = items ? items.length : undefined;
67
+
68
+ if (itemCount === oldItemCount) {
69
+ return;
70
+ }
71
+ // TODO: concern count === 0
72
+ if (itemCount === undefined) {
73
+ context.itemIndex = undefined;
74
+ } else if (oldItemCount === undefined) {
75
+ context.itemIndex = 0;
76
+ }
77
+
78
+ this._syncDisplayedSwipeableItemCount();
79
+ }
80
+
81
+ _syncDisplayedSwipeableItemCount() {
82
+ const context = this._swipeableContext;
83
+ const { itemCount } = context;
84
+ if (itemCount === context.displayedCount) {
85
+ return;
86
+ }
87
+ let { element } = this._view;
88
+ element = element.firstElementChild;
89
+ if (!element) {
90
+ return;
91
+ }
92
+ context.displayedCount = itemCount;
93
+ if (itemCount === undefined) {
94
+ element.removeAttribute('data-item-count');
95
+ } else {
96
+ element.setAttribute('data-item-count', itemCount);
97
+ }
98
+ }
99
+
100
+ _syncSwipeableItemIndex() {
101
+ const context = this._swipeableContext;
102
+ const index = context.itemIndex;
103
+ if (index === context.displayedIndex) {
104
+ return;
105
+ }
106
+ context.displayedIndex = index;
107
+
108
+ this._syncSwipeableListIndex(index);
109
+ this._syncSwipeableIndicatorIndex(index);
110
+ }
111
+
112
+ _syncSwipeableListIndex(index) {
113
+ const { element } = this._view;
114
+ const listElement = element && this._getListElement(element);
115
+ if (!listElement) {
116
+ return;
117
+ }
118
+ listElement.style.left = `${-index * 100}%`;
119
+ }
120
+
121
+ _syncSwipeableIndicatorIndex(index) {
122
+ const { element } = this._view;
123
+ const indicatorElement = element && this._getSwipeableIndicatorElement(element);
124
+ if (!indicatorElement) {
125
+ return;
126
+ }
127
+ let i = 0;
128
+ for (const item of indicatorElement.children) {
129
+ item.classList[ i === index ? 'add' : 'remove' ]('active');
130
+ i++;
131
+ }
132
+ }
133
+
134
+ _getSwipeableIndicatorElement(element) {
135
+ return element.querySelector('[data-role="index-indicator"]');
136
+ }
137
+
138
+ _emitSwipeableDisplayEvent() {
139
+ const context = this._swipeableContext;
140
+ const index = context.displayedIndex;
141
+ if (context.displayEventIndex === index) {
142
+ return;
143
+ }
144
+ context.displayEventIndex = index;
145
+ this._view._events.emit('display', { index });
146
+ }
147
+
148
+ _onClickSwipeable(event) {
149
+ const { target } = event;
150
+ if (target.closest(`[data-role="previous"]`)) {
151
+ event.preventDefault();
152
+ this.previous();
153
+ }
154
+ if (target.closest(`[data-role="next"]`)) {
155
+ event.preventDefault();
156
+ this.next();
157
+ }
158
+ }
159
+
160
+ // TODO: swipe gesture
161
+
162
+ }
@@ -1,7 +1,7 @@
1
1
  import { mixin } from '@miso.ai/commons';
2
- import Bindings from '../util/bindings.js';
3
- import Viewables from '../util/viewables.js';
4
- import { validateClick } from '../util/trackers.js';
2
+ import Bindings from '../../util/bindings.js';
3
+ import Viewables from '../../util/viewables.js';
4
+ import { validateClick, markAsTracked } from '../../util/trackers.js';
5
5
 
6
6
  export function makeTrackable(prototype) {
7
7
  mixin(prototype, TrackableMixin.prototype);
@@ -80,6 +80,7 @@ export class TrackableMixin {
80
80
  _trackClick(event, binding) {
81
81
  const { click: options } = this._view.tracker.options || {};
82
82
  if (validateClick(options, event, binding)) {
83
+ markAsTracked(event);
83
84
  this._view.tracker.click([binding.value]);
84
85
  }
85
86
  }
@@ -1,6 +1,6 @@
1
1
  import { mixin, viewable as whenViewable } from '@miso.ai/commons';
2
- import { STATUS } from '../constants.js';
3
- import { fields } from '../actor/index.js';
2
+ import { STATUS } from '../../constants.js';
3
+ import { fields } from '../../actor/index.js';
4
4
 
5
5
  export function makeTriggerable(prototype) {
6
6
  mixin(prototype, TriggerableMixin.prototype);