@mparticle/web-sdk 3.29.0 → 3.29.1
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/README.md +63 -32
- package/dist/mparticle.common.js +1 -1
- package/dist/mparticle.esm.js +1 -1
- package/dist/mparticle.js +129 -32
- package/dist/types/src/buttonTracking/buttonTracker.d.ts +1 -0
- package/dist/types/src/roktManager.d.ts +3 -0
- package/package.json +1 -1
- package/src/buttonTracking/buttonDescription.ts +163 -41
- package/src/buttonTracking/buttonTracker.ts +31 -7
- package/src/mp-instance.ts +8 -4
- package/src/roktManager.ts +13 -0
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.29.
|
|
207
|
+
var version = "3.29.1";
|
|
208
208
|
|
|
209
209
|
var Constants = {
|
|
210
210
|
sdkVersion: version,
|
|
@@ -11209,6 +11209,9 @@ var mParticle = (function () {
|
|
|
11209
11209
|
this.pendingCheckoutIntents = [];
|
|
11210
11210
|
// Off until SDK init finds the Rokt connection receives auto button clicks.
|
|
11211
11211
|
this.autoButtonClicksForwarded = false;
|
|
11212
|
+
// Off until SDK init finds the Rokt connection receives auto link clicks, or
|
|
11213
|
+
// link tracking is off. Only link clicks lose their intent without it.
|
|
11214
|
+
this.autoLinkClicksForwarded = false;
|
|
11212
11215
|
}
|
|
11213
11216
|
/**
|
|
11214
11217
|
* Sets a callback to be invoked when RoktManager becomes ready
|
|
@@ -11338,6 +11341,14 @@ var mParticle = (function () {
|
|
|
11338
11341
|
RoktManager.prototype.setAutoButtonClicksForwarded = function (forwarded) {
|
|
11339
11342
|
this.autoButtonClicksForwarded = forwarded;
|
|
11340
11343
|
};
|
|
11344
|
+
RoktManager.prototype.setAutoLinkClicksForwarded = function (forwarded) {
|
|
11345
|
+
this.autoLinkClicksForwarded = forwarded;
|
|
11346
|
+
};
|
|
11347
|
+
// The Rokt kit reads this too: while false, its page views keep
|
|
11348
|
+
// dispatchDelayMs, since a checkout link could never release the wait.
|
|
11349
|
+
RoktManager.prototype.areAutoLinkClicksForwarded = function () {
|
|
11350
|
+
return this.autoLinkClicksForwarded;
|
|
11351
|
+
};
|
|
11341
11352
|
// noTargeting (which noDeviceId implies), doNotShareOrSell or Global Privacy
|
|
11342
11353
|
// Control. Unlike noFunctional, these also stop auto button and link clicks
|
|
11343
11354
|
// from being logged.
|
|
@@ -12472,6 +12483,9 @@ var mParticle = (function () {
|
|
|
12472
12483
|
}
|
|
12473
12484
|
|
|
12474
12485
|
var DOWNLOAD_EXTENSIONS = /\.(pdf|zip|csv|txt|ics|docx?|xlsx?|pptx?|dmg|exe|apk|mp3|mp4)$/i;
|
|
12486
|
+
// A link run by script reads as a link, as an anchor does, so whoever reads
|
|
12487
|
+
// the click treats it the same.
|
|
12488
|
+
var LINK_CONTROL = 'a[href]:not([role="button"]), [role~="link"]:not([role~="button"])';
|
|
12475
12489
|
function elementKind(control) {
|
|
12476
12490
|
if (isWalletElement(control)) {
|
|
12477
12491
|
return 'wallet';
|
|
@@ -12480,9 +12494,7 @@ var mParticle = (function () {
|
|
|
12480
12494
|
if (tag === 'button' || tag === 'input') {
|
|
12481
12495
|
return tag;
|
|
12482
12496
|
}
|
|
12483
|
-
|
|
12484
|
-
// the click treats it the same.
|
|
12485
|
-
return control.matches('a[href]:not([role="button"]), [role~="link"]:not([role~="button"])') ? 'link' : 'role';
|
|
12497
|
+
return control.matches(LINK_CONTROL) ? 'link' : 'role';
|
|
12486
12498
|
}
|
|
12487
12499
|
var NOT_RENDERED = 'script, style, template, [hidden], [aria-hidden="true"], [inert]';
|
|
12488
12500
|
// The most nodes read for one label source, and images looked at for one
|
|
@@ -12773,11 +12785,28 @@ var mParticle = (function () {
|
|
|
12773
12785
|
var digits = id.replace(/[^0-9]/g, '').length;
|
|
12774
12786
|
return id.length <= 64 && digits <= 3 && HAND_WRITTEN_ID.test(id) ? id : undefined;
|
|
12775
12787
|
}
|
|
12788
|
+
// An id React's useId generates, in any of the forms React has used: `:r1:`,
|
|
12789
|
+
// `\u00ABr1\u00BB` and `_r_1_`. On the client it is r and a short base-32 counter; on
|
|
12790
|
+
// the server, R and a base-32 tree id, where a component's later ids add an H
|
|
12791
|
+
// and a counter (`:R1H1:`). A word that only starts with r, such as
|
|
12792
|
+
// `:region:`, is not one.
|
|
12793
|
+
var REACT_ID = /:(?:r[0-9a-v]{1,4}|R[0-9a-v]+(?:H[0-9a-v]+)?):|\u00ab(?:r[0-9a-v]{1,4}|R[0-9a-v]+(?:H[0-9a-v]+)?)\u00bb|_(?:r_[0-9a-v]{1,4}|R_[0-9a-v]+(?:H[0-9a-v]+)?)_/g;
|
|
12794
|
+
// What a UI library puts before the React id it names an unnamed group with
|
|
12795
|
+
// (`radix-:r1:`, `react-aria-:R1:`). Left on its own once that id is removed,
|
|
12796
|
+
// it names the library, not the question.
|
|
12797
|
+
var LIBRARY_ID_PREFIX = /^(?:radix|react-aria|headlessui(?:-[a-z]+)*|mui|chakra|reach|ark|mantine|base-ui)$/i;
|
|
12776
12798
|
// A picker's input name, which names its question. Sites often end it with the
|
|
12777
|
-
// id of the product it belongs to (`color-swatches-48213907`)
|
|
12778
|
-
//
|
|
12799
|
+
// id of the product it belongs to (`color-swatches-48213907`), or build it
|
|
12800
|
+
// with a React id (`variant_group__R_1mn59b_`); without either it still names
|
|
12801
|
+
// the question.
|
|
12779
12802
|
function questionName(value) {
|
|
12780
|
-
|
|
12803
|
+
if (!value) {
|
|
12804
|
+
return undefined;
|
|
12805
|
+
}
|
|
12806
|
+
var trimmed = value.trim();
|
|
12807
|
+
var withoutReactId = trimmed.replace(REACT_ID, '-');
|
|
12808
|
+
var name = stableIdentifier(withoutReactId.replace(/([-_])[-_]+/g, '$1').replace(/^[-_]+|[-_]+$/g, '').replace(/[-_][0-9]{4,}$/, ''));
|
|
12809
|
+
return name && withoutReactId !== trimmed && LIBRARY_ID_PREFIX.test(name) ? undefined : name;
|
|
12781
12810
|
}
|
|
12782
12811
|
var TEST_ID_ATTRIBUTES = ['data-testid', 'data-test-id', 'data-test', 'data-qa', 'data-cy'];
|
|
12783
12812
|
// The first of these attributes that holds a developer-written identifier.
|
|
@@ -12798,10 +12827,13 @@ var mParticle = (function () {
|
|
|
12798
12827
|
return stableIdentifier(ariaControls ? ariaControls.trim().split(/\s+/)[0] : null) || firstIdentifier(control, ['popovertarget', 'commandfor']);
|
|
12799
12828
|
}
|
|
12800
12829
|
// The id of the form a button or input actually submits, which the browser
|
|
12801
|
-
// resolves from its form attribute or its owning form.
|
|
12802
|
-
//
|
|
12830
|
+
// resolves from its form attribute or its owning form. A role control, such as
|
|
12831
|
+
// a styled radio box, belongs to the form around it, read without following a
|
|
12832
|
+
// form attribute or leaving its shadow root, since neither makes it part of
|
|
12833
|
+
// the form; a link, anchor or role, to none. getAttribute, because a field
|
|
12834
|
+
// named "id" shadows form.id.
|
|
12803
12835
|
function formIdentifier(control) {
|
|
12804
|
-
var form = control instanceof HTMLButtonElement || control instanceof HTMLInputElement ? control.form : null;
|
|
12836
|
+
var form = control instanceof HTMLButtonElement || control instanceof HTMLInputElement ? control.form : control.matches(LINK_CONTROL) ? null : control.closest('form');
|
|
12805
12837
|
return form ? stableIdentifier(form.getAttribute('id')) : undefined;
|
|
12806
12838
|
}
|
|
12807
12839
|
// The browser's normalised type: a button defaults to 'submit' and an unknown
|
|
@@ -13118,6 +13150,24 @@ var mParticle = (function () {
|
|
|
13118
13150
|
var row = option instanceof HTMLInputElement && option.parentElement ? option.parentElement.closest(CONTROL_SELECTOR) : null;
|
|
13119
13151
|
return !!row && group.contains(row) && !row.contains(choice) && isShown(row, group) && isVisible(row) && !isTransparent(row, group) && showsText(row, option);
|
|
13120
13152
|
}
|
|
13153
|
+
// Native boxes with no name, which the browser doesn't group, answer one
|
|
13154
|
+
// question only as menu option rows do: each in a shown row of its own, in the
|
|
13155
|
+
// same group. A card around a "make default" checkbox and another box has no
|
|
13156
|
+
// such rows. Rows alone can't tell a question from a card named for a person
|
|
13157
|
+
// around separate settings, so only a group that shows its question and holds
|
|
13158
|
+
// the rows directly counts.
|
|
13159
|
+
function unnamedOptionRows(option, choice, group) {
|
|
13160
|
+
return option instanceof HTMLInputElement && choice instanceof HTMLInputElement && option.type === choice.type && !option.getAttribute('name') && !choice.getAttribute('name') && option.form === choice.form && option.closest(NAMED_GROUP) === group && choice.closest(NAMED_GROUP) === group && showsRow(option, choice, group) && showsRow(choice, option, group) && showsQuestion(group);
|
|
13161
|
+
}
|
|
13162
|
+
// A native input the page hides, styling its label as the option instead, is
|
|
13163
|
+
// shown by that label: one of its own, in the group, that the browser draws
|
|
13164
|
+
// and that shows text of its own, as an option row must. Neither an opted-out
|
|
13165
|
+
// input nor an opted-out label counts.
|
|
13166
|
+
function showsLabel(option, choice, group) {
|
|
13167
|
+
return option instanceof HTMLInputElement && !!option.labels && !hasComposedAncestor(option, '[data-mp-no-track]') && Array.from(option.labels).some(function (label) {
|
|
13168
|
+
return group.contains(label) && !label.contains(choice) && !hasComposedAncestor(label, '[data-mp-no-track]') && isShown(label, group) && isVisible(label) && !isTransparent(label, group) && isComposedVisible(label) && showsText(label, option);
|
|
13169
|
+
});
|
|
13170
|
+
}
|
|
13121
13171
|
// Whether a group offers another answer to the question picked: a shown option
|
|
13122
13172
|
// to the same question that isn't part of the one picked. A card around one
|
|
13123
13173
|
// address radio and a "make default" checkbox, or around a role radio and the
|
|
@@ -13126,11 +13176,11 @@ var mParticle = (function () {
|
|
|
13126
13176
|
var options = group.querySelectorAll(CHOICE);
|
|
13127
13177
|
for (var i = 0; i < options.length; i++) {
|
|
13128
13178
|
var option = options[i];
|
|
13129
|
-
if (!option.contains(choice) && !choice.contains(option) && sameQuestion(option, choice) && isShown(option, group) && isVisible(option) && (
|
|
13130
|
-
// A fully transparent option, as a native input behind a
|
|
13131
|
-
// one often is, shows the shopper no second answer,
|
|
13132
|
-
// shown row around it does.
|
|
13133
|
-
!isTransparent(option, group) || showsRow(option, choice, group))) {
|
|
13179
|
+
if (!option.contains(choice) && !choice.contains(option) && (sameQuestion(option, choice) || unnamedOptionRows(option, choice, group)) && (isShown(option, group) && isVisible(option) && (
|
|
13180
|
+
// A fully transparent option, as a native input behind a
|
|
13181
|
+
// styled one often is, shows the shopper no second answer,
|
|
13182
|
+
// unless a shown row around it does.
|
|
13183
|
+
!isTransparent(option, group) || showsRow(option, choice, group)) || showsLabel(option, choice, group))) {
|
|
13134
13184
|
return true;
|
|
13135
13185
|
}
|
|
13136
13186
|
}
|
|
@@ -13171,18 +13221,48 @@ var mParticle = (function () {
|
|
|
13171
13221
|
}
|
|
13172
13222
|
return undefined;
|
|
13173
13223
|
}
|
|
13224
|
+
// Whether what names a group is shown as its question: a fieldset's own
|
|
13225
|
+
// legend, or the header that opens a section. nameOf reads an aria-label first,
|
|
13226
|
+
// so a group carrying one is named by that instead.
|
|
13227
|
+
function showsQuestion(group) {
|
|
13228
|
+
if (group.tagName === 'FIELDSET') {
|
|
13229
|
+
return namedByLegend(group);
|
|
13230
|
+
}
|
|
13231
|
+
return !nonBlank(group.getAttribute('aria-label')) && opensRegion(group, labelledBy(group));
|
|
13232
|
+
}
|
|
13233
|
+
// A fieldset's own legend, a direct child, as the browser reads it.
|
|
13234
|
+
function legendOf(group) {
|
|
13235
|
+
return group.tagName === 'FIELDSET' ? Array.from(group.children).filter(function (child) {
|
|
13236
|
+
return child.tagName === 'LEGEND';
|
|
13237
|
+
})[0] : undefined;
|
|
13238
|
+
}
|
|
13239
|
+
// Whether a fieldset is named by its own legend, which nameOf reads only when
|
|
13240
|
+
// no aria-label overrides it and any aria-labelledby points at that legend.
|
|
13241
|
+
function namedByLegend(group) {
|
|
13242
|
+
var legend = legendOf(group);
|
|
13243
|
+
return !!legend && !nonBlank(group.getAttribute('aria-label')) && (!group.getAttribute('aria-labelledby') || labelledBy(group) === legend);
|
|
13244
|
+
}
|
|
13245
|
+
// The first element a group's aria-labelledby points at.
|
|
13246
|
+
function labelledBy(group) {
|
|
13247
|
+
var ids = group.getAttribute('aria-labelledby');
|
|
13248
|
+
var root = isFunction(group.getRootNode) ? group.getRootNode() : document;
|
|
13249
|
+
return ids && root.getElementById ? root.getElementById(ids.trim().split(/\s+/)[0]) : null;
|
|
13250
|
+
}
|
|
13251
|
+
// Whether a region is opened by this accordion header: a tab outside any tab
|
|
13252
|
+
// list, holding no control or option of its own.
|
|
13253
|
+
function opensRegion(group, header) {
|
|
13254
|
+
return !!header && group.matches('[role="region"]') && (header.getAttribute('role') || '').trim() === 'tab' && !hasComposedAncestor(header, '[role~="tablist"]') && !header.querySelector(NOT_A_QUESTION);
|
|
13255
|
+
}
|
|
13174
13256
|
function nameOf(group) {
|
|
13175
13257
|
var raw = nonBlank(group.getAttribute('aria-label'));
|
|
13176
13258
|
if (!raw) {
|
|
13177
13259
|
var ids = group.getAttribute('aria-labelledby');
|
|
13178
|
-
var legend =
|
|
13179
|
-
return child.tagName === 'LEGEND';
|
|
13180
|
-
})[0] : undefined;
|
|
13260
|
+
var legend = legendOf(group);
|
|
13181
13261
|
var root_2 = isFunction(group.getRootNode) ? group.getRootNode() : document;
|
|
13182
|
-
var target = ids ?
|
|
13183
|
-
// A region opened by an accordion header
|
|
13184
|
-
//
|
|
13185
|
-
var header_1 =
|
|
13262
|
+
var target = ids ? labelledBy(group) : legend || null;
|
|
13263
|
+
// A region opened by an accordion header is named by the header's
|
|
13264
|
+
// first heading, never the rest of what it shows.
|
|
13265
|
+
var header_1 = ids && opensRegion(group, target) ? target : null;
|
|
13186
13266
|
var source_1 = header_1 ? header_1.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]') : target;
|
|
13187
13267
|
// A label that is, holds or sits in an option or a control, or in an
|
|
13188
13268
|
// option's label, would name the answer.
|
|
@@ -13748,8 +13828,9 @@ var mParticle = (function () {
|
|
|
13748
13828
|
// Bumped on reset so classification queued before it is dropped.
|
|
13749
13829
|
this.generation = 0;
|
|
13750
13830
|
this.lastCommitClick = -Infinity;
|
|
13751
|
-
// The latest trusted click: whether the page excluded it
|
|
13752
|
-
// already carries to the kit, with its
|
|
13831
|
+
// The latest trusted click: whether the page excluded it or it was withheld
|
|
13832
|
+
// from the kit, and the intent it already carries to the kit, with its
|
|
13833
|
+
// source, if any.
|
|
13753
13834
|
this.lastClick = null;
|
|
13754
13835
|
this.lastSent = {};
|
|
13755
13836
|
this.pagePath = null;
|
|
@@ -13937,11 +14018,19 @@ var mParticle = (function () {
|
|
|
13937
14018
|
var excluded = isExcludedPath(path);
|
|
13938
14019
|
var control = excluded ? undefined : findControl(path, true);
|
|
13939
14020
|
var logged = control || (excluded ? undefined : findControl(path, true, PATH_LIMIT) || findRoleControl(path, PATH_LIMIT));
|
|
14021
|
+
// A click that link tracking logs as Auto Link Click gives the kit no
|
|
14022
|
+
// intent when the Rokt connection does not receive those events.
|
|
14023
|
+
var withheld = false;
|
|
13940
14024
|
if (logged) {
|
|
13941
14025
|
// With links alone, only an anchor can be logged.
|
|
13942
|
-
|
|
14026
|
+
var logsThis = logsEvents && (this.logButtons || logged.matches('a[href]'));
|
|
14027
|
+
var linksWithheld = signalsKit && this.logLinks && !this.linkClicksReachKit();
|
|
14028
|
+
var described = logsThis || linksWithheld ? describeButtonClick(logged, path) : undefined;
|
|
14029
|
+
var linkClick = this.logLinks && !!described && isLinkClick(described);
|
|
14030
|
+
withheld = linksWithheld && linkClick;
|
|
14031
|
+
click = logsThis ? described : undefined;
|
|
13943
14032
|
if (click) {
|
|
13944
|
-
asLink =
|
|
14033
|
+
asLink = linkClick;
|
|
13945
14034
|
// Without link tracking, a link that leaves the page stays a
|
|
13946
14035
|
// button click, except a plain link to another page of the
|
|
13947
14036
|
// site, which button tracking never logged.
|
|
@@ -13954,7 +14043,7 @@ var mParticle = (function () {
|
|
|
13954
14043
|
box = logged;
|
|
13955
14044
|
}
|
|
13956
14045
|
}
|
|
13957
|
-
if (control) {
|
|
14046
|
+
if (control && !withheld) {
|
|
13958
14047
|
// Checkout intent is only worked out when the kit can use it.
|
|
13959
14048
|
var intent = signalsKit ? classifyIntent(control, pathname) : undefined;
|
|
13960
14049
|
kind = intent ? intent.kind : undefined;
|
|
@@ -13971,9 +14060,11 @@ var mParticle = (function () {
|
|
|
13971
14060
|
checkedNames = checked ? checked.names : undefined;
|
|
13972
14061
|
}
|
|
13973
14062
|
}
|
|
14063
|
+
// A withheld click cannot start an Apple Pay session for the kit
|
|
14064
|
+
// either.
|
|
13974
14065
|
this.lastClick = {
|
|
13975
14066
|
t: t,
|
|
13976
|
-
excluded: excluded,
|
|
14067
|
+
excluded: excluded || withheld,
|
|
13977
14068
|
kind: kind,
|
|
13978
14069
|
source: source
|
|
13979
14070
|
};
|
|
@@ -14116,6 +14207,10 @@ var mParticle = (function () {
|
|
|
14116
14207
|
var roktManager = this.mpInstance._RoktManager;
|
|
14117
14208
|
return !this.hasRoktKit || !roktManager || roktManager.isCheckoutIntentSuppressed();
|
|
14118
14209
|
};
|
|
14210
|
+
ButtonTracker.prototype.linkClicksReachKit = function () {
|
|
14211
|
+
var roktManager = this.mpInstance._RoktManager;
|
|
14212
|
+
return !!roktManager && roktManager.areAutoLinkClicksForwarded();
|
|
14213
|
+
};
|
|
14119
14214
|
ButtonTracker.prototype.log = function (message) {
|
|
14120
14215
|
this.mpInstance.Logger.verbose('mParticle button tracking: ' + message);
|
|
14121
14216
|
};
|
|
@@ -15074,6 +15169,7 @@ var mParticle = (function () {
|
|
|
15074
15169
|
// Button tracking never runs as a native passthrough.
|
|
15075
15170
|
resetButtonTracking();
|
|
15076
15171
|
mpInstance._RoktManager.setAutoButtonClicksForwarded(false);
|
|
15172
|
+
mpInstance._RoktManager.setAutoLinkClicksForwarded(false);
|
|
15077
15173
|
} else {
|
|
15078
15174
|
// Main SDK initialization flow
|
|
15079
15175
|
// Load any settings/identities/attributes from cookie or localStorage
|
|
@@ -15124,14 +15220,15 @@ var mParticle = (function () {
|
|
|
15124
15220
|
// init or from a returning shopper's landing page view.
|
|
15125
15221
|
var logButtons = !!getFeatureFlag(AutoButtonTracking);
|
|
15126
15222
|
var logLinks = !!getFeatureFlag(AutoLinkTracking);
|
|
15127
|
-
//
|
|
15128
|
-
//
|
|
15223
|
+
// With link tracking on, a click logged as Auto Link Click feeds intent
|
|
15224
|
+
// only when the kit also receives those events; button clicks still do.
|
|
15129
15225
|
var buttonClickBlocker = roktConfig_1 ? getButtonClickBlocker(roktConfig_1, kitBlocker) : undefined;
|
|
15130
15226
|
var linkClickBlocker = roktConfig_1 && logLinks ? getButtonClickBlocker(roktConfig_1, kitBlocker, true) : undefined;
|
|
15131
15227
|
if (logButtons && (buttonClickBlocker || linkClickBlocker)) {
|
|
15132
|
-
mpInstance.Logger.warning(buttonClickBlocker ? "Checkout intent is off: the ".concat(buttonClickBlocker, " keeps some or all Auto Button Click events from the Rokt kit.") : "Checkout intent
|
|
15228
|
+
mpInstance.Logger.warning(buttonClickBlocker ? "Checkout intent is off: the ".concat(buttonClickBlocker, " keeps some or all Auto Button Click events from the Rokt kit.") : "Checkout intent skips link clicks: the ".concat(linkClickBlocker, " keeps some or all Auto Link Click events from the Rokt kit."));
|
|
15133
15229
|
}
|
|
15134
|
-
mpInstance._RoktManager.setAutoButtonClicksForwarded(!!roktConfig_1 && !buttonClickBlocker
|
|
15230
|
+
mpInstance._RoktManager.setAutoButtonClicksForwarded(!!roktConfig_1 && !buttonClickBlocker);
|
|
15231
|
+
mpInstance._RoktManager.setAutoLinkClicksForwarded(!!roktConfig_1 && !linkClickBlocker);
|
|
15135
15232
|
mpInstance._Forwarders.processForwarders(config, mpInstance._APIClient.prepareForwardingStats);
|
|
15136
15233
|
mpInstance._Forwarders.processPixelConfigs(config);
|
|
15137
15234
|
// Checks if session is created, resumed, or needs to be ended
|
|
@@ -111,6 +111,7 @@ export default class RoktManager {
|
|
|
111
111
|
private stopRouteChangeWatch;
|
|
112
112
|
private pendingCheckoutIntents;
|
|
113
113
|
private autoButtonClicksForwarded;
|
|
114
|
+
private autoLinkClicksForwarded;
|
|
114
115
|
constructor(instanceName?: string);
|
|
115
116
|
/**
|
|
116
117
|
* Sets a callback to be invoked when RoktManager becomes ready
|
|
@@ -137,6 +138,8 @@ export default class RoktManager {
|
|
|
137
138
|
private watchRouteChanges;
|
|
138
139
|
private notifyRouteChange;
|
|
139
140
|
setAutoButtonClicksForwarded(forwarded: boolean): void;
|
|
141
|
+
setAutoLinkClicksForwarded(forwarded: boolean): void;
|
|
142
|
+
areAutoLinkClicksForwarded(): boolean;
|
|
140
143
|
hasTargetingOptOut(): boolean;
|
|
141
144
|
isCheckoutIntentSuppressed(): boolean;
|
|
142
145
|
isCheckoutIntentLive(): boolean;
|
package/package.json
CHANGED
|
@@ -56,6 +56,11 @@ export interface IButtonClick {
|
|
|
56
56
|
checked?: boolean;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// A link run by script reads as a link, as an anchor does, so whoever reads
|
|
60
|
+
// the click treats it the same.
|
|
61
|
+
const LINK_CONTROL =
|
|
62
|
+
'a[href]:not([role="button"]), [role~="link"]:not([role~="button"])';
|
|
63
|
+
|
|
59
64
|
function elementKind(control: Element): ButtonElementKind {
|
|
60
65
|
if (isWalletElement(control)) {
|
|
61
66
|
return 'wallet';
|
|
@@ -64,13 +69,7 @@ function elementKind(control: Element): ButtonElementKind {
|
|
|
64
69
|
if (tag === 'button' || tag === 'input') {
|
|
65
70
|
return tag;
|
|
66
71
|
}
|
|
67
|
-
|
|
68
|
-
// the click treats it the same.
|
|
69
|
-
return control.matches(
|
|
70
|
-
'a[href]:not([role="button"]), [role~="link"]:not([role~="button"])'
|
|
71
|
-
)
|
|
72
|
-
? 'link'
|
|
73
|
-
: 'role';
|
|
72
|
+
return control.matches(LINK_CONTROL) ? 'link' : 'role';
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const NOT_RENDERED =
|
|
@@ -388,11 +387,37 @@ function stableIdentifier(value: string | null): string | undefined {
|
|
|
388
387
|
: undefined;
|
|
389
388
|
}
|
|
390
389
|
|
|
390
|
+
// An id React's useId generates, in any of the forms React has used: `:r1:`,
|
|
391
|
+
// `«r1»` and `_r_1_`. On the client it is r and a short base-32 counter; on
|
|
392
|
+
// the server, R and a base-32 tree id, where a component's later ids add an H
|
|
393
|
+
// and a counter (`:R1H1:`). A word that only starts with r, such as
|
|
394
|
+
// `:region:`, is not one.
|
|
395
|
+
const REACT_ID = /:(?:r[0-9a-v]{1,4}|R[0-9a-v]+(?:H[0-9a-v]+)?):|\u00ab(?:r[0-9a-v]{1,4}|R[0-9a-v]+(?:H[0-9a-v]+)?)\u00bb|_(?:r_[0-9a-v]{1,4}|R_[0-9a-v]+(?:H[0-9a-v]+)?)_/g;
|
|
396
|
+
|
|
397
|
+
// What a UI library puts before the React id it names an unnamed group with
|
|
398
|
+
// (`radix-:r1:`, `react-aria-:R1:`). Left on its own once that id is removed,
|
|
399
|
+
// it names the library, not the question.
|
|
400
|
+
const LIBRARY_ID_PREFIX = /^(?:radix|react-aria|headlessui(?:-[a-z]+)*|mui|chakra|reach|ark|mantine|base-ui)$/i;
|
|
401
|
+
|
|
391
402
|
// A picker's input name, which names its question. Sites often end it with the
|
|
392
|
-
// id of the product it belongs to (`color-swatches-48213907`)
|
|
393
|
-
//
|
|
403
|
+
// id of the product it belongs to (`color-swatches-48213907`), or build it
|
|
404
|
+
// with a React id (`variant_group__R_1mn59b_`); without either it still names
|
|
405
|
+
// the question.
|
|
394
406
|
function questionName(value: string | null): string | undefined {
|
|
395
|
-
|
|
407
|
+
if (!value) {
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
const trimmed = value.trim();
|
|
411
|
+
const withoutReactId = trimmed.replace(REACT_ID, '-');
|
|
412
|
+
const name = stableIdentifier(
|
|
413
|
+
withoutReactId
|
|
414
|
+
.replace(/([-_])[-_]+/g, '$1')
|
|
415
|
+
.replace(/^[-_]+|[-_]+$/g, '')
|
|
416
|
+
.replace(/[-_][0-9]{4,}$/, '')
|
|
417
|
+
);
|
|
418
|
+
return name && withoutReactId !== trimmed && LIBRARY_ID_PREFIX.test(name)
|
|
419
|
+
? undefined
|
|
420
|
+
: name;
|
|
396
421
|
}
|
|
397
422
|
|
|
398
423
|
export const TEST_ID_ATTRIBUTES = [
|
|
@@ -429,14 +454,19 @@ function controlsIdentifier(control: Element): string | undefined {
|
|
|
429
454
|
}
|
|
430
455
|
|
|
431
456
|
// The id of the form a button or input actually submits, which the browser
|
|
432
|
-
// resolves from its form attribute or its owning form.
|
|
433
|
-
//
|
|
457
|
+
// resolves from its form attribute or its owning form. A role control, such as
|
|
458
|
+
// a styled radio box, belongs to the form around it, read without following a
|
|
459
|
+
// form attribute or leaving its shadow root, since neither makes it part of
|
|
460
|
+
// the form; a link, anchor or role, to none. getAttribute, because a field
|
|
461
|
+
// named "id" shadows form.id.
|
|
434
462
|
function formIdentifier(control: Element): string | undefined {
|
|
435
463
|
const form =
|
|
436
464
|
control instanceof HTMLButtonElement ||
|
|
437
465
|
control instanceof HTMLInputElement
|
|
438
466
|
? control.form
|
|
439
|
-
:
|
|
467
|
+
: control.matches(LINK_CONTROL)
|
|
468
|
+
? null
|
|
469
|
+
: control.closest('form');
|
|
440
470
|
return form ? stableIdentifier(form.getAttribute('id')) : undefined;
|
|
441
471
|
}
|
|
442
472
|
|
|
@@ -887,6 +917,55 @@ function showsRow(option: Element, choice: Element, group: Element): boolean {
|
|
|
887
917
|
);
|
|
888
918
|
}
|
|
889
919
|
|
|
920
|
+
// Native boxes with no name, which the browser doesn't group, answer one
|
|
921
|
+
// question only as menu option rows do: each in a shown row of its own, in the
|
|
922
|
+
// same group. A card around a "make default" checkbox and another box has no
|
|
923
|
+
// such rows. Rows alone can't tell a question from a card named for a person
|
|
924
|
+
// around separate settings, so only a group that shows its question and holds
|
|
925
|
+
// the rows directly counts.
|
|
926
|
+
function unnamedOptionRows(
|
|
927
|
+
option: Element,
|
|
928
|
+
choice: Element,
|
|
929
|
+
group: Element
|
|
930
|
+
): boolean {
|
|
931
|
+
return (
|
|
932
|
+
option instanceof HTMLInputElement &&
|
|
933
|
+
choice instanceof HTMLInputElement &&
|
|
934
|
+
option.type === choice.type &&
|
|
935
|
+
!option.getAttribute('name') &&
|
|
936
|
+
!choice.getAttribute('name') &&
|
|
937
|
+
option.form === choice.form &&
|
|
938
|
+
option.closest(NAMED_GROUP) === group &&
|
|
939
|
+
choice.closest(NAMED_GROUP) === group &&
|
|
940
|
+
showsRow(option, choice, group) &&
|
|
941
|
+
showsRow(choice, option, group) &&
|
|
942
|
+
showsQuestion(group)
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// A native input the page hides, styling its label as the option instead, is
|
|
947
|
+
// shown by that label: one of its own, in the group, that the browser draws
|
|
948
|
+
// and that shows text of its own, as an option row must. Neither an opted-out
|
|
949
|
+
// input nor an opted-out label counts.
|
|
950
|
+
function showsLabel(option: Element, choice: Element, group: Element): boolean {
|
|
951
|
+
return (
|
|
952
|
+
option instanceof HTMLInputElement &&
|
|
953
|
+
!!option.labels &&
|
|
954
|
+
!hasComposedAncestor(option, '[data-mp-no-track]') &&
|
|
955
|
+
Array.from(option.labels).some(
|
|
956
|
+
label =>
|
|
957
|
+
group.contains(label) &&
|
|
958
|
+
!label.contains(choice) &&
|
|
959
|
+
!hasComposedAncestor(label, '[data-mp-no-track]') &&
|
|
960
|
+
isShown(label, group) &&
|
|
961
|
+
isVisible(label) &&
|
|
962
|
+
!isTransparent(label, group) &&
|
|
963
|
+
isComposedVisible(label) &&
|
|
964
|
+
showsText(label, option)
|
|
965
|
+
)
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
890
969
|
// Whether a group offers another answer to the question picked: a shown option
|
|
891
970
|
// to the same question that isn't part of the one picked. A card around one
|
|
892
971
|
// address radio and a "make default" checkbox, or around a role radio and the
|
|
@@ -898,13 +977,16 @@ function holdsOptions(group: Element, choice: Element): boolean {
|
|
|
898
977
|
if (
|
|
899
978
|
!option.contains(choice) &&
|
|
900
979
|
!choice.contains(option) &&
|
|
901
|
-
sameQuestion(option, choice)
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
980
|
+
(sameQuestion(option, choice) ||
|
|
981
|
+
unnamedOptionRows(option, choice, group)) &&
|
|
982
|
+
((isShown(option, group) &&
|
|
983
|
+
isVisible(option) &&
|
|
984
|
+
// A fully transparent option, as a native input behind a
|
|
985
|
+
// styled one often is, shows the shopper no second answer,
|
|
986
|
+
// unless a shown row around it does.
|
|
987
|
+
(!isTransparent(option, group) ||
|
|
988
|
+
showsRow(option, choice, group))) ||
|
|
989
|
+
showsLabel(option, choice, group))
|
|
908
990
|
) {
|
|
909
991
|
return true;
|
|
910
992
|
}
|
|
@@ -964,34 +1046,74 @@ function groupName(choice: Element): string | undefined {
|
|
|
964
1046
|
return undefined;
|
|
965
1047
|
}
|
|
966
1048
|
|
|
1049
|
+
// Whether what names a group is shown as its question: a fieldset's own
|
|
1050
|
+
// legend, or the header that opens a section. nameOf reads an aria-label first,
|
|
1051
|
+
// so a group carrying one is named by that instead.
|
|
1052
|
+
function showsQuestion(group: Element): boolean {
|
|
1053
|
+
if (group.tagName === 'FIELDSET') {
|
|
1054
|
+
return namedByLegend(group);
|
|
1055
|
+
}
|
|
1056
|
+
return (
|
|
1057
|
+
!nonBlank(group.getAttribute('aria-label')) &&
|
|
1058
|
+
opensRegion(group, labelledBy(group))
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// A fieldset's own legend, a direct child, as the browser reads it.
|
|
1063
|
+
function legendOf(group: Element): Element | undefined {
|
|
1064
|
+
return group.tagName === 'FIELDSET'
|
|
1065
|
+
? Array.from(group.children).filter(
|
|
1066
|
+
child => child.tagName === 'LEGEND'
|
|
1067
|
+
)[0]
|
|
1068
|
+
: undefined;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// Whether a fieldset is named by its own legend, which nameOf reads only when
|
|
1072
|
+
// no aria-label overrides it and any aria-labelledby points at that legend.
|
|
1073
|
+
function namedByLegend(group: Element): boolean {
|
|
1074
|
+
const legend = legendOf(group);
|
|
1075
|
+
return (
|
|
1076
|
+
!!legend &&
|
|
1077
|
+
!nonBlank(group.getAttribute('aria-label')) &&
|
|
1078
|
+
(!group.getAttribute('aria-labelledby') || labelledBy(group) === legend)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// The first element a group's aria-labelledby points at.
|
|
1083
|
+
function labelledBy(group: Element): Element | null {
|
|
1084
|
+
const ids = group.getAttribute('aria-labelledby');
|
|
1085
|
+
const root = (isFunction(group.getRootNode)
|
|
1086
|
+
? group.getRootNode()
|
|
1087
|
+
: document) as Document | ShadowRoot;
|
|
1088
|
+
return ids && root.getElementById
|
|
1089
|
+
? root.getElementById(ids.trim().split(/\s+/)[0])
|
|
1090
|
+
: null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Whether a region is opened by this accordion header: a tab outside any tab
|
|
1094
|
+
// list, holding no control or option of its own.
|
|
1095
|
+
function opensRegion(group: Element, header: Element | null): boolean {
|
|
1096
|
+
return (
|
|
1097
|
+
!!header &&
|
|
1098
|
+
group.matches('[role="region"]') &&
|
|
1099
|
+
(header.getAttribute('role') || '').trim() === 'tab' &&
|
|
1100
|
+
!hasComposedAncestor(header, '[role~="tablist"]') &&
|
|
1101
|
+
!header.querySelector(NOT_A_QUESTION)
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
967
1105
|
function nameOf(group: Element): string | undefined {
|
|
968
1106
|
let raw = nonBlank(group.getAttribute('aria-label'));
|
|
969
1107
|
if (!raw) {
|
|
970
1108
|
const ids = group.getAttribute('aria-labelledby');
|
|
971
|
-
const legend =
|
|
972
|
-
group.tagName === 'FIELDSET'
|
|
973
|
-
? Array.from(group.children).filter(
|
|
974
|
-
child => child.tagName === 'LEGEND'
|
|
975
|
-
)[0]
|
|
976
|
-
: undefined;
|
|
1109
|
+
const legend = legendOf(group);
|
|
977
1110
|
const root = (isFunction(group.getRootNode)
|
|
978
1111
|
? group.getRootNode()
|
|
979
1112
|
: document) as Document | ShadowRoot;
|
|
980
|
-
const target = ids
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
: legend || null;
|
|
985
|
-
// A region opened by an accordion header outside a tab list is named
|
|
986
|
-
// by the header's first heading, never the rest of what it shows.
|
|
987
|
-
const header =
|
|
988
|
-
target &&
|
|
989
|
-
group.matches('[role="region"]') &&
|
|
990
|
-
(target.getAttribute('role') || '').trim() === 'tab' &&
|
|
991
|
-
!hasComposedAncestor(target, '[role~="tablist"]') &&
|
|
992
|
-
!target.querySelector(NOT_A_QUESTION)
|
|
993
|
-
? target
|
|
994
|
-
: null;
|
|
1113
|
+
const target = ids ? labelledBy(group) : legend || null;
|
|
1114
|
+
// A region opened by an accordion header is named by the header's
|
|
1115
|
+
// first heading, never the rest of what it shows.
|
|
1116
|
+
const header = ids && opensRegion(group, target) ? target : null;
|
|
995
1117
|
const source = header
|
|
996
1118
|
? header.querySelector('h1, h2, h3, h4, h5, h6, [role="heading"]')
|
|
997
1119
|
: target;
|