@pendo/agent 2.340.0 → 2.341.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 +1 -1
- package/dist/pendo.module.js +512 -302
- 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
|
@@ -4046,8 +4046,8 @@ let SERVER = '';
|
|
|
4046
4046
|
let ASSET_HOST = '';
|
|
4047
4047
|
let ASSET_PATH = '';
|
|
4048
4048
|
let DESIGNER_SERVER = '';
|
|
4049
|
-
let VERSION = '2.
|
|
4050
|
-
let PACKAGE_VERSION = '2.
|
|
4049
|
+
let VERSION = '2.341.0_';
|
|
4050
|
+
let PACKAGE_VERSION = '2.341.0';
|
|
4051
4051
|
let LOADER = 'xhr';
|
|
4052
4052
|
/* eslint-enable web-sdk-eslint-rules/no-gulp-env-references */
|
|
4053
4053
|
/**
|
|
@@ -14305,6 +14305,330 @@ var XHRGuideLoader = {
|
|
|
14305
14305
|
}
|
|
14306
14306
|
};
|
|
14307
14307
|
|
|
14308
|
+
function base32Encode(uint8array) {
|
|
14309
|
+
var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
14310
|
+
|
|
14311
|
+
var length = uint8array.length;
|
|
14312
|
+
|
|
14313
|
+
var bits = 0;
|
|
14314
|
+
var value = 0;
|
|
14315
|
+
var output = '';
|
|
14316
|
+
|
|
14317
|
+
for (var i = 0; i < length; i++) {
|
|
14318
|
+
value = (value << 8) | uint8array[i];
|
|
14319
|
+
bits += 8;
|
|
14320
|
+
|
|
14321
|
+
while (bits >= 5) {
|
|
14322
|
+
output += alphabet[(value >>> (bits - 5)) & 31];
|
|
14323
|
+
bits -= 5;
|
|
14324
|
+
}
|
|
14325
|
+
}
|
|
14326
|
+
|
|
14327
|
+
if (bits > 0) {
|
|
14328
|
+
output += alphabet[(value << (5 - bits)) & 31];
|
|
14329
|
+
}
|
|
14330
|
+
|
|
14331
|
+
return output;
|
|
14332
|
+
}
|
|
14333
|
+
|
|
14334
|
+
// A CNAME contentHost is commonly configured without a scheme, which would leave the rewritten
|
|
14335
|
+
// url relative to the current page rather than absolute. Leading-slash values are deliberate and
|
|
14336
|
+
// kept as-is: '//host' is protocol-relative, '/path' is same-origin (see fixContentHostUrl).
|
|
14337
|
+
function normalizeContentHost(contentHost) {
|
|
14338
|
+
if (/:\/\//.test(contentHost) || /^\//.test(contentHost))
|
|
14339
|
+
return contentHost;
|
|
14340
|
+
return 'https://' + contentHost;
|
|
14341
|
+
}
|
|
14342
|
+
function stripScheme(host) {
|
|
14343
|
+
return host.replace(/^https?:\/\//, '');
|
|
14344
|
+
}
|
|
14345
|
+
// Self hosted installs aim assetHost at their own host, or drop it entirely - the npm package
|
|
14346
|
+
// deletes it for the assets option - so it cannot locate our CDN. SERVER survives either way, and
|
|
14347
|
+
// app, cdn and static are sibling labels in every environment's domain template. Builds that point
|
|
14348
|
+
// SERVER somewhere other than an app host (--env=local) are left with assetHost.
|
|
14349
|
+
function getPendoCdnHost() {
|
|
14350
|
+
var appLabel = /(^|\.)app\./;
|
|
14351
|
+
var serverHost = stripScheme(SERVER);
|
|
14352
|
+
if (!appLabel.test(serverHost))
|
|
14353
|
+
return stripScheme(ASSET_HOST);
|
|
14354
|
+
return serverHost.replace(appLabel, '$1cdn.');
|
|
14355
|
+
}
|
|
14356
|
+
// NOTE: this previously was *only* checking the snippet provided config for contentHost
|
|
14357
|
+
function replaceWithContentHost(str) {
|
|
14358
|
+
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14359
|
+
if (!contentHost || !str)
|
|
14360
|
+
return str;
|
|
14361
|
+
contentHost = normalizeContentHost(contentHost);
|
|
14362
|
+
var cdnHost = getPendoCdnHost();
|
|
14363
|
+
var assetHost = stripScheme(ASSET_HOST);
|
|
14364
|
+
// Content proxy hosts are the environment's domain with static in the variable label, which is
|
|
14365
|
+
// not always the leading one - us1 is content-<subId>.us1.static.pendo.io
|
|
14366
|
+
var staticHost = cdnHost.replace(/(^|\.)cdn\./, '$1static.');
|
|
14367
|
+
// An unknown host has to be dropped rather than interpolated: an empty one leaves a pattern
|
|
14368
|
+
// that matches the // of every absolute url
|
|
14369
|
+
var hostPatterns = [
|
|
14370
|
+
SUBSCRIPTION_BUCKET && `${escapeRegExp(SUBSCRIPTION_BUCKET)}-\\d+\\.storage\\.googleapis\\.com`,
|
|
14371
|
+
GLOBAL_BUCKET && `${escapeRegExp(GLOBAL_BUCKET)}\\.storage\\.googleapis\\.com`,
|
|
14372
|
+
cdnHost && `content-\\d+\\.${escapeRegExp(staticHost)}`,
|
|
14373
|
+
cdnHost && escapeRegExp(cdnHost),
|
|
14374
|
+
assetHost && assetHost !== cdnHost && escapeRegExp(assetHost)
|
|
14375
|
+
];
|
|
14376
|
+
return _.reduce(hostPatterns, function (rewritten, hostPattern) {
|
|
14377
|
+
if (!hostPattern)
|
|
14378
|
+
return rewritten;
|
|
14379
|
+
return rewritten.replace(new RegExp(`(https:)?\\/\\/${hostPattern}`, 'g'), contentHost);
|
|
14380
|
+
}, str);
|
|
14381
|
+
}
|
|
14382
|
+
function replaceObjectWithContentHost(obj) {
|
|
14383
|
+
var contentHost = ConfigReader.getLocalConfig('contentHost');
|
|
14384
|
+
if (!contentHost)
|
|
14385
|
+
return obj;
|
|
14386
|
+
return JSON.parse(replaceWithContentHost(JSON.stringify(obj)));
|
|
14387
|
+
}
|
|
14388
|
+
// APP-37205 we're only checking the remote contentHost config setting
|
|
14389
|
+
function getContentHostRegex() {
|
|
14390
|
+
var ch = escapeRegExp(ConfigReader.getHostedConfig('contentHost'));
|
|
14391
|
+
if (ch)
|
|
14392
|
+
return ch + '*';
|
|
14393
|
+
}
|
|
14394
|
+
/**
|
|
14395
|
+
* @access private
|
|
14396
|
+
* @param {Function} isValidGuideResponse
|
|
14397
|
+
* @param {object} guideResponse
|
|
14398
|
+
*/
|
|
14399
|
+
function isValidGuideResponse(guideResponse) {
|
|
14400
|
+
// Is it in proper JSON format/an object? If guideResponse does not have the 'type', 'props', or the 'children'
|
|
14401
|
+
// prop, we should also return false.
|
|
14402
|
+
if (!(_.isObject(guideResponse) && guideResponse.hasOwnProperty('type') &&
|
|
14403
|
+
guideResponse.hasOwnProperty('children') && guideResponse.hasOwnProperty('props'))) {
|
|
14404
|
+
return false;
|
|
14405
|
+
}
|
|
14406
|
+
return true;
|
|
14407
|
+
}
|
|
14408
|
+
var ContentLoader = (function (pendo) {
|
|
14409
|
+
var cache = {};
|
|
14410
|
+
// For APP-37205, we're verifing that the result is what it is. I know.
|
|
14411
|
+
var replaceWithContentHostAndVerify = _.compose(function (url) {
|
|
14412
|
+
var test = getContentHostRegex();
|
|
14413
|
+
if (test) {
|
|
14414
|
+
var v = new RegExp(test);
|
|
14415
|
+
if (!v.test(url)) {
|
|
14416
|
+
log.debug('Failed to verify URL: ' + url + ' vs ' + test);
|
|
14417
|
+
}
|
|
14418
|
+
}
|
|
14419
|
+
return url;
|
|
14420
|
+
}, replaceWithContentHost);
|
|
14421
|
+
/**
|
|
14422
|
+
* @access private
|
|
14423
|
+
* @typedef {object} ContentRequest
|
|
14424
|
+
* @param {string} id uniquely identifies the container
|
|
14425
|
+
* @param {string} contentUrl
|
|
14426
|
+
* @param {string} contentUrlCss
|
|
14427
|
+
* @param {string} contentUrlJs
|
|
14428
|
+
*/
|
|
14429
|
+
/**
|
|
14430
|
+
* @access private
|
|
14431
|
+
* @typedef {object} ContentEntry
|
|
14432
|
+
* @param {string} content The raw content as a string
|
|
14433
|
+
* @param {Function} template The template function compiled from the raw content
|
|
14434
|
+
* @param {Function} script The javascript to execute
|
|
14435
|
+
*/
|
|
14436
|
+
/**
|
|
14437
|
+
* Loads the content for the given container
|
|
14438
|
+
*
|
|
14439
|
+
* @access private
|
|
14440
|
+
* @param {ContentRequest} contentContainer
|
|
14441
|
+
* @returns {Promise.<ContentEntry>} Resolved when all of the content is loaded
|
|
14442
|
+
*/
|
|
14443
|
+
function load(contentContainer) {
|
|
14444
|
+
try {
|
|
14445
|
+
var cacheEntry = cache[contentContainer.id];
|
|
14446
|
+
if (cacheEntry && cacheEntry.language !== contentContainer.language) {
|
|
14447
|
+
delete cache[contentContainer.id];
|
|
14448
|
+
}
|
|
14449
|
+
if (!cache[contentContainer.id]) {
|
|
14450
|
+
var promises = [];
|
|
14451
|
+
var entry = {
|
|
14452
|
+
deferred: {},
|
|
14453
|
+
language: contentContainer.language
|
|
14454
|
+
};
|
|
14455
|
+
var fetchedDomContent = false;
|
|
14456
|
+
cache[contentContainer.id] = entry;
|
|
14457
|
+
if (XHRGuideLoader.usesXhr() && contentContainer.domUrl) {
|
|
14458
|
+
entry.deferred.domJson = q.defer();
|
|
14459
|
+
ajax.get(replaceWithContentHostAndVerify(contentContainer.domUrl)).then(function (result) {
|
|
14460
|
+
entry.domJson = replaceObjectWithContentHost(result.data);
|
|
14461
|
+
var guideResponse = result.data;
|
|
14462
|
+
if (isValidGuideResponse(guideResponse)) {
|
|
14463
|
+
entry.deferred.domJson.resolve();
|
|
14464
|
+
}
|
|
14465
|
+
else {
|
|
14466
|
+
// Log that the guide was invalid
|
|
14467
|
+
// handle promise
|
|
14468
|
+
log.critical(new Error('Response from the guide was not in proper JSON format'));
|
|
14469
|
+
entry.deferred.domJson.reject();
|
|
14470
|
+
}
|
|
14471
|
+
}).catch(function (e) {
|
|
14472
|
+
log.critical(e);
|
|
14473
|
+
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14474
|
+
entry.deferred.domJson.reject('Failed to load content');
|
|
14475
|
+
});
|
|
14476
|
+
promises.push(entry.deferred.domJson.promise);
|
|
14477
|
+
fetchedDomContent = true;
|
|
14478
|
+
}
|
|
14479
|
+
else if (contentContainer.domJsonpUrl) {
|
|
14480
|
+
entry.deferred.domJson = q.defer();
|
|
14481
|
+
var jsonpNode = loadResource(replaceWithContentHostAndVerify(contentContainer.domJsonpUrl), function (err) {
|
|
14482
|
+
dom.removeNode(jsonpNode);
|
|
14483
|
+
if (err) {
|
|
14484
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14485
|
+
return;
|
|
14486
|
+
}
|
|
14487
|
+
}, true);
|
|
14488
|
+
promises.push(entry.deferred.domJson.promise);
|
|
14489
|
+
fetchedDomContent = true;
|
|
14490
|
+
}
|
|
14491
|
+
if (contentContainer.contentUrlJs && !ConfigReader.get('preventCodeInjection')) {
|
|
14492
|
+
// Content, template, and script for 2.1+ agents
|
|
14493
|
+
entry.deferred.content = q.defer();
|
|
14494
|
+
var scriptNode = loadResource(replaceWithContentHostAndVerify(lightningRedirect(contentContainer.contentUrlJs)), function (err) {
|
|
14495
|
+
dom.removeNode(scriptNode);
|
|
14496
|
+
if (err) {
|
|
14497
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14498
|
+
return;
|
|
14499
|
+
}
|
|
14500
|
+
}, true);
|
|
14501
|
+
promises.push(entry.deferred.content.promise);
|
|
14502
|
+
// Style
|
|
14503
|
+
if (contentContainer.contentUrlCss) {
|
|
14504
|
+
entry.deferred.css = q.defer();
|
|
14505
|
+
loadResource({ url: replaceWithContentHostAndVerify(contentContainer.contentUrlCss), type: 'text/css' }, function () {
|
|
14506
|
+
entry.deferred.css.resolve();
|
|
14507
|
+
});
|
|
14508
|
+
promises.push(entry.deferred.css.promise);
|
|
14509
|
+
}
|
|
14510
|
+
}
|
|
14511
|
+
else if (contentContainer.contentUrl && !ConfigReader.get('preventCodeInjection')) {
|
|
14512
|
+
// Content with inlined script and css (2.0.x agents, or guides that have not migrated to the 2.1 format yet)
|
|
14513
|
+
entry.deferred.content = q.defer();
|
|
14514
|
+
var contentNode = loadResource(addExtensionToUrl(replaceWithContentHostAndVerify(contentContainer.contentUrl), '.js'), function (err) {
|
|
14515
|
+
dom.removeNode(contentNode);
|
|
14516
|
+
if (err) {
|
|
14517
|
+
Events.resourceFetchFail.trigger(contentContainer, err);
|
|
14518
|
+
return;
|
|
14519
|
+
}
|
|
14520
|
+
}, true);
|
|
14521
|
+
promises.push(entry.deferred.content.promise);
|
|
14522
|
+
}
|
|
14523
|
+
else if (!fetchedDomContent) {
|
|
14524
|
+
return q.reject('fetchedDomContent is still false');
|
|
14525
|
+
}
|
|
14526
|
+
entry.deferred.promise = q.all(promises).then(function () {
|
|
14527
|
+
return _.omit(entry, 'deferred');
|
|
14528
|
+
});
|
|
14529
|
+
}
|
|
14530
|
+
return cache[contentContainer.id].deferred.promise;
|
|
14531
|
+
}
|
|
14532
|
+
catch (e) {
|
|
14533
|
+
Events.resourceFetchFail.trigger(contentContainer, e);
|
|
14534
|
+
throw e;
|
|
14535
|
+
}
|
|
14536
|
+
}
|
|
14537
|
+
// Clears the content cache. Used for testing.
|
|
14538
|
+
function reset() {
|
|
14539
|
+
cache = {};
|
|
14540
|
+
}
|
|
14541
|
+
/**
|
|
14542
|
+
* JSONP-style callback for loading guide content
|
|
14543
|
+
* from script resources. Step content stored in
|
|
14544
|
+
* GCS is wrapped with pendo.guideContent(...)
|
|
14545
|
+
*
|
|
14546
|
+
* @access private
|
|
14547
|
+
* @param {string} guideId
|
|
14548
|
+
* @param {string} stepId
|
|
14549
|
+
* @param {string} content
|
|
14550
|
+
* @param {Function} templateFn
|
|
14551
|
+
* @param {Function} scriptFn
|
|
14552
|
+
*/
|
|
14553
|
+
function guideContent(guideId, stepId, content, templateFn, scriptFn) {
|
|
14554
|
+
if (_.isString(content)) {
|
|
14555
|
+
content = preprocessGuideContent(content, guideId, stepId);
|
|
14556
|
+
if (!_.isFunction(templateFn)) {
|
|
14557
|
+
if (shouldCreateTemplate(content)) {
|
|
14558
|
+
templateFn = _.template(content);
|
|
14559
|
+
}
|
|
14560
|
+
else {
|
|
14561
|
+
templateFn = _.constant(content);
|
|
14562
|
+
}
|
|
14563
|
+
}
|
|
14564
|
+
var entry = cache[guideId + stepId];
|
|
14565
|
+
if (entry && entry.deferred.content) {
|
|
14566
|
+
entry.content = content;
|
|
14567
|
+
entry.template = templateFn;
|
|
14568
|
+
entry.script = scriptFn;
|
|
14569
|
+
entry.deferred.content.resolve();
|
|
14570
|
+
}
|
|
14571
|
+
}
|
|
14572
|
+
}
|
|
14573
|
+
// preprocess content to remove simple guide and step IDs templating
|
|
14574
|
+
function removeStandardTemplating(content, guideId, stepId) {
|
|
14575
|
+
const stdTemplating = `<% if (typeof guide !== 'undefined') { %>
|
|
14576
|
+
var guide = pendo.findGuideById('<%= guide.id %>');
|
|
14577
|
+
var step = guide && guide.findStepById('<%= step.id %>');
|
|
14578
|
+
<% } %>`;
|
|
14579
|
+
const sansTemplating = `var guide = pendo.findGuideById('${guideId}');
|
|
14580
|
+
var step = guide && guide.findStepById('${stepId}');`;
|
|
14581
|
+
return content.replace(stdTemplating, sansTemplating);
|
|
14582
|
+
}
|
|
14583
|
+
// execute all content preprocessors and return end result
|
|
14584
|
+
const GuideContentProcessorList = [removeStandardTemplating];
|
|
14585
|
+
function preprocessGuideContent(content, guideId, stepId) {
|
|
14586
|
+
return _.reduce(GuideContentProcessorList, (acc, procFn) => {
|
|
14587
|
+
return procFn(acc, guideId, stepId);
|
|
14588
|
+
}, content);
|
|
14589
|
+
}
|
|
14590
|
+
function shouldCreateTemplate(content) {
|
|
14591
|
+
return /<%/.test(content) && /%>/.test(content);
|
|
14592
|
+
}
|
|
14593
|
+
function receiveDomStructureJson(guideId, stepId, domJson) {
|
|
14594
|
+
var entry = cache[guideId + stepId];
|
|
14595
|
+
if (entry && entry.deferred.domJson) {
|
|
14596
|
+
entry.domJson = replaceObjectWithContentHost(domJson);
|
|
14597
|
+
entry.deferred.domJson.resolve();
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
function lightningRedirect(url) {
|
|
14601
|
+
if (isSfdcLightning()) {
|
|
14602
|
+
// https://pendo-static-1234.storage.googleapis.com/guide-content/:guideId/:stepId/:someKindaHash.guide.js
|
|
14603
|
+
var path = url.replace(/^https?:\/\/[^/]+\/guide-content\//, '').split('/');
|
|
14604
|
+
var guideId = path[0];
|
|
14605
|
+
/* global $A */
|
|
14606
|
+
var resourcePath = $A.get('$Resource.pendoGuide' + base32Encode(toUTF8Array(guideId)));
|
|
14607
|
+
if (_.isString(resourcePath)) {
|
|
14608
|
+
return resourcePath + '/' + path.join('/');
|
|
14609
|
+
}
|
|
14610
|
+
}
|
|
14611
|
+
return url;
|
|
14612
|
+
}
|
|
14613
|
+
function addExtensionToUrl(url, extension) {
|
|
14614
|
+
if (!_.isString(url))
|
|
14615
|
+
return url;
|
|
14616
|
+
var i = _.indexOf(url, '?');
|
|
14617
|
+
if (i < 0) {
|
|
14618
|
+
return url + extension;
|
|
14619
|
+
}
|
|
14620
|
+
else {
|
|
14621
|
+
return url.substring(0, i) + extension + url.substring(i);
|
|
14622
|
+
}
|
|
14623
|
+
}
|
|
14624
|
+
return {
|
|
14625
|
+
load,
|
|
14626
|
+
reset,
|
|
14627
|
+
guideContent,
|
|
14628
|
+
receiveDomStructureJson
|
|
14629
|
+
};
|
|
14630
|
+
})();
|
|
14631
|
+
|
|
14308
14632
|
function getHashFromContentUrl(url) {
|
|
14309
14633
|
var contentUrl = parseUrl(url);
|
|
14310
14634
|
var sha1Length = 27;
|
|
@@ -14391,10 +14715,12 @@ var ContentVerifier = (function () {
|
|
|
14391
14715
|
if (!_.isString(url))
|
|
14392
14716
|
return q.reject('unable to parse "' + url + '"');
|
|
14393
14717
|
var stepHash = computeStepHash(contentContainer, urlProp);
|
|
14394
|
-
|
|
14718
|
+
// Verify against the host the loader fetches from; the hash still comes from the original url.
|
|
14719
|
+
var fetchUrl = replaceWithContentHost(url);
|
|
14720
|
+
return ajax.get(fetchUrl).then(function (response) {
|
|
14395
14721
|
if (computeHashFromFile(response.data) === stepHash)
|
|
14396
14722
|
return;
|
|
14397
|
-
return q.reject('Unable to verify content at "' +
|
|
14723
|
+
return q.reject('Unable to verify content at "' + fetchUrl + '"');
|
|
14398
14724
|
});
|
|
14399
14725
|
}
|
|
14400
14726
|
function verifyContentHashUnlessSri(contentContainer, urlProp) {
|
|
@@ -14669,297 +14995,6 @@ function GuideStepFactory(step, guide) {
|
|
|
14669
14995
|
GuideFactory.behaviors = [];
|
|
14670
14996
|
GuideStepFactory.behaviors = [];
|
|
14671
14997
|
|
|
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
14998
|
var AsyncContent = (function () {
|
|
14964
14999
|
function AsyncContent(guide) {
|
|
14965
15000
|
var step = this;
|
|
@@ -15089,7 +15124,7 @@ var GuideRuntime = (function (agentEvents) {
|
|
|
15089
15124
|
}
|
|
15090
15125
|
function teardown() {
|
|
15091
15126
|
EventChannel.off(handleEvent);
|
|
15092
|
-
|
|
15127
|
+
pluginAddGlobalScripts.length = 0;
|
|
15093
15128
|
}
|
|
15094
15129
|
function addGlobalScript(obj) {
|
|
15095
15130
|
obj = autoSubscribe(obj);
|
|
@@ -23533,7 +23568,7 @@ function autofocusRuntimeScript(step, guide, pendo) {
|
|
|
23533
23568
|
if (pendo._.get(step, 'attributes.isAutoFocus')) {
|
|
23534
23569
|
BuildingBlockGuides.autofocusStep(step, guideContainer);
|
|
23535
23570
|
BuildingBlockGuides.trapFocusStep(step, guideContainer);
|
|
23536
|
-
step.hiddenElements = BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]);
|
|
23571
|
+
step.hiddenElements = (step.hiddenElements || []).concat(BuildingBlockGuides.addAriaHiddenOutsideGuide(step.guideElement[0]));
|
|
23537
23572
|
}
|
|
23538
23573
|
if (isGuideRCType(guide)) {
|
|
23539
23574
|
BuildingBlockGuides.attachRCFocusHandlers();
|
|
@@ -25168,22 +25203,31 @@ function hasLegacyGuides(guides, pendo) {
|
|
|
25168
25203
|
}
|
|
25169
25204
|
const DEFAULT_CSS_ID = '_pendo-default-css_';
|
|
25170
25205
|
const CUSTOM_CSS_ID = '_pendo-css_';
|
|
25206
|
+
// Several SDK instances can share a page. Scoping the <link> ids by api key stops one app's
|
|
25207
|
+
// teardown, or its "no custom css" path, from stripping the stylesheet another app injected.
|
|
25208
|
+
function getCssElementId(baseId) {
|
|
25209
|
+
return pendo$1.apiKey ? baseId + pendo$1.apiKey : baseId;
|
|
25210
|
+
}
|
|
25211
|
+
// Api keys are arbitrary customer-supplied strings, so '#' + id is not a safe selector.
|
|
25212
|
+
function removeGuideCss(baseId) {
|
|
25213
|
+
dom(document.getElementById(getCssElementId(baseId))).remove();
|
|
25214
|
+
}
|
|
25171
25215
|
function loadGuideCss() {
|
|
25172
25216
|
var promises = [];
|
|
25173
25217
|
if (!shouldLoadGlobalCSS()) {
|
|
25174
25218
|
return q.resolve();
|
|
25175
25219
|
}
|
|
25176
25220
|
if (hasLegacyGuides(getActiveGuides(), pendo$1)) {
|
|
25177
|
-
promises.push(loadExternalCss(DEFAULT_CSS_ID, getDefaultCssUrl()));
|
|
25221
|
+
promises.push(loadExternalCss(getCssElementId(DEFAULT_CSS_ID), getDefaultCssUrl()));
|
|
25178
25222
|
}
|
|
25179
25223
|
var guideWidget = pendo$1.guideWidget || {};
|
|
25180
25224
|
var data = guideWidget.data || {};
|
|
25181
25225
|
var customCssUrl = data.guideCssUrl;
|
|
25182
25226
|
if (customCssUrl) {
|
|
25183
|
-
promises.push(loadExternalCss(CUSTOM_CSS_ID, replaceWithContentHost(customCssUrl)));
|
|
25227
|
+
promises.push(loadExternalCss(getCssElementId(CUSTOM_CSS_ID), replaceWithContentHost(customCssUrl)));
|
|
25184
25228
|
}
|
|
25185
25229
|
else {
|
|
25186
|
-
|
|
25230
|
+
removeGuideCss(CUSTOM_CSS_ID);
|
|
25187
25231
|
}
|
|
25188
25232
|
return promises;
|
|
25189
25233
|
}
|
|
@@ -25400,8 +25444,8 @@ var initGuides = function (observer) {
|
|
|
25400
25444
|
_.each(teardownFns, function (teardownFn) {
|
|
25401
25445
|
teardownFn();
|
|
25402
25446
|
});
|
|
25403
|
-
|
|
25404
|
-
|
|
25447
|
+
removeGuideCss(DEFAULT_CSS_ID);
|
|
25448
|
+
removeGuideCss(CUSTOM_CSS_ID);
|
|
25405
25449
|
GuideRuntime.teardown();
|
|
25406
25450
|
};
|
|
25407
25451
|
};
|
|
@@ -25871,6 +25915,8 @@ _.extend(TopFrameRelay.prototype, {
|
|
|
25871
25915
|
if (!message.data)
|
|
25872
25916
|
return;
|
|
25873
25917
|
if (message.data.action === 'pendo::connect') {
|
|
25918
|
+
if (!isSameWindowTree(message, this._window))
|
|
25919
|
+
return;
|
|
25874
25920
|
this._impl.frameConnect(message);
|
|
25875
25921
|
}
|
|
25876
25922
|
if (message.data.action === REMOVE_FRAME_ACTION) {
|
|
@@ -25966,6 +26012,38 @@ var ImplCommon = {
|
|
|
25966
26012
|
function isPortValid(port) {
|
|
25967
26013
|
return port && _.all(['addEventListener', 'start'], function (cmd) { return _.isFunction(port[cmd]); });
|
|
25968
26014
|
}
|
|
26015
|
+
// A connect is only ever posted to window.top by a frame below us, so a sender we
|
|
26016
|
+
// cannot find in our own frame tree is not one of our frames no matter what it
|
|
26017
|
+
// claims. window.open gives an attacker a handle to post through, and neither
|
|
26018
|
+
// X-Frame-Options nor frame-ancestors covers that - they only govern embedding
|
|
26019
|
+
// (APP-168708). frames and its indexed children are readable across origins, so
|
|
26020
|
+
// this holds where an origin check can't: no ancestorOrigins needed, and sandboxed
|
|
26021
|
+
// frames (origin "null") are still found.
|
|
26022
|
+
function isFrameBelowUs(source, window) {
|
|
26023
|
+
if (!source)
|
|
26024
|
+
return false;
|
|
26025
|
+
if (source === window)
|
|
26026
|
+
return true;
|
|
26027
|
+
var pending = [window];
|
|
26028
|
+
while (pending.length) {
|
|
26029
|
+
var children = pending.shift().frames;
|
|
26030
|
+
var count = children ? children.length : 0;
|
|
26031
|
+
for (var i = 0; i < count; i++) {
|
|
26032
|
+
if (children[i] === source)
|
|
26033
|
+
return true;
|
|
26034
|
+
pending.push(children[i]);
|
|
26035
|
+
}
|
|
26036
|
+
}
|
|
26037
|
+
return false;
|
|
26038
|
+
}
|
|
26039
|
+
function isSameWindowTree(message, window) {
|
|
26040
|
+
try {
|
|
26041
|
+
return isFrameBelowUs(message.source, window);
|
|
26042
|
+
}
|
|
26043
|
+
catch (e) {
|
|
26044
|
+
return false;
|
|
26045
|
+
}
|
|
26046
|
+
}
|
|
25969
26047
|
function MessageChannelImpl(window, singletonMessageHandler) {
|
|
25970
26048
|
this._window = window;
|
|
25971
26049
|
this._ports = {};
|
|
@@ -43105,6 +43183,124 @@ function substitutionIcon(color, size) {
|
|
|
43105
43183
|
13.7239 15.5 14V17Z" fill="${color}"></path> </g></svg></div>`);
|
|
43106
43184
|
}
|
|
43107
43185
|
|
|
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
|
+
|
|
43108
43304
|
const requiredElement = '<span class="_pendo-required-indicator" style="color:red; font-style:italic;" title="Question is required"> *</span>';
|
|
43109
43305
|
const requiredSyntax = '{required/}';
|
|
43110
43306
|
const RequiredQuestions = {
|
|
@@ -43114,6 +43310,7 @@ const RequiredQuestions = {
|
|
|
43114
43310
|
let requiredPollIds = [];
|
|
43115
43311
|
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
43312
|
const requiredQuestions = processRequiredQuestions();
|
|
43313
|
+
let autoAdvance = createInertController();
|
|
43117
43314
|
if (requiredQuestions) {
|
|
43118
43315
|
pendo._.forEach(requiredQuestions, (question) => {
|
|
43119
43316
|
if (question.classList.contains('_pendo-open-text-poll-question')) {
|
|
@@ -43129,6 +43326,17 @@ const RequiredQuestions = {
|
|
|
43129
43326
|
}
|
|
43130
43327
|
});
|
|
43131
43328
|
}
|
|
43329
|
+
// Armed last, after the listeners above: runCustomScripts abandons the rest of
|
|
43330
|
+
// this script on a throw, and the submit button is already disabled by now.
|
|
43331
|
+
if (isAutoAdvanceRequested(step, pendo._)) {
|
|
43332
|
+
this.on('beforeUnmount', () => autoAdvance.cancel());
|
|
43333
|
+
try {
|
|
43334
|
+
autoAdvance = createAutoAdvanceController(step, pendo, submitButtons, requiredPollIds);
|
|
43335
|
+
}
|
|
43336
|
+
catch (error) {
|
|
43337
|
+
pendo.log('[auto-advance] arming failed, leaving the step on manual submit', error && error.message);
|
|
43338
|
+
}
|
|
43339
|
+
}
|
|
43132
43340
|
function getEligibleQuestions() {
|
|
43133
43341
|
const pollQuestions = pendo._.toArray(step.guideElement.find(`[class*=-poll-question]:contains(${requiredSyntax})`));
|
|
43134
43342
|
const surveyMatches = pendo._.toArray(step.guideElement.find(`[data-pendo-poll-id]:contains(${requiredSyntax})`));
|
|
@@ -43205,9 +43413,11 @@ const RequiredQuestions = {
|
|
|
43205
43413
|
}
|
|
43206
43414
|
if (allRequiredComplete) {
|
|
43207
43415
|
enableEligibleButtons(submitButtons);
|
|
43416
|
+
autoAdvance.notifyAnswered();
|
|
43208
43417
|
}
|
|
43209
43418
|
else {
|
|
43210
43419
|
disableEligibleButtons(submitButtons);
|
|
43420
|
+
autoAdvance.cancel();
|
|
43211
43421
|
}
|
|
43212
43422
|
}
|
|
43213
43423
|
function disableEligibleButtons(buttons) {
|