1function PageUI(selector, config, modelData) { 2 var that = this; 3 4 that.$element = $(selector); 5 that.config = $.extend(true, {}, config); 6 that.modelData = modelData || {}; 7 that.uiFormat = config.uiFormat || CONSTANTS.UIFormats.PollEmbed; 8 that.maxFrameHeight = config.frameHeight || -1; 9 that.displayFormat = config.displayFormat || 0; 10 11 that.processedExternalParams = false; 12 that.externalParams = null; 13 that.activePoll = null; // used for events like facebook shares 14 that.activePolls = []; 15 that.customerId = that.config.custId; 16 that.projectId = that.config.projectId; 17 18 window.tools = window.tools || window.TOOLS; 19 that.initialize(); 20} 21 22PageUI.prototype.initialize = function () { 23 var that = this; 24 switch (this.uiFormat) { 25 case CONSTANTS.UIFormats.ProjectPage: 26 that.initPollsOnPage(); 27 that._assignOnClickEvents(); 28 initProjectPage(); 29 break; 30 case CONSTANTS.UIFormats.PollEmbed: 31 that.initPollsOnPage(); 32 that._assignOnClickEvents(); 33 that._pollPageInit(); 34 break; 35 case CONSTANTS.UIFormats.ProjectCenteredEmbed: 36 that.initPollsOnPage(); 37 that._assignOnClickEvents(); 38 that._centeredEmbedInit(); 39 break; 40 default: 41 break; 42 } 43 44 if ((that.config.embed || that.config.embedId > 0) && !that.config.offsite) {
45 that._sendResizeMessage(); 46 setInterval(function () { 47 that._sendResizeMessage(); 48 }, 1000); 49 parent.postMessage("session_" + that.config.sessionId, "*"); 50 that._listenForMessages(); 51 } 52 function initProjectPage() { 53 if (that._projectPageInit) { 54 that._projectPageInit(); 55 } else { 56 setTimeout(function () { 57 initProjectPage(); 58 }, 100); 59 } 60 } 61 that._forgetMeNot(); 62}; 63 64PageUI.prototype.initPollsOnPage = function () { 65 var that = this; 66 var $pollElements = that.$element.find(".poll-content-div"); 67 68 $.each($pollElements, function (index, pollElement) { 69 var $outerDiv = $(pollElement).parent(); 70 that.activePolls[index] = new Poll($outerDiv, that); 71 that.activePolls[index].index = index; 72 }); 73 that.activePoll = that.activePolls.length ? that.activePolls[0] : {}; 74 75 if (that._disableAutoCompleteIfDataEntry) { 76 that._disableAutoCompleteIfDataEntry(); 77 } 78 return true; 79}; 80 81PageUI.prototype.initPollsInTargetDiv = function ($target) { 82 var that = this; 83 var $pollElements = $target.find(".poll-content-div"); 84 var ret = []; 85 $.each($pollElements, function (index, pollElement) { 86 var pollId = $(pollElement).find(".poll-meta").data("pollid"); 87 var activePollsIndex = findIndexByKeyValue(that.activePolls, "id", pollId); 88 89 if (activePollsIndex < 0) { 90 activePollsIndex = that.activePolls.length; 91 } 92 var $outerDiv = $(pollElement).parent(); 93 that.activePolls[activePollsIndex] = new Poll($outerDiv, that); 94 that.activePolls[activePollsIndex].index = activePollsIndex; 95 ret.push(that.activePolls[activePollsIndex]); 96 }); 97 that.activePoll = ret.length ? ret[0] : {}; 98 99 if (that._disableAutoCompleteIfDataEntry) { 100 that._disableAutoCompleteIfDataEntry(); 101 } 102 return ret; 103}; 104 105PageUI.prototype._pollPageInit = function () { 106 var that = this; 107 if (that.config.displayFormat == 2) { 108 that._initChatBot(); 109 } 110}; 111 112PageUI.prototype.logOut = function () { 113 if (this.activePoll) { 114 this._sendGAEvent( 115 "useractions", 116 "logout", 117 "question_" + this.activePoll.id + "-" + this.activePoll.description 118 ); 119 } 120 window.location = "/Display/LogOut?r=" + encodeURIComponent(document.URL); 121}; 122 123PageUI.prototype._sendResizeMessage = function () { 124 if (this.config.embedId < 1 || this.activePoll == null) { 125 return; 126 } 127 var height = Math.max( 128 document.body.offsetHeight, 129 document.documentElement.offsetHeight 130 ); 131 if ($(".scrollable").length && $(".scrollable")[0].scrollHeight > height) { 132 // issue with padding causing looping of +20 px 133 height = Math.max(height, $(".scrollable")[0].scrollHeight); 134 } 135 136 if (this.maxFrameHeight > 0) { 137 tools.applyMaxHeightScrolling(this.$element, this.maxFrameHeight); 138 } 139 parent.postMessage( 140 "cityzen_poll_" + 141 this.activePoll.id + 142 "_cont_" + 143 this.config.contId + 144 "_height_" + 145 height + 146 "_embed_" + 147 this.config.embedId + 148 "_adSet_" + 149 this.config.adSetId + 150 "_proj_" + 151 this.config.projectId + 152 "_", 153 "*" 154 ); 155}; 156 157PageUI.prototype._sendActivePing = function (url) { 158 url = url || ""; 159 this.activePingCt = this.activePingCt || 0; 160 this.activePingDiv = this.activePingDiv || document.createElement("div"); 161 if (this.activePingCt > 50) { 162 return; 163 } 164 document.body.appendChild(this.activePingDiv); 165 var st = document.createElement("img"); 166 st.height = 1; 167 st.width = 1; 168 st.style.display = "none"; 169 st.src = url; 170 this.activePingDiv.innerHTML = ""; 171 this.activePingDiv.appendChild(st); 172 this.activePingCt++; 173}; 174 175PageUI.prototype._initChatBot = function () { 176 var that = this; 177 ScriptLoader.loadScriptArray( 178 ScriptLoader.scriptArrays.chatBotScripts, 179 function () { 180 var $chat = this.$(document).find(".chat"); 181 that.chatWidget = CitizenChatWidget.initialize({ 182 widgetContainer: $chat, 183 dataLoadUrl: "/Display/ChatBot", 184 messagePreloaderDuration: 1000, 185 acceptResponseMessage: "Got it!", 186 dataLoadErrorMessage: 187 "There was an error connecting to the server. Please, try again later." 188 }); 189 } 190 ); 191}; 192 193PageUI.prototype._assignOnClickEvents = function () { 194 var that = this; 195 196 that.$element.on("click", ".social.fb, .btn-share.facebook", function () { 197 window.fbAsyncInit = function () { 198 FB.init({ 199 appId: cz.facebookAppId, 200 version: "v2.0", 201 status: true, // check login status 202 cookie: true, // enable cookies to allow the server to access the session 203 xfbml: true // parse XFBML 204 }); 205 }; 206 (function (d, s, id) { 207 var js, 208 fjs = d.getElementsByTagName(s)[0]; 209 if (d.getElementById(id)) { 210 return; 211 } 212 js = d.createElement(s); 213 js.id = id; 214 js.src = "//connect.facebook.net/en_US/sdk.js"; 215 fjs.parentNode.insertBefore(js, fjs); 216 })(document, "script", "facebook-jssdk"); 217 that._facebookShare(); 218 }); 219 220 that.$element.on("click", ".social.tw, .btn-share.twitter", function () { 221 that._twitterShare(); 222 }); 223 224 that.$element.on("click", ".btn-share.google", function () { 225 that._googleShare(); 226 }); 227 228 that.$element.on("click", ".btn-share.linkedin", function () {
229 that._linkedInShare(); 230 }); 231 232 that.$element.on("click", ".btn-share.email", function () { 233 that._emailShare(); 234 }); 235 236 that.$element.on("click", ".viz.btn", function () { 237 that.loadViz(false); 238 }); 239 240 that.$element.on("click", ".move-up", function () { 241 that._adminAdjustOffset(-1); 242 }); 243 that.$element.on("click", ".move-down", function () { 244 that._adminAdjustOffset(1); 245 }); 246 247 that.$element.on("click", ".cz-x", function () { 248 that.$element.hide(); 249 }); 250 251 $(window).resize(function () { 252 that._sendResizeMessage(); 253 }); 254 255 that.$element.on("click", ".log-out", function () { 256 that.logOut(); 257 }); 258}; 259 260PageUI.prototype._userResponseCallback = function (data) { 261 var that = this; 262 switch (this.uiFormat) { 263 case CONSTANTS.UIFormats.ProjectPage: 264 if (that.submitSurveyUI) { 265 that.submitSurveyUI.refreshEndzone(); 266 } 267 if (that.stepSurveyUI && that.stepSurveyUI.stepHasBeenSubmitted) { 268 that.stepSurveyUI._checkFieldsComplete(); 269 } 270 break; 271 case CONSTANTS.UIFormats.PollEmbed: 272 break; 273 default: 274 break; 275 } 276 if (data && data.dataPartners) { 277 if (data.dataPartners.blueConic) { 278 that.sendXDomainObject(data.dataPartners); 279 } 280 } 281 282 if (window.recaptchaService) { 283 window.recaptchaService.run("Response"); 284 } 285}; 286 287PageUI.prototype._sendGAEvent = function (cat, action, label) { 288 // GA4 uses gtag, UA uses ga 289 var gtagExists = typeof gtag === "function"; 290 var gaExists = typeof ga === "function"; 291 292 // --- Internal PublicInput Tracking --- 293 // Always send to internal GA4 if gtag is available 294 if (gtagExists) { 295 gtag("event", action, { 296 event_category: cat, 297 event_label: label, 298 // Ensure project_id and customer_id are sent if available 299 // These might need to be fetched from this.config or this.modelData if not implicitly sent 300 project_id: this.config.projectId, 301 customer_id: this.config.custId 302 }); 303 } 304 305 // --- Customer Tracking --- 306 if (this.config.gaOn && this.config.cgaId && this.config.cgaId.length) { 307 var customerGaId = this.config.cgaId; 308 var customerGaCategory = this.config.cgaCat || cat; // Use specific customer category if provided, else default 309 310 // Check if customer is using GA4 or UA 311 var isCustomerGa4 = customerGaId.startsWith("G-"); 312 var isCustomerUa = customerGaId.startsWith("UA-"); 313 314 if (isCustomerGa4 && gtagExists) { 315 // Send event to Customer's GA4 property 316 gtag("event", action, { 317 event_category: customerGaCategory, 318 event_label: label, 319 // Include project/customer IDs for customer GA4 as well 320 project_id: this.config.projectId, 321 customer_id: this.config.custId, 322 send_to: customerGaId // Route this event specifically to customer's GA4 323 }); 324 } else if (isCustomerUa && gaExists) { 325 // Send event to Customer's UA property (using named tracker 'cga')
326 try { 327 ga("cga.send", "event", customerGaCategory, action, label); 328 } catch (e) { 329 console.error( 330 "Failed to send event to customer UA tracker ('cga'). Was it initialized?", 331 e 332 ); 333 } 334 } else if (gaExists) { 335 // Fallback/Legacy: If customer ID format isn't recognized but ga exists, try the old UA method. 336 // This might happen if the ID format changes or is unexpected. 337 console.warn( 338 "Customer GA ID format not recognized as GA4 or UA, but 'ga' exists. Attempting legacy UA send.", 339 customerGaId 340 ); 341 try { 342 ga("cga.send", "event", customerGaCategory, action, label); 343 } catch (e) { 344 console.error( 345 "Failed to send fallback event to customer UA tracker ('cga').", 346 e 347 ); 348 } 349 } else { 350 console.warn( 351 "Customer GA is enabled but required GA function (gtag for GA4, ga for UA) not found." 352 ); 353 } 354 } 355}; 356 357PageUI.prototype._listenForMessages = function () { 358 var that = this; 359 if (this.config.offsite > 0) { 360 return; 361 } 362 363 var eventMethod = addEventListener ? "addEventListener" : "attachEvent", 364 listnr = window[eventMethod], 365 messageEvent = eventMethod == "attachEvent" ? "onmessage" : "message"; 366 this._userConflictReported = false; 367 368 listnr(messageEvent, function (e) { 369 var key = e.message ? "message" : "data"; 370 var data = e[key]; 371 var xDomainString = data.toString(); 372 if (xDomainString.substring(0, 10) == "cityzenurl") { 373 var split = data.split("_!_"); 374 that.config.topURL = split[1]; 375 } else if ( 376 xDomainString.substring(0, 5) == "czunl" && 377 !this.visualizerStarted 378 ) { 379 that.loadViz(true); 380 parent.postMessage("loaded", "*"); 381 } else if ( 382 xDomainString.substring(0, 6) == "czconf" && 383 !this._userConflictReported 384 ) { 385 that._userConflictCheck(data.split("_")[1], data.split("_")[2], true); 386 that._userConflictReported = true; 387 } else if (xDomainString.substring(0, 10) == "cityzen_ep") { 388 var extParams = JSON.parse(data.split("_")[2]); 389 that._processExternalParams(extParams); 390 } 391 }); 392}; 393 394PageUI.prototype._forgetMeNot = function () { 395 var that = this; 396 if (this.config.offsite > 0) { 397 return; 398 } 399 var userId = this.config.userId; 400 try { 401 if (typeof localStorage != "undefined" && localStorage != null) { 402 var localUserId = localStorage.CityzenView; 403 if (localUserId == null && userId != "" && userId != "undefined") { 404 localStorage.setItem("CityzenView", userId); 405 } else if (localUserId != userId) { 406 that._userConflictCheck(localUserId, userId); 407 } 408 } 409 } catch (err) { 410 console.warn( 411 "PublicInput local storage warn: localStorage not available; falling back to cookies" 412 ); 413 } 414}; 415 416PageUI.prototype._adminAdjustOffset = function (delta) { 417 var that = this; 418 if (this._offsetSendTimeout) { 419 clearTimeout(this._offsetSendTimeout); 420 } 421 this.config.offset = this.config.offset || 0; 422 parent.postMessage( 423 "cityzen_" + this.config.embedId + "_offset_" + delta + "_", 424 "*" 425 ); 426 427 this.config.offset += delta; 428 this._offsetSendTimeout = setTimeout(function () { 429 $.post("/Embed/AdjustOffset", that.config, function () { 430 that.config.offset = 0; 431 }); 432 }, 2000); 433}; 434 435PageUI.prototype._startWaiting = function (poll, message) { 436 var that = this; 437 $("head").append( 438 "<link rel='stylesheet' type='text/css' href='/Content/Spinner.css' />" 439 ); 440 this._popLogin(poll, function () { 441 that._keepTrying(poll, message); 442 }); 443}; 444PageUI.prototype._keepTrying = function (poll, message) { 445 var that = this; 446 this.tryCount = this.tryCount || 0; 447 if (this.tryCount > 200) { 448 return; 449 } 450 if (that.tryCount == 0) { 451 _showLoginPrompt(); 452 } else { 453 setTimeout(function () { 454 _showLoginPrompt(); 455 }, 3000); 456 } 457 function _showLoginPrompt() { 458 var loginText = 459 message == "validate" 460 ? "Someone from this IP address has already participated, and we need to verify you're a different person. Please sign in with an email or social account to participate." 461 : "Please sign in to participate:"; 462
463 var rUrl = that._getLoginWindowUrl(poll); 464 465 var loginUrl = that._getLoginURL(rUrl); 466 var customText = 467 loginText + 468 '<br><a href="' + 469 loginUrl + 470 '" class="btn btn-success m-t" target="_blank">Sign in with email or social account</a>'; 471 poll._overlayMessage(customText, true); 472 473 poll._vote(); 474 that.tryCount++; 475 } 476}; 477PageUI.prototype._getLoginURL = function (returnURL) { 478 var that = this; 479 return ( 480 "/Account/Login?projId=" + 481 that.config.projectId + 482 "&promptAlerts=true" + 483 (returnURL == null ? "" : "&returnUrl=" + returnURL) 484 ); 485}; 486// function PopLogin() { 487PageUI.prototype._popLogin = function (poll, callback) { 488 var that = this; 489 var commentData = poll.commentData || {}; 490 that.requestNumber = this.requestNumber || 0; 491 that.requestNumber++; 492 493 var rUrl = that._getLoginWindowUrl(poll); 494 that.popupWindow = window.open( 495 that._getLoginURL(rUrl), 496 "loginPop", 497 tools.getWindowFeatures(500, 300) 498 ); 499 if (callback) { 500 callback(); 501 } 502}; 503 504PageUI.prototype._getLoginWindowUrl = function (poll) { 505 var that = this; 506 var commentData = poll.commentData || {}; 507 var postData = poll.config(); 508 postData.optionString = (poll.selectedOptions || []).join(","); 509 510 if (commentData.comment && commentData.comment.length > 0) { 511 postData.parentId = commentData.parentId; 512 postData.isPrivate = commentData.isPrivate; 513 postData.comment = commentData.comment; 514 return encodeURIComponent( 515 that.config.urlPrefix + 516 "/Comments/Comment/?closeWindow=true&" + 517 TOOLS.serialize(postData) + 518 "&redirect=" + 519 encodeURIComponent(window.location.href) 520 ); 521 } else { 522 return encodeURIComponent( 523 that.config.urlPrefix + 524 "/Vote?closeWindow=true&" + 525 TOOLS.serialize(postData) + 526 "&redirect=" + 527 encodeURIComponent(window.location.href) 528 ); 529 } 530}; 531 532PageUI.prototype.loadViz = function (openedAutomatically) { 533 var that = this; 534 if (this.visualizerStarted) { 535 return; 536 } 537 this.visualizerStarted = true; 538 var $vizTarget = that.$element.find("#viz-target"), 539 $vizPreview = that.$element.find("#viz-preview"), 540 $vizButton = that.$element.find("#viz-btn"); 541 542 $vizPreview.show().html(tools.spin(150, "Loading Visualizer", true)); 543 $.getScript("//d3js.org/d3.v3.min.js", function () { 544 $.post("/Display/LoadFib", { projId: that.config.projectId }).done( 545 function (data) { 546 $vizTarget.html(data); 547 $vizPreview.fadeOut(500); 548 $vizButton.fadeOut(500); 549 $vizTarget.slideDown(1000, function () {
550 that._sendResizeMessage(); 551 }); 552 that._sendResizeMessage(); 553 that._sendGAEvent( 554 "useractions", 555 (openedAutomatically ? "Auto" : "") + "Opened Visualizer", 556 "" 557 ); 558 } 559 ); 560 }); 561}; 562 563PageUI.prototype._processExternalParams = function (extParams) { 564 var that = this; 565 extParams = extParams || { 566 goTo: tools.getURLParameter("goTo"), 567 scrollMe: tools.getURLParameter("scrollMe"), 568 voteOption: tools.getURLParameter("voteOption"), 569 embedId: that.config.embedId 570 }; 571 if ( 572 extParams.embedId == that.config.embedId && 573 extParams.goTo != null && 574 !that._processedExternalParams && 575 that.config.goTo != extParams.goTo 576 ) { 577 if (that.config.pollList.indexOf(String(extParams.goTo)) > -1) { 578 that.externalParams = extParams; 579 580 parent.postMessage("cityzen_ep_" + JSON.stringify(extParams), "*"); 581 if (that.displayFormat == 2 && that.chatWidget) { 582 that.chatWidget.ProcessExternalParams( 583 extParams.goTo, 584 extParams.voteOption 585 ); 586 that.processedExternalParams = true; 587 } else { 588 that.activePoll.refreshPoll(); 589 that.config.goTo = -1; 590 that.config.voteOption = -1; 591 that.processedExternalParams = true; 592 } 593 } 594 } 595 596 if ( 597 that.config.uiFormat == CONSTANTS.UIFormats.ProjectPage && 598 !!extParams.scrollMe && 599 extParams.scrollMe.toString().toLowerCase() == "true" && 600 !!extParams.goTo 601 ) { 602 document.addEventListener("readystatechange", function (event) { 603 if (event.target.readyState === "complete" && that.doScrolling) { 604 that.doScrolling( 605 document.getElementById("pluginDiv-poll-" + extParams.goTo), 606 2000 607 ); 608 } 609 }); 610 } 611}; 612 613PageUI.prototype.sendXDomainObject = function (objectToPass) { 614 var xDomainObject = { 615 name: "cityzenJSON", 616 embedId: this.config.embedId 617 }; 618 $.extend(true, xDomainObject, objectToPass); 619 parent.postMessage(JSON.stringify(xDomainObject), "*"); 620}; 621 622PageUI.prototype._facebookShare = function () { 623 var that = this; 624 var url; 625 var poll = this.activePoll; 626 if (this.config.uiFormat == CONSTANTS.UIFormats.ProjectPage) { 627 url = 628 "https://www.facebook.com/sharer/sharer.php?u=" + window.location.href; 629 } else { 630 var redirect = encodeURIComponent( 631 window._JSCONFIG.PrimaryWebDomainPlusHttps + 632 "/ActionCall/Share?pollId=" + 633 poll.id + 634 "&uid=" + 635 that.config.userId + 636 "&contId=" + 637 that.config.contId + 638 "&shareURL=" + 639 that.config.topURL + 640 "&topURL=" + 641 that.config.topURL 642 ); 643 url = 644 "https://www.facebook.com/dialog/feed?" + 645 "app_id=" + 646 that.config.facebookAppId + 647 "&display=popup&status=true&cookie=true" + 648 "&caption=" + 649 poll.caption.replace(/&/g, "&") + 650 (that.config.topURL == "" 651 ? "&name=" + poll.description.replace(/&/g, "&") 652 : "") + 653 "&link=" + 654 encodeURIComponent(that.config.topURL) + 655 "&redirect_uri=" + 656 redirect; 657 } 658 659 var newWin = window.open( 660 url, 661 "Facebook Share", 662 tools.getWindowFeatures(400, 600) 663 ); 664 tools.popupBlockerChecker.check( 665 newWin, 666 function () { 667 that._sendGAEvent( 668 "useractions", 669 "Facebook Popup Not Found", 670 "poll" + poll.id + "-" + poll.description 671 ); 672 }, 673 function () { 674 that._sendGAEvent( 675 "useractions", 676 "Facebook Popup Opened", 677 "poll" + poll.id + "-" + poll.description 678 ); 679 } 680 ); 681}; 682 683PageUI.prototype._twitterShare = function () { 684 var that = this; 685 var url; 686 var poll = this.activePoll; 687 if (this.config.uiFormat == CONSTANTS.UIFormats.ProjectPage) { 688 url = 689 "https://twitter.com/share?url=" + 690 window.location.href + 691 "&text=" + 692 encodeURIComponent(that.modelData.project.name); 693 } else { 694 url = 695 "//twitter.com/intent/tweet?&text=" + 696 encodeURIComponent(
697 poll.description.replace(/&/g, "&") + 698 " - " + 699 poll.caption.replace(/&/g, "&") 700 ) + 701 "&url=" + 702 that.config.topURL; 703 } 704 705 var newWin = window.open( 706 url, 707 "Twitter Share", 708 tools.getWindowFeatures(300, 600) 709 ); 710 tools.popupBlockerChecker.check( 711 newWin, 712 function () { 713 that._sendGAEvent( 714 "useractions", 715 "Twitter Popup Not Found", 716 "poll" + poll.id + "-" + poll.description 717 ); 718 }, 719 function () { 720 that._sendGAEvent( 721 "useractions", 722 "Twitter Popup Opened", 723 "poll" + poll.id + "-" + poll.description 724 ); 725 } 726 ); 727}; 728 729PageUI.prototype._googleShare = function () { 730 window.open( 731 "https://plus.google.com/share?url=" + window.location.href, 732 "", 733 tools.getWindowFeatures(400, 600) 734 ); 735}; 736 737PageUI.prototype._emailShare = function () { 738 var that = this; 739 window.open( 740 "http://api.addthis.com/oexchange/0.8/forward/email/offer?ct=1&pubid=ra-58f47be8ccff430b&url=" + 741 window.location.href + 742 "%2f&title=" + 743 that.modelData.project.name, 744 "", 745 tools.getWindowFeatures(400, 600) 746 ); 747}; 748 749PageUI.prototype._linkedInShare = function () { 750 window.open( 751 "https://www.linkedin.com/shareArticle?mini=true&url=" + 752 window.location.href, 753 "", 754 tools.getWindowFeatures(400, 600) 755 ); 756}; 757 758PageUI.prototype._userConflictCheck = function (u1, u2, postUpdate) { 759 var that = this; 760 /* 761 * this._userConflictChecked = this._userConflictChecked || false; 762 * if (this.config.offsite > 0 || u1 == null || u2 == null || this._userConflictChecked) 763 * return; 764 * var postData = this.config; 765 * postData.u1 = u1; 766 * postData.u2 = u2; 767 */ 768 769 /* 770 * $.post('/Sync/rc/', postData, function (msg) { 771 * that._userConflictChecked = true; 772 * that.config.userId = msg.winner; 773 * if (msg.winner != '' && msg.winner != 'undefined') 774 * localStorage.setItem('CityzenView', msg.winner); 775 * if (postUpdate) 776 * parent.postMessage('update_' + msg.winner, "*"); 777 * }); 778 */ 779}; 780 781PageUI.prototype.openLightLogin = function (callback) { 782 var that = this; 783 tools.requireCSSFile("/Content/login.css"); 784 that.lightLoginCallbacks = that.lightLoginCallbacks || []; 785 if (callback && that.lightLoginCallbacks.indexOf(callback) === -1) { 786 that.lightLoginCallbacks.push(callback); 787 } 788 789 var $lightLoginModal = $("#light-login-modal"); 790 $lightLoginModal.modal("show"); 791 792 $lightLoginModal.find('input[name="email"]').focus(); 793 794 if ($lightLoginModal.data("submit-action-assigned") == true) { 795 return; 796 } 797 798 $lightLoginModal.find("form.light-login-form").on("submit", function (e) { 799 e.preventDefault(); 800 that.lightLoginSubmit($(this)); 801 }); 802 $lightLoginModal.data("submit-action-assigned", true); 803}; 804 805PageUI.prototype.lightLoginSubmit = function ($form) { 806 var that = this; 807 var formData = $form.serializeObject(); 808 $form.hide(); 809 that.$lightLoginLoader = $('<div class="light-login-loading relative"></div>') 810 .append(tools.spin(150, "Getting things ready...")) 811 .height(200); 812 $form.parent().find(".light-login-loading").hide(); 813 $form.after(that.$lightLoginLoader); 814 $.post($form.attr("action"), formData).done(function (data) { 815 that.lightLoginCompletion($form, data); 816 }); 817}; 818 819PageUI.prototype.lightLoginCompletion = function ($form, data) { 820 var that = this; 821 that.lightLoginPendingPolls = that.lightLoginPendingPolls || []; 822 if (data.result == "success") { 823 that.modelData.userData = data.userData; 824 that.config.userId = data.userData.userId; 825 that.config.participantUserId = data.userData.userId; 826 that.config.ParticipantUserId = null; 827 that.config.UserId = null; 828 $("#light-login-modal").modal("hide"); 829 $.each(that.activePolls, function (idx, poll) { 830 if (that.lightLoginPendingPolls.indexOf(poll.id) < 0) { 831 poll.refreshPoll(0); 832 } 833 }); 834 if (that.lightLoginCallbacks) { 835 that.lightLoginCallbacks.forEach(function (callback) { 836 if (callback) { 837 callback(data); 838 } 839 }); 840 } 841 } else { 842 $form.find(".alert").hide(); 843 var $alert = $form.find(".alert").first().clone(); 844 $alert.show().find("p").text(data.message); 845 $form.find(".alert").first().after($alert); 846 847 $form.parent().find(".light-login-loading").hide(); 848 } 849 $form.show(); 850 $form.parent().find(".light-login-loading").hide(); 851}; 852 853PageUI.prototype._projectPageInit = function () { 854 var that = this; 855 that._processExternalParams(); 856 if (window.SubmitSurveyUI != null) { 857 that.submitSurveyUI = new SubmitSurveyUI(that); 858 } 859 if (window.StepSurveyUI != null) { 860 that.stepSurveyUI = new StepSurveyUI(this); 861 } 862 863 var fullWidth = $(".project-content").data("full-width"); 864 var showSidebar = $(".project-content").data("show-sidebar"); 865 if (typeof fullWidth !== "undefined") { 866 applyStepLayout(fullWidth, showSidebar); 867 } 868 869 setTimeout(function () { 870 that._formatPageHeight(); 871 }, 2000); 872 $(window).resize(function () { 873 that._formatPageHeight(); 874 }); 875 that._startPlaceHolderTranslationLoop(); 876 that._initKioskForms();
877 that._loadSupportTool(); 878 that._loadSlimScroll(); 879 that._loadImageZoom(); 880 that.initDomLibraries(); 881 that._loadVizTools(); 882 that._initOfflineEvents(); 883 that._populateTopicsUI(); // TODO include if for project type = topics 884 that._disableAutoCompleteIfDataEntry(); 885 that._initFAQClickEvents(); 886}; 887 888PageUI.prototype._centeredEmbedInit = function () { 889 var that = this; 890 that._formatPageHeight(); 891 $(window).resize(that._formatPageHeight()); 892 that._loadSupportTool(); 893 that._loadSlimScroll(); 894}; 895 896PageUI.prototype._loadVizTools = function () { 897 var that = this; 898 that.$element.find("#viz-target").each(function (index, element) { 899 if (that.config.voteTotal > 1) { 900 that.loadViz(); 901 } 902 }); 903}; 904PageUI.prototype.initDomLibraries = function () { 905 var that = this; 906 907 // WCAG 1.4.13: route through the shared accessible initializer (BootstrapShims.js) 908 // when present so tooltips are dismissable, hoverable, and persistent. 909 if (window.initializeTooltips) { 910 // .tooltip-info elements lack the toggle attribute; tag them so the shared 911 // initializer picks them up alongside [data-toggle=tooltip]. 912 that.$element.find(".tooltip-info").attr("data-bs-toggle", "tooltip"); 913 window.initializeTooltips(that.$element); 914 } else if ($.fn.tooltip) { 915 that.$element.find("[data-toggle=tooltip]").tooltip(); 916 that.$element.find(".tooltip-info").tooltip(); 917 } 918}; 919 920PageUI.prototype._initFAQClickEvents = function () { 921 var that = this; 922 923 if (that.$element.data("faq-click-events-set") === true) { 924 return; 925 } 926 927 // Custom FAQ click handling 928 that.$element.on("click", ".faq-link", function (e) { 929 e.preventDefault(); 930 var $this = $(this); 931 var $parent = $this.closest(".faq-panel-root"); 932 var $collapse = $parent.find(".faq-panel-collapse"); 933 934 // Custom behavior for hiding 935 if ($collapse.hasClass("in")) { 936 $parent.removeClass("zero-opacity"); 937 938 var $nextPanelRoot = $parent.next(".faq-panel-root"); 939 if ($nextPanelRoot.length) { 940 $nextPanelRoot.removeClass("zero-opacity"); 941 } 942 $collapse.removeClass("show in"); 943 } else { 944 // Custom behavior for showing 945 $parent.addClass("zero-opacity"); 946 $collapse.addClass("show in"); 947 } 948 }); 949 950 that.$element.data("faq-click-events-set", true); 951}; 952 953PageUI.prototype._loadSupportTool = function () { 954 var that = this; 955 if (that.config.embedId > 0) { 956 } 957 // Removed for help link at bottom 958}; 959 960PageUI.prototype._populateTopicsUI = function () { 961 $(".noImage").each(function () { 962 var text = $(this).closest(".row").find(".projName").text().split(" "); 963 var abbr; 964 if (text.length >= 2) { 965 abbr = String(text[0].charAt(0)) + text[1].charAt(0); 966 } else { 967 abbr = text[0].charAt(0); 968 } 969 $(this).text(abbr); 970 }); 971}; 972 973PageUI.prototype._loadImageZoom = function ($element) { 974 var that = this; 975 var selector = $element == null ? "#content img" : "img"; 976 $element = $element || that.$element; 977 var $imagesNeedingZoom = $element 978 .find(selector) 979 .not(".no-zoom") 980 .not(".project-footer-html img"); 981 if ($imagesNeedingZoom.length == 0) { 982 return; 983 } 984 985 $imagesNeedingZoom.attr("data-action", "zoom"); 986 ScriptLoader.loadScriptArray( 987 ScriptLoader.scriptArrays.magnificPopup, 988 function () { 989 tools.requireCSSFile("/lib/magnific-popup.js/magnific-popup.min.css"); 990 $("[data-action='zoom']").each(function () { 991 var $img = $(this); 992 if ($img.parent().is("a") || $img.parents(".map-poll").length) { 993 return; 994 } 995 var zoomSrc = 996 $img.data("original") || $img.data("zoom-src") || $img.attr("src"); 997 var $a = $("<a>") 998 .attr("href", zoomSrc) 999 .attr("title", $img.attr("title") || $img.attr("alt")) 1000 .attr( 1001 "aria-label", 1002 "Zoom image " + ($img.attr("title") || $img.attr("alt")) 1003 ) 1004 .addClass("image-zoom-popup"); 1005 $img.wrap($a); 1006 }); 1007 $(".image-zoom-popup").magnificPopup({ 1008 closeOnContentClick: false, 1009 type: "image", 1010 closeBtnInside: true, 1011 autoFocusLast: true, 1012 image: { 1013 verticalFit: true 1014 }, 1015 zoom: { 1016 enabled: true, 1017 duration: 300 // don't foget to change the duration also in CSS 1018 } 1019 }); 1020 1021 /* 1022 * if (that.imageZoomClickInitiated) 1023 * return; 1024 * $(document).on("click", ".mfp-img", function () { 1025 * var zoom = parseInt($(this).css("zoom")); 1026 * if (zoom == 180) { 1027 * $(this).css("zoom", "100%"); 1028 * } else { 1029 * $(this).css("zoom", zoom + 20 + "%"); 1030 * } 1031 * }); 1032 * that.imageZoomClickInitiated = true; 1033 */ 1034 } 1035 ); 1036}; 1037 1038PageUI.prototype._loadSlimScroll = function () { 1039 var that = this; 1040 that.$element.find(".no-touch .slim-scroll").each(function () { 1041 var $self = $(this), 1042 $data = $self.data(), 1043 $slimResize; 1044 $self.slimScroll($data); 1045 $(window).resize(function (e) { 1046 clearTimeout($slimResize); 1047 $slimResize = setTimeout(function () { 1048 $self.slimScroll($data); 1049 }, 500); 1050 });
1051 $(document).on("updateNav", function () { 1052 $self.slimScroll($data); 1053 }); 1054 }); 1055}; 1056 1057PageUI.prototype._formatPageHeight = function () { 1058 var that = this; 1059 if (that.config.embedId > 0) { 1060 return; 1061 } 1062 1063 var combinedHeaderHeight = 0; 1064 var pageWidth = $(window).width(); 1065 var $projectMain = $("#project-main"); 1066 1067 // Measure everything actually stacked above #project-main, not just <header> elements. 1068 // Customer-supplied header HTML (HeaderFooterStyleSetting.ProjectHeaderHtml) is rendered 1069 // as whatever tag the customer wrote, often a <div>, and was previously left out of the 1070 // offset, so the first rows of main content sat underneath it on desktop (AB#10619). 1071 // Fixed/absolute siblings (mobile drawers, cookie bars) take no vertical space, so skip them. 1072 // CenterEmbed.cshtml has no #project-main; there the headers are still the whole stack. 1073 if ($projectMain.length) { 1074 $projectMain.prevAll(":visible").each(function () { 1075 var position = $(this).css("position"); 1076 1077 if (position === "fixed" || position === "absolute") { 1078 return; 1079 } 1080 1081 combinedHeaderHeight += $(this).outerHeight(); 1082 }); 1083 } else { 1084 $(".vbox > header").each(function () { 1085 combinedHeaderHeight += $(this).outerHeight(); 1086 }); 1087 } 1088 1089 $(".vbox > header").each(function () { 1090 if ($(this).hasClass("notranslate")) { 1091 $("body").css("top", ""); 1092 setTimeout(function () { 1093 $("body").css("top", "40px"); 1094 }, 400); 1095 } 1096 }); 1097 1098 if ( 1099 window.CONSTANTS && 1100 this.config.uiFormat == CONSTANTS.UIFormats.ProjectCenteredEmbed 1101 ) { 1102 if ( 1103 pageWidth < 768 && 1104 $(".push-full").height() + combinedHeaderHeight < $(window).outerHeight() 1105 ) { 1106 $(".push-full").height($(window).outerHeight() - combinedHeaderHeight); 1107 } else { 1108 $(".push-full").height(null); 1109 } 1110 } 1111 1112 var $footer = that.$makeExistFooter(); 1113 1114 //on pages that do not fill the full height, set the push div margins to equal the footer height 1115 $(".push-full").css("margin-bottom", $footer.height() * -1); 1116 $(".push-footer").css("height", $footer.height()); 1117 1118 if (pageWidth < 768) { 1119 $projectMain.css("top", "0").css("cssText", "padding-top: 0px !important;"); 1120 } else { 1121 $projectMain.css("top", combinedHeaderHeight).css("padding-top", ""); 1122 } 1123}; 1124 1125PageUI.prototype.doScrolling = function (element, duration) { 1126 if (element) { 1127 setTimeout(function () { 1128 element.scrollIntoView({ behavior: "smooth", block: "start" }); 1129 }, duration); 1130 } 1131}; 1132 1133PageUI.prototype._startPlaceHolderTranslationLoop = function () { 1134 function isTranslated() { 1135 // true if the text has been translated 1136 var currentPH = $($("#placeholders > span")[0]).text(); 1137 return originalPH != currentPH; 1138 } 1139 1140 function updatePlaceholders() { 1141 $("#placeholders > span").each(function (i, div) { 1142 placeholders[i].placeholder = $(div).text(); 1143 }); 1144 } 1145 // // Google Translate fix to Find all placeholders 1146 var placeholders = document.querySelectorAll("textarea[placeholder]"); 1147 1148 if (placeholders.length) { 1149 // convert to array 1150 placeholders = Array.prototype.slice.call(placeholders); 1151 1152 // copy placeholder text to a hidden div 1153 var placeholderSpan = $('<span id="placeholders"></span>'); 1154 1155 placeholders.forEach(function (input) { 1156 var text = input.placeholder; 1157 placeholderSpan.append("<span>" + text + "</span>"); 1158 }); 1159 1160 $("footer small").append(placeholderSpan); 1161 1162 // save the first placeholder in a closure 1163 var originalPH = placeholders[0].placeholder; 1164 1165 // check for changes and update as needed 1166 setInterval(function () { 1167 if (isTranslated()) { 1168 updatePlaceholders(); 1169 originalPH = placeholders[0].placeholder; 1170 } 1171 }, 500); 1172 1173 // hoisted --------------------------- 1174 } 1175}; 1176 1177PageUI.prototype._initKioskForms = function () { 1178 var that = this; 1179 if ( 1180 this.modelData.kioskMode != null && 1181 !this.modelData.kioskMode.RegistrationLookup 1182 ) { 1183 var $kioskRestart = Mustache.render($("#footer-restart-kiosk").html(), { 1184 cz: cz, 1185 kioskMode: that.modelData.kioskMode 1186 });
1187 that.$makeExistFooter(); 1188 var $footer = that.$makeExistFooter(); 1189 1190 $footer.prepend($kioskRestart); 1191 $footer.find(".footer-links").hide(); 1192 $footer.find(".footer-social").hide(); 1193 if ( 1194 this.modelData.kioskMode.KioskFormat == CONSTANTS.KioskFormats.EventSignIn 1195 ) { 1196 this._setupKioskSigninForm(); 1197 } 1198 } 1199}; 1200 1201PageUI.prototype.$makeExistFooter = function () { 1202 var $footer = $("#footer"); 1203 1204 if ($footer.length === 0) { 1205 $footer = $('<div id="footer">'); 1206 $("#project-main .push-footer").after($footer); 1207 } 1208 1209 return $footer; 1210}; 1211 1212// Initialize the GoogleAutoComplete 1213PageUI.prototype.initGooglePlaces = function () { 1214 // Match on name, not id: ids are now scoped per form (WI 9139) but name stays the attribute key. 1215 var input = $('.google-autocomplete-input, [name="user_custom_attr_3"]'); 1216 1217 // If GoogleAutoComplete isn't loaded, log a warning and bypass autocomplete. 1218 if (typeof window.GoogleAutoComplete !== "function") { 1219 console.warn( 1220 "GoogleAutoComplete not available. Autocomplete will be bypassed." 1221 ); 1222 1223 // Do nothing further so that the zip field, etc can be submitted. 1224 return; 1225 } 1226 1227 var googleAutoComplete = new window.GoogleAutoComplete(input, { 1228 onPlaceChanged: function (address, changedInput) { 1229 const { 1230 streetNumber = "", 1231 streetName = "", 1232 city = "", 1233 state = "", 1234 county = "", 1235 postalCode = "" 1236 } = address; 1237 1238 // Trim and join the streetNumber and streetName to avoid extra spaces or commas 1239 const streetAddress = [streetNumber, streetName] 1240 .map((part) => part.trim()) 1241 .filter((part) => part) 1242 .join(" "); 1243 1244 const fullAddress = [streetAddress, city, state] 1245 .filter((part) => part) // Remove empty parts 1246 .join(", "); 1247 1248 // Write the sibling fields only inside the form that was actually edited -- the same 1249 // attributes can be rendered by more than one form on a page (WI 9139). 1250 const $scoped = changedInput 1251 ? $(changedInput).closest("form, .leadList-form, .rsvp-form") 1252 : $(); 1253 const $root = $scoped.length ? $scoped : $(document); 1254 1255 // Match on name, not id: ids carry a per-form suffix now, names do not. 1256 function setFieldByName(fieldName, value) { 1257 const $target = $root.find('[name="' + fieldName + '"]'); 1258 if ($target.length) { 1259 $target.val(value); 1260 } 1261 } 1262 1263 setFieldByName("user_custom_attr_3", fullAddress); 1264 setFieldByName("user_custom_attr_4", city); // City input 1265 setFieldByName("user_custom_attr_5", state); // State input 1266 setFieldByName("user_custom_attr_6", county); // County input 1267 setFieldByName("user_custom_attr_7", postalCode); // Postal code input 1268 } 1269 }); 1270 googleAutoComplete.init(); 1271}; 1272 1273PageUI.prototype._disableAutoCompleteIfDataEntry = function () { 1274 var that = this; 1275 if (that.modelData.kioskMode != null) { 1276 if (document.getElementsByTagName) { 1277 var inputElements = document.getElementsByTagName("input"); 1278 for (i = 0; inputElements[i]; i++) { 1279 inputElements[i].setAttribute("autocomplete", "off"); 1280 } 1281 } 1282 $(".leadList-form input").length && 1283 $(".leadList-form input[type='text']").val("") && 1284 $(".leadList-form input[data-type='email']").val(""); 1285 } 1286 setTimeout(() => { 1287 that.initGooglePlaces(); 1288 }, 500); 1289}; 1290 1291PageUI.prototype._setupKioskSigninForm = function () { 1292 var that = this; 1293 var $kioskSignInForm = this.$element.find(".rsvp-form"); 1294 1295 ScriptLoader.loadScriptArray(ScriptLoader.scriptArrays.parsley); 1296 1297 $kioskSignInForm.submit(function (e) { 1298 e.preventDefault(); 1299 that._submitNewKioskUser($kioskSignInForm); 1300 }); 1301}; 1302 1303PageUI.prototype._submitNewKioskUser = function ($kioskSignInForm) { 1304 var that = this; 1305 ScriptLoader.loadScriptArray(ScriptLoader.scriptArrays.parsley, function () { 1306 if ( 1307 $kioskSignInForm.parsley && 1308 $kioskSignInForm.parsley().validate() == false 1309 ) { 1310 return; //parsley will display any errors 1311 } 1312 1313 var $formData = $kioskSignInForm.serializeObject(); 1314 1315 $kioskSignInForm.html( 1316 tools.spin($kioskSignInForm.height(), "Signing in...", true) 1317 ); 1318 1319 $.post("/project/NewKioskUser", $formData) 1320 .fail(function () { 1321 alert( 1322 "An error occurred while attempting to submit this sign in form. Please refresh the page and try again." 1323 ); 1324 }) 1325 .done(function (data) { 1326 $kioskSignInForm.css("height", "auto"); 1327 that._showKioskSignInConfirmation(); 1328 }); 1329 }); 1330}; 1331 1332PageUI.prototype._showKioskSignInConfirmation = function () { 1333 var that = this; 1334 this.$element.find("#new-kiosk-form-outer").fadeOut(function () { 1335 var $kioskOuter = that.$element.find("#new-kiosk-form-outer").parent(); 1336 var $kioskInner = $( 1337 Mustache.render($("#survey-kiosk-confirm").html(), { 1338 cz: cz, 1339 kioskMode: that.modelData.kioskMode, 1340 completion: false 1341 }) 1342 ).hide(); 1343 $kioskOuter.append($kioskInner); 1344 $kioskOuter.find(".shadowed-box").removeClass("shadowed-box"); 1345 $kioskInner.fadeIn(); 1346 }); 1347}; 1348 1349PageUI.prototype._initOfflineEvents = function () { 1350 var that = this; 1351 if ( 1352 that.modelData.kioskMode && 1353 that.modelData.kioskMode.KioskFormat == 1354 CONSTANTS.KioskFormats.EventSignIn && 1355 !that.modelData.kioskMode.RegistrationLookup 1356 ) { 1357 //do not run the event init, this is a kiosk sign in form 1358 return; 1359 } 1360 1361 this.eventsArray = []; 1362 var $events = that.$element.find(".meeting-preview-outer"); 1363 if ($events.length) { 1364 ScriptLoader.loadScriptArray( 1365 ScriptLoader.scriptArrays.offlineEvent, 1366 function () { 1367 $events.each(function (idx, elem) { 1368 if ($(elem).data("event-initialized") == true) { 1369 return; 1370 } 1371 1372 var id = $(this).data("offline-event-id"); 1373 var offlineEvent = 1374 window.offlineEvents != null && window.offlineEvents[id] 1375 ? window.offlineEvents[id] 1376 : {}; 1377 var meetingAgendaItems = 1378 window.meetingAgendaItems != null && window.meetingAgendaItems[id] 1379 ? window.meetingAgendaItems[id] 1380 : {}; 1381 var config = { 1382 $element: $(this), 1383 pageUI: that, 1384 modelData: that.modelData, 1385 offlineEvent: offlineEvent, 1386 meetingAgendaItems: meetingAgendaItems, 1387 id: id 1388 }; 1389 that.eventsArray.push(new OfflineEventUI(config)); 1390 $(elem).data("event-initialized", true); 1391 }); 1392 } 1393 ); 1394 } 1395}; 1396 1397// How long submitSurvey() ignores repeat clicks after a submission starts. 1398var SUBMIT_DEBOUNCE_MS = 3000; 1399 1400function SubmitSurveyUI(_pageUI) { 1401 this.UI = _pageUI; 1402 this.customerId = _pageUI.customerId; 1403 this.projectId = _pageUI.projectId; 1404 this.projectType = _pageUI.modelData.project.projectTypeEnum;
1405 this.isCustomFormType = this.projectType == CONSTANTS.ProjectType.CUSTOM_FORM; 1406 this.onSubmitSettings = this.UI.modelData.onSubmitSettings || {}; 1407 this.UI.modelData.project.extendedOptions = 1408 this.UI.modelData.project.extendedOptions || {}; 1409 1410 if ( 1411 !this.onSubmitSettings.submitButtonText || 1412 !this.onSubmitSettings.submitButtonText.length 1413 ) { 1414 this.onSubmitSettings.submitButtonText = "Submit Survey"; 1415 } 1416 if (this.UI.modelData.project.extendedOptions.followUpModulePollId > 0) { 1417 this.onSubmitSettings.demographicModulePollId = 1418 this.UI.modelData.project.extendedOptions.followUpModulePollId; 1419 } 1420 1421 this.init(); 1422 this.refreshEndzone(); 1423} 1424 1425SubmitSurveyUI.prototype.init = function () { 1426 var that = this; 1427 var $element = that.UI.$element; 1428 if ( 1429 that.onSubmitSettings != null && 1430 that.onSubmitSettings.demographicModulePollId > 0 1431 ) { 1432 that._buildOnSubmitDemographicPoll(); 1433 } 1434 1435 // AB#10617: a page with no questions has nothing to submit, so don't offer a Submit button 1436 // that would record an empty completion. Multi-step surveys are exempt (see _hasNothingToSubmit). 1437 if ( 1438 that.onSubmitSettings != null && 1439 that.onSubmitSettings.enabled && 1440 !that._hasNothingToSubmit() 1441 ) { 1442 that.$endOfSurveyDiv = $element.find("#endzone"); 1443 that.$endOfSurveyDiv.append( 1444 Mustache.render( 1445 $("#survey-progress-default").html(), 1446 that.onSubmitSettings 1447 ) 1448 ); 1449 } 1450 1451 const $continueButton = $element.find(".step-continue-button"); 1452 1453 $continueButton.hide(); 1454 $element.find("#endzone").hide(); 1455 1456 if ( 1457 that.UI.modelData.project.extendedOptions.onSubmitSettings == undefined || 1458 that._hasNothingToSubmit() 1459 ) { 1460 that._assignOnclickEvents(); 1461 return; 1462 } 1463 1464 const totalSteps = that.UI.modelData.surveySteps ?? 0; 1465 const isSingleStep = that._isSingleStepPage(); 1466 const isLastStepActive = 1467 isSingleStep || $element.find(".surveyProgress").last().hasClass("active"); 1468 const lastStepIndex = $element 1469 .find(".surveyProgress") 1470 .last() 1471 .data("stepIndex"); 1472 const isLastStep = 1473 isLastStepActive && 1474 (lastStepIndex === undefined || 1475 (typeof lastStepIndex === "number" && 1476 totalSteps !== undefined && 1477 lastStepIndex === totalSteps - 1)); 1478 1479 const project = that.UI.modelData.project; 1480 const submitBtnIsEnabled = project.extendedOptions.onSubmitSettings.enabled; 1481 const hideContinueBtn = project.extendedOptions.hideContinue; 1482 1483 switch (project.type) { 1484 case CONSTANTS.ProjectType.DEFAULT: 1485 case CONSTANTS.ProjectType.TOPIC: 1486 if (isLastStep && submitBtnIsEnabled) { 1487 $element.find("#endzone").removeAttr("aria-hidden").show(); 1488 $continueButton.hide(); 1489 } else if (hideContinueBtn) { 1490 $continueButton.hide(); 1491 } else { 1492 $continueButton.show(); 1493 } 1494 break; 1495 case CONSTANTS.ProjectType.CUSTOM_FORM: 1496 $element.find("#endzone").removeAttr("aria-hidden").show(); 1497 break; 1498 default: 1499 $element.find("#endzone").removeAttr("aria-hidden").show(); 1500 break; 1501 } 1502 1503 that._assignOnclickEvents(); 1504}; 1505 1506SubmitSurveyUI.prototype._isSingleStepPage = function () { 1507 const totalSteps = this.UI.modelData.surveySteps ?? 0; 1508 return totalSteps === 0 || totalSteps === 1; 1509}; 1510 1511// True when an ordinary visitor page has no questions at all (AB#10617). Only applies to 1512// single-step pages: a multi-step survey can legitimately end on a content-only step whose earlier 1513// steps already hold the answers, and stepSurveyUI relies on #endButton existing to show the 1514// final-step endzone. A follow-up questions module counts as something to submit (it lives in a 1515// modal, not in activePolls), and kiosk / data-entry pages keep the endzone because their 1516// confirmation and "finish entry" controls render from the same template. 1517SubmitSurveyUI.prototype._hasNothingToSubmit = function () { 1518 var hasFollowUpQuestions = this.onSubmitSettings.demographicModulePollId > 0; 1519 var isOperatorMode = 1520 this.UI.modelData.kioskMode != null || !!this.UI.config.dataEntry; 1521 1522 return ( 1523 this._isSingleStepPage() && 1524 this.UI.activePolls.length === 0 && 1525 !hasFollowUpQuestions && 1526 !isOperatorMode 1527 ); 1528}; 1529 1530SubmitSurveyUI.prototype._buildOnSubmitDemographicPoll = function () { 1531 var that = this; 1532 that._buildFollowUpQuestionsModal(); 1533 that.$onSubmitModal = $("#followUpQuestionsModal");
1534 that.$onSubmitModalBody = that.$onSubmitModal.find(".modal-body"); 1535 $("body").append(that.$onSubmitModal); //append to body to prevent issues on ios 1536 1537 // Make sure the modal close button works 1538 that.$onSubmitModal 1539 .find(".modal-header .close") 1540 .attr("data-dismiss", "modal") 1541 .on("click", function () { 1542 that.$onSubmitModal.modal("hide"); 1543 }); 1544 1545 var $demographicModuleContainer = $("<div>").addClass( 1546 "demographic-module-outer" 1547 ); 1548 1549 $demographicModuleContainer.append( 1550 tools.spin(null, "Loading questions...", true) 1551 ); 1552 1553 that.$onSubmitModalBody.empty().append($demographicModuleContainer); 1554 1555 // Load required CSS files 1556 tools.requireCSSFile("/Content/PollCSS/demographicModuleViewer.css"); 1557 tools.requireCSSFile("/Scripts/chosen/chosen.css"); 1558 tools.requireCSSFile("/fonts/fontawesome/css/all.min.css"); 1559 tools.requireCSSFile("/fonts/fontawesome/css/v4-shims.css"); 1560 1561 // Create a standard Poll instance 1562 var demographicModulePoll = new Poll($demographicModuleContainer, that.UI); 1563 1564 demographicModulePoll = $.extend(demographicModulePoll, { 1565 id: that.onSubmitSettings.demographicModulePollId, 1566 data: { 1567 extendedOptions: { 1568 subType: 66 // 66 indicates follow-up questions module 1569 }, 1570 pollType: CONSTANTS.PollTypes.DemographicModule 1571 } 1572 }); 1573 1574 // Use a different approach - let's first load the demographic module script 1575 ScriptLoader.loadScriptArray( 1576 ScriptLoader.scriptArrays.demographicModule, 1577 function () { 1578 // Now create a demographic module directly 1579 var config = { 1580 poll: demographicModulePoll, 1581 $element: $demographicModuleContainer, 1582 customerId: that.customerId, 1583 projectId: that.projectId, 1584 voteCallback: function (optionIdArray) { 1585 demographicModulePoll.selectedOptions = optionIdArray; 1586 demographicModulePoll._vote(function () {}, { disableSpinner: true }); 1587 }, 1588 unvoteCallback: function (pollId) { 1589 demographicModulePoll._unVote(function () {}, pollId); 1590 }, 1591 antiForgeryToken: window.ajaxAntiForgeryToken 1592 }; 1593 1594 // The DemographicModule will fetch its own data with the right poll ID 1595 that.onSubmitFollowUpPoll = new DemographicModule(config); 1596 } 1597 ); 1598}; 1599 1600SubmitSurveyUI.prototype._buildFollowUpQuestionsModal = function () { 1601 if (!document.getElementById("followUpQuestionsModal")) { 1602 const modalHtml = ` 1603 <div id="followUpQuestionsModal" class="modal fade" tabindex="-1" role="dialog"> 1604 <div class="modal-dialog modal-lg" role="document"> 1605 <div class="modal-content"> 1606 <button type="button" class="close" data-bs-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> 1607 <div id="followUpQuestionsModalBody" class="modal-body"></div> 1608 </div> 1609 </div> 1610 </div>`; 1611 document.body.insertAdjacentHTML("beforeend", modalHtml); 1612 } 1613}; 1614 1615SubmitSurveyUI.prototype._assignOnclickEvents = function () { 1616 var that = this; 1617 1618 this.UI.$element.on("click", "#endButton", function () { 1619 that.submitSurvey(); 1620 }); 1621 1622 this.UI.$element.on("click", ".show-content", function () { 1623 that.UI.$element.find(".project-content").fadeIn(); 1624 that.UI.$element.find(".kiosk-show-content").hide(); 1625 that.UI.$element.find(".panel-show-content").hide(); 1626 }); 1627 1628 this.UI.$element.on("click", ".dropdown-menu.keep-open *", function (event) { 1629 // $(this).closest("ul").toggleClass('open'); 1630 event.stopPropagation(); 1631 }); 1632}; 1633SubmitSurveyUI.prototype.populateOnSubmitHtml = function ( 1634 $target, 1635 isStepSubmit 1636) { 1637 var that = this; 1638 if (that.onSubmitSettings.onSubmitFreeformId > 0) { 1639 that._populateOnSubmitFreeform($target, isStepSubmit); 1640 } else { 1641 that._populateDefaultSubmitMessage($target, isStepSubmit); 1642 } 1643 1644 // if custom form type, add center class to message box 1645 // add submit another response button 1646 if (that.isCustomFormType) { 1647 that.UI.$element.find("section .project-content").slideUp(); 1648 $target.addClass("text-center"); 1649 $target.append( 1650 "<button onClick='window.location.reload();' class='btn btn-primary btn-primary-new btn-s-lg m-b m-t' style='min-width: 40%;'>Submit A New Response</button>" 1651 ); 1652 } 1653}; 1654 1655SubmitSurveyUI.prototype._populateDefaultSubmitMessage = function ( 1656 $target, 1657 isStepSubmit 1658) { 1659 var that = this; 1660 var $successMessage = $(); 1661 if (isStepSubmit) { 1662 $successMessage = $( 1663 Mustache.render($("#survey-completion-success").html()) 1664 ); 1665 $target.empty().append($successMessage); 1666 } else { 1667 $target.html(Mustache.render($("#survey-completion-success").html())); 1668 } 1669}; 1670 1671SubmitSurveyUI.prototype._populateOnSubmitFreeform = function ( 1672 $target, 1673 isStepSubmit 1674) { 1675 var that = this; 1676 var $targetInner = $("<div>").addClass("survey-progress-complete custom"); 1677 $target.empty().append($targetInner); 1678 1679 $targetInner.html(tools.spin(null, "Submitting responses...", true)); 1680 $.post("/Project/GetOnSubmitFreeformHtml", { 1681 customerId: that.customerId, 1682 projectId: that.projectId, 1683 freeformId: that.onSubmitSettings.onSubmitFreeformId 1684 }).done(function (data) { 1685 if (data.htmlText && data.htmlText.length) { 1686 $targetInner.html(data.htmlText); 1687 } else { 1688 that._populateDefaultSubmitMessage($target, isStepSubmit); 1689 } 1690 }); 1691}; 1692 1693SubmitSurveyUI.prototype.refreshEndzone = function (data) { 1694 var that = this; 1695 if ( 1696 (that.UI.modelData.kioskMode != null || that.UI.config.dataEntry) && 1697 that.UI.modelData.dataEntryUser == null 1698 ) { 1699 return; 1700 } 1701 1702 if (this.currentlyProcessingSubmit) { 1703 return; 1704 } 1705 1706 if ( 1707 that.UI.modelData.project.submitSettings == 1 || 1708 that.UI.modelData.dataEntryUser != null 1709 ) { 1710 that.UI.$element.find("#endButton").show(); 1711 } 1712}; 1713 1714SubmitSurveyUI.prototype.submitSurvey = function () { 1715 var that = this; 1716 if (that._hasNothingToSubmit()) { 1717 // No questions on the page: never record an empty completion (AB#10617). 1718 return; 1719 } 1720 this.currentlyProcessingSubmit = true; 1721 var showFollowUpQuestionsOnSubmit = 1722 that.UI.modelData.onSubmitSettings.demographicModulePollId > 0; 1723 1724 that._submitOpenQuestions(null, function () { 1725 var displayEndButton = true; 1726 var progress = that._progressParse(); 1727 1728 that.$endButton = that.UI.$element.find("#endButton"); 1729 that.$endzone = that.UI.$element.find("#endzone"); 1730 that.$progressMsgRequired = that.UI.$element.find( 1731 "#progress-required-message-outer" 1732 ); 1733 that.$progressMsgRequired.hide(); 1734 1735 if (that.UI.modelData.kioskMode != null) { 1736 that._submitKioskSurvey(progress); 1737 } else if ( 1738 progress.answered === progress.total && 1739 !progress.missingRequired 1740 ) { 1741 if (showFollowUpQuestionsOnSubmit) { 1742 that._launchFollowUpQuestionsModal(); 1743 } 1744 1745 that.populateOnSubmitHtml( 1746 that.UI.$element.find("#progress-message-outer"), 1747 false 1748 ); 1749 1750 that.fireProjectSubmittedEvent(); 1751 } else { 1752 if (showFollowUpQuestionsOnSubmit && !progress.missingRequired) { 1753 that._launchFollowUpQuestionsModal(); 1754 } 1755 1756 if (that.isCustomFormType) { 1757 //If this is a form 1758 if (progress.missingRequired) { 1759 that._populateUnansweredDiv(progress); 1760 displayEndButton = false; 1761 } else { 1762 that.populateOnSubmitHtml( 1763 that.UI.$element.find("#progress-message-outer"), 1764 false 1765 ); 1766 that.fireProjectSubmittedEvent(); 1767 } 1768 } else { 1769 that._populateUnansweredDiv(progress); 1770 } 1771 } 1772 1773 if (displayEndButton) { 1774 that.$endButton.hide(); 1775 } 1776 1777 if (window.recaptchaService) { 1778 window.recaptchaService.run("Submit"); 1779 } 1780 1781 setTimeout(function () { 1782 that.currentlyProcessingSubmit = false; 1783 }, SUBMIT_DEBOUNCE_MS);
1784 window.parent.postMessage({ action: "endButtonClicked" }, "*"); 1785 }); 1786}; 1787 1788SubmitSurveyUI.prototype._launchFollowUpQuestionsModal = function (progress) { 1789 var $modal = $("#followUpQuestionsModal"); 1790 1791 // When modal is shown, focus on the first focusable element inside it 1792 $modal.one("shown.bs.modal", function () { 1793 $modal 1794 .find( 1795 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 1796 ) 1797 .first() 1798 .focus(); 1799 }); 1800 1801 $modal.modal("show"); 1802}; 1803SubmitSurveyUI.prototype._submitKioskSurvey = function (progress) { 1804 var that = this; 1805 progress = progress || that._progressParse(); 1806 that.UI.$element.find("#kiosk-confirm-outer").html( 1807 Mustache.render($("#survey-kiosk-confirm").html(), { 1808 cz: cz, 1809 kioskMode: that.UI.modelData.kioskMode, 1810 completion: true 1811 }) 1812 ); 1813 1814 if (progress.unansweredPolls.length > 0) { 1815 that._populateUnansweredDiv(progress, true); 1816 that.$endzone.find(".show-answers").hide(); 1817 } else { 1818 that._fadeOutContent(); 1819 that.$endButton.hide(); 1820 that.UI.$element.find(".kiosk-show-content").show(); 1821 } 1822}; 1823 1824// Pass-through URL parameters (#10540). A research panel links in with its own opaque id on the 1825// survey URL (?param-pid=ABC123) and needs it handed back on the completion redirect, which 1826// {{user-id}} cannot do -- that is our internal id, which the panel has never seen. 1827// 1828// The values are echoed straight back to the server and are re-validated there 1829// (PassThroughParamHelper); nothing here is a security control, and no captured value is ever 1830// written to the DOM. This exists only so the id survives a page reload mid-survey. 1831var PassThroughParams = { 1832 STORAGE_KEY: "czPassThroughParams", 1833 PREFIX: "param-", 1834 MAX_COUNT: 10, 1835 // Mirrors the server allowlist so we do not bother posting something it will drop. 1836 NAME_RE: /^param-[a-zA-Z0-9_-]{1,32}$/, 1837 VALUE_RE: /^[A-Za-z0-9._~-]{1,128}$/, 1838 1839 _fromSearch: function (search) { 1840 var found = {}; 1841 if (!search) return found; 1842 1843 var parts = search.replace(/^\?/, "").split("&"); 1844 for (var i = 0; i < parts.length && this._count(found) < this.MAX_COUNT; i++) { 1845 if (!parts[i]) continue; 1846 1847 var eq = parts[i].indexOf("="); 1848 if (eq < 1) continue; 1849 1850 var key, value; 1851 try { 1852 key = decodeURIComponent(parts[i].slice(0, eq).replace(/\+/g, " ")); 1853 value = decodeURIComponent(parts[i].slice(eq + 1).replace(/\+/g, " ")); 1854 } catch (e) { 1855 continue; // malformed percent-escape; ignore this pair 1856 } 1857 1858 if (!this.NAME_RE.test(key) || !this.VALUE_RE.test(value)) continue; 1859 if (Object.prototype.hasOwnProperty.call(found, key)) continue; 1860 1861 found[key] = value; 1862 } 1863 return found; 1864 }, 1865 1866 _count: function (obj) { 1867 var n = 0; 1868 for (var k in obj) { 1869 if (Object.prototype.hasOwnProperty.call(obj, k)) n++; 1870 } 1871 return n; 1872 }, 1873 1874 // Stash whatever the landing URL carried, scoped to this page so a second survey opened in the 1875 // same tab cannot inherit the first one's parameters. 1876 capture: function () { 1877 var found = this._fromSearch(window.location.search); 1878 if (this._count(found) === 0) return; 1879 1880 try { 1881 window.sessionStorage.setItem( 1882 this.STORAGE_KEY, 1883 JSON.stringify({ path: window.location.pathname, params: found }) 1884 ); 1885 } catch (e) { 1886 // Private mode / storage disabled. The current-URL path below still works. 1887 } 1888 }, 1889 1890 // Current URL wins; sessionStorage is the fallback for a mid-survey reload that dropped the 1891 // query string. 1892 read: function () { 1893 var found = this._fromSearch(window.location.search); 1894 if (this._count(found) > 0) return found; 1895 1896 try { 1897 var raw = window.sessionStorage.getItem(this.STORAGE_KEY); 1898 if (!raw) return {}; 1899 1900 var saved = JSON.parse(raw); 1901 if (!saved || saved.path !== window.location.pathname) return {}; 1902 1903 return saved.params || {}; 1904 } catch (e) { 1905 return {}; 1906 } 1907 } 1908}; 1909 1910PassThroughParams.capture(); 1911 1912SubmitSurveyUI.prototype.fireProjectSubmittedEvent = function () { 1913 var that = this; 1914 var payload = { projectId: that.projectId }; 1915 1916 var passThrough = PassThroughParams.read(); 1917 for (var key in passThrough) { 1918 if (Object.prototype.hasOwnProperty.call(passThrough, key)) { 1919 payload[key] = passThrough[key]; 1920 } 1921 } 1922 1923 $.post("/Project/LogSuccessfulSurveySubmit", payload) 1924 .done(function (data) { 1925 var redirect = data && data.redirect; 1926 if (redirect && redirect.Enabled) { 1927 that._applyPostSubmitRedirect(redirect); 1928 } 1929 }) 1930 .fail(function (xhr) { 1931 // Degrading to "no redirect" is correct, but make a systematic failure diagnosable
1932 // rather than invisible. 1933 if (window.console && window.console.warn) { 1934 window.console.warn( 1935 "LogSuccessfulSurveySubmit failed; post-submit redirect skipped.", 1936 xhr && xhr.status 1937 ); 1938 } 1939 }); 1940}; 1941 1942// Post-submission redirect (#9410). The URL is resolved, validated and merge-field-substituted 1943// server side; this only decides when to navigate. Kiosk mode never reaches here because 1944// _submitKioskSurvey does not fire the submitted event. 1945SubmitSurveyUI.prototype._applyPostSubmitRedirect = function (redirect) { 1946 var that = this; 1947 var url = redirect.Url; 1948 1949 // Belt and braces: the server allowlists absolute https, but never hand an unvalidated 1950 // string to window.location. 1951 if (typeof url !== "string" || !/^https:\/\//i.test(url)) { 1952 return; 1953 } 1954 1955 // Staff data-entry sessions are not the audience for a respondent-facing panel redirect -- 1956 // a clerk keying paper responses would be thrown off the page after every record. (Kiosk is 1957 // already excluded upstream: _submitKioskSurvey never fires the submitted event.) 1958 if ( 1959 (this.UI.config && this.UI.config.dataEntry) || 1960 (this.UI.modelData && this.UI.modelData.dataEntryUser != null) 1961 ) { 1962 return; 1963 } 1964 1965 // Never navigate out from under an open follow-up questionnaire. Demographics / Title VI 1966 // responses are a civil-rights reporting artifact for these agencies, and both features are 1967 // configured in the same admin partial, so an admin will have them on together. Defer until 1968 // the modal is dismissed rather than silently discarding the answers. 1969 var $followUp = this.UI.$element.find("#followUpQuestionsModal"); 1970 if ($followUp.length && $followUp.hasClass("in")) { 1971 $followUp.one("hidden.bs.modal", function () { 1972 that._navigatePostSubmitRedirect(redirect, url); 1973 }); 1974 return; 1975 } 1976 1977 this._navigatePostSubmitRedirect(redirect, url); 1978}; 1979 1980SubmitSurveyUI.prototype._navigatePostSubmitRedirect = function ( 1981 redirect, 1982 url 1983) { 1984 if (redirect.Mode === CONSTANTS.PostSubmitRedirectMode.AFTER_CONFIRMATION) { 1985 this._showPostSubmitRedirectNotice(url, redirect.DelaySeconds || 0); 1986 } else { 1987 this._navigateTo(url); 1988 } 1989}; 1990 1991// Navigate, suppressing the Referer so the destination's access logs do not also receive the 1992// survey page URL alongside whatever PII the merge fields put in the query string. 1993// 1994// In an iframe embed, replacing only the frame is the wrong outcome: panel and CRM hosts 1995// commonly send X-Frame-Options: DENY, so the participant's survey would end in a blank box. 1996// Target the top-level window there instead. 1997SubmitSurveyUI.prototype._navigateTo = function (url) { 1998 var framed = false; 1999 try { 2000 framed = window.top !== window.self; 2001 } catch (e) { 2002 // Cross-origin parent: we are definitely framed. 2003 framed = true; 2004 } 2005 2006 var $link = $("<a>") 2007 .attr("href", url) 2008 .attr("rel", "noreferrer noopener") 2009 .attr("target", framed ? "_top" : "_self") 2010 .css("display", "none"); 2011 2012 $("body").append($link); 2013 $link[0].click(); 2014 $link.remove(); 2015}; 2016 2017// Show the confirmation, announce the pending redirect, and give the participant control over the 2018// timer. WCAG 2.2.1 (Timing Adjustable) requires the time limit be turnable OFF, not merely 2019// accelerated -- so we offer both "Continue now" and "Stay on this page", the latter cancelling the 2020// pending navigation outright. 3.2.5: nothing moves until the timer elapses or the user chooses. 2021SubmitSurveyUI.prototype._showPostSubmitRedirectNotice = function ( 2022 url, 2023 delaySeconds 2024) { 2025 var that = this; 2026 var $confirmation = this.UI.$element.find("#progress-message-outer"); 2027 2028 if (!$confirmation.length) { 2029 this._navigateTo(url); 2030 return; 2031 } 2032 2033 // The server clamps this to a usable floor, but a stale/hand-built payload must not be able 2034 // to produce a zero-delay timer that fires before the cancel control can be operated. 2035 var seconds = Math.max( 2036 CONSTANTS.PostSubmitRedirect.MIN_DELAY_SECONDS, 2037 delaySeconds || 0 2038 ); 2039 2040 // Own container, appended as a SIBLING of #progress-message-outer. populateOnSubmitHtml and 2041 // _populateOnSubmitFreeform both call .html() on that node from a separate in-flight request,
2042 // which would otherwise detach this notice (and its cancel button) while the timer kept 2043 // running against a node no longer in the document. 2044 var $host = this.UI.$element.find("#post-submit-redirect-outer"); 2045 if (!$host.length) { 2046 $host = $("<div>").attr("id", "post-submit-redirect-outer"); 2047 $confirmation.after($host); 2048 } 2049 $host.empty(); 2050 2051 // Only one pending redirect at a time; a second invocation must not stack timers. 2052 this._clearPostSubmitRedirectTimer(); 2053 2054 var $notice = $("<p>") 2055 .addClass("post-submit-redirect-notice") 2056 .attr("role", "status") 2057 .attr("aria-live", "polite") 2058 .attr("tabindex", "-1"); 2059 2060 var $continue = $("<a>") 2061 .attr("href", url) 2062 .attr("rel", "noreferrer noopener") 2063 .addClass("post-submit-redirect-continue") 2064 .text("Continue now"); 2065 2066 var $cancel = $("<button>") 2067 .attr("type", "button") 2068 .addClass("btn btn-link post-submit-redirect-cancel") 2069 .text("Stay on this page") 2070 .on("click", function () { 2071 that._clearPostSubmitRedirectTimer(); 2072 // Keep the focused element in the DOM -- removing the button the user just activated 2073 // drops focus to <body> and loses their place (WCAG 2.4.3). 2074 $cancel.prop("disabled", true).text("Staying on this page"); 2075 $message.text("Redirect cancelled. "); 2076 }); 2077 2078 // Render the message and BOTH controls synchronously, before arming the timer. Previously the 2079 // text and controls were written on a separate 100ms timeout, so a short delay navigated away 2080 // before the "Stay on this page" control ever existed -- which is precisely the control that 2081 // makes the auto-redirect WCAG 2.2.1 compliant. 2082 var $message = $("<span>").text( 2083 "Redirecting you to another site in " + seconds + " seconds. " 2084 ); 2085 2086 $notice 2087 .append($message) 2088 .append($continue) 2089 .append(document.createTextNode(" ")) 2090 .append($cancel); 2091 2092 $host.append($notice); 2093 2094 // Move focus to the notice so a keyboard or screen-reader user lands on the cancel control 2095 // rather than having to hunt for it before the timer elapses. 2096 if ($notice[0] && typeof $notice[0].focus === "function") { 2097 $notice[0].focus(); 2098 } 2099 2100 this._postSubmitRedirectTimerId = window.setTimeout(function () { 2101 that._postSubmitRedirectTimerId = null; 2102 // The notice may have been torn out by a later re-render; do not navigate a participant 2103 // who can no longer see the warning or reach the cancel control. 2104 if (!$.contains(document, $notice[0])) { 2105 return; 2106 } 2107 that._navigateTo(url); 2108 }, seconds * 1000); 2109}; 2110 2111SubmitSurveyUI.prototype._clearPostSubmitRedirectTimer = function () { 2112 if (this._postSubmitRedirectTimerId) { 2113 window.clearTimeout(this._postSubmitRedirectTimerId); 2114 this._postSubmitRedirectTimerId = null; 2115 } 2116}; 2117 2118SubmitSurveyUI.prototype._submitOpenQuestions = function ( 2119 pollIdArray, 2120 callback 2121) { 2122 var that = this; 2123 var pollArray = []; 2124 var optionArray = []; 2125 var data = []; 2126 2127 pollIdArray = pollIdArray || []; 2128 2129 // check if custom form type, if so check if there is a contact question. 2130 // if contains a contact question, validate it first before looping and submitting every question 2131 if (that.isCustomFormType) { 2132 var continueFunction = true; 2133 2134 $.each(that.UI.activePolls, function (index, poll) { 2135 var isRequired = poll.metaData.required; 2136 var userHasVoted = poll.metaData.hasvoted; 2137 2138 switch (poll.metaData.polltype) { 2139 case CONSTANTS.PollTypes.LeadList: 2140 if (poll.contactForm) { 2141 poll.contactForm.$formElement.parsley().validate(); 2142 if (!poll.contactForm.$formElement.parsley().isValid()) { 2143 continueFunction = false; 2144 return false; 2145 } 2146 } 2147 break; 2148 case CONSTANTS.PollTypes.SingleVote: 2149 case CONSTANTS.PollTypes.MultiChoiceVote: 2150 if (poll.selectedOptions.length <= 0 && isRequired && !userHasVoted) { 2151 continueFunction = false; 2152 return false; 2153 } 2154 break; 2155 case CONSTANTS.PollTypes.CommentOnly: 2156 case CONSTANTS.PollTypes.ShortComment: 2157 var isValidComment = 2158 poll.data && 2159 poll.commentSection && 2160 poll.commentSection.hasValidComment && 2161 poll.commentSection.hasValidComment(); 2162 2163 if (!isValidComment && isRequired && !userHasVoted) { 2164 continueFunction = false; 2165 return false; 2166 } 2167 break; 2168 default: 2169 break; 2170 } 2171 });
2172 2173 if (!continueFunction) { 2174 callback && callback(); 2175 return false; 2176 } 2177 } 2178 2179 that._submitOpenFundingAllocatorPolls(continueFunction); 2180 2181 $.each(that.UI.activePolls, function (idx, poll) { 2182 var requireAllOptions = 2183 poll && 2184 poll.data.extendedOptions && 2185 poll.data.extendedOptions.requireAllOptions; 2186 2187 if ( 2188 pollIdArray.length > 0 && 2189 poll.id > 0 && 2190 pollIdArray.indexOf(poll.id) == -1 2191 ) { 2192 return; 2193 } 2194 2195 if ( 2196 poll.metaData.polltype == CONSTANTS.PollTypes.Ranking && 2197 requireAllOptions 2198 ) { 2199 return; 2200 } 2201 2202 if (poll.selectedOptions.length && !poll.metaData.hasvoted) { 2203 poll._vote(); 2204 poll.metaData.hasvoted = true; 2205 poll.$element.find(".poll-meta").data("hasvoted", true); 2206 } 2207 if (poll.metaData.polltype == CONSTANTS.PollTypes.Location) { 2208 poll.locationPoll && poll.locationPoll._onConfirmClick(); 2209 } 2210 2211 if (poll.metaData.polltype == CONSTANTS.PollTypes.LeadList) { 2212 if ( 2213 poll.contactForm && 2214 poll.contactForm.$formElement.find("input").filter(function () { 2215 return ( 2216 this.type != "hidden" && 2217 this.type != "submit" && 2218 Boolean(this.value) 2219 ); 2220 }).length 2221 ) { 2222 // check if user has answered 2223 var hasAnsweredLeadListPoll = poll.metaData.hasvoted; 2224 2225 // if user has not answered then submit the form 2226 if (!hasAnsweredLeadListPoll) { 2227 var submitSuccess = poll.contactForm._leadlistFormSubmit(); 2228 if (submitSuccess) { 2229 poll.metaData.hasvoted = true; 2230 poll.$element.find(".poll-meta").data("hasvoted", true); 2231 } 2232 } 2233 } 2234 } 2235 if ( 2236 poll.metaData.polltype == CONSTANTS.PollTypes.Ranking && 2237 !poll.metaData.hasvoted 2238 ) { 2239 if ( 2240 poll.rankPoll && 2241 poll.rankPoll.selectedOptionArray && 2242 poll.rankPoll.selectedOptionArray.length 2243 ) { 2244 poll.rankPoll.vote(); 2245 poll.metaData.hasvoted = true; 2246 poll.$element.find(".poll-meta").data("hasvoted", true); 2247 } 2248 } 2249 2250 if ( 2251 poll.data && 2252 poll.commentSection && 2253 poll.commentSection.hasValidComment && 2254 poll.commentSection.hasValidComment() 2255 ) { 2256 if ( 2257 poll.metaData.polltype == CONSTANTS.PollTypes.LeadList && 2258 !poll.metaData.hasvoted && 2259 poll.metaData.required 2260 ) { 2261 return; 2262 } 2263 poll.commentSection.submitPrimaryComment(); 2264 poll.metaData.hascommented = true; 2265 poll.$element.find(".poll-meta").data("hascommented", true); 2266 } 2267 }); 2268 callback && callback(); 2269}; 2270 2271SubmitSurveyUI.prototype._submitOpenFundingAllocatorPolls = function ( 2272 continueFunction 2273) { 2274 var that = this; 2275 $.each(that.UI.activePolls, function (idx, poll) { 2276 // Auto-submit Funding Allocator responses if user has filled out the form 2277 // but has not yet clicked its internal "Submit" button. 2278 if ( 2279 poll.metaData && 2280 poll.metaData.polltype === CONSTANTS.PollTypes.FundingAllocator && 2281 poll.fundingAllocator && 2282 !poll.fundingAllocator.isSubmitted 2283 ) { 2284 // Ensure contact form is valid before submitting 2285 var $faContact = poll.fundingAllocator.$el.find(".fa-contact-form"); 2286 if ($faContact.length && window.Parsley) { 2287 $faContact.parsley().validate(); 2288 if (!$faContact.parsley().isValid()) { 2289 continueFunction = false; 2290 return false; // break out of each 2291 } 2292 } 2293 2294 // Trigger the Funding Allocator's submit routine (will handle budget validation internally) 2295 poll.fundingAllocator.submit(); 2296 // End-zone logic will pick up isSubmitted once the ajax call completes 2297 return; // move to next poll 2298 } 2299 }); 2300}; 2301 2302SubmitSurveyUI.prototype._progressParse = function (activePolls) { 2303 var that = this; 2304 activePolls = activePolls || that.UI.activePolls; 2305 var ret = { 2306 answered: 0, 2307 requiredUnanswered: 0, 2308 unanswered: 0, 2309 total: 0, 2310 unansweredPolls: [], 2311 requiredUnansweredPolls: [], 2312 missingRequired: false 2313 }; 2314 2315 $.each(activePolls, function (idx, poll) { 2316 // If the poll is hidden in the DOM (by show/hide rules), skip it entirely 2317 try { 2318 var pollId = poll && poll.id; 2319 var $pollEl = poll && poll.$element; 2320 var $domEl = pollId != null ? $("#pluginDiv-poll-" + pollId) : $(); 2321 var isHidden = false; 2322 2323 if ($pollEl && $pollEl.length) { 2324 isHidden = $pollEl.hasClass("hide") || !$pollEl.is(":visible"); 2325 } 2326 if (!isHidden && $domEl.length) { 2327 isHidden = $domEl.hasClass("hide") || !$domEl.is(":visible"); 2328 }
2329 if (isHidden) { 2330 return; // do not count hidden questions toward required/unanswered 2331 } 2332 } catch (e) {} 2333 // --- Special handling for Funding Allocator poll type (budget allocation) --- 2334 if ( 2335 poll.metaData && 2336 poll.metaData.polltype === CONSTANTS.PollTypes.FundingAllocator 2337 ) { 2338 // A FundingAllocator poll embeds the FundingAllocator widget which manages its own 2339 // submit flow. The widget exposes `isSubmitted` once the user has successfully 2340 // sent their allocations. Use this flag to determine completion so the End-Zone 2341 // logic does not incorrectly mark the question as unanswered. 2342 2343 if (poll.fundingAllocator && poll.fundingAllocator.isSubmitted) { 2344 ret.answered++; 2345 } else { 2346 // Treat as unanswered (required or optional) if submission not complete 2347 if (poll.data && poll.data.requiredFormat == 1) { 2348 ret.missingRequired = true; 2349 ret.requiredUnanswered++; 2350 ret.requiredUnansweredPolls.push(poll.data); 2351 } else { 2352 ret.unanswered++; 2353 ret.unansweredPolls.push(poll.data); 2354 } 2355 } 2356 return; // move to next poll â no further checks needed for this type 2357 } 2358 poll._doubleCheckHasResponded(); 2359 var metaData = poll.metaData; 2360 var hasAlreadyCommented = metaData.hascommented; 2361 2362 // checks to see if poll comment textbox is NOT answered 2363 if ( 2364 poll.data && 2365 poll.data.commentSettings == CONSTANTS.CommentSettings.Open && 2366 poll.commentSection && 2367 poll.commentSection.hasValidComment && 2368 !poll.commentSection.hasValidComment() && 2369 poll.data.private == false && 2370 !hasAlreadyCommented 2371 ) { 2372 if (poll.data.requiredFormat == 1) { 2373 ret.missingRequired = true; 2374 ret.requiredUnanswered++; 2375 ret.requiredUnansweredPolls.push(poll.data); 2376 } else { 2377 ret.unanswered++; 2378 ret.unansweredPolls.push(poll.data); 2379 } 2380 return; 2381 } 2382 2383 // checks to see if poll comment has been populated, but required info not provided 2384 if ( 2385 poll.data && 2386 poll.commentSection && 2387 poll.commentSection.hasValidComment && 2388 poll.commentSection.commentSectionMissingRequiredFields && 2389 poll.commentSection.commentSectionMissingRequiredFields() && 2390 metaData.cancomment && 2391 !(metaData.hascommented || poll.commentSection.hasValidComment()) 2392 ) { 2393 ret.missingRequired = true; 2394 ret.requiredUnanswered++; 2395 ret.requiredUnansweredPolls.push(poll.data); 2396 2397 return; 2398 } 2399 2400 if ( 2401 (metaData.cancomment && metaData.hascommented) || 2402 (metaData.canvote && metaData.hasvoted) 2403 ) { 2404 ret.answered++; 2405 return; 2406 } 2407 2408 if (metaData.cancomment || metaData.canvote) { 2409 if (poll.selectedOptions && poll.selectedOptions.length) { 2410 // we're submitting this currently (most likely multi-choice) 2411 ret.answered++; 2412 console.log({ 2413 note: "Unanswered but prob being submitted! " + poll.data.description, 2414 poll: poll.data 2415 }); 2416 } else if (poll.$element.find(".poll-results-outer").length) { 2417 ret.answered++; 2418 } else if (poll.data.requiredFormat == 1) { 2419 ret.missingRequired = true; 2420 ret.requiredUnanswered++; 2421 ret.requiredUnansweredPolls.push(poll.data); 2422 } else { 2423 ret.unanswered++; 2424 ret.unansweredPolls.push(poll.data); 2425 } 2426 } 2427 }); 2428 2429 ret.total = ret.answered + ret.unanswered; 2430 2431 var unansweredString = ""; 2432 if (ret.unanswered > 0) { 2433 unansweredString = 2434 (ret.unanswered == 1 ? "one" : ret.unanswered) + 2435 " unanswered question" + 2436 (ret.unanswered == 1 ? "" : "s"); 2437 } 2438 2439 var reqUnansweredString = ""; 2440 if (ret.requiredUnanswered > 0) { 2441 reqUnansweredString = 2442 (ret.requiredUnanswered == 1 ? " one" : ret.requiredUnanswered) + 2443 " required unanswered question" + 2444 (ret.requiredUnanswered == 1 ? "" : "s"); 2445 } 2446 2447 if (unansweredString.length && reqUnansweredString.length) { 2448 ret.remainingString = unansweredString + " and " + reqUnansweredString; 2449 } else if (unansweredString.length || reqUnansweredString.length) { 2450 // just append the string with a count - one or both will be empty 2451 ret.remainingString = unansweredString + reqUnansweredString; 2452 } 2453 2454 return ret; 2455}; 2456 2457SubmitSurveyUI.prototype._populateUnansweredDiv = function (progress, fadeOut) { 2458 var that = this; 2459 var $progressMsgRequired = that.UI.$element.find( 2460 "#progress-required-message-outer" 2461 ); 2462 var $progressMsg = that.UI.$element.find("#progress-message-outer"); 2463 var hasOnSubmitFreeForm = that.onSubmitSettings.onSubmitFreeformId > 0; 2464 2465 $progressMsgRequired.html( 2466 Mustache.render($("#survey-required-completion-pending").html(), { 2467 progress: progress 2468 }) 2469 ); 2470 $progressMsg.html( 2471 Mustache.render($("#survey-completion-pending").html(), { 2472 progress: progress 2473 }) 2474 ); 2475 2476 var $progressNotRequiredOpenQuestions = $progressMsg.find( 2477 ".input-not-required" 2478 ); 2479 $progressMsgRequired.hide(); 2480 $progressNotRequiredOpenQuestions.hide(); 2481 2482 var $requiredUnansweredDiv = that.UI.$element.find( 2483 "#required-unanswered-questions" 2484 ); 2485 $requiredUnansweredDiv.empty().hide(); 2486 2487 var $unansweredDiv = that.UI.$element.find("#unanswered-questions"); 2488 $unansweredDiv.empty(); 2489 2490 $.each( 2491 progress.requiredUnansweredPolls, 2492 function (idx, requiredUnansweredPoll) { 2493 if ( 2494 requiredUnansweredPoll.formattedDescription == null || 2495 requiredUnansweredPoll.formattedDescription.length == 0 2496 ) { 2497 requiredUnansweredPoll.formattedDescription = "Multi-question module"; 2498 } 2499 $requiredUnansweredDiv.append( 2500 Mustache.render($("#required-unanswered-question").html(), { 2501 poll: requiredUnansweredPoll 2502 }) 2503 ); 2504 $requiredUnansweredDiv.show(); 2505 $progressMsgRequired.show(); 2506 } 2507 ); 2508 2509 if (fadeOut) { 2510 that._fadeOutContent(); 2511 } else { 2512 that.UI.$element.find(".panel-show-content").hide(); 2513 } 2514 2515 if (progress.requiredUnanswered > 0) { 2516 return; 2517 } 2518 2519 $.each(progress.unansweredPolls, function (idx, unansweredPoll) { 2520 if ( 2521 unansweredPoll.formattedDescription == null || 2522 unansweredPoll.formattedDescription.length == 0 2523 ) { 2524 unansweredPoll.formattedDescription = "Multi-question module"; 2525 } 2526 $unansweredDiv.append( 2527 Mustache.render($("#unanswered-question").html(), { 2528 poll: unansweredPoll 2529 }) 2530 ); 2531 $progressNotRequiredOpenQuestions.show(); 2532 }); 2533 2534 if (hasOnSubmitFreeForm) { 2535 if (progress.requiredUnanswered > 0) { 2536 var $customMsg = $("<div>").addClass("hidden"); 2537 } else { 2538 var $customMsg = $("<div>").addClass("survey-progress-complete custom"); 2539 } 2540 var postData = { 2541 customerId: that.customerId, 2542 projectId: that.projectId, 2543 freeformId: that.onSubmitSettings.onSubmitFreeformId 2544 }; 2545 2546 $progressMsg.prepend($customMsg); 2547 2548 $customMsg.html(tools.spin(null, "Submitting responses...", true)); 2549 2550 $.post("/Project/GetOnSubmitFreeformHtml", postData).done(function (data) { 2551 if (data.htmlText && data.htmlText.length) { 2552 $customMsg.html(data.htmlText); 2553 2554 that.UI.$element.find("#responsesSubmitted").hide(); 2555 } else { 2556 that._populateDefaultSubmitMessage($progressMsg, false); 2557 } 2558 });
2559 } 2560}; 2561 2562SubmitSurveyUI.prototype._fadeOutContent = function () { 2563 this.UI.$element.find(".panel-show-content").show(); 2564 this.UI.$element.find(".project-content").slideToggle("slow"); 2565 this.UI.$element.find(".project-content").fadeOut(); 2566}; 2567 2568$("#content").delegate("*", "focus blur", function () { 2569 var elem = $(this); 2570 setTimeout(function () { 2571 elem.toggleClass( 2572 "focused-element", 2573 elem.is(":focus") && elem.is(".vote-button") 2574 ); 2575 }, 0); 2576});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.