@pendo/agent 2.340.0 → 2.342.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 +13 -1
- package/dist/pendo.module.js +562 -324
- package/dist/pendo.module.min.js +14 -14
- 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.342.0_';
|
|
4062
|
+
let PACKAGE_VERSION = '2.342.0';
|
|
4051
4063
|
let LOADER = 'xhr';
|
|
4052
4064
|
/* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
|
|
4053
4065
|
/**
|
|
@@ -14305,6 +14317,334 @@ var XHRGuideLoader = {
|
|
|
14305
14317
|
}
|
|
14306
14318
|
};
|
|
14307
14319
|
|
|
14320
|
+
function base32Encode(uint8array) {
|
|
14321
|
+
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
14322
|
+
|
|
14323
|
+
var length = uint8array.length;
|
|
14324
|
+
|
|
14325
|
+
var bits = 0;
|
|
14326
|
+
var value = 0;
|
|
14327
|
+
var output = '';
|
|
14328
|
+
|
|
14329
|
+
for (var i = 0; i < length; i++) {
|
|
14330
|
+
value = (value << 8) | uint8array[i];
|
|
14331
|
+
bits += 8;
|
|
14332
|
+
|
|
14333
|
+
while (bits >= 5) {
|
|
14334
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
14335
|
+
bits -= 5;
|
|
14336
|
+
}
|
|
14337
|
+
}
|
|
14338
|
+
|
|
14339
|
+
if (bits > 0) {
|
|
14340
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
14341
|
+
}
|
|
14342
|
+
|
|
14343
|
+
return output;
|
|
14344
|
+
}
|
|
14345
|
+
|
|
14346
|
+
// A CNAME contentHost is commonly configured without a scheme, which would leave the rewritten
|
|
14347
|
+
// url relative to the current page rather than absolute. Leading-slash values are deliberate and
|
|
14348
|
+
// kept as-is: '//host' is protocol-relative, '/path' is same-origin (see fixContentHostUrl).
|
|
14349
|
+
function normalizeContentHost(contentHost) {
|
|
14350
|
+
if (/:\/\//.test(contentHost) || /^\//.test(contentHost))
|
|
14351
|
+
return contentHost;
|
|
14352
|
+
return 'https://' + contentHost;
|
|
14353
|
+
}
|
|
14354
|
+
function stripScheme(host) {
|
|
14355
|
+
return host.replace(/^https?:\/\//, '');
|
|
14356
|
+
}
|
|
14357
|
+
// Self hosted installs aim assetHost at their own host, or drop it entirely - the npm package
|
|
14358
|
+
// deletes it for the assets option - so it cannot locate our CDN. SERVER survives either way, and
|
|
14359
|
+
// app, cdn and static are sibling labels in every environment's domain template. Builds that point
|
|
14360
|
+
// SERVER somewhere other than an app host (--env=local) are left with assetHost.
|
|
14361
|
+
function getPendoCdnHost() {
|
|
14362
|
+
var appLabel = /(^|\.)app\./;
|
|
14363
|
+
var serverHost = stripScheme(SERVER);
|
|
14364
|
+
if (!appLabel.test(serverHost))
|
|
14365
|
+
return stripScheme(ASSET_HOST);
|
|
14366
|
+
return serverHost.replace(appLabel, '$1cdn.');
|
|
14367
|
+
}
|
|
14368
|
+
// A contentHost set up as a CNAME in app settings is delivered in the hosted config, while a
|
|
14369
|
+
// snippet value is the customer's own override of it. Same precedence getDataHost uses.
|
|
14370
|
+
function getConfiguredContentHost() {
|
|
14371
|
+
return ConfigReader.getLocalConfig('contentHost') || ConfigReader.getHostedConfig('contentHost');
|
|
14372
|
+
}
|
|
14373
|
+
function replaceWithContentHost(str) {
|
|
14374
|
+
var contentHost = getConfiguredContentHost();
|
|
14375
|
+
if (!contentHost || !str)
|
|
14376
|
+
return str;
|
|
14377
|
+
contentHost = normalizeContentHost(contentHost);
|
|
14378
|
+
var cdnHost = getPendoCdnHost();
|
|
14379
|
+
var assetHost = stripScheme(ASSET_HOST);
|
|
14380
|
+
// Content proxy hosts are the environment's domain with static in the variable label, which is
|
|
14381
|
+
// not always the leading one - us1 is content-<subId>.us1.static.pendo.io
|
|
14382
|
+
var staticHost = cdnHost.replace(/(^|\.)cdn\./, '$1static.');
|
|
14383
|
+
// An unknown host has to be dropped rather than interpolated: an empty one leaves a pattern
|
|
14384
|
+
// that matches the // of every absolute url
|
|
14385
|
+
var hostPatterns = [
|
|
14386
|
+
SUBSCRIPTION_BUCKET && `${escapeRegExp(SUBSCRIPTION_BUCKET)}-\\d+\\.storage\\.googleapis\\.com`,
|
|
14387
|
+
GLOBAL_BUCKET && `${escapeRegExp(GLOBAL_BUCKET)}\\.storage\\.googleapis\\.com`,
|
|
14388
|
+
cdnHost && `content-\\d+\\.${escapeRegExp(staticHost)}`,
|
|
14389
|
+
cdnHost && escapeRegExp(cdnHost),
|
|
14390
|
+
assetHost && assetHost !== cdnHost && escapeRegExp(assetHost)
|
|
14391
|
+
];
|
|
14392
|
+
return _.reduce(hostPatterns, function (rewritten, hostPattern) {
|
|
14393
|
+
if (!hostPattern)
|
|
14394
|
+
return rewritten;
|
|
14395
|
+
return rewritten.replace(new RegExp(`(https:)?\\/\\/${hostPattern}`, 'g'), contentHost);
|
|
14396
|
+
}, str);
|
|
14397
|
+
}
|
|
14398
|
+
function replaceObjectWithContentHost(obj) {
|
|
14399
|
+
var contentHost = getConfiguredContentHost();
|
|
14400
|
+
if (!contentHost)
|
|
14401
|
+
return obj;
|
|
14402
|
+
return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
|
|
14403
|
+
}
|
|
14404
|
+
// APP-37205 we're only checking the remote contentHost config setting
|
|
14405
|
+
function getContentHostRegex() {
|
|
14406
|
+
var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
|
|
14407
|
+
if (ch)
|
|
14408
|
+
return ch + '*';
|
|
14409
|
+
}
|
|
14410
|
+
/**
|
|
14411
|
+
* @access private
|
|
14412
|
+
* @param {Function} isValidGuideResponse
|
|
14413
|
+
* @param {object} guideResponse
|
|
14414
|
+
*/
|
|
14415
|
+
function isValidGuideResponse(guideResponse) {
|
|
14416
|
+
// Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
|
|
14417
|
+
// prop, we should also return false.
|
|
14418
|
+
if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
|
|
14419
|
+
guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
|
|
14420
|
+
return false;
|
|
14421
|
+
}
|
|
14422
|
+
return true;
|
|
14423
|
+
}
|
|
14424
|
+
var ContentLoader = (function (pendo) {
|
|
14425
|
+
var cache = {};
|
|
14426
|
+
// For APP-37205, we're verifing that the result is what it is. I know.
|
|
14427
|
+
var replaceWithContentHostAndVerify = _.compose(function (url) {
|
|
14428
|
+
var test = getContentHostRegex();
|
|
14429
|
+
if (test) {
|
|
14430
|
+
var v = new RegExp(test);
|
|
14431
|
+
if (!v.test(url)) {
|
|
14432
|
+
log.debug('Failed to verify URL: ' + url + ' vs ' + test);
|
|
14433
|
+
}
|
|
14434
|
+
}
|
|
14435
|
+
return url;
|
|
14436
|
+
}, replaceWithContentHost);
|
|
14437
|
+
/**
|
|
14438
|
+
* @access private
|
|
14439
|
+
* @typedef {object} ContentRequest
|
|
14440
|
+
* @param {string} id uniquely identifies the container
|
|
14441
|
+
* @param {string} contentUrl
|
|
14442
|
+
* @param {string} contentUrlCss
|
|
14443
|
+
* @param {string} contentUrlJs
|
|
14444
|
+
*/
|
|
14445
|
+
/**
|
|
14446
|
+
* @access private
|
|
14447
|
+
* @typedef {object} ContentEntry
|
|
14448
|
+
* @param {string} content The raw content as a string
|
|
14449
|
+
* @param {Function} template The template function compiled from the raw content
|
|
14450
|
+
* @param {Function} script The javascript to execute
|
|
14451
|
+
*/
|
|
14452
|
+
/**
|
|
14453
|
+
* Loads the content for the given container
|
|
14454
|
+
*
|
|
14455
|
+
* @access private
|
|
14456
|
+
* @param {ContentRequest} contentContainer
|
|
14457
|
+
* @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
|
|
14458
|
+
*/
|
|
14459
|
+
function load(contentContainer) {
|
|
14460
|
+
try {
|
|
14461
|
+
var cacheEntry = cache[contentContainer.id];
|
|
14462
|
+
if (cacheEntry && cacheEntry.language !== contentContainer.language) {
|
|
14463
|
+
delete cache[contentContainer.id];
|
|
14464
|
+
}
|
|
14465
|
+
if (!cache[contentContainer.id]) {
|
|
14466
|
+
var promises = [];
|
|
14467
|
+
var entry = {
|
|
14468
|
+
deferred: {},
|
|
14469
|
+
language: contentContainer.language
|
|
14470
|
+
};
|
|
14471
|
+
var fetchedDomContent = false;
|
|
14472
|
+
cache[contentContainer.id] = entry;
|
|
14473
|
+
if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
|
|
14474
|
+
entry.deferred.domJson = q.defer();
|
|
14475
|
+
ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
|
|
14476
|
+
entry.domJson = replaceObjectWithContentHost(result.data);
|
|
14477
|
+
var guideResponse = result.data;
|
|
14478
|
+
if (isValidGuideResponse(guideResponse)) {
|
|
14479
|
+
entry.deferred.domJson.resolve();
|
|
14480
|
+
}
|
|
14481
|
+
else {
|
|
14482
|
+
// Log that the guide was invalid
|
|
14483
|
+
// handle promise
|
|
14484
|
+
log.critical(new Error('Response from the guide was not in proper JSON format'));
|
|
14485
|
+
entry.deferred.domJson.reject();
|
|
14486
|
+
}
|
|
14487
|
+
}).catch(function (e) {
|
|
14488
|
+
log.critical(e);
|
|
14489
|
+
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14490
|
+
entry.deferred.domJson.reject('Failed to load content');
|
|
14491
|
+
});
|
|
14492
|
+
promises.push(entry.deferred.domJson.promise);
|
|
14493
|
+
fetchedDomContent = true;
|
|
14494
|
+
}
|
|
14495
|
+
else if (contentContainer.domJsonpUrl) {
|
|
14496
|
+
entry.deferred.domJson = q.defer();
|
|
14497
|
+
var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
|
|
14498
|
+
dom.removeNode(jsonpNode);
|
|
14499
|
+
if (err) {
|
|
14500
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14501
|
+
return;
|
|
14502
|
+
}
|
|
14503
|
+
}, true);
|
|
14504
|
+
promises.push(entry.deferred.domJson.promise);
|
|
14505
|
+
fetchedDomContent = true;
|
|
14506
|
+
}
|
|
14507
|
+
if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
|
|
14508
|
+
// Content, template, and script for 2.1+ agents
|
|
14509
|
+
entry.deferred.content = q.defer();
|
|
14510
|
+
var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
|
|
14511
|
+
dom.removeNode(scriptNode);
|
|
14512
|
+
if (err) {
|
|
14513
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14514
|
+
return;
|
|
14515
|
+
}
|
|
14516
|
+
}, true);
|
|
14517
|
+
promises.push(entry.deferred.content.promise);
|
|
14518
|
+
// Style
|
|
14519
|
+
if (contentContainer.contentUrlCss) {
|
|
14520
|
+
entry.deferred.css = q.defer();
|
|
14521
|
+
loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
|
|
14522
|
+
entry.deferred.css.resolve();
|
|
14523
|
+
});
|
|
14524
|
+
promises.push(entry.deferred.css.promise);
|
|
14525
|
+
}
|
|
14526
|
+
}
|
|
14527
|
+
else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
|
|
14528
|
+
// Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
|
|
14529
|
+
entry.deferred.content = q.defer();
|
|
14530
|
+
var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
|
|
14531
|
+
dom.removeNode(contentNode);
|
|
14532
|
+
if (err) {
|
|
14533
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14534
|
+
return;
|
|
14535
|
+
}
|
|
14536
|
+
}, true);
|
|
14537
|
+
promises.push(entry.deferred.content.promise);
|
|
14538
|
+
}
|
|
14539
|
+
else if (!fetchedDomContent) {
|
|
14540
|
+
return q.reject('fetchedDomContent is still false');
|
|
14541
|
+
}
|
|
14542
|
+
entry.deferred.promise = q.all(promises).then(function () {
|
|
14543
|
+
return _.omit(entry, 'deferred');
|
|
14544
|
+
});
|
|
14545
|
+
}
|
|
14546
|
+
return cache[contentContainer.id].deferred.promise;
|
|
14547
|
+
}
|
|
14548
|
+
catch (e) {
|
|
14549
|
+
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14550
|
+
throw e;
|
|
14551
|
+
}
|
|
14552
|
+
}
|
|
14553
|
+
// Clears the content cache. Used for testing.
|
|
14554
|
+
function reset() {
|
|
14555
|
+
cache = {};
|
|
14556
|
+
}
|
|
14557
|
+
/**
|
|
14558
|
+
* JSONP-style callback for loading guide content
|
|
14559
|
+
* from script resources. Step content stored in
|
|
14560
|
+
* GCS is wrapped with pendo.guideContent(...)
|
|
14561
|
+
*
|
|
14562
|
+
* @access private
|
|
14563
|
+
* @param {string} guideId
|
|
14564
|
+
* @param {string} stepId
|
|
14565
|
+
* @param {string} content
|
|
14566
|
+
* @param {Function} templateFn
|
|
14567
|
+
* @param {Function} scriptFn
|
|
14568
|
+
*/
|
|
14569
|
+
function guideContent(guideId, stepId, content, templateFn, scriptFn) {
|
|
14570
|
+
if (_.isString(content)) {
|
|
14571
|
+
content = preprocessGuideContent(content, guideId, stepId);
|
|
14572
|
+
if (!_.isFunction(templateFn)) {
|
|
14573
|
+
if (shouldCreateTemplate(content)) {
|
|
14574
|
+
templateFn = _.template(content);
|
|
14575
|
+
}
|
|
14576
|
+
else {
|
|
14577
|
+
templateFn = _.constant(content);
|
|
14578
|
+
}
|
|
14579
|
+
}
|
|
14580
|
+
var entry = cache[guideId + stepId];
|
|
14581
|
+
if (entry && entry.deferred.content) {
|
|
14582
|
+
entry.content = content;
|
|
14583
|
+
entry.template = templateFn;
|
|
14584
|
+
entry.script = scriptFn;
|
|
14585
|
+
entry.deferred.content.resolve();
|
|
14586
|
+
}
|
|
14587
|
+
}
|
|
14588
|
+
}
|
|
14589
|
+
// preprocess content to remove simple guide and step IDs templating
|
|
14590
|
+
function removeStandardTemplating(content, guideId, stepId) {
|
|
14591
|
+
const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
|
|
14592
|
+
var guide = pendo.findGuideById('<%= guide.id %>');
|
|
14593
|
+
var step = guide && guide.findStepById('<%= step.id %>');
|
|
14594
|
+
<% } %>`;
|
|
14595
|
+
const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
|
|
14596
|
+
var step = guide && guide.findStepById('${stepId}');`;
|
|
14597
|
+
return content.replace(stdTemplating, sansTemplating);
|
|
14598
|
+
}
|
|
14599
|
+
// execute all content preprocessors and return end result
|
|
14600
|
+
const GuideContentProcessorList = [removeStandardTemplating];
|
|
14601
|
+
function preprocessGuideContent(content, guideId, stepId) {
|
|
14602
|
+
return _.reduce(GuideContentProcessorList, (acc, procFn) => {
|
|
14603
|
+
return procFn(acc, guideId, stepId);
|
|
14604
|
+
}, content);
|
|
14605
|
+
}
|
|
14606
|
+
function shouldCreateTemplate(content) {
|
|
14607
|
+
return /<%/.test(content) && /%>/.test(content);
|
|
14608
|
+
}
|
|
14609
|
+
function receiveDomStructureJson(guideId, stepId, domJson) {
|
|
14610
|
+
var entry = cache[guideId + stepId];
|
|
14611
|
+
if (entry && entry.deferred.domJson) {
|
|
14612
|
+
entry.domJson = replaceObjectWithContentHost(domJson);
|
|
14613
|
+
entry.deferred.domJson.resolve();
|
|
14614
|
+
}
|
|
14615
|
+
}
|
|
14616
|
+
function lightningRedirect(url) {
|
|
14617
|
+
if (isSfdcLightning()) {
|
|
14618
|
+
// https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
|
|
14619
|
+
var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
|
|
14620
|
+
var guideId = path[0];
|
|
14621
|
+
/* global $A */
|
|
14622
|
+
var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
|
|
14623
|
+
if (_.isString(resourcePath)) {
|
|
14624
|
+
return resourcePath + '/' + path.join('/');
|
|
14625
|
+
}
|
|
14626
|
+
}
|
|
14627
|
+
return url;
|
|
14628
|
+
}
|
|
14629
|
+
function addExtensionToUrl(url, extension) {
|
|
14630
|
+
if (!_.isString(url))
|
|
14631
|
+
return url;
|
|
14632
|
+
var i = _.indexOf(url, '?');
|
|
14633
|
+
if (i < 0) {
|
|
14634
|
+
return url + extension;
|
|
14635
|
+
}
|
|
14636
|
+
else {
|
|
14637
|
+
return url.substring(0, i) + extension + url.substring(i);
|
|
14638
|
+
}
|
|
14639
|
+
}
|
|
14640
|
+
return {
|
|
14641
|
+
load,
|
|
14642
|
+
reset,
|
|
14643
|
+
guideContent,
|
|
14644
|
+
receiveDomStructureJson
|
|
14645
|
+
};
|
|
14646
|
+
})();
|
|
14647
|
+
|
|
14308
14648
|
function getHashFromContentUrl(url) {
|
|
14309
14649
|
var contentUrl = parseUrl(url);
|
|
14310
14650
|
var sha1Length = 27;
|
|
@@ -14391,10 +14731,12 @@ var ContentVerifier = (function () {
|
|
|
14391
14731
|
if (!_.isString(url))
|
|
14392
14732
|
return q.reject('unable to parse "' + url + '"');
|
|
14393
14733
|
var stepHash = computeStepHash(contentContainer, urlProp);
|
|
14394
|
-
|
|
14734
|
+
// Verify against the host the loader fetches from; the hash still comes from the original url.
|
|
14735
|
+
var fetchUrl = replaceWithContentHost(url);
|
|
14736
|
+
return ajax.get(fetchUrl).then(function (response) {
|
|
14395
14737
|
if (computeHashFromFile(response.data) === stepHash)
|
|
14396
14738
|
return;
|
|
14397
|
-
return q.reject('Unable to verify content at "' +
|
|
14739
|
+
return q.reject('Unable to verify content at "' + fetchUrl + '"');
|
|
14398
14740
|
});
|
|
14399
14741
|
}
|
|
14400
14742
|
function verifyContentHashUnlessSri(contentContainer, urlProp) {
|
|
@@ -14669,297 +15011,6 @@ function GuideStepFactory(step, guide) {
|
|
|
14669
15011
|
GuideFactory.behaviors = [];
|
|
14670
15012
|
GuideStepFactory.behaviors = [];
|
|
14671
15013
|
|
|
14672
|
-
function base32Encode(uint8array) {
|
|
14673
|
-
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
14674
|
-
|
|
14675
|
-
var length = uint8array.length;
|
|
14676
|
-
|
|
14677
|
-
var bits = 0;
|
|
14678
|
-
var value = 0;
|
|
14679
|
-
var output = '';
|
|
14680
|
-
|
|
14681
|
-
for (var i = 0; i < length; i++) {
|
|
14682
|
-
value = (value << 8) | uint8array[i];
|
|
14683
|
-
bits += 8;
|
|
14684
|
-
|
|
14685
|
-
while (bits >= 5) {
|
|
14686
|
-
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
14687
|
-
bits -= 5;
|
|
14688
|
-
}
|
|
14689
|
-
}
|
|
14690
|
-
|
|
14691
|
-
if (bits > 0) {
|
|
14692
|
-
output += alphabet[(value << (5 - bits)) & 31];
|
|
14693
|
-
}
|
|
14694
|
-
|
|
14695
|
-
return output;
|
|
14696
|
-
}
|
|
14697
|
-
|
|
14698
|
-
// NOTE: this previously was *only* checking the snippet provided config for contentHost
|
|
14699
|
-
function replaceWithContentHost(str) {
|
|
14700
|
-
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14701
|
-
if (!contentHost || !str)
|
|
14702
|
-
return str;
|
|
14703
|
-
var subBucketPattern = new RegExp(`(https:)?\\/\\/${SUBSCRIPTION_BUCKET}-\\d+\\.storage\\.googleapis\\.com`, 'g');
|
|
14704
|
-
var globalBucketPattern = new RegExp(`(https:)?\\/\\/${GLOBAL_BUCKET}\\.storage\\.googleapis\\.com`, 'g');
|
|
14705
|
-
// Content proxy hosts look like content-<subId>.static.<envDomain>, where envDomain is ASSET_HOST minus its cdn. prefix
|
|
14706
|
-
var envDomain = ASSET_HOST.replace(/^cdn\./, '');
|
|
14707
|
-
var contentProxyPattern = new RegExp(`(https:)?\\/\\/content-\\d+\\.static\\.${escapeRegExp(envDomain)}`, 'g');
|
|
14708
|
-
return str.replace(subBucketPattern, contentHost)
|
|
14709
|
-
.replace(globalBucketPattern, contentHost)
|
|
14710
|
-
.replace(contentProxyPattern, contentHost)
|
|
14711
|
-
.replace(new RegExp(`(https:)?\\/\\/${escapeRegExp(ASSET_HOST)}`, 'g'), contentHost);
|
|
14712
|
-
}
|
|
14713
|
-
function replaceObjectWithContentHost(obj) {
|
|
14714
|
-
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14715
|
-
if (!contentHost)
|
|
14716
|
-
return obj;
|
|
14717
|
-
return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
|
|
14718
|
-
}
|
|
14719
|
-
// APP-37205 we're only checking the remote contentHost config setting
|
|
14720
|
-
function getContentHostRegex() {
|
|
14721
|
-
var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
|
|
14722
|
-
if (ch)
|
|
14723
|
-
return ch + '*';
|
|
14724
|
-
}
|
|
14725
|
-
/**
|
|
14726
|
-
* @access private
|
|
14727
|
-
* @param {Function} isValidGuideResponse
|
|
14728
|
-
* @param {object} guideResponse
|
|
14729
|
-
*/
|
|
14730
|
-
function isValidGuideResponse(guideResponse) {
|
|
14731
|
-
// Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
|
|
14732
|
-
// prop, we should also return false.
|
|
14733
|
-
if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
|
|
14734
|
-
guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
|
|
14735
|
-
return false;
|
|
14736
|
-
}
|
|
14737
|
-
return true;
|
|
14738
|
-
}
|
|
14739
|
-
var ContentLoader = (function (pendo) {
|
|
14740
|
-
var cache = {};
|
|
14741
|
-
// For APP-37205, we're verifing that the result is what it is. I know.
|
|
14742
|
-
var replaceWithContentHostAndVerify = _.compose(function (url) {
|
|
14743
|
-
var test = getContentHostRegex();
|
|
14744
|
-
if (test) {
|
|
14745
|
-
var v = new RegExp(test);
|
|
14746
|
-
if (!v.test(url)) {
|
|
14747
|
-
log.debug('Failed to verify URL: ' + url + ' vs ' + test);
|
|
14748
|
-
}
|
|
14749
|
-
}
|
|
14750
|
-
return url;
|
|
14751
|
-
}, replaceWithContentHost);
|
|
14752
|
-
/**
|
|
14753
|
-
* @access private
|
|
14754
|
-
* @typedef {object} ContentRequest
|
|
14755
|
-
* @param {string} id uniquely identifies the container
|
|
14756
|
-
* @param {string} contentUrl
|
|
14757
|
-
* @param {string} contentUrlCss
|
|
14758
|
-
* @param {string} contentUrlJs
|
|
14759
|
-
*/
|
|
14760
|
-
/**
|
|
14761
|
-
* @access private
|
|
14762
|
-
* @typedef {object} ContentEntry
|
|
14763
|
-
* @param {string} content The raw content as a string
|
|
14764
|
-
* @param {Function} template The template function compiled from the raw content
|
|
14765
|
-
* @param {Function} script The javascript to execute
|
|
14766
|
-
*/
|
|
14767
|
-
/**
|
|
14768
|
-
* Loads the content for the given container
|
|
14769
|
-
*
|
|
14770
|
-
* @access private
|
|
14771
|
-
* @param {ContentRequest} contentContainer
|
|
14772
|
-
* @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
|
|
14773
|
-
*/
|
|
14774
|
-
function load(contentContainer) {
|
|
14775
|
-
try {
|
|
14776
|
-
var cacheEntry = cache[contentContainer.id];
|
|
14777
|
-
if (cacheEntry && cacheEntry.language !== contentContainer.language) {
|
|
14778
|
-
delete cache[contentContainer.id];
|
|
14779
|
-
}
|
|
14780
|
-
if (!cache[contentContainer.id]) {
|
|
14781
|
-
var promises = [];
|
|
14782
|
-
var entry = {
|
|
14783
|
-
deferred: {},
|
|
14784
|
-
language: contentContainer.language
|
|
14785
|
-
};
|
|
14786
|
-
var fetchedDomContent = false;
|
|
14787
|
-
cache[contentContainer.id] = entry;
|
|
14788
|
-
if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
|
|
14789
|
-
entry.deferred.domJson = q.defer();
|
|
14790
|
-
ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
|
|
14791
|
-
entry.domJson = replaceObjectWithContentHost(result.data);
|
|
14792
|
-
var guideResponse = result.data;
|
|
14793
|
-
if (isValidGuideResponse(guideResponse)) {
|
|
14794
|
-
entry.deferred.domJson.resolve();
|
|
14795
|
-
}
|
|
14796
|
-
else {
|
|
14797
|
-
// Log that the guide was invalid
|
|
14798
|
-
// handle promise
|
|
14799
|
-
log.critical(new Error('Response from the guide was not in proper JSON format'));
|
|
14800
|
-
entry.deferred.domJson.reject();
|
|
14801
|
-
}
|
|
14802
|
-
}).catch(function (e) {
|
|
14803
|
-
log.critical(e);
|
|
14804
|
-
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14805
|
-
entry.deferred.domJson.reject('Failed to load content');
|
|
14806
|
-
});
|
|
14807
|
-
promises.push(entry.deferred.domJson.promise);
|
|
14808
|
-
fetchedDomContent = true;
|
|
14809
|
-
}
|
|
14810
|
-
else if (contentContainer.domJsonpUrl) {
|
|
14811
|
-
entry.deferred.domJson = q.defer();
|
|
14812
|
-
var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
|
|
14813
|
-
dom.removeNode(jsonpNode);
|
|
14814
|
-
if (err) {
|
|
14815
|
-
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14816
|
-
return;
|
|
14817
|
-
}
|
|
14818
|
-
}, true);
|
|
14819
|
-
promises.push(entry.deferred.domJson.promise);
|
|
14820
|
-
fetchedDomContent = true;
|
|
14821
|
-
}
|
|
14822
|
-
if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
|
|
14823
|
-
// Content, template, and script for 2.1+ agents
|
|
14824
|
-
entry.deferred.content = q.defer();
|
|
14825
|
-
var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
|
|
14826
|
-
dom.removeNode(scriptNode);
|
|
14827
|
-
if (err) {
|
|
14828
|
-
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14829
|
-
return;
|
|
14830
|
-
}
|
|
14831
|
-
}, true);
|
|
14832
|
-
promises.push(entry.deferred.content.promise);
|
|
14833
|
-
// Style
|
|
14834
|
-
if (contentContainer.contentUrlCss) {
|
|
14835
|
-
entry.deferred.css = q.defer();
|
|
14836
|
-
loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
|
|
14837
|
-
entry.deferred.css.resolve();
|
|
14838
|
-
});
|
|
14839
|
-
promises.push(entry.deferred.css.promise);
|
|
14840
|
-
}
|
|
14841
|
-
}
|
|
14842
|
-
else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
|
|
14843
|
-
// Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
|
|
14844
|
-
entry.deferred.content = q.defer();
|
|
14845
|
-
var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
|
|
14846
|
-
dom.removeNode(contentNode);
|
|
14847
|
-
if (err) {
|
|
14848
|
-
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14849
|
-
return;
|
|
14850
|
-
}
|
|
14851
|
-
}, true);
|
|
14852
|
-
promises.push(entry.deferred.content.promise);
|
|
14853
|
-
}
|
|
14854
|
-
else if (!fetchedDomContent) {
|
|
14855
|
-
return q.reject('fetchedDomContent is still false');
|
|
14856
|
-
}
|
|
14857
|
-
entry.deferred.promise = q.all(promises).then(function () {
|
|
14858
|
-
return _.omit(entry, 'deferred');
|
|
14859
|
-
});
|
|
14860
|
-
}
|
|
14861
|
-
return cache[contentContainer.id].deferred.promise;
|
|
14862
|
-
}
|
|
14863
|
-
catch (e) {
|
|
14864
|
-
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14865
|
-
throw e;
|
|
14866
|
-
}
|
|
14867
|
-
}
|
|
14868
|
-
// Clears the content cache. Used for testing.
|
|
14869
|
-
function reset() {
|
|
14870
|
-
cache = {};
|
|
14871
|
-
}
|
|
14872
|
-
/**
|
|
14873
|
-
* JSONP-style callback for loading guide content
|
|
14874
|
-
* from script resources. Step content stored in
|
|
14875
|
-
* GCS is wrapped with pendo.guideContent(...)
|
|
14876
|
-
*
|
|
14877
|
-
* @access private
|
|
14878
|
-
* @param {string} guideId
|
|
14879
|
-
* @param {string} stepId
|
|
14880
|
-
* @param {string} content
|
|
14881
|
-
* @param {Function} templateFn
|
|
14882
|
-
* @param {Function} scriptFn
|
|
14883
|
-
*/
|
|
14884
|
-
function guideContent(guideId, stepId, content, templateFn, scriptFn) {
|
|
14885
|
-
if (_.isString(content)) {
|
|
14886
|
-
content = preprocessGuideContent(content, guideId, stepId);
|
|
14887
|
-
if (!_.isFunction(templateFn)) {
|
|
14888
|
-
if (shouldCreateTemplate(content)) {
|
|
14889
|
-
templateFn = _.template(content);
|
|
14890
|
-
}
|
|
14891
|
-
else {
|
|
14892
|
-
templateFn = _.constant(content);
|
|
14893
|
-
}
|
|
14894
|
-
}
|
|
14895
|
-
var entry = cache[guideId + stepId];
|
|
14896
|
-
if (entry && entry.deferred.content) {
|
|
14897
|
-
entry.content = content;
|
|
14898
|
-
entry.template = templateFn;
|
|
14899
|
-
entry.script = scriptFn;
|
|
14900
|
-
entry.deferred.content.resolve();
|
|
14901
|
-
}
|
|
14902
|
-
}
|
|
14903
|
-
}
|
|
14904
|
-
// preprocess content to remove simple guide and step IDs templating
|
|
14905
|
-
function removeStandardTemplating(content, guideId, stepId) {
|
|
14906
|
-
const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
|
|
14907
|
-
var guide = pendo.findGuideById('<%= guide.id %>');
|
|
14908
|
-
var step = guide && guide.findStepById('<%= step.id %>');
|
|
14909
|
-
<% } %>`;
|
|
14910
|
-
const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
|
|
14911
|
-
var step = guide && guide.findStepById('${stepId}');`;
|
|
14912
|
-
return content.replace(stdTemplating, sansTemplating);
|
|
14913
|
-
}
|
|
14914
|
-
// execute all content preprocessors and return end result
|
|
14915
|
-
const GuideContentProcessorList = [removeStandardTemplating];
|
|
14916
|
-
function preprocessGuideContent(content, guideId, stepId) {
|
|
14917
|
-
return _.reduce(GuideContentProcessorList, (acc, procFn) => {
|
|
14918
|
-
return procFn(acc, guideId, stepId);
|
|
14919
|
-
}, content);
|
|
14920
|
-
}
|
|
14921
|
-
function shouldCreateTemplate(content) {
|
|
14922
|
-
return /<%/.test(content) && /%>/.test(content);
|
|
14923
|
-
}
|
|
14924
|
-
function receiveDomStructureJson(guideId, stepId, domJson) {
|
|
14925
|
-
var entry = cache[guideId + stepId];
|
|
14926
|
-
if (entry && entry.deferred.domJson) {
|
|
14927
|
-
entry.domJson = replaceObjectWithContentHost(domJson);
|
|
14928
|
-
entry.deferred.domJson.resolve();
|
|
14929
|
-
}
|
|
14930
|
-
}
|
|
14931
|
-
function lightningRedirect(url) {
|
|
14932
|
-
if (isSfdcLightning()) {
|
|
14933
|
-
// https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
|
|
14934
|
-
var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
|
|
14935
|
-
var guideId = path[0];
|
|
14936
|
-
/* global $A */
|
|
14937
|
-
var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
|
|
14938
|
-
if (_.isString(resourcePath)) {
|
|
14939
|
-
return resourcePath + '/' + path.join('/');
|
|
14940
|
-
}
|
|
14941
|
-
}
|
|
14942
|
-
return url;
|
|
14943
|
-
}
|
|
14944
|
-
function addExtensionToUrl(url, extension) {
|
|
14945
|
-
if (!_.isString(url))
|
|
14946
|
-
return url;
|
|
14947
|
-
var i = _.indexOf(url, '?');
|
|
14948
|
-
if (i < 0) {
|
|
14949
|
-
return url + extension;
|
|
14950
|
-
}
|
|
14951
|
-
else {
|
|
14952
|
-
return url.substring(0, i) + extension + url.substring(i);
|
|
14953
|
-
}
|
|
14954
|
-
}
|
|
14955
|
-
return {
|
|
14956
|
-
load,
|
|
14957
|
-
reset,
|
|
14958
|
-
guideContent,
|
|
14959
|
-
receiveDomStructureJson
|
|
14960
|
-
};
|
|
14961
|
-
})();
|
|
14962
|
-
|
|
14963
15014
|
var AsyncContent = (function () {
|
|
14964
15015
|
function AsyncContent(guide) {
|
|
14965
15016
|
var step = this;
|
|
@@ -15089,7 +15140,7 @@ var GuideRuntime = (function (agentEvents) {
|
|
|
15089
15140
|
}
|
|
15090
15141
|
function teardown() {
|
|
15091
15142
|
EventChannel.off(handleEvent);
|
|
15092
|
-
|
|
15143
|
+
pluginAddGlobalScripts.length = 0;
|
|
15093
15144
|
}
|
|
15094
15145
|
function addGlobalScript(obj) {
|
|
15095
15146
|
obj = autoSubscribe(obj);
|
|
@@ -21996,7 +22047,9 @@ Badge.behaviors = [
|
|
|
21996
22047
|
function InlinePosition() {
|
|
21997
22048
|
function getShadowDomAdjustedTarget(target) {
|
|
21998
22049
|
try {
|
|
21999
|
-
|
|
22050
|
+
// Only a default (unnamed) slot can render a badge appended as a light-DOM
|
|
22051
|
+
// child, so fall back to the shadow root when there isn't one
|
|
22052
|
+
if (target.shadowRoot && !target.shadowRoot.querySelector('slot:not([name])')) {
|
|
22000
22053
|
return target.shadowRoot;
|
|
22001
22054
|
}
|
|
22002
22055
|
}
|
|
@@ -22017,7 +22070,7 @@ function InlinePosition() {
|
|
|
22017
22070
|
if (targetElem && targetElem.tagName) {
|
|
22018
22071
|
var tagName = targetElem.tagName.toLowerCase();
|
|
22019
22072
|
var shadowDomAdjustedTarget = getShadowDomAdjustedTarget(targetElem);
|
|
22020
|
-
if (
|
|
22073
|
+
if (/^(?:br|input|img|select|textarea)$/.test(tagName)) {
|
|
22021
22074
|
if (badgeElem.parentNode === targetElem.parentNode)
|
|
22022
22075
|
return;
|
|
22023
22076
|
// Insert after if targetElem cannot have children
|
|
@@ -23533,7 +23586,7 @@ function autofocusRuntimeScript(step, guide, pendo) {
|
|
|
23533
23586
|
if (pendo._.get(step, 'attributes.isAutoFocus')) {
|
|
23534
23587
|
BuildingBlockGuides.autofocusStep(step, guideContainer);
|
|
23535
23588
|
BuildingBlockGuides.trapFocusStep(step, guideContainer);
|
|
23536
|
-
step.hiddenElements = BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]);
|
|
23589
|
+
step.hiddenElements = (step.hiddenElements || []).concat(BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]));
|
|
23537
23590
|
}
|
|
23538
23591
|
if (isGuideRCType(guide)) {
|
|
23539
23592
|
BuildingBlockGuides.attachRCFocusHandlers();
|
|
@@ -23555,16 +23608,23 @@ function backdropInertRuntimeScript(step, guide, pendo) {
|
|
|
23555
23608
|
return;
|
|
23556
23609
|
if (step.hasBackdrop) {
|
|
23557
23610
|
const BuildingBlockGuides = pendo.BuildingBlocks.BuildingBlockGuides;
|
|
23558
|
-
const
|
|
23559
|
-
|
|
23560
|
-
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
23565
|
-
|
|
23566
|
-
|
|
23611
|
+
const guideElement = step.guideElement[0];
|
|
23612
|
+
const excludeElements = getExcludedElements(step);
|
|
23613
|
+
const disabledElements = sniffer.supportsInert()
|
|
23614
|
+
? BuildingBlockGuides.addInertOutsideGuide(guideElement, [], excludeElements)
|
|
23615
|
+
: BuildingBlockGuides.addAriaHiddenOutsideGuide(guideElement, [], excludeElements);
|
|
23616
|
+
step.hiddenElements = (step.hiddenElements || []).concat(disabledElements);
|
|
23617
|
+
}
|
|
23618
|
+
}
|
|
23619
|
+
function getExcludedElements(step) {
|
|
23620
|
+
const excludeElements = getTargetAreaElementsForBlockOut(step);
|
|
23621
|
+
// The preview bar iframe is a sibling of the guide under <body>; inerting it
|
|
23622
|
+
// would make preview mode's controls unclickable while the backdrop shows.
|
|
23623
|
+
const previewBar = document.getElementById('pendo-preview');
|
|
23624
|
+
if (previewBar) {
|
|
23625
|
+
excludeElements.push(previewBar);
|
|
23567
23626
|
}
|
|
23627
|
+
return excludeElements;
|
|
23568
23628
|
}
|
|
23569
23629
|
var backdropInertStep = {
|
|
23570
23630
|
script: backdropInertRuntimeScript
|
|
@@ -25168,22 +25228,31 @@ function hasLegacyGuides(guides, pendo) {
|
|
|
25168
25228
|
}
|
|
25169
25229
|
const DEFAULT_CSS_ID = '_pendo-default-css_';
|
|
25170
25230
|
const CUSTOM_CSS_ID = '_pendo-css_';
|
|
25231
|
+
// Several SDK instances can share a page. Scoping the <link> ids by api key stops one app's
|
|
25232
|
+
// teardown, or its "no custom css" path, from stripping the stylesheet another app injected.
|
|
25233
|
+
function getCssElementId(baseId) {
|
|
25234
|
+
return pendo$1.apiKey ? baseId + pendo$1.apiKey : baseId;
|
|
25235
|
+
}
|
|
25236
|
+
// Api keys are arbitrary customer-supplied strings, so '#' + id is not a safe selector.
|
|
25237
|
+
function removeGuideCss(baseId) {
|
|
25238
|
+
dom(document.getElementById(getCssElementId(baseId))).remove();
|
|
25239
|
+
}
|
|
25171
25240
|
function loadGuideCss() {
|
|
25172
25241
|
var promises = [];
|
|
25173
25242
|
if (!shouldLoadGlobalCSS()) {
|
|
25174
25243
|
return q.resolve();
|
|
25175
25244
|
}
|
|
25176
25245
|
if (hasLegacyGuides(getActiveGuides(), pendo$1)) {
|
|
25177
|
-
promises.push(loadExternalCss(DEFAULT_CSS_ID, getDefaultCssUrl()));
|
|
25246
|
+
promises.push(loadExternalCss(getCssElementId(DEFAULT_CSS_ID), getDefaultCssUrl()));
|
|
25178
25247
|
}
|
|
25179
25248
|
var guideWidget = pendo$1.guideWidget || {};
|
|
25180
25249
|
var data = guideWidget.data || {};
|
|
25181
25250
|
var customCssUrl = data.guideCssUrl;
|
|
25182
25251
|
if (customCssUrl) {
|
|
25183
|
-
promises.push(loadExternalCss(CUSTOM_CSS_ID, replaceWithContentHost(customCssUrl)));
|
|
25252
|
+
promises.push(loadExternalCss(getCssElementId(CUSTOM_CSS_ID), replaceWithContentHost(customCssUrl)));
|
|
25184
25253
|
}
|
|
25185
25254
|
else {
|
|
25186
|
-
|
|
25255
|
+
removeGuideCss(CUSTOM_CSS_ID);
|
|
25187
25256
|
}
|
|
25188
25257
|
return promises;
|
|
25189
25258
|
}
|
|
@@ -25400,8 +25469,8 @@ var initGuides = function (observer) {
|
|
|
25400
25469
|
_.each(teardownFns, function (teardownFn) {
|
|
25401
25470
|
teardownFn();
|
|
25402
25471
|
});
|
|
25403
|
-
|
|
25404
|
-
|
|
25472
|
+
removeGuideCss(DEFAULT_CSS_ID);
|
|
25473
|
+
removeGuideCss(CUSTOM_CSS_ID);
|
|
25405
25474
|
GuideRuntime.teardown();
|
|
25406
25475
|
};
|
|
25407
25476
|
};
|
|
@@ -25871,6 +25940,8 @@ _.extend(TopFrameRelay.prototype, {
|
|
|
25871
25940
|
if (!message.data)
|
|
25872
25941
|
return;
|
|
25873
25942
|
if (message.data.action === 'pendo::connect') {
|
|
25943
|
+
if (!isSameWindowTree(message, this._window))
|
|
25944
|
+
return;
|
|
25874
25945
|
this._impl.frameConnect(message);
|
|
25875
25946
|
}
|
|
25876
25947
|
if (message.data.action === REMOVE_FRAME_ACTION) {
|
|
@@ -25966,6 +26037,38 @@ var ImplCommon = {
|
|
|
25966
26037
|
function isPortValid(port) {
|
|
25967
26038
|
return port && _.all(['addEventListener', 'start'], function (cmd) { return _.isFunction(port[cmd]); });
|
|
25968
26039
|
}
|
|
26040
|
+
// A connect is only ever posted to window.top by a frame below us, so a sender we
|
|
26041
|
+
// cannot find in our own frame tree is not one of our frames no matter what it
|
|
26042
|
+
// claims. window.open gives an attacker a handle to post through, and neither
|
|
26043
|
+
// X-Frame-Options nor frame-ancestors covers that - they only govern embedding
|
|
26044
|
+
// (APP-168708). frames and its indexed children are readable across origins, so
|
|
26045
|
+
// this holds where an origin check can't: no ancestorOrigins needed, and sandboxed
|
|
26046
|
+
// frames (origin "null") are still found.
|
|
26047
|
+
function isFrameBelowUs(source, window) {
|
|
26048
|
+
if (!source)
|
|
26049
|
+
return false;
|
|
26050
|
+
if (source === window)
|
|
26051
|
+
return true;
|
|
26052
|
+
var pending = [window];
|
|
26053
|
+
while (pending.length) {
|
|
26054
|
+
var children = pending.shift().frames;
|
|
26055
|
+
var count = children ? children.length : 0;
|
|
26056
|
+
for (var i = 0; i < count; i++) {
|
|
26057
|
+
if (children[i] === source)
|
|
26058
|
+
return true;
|
|
26059
|
+
pending.push(children[i]);
|
|
26060
|
+
}
|
|
26061
|
+
}
|
|
26062
|
+
return false;
|
|
26063
|
+
}
|
|
26064
|
+
function isSameWindowTree(message, window) {
|
|
26065
|
+
try {
|
|
26066
|
+
return isFrameBelowUs(message.source, window);
|
|
26067
|
+
}
|
|
26068
|
+
catch (e) {
|
|
26069
|
+
return false;
|
|
26070
|
+
}
|
|
26071
|
+
}
|
|
25969
26072
|
function MessageChannelImpl(window, singletonMessageHandler) {
|
|
25970
26073
|
this._window = window;
|
|
25971
26074
|
this._ports = {};
|
|
@@ -30854,21 +30957,26 @@ var BuildingBlockGuides = (function () {
|
|
|
30854
30957
|
domElem.append(endFocusBumper);
|
|
30855
30958
|
return [startFocusBumper, endFocusBumper];
|
|
30856
30959
|
}
|
|
30857
|
-
|
|
30960
|
+
// native contains() can't see into shadow trees, so excluded elements inside a
|
|
30961
|
+
// shadow root (e.g. blockOut target areas) need the shadow-aware walk
|
|
30962
|
+
function containsExcludedElement(elem, excludeElements) {
|
|
30963
|
+
return _.some(excludeElements, (excluded) => excluded && (elem === excluded || contains(elem, excluded)));
|
|
30964
|
+
}
|
|
30965
|
+
function addAriaHiddenOutsideGuide(elem, hidden = [], excludeElements = []) {
|
|
30858
30966
|
if (!elem || elem === getBody())
|
|
30859
30967
|
return hidden;
|
|
30860
30968
|
const siblings = getSiblings(elem);
|
|
30861
30969
|
_.each(siblings, (s) => {
|
|
30862
|
-
|
|
30863
|
-
|
|
30864
|
-
|
|
30865
|
-
|
|
30866
|
-
|
|
30867
|
-
|
|
30970
|
+
const tagName = s.tagName.toLowerCase();
|
|
30971
|
+
const alreadyHidden = s.getAttribute('aria-hidden') === 'true';
|
|
30972
|
+
const isExcluded = containsExcludedElement(s, excludeElements);
|
|
30973
|
+
if (hidden.indexOf(s) === -1 && !alreadyHidden && !isExcluded && tagName !== 'script' && tagName !== 'style') {
|
|
30974
|
+
s.setAttribute('aria-hidden', 'true');
|
|
30975
|
+
hidden.push(s);
|
|
30868
30976
|
}
|
|
30869
30977
|
});
|
|
30870
30978
|
if (elem.parentNode) {
|
|
30871
|
-
addAriaHiddenOutsideGuide(elem.parentNode, hidden);
|
|
30979
|
+
addAriaHiddenOutsideGuide(elem.parentNode, hidden, excludeElements);
|
|
30872
30980
|
}
|
|
30873
30981
|
return hidden;
|
|
30874
30982
|
}
|
|
@@ -30882,9 +30990,7 @@ var BuildingBlockGuides = (function () {
|
|
|
30882
30990
|
_.each(siblings, (s) => {
|
|
30883
30991
|
const tagName = s.tagName.toLowerCase();
|
|
30884
30992
|
const alreadyDisabled = s.getAttribute('inert') === 'true';
|
|
30885
|
-
|
|
30886
|
-
// shadow root (e.g. blockOut target areas) need the shadow-aware walk
|
|
30887
|
-
const isExcluded = _.some(excludeElements, (excluded) => excluded && (s === excluded || contains(s, excluded)));
|
|
30993
|
+
const isExcluded = containsExcludedElement(s, excludeElements);
|
|
30888
30994
|
if (hidden.indexOf(s) === -1 && !alreadyDisabled && !isExcluded && tagName !== 'script' && tagName !== 'style') {
|
|
30889
30995
|
s.setAttribute('inert', 'true');
|
|
30890
30996
|
hidden.push(s);
|
|
@@ -43105,6 +43211,124 @@ function substitutionIcon(color, size) {
|
|
|
43105
43211
|
13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
|
|
43106
43212
|
}
|
|
43107
43213
|
|
|
43214
|
+
const AUTO_ADVANCE_DELAY_MS = 250;
|
|
43215
|
+
function isAutoAdvanceRequested(step, _) {
|
|
43216
|
+
return _.get(step, 'attributes.autoAdvance') === true;
|
|
43217
|
+
}
|
|
43218
|
+
// Survey-authored DOM carries no question-type classes, so eligibility keys off
|
|
43219
|
+
// answer-control shape: every required poll id must be radio-backed, which also
|
|
43220
|
+
// refuses a future non-radio type by default.
|
|
43221
|
+
function isStepEligibleInDom(step, _, requiredPollIds) {
|
|
43222
|
+
const guideElement = step && step.guideElement;
|
|
43223
|
+
if (!guideElement)
|
|
43224
|
+
return false;
|
|
43225
|
+
if (guideElement.find('textarea, input:text, input:checkbox, select').length)
|
|
43226
|
+
return false;
|
|
43227
|
+
const pollIds = _.uniq(_.map(guideElement.find('[data-pendo-poll-id]'), (element) => element.getAttribute('data-pendo-poll-id')));
|
|
43228
|
+
if (!pollIds.length)
|
|
43229
|
+
return false;
|
|
43230
|
+
return _.all(pollIds, (pollId) => _.contains(requiredPollIds, pollId) &&
|
|
43231
|
+
guideElement.find(`input:radio[data-pendo-poll-id="${pollId}"]`).length > 0);
|
|
43232
|
+
}
|
|
43233
|
+
function hideSubmitButtons(step, _, submitButtons) {
|
|
43234
|
+
const hiddenButtons = [];
|
|
43235
|
+
_.each(submitButtons, (button) => {
|
|
43236
|
+
const element = step.guideElement.find(`#${button.props.id}`)[0];
|
|
43237
|
+
if (!element)
|
|
43238
|
+
return;
|
|
43239
|
+
// display, not visibility: a reserved-but-invisible box leaves an empty gap
|
|
43240
|
+
// at the foot of the step. Authored styles reach the button inline
|
|
43241
|
+
// (build.js:151), so the original value is kept rather than blanked.
|
|
43242
|
+
hiddenButtons.push({ element, display: element.style.display });
|
|
43243
|
+
element.style.display = 'none';
|
|
43244
|
+
});
|
|
43245
|
+
return function restoreSubmitButtons() {
|
|
43246
|
+
_.each(hiddenButtons, (hiddenButton) => {
|
|
43247
|
+
hiddenButton.element.style.display = hiddenButton.display;
|
|
43248
|
+
});
|
|
43249
|
+
};
|
|
43250
|
+
}
|
|
43251
|
+
// The container's pixel height is measured during render (layout.js) before custom
|
|
43252
|
+
// scripts run, so collapsing a row afterwards leaves the box stale and the step keeps
|
|
43253
|
+
// the empty space. Same reflow pair metadata-substitution uses, scoped to this step so
|
|
43254
|
+
// a second guide on screen is left alone.
|
|
43255
|
+
function reflowGuide(step, _, pendo) {
|
|
43256
|
+
const recalculateGuideHeight = _.get(pendo, 'BuildingBlocks.BuildingBlockGuides.recalculateGuideHeight');
|
|
43257
|
+
try {
|
|
43258
|
+
if (_.isFunction(pendo.flexElement)) {
|
|
43259
|
+
pendo.flexElement(step.guideElement.find(guideMarkdownUtil.containerSelector));
|
|
43260
|
+
}
|
|
43261
|
+
if (_.isFunction(recalculateGuideHeight)) {
|
|
43262
|
+
recalculateGuideHeight(`pendo-g-${step.id}`, step.guideElement);
|
|
43263
|
+
}
|
|
43264
|
+
}
|
|
43265
|
+
catch (error) {
|
|
43266
|
+
pendo.log('[auto-advance] guide reflow failed, the step may keep an empty row', error && error.message);
|
|
43267
|
+
}
|
|
43268
|
+
}
|
|
43269
|
+
function createInertController() {
|
|
43270
|
+
return { isActive: false, notifyAnswered() { }, cancel() { } };
|
|
43271
|
+
}
|
|
43272
|
+
function createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds) {
|
|
43273
|
+
const _ = pendo._;
|
|
43274
|
+
const inert = createInertController();
|
|
43275
|
+
if (!isAutoAdvanceRequested(step, _))
|
|
43276
|
+
return inert;
|
|
43277
|
+
// Only reachable for API-authored guides; choosing between two
|
|
43278
|
+
// submitPollAndGoToStep targets would be a guess.
|
|
43279
|
+
if (_.size(submitButtons) !== 1) {
|
|
43280
|
+
pendo.log(`[auto-advance] not arming: step ${step.id} needs exactly one submit button, found ${_.size(submitButtons)}`);
|
|
43281
|
+
return inert;
|
|
43282
|
+
}
|
|
43283
|
+
if (!isStepEligibleInDom(step, _, requiredPollIds)) {
|
|
43284
|
+
pendo.log(`[auto-advance] not arming: rendered step ${step.id} failed the eligibility re-check`);
|
|
43285
|
+
return inert;
|
|
43286
|
+
}
|
|
43287
|
+
const submitButton = submitButtons[0];
|
|
43288
|
+
// A click binds every action on the node and dispatches one event each
|
|
43289
|
+
// (build.js:205-207); we dispatch one, so a second action would be dropped.
|
|
43290
|
+
if (_.size(submitButton.actions) !== 1) {
|
|
43291
|
+
pendo.log(`[auto-advance] not arming: step ${step.id} submit button carries ${_.size(submitButton.actions)} actions, expected one`);
|
|
43292
|
+
return inert;
|
|
43293
|
+
}
|
|
43294
|
+
const submitAction = _.find(submitButton.actions, (action) => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep');
|
|
43295
|
+
if (!submitAction)
|
|
43296
|
+
return inert;
|
|
43297
|
+
let hasFired = false;
|
|
43298
|
+
let restoreSubmitButtons = _.noop;
|
|
43299
|
+
function fire() {
|
|
43300
|
+
if (hasFired)
|
|
43301
|
+
return;
|
|
43302
|
+
hasFired = true;
|
|
43303
|
+
try {
|
|
43304
|
+
// Shape-identical to what bindActionToNode emits for a click, so
|
|
43305
|
+
// beforeAdvance consumers intercept an auto-advance the same way.
|
|
43306
|
+
step.eventRouter.eventable.trigger('pendoEvent', {
|
|
43307
|
+
action: submitAction.action,
|
|
43308
|
+
params: submitAction.parameters,
|
|
43309
|
+
step,
|
|
43310
|
+
ignore: !!submitAction.ignore,
|
|
43311
|
+
srcElement: step.guideElement.find(`#${submitButton.props.id}`)[0]
|
|
43312
|
+
});
|
|
43313
|
+
}
|
|
43314
|
+
catch (error) {
|
|
43315
|
+
restoreSubmitButtons();
|
|
43316
|
+
reflowGuide(step, _, pendo);
|
|
43317
|
+
pendo.log('[auto-advance] submit dispatch failed, restored the submit button', error && error.message);
|
|
43318
|
+
}
|
|
43319
|
+
}
|
|
43320
|
+
const debouncedFire = _.debounce(fire, AUTO_ADVANCE_DELAY_MS);
|
|
43321
|
+
// Last, so nothing between here and the return can throw and leave the step
|
|
43322
|
+
// with a hidden button and no controller to un-hide it.
|
|
43323
|
+
restoreSubmitButtons = hideSubmitButtons(step, _, submitButtons);
|
|
43324
|
+
reflowGuide(step, _, pendo);
|
|
43325
|
+
return {
|
|
43326
|
+
isActive: true,
|
|
43327
|
+
notifyAnswered: debouncedFire,
|
|
43328
|
+
cancel: debouncedFire.cancel
|
|
43329
|
+
};
|
|
43330
|
+
}
|
|
43331
|
+
|
|
43108
43332
|
const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
|
|
43109
43333
|
const requiredSyntax = '{required/}';
|
|
43110
43334
|
const RequiredQuestions = {
|
|
@@ -43114,6 +43338,7 @@ const RequiredQuestions = {
|
|
|
43114
43338
|
let requiredPollIds = [];
|
|
43115
43339
|
let submitButtons = (_a = guideMarkdownUtil.lookupGuideButtons(step.domJson)) === null || _a === void 0 ? void 0 : _a.filter(button => button.actions.find(action => action.action === 'submitPoll' || action.action === 'submitPollAndGoToStep'));
|
|
43116
43340
|
const requiredQuestions = processRequiredQuestions();
|
|
43341
|
+
let autoAdvance = createInertController();
|
|
43117
43342
|
if (requiredQuestions) {
|
|
43118
43343
|
pendo._.forEach(requiredQuestions, (question) => {
|
|
43119
43344
|
if (question.classList.contains('_pendo-open-text-poll-question')) {
|
|
@@ -43129,6 +43354,17 @@ const RequiredQuestions = {
|
|
|
43129
43354
|
}
|
|
43130
43355
|
});
|
|
43131
43356
|
}
|
|
43357
|
+
// Armed last, after the listeners above: runCustomScripts abandons the rest of
|
|
43358
|
+
// this script on a throw, and the submit button is already disabled by now.
|
|
43359
|
+
if (isAutoAdvanceRequested(step, pendo._)) {
|
|
43360
|
+
this.on('beforeUnmount', () => autoAdvance.cancel());
|
|
43361
|
+
try {
|
|
43362
|
+
autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
|
|
43363
|
+
}
|
|
43364
|
+
catch (error) {
|
|
43365
|
+
pendo.log('[auto-advance] arming failed, leaving the step on manual submit', error && error.message);
|
|
43366
|
+
}
|
|
43367
|
+
}
|
|
43132
43368
|
function getEligibleQuestions() {
|
|
43133
43369
|
const pollQuestions = pendo._.toArray(step.guideElement.find(`[class*=-poll-question]:contains(${requiredSyntax})`));
|
|
43134
43370
|
const surveyMatches = pendo._.toArray(step.guideElement.find(`[data-pendo-poll-id]:contains(${requiredSyntax})`));
|
|
@@ -43205,9 +43441,11 @@ const RequiredQuestions = {
|
|
|
43205
43441
|
}
|
|
43206
43442
|
if (allRequiredComplete) {
|
|
43207
43443
|
enableEligibleButtons(submitButtons);
|
|
43444
|
+
autoAdvance.notifyAnswered();
|
|
43208
43445
|
}
|
|
43209
43446
|
else {
|
|
43210
43447
|
disableEligibleButtons(submitButtons);
|
|
43448
|
+
autoAdvance.cancel();
|
|
43211
43449
|
}
|
|
43212
43450
|
}
|
|
43213
43451
|
function disableEligibleButtons(buttons) {
|