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.