@miso.ai/client-sdk-ui 1.10.0-beta.1 → 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.
Files changed (78) hide show
  1. package/dist/cjs/actor/data.js +1 -1
  2. package/dist/cjs/actor/tracker.js +46 -70
  3. package/dist/cjs/actor/trackers.js +5 -1
  4. package/dist/cjs/actor/utils.js +6 -2
  5. package/dist/cjs/actor/view.js +14 -3
  6. package/dist/cjs/actor/views.js +9 -1
  7. package/dist/cjs/affiliation/index.js +126 -0
  8. package/dist/cjs/combo/ask/options.js +1 -1
  9. package/dist/cjs/combo/base.js +1 -1
  10. package/dist/cjs/constants.js +6 -3
  11. package/dist/cjs/defaults/ask/controls.js +1 -1
  12. package/dist/cjs/defaults/ask/phrases.js +1 -1
  13. package/dist/cjs/defaults/ask/templates.js +14 -4
  14. package/dist/cjs/element/role/index.js +7 -0
  15. package/dist/cjs/element/role/miso-affiliation.js +20 -0
  16. package/dist/cjs/layout/affiliation/affiliation.js +330 -0
  17. package/dist/cjs/layout/banner/banner.js +1 -1
  18. package/dist/cjs/layout/container/container.js +1 -1
  19. package/dist/cjs/layout/error/error.js +1 -1
  20. package/dist/cjs/layout/index.js +7 -0
  21. package/dist/cjs/layout/input/option-list.js +35 -50
  22. package/dist/cjs/layout/input/search-box.js +17 -5
  23. package/dist/cjs/layout/list/cards.js +1 -1
  24. package/dist/cjs/layout/list/carousel.js +5 -3
  25. package/dist/cjs/layout/list/collection.js +38 -48
  26. package/dist/cjs/layout/list/list.js +1 -1
  27. package/dist/cjs/layout/radio/feedback.js +1 -1
  28. package/dist/cjs/layout/radio/radio.js +1 -1
  29. package/dist/cjs/layout/raf.js +1 -1
  30. package/dist/cjs/layout/template.js +1 -1
  31. package/dist/cjs/layout/templates.js +135 -44
  32. package/dist/cjs/layout/text/text.js +1 -1
  33. package/dist/cjs/layout/text/typewriter/index.js +3 -3
  34. package/dist/cjs/layout/trackable.js +124 -0
  35. package/dist/cjs/plugin.js +1 -1
  36. package/dist/cjs/util/bindings.js +58 -110
  37. package/dist/cjs/util/states.js +31 -24
  38. package/dist/cjs/util/trackers.js +75 -0
  39. package/dist/cjs/util/viewables.js +55 -0
  40. package/dist/cjs/version.js +1 -1
  41. package/dist/cjs/workflow/ask.js +14 -5
  42. package/dist/cjs/workflow/asks.js +1 -1
  43. package/dist/cjs/workflow/base.js +5 -5
  44. package/dist/cjs/workflow/explore.js +1 -1
  45. package/dist/cjs/workflow/options.js +6 -1
  46. package/dist/cjs/workflow/recommendation.js +2 -0
  47. package/package.json +2 -2
  48. package/src/actor/tracker.js +39 -62
  49. package/src/actor/trackers.js +3 -1
  50. package/src/actor/utils.js +6 -2
  51. package/src/actor/view.js +12 -1
  52. package/src/actor/views.js +11 -0
  53. package/src/affiliation/index.js +108 -0
  54. package/src/constants.js +10 -1
  55. package/src/defaults/ask/phrases.js +1 -1
  56. package/src/defaults/ask/templates.js +13 -2
  57. package/src/element/role/index.js +1 -0
  58. package/src/element/role/miso-affiliation.js +16 -0
  59. package/src/layout/affiliation/affiliation.js +295 -0
  60. package/src/layout/index.js +1 -0
  61. package/src/layout/input/option-list.js +35 -43
  62. package/src/layout/input/search-box.js +7 -2
  63. package/src/layout/list/carousel.js +4 -2
  64. package/src/layout/list/collection.js +37 -43
  65. package/src/layout/templates.js +101 -26
  66. package/src/layout/text/typewriter/index.js +2 -2
  67. package/src/layout/trackable.js +106 -0
  68. package/src/util/bindings.js +47 -103
  69. package/src/util/states.js +26 -20
  70. package/src/util/trackers.js +69 -0
  71. package/src/util/viewables.js +47 -0
  72. package/src/version.js +1 -1
  73. package/src/workflow/ask.js +7 -3
  74. package/src/workflow/base.js +1 -1
  75. package/src/workflow/options.js +5 -1
  76. package/src/workflow/recommendation.js +1 -0
  77. package/dist/cjs/util/tracker.js +0 -348
  78. package/src/util/tracker.js +0 -318
@@ -42,37 +42,95 @@ export function article(layout, state, data, meta) {
42
42
  ].join('');
43
43
  }
44
44
 
45
- export function productInfoBlock({ className }, { title, description, sale_price, original_price }) {
46
- let content = '';
47
- if (title) {
48
- content += `<div class="${className}__item-title">${escapeHtml(title)}</div>`;
49
- }
50
- if (description) {
51
- content += `<div class="${className}__item-desc">${escapeHtml(description)}</div>`;
52
- }
53
- const price = sale_price || original_price;
54
- if (price) {
55
- content += `<hr><div class="${className}__item-price">${price}</div>`;
56
- // TODO: enhance this
45
+ export function affiliation(layout, state, data, meta) {
46
+ const { templates } = layout;
47
+ const [openTag, closeTag] = tagPair(layout, data);
48
+ return [
49
+ openTag,
50
+ (templates.infoBlock || templates.affiliationInfoBlock || affiliationInfoBlock)(layout, data, meta),
51
+ (templates.imageBlock || imageBlock)(layout, data, meta),
52
+ (templates.brandBlock || brandBlock)(layout, data, meta),
53
+ closeTag,
54
+ ].join('');
55
+ }
56
+
57
+ export function affiliationInfoBlock(layout, data, meta) {
58
+ const { className, templates } = layout;
59
+ return `<div class="${className}__item-info-container">${[
60
+ (templates.titleBlock || titleBlock)(layout, data, meta),
61
+ (templates.descriptionBlock || descriptionBlock)(layout, data, meta),
62
+ (templates.priceBlock || priceBlock)(layout, data, meta),
63
+ (templates.ctaBlock || ctaBlock)(layout, data, meta),
64
+ ].join('')}</div>`;
65
+ }
66
+
67
+ export function productInfoBlock(layout, data, meta) {
68
+ const { className, templates } = layout;
69
+ return `<div class="${className}__item-info-container">${[
70
+ (templates.titleBlock || titleBlock)(layout, data, meta),
71
+ (templates.descriptionBlock || descriptionBlock)(layout, data, meta),
72
+ (templates.priceBlock || priceBlock)(layout, data, meta),
73
+ ].join('')}</div>`;
74
+ }
75
+
76
+ export function articleInfoBlock(layout, data, meta) {
77
+ const { className, templates } = layout;
78
+ return `<div class="${className}__item-info-container">${[
79
+ (templates.titleBlock || titleBlock)(layout, data, meta),
80
+ (templates.dateBlock || dateBlock)(layout, data, meta),
81
+ (templates.descriptionBlock || descriptionBlock)(layout, data, meta),
82
+ (templates.ctaBlock || ctaBlock)(layout, data, meta),
83
+ ].join('')}</div>`;
84
+ }
85
+
86
+ export function titleBlock({ className }, { title }) {
87
+ return title ? `<div class="${className}__item-title">${escapeHtml(title)}</div>` : '';
88
+ }
89
+
90
+ export function brandBlock({ className }, { brand, brand_logo }) {
91
+ if (!brand && !brand_logo) {
92
+ return '';
57
93
  }
58
- return `<div class="${className}__item-info-container">${content}</div>`;
94
+ const content = brand_logo ? `<img class="${className}__item-brand-logo" src="${brand_logo}"${brand ? ` alt="${brand}"` : ''}>` : `<div class="${className}__item-brand">${brand}</div>`;
95
+ return `<div class="${className}__item-brand-container">${content}</div>`;
59
96
  }
60
97
 
61
- export function articleInfoBlock({ className, templates }, { title, snippet, description, created_at, updated_at, published_at }) {
98
+ export function descriptionBlock({ className }, { snippet, description }) {
99
+ return snippet ? `<div class="${className}__item-snippet">${snippet}</div>` : description ? `<div class="${className}__item-desc">${escapeHtml(description)}</div>` : '';
100
+ }
101
+
102
+ export function dateBlock({ className, templates }, { created_at, updated_at, published_at }) {
62
103
  const date = published_at || created_at || updated_at;
63
- let content = '';
64
- if (title) {
65
- content += `<div class="${className}__item-title">${escapeHtml(title)}</div>`;
104
+ return date ? `<div class="${className}__item-date">${formatDate(date, templates.date)}</div>` : '';
105
+ }
106
+
107
+ export function priceBlock(layout, data) {
108
+ const { className, templates } = layout;
109
+ const { sale_price, original_price, currency = 'USD' } = data;
110
+ const price = sale_price || original_price;
111
+ if (!price) {
112
+ return '';
66
113
  }
67
- if (date) {
68
- content += `<div class="${className}__item-date">${formatDate(date, templates.date)}</div>`;
114
+ const has_price_difference = original_price !== undefined && sale_price !== undefined && sale_price < original_price;
115
+ const discount_rate_percent = data.discount_rate_percent || (has_price_difference ? Math.floor((1 - sale_price / original_price) * 100) : undefined);
116
+
117
+ let content = '';
118
+ // original price
119
+ if (has_price_difference) {
120
+ content += `<span class="${className}__item-original-price miso-price" data-currency="${currency}">${original_price}</span><br>`;
69
121
  }
70
- if (snippet) {
71
- content += `<div class="${className}__item-snippet">${snippet}</div>`;
72
- } else if (description) {
73
- content += `<div class="${className}__item-desc">${escapeHtml(description)}</div>`;
122
+ // current price
123
+ content += `<span class="${className}__item-price miso-price" data-currency="${currency}">${price}</span>`;
124
+ // discount rate
125
+ if (discount_rate_percent) {
126
+ const text = (templates.discountRateText || discountRateText)(layout, { ...data, discount_rate_percent });
127
+ content += ` <span class="${className}__item-discount-rate">${text}</span>`;
74
128
  }
75
- return `<div class="${className}__item-info-container">${content}</div>`;
129
+ return `<div class="${className}__item-price-container">${content}</div>`;
130
+ }
131
+
132
+ export function discountRateText(layout, { discount_rate_percent }) {
133
+ return `(${discount_rate_percent}% off)`;
76
134
  }
77
135
 
78
136
  export function imageBlock({ className }, { cover_image }) {
@@ -84,8 +142,25 @@ export function imageBlock({ className }, { cover_image }) {
84
142
  }
85
143
 
86
144
  export function indexBlock({ className }, data, { index }) {
87
- const i = index + 1;
88
- return `<div class="${className}__item-index-container"><span class="${className}__item-index miso-citation-index" data-index="${i}"></span></div>`;
145
+ return `<div class="${className}__item-index-container"><span class="${className}__item-index miso-citation-index" data-index="${index + 1}"></span></div>`;
146
+ }
147
+
148
+ export function ctaBlock(layout, data, meta) {
149
+ const { className, templates } = layout;
150
+ let _cta = templates.cta || cta;
151
+ if (typeof _cta === 'function') {
152
+ _cta = _cta(layout, data, meta);
153
+ }
154
+ return _cta ? `<div class="${className}__item-cta-container">${_cta}</div>` : '';
155
+ }
156
+
157
+ export function cta(layout, data, meta) {
158
+ const { className, templates } = layout;
159
+ let text = templates.ctaText;
160
+ if (typeof text === 'function') {
161
+ text = text(layout, data, meta);
162
+ }
163
+ return text ? `<div class="${className}__item-cta">${text}</div>` : '';
89
164
  }
90
165
 
91
166
  // helpers //
@@ -81,7 +81,7 @@ export default class TypewriterLayout extends ProgressiveLayout {
81
81
  onDebug,
82
82
  });
83
83
  // capture citation link click if necessary
84
- this._unsubscribes.push(this._view.proxyElement.on('click', (e) => this._handleClick(e)));
84
+ this._unsubscribes.push(this._view.proxyElement.on('click', (e) => this._onClick(e)));
85
85
  }
86
86
 
87
87
  _getSource(index) {
@@ -148,7 +148,7 @@ export default class TypewriterLayout extends ProgressiveLayout {
148
148
  }
149
149
 
150
150
  // event //
151
- _handleClick({ target }) {
151
+ _onClick({ target }) {
152
152
  const element = target.closest(`[data-role="citation-link"]`);
153
153
  if (!element) {
154
154
  return;
@@ -0,0 +1,106 @@
1
+ import { EVENT_TYPE } from '../constants.js';
2
+ import Bindings from '../util/bindings.js';
3
+ import Viewables from '../util/viewables.js';
4
+ import { validateClick } from '../util/trackers.js';
5
+
6
+ export function makeTrackable(prototype) {
7
+ _mixin(prototype, TrackableMixin.prototype);
8
+ }
9
+
10
+ function _mixin(target, source) {
11
+ const descriptors = Object.getOwnPropertyDescriptors(source);
12
+ for (const key in descriptors) {
13
+ if (key === 'constructor') {
14
+ continue;
15
+ }
16
+ const { value } = descriptors[key];
17
+ if (target[key] === undefined && typeof value === 'function') {
18
+ target[key] = value;
19
+ }
20
+ // TODO: getter, setter
21
+ }
22
+ }
23
+
24
+ export class TrackableMixin {
25
+
26
+ _initTrackable() {
27
+ this._bindings = new Bindings();
28
+ this._viewables = new Viewables();
29
+ }
30
+
31
+ _syncBindings(element, state) {
32
+ const values = this._getItems(state) || [];
33
+ const keys = values.map(value => this._getItemKey(value));
34
+ const elements = this._getItemElements(element);
35
+
36
+ const { bounds, unbounds } = this._bindings.update(keys, values, elements);
37
+ this._onBindingsUpdate(bounds, unbounds);
38
+ }
39
+
40
+ _clearBindings() {
41
+ const { bounds, unbounds } = this._bindings.clear();
42
+ this._onBindingsUpdate(bounds, unbounds);
43
+ }
44
+
45
+ _getItemKey(value) {
46
+ // TODO: ad-hoc!
47
+ return value.product_id || value.text || value.id || value;
48
+ }
49
+
50
+ _getItems(state) {
51
+ throw new Error(`Not implemented.`);
52
+ }
53
+
54
+ _getItemElements(element) {
55
+ throw new Error(`Not implemented.`);
56
+ }
57
+
58
+ _onBindingsUpdate(bounds, unbounds) {
59
+ this._trackImpressions(bounds);
60
+ this._untrackViewables(unbounds);
61
+ this._trackViewables(bounds);
62
+ }
63
+
64
+ _trackImpressions(entries) {
65
+ if (entries.length === 0) {
66
+ return;
67
+ }
68
+ const { impression: options } = this._view.trackerOptions || {};
69
+ if (!options) {
70
+ return;
71
+ }
72
+ setTimeout(() => this._view._track(EVENT_TYPE.IMPRESSION, entries.map(({ value }) => value)), 0);
73
+ }
74
+
75
+ _untrackViewables(entries) {
76
+ for (const { element } of entries) {
77
+ this._viewables.untrack(element);
78
+ }
79
+ }
80
+
81
+ _trackViewables(entries) {
82
+ if (entries.length === 0) {
83
+ return;
84
+ }
85
+ const { viewable: options } = this._view.trackerOptions || {};
86
+ if (!options) {
87
+ return;
88
+ }
89
+ for (const { value, element } of entries) {
90
+ (async () => (await this._viewables.track(element, options)) && this._view._track(EVENT_TYPE.VIEWABLE, [value]))();
91
+ }
92
+ }
93
+
94
+ _trackClick(event, binding) {
95
+ const { click: options } = this._view.trackerOptions || {};
96
+ if (validateClick(options, event, binding)) {
97
+ this._view._track(EVENT_TYPE.CLICK, [binding.value]);
98
+ }
99
+ }
100
+
101
+ _destroyTrackable() {
102
+ const { unbounds } = this._bindings.clear();
103
+ this._untrackViewables(unbounds);
104
+ }
105
+
106
+ }
@@ -1,122 +1,66 @@
1
- import { isElement, isInDocument, findInAncestors } from '@miso.ai/commons';
2
-
3
- export function fallbackListFunction(keyAttrName) {
4
- return element => Array.from(element.querySelectorAll(`[${keyAttrName}]`)).map(element => ({
5
- element,
6
- value: {
7
- product_id: element.getAttribute(keyAttrName), // TODO: product_id is ad-hoc
8
- },
9
- }));
1
+ function tuple(keys, values, elements) {
2
+ const k2b = new Map();
3
+ const e2b = new Map();
4
+ const entries = [];
5
+ for (let index = 0; index < keys.length; index++) {
6
+ const key = keys[index];
7
+ const element = elements[index];
8
+ const value = values[index];
9
+ const entry = Object.freeze({ index, key, value, element });
10
+ k2b.set(key, entry);
11
+ e2b.set(element, entry);
12
+ entries.push(entry);
13
+ }
14
+ return { k2b, e2b, entries };
10
15
  }
11
16
 
12
- export class Bindings {
13
-
14
- constructor({
15
- keyAttrName = 'data-key',
16
- list,
17
- } = {}) {
18
- this._list = list || fallbackListFunction(keyAttrName);
19
- this._bindings = new Map();
20
- this._e2b = new WeakMap();
17
+ function diff(from, to) {
18
+ const entries = [];
19
+ for (const entry of to.entries) {
20
+ const f = from.e2b.get(entry.element);
21
+ if (!f || f.key !== entry.key) {
22
+ entries.push(entry);
23
+ }
21
24
  }
25
+ return entries;
26
+ }
22
27
 
23
- list() {
24
- return [...this._bindings.values()];
25
- }
28
+ export default class Bindings {
26
29
 
27
- get(ref) {
28
- if (!ref) {
29
- throw new Error(`Value or element is required.`);
30
- }
31
- if (!this._root) {
32
- return undefined;
33
- }
34
- if (!isElement(ref)) {
35
- return this._bindings.get(ref);
36
- }
37
- return findInAncestors(ref, element => this._e2b.get(element) || undefined, { root: this._root });
30
+ constructor() {
31
+ this._k2b = new Map();
32
+ this._e2b = new Map();
33
+ this._entries = [];
38
34
  }
39
35
 
40
- refresh(rootElement) {
41
- if (!rootElement) {
42
- return this.unbindAll();
43
- }
44
- this._root = rootElement;
45
- const unbounds = this._purge();
46
- const bounds = [];
47
- // warn for conflict productIds
48
- // TODO
49
- for (const pair of this._list(rootElement)) {
50
- const [binding, ...ubs] = this._bind(pair);
51
- if (binding) {
52
- bounds.push(binding);
53
- }
54
- for (const u of ubs) {
55
- unbounds.push(u);
56
- }
57
- }
58
- return { bounds, unbounds };
36
+ get entries() {
37
+ return [...this._entries];
59
38
  }
60
39
 
61
- unbindAll() {
62
- const unbounds = this.list();
63
- this._bindings.clear();
64
- this._root = undefined;
65
- return { bounds: [], unbounds };
40
+ get(element) {
41
+ return this._e2b.get(element) || undefined;
66
42
  }
67
43
 
44
+ update(keys, values, elements) {
45
+ const newTuple = tuple(keys, values, elements);
46
+ const oldTuple = { k2b: this._k2b, e2b: this._e2b, entries: this._entries };
68
47
 
48
+ const bounds = diff(oldTuple, newTuple);
49
+ const unbounds = diff(newTuple, oldTuple);
69
50
 
70
- _bind({ element, key, value }) {
71
- key = key || value;
72
- if (!key) {
73
- throw new Error(`Key or value is required.`);
74
- }
75
- if (!element) {
76
- throw new Error(`Element is required.`);
77
- }
78
-
79
- // check if binding already exist
80
- const oldBinding = this._bindings.get(key);
81
- if (oldBinding && oldBinding.element === element) {
82
- return [];
83
- }
84
- const unbound0 = this._unbind(this.get(key));
85
- const unbound1 = this._unbind(this.get(element));
86
-
87
- const binding = Object.freeze({ key, value, element });
88
- this._bindings.set(key, binding);
89
- this._e2b.set(element, binding);
90
-
91
- const results = [binding];
92
- unbound0 && results.push(unbound0);
93
- unbound1 && results.push(unbound1);
94
-
95
- return results;
96
- }
51
+ this._k2b = newTuple.k2b;
52
+ this._e2b = newTuple.e2b;
53
+ this._entries = newTuple.entries;
97
54
 
98
- _purge() {
99
- const purged = [];
100
- for (const binding of this._bindings.values()) {
101
- const { element } = binding;
102
- if (isInDocument(element)) {
103
- continue;
104
- }
105
- purged.push(this._unbind(binding));
106
- }
107
- return purged;
108
- }
109
-
110
- _unbind(binding) {
111
- if (binding) {
112
- this._bindings.delete(binding.key);
113
- this._e2b.delete(binding.element);
114
- }
115
- return binding;
55
+ return { bounds, unbounds };
116
56
  }
117
57
 
118
- _destroy() {
119
- this.unbindAll();
58
+ clear() {
59
+ const unbounds = this._entries;
60
+ this._k2b = new Map();
61
+ this._e2b = new Map();
62
+ this._entries = [];
63
+ return { bounds: [], unbounds };
120
64
  }
121
65
 
122
66
  }
@@ -4,6 +4,7 @@ import { EVENT_TYPE, TRACKING_STATUS, validateEventType, validateTrackingStatus
4
4
  const { IMPRESSION, VIEWABLE, CLICK } = EVENT_TYPE;
5
5
  const { UNTRACKED, TRIGGERED } = TRACKING_STATUS;
6
6
 
7
+ /*
7
8
  const STATES = {
8
9
  NEW: Object.freeze({
9
10
  [IMPRESSION]: UNTRACKED,
@@ -15,51 +16,56 @@ const STATES = {
15
16
  [CLICK]: UNTRACKED,
16
17
  }),
17
18
  };
19
+ */
20
+
21
+ const UNTRACKED_STATES = Object.freeze({
22
+ [IMPRESSION]: UNTRACKED,
23
+ [VIEWABLE]: UNTRACKED,
24
+ [CLICK]: UNTRACKED,
25
+ });
18
26
 
19
27
  /**
20
28
  * Tracking states for a session to keep track of the tracking status of each product.
21
- * For each entry there are status of impression, viewable, and click events.
22
- * The actual click tracking status depends on the presence of root element, which is not respected in this object.
29
+ * For each entry there are status of impression, viewable, and click events.
23
30
  */
24
31
  export default class States {
25
32
 
26
33
  constructor(sessionId) {
27
34
  this.sessionId = sessionId;
28
- this._states = new Map(); // productId -> {impression, viewable, click}
35
+ this._states = new Map(); // item key -> {impression, viewable, click}
29
36
  }
30
37
 
31
- getFullState(productId) {
38
+ getFullState(item) {
32
39
  // when we receive a bind event, impression is trigger, making an entry here
33
40
  // so if the state is not found, the product is not present yet => all untracked
34
- const state = this._states.get(getKey(productId));
35
- return state ? { ...state } : STATES.UNTRACKED;
41
+ const state = this._states.get(this._getKey(item));
42
+ return state ? { ...state } : UNTRACKED_STATES;
36
43
  }
37
44
 
38
- get(productId, type) {
39
- const state = this._states.get(getKey(productId));
45
+ get(item, type) {
46
+ const state = this._states.get(this._getKey(item));
40
47
  return state && state[type] || UNTRACKED;
41
48
  }
42
49
 
43
- set(productIds, type, status) {
50
+ set(items, type, status) {
44
51
  validateEventType(type);
45
52
  validateTrackingStatus(status);
46
- for (const productId of asArray(productIds)) {
47
- this._setOne(getKey(productId), type, status);
53
+ for (const item of asArray(items)) {
54
+ this._setOne(item, type, status);
48
55
  }
49
56
  }
50
57
 
51
- _setOne(productId, type, status) {
52
- // leave click untracked/tracking judged by its global state
53
- computeIfAbsent(this._states, productId, () => ({ ...STATES.NEW }))[type] = status;
58
+ untriggered(items, type) {
59
+ return items.filter(item => this.get(item, type) !== TRIGGERED);
54
60
  }
55
61
 
56
- untriggered(productIds, type) {
57
- return productIds.filter(productId => this.get(productId, type) !== TRIGGERED);
62
+ _getKey(item) {
63
+ // TODO: ad-hoc! should move away to higher level
64
+ return item.product_id || item.text || item.id || item;
58
65
  }
59
66
 
60
- }
67
+ _setOne(item, type, status) {
68
+ computeIfAbsent(this._states, this._getKey(item), () => ({ ...UNTRACKED_STATES }))[type] = status;
69
+ }
61
70
 
62
- function getKey(value) {
63
- // TODO: ad-hoc!
64
- return value.product_id || value.text || value;
65
71
  }
@@ -0,0 +1,69 @@
1
+ import { findInAncestors, trimObj } from '@miso.ai/commons';
2
+
3
+ const DEFAULT_TRACKER_OPTIONS = Object.freeze({
4
+ impression: Object.freeze({
5
+ }),
6
+ viewable: Object.freeze({
7
+ area: 0.5,
8
+ duration: 1000,
9
+ }),
10
+ click: Object.freeze({
11
+ lenient: false,
12
+ }),
13
+ watch: false,
14
+ });
15
+
16
+ function mergeOptions(def, opt) {
17
+ // if opt is falsy then return false, merge otherwise
18
+ return !!opt && { ...def, ...opt };
19
+ }
20
+
21
+ export function normalizeTrackerOptions(options) {
22
+ if (options === false) {
23
+ return false; // turn off all tracking
24
+ }
25
+ if (options === true) {
26
+ options = {};
27
+ }
28
+ if (typeof options !== 'object') {
29
+ throw new Error(`Invalid options: ${options}`);
30
+ }
31
+ const { impression = {}, viewable = {}, click = {}, ...rest } = options;
32
+ return trimObj({
33
+ ...DEFAULT_TRACKER_OPTIONS,
34
+ ...rest,
35
+ impression: mergeOptions(DEFAULT_TRACKER_OPTIONS.impression, impression),
36
+ viewable: mergeOptions(DEFAULT_TRACKER_OPTIONS.viewable, viewable),
37
+ click: mergeOptions(DEFAULT_TRACKER_OPTIONS.click, click),
38
+ });
39
+ }
40
+
41
+ export function validateClick(options = {}, event) {
42
+ if (!options) {
43
+ return false;
44
+ }
45
+
46
+ if (typeof options.validate === 'function') {
47
+ return options.validate(event);
48
+ }
49
+
50
+ if (options.lenient) {
51
+ return true;
52
+ }
53
+
54
+ // standard criteria
55
+ // 1. it must be a left click
56
+ if (event.button !== 0) {
57
+ return false;
58
+ }
59
+ // 2. event default must not be prevented
60
+ if (event.defaultPrevented) {
61
+ return false;
62
+ }
63
+ // 3. must go through a real link
64
+ if (!findInAncestors(event.target, element => (element.tagName === 'A' && element.href) || undefined)) {
65
+ return false;
66
+ }
67
+
68
+ return true;
69
+ }
@@ -0,0 +1,47 @@
1
+ import { viewable as whenViewable } from '@miso.ai/commons';
2
+
3
+ export default class Viewables {
4
+
5
+ constructor() {
6
+ this._entries = new WeakMap();
7
+ }
8
+
9
+ isTracked(element) {
10
+ return this._entries.has(element);
11
+ }
12
+
13
+ async track(element, options = {}) {
14
+ if (!options) {
15
+ return false;
16
+ }
17
+ const entry = this._entries.get(element);
18
+ if (entry) {
19
+ return false;
20
+ }
21
+ const ac = new AbortController();
22
+ const { signal } = ac;
23
+ this._entries.set(element, { ac });
24
+ let result = false;
25
+ try {
26
+ await whenViewable(element, { ...options, signal });
27
+ result = true;
28
+ } catch (e) {
29
+ if (e.name !== 'AbortError') {
30
+ throw e;
31
+ }
32
+ } finally {
33
+ this._entries.delete(element);
34
+ }
35
+ return result;
36
+ }
37
+
38
+ untrack(element) {
39
+ const entry = this._entries.get(element);
40
+ if (!entry) {
41
+ return;
42
+ }
43
+ entry.ac.abort();
44
+ this._entries.delete(element);
45
+ }
46
+
47
+ }
package/src/version.js CHANGED
@@ -1 +1 @@
1
- export default '1.10.0-beta.1';
1
+ export default '1.10.0-beta.10';