@pendo/agent 2.341.0 → 2.343.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/dom.esm.js +14 -25
- package/dist/pendo.module.js +520 -216
- package/dist/pendo.module.min.js +12 -12
- package/dist/servers.json +8 -8
- package/package.json +1 -1
package/dist/pendo.module.js
CHANGED
|
@@ -3926,6 +3926,17 @@ function shouldWrapHashChange() {
|
|
|
3926
3926
|
function isMobileUserAgent() {
|
|
3927
3927
|
return (/Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i).test(getUA());
|
|
3928
3928
|
}
|
|
3929
|
+
// WICG inert polyfills through 3.1.0 install their MutationObserver-driven manager even where
|
|
3930
|
+
// the browser supports inert natively, and inject #inert-style when they do. With one active,
|
|
3931
|
+
// inerting a large subtree costs a full tree walk plus a subtree observer for as long as the
|
|
3932
|
+
// attribute stays, so treat the page as not supporting inert.
|
|
3933
|
+
function supportsInert() {
|
|
3934
|
+
const descriptor = window.HTMLElement && _.isFunction(Object.getOwnPropertyDescriptor) &&
|
|
3935
|
+
Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, 'inert');
|
|
3936
|
+
const isNative = !!(descriptor && descriptor.get && isNativeCode(descriptor.get));
|
|
3937
|
+
const isPolyfilled = !!document.getElementById('inert-style');
|
|
3938
|
+
return isNative && !isPolyfilled;
|
|
3939
|
+
}
|
|
3929
3940
|
function isSafari(userAgent = getUA()) {
|
|
3930
3941
|
return userAgent.indexOf('Safari') >= 0 && !isChromium(userAgent);
|
|
3931
3942
|
}
|
|
@@ -3938,6 +3949,7 @@ function isChromium(userAgent = getUA()) {
|
|
|
3938
3949
|
const sniffer = {
|
|
3939
3950
|
supportsHistoryApi: shouldWrapNativeHistory,
|
|
3940
3951
|
supportsHashChange: shouldWrapHashChange,
|
|
3952
|
+
supportsInert,
|
|
3941
3953
|
hasEvent(event) {
|
|
3942
3954
|
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
|
|
3943
3955
|
// it. In particular the event is not fired when backspace or delete key are pressed or
|
|
@@ -4046,8 +4058,8 @@ let SERVER = '';
|
|
|
4046
4058
|
let ASSET_HOST = '';
|
|
4047
4059
|
let ASSET_PATH = '';
|
|
4048
4060
|
let DESIGNER_SERVER = '';
|
|
4049
|
-
let VERSION = '2.
|
|
4050
|
-
let PACKAGE_VERSION = '2.
|
|
4061
|
+
let VERSION = '2.343.0_';
|
|
4062
|
+
let PACKAGE_VERSION = '2.343.0';
|
|
4051
4063
|
let LOADER = 'xhr';
|
|
4052
4064
|
/* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
|
|
4053
4065
|
/**
|
|
@@ -4464,15 +4476,35 @@ class LogEmitter extends EventTarget {
|
|
|
4464
4476
|
}
|
|
4465
4477
|
const log = new LogEmitter();
|
|
4466
4478
|
|
|
4467
|
-
/** Key used to lookup the consent collection policy in the pendo configuration. */
|
|
4468
4479
|
const CONSENT_COLLECTION = 'cookieConsentCollection';
|
|
4469
|
-
/** Key used to lookup the analytics events in the consent collection policy. */
|
|
4470
4480
|
const ANALYTICS = 'analytics';
|
|
4471
|
-
/** Key used to lookup the guide analytics events in the consent collection policy. */
|
|
4472
4481
|
const GUIDE_ANALYTICS = 'guideAnalytics';
|
|
4473
|
-
/** Key used to lookup the text collection mode in the consent collection policy. */
|
|
4474
4482
|
const TEXT_COLLECTION = 'textCollection';
|
|
4475
4483
|
const TEXT_COLLECTION_LEVELS = ['none', 'whitelist', 'all'];
|
|
4484
|
+
const ANALYTICS_EVENT_TYPES = {
|
|
4485
|
+
agentResponse: 'agent_response',
|
|
4486
|
+
clicks: 'click',
|
|
4487
|
+
fieldChanges: 'change',
|
|
4488
|
+
focus: 'focus',
|
|
4489
|
+
formSubmits: 'submit',
|
|
4490
|
+
identify: 'identify',
|
|
4491
|
+
meta: 'meta',
|
|
4492
|
+
pageLoads: 'load',
|
|
4493
|
+
prompt: 'prompt',
|
|
4494
|
+
trackEvents: 'track',
|
|
4495
|
+
userReaction: 'user_reaction'
|
|
4496
|
+
};
|
|
4497
|
+
const GUIDE_EVENT_TYPES = [
|
|
4498
|
+
'guideActivity',
|
|
4499
|
+
'guideAdvanced',
|
|
4500
|
+
'guideDismissed',
|
|
4501
|
+
'guideNotSeen',
|
|
4502
|
+
'guideSeen',
|
|
4503
|
+
'guideSnoozeCanceled',
|
|
4504
|
+
'guideSnoozed',
|
|
4505
|
+
'guideTimeout',
|
|
4506
|
+
'pollResponse'
|
|
4507
|
+
];
|
|
4476
4508
|
let enabledProducts = null;
|
|
4477
4509
|
function getClassification() {
|
|
4478
4510
|
return ConfigReader.get(CONSENT_COLLECTION) || {};
|
|
@@ -4492,30 +4524,59 @@ function warnOnUnconfiguredTiers(grantedTiers, classification) {
|
|
|
4492
4524
|
function moreTextPermissive(a, b) {
|
|
4493
4525
|
return TEXT_COLLECTION_LEVELS.indexOf(b) > TEXT_COLLECTION_LEVELS.indexOf(a) ? b : a;
|
|
4494
4526
|
}
|
|
4495
|
-
function
|
|
4496
|
-
return
|
|
4527
|
+
function toTextCollectionLevel(mode) {
|
|
4528
|
+
return _.contains(TEXT_COLLECTION_LEVELS, mode) ? mode : TEXT_COLLECTION_LEVELS[0];
|
|
4497
4529
|
}
|
|
4498
|
-
function
|
|
4499
|
-
return _.
|
|
4530
|
+
function toAnalyticsEventTypes(eventCollection) {
|
|
4531
|
+
return _.reduce(ANALYTICS_EVENT_TYPES, function (types, eventType, key) {
|
|
4532
|
+
if (eventCollection[key] === true) {
|
|
4533
|
+
types.push(eventType);
|
|
4534
|
+
}
|
|
4535
|
+
return types;
|
|
4536
|
+
}, []);
|
|
4537
|
+
}
|
|
4538
|
+
function normalizeTier(tier) {
|
|
4539
|
+
const { visitorIdentity = {}, analytics = {}, guides = {}, sessionReplay = {} } = tier || {};
|
|
4540
|
+
const eventCollection = analytics.eventCollection || {};
|
|
4541
|
+
const analyticsEnabled = analytics.enabled === true;
|
|
4542
|
+
return {
|
|
4543
|
+
identity: visitorIdentity.trackVisitors === true,
|
|
4544
|
+
guides: guides.enabled === true,
|
|
4545
|
+
replay: sessionReplay.enabled === true,
|
|
4546
|
+
analytics: analyticsEnabled ? toAnalyticsEventTypes(eventCollection) : [],
|
|
4547
|
+
guideAnalytics: analyticsEnabled && eventCollection.guideServing === true ? GUIDE_EVENT_TYPES.slice() : [],
|
|
4548
|
+
textCollection: analyticsEnabled ? toTextCollectionLevel(eventCollection.textCollectionMode) : TEXT_COLLECTION_LEVELS[0]
|
|
4549
|
+
};
|
|
4550
|
+
}
|
|
4551
|
+
function deniedProducts() {
|
|
4552
|
+
return {
|
|
4553
|
+
identity: false,
|
|
4554
|
+
guides: false,
|
|
4555
|
+
replay: false,
|
|
4556
|
+
[ANALYTICS]: [],
|
|
4557
|
+
[GUIDE_ANALYTICS]: [],
|
|
4558
|
+
[TEXT_COLLECTION]: TEXT_COLLECTION_LEVELS[0]
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
function combineTiers(resolved, granted) {
|
|
4562
|
+
return {
|
|
4563
|
+
identity: resolved.identity || granted.identity,
|
|
4564
|
+
guides: resolved.guides || granted.guides,
|
|
4565
|
+
replay: resolved.replay || granted.replay,
|
|
4566
|
+
[ANALYTICS]: _.union(resolved[ANALYTICS], granted[ANALYTICS]),
|
|
4567
|
+
[GUIDE_ANALYTICS]: _.union(resolved[GUIDE_ANALYTICS], granted[GUIDE_ANALYTICS]),
|
|
4568
|
+
[TEXT_COLLECTION]: moreTextPermissive(resolved[TEXT_COLLECTION], granted[TEXT_COLLECTION])
|
|
4569
|
+
};
|
|
4500
4570
|
}
|
|
4501
|
-
// How a granted tier's value for a product combines with what's been resolved so far.
|
|
4502
|
-
// `initial` doubles as what an ungranted tier contributes -- an unlisted product falls
|
|
4503
|
-
// back to DEFAULT_RESOLVER, the plain "any granted tier turns it on" boolean rule.
|
|
4504
|
-
const RESOLVERS = {
|
|
4505
|
-
[TEXT_COLLECTION]: { initial: TEXT_COLLECTION_LEVELS[0], combine: moreTextPermissive },
|
|
4506
|
-
[ANALYTICS]: { initial: [], combine: unionArrays },
|
|
4507
|
-
[GUIDE_ANALYTICS]: { initial: [], combine: unionArrays }
|
|
4508
|
-
};
|
|
4509
|
-
const DEFAULT_RESOLVER = { initial: false, combine: orBoolean };
|
|
4510
4571
|
/**
|
|
4511
|
-
* Resolves the
|
|
4572
|
+
* Resolves the tiers a visitor consented to into what each product is allowed to do.
|
|
4512
4573
|
*
|
|
4513
|
-
*
|
|
4514
|
-
*
|
|
4515
|
-
*
|
|
4516
|
-
*
|
|
4517
|
-
*
|
|
4518
|
-
*
|
|
4574
|
+
* Every product starts denied and only granted tiers contribute, so a tier the visitor
|
|
4575
|
+
* hasn't opted into has no say -- it can't deny anything, and it can't allow anything.
|
|
4576
|
+
*
|
|
4577
|
+
* A subscription with no cookie consent configured resolves to an empty map instead, which
|
|
4578
|
+
* every read below treats as unconstrained: consent that was never set up can't deny
|
|
4579
|
+
* anything either.
|
|
4519
4580
|
*
|
|
4520
4581
|
* @access private
|
|
4521
4582
|
* @param {string | string[]} tiers tier or tiers the visitor consented to
|
|
@@ -4524,26 +4585,22 @@ const DEFAULT_RESOLVER = { initial: false, combine: orBoolean };
|
|
|
4524
4585
|
function applyConsentTiers(tiers) {
|
|
4525
4586
|
const cookieConsentCollection = getClassification();
|
|
4526
4587
|
const consentedCookieTiers = toTierList(tiers);
|
|
4527
|
-
const resolved = {};
|
|
4528
4588
|
warnOnUnconfiguredTiers(consentedCookieTiers, cookieConsentCollection);
|
|
4529
|
-
_.
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
});
|
|
4538
|
-
enabledProducts = resolved;
|
|
4589
|
+
if (_.isEmpty(cookieConsentCollection)) {
|
|
4590
|
+
enabledProducts = {};
|
|
4591
|
+
return getEnabledProducts();
|
|
4592
|
+
}
|
|
4593
|
+
enabledProducts = _.reduce(consentedCookieTiers, function (resolved, tier) {
|
|
4594
|
+
const granted = cookieConsentCollection[tier];
|
|
4595
|
+
return granted ? combineTiers(resolved, normalizeTier(granted)) : resolved;
|
|
4596
|
+
}, deniedProducts());
|
|
4539
4597
|
return getEnabledProducts();
|
|
4540
4598
|
}
|
|
4541
4599
|
function getEnabledProducts() {
|
|
4542
4600
|
return enabledProducts ? _.extend({}, enabledProducts) : null;
|
|
4543
4601
|
}
|
|
4544
4602
|
/**
|
|
4545
|
-
* A product is enabled
|
|
4546
|
-
* consent configured at all, or a product no tier ever mentions, there's nothing to deny.
|
|
4603
|
+
* A product is enabled only if some granted tier turned it on or cookie consent has not been set.
|
|
4547
4604
|
*
|
|
4548
4605
|
* @access private
|
|
4549
4606
|
* @param {string} product
|
|
@@ -4557,6 +4614,9 @@ function isProductEnabled(product) {
|
|
|
4557
4614
|
function isTrackingConsentDenied() {
|
|
4558
4615
|
return !isProductEnabled('identity');
|
|
4559
4616
|
}
|
|
4617
|
+
function isGuideConsentDenied() {
|
|
4618
|
+
return !isProductEnabled('guides');
|
|
4619
|
+
}
|
|
4560
4620
|
/**
|
|
4561
4621
|
* Session replay is denied whenever tracking itself is denied (DNT) -- a visitor
|
|
4562
4622
|
* who can't be identified shouldn't have their session recorded either -- or when a
|
|
@@ -4592,6 +4652,9 @@ function hasConsentForAnalyticsEvent(type) {
|
|
|
4592
4652
|
function hasConsentForGuideEvent(type) {
|
|
4593
4653
|
return hasConsentForProduct(type, GUIDE_ANALYTICS);
|
|
4594
4654
|
}
|
|
4655
|
+
function resetConsentPolicy() {
|
|
4656
|
+
enabledProducts = null;
|
|
4657
|
+
}
|
|
4595
4658
|
|
|
4596
4659
|
const allowKeys = function () {
|
|
4597
4660
|
var allowKeys = ConfigReader.get('storage.allowKeys') || '*';
|
|
@@ -14353,9 +14416,13 @@ function getPendoCdnHost() {
|
|
|
14353
14416
|
return stripScheme(ASSET_HOST);
|
|
14354
14417
|
return serverHost.replace(appLabel, '$1cdn.');
|
|
14355
14418
|
}
|
|
14356
|
-
//
|
|
14419
|
+
// A contentHost set up as a CNAME in app settings is delivered in the hosted config, while a
|
|
14420
|
+
// snippet value is the customer's own override of it. Same precedence getDataHost uses.
|
|
14421
|
+
function getConfiguredContentHost() {
|
|
14422
|
+
return ConfigReader.getLocalConfig('contentHost') || ConfigReader.getHostedConfig('contentHost');
|
|
14423
|
+
}
|
|
14357
14424
|
function replaceWithContentHost(str) {
|
|
14358
|
-
var contentHost =
|
|
14425
|
+
var contentHost = getConfiguredContentHost();
|
|
14359
14426
|
if (!contentHost || !str)
|
|
14360
14427
|
return str;
|
|
14361
14428
|
contentHost = normalizeContentHost(contentHost);
|
|
@@ -14380,7 +14447,7 @@ function replaceWithContentHost(str) {
|
|
|
14380
14447
|
}, str);
|
|
14381
14448
|
}
|
|
14382
14449
|
function replaceObjectWithContentHost(obj) {
|
|
14383
|
-
var contentHost =
|
|
14450
|
+
var contentHost = getConfiguredContentHost();
|
|
14384
14451
|
if (!contentHost)
|
|
14385
14452
|
return obj;
|
|
14386
14453
|
return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
|
|
@@ -22031,7 +22098,9 @@ Badge.behaviors = [
|
|
|
22031
22098
|
function InlinePosition() {
|
|
22032
22099
|
function getShadowDomAdjustedTarget(target) {
|
|
22033
22100
|
try {
|
|
22034
|
-
|
|
22101
|
+
// Only a default (unnamed) slot can render a badge appended as a light-DOM
|
|
22102
|
+
// child, so fall back to the shadow root when there isn't one
|
|
22103
|
+
if (target.shadowRoot && !target.shadowRoot.querySelector('slot:not([name])')) {
|
|
22035
22104
|
return target.shadowRoot;
|
|
22036
22105
|
}
|
|
22037
22106
|
}
|
|
@@ -22052,7 +22121,7 @@ function InlinePosition() {
|
|
|
22052
22121
|
if (targetElem && targetElem.tagName) {
|
|
22053
22122
|
var tagName = targetElem.tagName.toLowerCase();
|
|
22054
22123
|
var shadowDomAdjustedTarget = getShadowDomAdjustedTarget(targetElem);
|
|
22055
|
-
if (
|
|
22124
|
+
if (/^(?:br|input|img|select|textarea)$/.test(tagName)) {
|
|
22056
22125
|
if (badgeElem.parentNode === targetElem.parentNode)
|
|
22057
22126
|
return;
|
|
22058
22127
|
// Insert after if targetElem cannot have children
|
|
@@ -22143,14 +22212,29 @@ function ClickActivation() {
|
|
|
22143
22212
|
}
|
|
22144
22213
|
stopEvent(evt);
|
|
22145
22214
|
};
|
|
22215
|
+
// Safari does not focus the badge on mousedown, so focus leaves the app instead.
|
|
22216
|
+
// Apps that re-render on blur can remove the badge before mouseup, and the browser
|
|
22217
|
+
// then generates no click at all. Holding focus where it is avoids that (APP-168412)
|
|
22218
|
+
var preserveFocus = function (evt) {
|
|
22219
|
+
if (evt.preventDefault) {
|
|
22220
|
+
evt.preventDefault();
|
|
22221
|
+
}
|
|
22222
|
+
else {
|
|
22223
|
+
evt.returnValue = false;
|
|
22224
|
+
}
|
|
22225
|
+
};
|
|
22146
22226
|
badge.after('show', function () {
|
|
22147
22227
|
if (!attached) {
|
|
22148
22228
|
attachEvent(badgeElem, 'click', click);
|
|
22229
|
+
if (badge.preserveFocusOnClick) {
|
|
22230
|
+
attachEvent(badgeElem, 'mousedown', preserveFocus);
|
|
22231
|
+
}
|
|
22149
22232
|
attached = true;
|
|
22150
22233
|
}
|
|
22151
22234
|
});
|
|
22152
22235
|
badge.after('hide', function () {
|
|
22153
22236
|
detachEvent(badgeElem, 'click', click);
|
|
22237
|
+
detachEvent(badgeElem, 'mousedown', preserveFocus);
|
|
22154
22238
|
attached = false;
|
|
22155
22239
|
});
|
|
22156
22240
|
return badge;
|
|
@@ -22621,7 +22705,7 @@ function stepShowingProc(guide, step) {
|
|
|
22621
22705
|
return;
|
|
22622
22706
|
}
|
|
22623
22707
|
const embedSelector = _.get(guide, 'attributes.embedConfig.selector');
|
|
22624
|
-
if (embedSelector && !dom(embedSelector).length) {
|
|
22708
|
+
if (embedSelector && (!dom(embedSelector).length || (guideElement && !isInDocument(guideElement[0])))) {
|
|
22625
22709
|
step.hide();
|
|
22626
22710
|
return;
|
|
22627
22711
|
}
|
|
@@ -23590,16 +23674,23 @@ function backdropInertRuntimeScript(step, guide, pendo) {
|
|
|
23590
23674
|
return;
|
|
23591
23675
|
if (step.hasBackdrop) {
|
|
23592
23676
|
const BuildingBlockGuides = pendo.BuildingBlocks.BuildingBlockGuides;
|
|
23593
|
-
const
|
|
23594
|
-
|
|
23595
|
-
|
|
23596
|
-
|
|
23597
|
-
|
|
23598
|
-
|
|
23599
|
-
|
|
23600
|
-
|
|
23601
|
-
|
|
23677
|
+
const guideElement = step.guideElement[0];
|
|
23678
|
+
const excludeElements = getExcludedElements(step);
|
|
23679
|
+
const disabledElements = sniffer.supportsInert()
|
|
23680
|
+
? BuildingBlockGuides.addInertOutsideGuide(guideElement, [], excludeElements)
|
|
23681
|
+
: BuildingBlockGuides.addAriaHiddenOutsideGuide(guideElement, [], excludeElements);
|
|
23682
|
+
step.hiddenElements = (step.hiddenElements || []).concat(disabledElements);
|
|
23683
|
+
}
|
|
23684
|
+
}
|
|
23685
|
+
function getExcludedElements(step) {
|
|
23686
|
+
const excludeElements = getTargetAreaElementsForBlockOut(step);
|
|
23687
|
+
// The preview bar iframe is a sibling of the guide under <body>; inerting it
|
|
23688
|
+
// would make preview mode's controls unclickable while the backdrop shows.
|
|
23689
|
+
const previewBar = document.getElementById('pendo-preview');
|
|
23690
|
+
if (previewBar) {
|
|
23691
|
+
excludeElements.push(previewBar);
|
|
23602
23692
|
}
|
|
23693
|
+
return excludeElements;
|
|
23603
23694
|
}
|
|
23604
23695
|
var backdropInertStep = {
|
|
23605
23696
|
script: backdropInertRuntimeScript
|
|
@@ -25358,6 +25449,9 @@ var initGuides = function (observer) {
|
|
|
25358
25449
|
return;
|
|
25359
25450
|
guides ? enableGuides() : disableGuides();
|
|
25360
25451
|
});
|
|
25452
|
+
if (isGuideConsentDenied()) {
|
|
25453
|
+
disableGuides();
|
|
25454
|
+
}
|
|
25361
25455
|
teardownFns.push(onTurbolinksPageLoad(document, function onPageLoad() {
|
|
25362
25456
|
if (pendoDotUrl.get() === reloadGuides.lastUrl) {
|
|
25363
25457
|
// Force a reload if Turbolinks replaces the document body without changing the url
|
|
@@ -29300,6 +29394,7 @@ const PluginAPI = {
|
|
|
29300
29394
|
q,
|
|
29301
29395
|
SendQueue,
|
|
29302
29396
|
sessionStorage: agentStorage.getSession(),
|
|
29397
|
+
storageIsDisabled,
|
|
29303
29398
|
sizeElements,
|
|
29304
29399
|
store,
|
|
29305
29400
|
Tombstone,
|
|
@@ -29478,6 +29573,34 @@ function initIdentityEvents() {
|
|
|
29478
29573
|
};
|
|
29479
29574
|
}
|
|
29480
29575
|
|
|
29576
|
+
/**
|
|
29577
|
+
* Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
|
|
29578
|
+
* and is used to determine whether each product the web SDK may run.
|
|
29579
|
+
*
|
|
29580
|
+
* @access public
|
|
29581
|
+
* @category Core
|
|
29582
|
+
* @param {string | string[]} tiers tier or tiers the visitor consented to
|
|
29583
|
+
* @returns {object} map of product to its resolved value -- `guides` and `identity` are
|
|
29584
|
+
* booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
|
|
29585
|
+
* `analytics` and `guideAnalytics` are each an array of allowed event types
|
|
29586
|
+
* @example
|
|
29587
|
+
* pendo.updateCookieConsent('strictlyNecessary');
|
|
29588
|
+
* @example
|
|
29589
|
+
* pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
|
|
29590
|
+
*/
|
|
29591
|
+
function updateCookieConsent(tiers) {
|
|
29592
|
+
const enabledProducts = applyConsentTiers(tiers);
|
|
29593
|
+
Events['consent:changed'].trigger(enabledProducts);
|
|
29594
|
+
log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
|
|
29595
|
+
return enabledProducts;
|
|
29596
|
+
}
|
|
29597
|
+
function initializeCookieConsent(tiers) {
|
|
29598
|
+
resetConsentPolicy();
|
|
29599
|
+
if (tiers) {
|
|
29600
|
+
updateCookieConsent(tiers);
|
|
29601
|
+
}
|
|
29602
|
+
}
|
|
29603
|
+
|
|
29481
29604
|
/*
|
|
29482
29605
|
* ResourceCenterActivity Event
|
|
29483
29606
|
*
|
|
@@ -29961,6 +30084,8 @@ function initialize(options) {
|
|
|
29961
30084
|
}
|
|
29962
30085
|
// Save the options somewhere
|
|
29963
30086
|
ConfigReader.setLocalConfig(options);
|
|
30087
|
+
const cookieConsentTiers = _.get(options, 'cookieConsentTiers');
|
|
30088
|
+
initializeCookieConsent(cookieConsentTiers);
|
|
29964
30089
|
initDataHost();
|
|
29965
30090
|
exportDataHost(pendo$1);
|
|
29966
30091
|
const cookieDomain = ConfigReader.get('cookieDomain');
|
|
@@ -30932,21 +31057,26 @@ var BuildingBlockGuides = (function () {
|
|
|
30932
31057
|
domElem.append(endFocusBumper);
|
|
30933
31058
|
return [startFocusBumper, endFocusBumper];
|
|
30934
31059
|
}
|
|
30935
|
-
|
|
31060
|
+
// native contains() can't see into shadow trees, so excluded elements inside a
|
|
31061
|
+
// shadow root (e.g. blockOut target areas) need the shadow-aware walk
|
|
31062
|
+
function containsExcludedElement(elem, excludeElements) {
|
|
31063
|
+
return _.some(excludeElements, (excluded) => excluded && (elem === excluded || contains(elem, excluded)));
|
|
31064
|
+
}
|
|
31065
|
+
function addAriaHiddenOutsideGuide(elem, hidden = [], excludeElements = []) {
|
|
30936
31066
|
if (!elem || elem === getBody())
|
|
30937
31067
|
return hidden;
|
|
30938
31068
|
const siblings = getSiblings(elem);
|
|
30939
31069
|
_.each(siblings, (s) => {
|
|
30940
|
-
|
|
30941
|
-
|
|
30942
|
-
|
|
30943
|
-
|
|
30944
|
-
|
|
30945
|
-
|
|
31070
|
+
const tagName = s.tagName.toLowerCase();
|
|
31071
|
+
const alreadyHidden = s.getAttribute('aria-hidden') === 'true';
|
|
31072
|
+
const isExcluded = containsExcludedElement(s, excludeElements);
|
|
31073
|
+
if (hidden.indexOf(s) === -1 && !alreadyHidden && !isExcluded && tagName !== 'script' && tagName !== 'style') {
|
|
31074
|
+
s.setAttribute('aria-hidden', 'true');
|
|
31075
|
+
hidden.push(s);
|
|
30946
31076
|
}
|
|
30947
31077
|
});
|
|
30948
31078
|
if (elem.parentNode) {
|
|
30949
|
-
addAriaHiddenOutsideGuide(elem.parentNode, hidden);
|
|
31079
|
+
addAriaHiddenOutsideGuide(elem.parentNode, hidden, excludeElements);
|
|
30950
31080
|
}
|
|
30951
31081
|
return hidden;
|
|
30952
31082
|
}
|
|
@@ -30960,9 +31090,7 @@ var BuildingBlockGuides = (function () {
|
|
|
30960
31090
|
_.each(siblings, (s) => {
|
|
30961
31091
|
const tagName = s.tagName.toLowerCase();
|
|
30962
31092
|
const alreadyDisabled = s.getAttribute('inert') === 'true';
|
|
30963
|
-
|
|
30964
|
-
// shadow root (e.g. blockOut target areas) need the shadow-aware walk
|
|
30965
|
-
const isExcluded = _.some(excludeElements, (excluded) => excluded && (s === excluded || contains(s, excluded)));
|
|
31093
|
+
const isExcluded = containsExcludedElement(s, excludeElements);
|
|
30966
31094
|
if (hidden.indexOf(s) === -1 && !alreadyDisabled && !isExcluded && tagName !== 'script' && tagName !== 'style') {
|
|
30967
31095
|
s.setAttribute('inert', 'true');
|
|
30968
31096
|
hidden.push(s);
|
|
@@ -31204,28 +31332,6 @@ logPublic.setActiveContexts = setActiveContexts;
|
|
|
31204
31332
|
logPublic.showLogHistory = showLogHistory;
|
|
31205
31333
|
logPublic.getLoggedContexts = getLoggedContexts;
|
|
31206
31334
|
|
|
31207
|
-
/**
|
|
31208
|
-
* Enforces the cookie consent policy a visitor has agreed to. The policy is defined by the subscription's cookie consent configuration,
|
|
31209
|
-
* and is used to determine whether each product the web SDK may run.
|
|
31210
|
-
*
|
|
31211
|
-
* @access public
|
|
31212
|
-
* @category Core
|
|
31213
|
-
* @param {string | string[]} tiers tier or tiers the visitor consented to
|
|
31214
|
-
* @returns {object} map of product to its resolved value -- `guides` and `identity` are
|
|
31215
|
-
* booleans (identity disabled means DNT), `textCollection` is `none`/`whitelist`/`all`,
|
|
31216
|
-
* `analytics` and `guideAnalytics` are each an array of allowed event types
|
|
31217
|
-
* @example
|
|
31218
|
-
* pendo.updateCookieConsent('strictlyNecessary');
|
|
31219
|
-
* @example
|
|
31220
|
-
* pendo.updateCookieConsent(['strictlyNecessary', 'functional']);
|
|
31221
|
-
*/
|
|
31222
|
-
function updateCookieConsent(tiers) {
|
|
31223
|
-
const enabledProducts = applyConsentTiers(tiers);
|
|
31224
|
-
Events['consent:changed'].trigger(enabledProducts);
|
|
31225
|
-
log.debug('Cookie consent updated for ' + JSON.stringify(tiers) + '.');
|
|
31226
|
-
return enabledProducts;
|
|
31227
|
-
}
|
|
31228
|
-
|
|
31229
31335
|
/**
|
|
31230
31336
|
* Shared helper for event collection
|
|
31231
31337
|
*/
|
|
@@ -42707,6 +42813,310 @@ var guideMarkdownUtil = {
|
|
|
42707
42813
|
removeMarkdownSyntax
|
|
42708
42814
|
};
|
|
42709
42815
|
|
|
42816
|
+
const AUTO_ADVANCE_DELAY_MS = 250;
|
|
42817
|
+
function isAutoAdvanceRequested(step, _) {
|
|
42818
|
+
return _.get(step, 'attributes.autoAdvance') === true;
|
|
42819
|
+
}
|
|
42820
|
+
// Survey-authored DOM carries no question-type classes, so eligibility keys off
|
|
42821
|
+
// answer-control shape: every required poll id must be radio-backed, which also
|
|
42822
|
+
// refuses a future non-radio type by default.
|
|
42823
|
+
function isStepEligibleInDom(step, _, requiredPollIds) {
|
|
42824
|
+
const guideElement = step && step.guideElement;
|
|
42825
|
+
if (!guideElement)
|
|
42826
|
+
return false;
|
|
42827
|
+
if (guideElement.find('textarea, input:text, input:checkbox, select').length)
|
|
42828
|
+
return false;
|
|
42829
|
+
const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
|
|
42830
|
+
if (!pollIds.length)
|
|
42831
|
+
return false;
|
|
42832
|
+
return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
|
|
42833
|
+
guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
|
|
42834
|
+
}
|
|
42835
|
+
function hideSubmitButtons(step, _, submitButtons) {
|
|
42836
|
+
const hiddenButtons = [];
|
|
42837
|
+
_.each(submitButtons, (button) => {
|
|
42838
|
+
const element = step.guideElement.find(`#${button.props.id}`)[0];
|
|
42839
|
+
if (!element)
|
|
42840
|
+
return;
|
|
42841
|
+
// display, not visibility: a reserved-but-invisible box leaves an empty gap
|
|
42842
|
+
// at the foot of the step. Authored styles reach the button inline
|
|
42843
|
+
// (build.js:151), so the original value is kept rather than blanked.
|
|
42844
|
+
hiddenButtons.push({ element, display: element.style.display });
|
|
42845
|
+
element.style.display = 'none';
|
|
42846
|
+
});
|
|
42847
|
+
return function restoreSubmitButtons() {
|
|
42848
|
+
_.each(hiddenButtons, (hiddenButton) => {
|
|
42849
|
+
hiddenButton.element.style.display = hiddenButton.display;
|
|
42850
|
+
});
|
|
42851
|
+
};
|
|
42852
|
+
}
|
|
42853
|
+
// The container's pixel height is measured during render (layout.js) before custom
|
|
42854
|
+
// scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
|
|
42855
|
+
// the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
|
|
42856
|
+
// a second guide on screen is left alone.
|
|
42857
|
+
function reflowGuide(step, _, pendo) {
|
|
42858
|
+
const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
|
|
42859
|
+
try {
|
|
42860
|
+
if (_.isFunction(pendo.flexElement)) {
|
|
42861
|
+
pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
|
|
42862
|
+
}
|
|
42863
|
+
if (_.isFunction(recalculateGuideHeight)) {
|
|
42864
|
+
recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
|
|
42865
|
+
}
|
|
42866
|
+
}
|
|
42867
|
+
catch (error) {
|
|
42868
|
+
pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
|
|
42869
|
+
}
|
|
42870
|
+
}
|
|
42871
|
+
function createInertController() {
|
|
42872
|
+
return { isActive: false, notifyAnswered() { }, cancel() { } };
|
|
42873
|
+
}
|
|
42874
|
+
function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
|
|
42875
|
+
const _ = pendo._;
|
|
42876
|
+
const inert = createInertController();
|
|
42877
|
+
if (!isAutoAdvanceRequested(step, _))
|
|
42878
|
+
return inert;
|
|
42879
|
+
// Only reachable for API-authored guides; choosing between two
|
|
42880
|
+
// submitPollAndGoToStep targets would be a guess.
|
|
42881
|
+
if (_.size(submitButtons) !== 1) {
|
|
42882
|
+
pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
|
|
42883
|
+
return inert;
|
|
42884
|
+
}
|
|
42885
|
+
if (!isStepEligibleInDom(step, _, requiredPollIds)) {
|
|
42886
|
+
pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
|
|
42887
|
+
return inert;
|
|
42888
|
+
}
|
|
42889
|
+
const submitButton = submitButtons[0];
|
|
42890
|
+
// A click binds every action on the node and dispatches one event each
|
|
42891
|
+
// (build.js:205-207); we dispatch one, so a second action would be dropped.
|
|
42892
|
+
if (_.size(submitButton.actions) !== 1) {
|
|
42893
|
+
pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
|
|
42894
|
+
return inert;
|
|
42895
|
+
}
|
|
42896
|
+
const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
|
|
42897
|
+
if (!submitAction)
|
|
42898
|
+
return inert;
|
|
42899
|
+
let hasFired = false;
|
|
42900
|
+
let restoreSubmitButtons = _.noop;
|
|
42901
|
+
function fire() {
|
|
42902
|
+
if (hasFired)
|
|
42903
|
+
return;
|
|
42904
|
+
hasFired = true;
|
|
42905
|
+
try {
|
|
42906
|
+
// Shape-identical to what bindActionToNode emits for a click, so
|
|
42907
|
+
// beforeAdvance consumers intercept an auto-advance the same way.
|
|
42908
|
+
step.eventRouter.eventable.trigger('pendoEvent', {
|
|
42909
|
+
action: submitAction.action,
|
|
42910
|
+
params: submitAction.parameters,
|
|
42911
|
+
step,
|
|
42912
|
+
ignore: !!submitAction.ignore,
|
|
42913
|
+
srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
|
|
42914
|
+
});
|
|
42915
|
+
}
|
|
42916
|
+
catch (error) {
|
|
42917
|
+
restoreSubmitButtons();
|
|
42918
|
+
reflowGuide(step, _, pendo);
|
|
42919
|
+
pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
|
|
42920
|
+
}
|
|
42921
|
+
}
|
|
42922
|
+
const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
|
|
42923
|
+
// Last, so nothing between here and the return can throw and leave the step
|
|
42924
|
+
// with a hidden button and no controller to un-hide it.
|
|
42925
|
+
restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
|
|
42926
|
+
reflowGuide(step, _, pendo);
|
|
42927
|
+
return {
|
|
42928
|
+
isActive: true,
|
|
42929
|
+
notifyAnswered: debouncedFire,
|
|
42930
|
+
cancel: debouncedFire.cancel
|
|
42931
|
+
};
|
|
42932
|
+
}
|
|
42933
|
+
|
|
42934
|
+
const SURVEY_ANSWERS_KEY = 'pendo_survey_answers';
|
|
42935
|
+
const ANSWER_CONTROLS = 'input:radio, input:text, textarea, select';
|
|
42936
|
+
const TEXT_CONTROLS = 'input:text, textarea';
|
|
42937
|
+
// Matches MULTISTEP_CONTINUATION_TIME_LIMIT (walkthrough-guide.js:67), past which a guide is not continued.
|
|
42938
|
+
const ANSWER_TTL_MS = 12 * 60 * 60 * 1000;
|
|
42939
|
+
let pluginApi = null;
|
|
42940
|
+
let inMemoryAnswers = {};
|
|
42941
|
+
let persistedAnswersDropped = false;
|
|
42942
|
+
// A restricted storage.allowKeys silently rejects the session write, so mirror the visit here.
|
|
42943
|
+
const visitedSteps = {};
|
|
42944
|
+
function setPluginApi(api) {
|
|
42945
|
+
pluginApi = api;
|
|
42946
|
+
if (!api || !api.agentStorage)
|
|
42947
|
+
return;
|
|
42948
|
+
/**
|
|
42949
|
+
* Answers the visitor has given on each step of a survey, so a step re-shown
|
|
42950
|
+
* after Previous comes back filled in rather than blank.
|
|
42951
|
+
*
|
|
42952
|
+
* @name pendo_survey_answers
|
|
42953
|
+
* @category Cookies/sessionStorage
|
|
42954
|
+
* @access public
|
|
42955
|
+
* @label SURVEY_ANSWERS_KEY
|
|
42956
|
+
*/
|
|
42957
|
+
api.agentStorage.registry.addSession(SURVEY_ANSWERS_KEY);
|
|
42958
|
+
}
|
|
42959
|
+
// getSession() checks the allow-list but not storageIsDisabled, which the cookie path applies.
|
|
42960
|
+
function persistenceAllowed() {
|
|
42961
|
+
const allowed = !!pluginApi &&
|
|
42962
|
+
!!pluginApi.sessionStorage &&
|
|
42963
|
+
typeof pluginApi.storageIsDisabled === 'function' &&
|
|
42964
|
+
!pluginApi.storageIsDisabled();
|
|
42965
|
+
if (allowed) {
|
|
42966
|
+
persistedAnswersDropped = false;
|
|
42967
|
+
}
|
|
42968
|
+
else {
|
|
42969
|
+
dropPersistedAnswers();
|
|
42970
|
+
}
|
|
42971
|
+
return allowed;
|
|
42972
|
+
}
|
|
42973
|
+
// agentStorage.clearAll only walks local keys, so a session key has to be dropped here.
|
|
42974
|
+
function dropPersistedAnswers() {
|
|
42975
|
+
if (persistedAnswersDropped || !pluginApi || !pluginApi.sessionStorage)
|
|
42976
|
+
return;
|
|
42977
|
+
persistedAnswersDropped = true;
|
|
42978
|
+
pluginApi.sessionStorage.removeItem(SURVEY_ANSWERS_KEY);
|
|
42979
|
+
}
|
|
42980
|
+
function readAll() {
|
|
42981
|
+
if (!persistenceAllowed())
|
|
42982
|
+
return inMemoryAnswers;
|
|
42983
|
+
try {
|
|
42984
|
+
return JSON.parse(pluginApi.sessionStorage.getItem(SURVEY_ANSWERS_KEY)) || {};
|
|
42985
|
+
}
|
|
42986
|
+
catch (error) {
|
|
42987
|
+
return {};
|
|
42988
|
+
}
|
|
42989
|
+
}
|
|
42990
|
+
function writeAll(answers) {
|
|
42991
|
+
if (!persistenceAllowed()) {
|
|
42992
|
+
inMemoryAnswers = answers;
|
|
42993
|
+
return;
|
|
42994
|
+
}
|
|
42995
|
+
pluginApi.sessionStorage.setItem(SURVEY_ANSWERS_KEY, JSON.stringify(answers));
|
|
42996
|
+
}
|
|
42997
|
+
// identify() sets the new id before the reload tears the step down, so resolve this once per render.
|
|
42998
|
+
function visitorKey(pendo) {
|
|
42999
|
+
return (pendo._.isFunction(pendo.get_visitor_id) && pendo.get_visitor_id()) || 'unknown';
|
|
43000
|
+
}
|
|
43001
|
+
function stateKey(step, guide, visitor) {
|
|
43002
|
+
return `${visitor}:${guide.id}:${step.id}`;
|
|
43003
|
+
}
|
|
43004
|
+
// Host pages reassign Date.now to return a Date, which JSON writes out as a string.
|
|
43005
|
+
function getNow() {
|
|
43006
|
+
return new Date().getTime();
|
|
43007
|
+
}
|
|
43008
|
+
function isFresh(writtenAt) {
|
|
43009
|
+
return !!writtenAt && (getNow() - writtenAt) <= ANSWER_TTL_MS;
|
|
43010
|
+
}
|
|
43011
|
+
function readRecord(step, guide, visitor) {
|
|
43012
|
+
const forVisitor = readAll()[visitor];
|
|
43013
|
+
const forGuide = forVisitor && forVisitor[guide.id];
|
|
43014
|
+
const record = (forGuide && forGuide[step.id]) || null;
|
|
43015
|
+
return isFresh(record && record.at) ? record : null;
|
|
43016
|
+
}
|
|
43017
|
+
function updateRecord(step, guide, visitor, changes) {
|
|
43018
|
+
const all = readAll();
|
|
43019
|
+
const current = readRecord(step, guide, visitor) || {};
|
|
43020
|
+
if (!all[visitor])
|
|
43021
|
+
all[visitor] = {};
|
|
43022
|
+
if (!all[visitor][guide.id])
|
|
43023
|
+
all[visitor][guide.id] = {};
|
|
43024
|
+
all[visitor][guide.id][step.id] = {
|
|
43025
|
+
at: getNow(),
|
|
43026
|
+
visited: changes.visited || current.visited || 0,
|
|
43027
|
+
answers: changes.answers || current.answers || {}
|
|
43028
|
+
};
|
|
43029
|
+
writeAll(all);
|
|
43030
|
+
}
|
|
43031
|
+
// Previous only renders after an auto-advance step, so any other guide needs none of this.
|
|
43032
|
+
function guideAutoAdvances(guide, _) {
|
|
43033
|
+
return _.any(_.get(guide, 'steps', []), (step) => isAutoAdvanceRequested(step, _));
|
|
43034
|
+
}
|
|
43035
|
+
function pollIdFor(control) {
|
|
43036
|
+
let node = control;
|
|
43037
|
+
while (node && node.getAttribute) {
|
|
43038
|
+
const pollId = node.getAttribute('data-pendo-poll-id');
|
|
43039
|
+
if (pollId)
|
|
43040
|
+
return pollId;
|
|
43041
|
+
node = node.parentNode;
|
|
43042
|
+
}
|
|
43043
|
+
return null;
|
|
43044
|
+
}
|
|
43045
|
+
// submitPoll never reads a checkbox back (event-router.js:264), so restoring one shows nothing sent.
|
|
43046
|
+
function isRadio(control) {
|
|
43047
|
+
return control.type === 'radio';
|
|
43048
|
+
}
|
|
43049
|
+
function collectAnswers(step, pendo) {
|
|
43050
|
+
const answers = {};
|
|
43051
|
+
pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
|
|
43052
|
+
const pollId = pollIdFor(control);
|
|
43053
|
+
// One value per poll id, first in document order, as submitPoll reads it.
|
|
43054
|
+
if (!pollId || answers[pollId])
|
|
43055
|
+
return;
|
|
43056
|
+
if (isRadio(control) && !control.checked)
|
|
43057
|
+
return;
|
|
43058
|
+
if (!control.value)
|
|
43059
|
+
return;
|
|
43060
|
+
answers[pollId] = [control.value];
|
|
43061
|
+
});
|
|
43062
|
+
return answers;
|
|
43063
|
+
}
|
|
43064
|
+
// No event dispatched: a restored answer must not reach the gate listeners or re-arm auto-advance.
|
|
43065
|
+
function restoreAnswers(step, pendo, answers) {
|
|
43066
|
+
pendo._.each(step.guideElement.find(ANSWER_CONTROLS), (control) => {
|
|
43067
|
+
const pollId = pollIdFor(control);
|
|
43068
|
+
const values = pollId && answers[pollId];
|
|
43069
|
+
if (!values)
|
|
43070
|
+
return;
|
|
43071
|
+
if (isRadio(control)) {
|
|
43072
|
+
if (pendo._.contains(values, control.value))
|
|
43073
|
+
control.checked = true;
|
|
43074
|
+
return;
|
|
43075
|
+
}
|
|
43076
|
+
control.value = values[0];
|
|
43077
|
+
});
|
|
43078
|
+
}
|
|
43079
|
+
function isRevisit(step, guide, pendo) {
|
|
43080
|
+
if (!guideAutoAdvances(guide, pendo._))
|
|
43081
|
+
return false;
|
|
43082
|
+
const visitor = visitorKey(pendo);
|
|
43083
|
+
if (isFresh(visitedSteps[stateKey(step, guide, visitor)]))
|
|
43084
|
+
return true;
|
|
43085
|
+
const record = readRecord(step, guide, visitor);
|
|
43086
|
+
return !!record && (!!record.visited || !pendo._.isEmpty(record.answers));
|
|
43087
|
+
}
|
|
43088
|
+
const AnswerCache = {
|
|
43089
|
+
name: 'AnswerCache',
|
|
43090
|
+
script(step, guide, pendo) {
|
|
43091
|
+
const visitor = visitorKey(pendo);
|
|
43092
|
+
const record = readRecord(step, guide, visitor);
|
|
43093
|
+
restoreAnswers(step, pendo, (record && record.answers) || {});
|
|
43094
|
+
// Marked on leaving, not on render: sessionStorage survives a reload, which is not a revisit.
|
|
43095
|
+
this.on('beforeUnmount', (event) => {
|
|
43096
|
+
if (event && event.reason === 'hidden')
|
|
43097
|
+
return;
|
|
43098
|
+
visitedSteps[stateKey(step, guide, visitor)] = getNow();
|
|
43099
|
+
updateRecord(step, guide, visitor, { visited: 1 });
|
|
43100
|
+
});
|
|
43101
|
+
const stepRoot = step.guideElement[0];
|
|
43102
|
+
// change does not bubble on legacy browsers; it is here only for keyboard selection.
|
|
43103
|
+
step.attachEvent(stepRoot, 'click', capture);
|
|
43104
|
+
step.attachEvent(stepRoot, 'change', capture);
|
|
43105
|
+
pendo._.each(step.guideElement.find(TEXT_CONTROLS), (control) => {
|
|
43106
|
+
step.attachEvent(control, 'input', capture);
|
|
43107
|
+
});
|
|
43108
|
+
function capture() {
|
|
43109
|
+
updateRecord(step, guide, visitor, { answers: collectAnswers(step, pendo) });
|
|
43110
|
+
}
|
|
43111
|
+
},
|
|
43112
|
+
test(step, guide, pendo) {
|
|
43113
|
+
const guideElement = step && step.guideElement;
|
|
43114
|
+
return !!guideElement &&
|
|
43115
|
+
guideAutoAdvances(guide, pendo._) &&
|
|
43116
|
+
guideElement.find(ANSWER_CONTROLS).length > 0;
|
|
43117
|
+
}
|
|
43118
|
+
};
|
|
43119
|
+
|
|
42710
43120
|
function getZoneSafeMethod(_, method, target = window) {
|
|
42711
43121
|
var zoneSymbol = '__symbol__';
|
|
42712
43122
|
/* global Zone */
|
|
@@ -43183,124 +43593,6 @@ function substitutionIcon(color, size) {
|
|
|
43183
43593
|
13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
|
|
43184
43594
|
}
|
|
43185
43595
|
|
|
43186
|
-
const AUTO_ADVANCE_DELAY_MS = 250;
|
|
43187
|
-
function isAutoAdvanceRequested(step, _) {
|
|
43188
|
-
return _.get(step, 'attributes.autoAdvance') === true;
|
|
43189
|
-
}
|
|
43190
|
-
// Survey-authored DOM carries no question-type classes, so eligibility keys off
|
|
43191
|
-
// answer-control shape: every required poll id must be radio-backed, which also
|
|
43192
|
-
// refuses a future non-radio type by default.
|
|
43193
|
-
function isStepEligibleInDom(step, _, requiredPollIds) {
|
|
43194
|
-
const guideElement = step && step.guideElement;
|
|
43195
|
-
if (!guideElement)
|
|
43196
|
-
return false;
|
|
43197
|
-
if (guideElement.find('textarea, input:text, input:checkbox, select').length)
|
|
43198
|
-
return false;
|
|
43199
|
-
const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
|
|
43200
|
-
if (!pollIds.length)
|
|
43201
|
-
return false;
|
|
43202
|
-
return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
|
|
43203
|
-
guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
|
|
43204
|
-
}
|
|
43205
|
-
function hideSubmitButtons(step, _, submitButtons) {
|
|
43206
|
-
const hiddenButtons = [];
|
|
43207
|
-
_.each(submitButtons, (button) => {
|
|
43208
|
-
const element = step.guideElement.find(`#${button.props.id}`)[0];
|
|
43209
|
-
if (!element)
|
|
43210
|
-
return;
|
|
43211
|
-
// display, not visibility: a reserved-but-invisible box leaves an empty gap
|
|
43212
|
-
// at the foot of the step. Authored styles reach the button inline
|
|
43213
|
-
// (build.js:151), so the original value is kept rather than blanked.
|
|
43214
|
-
hiddenButtons.push({ element, display: element.style.display });
|
|
43215
|
-
element.style.display = 'none';
|
|
43216
|
-
});
|
|
43217
|
-
return function restoreSubmitButtons() {
|
|
43218
|
-
_.each(hiddenButtons, (hiddenButton) => {
|
|
43219
|
-
hiddenButton.element.style.display = hiddenButton.display;
|
|
43220
|
-
});
|
|
43221
|
-
};
|
|
43222
|
-
}
|
|
43223
|
-
// The container's pixel height is measured during render (layout.js) before custom
|
|
43224
|
-
// scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
|
|
43225
|
-
// the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
|
|
43226
|
-
// a second guide on screen is left alone.
|
|
43227
|
-
function reflowGuide(step, _, pendo) {
|
|
43228
|
-
const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
|
|
43229
|
-
try {
|
|
43230
|
-
if (_.isFunction(pendo.flexElement)) {
|
|
43231
|
-
pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
|
|
43232
|
-
}
|
|
43233
|
-
if (_.isFunction(recalculateGuideHeight)) {
|
|
43234
|
-
recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
|
|
43235
|
-
}
|
|
43236
|
-
}
|
|
43237
|
-
catch (error) {
|
|
43238
|
-
pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
|
|
43239
|
-
}
|
|
43240
|
-
}
|
|
43241
|
-
function createInertController() {
|
|
43242
|
-
return { isActive: false, notifyAnswered() { }, cancel() { } };
|
|
43243
|
-
}
|
|
43244
|
-
function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
|
|
43245
|
-
const _ = pendo._;
|
|
43246
|
-
const inert = createInertController();
|
|
43247
|
-
if (!isAutoAdvanceRequested(step, _))
|
|
43248
|
-
return inert;
|
|
43249
|
-
// Only reachable for API-authored guides; choosing between two
|
|
43250
|
-
// submitPollAndGoToStep targets would be a guess.
|
|
43251
|
-
if (_.size(submitButtons) !== 1) {
|
|
43252
|
-
pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
|
|
43253
|
-
return inert;
|
|
43254
|
-
}
|
|
43255
|
-
if (!isStepEligibleInDom(step, _, requiredPollIds)) {
|
|
43256
|
-
pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
|
|
43257
|
-
return inert;
|
|
43258
|
-
}
|
|
43259
|
-
const submitButton = submitButtons[0];
|
|
43260
|
-
// A click binds every action on the node and dispatches one event each
|
|
43261
|
-
// (build.js:205-207); we dispatch one, so a second action would be dropped.
|
|
43262
|
-
if (_.size(submitButton.actions) !== 1) {
|
|
43263
|
-
pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
|
|
43264
|
-
return inert;
|
|
43265
|
-
}
|
|
43266
|
-
const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
|
|
43267
|
-
if (!submitAction)
|
|
43268
|
-
return inert;
|
|
43269
|
-
let hasFired = false;
|
|
43270
|
-
let restoreSubmitButtons = _.noop;
|
|
43271
|
-
function fire() {
|
|
43272
|
-
if (hasFired)
|
|
43273
|
-
return;
|
|
43274
|
-
hasFired = true;
|
|
43275
|
-
try {
|
|
43276
|
-
// Shape-identical to what bindActionToNode emits for a click, so
|
|
43277
|
-
// beforeAdvance consumers intercept an auto-advance the same way.
|
|
43278
|
-
step.eventRouter.eventable.trigger('pendoEvent', {
|
|
43279
|
-
action: submitAction.action,
|
|
43280
|
-
params: submitAction.parameters,
|
|
43281
|
-
step,
|
|
43282
|
-
ignore: !!submitAction.ignore,
|
|
43283
|
-
srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
|
|
43284
|
-
});
|
|
43285
|
-
}
|
|
43286
|
-
catch (error) {
|
|
43287
|
-
restoreSubmitButtons();
|
|
43288
|
-
reflowGuide(step, _, pendo);
|
|
43289
|
-
pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
|
|
43290
|
-
}
|
|
43291
|
-
}
|
|
43292
|
-
const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
|
|
43293
|
-
// Last, so nothing between here and the return can throw and leave the step
|
|
43294
|
-
// with a hidden button and no controller to un-hide it.
|
|
43295
|
-
restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
|
|
43296
|
-
reflowGuide(step, _, pendo);
|
|
43297
|
-
return {
|
|
43298
|
-
isActive: true,
|
|
43299
|
-
notifyAnswered: debouncedFire,
|
|
43300
|
-
cancel: debouncedFire.cancel
|
|
43301
|
-
};
|
|
43302
|
-
}
|
|
43303
|
-
|
|
43304
43596
|
const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
|
|
43305
43597
|
const requiredSyntax = '{required/}';
|
|
43306
43598
|
const RequiredQuestions = {
|
|
@@ -43326,9 +43618,14 @@ const RequiredQuestions = {
|
|
|
43326
43618
|
}
|
|
43327
43619
|
});
|
|
43328
43620
|
}
|
|
43621
|
+
// Restored answers reach no listener, and the controller is still inert here.
|
|
43622
|
+
const revisited = isRevisit(step, guide, pendo);
|
|
43623
|
+
if (revisited) {
|
|
43624
|
+
evaluateRequiredQuestions(requiredQuestions);
|
|
43625
|
+
}
|
|
43329
43626
|
// Armed last, after the listeners above: runCustomScripts abandons the rest of
|
|
43330
43627
|
// this script on a throw, and the submit button is already disabled by now.
|
|
43331
|
-
if (isAutoAdvanceRequested(step, pendo._)) {
|
|
43628
|
+
if (isAutoAdvanceRequested(step, pendo._) && !revisited) {
|
|
43332
43629
|
this.on('beforeUnmount', () => autoAdvance.cancel());
|
|
43333
43630
|
try {
|
|
43334
43631
|
autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
|
|
@@ -43679,7 +43976,9 @@ function GuideMarkdown() {
|
|
|
43679
43976
|
const markdownScriptsEnabled = configReader.get(GUIDEMARKDOWN_CONFIG);
|
|
43680
43977
|
if (!markdownScriptsEnabled)
|
|
43681
43978
|
return;
|
|
43682
|
-
|
|
43979
|
+
setPluginApi(PluginAPI);
|
|
43980
|
+
// AnswerCache first: it restores the answers RequiredQuestions reads the gate against.
|
|
43981
|
+
guideMarkdown = [AnswerCache, PollBranching, MetadataSubstitution, RequiredQuestions, SkipToEligibleStep];
|
|
43683
43982
|
if (!pendo.designer) {
|
|
43684
43983
|
pendo._.each(guideMarkdown, function (script) {
|
|
43685
43984
|
PluginAPI.GlobalRuntime.addGlobalScript(script);
|
|
@@ -51470,11 +51769,13 @@ class Transport {
|
|
|
51470
51769
|
}
|
|
51471
51770
|
}
|
|
51472
51771
|
|
|
51473
|
-
const CATCHALL_TYPES = ['load', 'click', 'track'];
|
|
51772
|
+
const CATCHALL_TYPES = ['load', 'click', 'track', 'guideSeen'];
|
|
51773
|
+
const AGENTIC_TYPES = ["prompt", "agent_response", "user_reaction"];
|
|
51474
51774
|
const EVENT_KINDS = {
|
|
51475
|
-
|
|
51476
|
-
|
|
51477
|
-
|
|
51775
|
+
AGENTIC: 'agentic',
|
|
51776
|
+
CATCHALL: 'catchall',
|
|
51777
|
+
DEVLOG: 'devlog',
|
|
51778
|
+
RECORDING: 'recording'
|
|
51478
51779
|
};
|
|
51479
51780
|
class LiveReplayClient {
|
|
51480
51781
|
constructor(pendo, api) {
|
|
@@ -51555,18 +51856,21 @@ class LiveReplayClient {
|
|
|
51555
51856
|
catch (_a) { }
|
|
51556
51857
|
}
|
|
51557
51858
|
sendRecordingEvent(fields) {
|
|
51558
|
-
this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.
|
|
51859
|
+
this.send(Object.assign(Object.assign({}, fields), { kind: EVENT_KINDS.RECORDING, sequence: this.sequence++ }));
|
|
51559
51860
|
}
|
|
51560
51861
|
forwardEvent(event) {
|
|
51561
51862
|
if (!event || !event.data || !event.data.length)
|
|
51562
51863
|
return;
|
|
51563
51864
|
const capturedEvent = event.data[0];
|
|
51564
51865
|
const config = { event: capturedEvent };
|
|
51866
|
+
if (this.pendo._.contains(AGENTIC_TYPES, capturedEvent.type)) {
|
|
51867
|
+
config.kind = EVENT_KINDS.AGENTIC;
|
|
51868
|
+
}
|
|
51565
51869
|
if (this.pendo._.contains(CATCHALL_TYPES, capturedEvent.type)) {
|
|
51566
|
-
config.kind = EVENT_KINDS.
|
|
51870
|
+
config.kind = EVENT_KINDS.CATCHALL;
|
|
51567
51871
|
}
|
|
51568
|
-
if (capturedEvent.type === EVENT_KINDS.
|
|
51569
|
-
config.kind = EVENT_KINDS.
|
|
51872
|
+
if (capturedEvent.type === EVENT_KINDS.DEVLOG) {
|
|
51873
|
+
config.kind = EVENT_KINDS.DEVLOG;
|
|
51570
51874
|
}
|
|
51571
51875
|
if (!config.kind)
|
|
51572
51876
|
return;
|
|
@@ -52153,7 +52457,6 @@ class SessionRecorder {
|
|
|
52153
52457
|
var bind = this.pendo._.bind;
|
|
52154
52458
|
this.visitorConfig = visitorConfig;
|
|
52155
52459
|
this.clearOldSessionId();
|
|
52156
|
-
this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId);
|
|
52157
52460
|
this.sendQueue.start();
|
|
52158
52461
|
this.interval = setInterval(() => {
|
|
52159
52462
|
if (!this.sendQueue.failed()) {
|
|
@@ -52166,6 +52469,7 @@ class SessionRecorder {
|
|
|
52166
52469
|
errorHandler: this.errorHandler
|
|
52167
52470
|
}, config));
|
|
52168
52471
|
this._refreshIds();
|
|
52472
|
+
this.liveReplayClient.onLiveFlag(visitorConfig.live, this.visitorId, () => this.snapshot());
|
|
52169
52473
|
this.onRecordingStart();
|
|
52170
52474
|
}
|
|
52171
52475
|
catch (e) {
|