@mparticle/web-sdk 3.28.0 → 3.29.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.
package/dist/mparticle.js CHANGED
@@ -204,7 +204,7 @@ var mParticle = (function () {
204
204
  Base64: Base64$1
205
205
  };
206
206
 
207
- var version = "3.28.0";
207
+ var version = "3.29.0";
208
208
 
209
209
  var Constants = {
210
210
  sdkVersion: version,
@@ -12587,12 +12587,14 @@ var mParticle = (function () {
12587
12587
  function textOf(element) {
12588
12588
  return element && !isUnsafeTextSource(element) && isShown(element) ? renderedText(element) : '';
12589
12589
  }
12590
- // The control itself is already known to be shown.
12590
+ // The control itself is already known to be shown. An inline icon names
12591
+ // itself with aria-label rather than alt.
12591
12592
  function shownImageAlt(control) {
12592
- var images = control.querySelectorAll('img[alt]');
12593
+ var images = control.querySelectorAll('img[alt], svg[aria-label], [role="img"][aria-label]');
12593
12594
  for (var i = 0; i < images.length && i < MAX_READS; i++) {
12594
- if (isShown(images[i], control) && isVisible(images[i])) {
12595
- return nonBlank(images[i].getAttribute('alt'));
12595
+ var alt = nonBlank(images[i].getAttribute('alt') || images[i].getAttribute('aria-label'));
12596
+ if (alt && isShown(images[i], control) && isVisible(images[i])) {
12597
+ return alt;
12596
12598
  }
12597
12599
  }
12598
12600
  return undefined;
@@ -13002,7 +13004,7 @@ var mParticle = (function () {
13002
13004
  // aria-label, aria-labelledby or legend, masked like a label and cut at its
13003
13005
  // first colon, since a legend can repeat the pick ("Color: Navy"). Never the
13004
13006
  // option's own text, which is the shopper's answer.
13005
- var NAMED_GROUP = '[role="radiogroup"], [role="listbox"], [role="group"], fieldset';
13007
+ var NAMED_GROUP = '[role="radiogroup"], [role="listbox"], [role="group"], fieldset, ' + '[role="region"][aria-label], [role="region"][aria-labelledby]';
13006
13008
  // A control such as the button that opens a list box shows the option picked.
13007
13009
  var NOT_A_QUESTION = CHOICE + ', ' + CONTROL_SELECTOR + ', a[href], input, select, textarea, slot, [aria-haspopup], [role~="button"], ' + '[role~="link"], [role~="tab"], [role~="combobox"]';
13008
13010
  var OPTION_KINDS = ['radio', 'menuitemradio', 'checkbox', 'menuitemcheckbox', 'switch', 'option'];
@@ -13027,6 +13029,95 @@ var mParticle = (function () {
13027
13029
  var name = choice.getAttribute('name');
13028
13030
  return option instanceof HTMLInputElement && option.type === choice.type && !!name && option.getAttribute('name') === name && option.form === choice.form;
13029
13031
  }
13032
+ function isTransparent(element, until) {
13033
+ return someComposedAncestor(element, function (node) {
13034
+ return window.getComputedStyle(node).opacity === '0';
13035
+ }, until);
13036
+ }
13037
+ // Content that is never painted where it sits: SVG that is only referenced or
13038
+ // read, and a canvas's fallback, which some browsers still give boxes.
13039
+ var UNDRAWN = 'svg title, svg desc, svg metadata, svg defs, svg symbol, svg clipPath, ' + 'svg mask, svg pattern, svg marker, svg linearGradient, ' + 'svg radialGradient, svg filter, canvas';
13040
+ // Whether text sits in any closed details element outside its summary.
13041
+ function isInClosedDetails(text, parent) {
13042
+ var details = parent.closest('details:not([open])');
13043
+ while (details) {
13044
+ var summary = Array.from(details.children).filter(function (child) {
13045
+ return child.tagName === 'SUMMARY';
13046
+ })[0];
13047
+ if (!summary || !summary.contains(text)) {
13048
+ return true;
13049
+ }
13050
+ details = details.parentElement ? details.parentElement.closest('details:not([open])') : null;
13051
+ }
13052
+ return false;
13053
+ }
13054
+ // Whether the browser shows an element through every ancestor it renders
13055
+ // under, shadow trees included, which a walk of light ancestors can't see
13056
+ // into when a shadow root is closed. Where it can't say (checkVisibility
13057
+ // arrived in Chrome 105, Firefox 125 and Safari 17.4), the element counts as
13058
+ // not shown, so option rows name no group in older browsers.
13059
+ function isComposedVisible(element) {
13060
+ var check = element.checkVisibility;
13061
+ return isFunction(check) &&
13062
+ // Older Chrome versions read the first two names, newer browsers the rest.
13063
+ check.call(element, {
13064
+ checkOpacity: true,
13065
+ checkVisibilityCSS: true,
13066
+ opacityProperty: true,
13067
+ visibilityProperty: true
13068
+ });
13069
+ }
13070
+ // Whether the browser lays a text node out in a box of its own. Text it never
13071
+ // draws, such as an SVG definition, canvas fallback, closed details or an
13072
+ // unslotted child of a shadow host, has none. Where boxes can't be read, the
13073
+ // text counts as not drawn.
13074
+ function isDrawn(text) {
13075
+ var range = isFunction(document.createRange) ? document.createRange() : null;
13076
+ if (!range || !isFunction(range.getClientRects)) {
13077
+ return false;
13078
+ }
13079
+ range.selectNodeContents(text);
13080
+ return Array.from(range.getClientRects()).some(function (box) {
13081
+ return box.width > 0 && box.height > 0;
13082
+ });
13083
+ }
13084
+ // Whether a row shows text of its own: a drawn, shown, non-transparent text
13085
+ // node, outside any form field, editable area or opted-out part, beside
13086
+ // nothing but the row's own input. Not a row with a shadow root, slot or
13087
+ // custom element, whose rendered text a walk of its light children can't see.
13088
+ function showsText(row, own) {
13089
+ if (isUnsafeTextSource(row, own) || hasComposedAncestor(own, '[data-mp-no-track]') || row.shadowRoot || row.tagName.indexOf('-') !== -1 || row.querySelector('slot')) {
13090
+ return false;
13091
+ }
13092
+ // A row too large to check in full counts as showing nothing.
13093
+ var descendants = row.querySelectorAll('*');
13094
+ if (descendants.length > MAX_READS) {
13095
+ return false;
13096
+ }
13097
+ for (var i = 0; i < descendants.length; i++) {
13098
+ if (descendants[i].shadowRoot || descendants[i].tagName.indexOf('-') !== -1) {
13099
+ return false;
13100
+ }
13101
+ }
13102
+ var walker = document.createTreeWalker(row, NodeFilter.SHOW_TEXT);
13103
+ for (var i = 0; i < MAX_READS && walker.nextNode(); i++) {
13104
+ var parent_1 = walker.currentNode.parentElement;
13105
+ if (parent_1 && nonBlank(walker.currentNode.nodeValue) &&
13106
+ // Undrawn content, and the body of a closed details element,
13107
+ // which browsers still give layout boxes.
13108
+ !parent_1.closest(UNDRAWN) && !isInClosedDetails(walker.currentNode, parent_1) && isShown(parent_1, row) && isVisible(parent_1) && !isTransparent(parent_1, row) && isComposedVisible(parent_1) && isDrawn(walker.currentNode)) {
13109
+ return true;
13110
+ }
13111
+ }
13112
+ return false;
13113
+ }
13114
+ // A native input inside a shown control of its own, such as a menu option row
13115
+ // with a styled radio, which shows text of its own and doesn't also hold the
13116
+ // option picked.
13117
+ function showsRow(option, choice, group) {
13118
+ var row = option instanceof HTMLInputElement && option.parentElement ? option.parentElement.closest(CONTROL_SELECTOR) : null;
13119
+ return !!row && group.contains(row) && !row.contains(choice) && isShown(row, group) && isVisible(row) && !isTransparent(row, group) && showsText(row, option);
13120
+ }
13030
13121
  // Whether a group offers another answer to the question picked: a shown option
13031
13122
  // to the same question that isn't part of the one picked. A card around one
13032
13123
  // address radio and a "make default" checkbox, or around a role radio and the
@@ -13035,12 +13126,11 @@ var mParticle = (function () {
13035
13126
  var options = group.querySelectorAll(CHOICE);
13036
13127
  for (var i = 0; i < options.length; i++) {
13037
13128
  var option = options[i];
13038
- if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) &&
13129
+ if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) && (
13039
13130
  // A fully transparent option, as a native input behind a styled
13040
- // one often is, shows the shopper no second answer.
13041
- !someComposedAncestor(option, function (node) {
13042
- return window.getComputedStyle(node).opacity === '0';
13043
- }, group)) {
13131
+ // one often is, shows the shopper no second answer, unless a
13132
+ // shown row around it does.
13133
+ !isTransparent(option, group) || showsRow(option, choice, group))) {
13044
13134
  return true;
13045
13135
  }
13046
13136
  }
@@ -13058,27 +13148,30 @@ var mParticle = (function () {
13058
13148
  });
13059
13149
  return labels;
13060
13150
  }
13061
- // The group asking the question a pick answers. A role radio's is its
13062
- // radiogroup and an option's its list box; a role toggle, such as a switch,
13063
- // stands alone. A native box's is the nearest group offering other answers,
13064
- // since a group around one option, such as a saved address card, can be
13065
- // named by that option.
13066
- function questionGroup(choice) {
13151
+ // The name of the group asking the question a pick answers. A role radio's
13152
+ // group is its radiogroup and an option's its list box; a role toggle, such as
13153
+ // a switch, stands alone. A native box's is the nearest group offering other
13154
+ // answers, since a group around one option, such as a saved address card, can
13155
+ // be named by that option. A region with no usable name is passed over, so a
13156
+ // group around it can still name the pick. Each group's name is read once.
13157
+ function groupName(choice) {
13067
13158
  if (!(choice instanceof HTMLInputElement)) {
13068
13159
  var container = choice.matches('[role~="option"]') ? choice.closest('[role="listbox"]') : choice.matches('[role~="radio"]') ? choice.closest('[role="radiogroup"]') : null;
13069
- return container && holdsOptions(container, choice) ? container : null;
13160
+ return container && holdsOptions(container, choice) ? nameOf(container) : undefined;
13070
13161
  }
13071
13162
  var group = choice.closest(NAMED_GROUP);
13072
- while (group && !holdsOptions(group, choice)) {
13163
+ while (group) {
13164
+ if (holdsOptions(group, choice)) {
13165
+ var name_2 = nameOf(group);
13166
+ if (name_2 || !group.matches('[role="region"]')) {
13167
+ return name_2;
13168
+ }
13169
+ }
13073
13170
  group = group.parentElement ? group.parentElement.closest(NAMED_GROUP) : null;
13074
13171
  }
13075
- return group;
13172
+ return undefined;
13076
13173
  }
13077
- function groupName(choice) {
13078
- var group = questionGroup(choice);
13079
- if (!group) {
13080
- return undefined;
13081
- }
13174
+ function nameOf(group) {
13082
13175
  var raw = nonBlank(group.getAttribute('aria-label'));
13083
13176
  if (!raw) {
13084
13177
  var ids = group.getAttribute('aria-labelledby');
@@ -13086,7 +13179,11 @@ var mParticle = (function () {
13086
13179
  return child.tagName === 'LEGEND';
13087
13180
  })[0] : undefined;
13088
13181
  var root_2 = isFunction(group.getRootNode) ? group.getRootNode() : document;
13089
- var source_1 = ids ? root_2.getElementById ? root_2.getElementById(ids.trim().split(/\s+/)[0]) : null : legend || null;
13182
+ var target = ids ? root_2.getElementById ? root_2.getElementById(ids.trim().split(/\s+/)[0]) : null : legend || null;
13183
+ // A region opened by an accordion header outside a tab list is named
13184
+ // by the header's first heading, never the rest of what it shows.
13185
+ var header_1 = target && group.matches('[role="region"]') && (target.getAttribute('role') || '').trim() === 'tab' && !hasComposedAncestor(target, '[role~="tablist"]') && !target.querySelector(NOT_A_QUESTION) ? target : null;
13186
+ var source_1 = header_1 ? header_1.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]') : target;
13090
13187
  // A label that is, holds or sits in an option or a control, or in an
13091
13188
  // option's label, would name the answer.
13092
13189
  var holder = source_1 ? source_1.closest('label') : null;
@@ -13097,7 +13194,9 @@ var mParticle = (function () {
13097
13194
  return source_1.contains(label) || label.contains(source_1);
13098
13195
  });
13099
13196
  });
13100
- raw = source_1 && !labelsOption && !hasComposedAncestor(source_1, NOT_A_QUESTION) && !source_1.querySelector(NOT_A_QUESTION) && !(holder && (holder.control || holder.querySelector(CHOICE))) ? textOf(source_1) : undefined;
13197
+ raw = source_1 && !labelsOption && !someComposedAncestor(source_1, function (node) {
13198
+ return node !== header_1 && node.matches(NOT_A_QUESTION);
13199
+ }) && !source_1.querySelector(NOT_A_QUESTION) && !(holder && (holder.control || holder.querySelector(CHOICE))) ? textOf(source_1) : undefined;
13101
13200
  }
13102
13201
  return raw ? maskLabel(raw.split(':')[0]) : undefined;
13103
13202
  }
@@ -13115,9 +13214,9 @@ var mParticle = (function () {
13115
13214
  return PAYMENT_QUESTION.test(nameWords(words));
13116
13215
  });
13117
13216
  }
13118
- // What names a payment option: the radio's accessible name, the text and
13119
- // image of each label, and its hand-written ids, text masked like a label.
13120
- function optionNames(radio) {
13217
+ // What names a payment option to shoppers: the radio's accessible name and the
13218
+ // text and image of each label, masked like a label.
13219
+ function shownOptionNames(radio) {
13121
13220
  var names = [maskLabel(accessibleName(radio).raw)];
13122
13221
  var labels = radio instanceof HTMLInputElement && radio.labels ? Array.from(radio.labels).slice(0, 2) : [];
13123
13222
  labels.forEach(function (label) {
@@ -13126,11 +13225,18 @@ var mParticle = (function () {
13126
13225
  names.push(maskLabel(shownImageAlt(label)));
13127
13226
  }
13128
13227
  });
13129
- names.push(stableIdentifier(radio.getAttribute('id')), firstIdentifier(radio, TEST_ID_ATTRIBUTES));
13228
+ return names;
13229
+ }
13230
+ function distinctNames(names) {
13130
13231
  return names.filter(function (name, index) {
13131
13232
  return !!name && names.indexOf(name) === index;
13132
13233
  });
13133
13234
  }
13235
+ // What names a payment option: the names shoppers read, then its hand-written
13236
+ // ids.
13237
+ function optionNames(radio) {
13238
+ return distinctNames(shownOptionNames(radio).concat(stableIdentifier(radio.getAttribute('id')), firstIdentifier(radio, TEST_ID_ATTRIBUTES)));
13239
+ }
13134
13240
  function describePaymentChoice(control, path) {
13135
13241
  if (path === void 0) {
13136
13242
  path = [];
@@ -13145,6 +13251,39 @@ var mParticle = (function () {
13145
13251
  names: names
13146
13252
  } : undefined;
13147
13253
  }
13254
+ var CHECKED_RADIO = 'input[type="radio"]:checked, [role~="radio"][aria-checked="true"], [role~="menuitemradio"][aria-checked="true"]';
13255
+ // The payment radio the page holds checked, such as a card option checked by
13256
+ // default that the shopper never clicked. A page holding more than one option
13257
+ // names no single method, so it describes nothing. One option drawn twice
13258
+ // counts once: a role radio wrapping its native input, a hidden input a
13259
+ // component library keeps beside its role radio, or one group rendered for two
13260
+ // layouts, whose copies share the names shoppers read but not their ids. An
13261
+ // option shoppers see no name for is told apart by its ids.
13262
+ function describeCheckedPaymentChoice() {
13263
+ var radios = Array.from(document.querySelectorAll(CHECKED_RADIO)).filter(function (radio) {
13264
+ return isPicked(radio) && radio.getAttribute('aria-hidden') !== 'true' && !hasComposedAncestor(radio, '[data-mp-no-track]') && isPaymentQuestion(radio);
13265
+ });
13266
+ var choices = radios.filter(function (radio) {
13267
+ return !radios.some(function (other) {
13268
+ return other !== radio && radio.contains(other);
13269
+ });
13270
+ }).map(function (radio) {
13271
+ var shown = distinctNames(shownOptionNames(radio));
13272
+ var names = optionNames(radio);
13273
+ return {
13274
+ radio: radio,
13275
+ names: names,
13276
+ option: (shown.length ? shown : names).join('\n')
13277
+ };
13278
+ });
13279
+ var options = new Set(choices.map(function (choice) {
13280
+ return choice.option;
13281
+ }));
13282
+ return options.size === 1 && choices[0].names.length ? {
13283
+ radio: choices[0].radio,
13284
+ names: choices[0].names
13285
+ } : undefined;
13286
+ }
13148
13287
  // Read once the page has handled the click, which may have cancelled it.
13149
13288
  function isPicked(radio) {
13150
13289
  return radio instanceof HTMLInputElement ? radio.checked : radio.getAttribute('aria-checked') === 'true';
@@ -13191,6 +13330,9 @@ var mParticle = (function () {
13191
13330
  var walletPhrases = new RegExp('^(?:(?:' + WALLET_VERBS + ')(?: now| securely)? (?:with|using|via|through|by) (?:' + WALLET_NAMES + ')(?: express(?: checkout)?| checkout)?|(?:continue|proceed|go) (?:to|with) (?:' + WALLET_NAMES + '))$');
13192
13331
  // A wallet's own name on a button: "PayPal", "Klarna checkout", "Apple Pay logo".
13193
13332
  var walletBrands = new RegExp('^(?:' + WALLET_NAMES + ')(?: (?:checkout|check out|express(?: checkout)?|express pay|buy now|pay now|pay|now)' + '| (?:logo|icon|mark|button)(?: (?:logo|icon|mark|button))*)?$');
13333
+ // The wallets the Rokt kit reads from a click's label or identifiers, as words:
13334
+ // "Place order with PayPal!" and "paypal-pay-button" both name PayPal.
13335
+ var KIT_WALLET_WORDS = /(?:^| )(?:apple ?pay|google ?pay|g ?pay|android ?pay|venmo|pay ?pal|amazon ?pay|shop ?pay|klarna|after ?pay|clear ?pay|affirm|cash ?app|sezzle|samsung ?pay)(?= |$)/;
13194
13336
  // Exact HTTPS hosts of hosted wallet button frames.
13195
13337
  var walletFrameFamilies = {
13196
13338
  'pay.google.com': 'google_pay',
@@ -13394,6 +13536,24 @@ var mParticle = (function () {
13394
13536
  }
13395
13537
  return cart;
13396
13538
  }
13539
+ // The Rokt kit's own word split, so "PayPal!" and "paypal2" both read as
13540
+ // "paypal" here as they do there.
13541
+ function kitWords(value) {
13542
+ return (value || '').replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().replace(/[^a-z]+/g, ' ').trim();
13543
+ }
13544
+ // A confirm control may name its wallet, so "Place order with PayPal" and a
13545
+ // "Place order" button with id paypal-pay-button are confirm clicks that still
13546
+ // name their own method. A logged click is judged on the fields the kit reads
13547
+ // a method from; a click that isn't logged, on the control's own label and
13548
+ // identifiers.
13549
+ function namesWallet(control, click) {
13550
+ var values = click ? [click.label, click.ariaLabel, click.elementId, click.elementName, click.testId] : labelSources(control).concat(['id', 'name'].concat(TEST_ID_ATTRIBUTES).map(function (attribute) {
13551
+ return control.getAttribute(attribute);
13552
+ }));
13553
+ return values.some(function (value) {
13554
+ return KIT_WALLET_WORDS.test(kitWords(value));
13555
+ });
13556
+ }
13397
13557
  // The host is the domain or one of its subdomains, never a look-alike suffix.
13398
13558
  function isOnDomain(host, domain) {
13399
13559
  return host === domain || host.length > domain.length && host.slice(host.length - domain.length - 1) === '.' + domain;
@@ -13768,6 +13928,7 @@ var mParticle = (function () {
13768
13928
  var source;
13769
13929
  var adds;
13770
13930
  var choice;
13931
+ var checkedNames;
13771
13932
  var family;
13772
13933
  var unconfirmedCommit = false;
13773
13934
  if (layer === 'click') {
@@ -13800,6 +13961,15 @@ var mParticle = (function () {
13800
13961
  source = intent ? intent.source : undefined;
13801
13962
  adds = intent ? intent.adds : undefined;
13802
13963
  choice = signalsKit ? describePaymentChoice(control, path) : undefined;
13964
+ // A confirm or checkout-step click picks no radio itself, so it
13965
+ // carries the one the page holds checked as read here. It is not
13966
+ // read again at delivery: the page's own handlers may clear or
13967
+ // replace it by then. A wallet tap names its own method, and
13968
+ // so does a confirm that names a wallet.
13969
+ if (!choice && (kind === 'commit' || kind === 'checkout_step') && !namesWallet(control, click)) {
13970
+ var checked = describeCheckedPaymentChoice();
13971
+ checkedNames = checked ? checked.names : undefined;
13972
+ }
13803
13973
  }
13804
13974
  this.lastClick = {
13805
13975
  t: t,
@@ -13854,6 +14024,8 @@ var mParticle = (function () {
13854
14024
  // with this pick.
13855
14025
  if (choice && isPicked(choice.radio)) {
13856
14026
  _this.pickPayment(t, pathname, choice.names);
14027
+ } else if (checkedNames) {
14028
+ _this.pickPayment(t, pathname, checkedNames);
13857
14029
  } else if (family) {
13858
14030
  _this.pickPayment(t, pathname, undefined, family);
13859
14031
  }
@@ -20,6 +20,7 @@ export interface IButtonClick {
20
20
  fileExtension?: string;
21
21
  checked?: boolean;
22
22
  }
23
+ export declare const TEST_ID_ATTRIBUTES: string[];
23
24
  export declare function isLinkClick(click: IButtonClick): boolean;
24
25
  export declare function describeButtonClick(control: Element, path?: Array<EventTarget | null>): IButtonClick | undefined;
25
26
  export interface IPaymentChoice {
@@ -27,4 +28,5 @@ export interface IPaymentChoice {
27
28
  names: string[];
28
29
  }
29
30
  export declare function describePaymentChoice(control: Element, path?: Array<EventTarget | null>): IPaymentChoice | undefined;
31
+ export declare function describeCheckedPaymentChoice(): IPaymentChoice | undefined;
30
32
  export declare function isPicked(radio: Element): boolean;
@@ -1,3 +1,4 @@
1
+ import { IButtonClick } from './buttonDescription';
1
2
  export type CheckoutIntentKind = 'commit' | 'wallet' | 'checkout_step' | 'frame' | 'cart';
2
3
  export type PaymentFrameFamily = 'apple_pay' | 'google_pay' | 'amazon_pay' | 'shop_pay' | 'card';
3
4
  export type CheckoutIntentSource = 'vendor' | 'label' | 'brand' | 'frame' | 'submit' | 'name' | 'api';
@@ -7,6 +8,7 @@ export interface ICheckoutIntent {
7
8
  adds?: true;
8
9
  }
9
10
  export declare function classifyIntent(control: Element, pathname: string): ICheckoutIntent | undefined;
11
+ export declare function namesWallet(control: Element, click?: IButtonClick): boolean;
10
12
  export declare function classifyIntentControl(control: Element, pathname: string): Exclude<CheckoutIntentKind, 'frame'> | undefined;
11
13
  export interface IPaymentFrame {
12
14
  kind: 'wallet' | 'frame';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mparticle/web-sdk",
3
- "version": "3.28.0",
3
+ "version": "3.29.0",
4
4
  "description": "mParticle core SDK for web applications",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [