PageSourceSearch

https://kciaplanning.com/Scripts/Project/ProjectPageUI.js

js kciaplanning.com collected 2026-10-05 22:48:13 UTC 82,173 bytes, 2,576 lines download raw bytes

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(/&amp;/g, "&") +
650      (that.config.topURL == ""
651        ? "&name=" + poll.description.replace(/&amp;/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?&amp;text=" +
696      encodeURIComponent(
697        poll.description.replace(/&amp;/g, "&") +
698          " - " +
699          poll.caption.replace(/&amp;/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">&times;</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.