PageSourceSearch

https://nystransportationmasterplan.com/Scripts/chat-widget.js

js nystransportationmasterplan.com collected 2026-09-29 15:07:31 UTC 25,282 bytes, 783 lines download raw bytes

1var CitizenChatWidget = (function(window, $) {
2    if (window.cz == null)
3        window.cz = {};
4
5    /**
6     * Default configuration values
7     */
8    var DEFAULTS = {
9        WIDGET_CONTAINER: null,
10        DATA_LOAD_URL: "",
11        SEND_RESPONSE_URL: "/Comments/Comment",
12        SEND_VOTE_URL: "/Vote/ChatBotResponse",
13        MESSAGE_PRELOADER_DURATION: 1000,
14        ACCEPT_RESPONSE_MESSAGE: "Got it!",
15        DATA_LOAD_ERROR_MESSAGE: "There was an error during connection to the server. Please, try again later"
16    };
17
18    /**
19     * Widget configuration
20     * @param configuration - object with a widget configuration
21     */
22    function Configuration(configuration) {
23        var widgetContainer = configuration.widgetContainer || DEFAULTS.WIDGET_CONTAINER,
24            dataLoadUrl = configuration.dataLoadUrl || DEFAULTS.DATA_LOAD_URL,
25            sendResponseUrl = configuration.sendResponseUrl || DEFAULTS.SEND_RESPONSE_URL,
26            sendVoteUrl = configuration.sendVoteUrl || DEFAULTS.SEND_VOTE_URL,
27            acceptResponseMessage = configuration.acceptResponseMessage || DEFAULTS.ACCEPT_RESPONSE_MESSAGE,
28            dataLoadErrorMessage = configuration.dataLoadErrorMessage || DEFAULTS.DATA_LOAD_ERROR_MESSAGE;
29
30
31        this.$widgetContainer = isJQueryElement(widgetContainer) ? widgetContainer : $(widgetContainer);
32        this.dataLoadUrl = dataLoadUrl.trim();
33        this.sendResponseUrl = sendResponseUrl.trim();
34        this.sendVoteUrl = sendVoteUrl.trim();
35        this.acceptResponseMessage = acceptResponseMessage.trim();
36        this.dataLoadErrorMessage = dataLoadErrorMessage.trim();
37        this.messagePreloaderDuration = configuration.messagePreloaderDuration || DEFAULTS.MESSAGE_PRELOADER_DURATION;
38    }
39
40    /**
41     * Validates the current configuration
42     * @returns {boolean} - whether the current configuration is valid
43     */
44    Configuration.prototype.validate = function() {
45        if (this.$widgetContainer.length === 0)
46            return false;
47
48        if (!this.dataLoadUrl)
49            return false;
50
51        return true;
52    };
53
54
55    /**
56     * User interface for the widget
57     */
58    function UI(configuration) {
59        this._configuration = configuration;
60        this._currentPromptIndex = 0;
61        this._shownPromptsCount = 0;
62
63        this._configuration.$widgetContainer.addClass("ccw-container");
64        this._$content = createElement("div", "ccw-content").appendTo(this._configuration.$widgetContainer);
65
66        this._addEventHandlers();
67    }
68
69    /**
70     * Handles success data loading event
71     */
72    UI.prototype.onDataLoadSuccess = function(data) {
73        this._data = data;
74
75        this._startQuiz();
76        if (this._data._externalParams)
77            this._processExternalParams(this._data._externalParams.goTo, this._data._externalParams.voteOption);
78    };
79
80    
81
82    /**
83     * Handles failed data loading event
84     */
85    UI.prototype.onDataLoadFail = function() {
86        this._showErrorMessage("There was an error during connection to the server. Please, try again later");
87    };
88
89    /**
90     * Adds event handlers to the widget
91     * @private
92     */
93    UI.prototype._addEventHandlers = function() {
94        var that = this;
95
96        this._$content.on("click", ".ccw-response", function() {
97            var $commentsContainer = that._$content.find(".ccw-comments-container"),
98                $responsesContainer = $(this).parent();
99
100            var currentPrompt = that._getCurrentPrompt();
101            if (!currentPrompt) {
102                return true;
103            }
104
105            var response = that._findResponseById(currentPrompt, $(this).data("id"));
106
107            $commentsContainer.remove();
108            $responsesContainer.remove();
109            that._sendVoteResponse(response)
110            that._showUserMessage(response.Text);
111            that._showVotesStatistics(response);
112
113            that._showPrompt(that._getNextPrompt());
114
115            return false;
116        });
117
118        this._$content.on("submit", ".ccw-comments-container", function() {
119            var $commentsForm = $(this),
120                $commentsInput = $commentsForm.find(".ccw-comments-input"),
121                $responsesContainer = that._$content.find(".ccw-responses-container");
122
123            var responseText = $commentsInput.val().trim();
124
125            if (responseText === "") {
126                return false;
127            }
128
129            if (that._configuration.sendResponseUrl) {
130                var postData = cz;
131                postData.comment = responseText;
132                postData.pollId = that._getCurrentPrompt().Id;
133
134                $.post(that._configuration.sendResponseUrl, postData);
135            }
136
137            $commentsForm.remove();
138            $responsesContainer.remove();
139
140            that._showUserMessage(responseText);
141            that._showAcceptResponseMessage();
142            that._showPrompt(that._getNextPrompt());
143
144            return false;
145        });
146    };
147
148    /**
149     * Starts the quiz
150     * @private
151     */
152    UI.prototype._startQuiz = function() {
153        this._showPrompt(this._getFirstPrompt());
154    };
155
156
157
158    /**
159     * Clears the window
160     * @private
161     */
162    UI.prototype._clearChatWindow = function () {
163        this._$content.html("");
164    };
165
166
167    /**
168     * Finds a response by it's id
169     * @param prompt - prompt object
170     * @param id - id of the response to find
171     * @returns {Object} - found response
172     * @private
173     */
174    UI.prototype._findResponseById = function(prompt, id) {
175        return prompt.Responses.filter(function(response) {
176            return response.Id === (id * 1);
177        })[0];
178    };
179
180
181    /**
182     * Finds prompt that has the response with matching option id
183     * @param optionid - poll option id of the response to find
184     * @returns {Object} - found prompt
185     * @private
186     */
187    UI.prototype._getPromptByResponseOptionId = function (optionId) {
188        var prompts = this._data.Prompts;
189        return prompts.filter(function (prompt) {
190            return prompt.Responses.filter(function (response) {
191                response.Id === (optionId * 1);
192            })
193        })[0];
194    }
195
196    /**
197     * Moves prompt to different index
198     * @private
199     */
200    UI.prototype._changePromptIndex = function (oldIndex, newIndex) {
201        var prompts = this._data.Prompts;
202        prompts.move(oldIndex, newIndex);
203    }
204
205    Array.prototype.move = function (old_index, new_index) {
206        if (new_index >= this.length) {
207            var k = new_index - this.length;
208            while ((k--) + 1) {
209                this.push(undefined);
210            }
211        }
212        this.splice(new_index, 0, this.splice(old_index, 1)[0]);
213        return this; // for testing purposes
214    };
215
216
217    this.ProcessExternalParams = function (goTo, voteOption) {
218        return this._ui._processExternalParams(goTo, voteOption);
219    }
220
221    /**
222     * Handles the external vote and goto parameters that may arise from a click-to-vote
223     * @param goTo - the poll to show first in the series
224     * @param voteOption - the option the user has clicked, wishing to vote for
225     * @public
226     */
227    UI.prototype._processExternalParams = function (goTo, voteOption) {
228        if (!this._data.Prompts) {
229            this._data._externalParams = {
230                goTo: goTo,
231                voteOption: voteOption
232            };
233            return;
234        }
235        var that = this;
236            var $commentsContainer = that._$content.find(".ccw-comments-container"),
237                $responsesContainer = $(this).parent();
238
239            var prompt = that._getCurrentPrompt();
240            var newFirstIdx = 0;
241            if (voteOption > 0) {
242                clearTimeout(that._showPromptTimeout);
243                that._clearChatWindow();
244                prompt = that._getPromptByResponseOptionId(voteOption) || that._getCurrentPrompt();
245                var response = that._findResponseById(prompt, voteOption);
246
247
248                if (!response) return;
249
250                var oldIndex = that._data.Prompts.indexOf(prompt);
251                that._changePromptIndex(oldIndex, newFirstIdx);
252
253                $commentsContainer.remove();
254                $responsesContainer.remove();
255                that._showHostMessage(prompt.Text);
256                that._registerPollView(prompt);
257
258                that._data.VotedOptionIdList.push(response.Id);
259                that._showUserMessage(response.Text);
260                that._showVotesStatistics(response);
261                that._showPrompt(that._getNextPrompt());
262                if (goTo < 1 || goTo == prompt.Id)
263                    return;
264                newFirstIdx++;
265            }
266            else if (goTo > 0) {
267                clearTimeout(that._showPromptTimeout);
268                that._clearChatWindow();
269                prompt = that._getPromptByPollId(goTo) || that._getCurrentPrompt();
270                if (!prompt) return;
271
272                var oldIndex = that._data.Prompts.indexOf(prompt);
273                that._changePromptIndex(oldIndex, newFirstIdx);
274
275                $commentsContainer.remove();
276                $responsesContainer.remove();
277                that._showPrompt(prompt);
278                return;
279            }
280    }
281
282
283
284    /**
285     * Shows the prompt
286     * @param prompt - a prompt message to be shown
287     * @private
288     */
289    UI.prototype._showPrompt = function(prompt) {
290        if (this._noPromptsToShow()) {
291            this._showConclusion();
292            return;
293        }
294
295        if (!prompt) {
296            this._showPrompt(this._getNextPrompt())
297            return;
298        }
299
300        if (prompt.SkipLogic != null) {
301            var overlap = intersect_safe(prompt.SkipLogic.OptionTriggerList, this._data.VotedOptionIdList);
302            if (overlap.length > 0 && prompt.SkipLogic.SkipToPollId > 0) {
303                this._showPrompt(this._getPromptByPollId(prompt.SkipLogic.SkipToPollId));
304            }
305            else
306                this._showPrompt(this._getNextPrompt())
307            return;
308        }
309
310        this._showHostMessage(prompt.Text);
311        this._registerPollView(prompt);
312
313        var that = this;
314        that._showPromptTimeout = setTimeout(function() {
315            that._showResponses(prompt.Responses);
316
317            if (prompt.AcceptComments) {
318                that._showCommentsInput();
319            }
320        }, this._configuration.messagePreloaderDuration);
321
322        this._updateScroll();
323    };
324
325    /**
326      * Shows a pixel for the 'view' event
327      * @param prompt - prompt object
328      * @private
329      */
330    UI.prototype._registerPollView = function (prompt) {
331        var postData = cz;
332        postData.pollId = prompt.Id;
333        postData.evId = 39;
334        $.post("/Pixel/GenEvent", postData);
335    }
336
337
338    /**
339     * Shows votes statistics
340     * @param response - response object
341     * @private
342     */
343    UI.prototype._showVotesStatistics = function(response) {
344        var sumVotes = this._getCurrentPrompt().Responses
345            .map(function(response) {
346                return response.VoteTotal || 0;
347            })
348            .reduce(function(v1, v2) {
349                return v1 + v2;
350            });
351
352        var votesPercentage = Math.round(((response.VoteTotal || 0) + 1) / (sumVotes + 1) * 100);
353
354        this._showHostMessage(response.Text + " received " + votesPercentage + "% of the responses", true);
355    };
356
357    /**
358     * Posts User Vote
359     * @param response - response object
360     * @private
361     */
362    UI.prototype._sendVoteResponse = function (response) {        
363        var that = this;
364        var postData = cz;
365        postData.option = response.Id + '';
366        postData.pollId = response.PollId;
367        $.post(this._configuration.sendVoteUrl, postData);
368        this._data.VotedOptionIdList.push(response.Id);
369        if (response.Href != null)
370            window.open(response.Href);
371        var img = new Image(1, 1);
372        if (this._data.MetaData.VotePixelSrc.length > 0)
373            img.src = this._data.MetaData.VotePixelSrc + "&extra=" + response.Id
374    };
375
376
377    /**
378     * Shows the responses to select
379     * @param responses - an array of responses
380     * @private
381     */
382    UI.prototype._showResponses = function(responses) {
383        if (!responses || responses.length === 0) {
384            return;
385        }
386
387        $responsesContainer = createElement("div", "ccw-responses-container clearfix").appendTo(this._$content);
388
389        var that = this;
390        responses.forEach(function(response) {
391            if (that._validateResponse(response)) {
392                createElement("a", "ccw-response", {
393                    "href": response.Href,
394                    "target": response.Href ? "_blank" : "",
395                    "text": response.Text.trim(),
396                    "data-id": response.Id,
397                    "data-value": response.Text.trim()
398                }).appendTo($responsesContainer);
399            }
400        });
401
402        this._updateScroll();
403    };
404
405    /**
406     * Shows a host message
407     * @param text - the message text
408     * @private
409     */
410    UI.prototype._showHostMessage = function(text, withoutAnimation) {
411        var hostText = this._shownPromptsCount === 0 ?
412            //this._data.MetaData.Name + ": " + 
413            text.trim() :
414            text.trim();
415
416        var $messageContainer = createElement("div", "ccw-message clearfix").appendTo(this._$content),
417            $messageIcon = createElement("img", "ccw-message-icon", { src: this._data.MetaData.IconSrc }).appendTo($messageContainer),
418            $messageText = createElement("div", "ccw-message-text").appendTo($messageContainer);
419
420        if (withoutAnimation) {
421            $messageText.html(hostText);
422        } else {
423            this._toggleTypingPreloader($messageText);
424
425            var that = this;
426            setTimeout(function() {
427                that._toggleTypingPreloader($messageText);
428
429                $messageText.html(hostText);
430            }, this._configuration.messagePreloaderDuration);
431        }
432
433        this._shownPromptsCount++;
434
435        this._removePreviousMessageIcon($messageContainer);
436    };
437
438    /**
439     * Shows a user message
440     * @param text - the message text
441     * @private
442     */
443    UI.prototype._showUserMessage = function(text) {
444        var $messageContainer = createElement("div", "ccw-message ccw-user-message clearfix").appendTo(this._$content),
445            $messageText = createElement("div", "ccw-user-message-text", { text: text }).appendTo($messageContainer);
446    };
447
448    /**
449     * Shows an accept response host message
450     * @param text - the message text
451     * @private
452     */
453    UI.prototype._showAcceptResponseMessage = function() {
454        var text = this._configuration.acceptResponseMessage;
455
456        var $messageContainer = createElement("div", "ccw-message clearfix").appendTo(this._$content),
457            $messageIcon = createElement("img", "ccw-message-icon", { src: this._data.MetaData.IconSrc }).appendTo($messageContainer),
458            $messageText = createElement("div", "ccw-accept-message-text", { text: text }).appendTo($messageContainer);
459
460        this._removePreviousMessageIcon($messageContainer);
461    };
462
463    UI.prototype._showCommentsInput = function() {
464        var $commentsContainer = createElement("form", "ccw-comments-container clearfix").appendTo(this._$content);
465
466        var $commentsIcon = createElement("div", "ccw-comments-icon").appendTo($commentsContainer),
467            $commentsInput = createElement("input", "ccw-comments-input", { type: "text" }).appendTo($commentsContainer).focus(),
468            $commentsButton = createElement("button", "ccw-comments-button", { type: "submit", text: "Send" }).appendTo($commentsContainer);
469    };
470
471    UI.prototype._showConclusion = function() {
472        if (!this._validateConclusion())
473            return;
474
475        var $conclusionContainer = createElement("div", "ccw-message ccw-conclusion clearfix").appendTo(this._$content),
476            $conclusionImage = createElement("img", "ccw-message-icon", { src: this._data.MetaData.IconSrc }).appendTo($conclusionContainer),
477            $conclusionContent = createElement("div", "ccw-conclusion-content").appendTo($conclusionContainer),
478            $conclusionObject = createConclusionObject(this._data.Conclusion);
479
480        this._toggleTypingPreloader($conclusionContent);
481
482        var that = this;
483        setTimeout(function() {
484            that._toggleTypingPreloader($conclusionContent);
485
486            $conclusionContent.append($conclusionObject);
487
488            that._showResponses(that._data.Conclusion.Responses, true);
489        }, this._configuration.messagePreloaderDuration);
490
491        this._removePreviousMessageIcon($conclusionContainer);
492        this._updateScroll();
493
494
495        function createConclusionObject(conclusion) {
496            var $conclusionObject;
497            if(conclusion.href)
498                $conclusionObject = createElement("a", "", { "href": conclusion.Href, target: "_blank" });
499            else
500                $conclusionObject = createElement("span", "");
501
502            if (conclusion.Image) {
503                createElement("img", "ccw-conclusion-image", { src: conclusion.Image }).appendTo($conclusionObject)
504            }
505
506            if (conclusion.Text) {
507                createElement("div", "ccw-conclusion-message", { text: conclusion.Text }).appendTo($conclusionObject)
508            }
509
510            return $conclusionObject;
511        }
512    };
513
514    /**
515     * Shows an error message
516     * @param text - the message text
517     * @private
518     */
519    UI.prototype._showErrorMessage = function(text) {
520        this._showHostMessage(text);
521    };
522
523    /**
524     * Updates a scroll position so it will be at the bottom of the widget
525     * @private
526     */
527    UI.prototype._updateScroll = function() {
528        this._$content.scrollTop(this._$content[0].scrollHeight);
529    };
530
531    /**
532     * Toggles typing preloader for the message container
533     * @param $messageContainer - message container which append a preloader to
534     * @private
535     */
536    UI.prototype._toggleTypingPreloader = function($element) {
537        if ($element.hasClass("ccw-typing")) {
538            $element.removeClass("ccw-typing");
539        } else {
540            $element.addClass("ccw-typing");
541        }
542    };
543
544    UI.prototype._removePreviousMessageIcon = function($element) {
545        var $previousElements = $element.prevUntil(".ccw-user-message", ".ccw-message");
546
547        $previousElements.find(".ccw-message-icon").remove();
548    };
549
550    /**
551     * Returns the first prompt object
552     * @returns {Object} - object that represents the first prompt
553     * @private
554     */
555    UI.prototype._getFirstPrompt = function() {
556        var prompt = this._data.Prompts[0];
557
558        if (!this._validatePrompt(prompt))
559            return null;
560
561        return prompt;
562    };
563
564    /**
565     * Returns the current prompt
566     * @returns {Object} - object that represents the current prompt
567     * @private
568     */
569    UI.prototype._getCurrentPrompt = function() {
570        var prompt = this._data.Prompts[this._currentPromptIndex];
571
572        if (!this._validatePrompt(prompt))
573            return null;
574
575        return prompt;
576    };
577
578    /**
579     * Returns the next prompt object
580     * @returns {Object} - object that represents the next prompt
581     * @private
582     */
583    UI.prototype._getNextPrompt = function() {
584        var prompt = this._data.Prompts[++this._currentPromptIndex];
585
586        if (!this._validatePrompt(prompt))
587            return null;
588
589        return prompt;
590    };
591
592    UI.prototype._getPromptByPollId = function (pollId) {
593        var i, len;
594        for (len = this._data.Prompts.length, i = 0; i < len; ++i) {
595            if (this._data.Prompts[i].Id == pollId) {
596                if (!this._validatePrompt(this._data.Prompts[i]))
597                    return null;
598
599                this._currentPromptIndex = i;
600                return this._data.Prompts[i];
601            } 
602        }
603        return null;
604    };
605
606    UI.prototype._noPromptsToShow = function() {
607        return this._currentPromptIndex >= this._data.Prompts.length;
608    }
609
610    UI.prototype._validatePrompt = function(prompt) {
611        if (!prompt)
612            return false;
613
614        if (isNullOrUndefined(prompt.Id))
615            return false;
616
617        if (!prompt.SkipLogic) {
618            if (!prompt.Text || prompt.Text.trim() === "")
619                return false;
620
621            if (!prompt.AcceptComments && !(prompt.Responses && prompt.Responses.length > 0))
622                return false;
623        }
624        
625
626        return true;
627    };
628
629    UI.prototype._validateResponse = function(response) {
630        if (!response.Id)
631            return false;
632
633        if (!response.Text || response.Text.trim() === "")
634            return false;
635
636        return true;
637    };
638
639    UI.prototype._validateConclusion = function() {
640        if (!this._data.Conclusion)
641            return false;
642
643        if (!this._data.Conclusion.Image
644            && !(this._data.Conclusion.Text && this._data.Conclusion.Text.trim())
645            && !(this._data.Conclusion.Responses && this._data.Conclusion.Responses.length > 0)) {
646
647            return false;
648        }
649
650        return true;
651    }
652
653
654    /**
655     * Widget data handler
656     * @param url - URL which is used to load the widget data
657     */
658    function DataHandler(url, ui) {
659        if (!url) {
660            throw new Error("CitizenChatWidget - URL to load data is not defined");
661        }
662
663        this._url = url;
664        this._ui = ui;
665    }
666
667    /**
668     * Loads data from the endpoint
669     */
670    DataHandler.prototype.loadData = function() {
671        var that = this;
672
673        return $.post(this._url, cz)
674            .done(onDataLoadSuccess)
675            .fail(onDataLoadFail);
676
677
678        function onDataLoadSuccess(data) {
679            if(!validateData(data)) {
680                console.warn("CitizenChatWidget - The data received is not in a proper format");
681                return;
682            }
683
684            that._ui.onDataLoadSuccess(data);
685        }
686
687        function onDataLoadFail() {
688            console.warn("CitizenChatWidget - There is an error during loading of the widget data");
689
690            that._ui.onDataLoadFail();
691        }
692
693        function validateData() {
694            return true;
695        }
696    };
697
698
699    function isJQueryElement(element) {
700        return element instanceof $;
701    }
702
703    function createElement(type, classNames, properties) {
704        var elementProperties = $.extend({ "class": classNames }, properties);
705
706        return $("<" + type + "/>", elementProperties);
707    }
708
709    function isNullOrUndefined(obj) {
710        return (obj === null || obj === undefined);
711    }
712
713
714    return {
715        _configuration: null,
716        _dataHandler: null,
717        _ui: null,
718
719        /**
720         * Initializes widget
721         * @param configuration - configuration object to setup the widget
722         * @returns {boolean} - whether the widget is initialized correctly
723         */
724        initialize: function(configuration) {
725            // configuration
726            this._configuration = new Configuration(configuration || {});
727
728            if (!this._configuration.validate()) {
729                console.warn("CitizenChatWidget - Configuration object is invalid");
730                return false;
731            }
732
733            // UI
734            this._ui = new UI(this._configuration);
735
736            // data loading
737            this._dataHandler = new DataHandler(this._configuration.dataLoadUrl, this._ui);
738            this._dataHandler.loadData();
739
740            return true;
741        },
742
743        /* test code*/
744        Configuration: Configuration,
745        DataHandler: DataHandler,
746        UI: UI
747        /* end test code */
748    };
749
750    /* finds the intersection of 
751 * two arrays in a simple fashion.  
752 *
753 * PARAMS
754 *  a - first array
755 *  b - second array
756 *
757 * NOTES
758 *
759 *  Should have O(n) operations, where n is 
760 *    n = MIN(a.length(), b.length())
761 */
762    function intersect_safe(a, b)
763    {
764        a = a.sort();
765        b = b.sort();
766        var ai=0, bi=0;
767        var result = [];
768
769        while( ai < a.length && bi < b.length )
770        {
771            if      (a[ai] < b[bi] ){ ai++; }
772            else if (a[ai] > b[bi] ){ bi++; }
773            else /* they're equal */
774            {
775                result.push(a[ai]);
776                ai++;
777                bi++;
778            }
779        }
780
781        return result;
782    }
783})(window, jQuery);

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.