1//// .faq toggle 2$(function(){ 3 $('.faq__toggle').click(function () { 4 $(this).siblings('.faq__answer').slideToggle(300); 5 $(this).children('.faq__icon').toggleClass('rotate-90'); 6 }); 7}); 8 9//// .table-product-features toggle 10$(function () { 11 $('.feature-table-wrap > .feature-toggle').click(function () { 12 $(this).toggleClass('feature-open'); 13 $(this).siblings('div').slideToggle(300); 14 $(this).children('.icon').toggleClass('rotate-90'); 15 }); 16}); 17// Smooth scroll to a section 18function scrollTo(trigger, section) { 19 $(trigger).click(function (e) { 20 $('html, body').animate({ 21 scrollTop: $(section).offset().top 22 }, 'slow'); 23 e.preventDefault(); 24 }); 25} 26/* 27 Webnames Analytics 28 -------------------------------- 29 Module to allow easy abstraction of analytics work in Angular. 30 31 Has services for Google Analytics. 32 33 It's best implemented using the server side Analytics helpers 34 to construct the required JSON objects from BillPackages etc. 35 36 Modules 37 GAService webnamesAnalytics.ga.service.cs 38 39 Server Side Helpers 40 GoogleAnalyticsHelpers.cs 41 42 Dependencies 43 ~ Underscore : Using UnderscoreJS for any array/object work. 44 45 Author: Akos Sebestyen 2015 46*/ 47 48(function () { 49 angular.module("WebnamesAnalytics", []) 50 .config(['$logProvider', function($logProvider) { 51 $logProvider.debugEnabled(true); 52 } 53 ]); 54})(); 55/***** 56 WebnamesAnalytics::GoogleAnalytics Service 57 58 Handles Analytics requests from an Ajax call to push to Google Analytics 59 60 Sample Usage (in controller, etc) 61 GAService.AddProduct($a.remove[i][0]); 62 63 Depends on: 64 $window 65 $log 66****/ 67(function () { 68 angular.module("WebnamesAnalytics") 69 .service('GAService', ['$window', '$log', gaService]); 70 71 function gaService($window, $log) { 72 if (!$window.ga) { 73 // This could be inject tested. (like underscore) 74 throw "You must have GA defined globally to use this service!"; 75 } 76 77 //***************** IMPORTANT ***************/ 78 // use GoogleAnalyticsHelper.cs to create 'objects' server side and then pass those in 79 80 var service = this; 81 82 // Example 83 // ga('ec:addProduct', { 'id': 'PHOSTUNIX1FD_NEW' , 'name': 'Hosting Value Plan (Free Domain)' , 'brand': 'Webnames.ca' , 'variant': '6' , 'quantity': 1 , 'price': 37.5000 }); 84 service.AddProduct = addProduct; 85 service.AddProductAndSend = addProductAndSend; 86 87 service.RemoveProductAndSend = removeProductAndSend; 88 89 // Must be set by calling module module (set to undefined, but we don't test this later on) 90 service.GAProperties = undefined; 91 92 // Example 93 // ga('ec:setAction', 'add', { 'list': 'Cart Page' }); 94 service.TrackAction = trackAction; 95 96 // Example 97 // ga('send', 'event', 'Cart Page', 'Add to Cart'); 98 service.Send = send; 99 100 function addProduct(obj) { 101 $log.debug('ec:addProduct', obj); 102 $window.ga('ec:addProduct', obj); 103 } 104 105 function addProductAndSend(categoryName, actionName, arrProperties) { 106 for (var i = 0; i < arrProperties.length; i++) { 107 service.AddProduct(arrProperties[i]); 108 } 109 110 service.TrackAction('add', { 'list': actionName }); 111 112 service.Send(categoryName, actionName); 113 } 114 115 function removeProductAndSend(categoryName, actionName, arrProperties) { 116 for (var i = 0; i < arrProperties.length; i++) { 117 service.AddProduct(arrProperties[i]); 118 } 119 120 service.TrackAction('remove', { 'list': actionName }); 121 122 service.Send(categoryName, actionName); 123 } 124 125 function trackAction(action, obj) { 126 $log.debug('ec:setAction', action, obj); 127 $window.ga('ec:setAction', action, obj); 128 } 129 130 function send(name, desc) { 131 $log.debug('send', 'event', name, desc); 132 $window.ga('send', 'event', name, desc); 133 } 134 } 135})(); 136(function () { 137 angular.module("WebnamesAnalytics") 138 .service('GA4Service', ['$window', '$log', GA4Service]); 139 140 function GA4Service($window, $log) { 141 142 var service = this; 143 144 function sendEvent (sEventName, sEventData) { 145 gtag('event', sEventName, sEventData); 146 }; 147 148 service.sendAddToCart = function (oData) { 149 var sEventName = "add_to_cart"; 150 sendEvent(sEventName, oData); 151 152 } 153 service.sendRemoveFromCart = function (oData) { 154 var sEventName = "remove_from_cart"; 155 sendEvent(sEventName, oData); 156 157 } 158 } 159})(); 160/* 161 Webnames Common 162 -------------------------------- 163 Module to hold common Angular functions 164 165 Author: Akos Sebestyen 2015 166*/ 167(function () { 168 "use strict"; 169
170 angular.module('WebnamesCommon', []); 171 172})(); 173 174 175/* 176 Generic Injector 177 -------------------------------- 178 Used to inject data into Angular on pageload. 179 180 The provider exposes a "InjectGenericProperty" method that can be used at config time to get JSON data into the service. 181 182 Then you can access the data from GenericInjector.PropBag.yourProperty 183 184 Author: Akos Sebestyen 2015 185*/ 186(function () { 187 "use strict"; 188 189 angular.module('WebnamesCommon') 190 .provider('GenericInjector', function () { 191 var _propBag = {}; 192 193 // Assign the prop bag 194 var GenericInjector = function () { 195 this.PropBag = _propBag; 196 }; 197 198 return { 199 $get: ['$injector', function ($injector) { return $injector.instantiate(GenericInjector) }], 200 InjectGenericProperty: function (propName, propValue) { _propBag[propName] = propValue} 201 } 202 }); 203 204})(); 205 206 207(function () { 208 "use strict"; 209 210 angular.module("WebnamesCommon") 211 .factory("WnFocus", ['$timeout', '$document', function ($timeout, $document) { 212 return function(id) { 213 $timeout(function () { 214 var element = $document.getElementById(id); 215 if (element) 216 element.focus(); 217 }); 218 } 219 }]); 220})(); 221// Scrolls to the top of the given selector. Use offset to offset the value by pixels 222 223(function () { 224 "use strict"; 225 226 angular.module("WebnamesCommon") 227 .factory("WnScroll", ['$timeout', function ($timeout) { 228 return function (selector, offset, animateTime) { 229 if (!offset) offset = 0; 230 if (!animateTime) animateTime = 500; 231 $timeout(function () { 232 $('html, body').animate({ scrollTop: ($(selector).offset().top + offset) }, animateTime); 233 }); 234 } 235 }]); 236})(); 237// https://gist.github.com/tommaitland/7579618 238(function () { 239 angular.module('WebnamesCommon').directive('ngDebounce', ['$timeout', function ($timeout) { 240 return { 241 restrict: 'A', 242 require: 'ngModel', 243 priority: 99, 244 link: function (scope, elm, attr, ngModelCtrl) { 245 if (attr.type === 'radio' || attr.type === 'checkbox') return; 246 247 elm.unbind('input'); 248 249 var debounce; 250 elm.bind('input', function () { 251 $timeout.cancel(debounce); 252 debounce = $timeout(function () { 253 scope.$apply(function () { 254 ngModelCtrl.$setViewValue(elm.val()); 255 }); 256 }, attr.ngDebounce || 1000); 257 }); 258 elm.bind('blur', function () { 259 scope.$apply(function () { 260 ngModelCtrl.$setViewValue(elm.val()); 261 }); 262 }); 263 } 264 265 } 266 }]); 267})(); 268 269(function () { 270 "use strict"; 271 272 angular.module('WebnamesCommon') 273 .filter('startFrom', function () { 274 275 return function (input, start) { 276 if (input !== undefined && angular.isArray(input)) { 277 return input.slice(start); 278 } else { 279 return []; 280 } 281 } 282 }) 283 .filter("getActiveKeys", [function () { 284 /** 285 * Takes an object and returns an array of keys where the values are truthy 286 * @param {Object} obj Flat object of key value pairs 287 * @returns {Array} array of keys with truthy values 288 */ 289 return function(obj) { 290 return _.chain(obj) 291 .pairs() // Turn object into array of arrays as key value pairs 292 .filter(function (kvp) { return !!kvp[1] }) // Return all Key-Value pairs where value is truthy 293 .map(function (kvp) { return kvp[0] }) // Map into an array of just strings of keys 294 .value(); 295 } 296 }]) 297 .filter('nullToEnd', function () { 298 return function (array, key) { 299 if (!angular.isArray(array)) return; 300 301 var present = array.filter(function (item) { 302 return item[key]; 303 }); 304 var empty = array.filter(function (item) { 305 return !item[key]; 306 }); 307 308 return present.concat(empty); 309 } 310 }) 311 .run(["$http", function ($http) { 312 $http.defaults.headers.common['X-XSRF-Token'] = 313 angular.element(document.querySelector('input[name="__RequestVerificationToken"]')).attr('value'); 314 }]); 315})(); 316 317(function () { 318 "use strict"; 319 angular.module("WebnamesLocationHelper", ["WebnamesCommon"]); 320})(); 321(function () { 322 "use strict"; 323 324 angular 325 .module("WebnamesLocationHelper") 326 .service("QueryStringFilterBinder", ["$location", "$rootScope", "$filter", QueryStringFilterBinder]); 327 328 function QueryStringFilterBinder($location, $rootScope, $filter) { 329 var service = this; 330 331 var currentBindings = []; 332 333 /** 334 * Unbinds all listeners set up 335 * @returns {undefined} 336 */ 337 service.UnbindAllFilters = function () { 338 var startingLength = currentBindings.length; 339 for (var i = 0; i < startingLength; i++) { 340 var binding = currentBindings.pop(); 341 342 binding.deregisterLocationListener(); 343 binding.deregisterWatch(); 344 } 345 } 346 347 /** 348 * Binds a flat object of filters to url params 349 * @param {Object} filterObj a flat object of filters which will be bound to the url params by type 350 * @param {Boolean} bExcludeFilterKeys a boolean flag to indicate whether it shouldn't bind to URL params if all of the filter properties are true 351 * @param {Object}
351 excludeObj an object containing parameters with keys that are not counted when determining if location parameters are applied 352 * @param {Object} includeObj an object containing parameters with keys that are counted when determining if location parameters are applied 353 * Note: if (includeObj === undefined), all parameters (even random ones) will be counted 354 * @returns {Function} a deregister function for this binding 355 */ 356 service.BindFilters = function (filterObj, bExcludeFilterKeys, excludeObj, includeObj) { 357 358 if (bExcludeFilterKeys === undefined) bExcludeFilterKeys = false; 359 if (excludeObj === undefined) excludeObj = {}; 360 361 if (!_.isObject(filterObj)) throw new Error("parameter being bound must be an object"); 362 363 // only apply location parameters if there are any in the URL 364 //(that aren't "excluded", and, if includeObj is defined, also are "included") 365 var bApplyLocationParams = false; 366 for (var sParam in $location.search()) { 367 if ($location.search().hasOwnProperty(sParam)) { 368 var bDontExclude = true; 369 for (var sExcludeParam in excludeObj) { 370 if (excludeObj.hasOwnProperty(sExcludeParam)) { 371 if (sParam == sExcludeParam) { 372 bDontExclude = false; 373 break; 374 } 375 } 376 } 377 if (!bDontExclude) continue; 378 379 if (includeObj === undefined) { 380 bApplyLocationParams = true; 381 break; 382 } 383 var bInclude = false; 384 for (var sIncudeParam in includeObj) { 385 if (includeObj.hasOwnProperty(sIncudeParam)) { 386 if (sParam == sIncudeParam) { 387 bInclude = true; 388 break; 389 } 390 } 391 } 392 if (bInclude) { 393 bApplyLocationParams = true; 394 break; 395 } 396 } 397 } 398 if (bApplyLocationParams) { 399 applyLocationParams(filterObj, bExcludeFilterKeys, true); 400 } 401 402 // Set up a watch on filter object to bind each one of its properties to changing the url 403 var deregisterWatch = $rootScope.$watch(function () { return filterObj }, function (newVal) { 404 _.each(_.keys(newVal), function (key) { 405 if (_.isBoolean(newVal[key])) { 406 // if we are doing exclusion, reverse our set operation 407 if (!bExcludeFilterKeys) { 408 $location.search(key, newVal[key] ? true : null); 409 } else { 410 $location.search(key, !newVal[key] ? true : null); 411 } 412 } 413 else if (_.isNumber(newVal[key]) || _.isString(newVal[key])) { 414 $location.search(key, newVal[key]); 415 } 416 else { 417 $location.search(key, newVal[key]); 418 } 419 }); 420 }, true); 421 422 var deregisterLocationListener = $rootScope.$on("$locationChangeSuccess", function () { 423 applyLocationParams(filterObj, bExcludeFilterKeys); 424 }); 425 426 currentBindings.push({ 427 filterObj: filterObj, 428 deregisterLocationListener: deregisterLocationListener, 429 deregisterWatch: deregisterWatch 430 }); 431 432 return function () { 433 var bindingIndex = _.findIndex(currentBindings, function(binding) { 434 return binding.filterObj === filterObj; 435 }); 436 currentBindings[bindingIndex].deregisterWatch(); 437 currentBindings[bindingIndex].deregisterLocationListener(); 438 currentBindings.splice(bindingIndex, 1); 439 } 440 } 441 442 function applyLocationParams(filterObj, allTrueDontBind) { 443 var params = $location.search(); 444 var paramKeys = _.keys(params); 445 // Loop through each key of the filter object and update its value based on URL parameters 446 _.each(_.keys(filterObj), function (key) { 447 448 switch (typeof filterObj[key]) { 449 case "boolean": 450 filterObj[key] = ((_.indexOf(paramKeys, key) < 0) ? !!allTrueDontBind : !allTrueDontBind); 451 break; 452 case "number": 453 if (params.hasOwnProperty(key) && !isNaN(params[key])) { 454 filterObj[key] = parseInt(params[key]); 455 } 456 break; 457 default: 458 filterObj[key]= params[key]; 459 break; 460 } 461 }); 462 } 463 } 464})(); 465 466(function () { 467 "use strict"; 468 469 angular.module("WebnamesLocationHelper") 470 .service("LegacyParam", ['$window', function($window) { 471 var service = this; 472 473 service.Params = {}; 474 475 /** 476 * Helper to get a list of domain name parameters from the URL 477 * @returns {Array} An array of domain parameters (params that begin with 'dom') 478 */ 479 service.GetDomains = function() { 480 return _.chain(service.Params) 481 .map(function (value, key) { 482 if (key.substr(0, 3) === "dom") return value; 483 else return undefined; 484 }) 485 .compact() 486 .uniq() 487 .value(); 488 } 489 490 var paramArray = $window.location.search.substr(1).split("&"); 491 492 _.each(paramArray, function (param) { 493 var pair = param.split("="); 494 service.Params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]); 495 }); 496 }]); 497})(); 498(function () { 499 "use strict"; 500 501 angular 502 .module("WebnamesLocationHelper") 503 .directive("wnHref", ["$window", wnHref]); 504 505 function wnHref($window) { 506 var directive = { 507 link: link, 508 restrict: "A" 509 }; 510 511 function link(scope, element, attrs) { 512 attrs.$set("href", $window._SmartURL + attrs.wnHref); 513 } 514 515 return directive; 516 } 517})(); 518/** 519 * @license AngularJS v1.8.2
vendor: 25,829 bytes, lines 519-1209
519 520 * (c) 2010-2020 Google LLC. http://angularjs.org 521 * License: MIT 522 */ 523(function(window, angular) {'use strict'; 524 525/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 526 * Any commits to this file should be reviewed with security in mind. * 527 * Changes to this file can potentially create security vulnerabilities. * 528 * An approval from 2 Core members with history of modifying * 529 * this file is required. * 530 * * 531 * Does the change somehow allow for arbitrary javascript to be executed? * 532 * Or allows for someone to change the prototype of built-in objects? * 533 * Or gives undesired access to variables likes document or window? * 534 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ 535 536var $sanitizeMinErr = angular.$$minErr('$sanitize'); 537var bind; 538var extend; 539var forEach; 540var isArray; 541var isDefined; 542var lowercase; 543var noop; 544var nodeContains; 545var htmlParser; 546var htmlSanitizeWriter; 547 548/** 549 * @ngdoc module 550 * @name ngSanitize 551 * @description 552 * 553 * The `ngSanitize` module provides functionality to sanitize HTML. 554 * 555 * See {@link ngSanitize.$sanitize `$sanitize`} for usage. 556 */ 557 558/** 559 * @ngdoc service 560 * @name $sanitize 561 * @kind function 562 * 563 * @description 564 * Sanitizes an html string by stripping all potentially dangerous tokens. 565 * 566 * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a trusted URI list) are 567 * then serialized back to a properly escaped HTML string. This means that no unsafe input can make 568 * it into the returned string. 569 * 570 * The trusted URIs for URL sanitization of attribute values is configured using the functions 571 * `aHrefSanitizationTrustedUrlList` and `imgSrcSanitizationTrustedUrlList` of {@link $compileProvider}. 572 * 573 * The input may also contain SVG markup if this is enabled via {@link $sanitizeProvider}. 574 * 575 * @param {string} html HTML input. 576 * @returns {string} Sanitized HTML. 577 * 578 * @example 579 <example module="sanitizeExample" deps="angular-sanitize.js" name="sanitize-service"> 580 <file name="index.html"> 581 <script> 582 angular.module('sanitizeExample', ['ngSanitize']) 583 .controller('ExampleController', ['$scope', '$sce', function($scope, $sce) { 584 $scope.snippet = 585 '<p style="color:blue">an html\n' + 586 '<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' + 587 'snippet</p>'; 588 $scope.deliberatelyTrustDangerousSnippet = function() { 589 return $sce.trustAsHtml($scope.snippet); 590 }; 591 }]); 592 </script> 593 <div ng-controller="ExampleController"> 594 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> 595 <table> 596 <tr> 597 <td>Directive</td> 598 <td>How</td> 599 <td>Source</td> 600 <td>Rendered</td> 601 </tr> 602 <tr id="bind-html-with-sanitize"> 603 <td>ng-bind-html</td> 604 <td>Automatically uses $sanitize</td> 605 <td><pre><div ng-bind-html="snippet"><br/></div></pre></td> 606 <td><div ng-bind-html="snippet"></div></td> 607 </tr> 608 <tr id="bind-html-with-trust"> 609 <td>ng-bind-html</td> 610 <td>Bypass $sanitize by explicitly trusting the dangerous value</td> 611 <td> 612 <pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()"> 613</div></pre> 614 </td> 615 <td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td> 616 </tr> 617 <tr id="bind-default"> 618 <td>ng-bind</td> 619 <td>Automatically escapes</td> 620 <td><pre><div ng-bind="snippet"><br/></div></pre></td> 621 <td><div ng-bind="snippet"></div></td> 622 </tr> 623 </table> 624 </div> 625 </file> 626 <file name="protractor.js" type="protractor"> 627 it('should sanitize the html snippet by default', function() { 628 expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')). 629 toBe('<p>an html\n<em>click here</em>\nsnippet</p>'); 630 }); 631 632 it('should inline raw snippet if bound to a trusted value', function() { 633 expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')). 634 toBe("<p style=\"color:blue\">an html\n" + 635 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 636 "snippet</p>"); 637 }); 638 639 it('should escape snippet without any filter', function() { 640 expect(element(by.css('#bind-default div')).getAttribute('innerHTML')). 641 toBe("<p style=\"color:blue\">an html\n" + 642 "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + 643 "snippet</p>"); 644 }); 645 646 it('should update', function() { 647 element(by.model('snippet')).clear(); 648 element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>'); 649 expect(element(by.css('#bind-html-with-sanitize div')).getAttribute('innerHTML')). 650 toBe('new <b>text</b>'); 651 expect(element(by.css('#bind-html-with-trust div')).getAttribute('innerHTML')).toBe( 652 'new <b onclick="alert(1)">text</b>'); 653 expect(element(by.css('#bind-default div')).getAttribute('innerHTML')).toBe( 654 "new <b onclick=\"alert(1)\">text</b>"); 655 }); 656 </file> 657 </example> 658 */ 659 660 661/** 662 * @ngdoc provider 663 * @name $sanitizeProvider 664 * @this 665 * 666 * @description 667 * Creates and configures {@link $sanitize} instance. 668 */ 669function $SanitizeProvider() { 670 var hasBeenInstantiated = false; 671 var svgEnabled = false; 672 673 this.$get = ['$$sanitizeUri', function($$sanitizeUri) { 674 hasBeenInstantiated = true; 675 if (svgEnabled) { 676 extend(validElements, svgElements); 677 } 678 return function(html) { 679 var buf = []; 680 htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { 681 return !/^unsafe:/.test($$sanitizeUri(uri, isImage)); 682 })); 683 return buf.join(''); 684 }; 685 }]; 686 687 688 /** 689 * @ngdoc method 690 * @name $sanitizeProvider#enableSvg 691 * @kind function 692 * 693 * @description 694 * Enables a subset of svg to be supported by the sanitizer. 695 * 696 * <div class="alert alert-warning"> 697 * <p>By enabling this setting without taking other precautions, you might expose your 698 * application to click-hijacking attacks. In these attacks, sanitized svg elements could be positioned 699 * outside of the containing element and be rendered over other elements on the page (e.g. a login 700 * link). Such behavior can then result in phishing incidents.</p> 701 * 702 * <p>To protect against these, explicitly setup `overflow: hidden` css rule for all potential svg 703 * tags within the sanitized content:</p> 704 * 705 * <br> 706 * 707 * <pre><code> 708 * .rootOfTheIncludedContent svg { 709 * overflow: hidden !important; 710 * } 711 * </code></pre> 712 * </div> 713 * 714 * @param {boolean=} flag Enable or disable SVG support in the sanitizer. 715 * @returns {boolean|$sanitizeProvider} Returns the currently configured value if called 716 * without an argument or self for chaining otherwise. 717 */ 718 this.enableSvg = function(enableSvg) { 719 if (isDefined(enableSvg)) { 720 svgEnabled = enableSvg; 721 return this; 722 } else { 723 return svgEnabled; 724 } 725 }; 726 727 728 /** 729 * @ngdoc method 730 * @name $sanitizeProvider#addValidElements 731 * @kind function 732 * 733 * @description 734 * Extends the built-in lists of valid HTML/SVG elements, i.e. elements that are considered safe 735 * and are not stripped off during sanitization. You can extend the following lists of elements: 736 * 737 * - `htmlElements`: A list of elements (tag names) to extend the current list of safe HTML 738 * elements. HTML elements considered safe will not be removed during sanitization. All other 739 * elements will be stripped off. 740 * 741 * - `htmlVoidElements`: This is similar to `htmlElements`, but marks the elements as 742 * "void elements" (similar to HTML 743 * [void elements](https://rawgit.com/w3c/html/html5.1-2/single-page.html#void-elements)). These 744 * elements have no end tag and cannot have content. 745 * 746 * - `svgElements`: This is similar to `htmlElements`, but for SVG elements. This list is only 747 * taken into account if SVG is {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for 748 * `$sanitize`. 749 * 750 * <div class="alert alert-info"> 751 * This method must be called during the {@link angular.Module#config config} phase. Once the 752 * `$sanitize` service has been instantiated, this method has no effect. 753 * </div> 754 * 755 * <div class="alert alert-warning"> 756 * Keep in mind that extending the built-in lists of elements may expose your app to XSS or 757 * other vulnerabilities. Be very mindful of the elements you add. 758 * </div> 759 * 760 * @param {Array<String>|Object} elements - A list of valid HTML elements or an object with one or 761 * more of the following properties: 762 * - **htmlElements** - `{Array<String>}` - A list of elements to extend the current list of 763 * HTML elements. 764 * - **htmlVoidElements** - `{Array<String>}` - A list of elements to extend the current list of 765 * void HTML elements; i.e. elements that do not have an end tag. 766 * - **svgElements** - `{Array<String>}` - A list of elements to extend the current list of SVG 767 * elements. The list of SVG elements is only taken into account if SVG is 768 * {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for `$sanitize`. 769 * 770 * Passing an array (`[...]`) is equivalent to passing `{htmlElements: [...]}`. 771 * 772 * @return {$sanitizeProvider} Returns self for chaining. 773 */ 774 this.addValidElements = function(elements) { 775 if (!hasBeenInstantiated) { 776 if (isArray(elements)) { 777 elements = {htmlElements: elements}; 778 } 779 780 addElementsTo(svgElements, elements.svgElements); 781 addElementsTo(voidElements, elements.htmlVoidElements); 782 addElementsTo(validElements, elements.htmlVoidElements); 783 addElementsTo(validElements, elements.htmlElements); 784 } 785 786 return this; 787 }; 788 789 790 /** 791 * @ngdoc method 792 * @name $sanitizeProvider#addValidAttrs 793 * @kind function 794 * 795 * @description 796 * Extends the built-in list of valid attributes, i.e. attributes that are considered safe and are 797 * not stripped off during sanitization. 798 * 799 * **Note**: 800 * The new attributes will not be treated as URI attributes, which means their values will not be 801 * sanitized as URIs using `$compileProvider`'s 802 * {@link ng.$compileProvider#aHrefSanitizationTrustedUrlList aHrefSanitizationTrustedUrlList} and 803 * {@link ng.$compileProvider#imgSrcSanitizationTrustedUrlList imgSrcSanitizationTrustedUrlList}. 804 * 805 * <div class="alert alert-info"> 806 * This method must be called during the {@link angular.Module#config config} phase. Once the 807 * `$sanitize` service has been instantiated, this method has no effect. 808 * </div> 809 * 810 * <div class="alert alert-warning"> 811 * Keep in mind that extending the built-in list of attributes may expose your app to XSS or 812 * other vulnerabilities. Be very mindful of the attributes you add. 813 * </div> 814 * 815 * @param {Array<String>} attrs - A list of valid attributes. 816 * 817 * @returns {$sanitizeProvider} Returns self for chaining. 818 */ 819 this.addValidAttrs = function(attrs) { 820 if (!hasBeenInstantiated) { 821 extend(validAttrs, arrayToMap(attrs, true)); 822 } 823 return this; 824 }; 825 826 ////////////////////////////////////////////////////////////////////////////////////////////////// 827 // Private stuff 828 ////////////////////////////////////////////////////////////////////////////////////////////////// 829 830 bind = angular.bind; 831 extend = angular.extend; 832 forEach = angular.forEach; 833 isArray = angular.isArray; 834 isDefined = angular.isDefined; 835 lowercase = angular.$$lowercase; 836 noop = angular.noop; 837 838 htmlParser = htmlParserImpl; 839 htmlSanitizeWriter = htmlSanitizeWriterImpl; 840 841 nodeContains = window.Node.prototype.contains || /** @this */ function(arg) { 842 // eslint-disable-next-line no-bitwise 843 return !!(this.compareDocumentPosition(arg) & 16); 844 }; 845 846 // Regular Expressions for parsing tags and attributes 847 var SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g, 848 // Match everything outside of normal chars and " (quote character) 849 NON_ALPHANUMERIC_REGEXP = /([^#-~ |!])/g; 850 851 852 // Good source of info about elements and attributes 853 // http://dev.w3.org/html5/spec/Overview.html#semantics 854 // http://simon.html5.org/html-elements 855 856 // Safe Void Elements - HTML5 857 // http://dev.w3.org/html5/spec/Overview.html#void-elements 858 var voidElements = stringToMap('area,br,col,hr,img,wbr'); 859 860 // Elements that you can, intentionally, leave open (and which close themselves) 861 // http://dev.w3.org/html5/spec/Overview.html#optional-tags 862 var optionalEndTagBlockElements = stringToMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'), 863 optionalEndTagInlineElements = stringToMap('rp,rt'), 864 optionalEndTagElements = extend({}, 865 optionalEndTagInlineElements, 866 optionalEndTagBlockElements); 867 868 // Safe Block Elements - HTML5 869 var blockElements = extend({}, optionalEndTagBlockElements, stringToMap('address,article,' + 870 'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' + 871 'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul')); 872 873 // Inline Elements - HTML5 874 var inlineElements = extend({}, optionalEndTagInlineElements, stringToMap('a,abbr,acronym,b,' + 875 'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' + 876 'samp,small,span,strike,strong,sub,sup,time,tt,u,var')); 877 878 // SVG Elements 879 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements 880 // Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted. 881 // They can potentially allow for arbitrary javascript to be executed. See #11290 882 var svgElements = stringToMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' + 883 'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' + 884 'radialGradient,rect,stop,svg,switch,text,title,tspan'); 885 886 // Blocked Elements (will be stripped) 887 var blockedElements = stringToMap('script,style'); 888 889 var validElements = extend({}, 890 voidElements, 891 blockElements, 892 inlineElements, 893 optionalEndTagElements); 894 895 //Attributes that have href and hence need to be sanitized 896 var uriAttrs = stringToMap('background,cite,href,longdesc,src,xlink:href,xml:base'); 897 898 var htmlAttrs = stringToMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' + 899 'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' + 900 'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' + 901 'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' + 902 'valign,value,vspace,width'); 903 904 // SVG attributes (without "id" and "name" attributes) 905 // https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes 906 var svgAttrs = stringToMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' + 907 'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' + 908 'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' + 909 'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' + 910 'height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,' + 911 'marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,' + 912 'max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,' + 913 'path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,' + 914 'requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,' + 915 'stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,' + 916 'stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,' + 917 'stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,' + 918 'underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,' + 919 'width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,' + 920 'xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan', true); 921 922 var validAttrs = extend({}, 923 uriAttrs, 924 svgAttrs, 925 htmlAttrs); 926 927 function stringToMap(str, lowercaseKeys) { 928 return arrayToMap(str.split(','), lowercaseKeys); 929 } 930 931 function arrayToMap(items, lowercaseKeys) { 932 var obj = {}, i; 933 for (i = 0; i < items.length; i++) { 934 obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true; 935 } 936 return obj; 937 } 938 939 function addElementsTo(elementsMap, newElements) { 940 if (newElements && newElements.length) { 941 extend(elementsMap, arrayToMap(newElements)); 942 } 943 } 944 945 /** 946 * Create an inert document that contains the dirty HTML that needs sanitizing. 947 * We use the DOMParser API by default and fall back to createHTMLDocument if DOMParser is not 948 * available. 949 */ 950 var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function(window, document) { 951 if (isDOMParserAvailable()) { 952 return getInertBodyElement_DOMParser; 953 } 954 955 if (!document || !document.implementation) { 956 throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document'); 957 } 958 var inertDocument = document.implementation.createHTMLDocument('inert'); 959 var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body'); 960 return getInertBodyElement_InertDocument; 961 962 function isDOMParserAvailable() { 963 try { 964 return !!getInertBodyElement_DOMParser(''); 965 } catch (e) { 966 return false; 967 } 968 } 969 970 function getInertBodyElement_DOMParser(html) { 971 // We add this dummy element to ensure that the rest of the content is parsed as expected 972 // e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag. 973 html = '<remove></remove>' + html; 974 try { 975 var body = new window.DOMParser().parseFromString(html, 'text/html').body; 976 body.firstChild.remove(); 977 return body; 978 } catch (e) { 979 return undefined; 980 } 981 } 982 983 function getInertBodyElement_InertDocument(html) { 984 inertBodyElement.innerHTML = html; 985 986 // Support: IE 9-11 only 987 // strip custom-namespaced attributes on IE<=11 988 if (document.documentMode) { 989 stripCustomNsAttrs(inertBodyElement); 990 } 991 992 return inertBodyElement; 993 } 994 })(window, window.document); 995 996 /** 997 * @example 998 * htmlParser(htmlString, { 999 * start: function(tag, attrs) {}, 1000 * end: function(tag) {}, 1001 * chars: function(text) {}, 1002 * comment: function(text) {} 1003 * }); 1004 * 1005 * @param {string} html string 1006 * @param {object} handler 1007 */ 1008 function htmlParserImpl(html, handler) { 1009 if (html === null || html === undefined) { 1010 html = ''; 1011 } else if (typeof html !== 'string') { 1012 html = '' + html; 1013 } 1014 1015 var inertBodyElement = getInertBodyElement(html); 1016 if (!inertBodyElement) return ''; 1017 1018 //mXSS protection 1019 var mXSSAttempts = 5; 1020 do { 1021 if (mXSSAttempts === 0) { 1022 throw $sanitizeMinErr('uinput', 'Failed to sanitize html because the input is unstable'); 1023 } 1024 mXSSAttempts--; 1025 1026 // trigger mXSS if it is going to happen by reading and writing the innerHTML 1027 html = inertBodyElement.innerHTML; 1028 inertBodyElement = getInertBodyElement(html); 1029 } while (html !== inertBodyElement.innerHTML); 1030 1031 var node = inertBodyElement.firstChild; 1032 while (node) { 1033 switch (node.nodeType) { 1034 case 1: // ELEMENT_NODE 1035 handler.start(node.nodeName.toLowerCase(), attrToMap(node.attributes)); 1036 break; 1037 case 3: // TEXT NODE 1038 handler.chars(node.textContent); 1039 break; 1040 } 1041 1042 var nextNode; 1043 if (!(nextNode = node.firstChild)) { 1044 if (node.nodeType === 1) { 1045 handler.end(node.nodeName.toLowerCase()); 1046 } 1047 nextNode = getNonDescendant('nextSibling', node); 1048 if (!nextNode) { 1049 while (nextNode == null) { 1050 node = getNonDescendant('parentNode', node); 1051 if (node === inertBodyElement) break; 1052 nextNode = getNonDescendant('nextSibling', node); 1053 if (node.nodeType === 1) { 1054 handler.end(node.nodeName.toLowerCase()); 1055 } 1056 } 1057 } 1058 } 1059 node = nextNode; 1060 } 1061 1062 while ((node = inertBodyElement.firstChild)) { 1063 inertBodyElement.removeChild(node); 1064 } 1065 } 1066 1067 function attrToMap(attrs) { 1068 var map = {}; 1069 for (var i = 0, ii = attrs.length; i < ii; i++) { 1070 var attr = attrs[i]; 1071 map[attr.name] = attr.value; 1072 } 1073 return map; 1074 } 1075 1076 1077 /** 1078 * Escapes all potentially dangerous characters, so that the 1079 * resulting string can be safely inserted into attribute or 1080 * element text. 1081 * @param value 1082 * @returns {string} escaped text 1083 */ 1084 function encodeEntities(value) { 1085 return value. 1086 replace(/&/g, '&'). 1087 replace(SURROGATE_PAIR_REGEXP, function(value) { 1088 var hi = value.charCodeAt(0); 1089 var low = value.charCodeAt(1); 1090 return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'; 1091 }). 1092 replace(NON_ALPHANUMERIC_REGEXP, function(value) { 1093 return '&#' + value.charCodeAt(0) + ';'; 1094 }). 1095 replace(/</g, '<'). 1096 replace(/>/g, '>'); 1097 } 1098 1099 /** 1100 * create an HTML/XML writer which writes to buffer 1101 * @param {Array} buf use buf.join('') to get out sanitized html string 1102 * @returns {object} in the form of { 1103 * start: function(tag, attrs) {}, 1104 * end: function(tag) {}, 1105 * chars: function(text) {}, 1106 * comment: function(text) {} 1107 * } 1108 */ 1109 function htmlSanitizeWriterImpl(buf, uriValidator) { 1110 var ignoreCurrentElement = false; 1111 var out = bind(buf, buf.push); 1112 return { 1113 start: function(tag, attrs) { 1114 tag = lowercase(tag); 1115 if (!ignoreCurrentElement && blockedElements[tag]) { 1116 ignoreCurrentElement = tag; 1117 } 1118 if (!ignoreCurrentElement && validElements[tag] === true) { 1119 out('<'); 1120 out(tag); 1121 forEach(attrs, function(value, key) { 1122 var lkey = lowercase(key); 1123 var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); 1124 if (validAttrs[lkey] === true && 1125 (uriAttrs[lkey] !== true || uriValidator(value, isImage))) { 1126 out(' '); 1127 out(key); 1128 out('="'); 1129 out(encodeEntities(value)); 1130 out('"'); 1131 } 1132 }); 1133 out('>'); 1134 } 1135 }, 1136 end: function(tag) { 1137 tag = lowercase(tag); 1138 if (!ignoreCurrentElement && validElements[tag] === true && voidElements[tag] !== true) { 1139 out('</'); 1140 out(tag); 1141 out('>'); 1142 } 1143 // eslint-disable-next-line eqeqeq 1144 if (tag == ignoreCurrentElement) { 1145 ignoreCurrentElement = false; 1146 } 1147 }, 1148 chars: function(chars) { 1149 if (!ignoreCurrentElement) { 1150 out(encodeEntities(chars)); 1151 } 1152 } 1153 }; 1154 } 1155 1156 1157 /** 1158 * When IE9-11 comes across an unknown namespaced attribute e.g. 'xlink:foo' it adds 'xmlns:ns1' attribute to declare 1159 * ns1 namespace and prefixes the attribute with 'ns1' (e.g. 'ns1:xlink:foo'). This is undesirable since we don't want 1160 * to allow any of these custom attributes. This method strips them all. 1161 * 1162 * @param node Root element to process 1163 */ 1164 function stripCustomNsAttrs(node) { 1165 while (node) { 1166 if (node.nodeType === window.Node.ELEMENT_NODE) { 1167 var attrs = node.attributes; 1168 for (var i = 0, l = attrs.length; i < l; i++) { 1169 var attrNode = attrs[i]; 1170 var attrName = attrNode.name.toLowerCase(); 1171 if (attrName === 'xmlns:ns1' || attrName.lastIndexOf('ns1:', 0) === 0) { 1172 node.removeAttributeNode(attrNode); 1173 i--; 1174 l--; 1175 } 1176 } 1177 } 1178 1179 var nextNode = node.firstChild; 1180 if (nextNode) { 1181 stripCustomNsAttrs(nextNode); 1182 } 1183 1184 node = getNonDescendant('nextSibling', node); 1185 } 1186 } 1187 1188 function getNonDescendant(propName, node) { 1189 // An element is clobbered if its `propName` property points to one of its descendants 1190 var nextNode = node[propName]; 1191 if (nextNode && nodeContains.call(node, nextNode)) { 1192 throw $sanitizeMinErr('elclob', 'Failed to sanitize html because the element is clobbered: {0}', node.outerHTML || node.outerText); 1193 } 1194 return nextNode; 1195 } 1196} 1197 1198function sanitizeText(chars) { 1199 var buf = []; 1200 var writer = htmlSanitizeWriter(buf, noop); 1201 writer.chars(chars); 1202 return buf.join(''); 1203} 1204 1205 1206// define ngSanitize module and register $sanitize service 1207angular.module('ngSanitize', []) 1208 .provider('$sanitize', $SanitizeProvider) 1209 .info({ angularVersion: '1.8.
12092
vendor: 7,335 bytes, lines 1209-1408
1209' }); 1210 1211/** 1212 * @ngdoc filter 1213 * @name linky 1214 * @kind function 1215 * 1216 * @description 1217 * Finds links in text input and turns them into html links. Supports `http/https/ftp/sftp/mailto` and 1218 * plain email address links. 1219 * 1220 * Requires the {@link ngSanitize `ngSanitize`} module to be installed. 1221 * 1222 * @param {string} text Input text. 1223 * @param {string} [target] Window (`_blank|_self|_parent|_top`) or named frame to open links in. 1224 * @param {object|function(url)} [attributes] Add custom attributes to the link element. 1225 * 1226 * Can be one of: 1227 * 1228 * - `object`: A map of attributes 1229 * - `function`: Takes the url as a parameter and returns a map of attributes 1230 * 1231 * If the map of attributes contains a value for `target`, it overrides the value of 1232 * the target parameter. 1233 * 1234 * 1235 * @returns {string} Html-linkified and {@link $sanitize sanitized} text. 1236 * 1237 * @usage 1238 <span ng-bind-html="linky_expression | linky"></span> 1239 * 1240 * @example 1241 <example module="linkyExample" deps="angular-sanitize.js" name="linky-filter"> 1242 <file name="index.html"> 1243 <div ng-controller="ExampleController"> 1244 Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea> 1245 <table> 1246 <tr> 1247 <th>Filter</th> 1248 <th>Source</th> 1249 <th>Rendered</th> 1250 </tr> 1251 <tr id="linky-filter"> 1252 <td>linky filter</td> 1253 <td> 1254 <pre><div ng-bind-html="snippet | linky"><br></div></pre> 1255 </td> 1256 <td> 1257 <div ng-bind-html="snippet | linky"></div> 1258 </td> 1259 </tr> 1260 <tr id="linky-target"> 1261 <td>linky target</td> 1262 <td> 1263 <pre><div ng-bind-html="snippetWithSingleURL | linky:'_blank'"><br></div></pre> 1264 </td> 1265 <td> 1266 <div ng-bind-html="snippetWithSingleURL | linky:'_blank'"></div> 1267 </td> 1268 </tr> 1269 <tr id="linky-custom-attributes"> 1270 <td>linky custom attributes</td> 1271 <td> 1272 <pre><div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"><br></div></pre> 1273 </td> 1274 <td> 1275 <div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"></div> 1276 </td> 1277 </tr> 1278 <tr id="escaped-html"> 1279 <td>no filter</td> 1280 <td><pre><div ng-bind="snippet"><br></div></pre></td> 1281 <td><div ng-bind="snippet"></div></td> 1282 </tr> 1283 </table> 1284 </file> 1285 <file name="script.js"> 1286 angular.module('linkyExample', ['ngSanitize']) 1287 .controller('ExampleController', ['$scope', function($scope) { 1288 $scope.snippet = 1289 'Pretty text with some links:\n' + 1290 'http://angularjs.org/,\n' + 1291 'mailto:[email protected],\n' + 1292 '[email protected],\n' + 1293 'and one more: ftp://127.0.0.1/.'; 1294 $scope.snippetWithSingleURL = 'http://angularjs.org/'; 1295 }]); 1296 </file> 1297 <file name="protractor.js" type="protractor"> 1298 it('should linkify the snippet with urls', function() { 1299 expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 1300 toBe('Pretty text with some links: http://angularjs.org/, [email protected], ' + 1301 '[email protected], and one more: ftp://127.0.0.1/.'); 1302 expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); 1303 }); 1304 1305 it('should not linkify snippet without the linky filter', function() { 1306 expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). 1307 toBe('Pretty text with some links: http://angularjs.org/, mailto:[email protected], ' + 1308 '[email protected], and one more: ftp://127.0.0.1/.'); 1309 expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); 1310 }); 1311 1312 it('should update', function() { 1313 element(by.model('snippet')).clear(); 1314 element(by.model('snippet')).sendKeys('new http://link.'); 1315 expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). 1316 toBe('new http://link.'); 1317 expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); 1318 expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) 1319 .toBe('new http://link.'); 1320 }); 1321 1322 it('should work with the target property', function() { 1323 expect(element(by.id('linky-target')). 1324 element(by.binding("snippetWithSingleURL | linky:'_blank'")).getText()). 1325 toBe('http://angularjs.org/'); 1326 expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); 1327 }); 1328 1329 it('should optionally add custom attributes', function() { 1330 expect(element(by.id('linky-custom-attributes')). 1331 element(by.binding("snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}")).getText()). 1332 toBe('http://angularjs.org/'); 1333 expect(element(by.css('#linky-custom-attributes a')).getAttribute('rel')).toEqual('nofollow'); 1334 }); 1335 </file> 1336 </example> 1337 */ 1338angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { 1339 var LINKY_URL_REGEXP = 1340 /((s?ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"\u201d\u2019]/i, 1341 MAILTO_REGEXP = /^mailto:/i; 1342 1343 var linkyMinErr = angular.$$minErr('linky'); 1344 var isDefined = angular.isDefined; 1345 var isFunction = angular.isFunction; 1346 var isObject = angular.isObject; 1347 var isString = angular.isString; 1348 1349 return function(text, target, attributes) { 1350 if (text == null || text === '') return text; 1351 if (!isString(text)) throw linkyMinErr('notstring', 'Expected string but received: {0}', text); 1352 1353 var attributesFn = 1354 isFunction(attributes) ? attributes : 1355 isObject(attributes) ? function getAttributesObject() {return attributes;} : 1356 function getEmptyAttributesObject() {return {};}; 1357 1358 var match; 1359 var raw = text; 1360 var html = []; 1361 var url; 1362 var i; 1363 while ((match = raw.match(LINKY_URL_REGEXP))) { 1364 // We can not end in these as they are sometimes found at the end of the sentence 1365 url = match[0]; 1366 // if we did not match ftp/http/www/mailto then assume mailto 1367 if (!match[2] && !match[4]) { 1368 url = (match[3] ? 'http://' : 'mailto:') + url; 1369 } 1370 i = match.index; 1371 addText(raw.substr(0, i)); 1372 addLink(url, match[0].replace(MAILTO_REGEXP, '')); 1373 raw = raw.substring(i + match[0].length); 1374 } 1375 addText(raw); 1376 return $sanitize(html.join('')); 1377 1378 function addText(text) { 1379 if (!text) { 1380 return; 1381 } 1382 html.push(sanitizeText(text)); 1383 } 1384 1385 function addLink(url, text) { 1386 var key, linkAttributes = attributesFn(url); 1387 html.push('<a '); 1388 1389 for (key in linkAttributes) { 1390 html.push(key + '="' + linkAttributes[key] + '" '); 1391 } 1392 1393 if (isDefined(target) && !('target' in linkAttributes)) { 1394 html.push('target="', 1395 target, 1396 '" '); 1397 } 1398 html.push('href="', 1399 url.replace(/"/g, '"'), 1400 '">'); 1401 addText(text); 1402 html.push('</a>'); 1403 } 1404 }; 1405}]); 1406 1407 1408})(window, window.angular);
1409 1410/* 1411 Webnames Error 1412 ----------------------------- 1413 Module is meant to handle server and client side errors. 1414 1415Scenarios: 14161.) Unexpected error 1417 StatusCode = 500 1418 1419 Symptoms: 1420 - threw out of controller action method 1421 1422 What should happen: 1423 - gets logged as special critical error to investigate 1424 - if Angular error handler is present, let that handle the user error 1425 - if it's a production environment, allow them to log an error report 1426 - if it's local host, display stack trace etc 1427 1428 Examples: 1429 - developer forgets to handle an exception that can be thrown, fixable error occurs (null reference) 1430 - the user should be able to report that an error occurred and the error should be investigated 1431 - these prohibit the user from continuing their intended action on the site 1432 1433 Solution: 1434 - the user should log an error 1435 1436 - inline form or link to oops page 1437 1438 14392.) Expected error 1440 StatusCode = 422 1441 1442 Symptoms: 1443 - it was caught in the controller, the StatusCode was manually set to 422 1444 1445 What should happen: 1446 - should be handled in Angular controller, displaying a special message to the user (should/could use the WnErrorHandler) 1447 - handle required view state on the promise rejection 1448 1449 Examples: 1450 - server side validation of a domain name fails, return an error object and error message to display to the user 1451 1452 Author: Akos Sebestyen 1453*/ 1454(function () { 1455 "use strict"; 1456 1457 angular.module('WebnamesError', ['ngSanitize']) 1458 1459 .config(['$httpProvider', function ($httpProvider) { 1460 $httpProvider.interceptors.push('WnHttpInterceptor'); 1461 }]) 1462 1463 .factory('WnHttpInterceptor', ['$log', '$q', '$sce', 'WnErrorHandler', function ($log, $q, $sce, WnErrorHandler) { 1464 return { 1465 'request': function (config) { 1466 // Add a custom header so our BaseController.cs can recognize that the WebnamesError module is present. 1467 return config; 1468 }, 1469 1470 'responseError': function (response) { 1471 // Convert the array buffer data into an object 1472 if (response.data instanceof ArrayBuffer) { 1473 response.data = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(response.data))); 1474 } 1475 1476 // Push the error into the error handler directive 1477 if (response.status === 500 && !!response.data.wnAngularError) { 1478 WnErrorHandler.PushError(!!response.data.debugMode ? $sce.trustAsHtml(response.data.wnAngularError) : response.data.wnAngularError, true); 1479 } 1480 1481 return $q.reject(response); 1482 } 1483 } 1484 }]); 1485 1486})(); 1487 1488/* 1489 * Webnames Error 1490 * ---------------- 1491 * Directives for displaying errors to the user from Angular. 1492 * 1493 * Author: Akos Sebestyen 1494 */ 1495(function () { 1496 "use strict"; 1497 1498 angular.module('WebnamesError') 1499 1500 // you can specify the 'timeout-length' property to determine how long the notification should stay on screen 1501 .directive('wnNotifications', wnNotifications) 1502 1503 // errors for the user to see 1504 .directive('wnErrors', wnErrors) 1505 1506 .directive('helpdeskError', helpdeskError); 1507 1508 function wnNotifications() { 1509 return { 1510 restrict: 'EA', 1511 replace: true, 1512 template: '<div class="alert {{ notification.sCSSClass || \'alert-info\' }} alert-dismissible" data-ng-repeat="notification in wnn.notifications track by notification.id" role="alert">' + 1513 '<button class="close" data-dismiss="alert" aria-label="Close"><i class="fas fa-times"></i></button>' + 1514 '<p data-ng-bind-html="notification.message"></p>' + 1515 '</div>', 1516 controllerAs: 'wnn', 1517 link: function (scope, elem, attrs) { 1518 scope.$emit('wnNotifications loaded'); 1519 if (!!attrs.timeoutLength) { 1520 scope.timeoutLength = attrs.timeoutLength; 1521 } 1522 }, 1523 controller: ['$scope', '$timeout', 'WnErrorHandler', function ($scope, $timeout, WnErrorHandler) { 1524 var vm = this; 1525 1526 vm.notifications = WnErrorHandler.Notifications; 1527 1528 $scope.$on('WnNotificationPushed', function (event, id) { 1529 if (!!$scope.timeoutLength && $scope.timeoutLength > 0) { 1530 $timeout(function () { 1531 WnErrorHandler.RemoveById(id); 1532 }, $scope.timeoutLength * 1000); 1533 } 1534 }); 1535 1536 vm.clear = function (id) { 1537 WnErrorHandler.RemoveById(id); 1538 } 1539 }] 1540 } 1541 } 1542 1543 function wnErrors() { 1544 return { 1545 restrict: 'EA', 1546 replace: true, 1547 template: '<div id="webnames-error-directive" class="alert alert-danger alert-dismissible" data-ng-repeat="error in wne.errors track by error.id" role="alert">
1547' + 1548 '<button class="close" data-dismiss="alert" aria-label="Close" data-ng-if="!error.isCritical" data-ng-click="wne.clear(error.id)"><i class="fas fa-times"></i></button>' + 1549 '<p data-ng-bind-html="error.message"></p>' + 1550 '<p data-ng-if="error.isCritical">Timestamp: {{error.dtTimestamp | date: "yyyy-MM-dd H:mm:ss.sss"}}</p>' + 1551 '</div>', 1552 link: function (scope) { 1553 scope.$emit('wnErrors loaded'); 1554 }, 1555 controllerAs: 'wne', 1556 controller: ['$scope', '$timeout', 'WnErrorHandler', function ($scope, $timeout, WnErrorHandler) { 1557 var vm = this; 1558 1559 vm.errors = WnErrorHandler.Errors; 1560 1561 vm.clear = function (id) { 1562 WnErrorHandler.RemoveById(id); 1563 } 1564 }] 1565 } 1566 } 1567 1568 function helpdeskError() { 1569 return { 1570 restrict: 'EA', 1571 template: '<div class="red-tip">' + 1572 '<h1>Cart Errors</h1>' + 1573 '<h3>Message</h3>' + 1574 '<p>{{message}}</p>' + 1575 '<h3>StackTrace</h3>' + 1576 '<p>{{stackTrace}}</p>' + 1577 '<h3>Suggested Solution</h3>' + 1578 '<p>{{suggestedSolution}}</p>' + 1579 '<h3>Timestamp</h3>' + 1580 '<p>{{dtCreated}}</p>' + 1581 '</div>', 1582 scope: { 1583 error: '=' 1584 }, 1585 controller: [ 1586 '$scope', function ($scope) { 1587 // When the error object changes, make sure it has all attributes and then display the error 1588 $scope.$watch('error', function () { 1589 if ($scope.error && 1590 $scope.error.message && 1591 $scope.error.stackTrace && 1592 $scope.error.suggestedSolution && 1593 $scope.error.dtCreated) { 1594 $scope.message = $scope.error.message; 1595 $scope.stackTrace = $scope.error.stackTrace; 1596 $scope.suggestedSolution = $scope.error.suggestedSolution; 1597 $scope.dtCreated = $scope.error.dtCreated; 1598 } 1599 }); 1600 } 1601 ] 1602 } 1603 }; 1604})(); 1605 1606/* 1607 WebnamesError - WnErrorHandler 1608 ------------------ 1609 Service used to push messages to the directive which will display them to the user. 1610 1611 Author: Akos Sebestyen 1612 */ 1613(function () { 1614 "use strict"; 1615 1616 angular.module('WebnamesError') 1617 .service('WnErrorHandler', ['$rootScope', '$timeout', '$window', '$location', '$anchorScroll', 'WnMessage', '$log', function ($rootScope, $timeout, $window, $location, $anchorScroll, WnMessage, $log) { 1618 var service = this; 1619 var messageCount = 0; 1620 1621 var wnNotificationsPresent = false; 1622 var wnErrorsPresent = false; 1623 1624 service.scrollToError = true; 1625 service.Notifications = []; 1626 service.Errors = []; 1627 1628 service.ClearAll = function () { 1629 // Must clear array using length so that references aren't lost 1630 service.Notifications.length = 0; 1631 service.Errors.length = 0; 1632 } 1633 1634 // Use this to push notifications to the user 1635 service.PushNotification = function (errorString, sCSSClass) { 1636 var id = messageCount; 1637 service.Notifications.push(new WnMessage(id, errorString, false, sCSSClass)); 1638 $rootScope.$broadcast('WnNotificationPushed', id); 1639 messageCount++; 1640 1641 return service.Notifications[service.Notifications.length - 1]; 1642 } 1643 1644 // Use this to push errors to the user 1645 service.PushError = function (errorString, isCritical) { 1646 var id = messageCount; 1647 service.Errors.push(new WnMessage(id, errorString, !!isCritical)); 1648 $rootScope.$broadcast('WnErrorPushed', id); 1649 messageCount++; 1650 1651 if (service.scrollToError) { 1652 $location.hash("webnames-error-directive"); 1653 $anchorScroll(); 1654 } 1655 1656 return service.Errors[service.Errors.length - 1]; 1657 } 1658 1659 service.GetById = function (id) { 1660 return _.findWhere(service.Notifications, { id: id }); 1661 } 1662 1663 service.RemoveById = function (id) { 1664 var index = _.findIndex(service.Notifications, { id: id }); 1665 1666 if (index < 0) { 1667 index = _.findIndex(service.Errors, { id: id }); 1668 if (index > -1 && !service.Errors[index].isCritical) service.Errors.splice(index, 1); 1669 } else { 1670 service.Notifications.splice(index, 1); 1671 } 1672 } 1673 1674 // TODO: implement parameters 1675 /** 1676 * Redirects client to the robot page. 1677 * @returns {} 1678 */ 1679 service.KillNonRecoverable = function () { 1680 $window.location.href = $window._SmartURL + "error/oops/default.aspx"; 1681 } 1682 1683 // Save events from link function of directives for page validation 1684 $rootScope.$on('wnNotifications loaded', function () { 1685 wnNotificationsPresent = true; 1686 }); 1687 1688 $rootScope.$on('wnErrors loaded', function () { 1689 wnErrorsPresent = true; 1690 }); 1691 1692 // Used for validation of the Angular page 1693 $timeout(function () { 1694 if (!wnErrorsPresent) { 1695 throw "No 'wnErrors' directive found! You should not use this module without placing this directive on your page first!"; 1696 } 1697 if (!wnNotificationsPresent) { 1698 throw "No 'wnNotifications' directive found! You should not use this module without placing this directive on your page first!"; 1699 } 1700 }); 1701 }]) 1702 1703 .factory('WnMessage', [function () { 1704 function WnMessage(id, message, isCritical, sCSSClass) { 1705 var model = this; 1706 1707 model.id = id; 1708 model.dtTimestamp = new Date(); 1709 model.isCritical = isCritical; 1710 model.sCSSClass = sCSSClass || ""; 1711 1712 model.message = message; 1713 } 1714 1715 return WnMessage; 1716 }]); 1717})(); 1718 1719(function() { 1720 "use strict"; 1721 1722 angular.module("DomainSearch", [ 1723 'ui.bootstrap', 1724 'WebnamesAnalytics', 1725 'WebnamesCommon', 1726 'WebnamesError', 1727 'WebnamesLocationHelper' 1728 ]); 1729})(); 1730/* 1731 Domain Search Domains Data Model 1732 -------------------------------- 1733 This will hold the domains data and can be injected into any 1734 directive or application that needs access to domain search data. 1735 1736 The goal of this model is to not bind into angular, to stay lean and fast. 1737 1738 Acts as a local, in memory storage of your Domains data. 1739 1740 Provides useful functions for moving domains around (although I've found it, 1741 easier to order domains using Angular's filter functions, not directly through 1742 the data model) 1743 1744 Can be used to flag domains that Need Results. 1745 1746 Remark: does not allow duplicate domain names 1747 1748 Dependencies 1749 ~ Underscore : Using UnderscoreJS for any array/object work. 1750 1751 Author: Akos Sebestyen 2015 1752*/ 1753(function () { 1754 "use strict"; 1755 1756 angular.module('DomainSearch') 1757 .config([ 1758 '$logProvider', function($logProvider) { 1759 $logProvider.debugEnabled(true); 1760 } 1761 ]) 1762 1763 // Data Model to persist domain search data across modules 1764 .factory('DomainsDataModel', ['$log', 'Domain', domainsDataModel]); 1765 1766 function domainsDataModel($log, Domain) { 1767 var vm = this; 1768 1769 // Hold our current dataset in domains 1770 // NOTE: the domain data is stored under the "data" property. This is the serializable object from the server 1771 vm.domains = []; 1772 vm.ParseDomainData = parseDomainData; 1773 vm.GetDomainByName = getDomainByName; 1774 vm.MoveDomainToFrontByIndex = moveDomainToFrontByIndex; 1775 vm.FlagDomainsNeedingResultsByDomainName = flagDomainsNeedingResultsByDomainName; 1776 // Takes a list of extensions and removes all domains that don't have an extension contained in the list 1777 vm.FilterDomainsByExtensions = filterDomainsByExtensions; 1778 vm.UpdateBundleDisplay = UpdateBundleDisplay; 1779 vm.i_MAX_GPT_SUGGESTIONS = 5; 1780 vm.Bundle = null; 1781 vm.BundlesProcessed = false; 1782 1783 /********************** BACKERS ******************/ 1784 1785 // Parse incoming data: either update existing data or add it 1786 function parseDomainData(domainsData, suggestionPanelIndex) { 1787 1788 // In case caller passes single domain object rather than whole array of results: 1789 if (_.isObject(domainsData) && !_.isArray(domainsData)) { 1790 return upsertDomain(domainsData, suggestionPanelIndex); 1791 } 1792 1793 _.each(domainsData, function (domain) { 1794 upsertDomain(domain, suggestionPanelIndex); 1795 }); 1796 1797 // For Verisign Name Studio suggestions, ensure only available domains are inserted. 1798 var tempDomains = vm.domains; 1799 _.each(tempDomains, function (domain) { 1800 if (!!domain && !!domain.isNameSuggestion) { 1801 if (!!domain.data.result && domain.data.result != 'AVAIL') { 1802 var index = _.findIndex(vm.domains, function (tempDomain) { return tempDomain.data.name === domain.data.name }); 1803 if (index > -1) { 1804 vm.domains.splice(index, 1); 1805 } 1806 } 1807 } 1808 }); 1809 1810 return vm.domains; 1811 } 1812 1813 function upsertDomain(domain, suggestionPanelIndex) { 1814 1815 // Add or merge? 1816 var index = _.findIndex(vm.domains, function (tempDomain) { return tempDomain.data.name === domain.name }); 1817 1818 if (index > -1) { 1819 1820 if (!vm.domains[index].data.result) { 1821 // Preserve suggestion explanation, as it doesn't come back in refresh calls. 1822 var sExplanation = vm.domains[index].data.Explanation; 1823 1824 // Merge (replace data with refresh result data.) 1825 vm.domains[index].data = domain; 1826 1827 vm.domains[index].data.Explanation = sExplanation; 1828 } 1829 1830 if (!!domain.popularitySortOrder) 1831 vm.domains[index].SortOrder = domain.popularitySortOrder + 50; 1832 1833 } else if (domain.SubSourceCode == 'GPTSUGGESTIONS') { 1834 1835 // ChatGPT suggestion: add in designated spot (e.g. slots 4 through 8.) 1836 if (vm.domains.length < suggestionPanelIndex) { 1837 1838 // Too few domains to add suggestion in normal slot: just add to end of list. 1839 index = vm.domains.length; 1840 vm.domains.push(new Domain(domain)); 1841 1842 } else { 1843 1844 // Determine the number of GPT suggestions added, if any, and insert after that. 1845 var iSuggestionsAdded = 0; 1846 1847 for (var i = 0;
1847 i < vm.domains.length && i < suggestionPanelIndex + vm.i_MAX_GPT_SUGGESTIONS; i++) { 1848 if (vm.domains[i].SubSourceCode == 'GPTSUGGESTIONS') { 1849 ++iSuggestionsAdded; 1850 } 1851 } 1852 1853 if (iSuggestionsAdded < vm.i_MAX_GPT_SUGGESTIONS) { 1854 index = suggestionPanelIndex + iSuggestionsAdded; 1855 vm.domains.splice(index, 0, new Domain(domain)); 1856 } 1857 else { 1858 return null; // Reached limit of suggestions to add. 1859 } 1860 } 1861 } else { 1862 // Normal result: add to end of list. 1863 index = vm.domains.push(new Domain(domain)) - 1; 1864 } 1865 1866 if (!!vm.domains[index].data.result) { 1867 vm.domains[index].NeedsResult = false; 1868 } 1869 1870 return vm.domains[index]; 1871 } 1872 1873 function getDomainByName(domainName) { 1874 return vm.domains[_.findIndex(vm.domains, function (domain) { return domain.data.name === domainName })]; 1875 } 1876 1877 function moveDomainToFrontByIndex(index) { 1878 if (!_.isNumber(index)) { 1879 throw "Must be looked moved by index"; 1880 } 1881 if (index < 0 || (index > vm.domains.length - 1)) { 1882 throw "Index is out of range"; 1883 } 1884 1885 vm.domains.unshift(vm.domains[index]); 1886 vm.domains.splice(index + 1, 1); 1887 } 1888 1889 function flagDomainsNeedingResultsByDomainName(domainNames) { 1890 // Clean up stragglers from a possible search earlier 1891 _.each(vm.domains, function(domain) { 1892 domain.NeedsResult = false; 1893 }); 1894 1895 // Loop through every domain name passed in as needing a result, and update the corresponding domain object in the overall results array to flag it. 1896 _.each(domainNames, function (domainName) { 1897 var domain = _.find(vm.domains, function (tempDomain) { return !tempDomain.data.result && (tempDomain.data.stem + tempDomain.data.ext === domainName) }); 1898 if (!!domain) { 1899 domain.NeedsResult = true; 1900 } 1901 }); 1902 } 1903 1904 function filterDomainsByExtensions(extensions) { 1905 1906 var extensionsToPrune = _.difference(_.map(vm.domains, function(tempDomain) { return tempDomain.data.ext }), extensions); 1907 1908 _.each(extensionsToPrune, function(extension) { 1909 var index = _.findIndex(vm.domains, function (tempDomain) { return tempDomain.data.ext === extension }); 1910 1911 if (index > -1) { 1912 vm.domains.splice(index, 1); 1913 } 1914 }); 1915 } 1916 1917 // Determine if we have a bundle promo to upsell the customer on multiple available domains with different TLDs: 1918 function UpdateBundleDisplay(iSpecificDomainIndex) { 1919 1920 if (vm.lDomainBundles && !vm.BundlesProcessed && vm.domains.length > 0) { 1921 1922 const aAvailableDomains = vm.domains.filter(d => d.data.result == "AVAIL"); 1923 1924 for (const oBundle of vm.lDomainBundles) { 1925 if (aAvailableDomains.length >= oBundle.MinimumCount) { 1926 1927 // Sort possible bundle TLDs by specific TLD first, followed by TLDs already added to cart, followed by default sort order: 1928 const aFavoredExts = new Set(aAvailableDomains.filter(d => d === vm.domains[iSpecificDomainIndex] || d?.data?.inCart).map(d => d.data.ext)); 1929 var sSpecificTLD = vm.domains[iSpecificDomainIndex]?.data?.ext; 1930 1931 // Use subtraction trick to return sort of -1, 0, or 1: has() returns a boolean, and true - false === 1, false - true === -1, false - false === 0. 1932 var aTLDs = oBundle.TLDs.toSorted((t1, t2) => aFavoredExts.has(t2) - aFavoredExts.has(t1)).toSorted((t1, t2) => (t2 === sSpecificTLD) - (t1 === sSpecificTLD)); 1933 1934 if (oBundle.SuppressForSpecificTLDResult && aFavoredExts.has(sSpecificTLD)) {
1935 aTLDs.splice(0, 1); // Remove specific TLD as we are in the B group of the A/B test, where we don't include the specific TLD in the bundle offer. 1936 } 1937 1938 var oSuggestedBundle = { 1939 Domains: [], 1940 TotalPrice: null, 1941 TotalRegularPrice: null, 1942 Added: false, 1943 HasSpecificTLD: false 1944 }; 1945 1946 for (const sTLD of aTLDs) { 1947 1948 const oDomain = aAvailableDomains.find(d => d.data.ext == sTLD); 1949 1950 if (oDomain) { 1951 1952 oSuggestedBundle.Domains.push(oDomain); 1953 1954 var dDomainPriceInBundle = oBundle.TLDPrices[sTLD]; 1955 1956 if (oDomain.data.displayedPrice > dDomainPriceInBundle) { 1957 oDomain.PromoCode = oBundle.PromoCode; 1958 } else { 1959 // Existing TLD promotion is cheaper than bundle promo for this TLD: include domain with default non-bundle promo. 1960 oDomain.PromoCode = null; 1961 dDomainPriceInBundle = oDomain.data.displayedPrice; 1962 } 1963 1964 oSuggestedBundle.TotalPrice = (oSuggestedBundle.TotalPrice || 0) + dDomainPriceInBundle; 1965 oSuggestedBundle.TotalRegularPrice = (oSuggestedBundle.TotalRegularPrice || 0) + oDomain.data.displayedPrice; 1966 1967 // Ensure the bundle offers some savings over regular price and meets the minimum number of domains. 1968 if (oSuggestedBundle.Domains.length >= oBundle.DefaultCount && oSuggestedBundle.TotalPrice < oSuggestedBundle.TotalRegularPrice) { 1969 1970 oSuggestedBundle.DomainBundle = oBundle; 1971 1972 console.debug("SuggestedBundle", oSuggestedBundle); 1973 1974 vm.Bundle = oSuggestedBundle; 1975 1976 var oTopTLDs = aTLDs.slice(0, oSuggestedBundle.Domains.length).reduce((oValues, sTLD) => { oValues[sTLD] = true; return oValues }, {}); 1977 1978 oSuggestedBundle.HasSpecificTLD = iSpecificDomainIndex >= 0 && (vm.domains[iSpecificDomainIndex].data.ext in oTopTLDs || oBundle.SuppressForSpecificTLDResult); 1979 1980 // If the current bundle matches the top favored TLDs, this bundle is as good as we'll get, 1981 // and we set the BundlesProcessed flag to stop further bundle checks for this search. 1982 // Otherwise, we allow the bundle to be overridden if further results are returned. 1983 if (oSuggestedBundle.Domains.map((d) => d.data.ext).every((sTLD) => sTLD in oTopTLDs)) { 1984 vm.BundlesProcessed = true; 1985 } 1986 1987 break; 1988 } 1989 } 1990 } 1991 1992 // If we have results (not ERROR/UNKNOWN/null) for domains matching all aTLDs, then stop checking for bundle applicability: 1993 if (aTLDs.every(sTLD => vm.domains.find(d => d.data.ext == sTLD && d.data.result && !(d.data.result == "ERROR" || d.data.result == "ERR" || d.data.result == "UNKNW")))) { 1994 vm.BundlesProcessed = true; 1995 } 1996 } 1997 } 1998 } 1999 } 2000 2001 return vm; 2002 } 2003 2004})(); 2005/* 2006 Domain Search Domain Class 2007 -------------------------------- 2008 Represents a Domain object for a domain search. Works in harmony 2009 with DomainSearchManager and DomainsDataModel to model our 2010 domain search data. 2011 2012 It also provides functionality to add and remove purchases 2013 and preregistrations from the cart (determined by the implementing 2014 module). 2015 2016 The object also stores the entire serialized Angular.Domain object 2017 from .NET, under its 'data' property. Can be swapped out at any time 2018 while maintaining other client side properties assigned. 2019 2020 Dependencies 2021 ~ Underscore : Using UnderscoreJS for any array/object work. 2022 2023 Author: Akos Sebestyen 2015 2024*/ 2025(function () { 2026 "use strict"; 2027 2028 angular.module('DomainSearch') 2029 2030 // Domain data model. Provides AddToCart() and RemoveFromCart() 2031 .factory('Domain', ['$log', '$q', '$window', 'GAService', 'DomainSearchServices', 'CartCounter', 'WnErrorHandler','GA4Service', domain]); 2032 2033 function domain($log, $q, $window, GAService, DomainSearchService, CartCounter, WnErrorHandler, GA4Service) { 2034 function Domain(data) { 2035 var self = this; 2036 2037 // serialized .NET Angular.Domain data 2038 self.data = data; 2039 self.IsAlternativeSuggestion = !!data.isAlternativeSuggestion; 2040 self.isNameSuggestion = !!data.isNameSuggestion; 2041 self.IsExactSuggestion = !!data.isExactSuggestion; 2042 self.SubSourceCode = data.SubSourceCode; 2043 self.TokenList = data.tokenList; 2044 2045 if (self.isNameSuggestion || self.IsExactSuggestion) { 2046 self.SubSourceCode = self.isNameSuggestion ? self.SubSourceCode : "EXACTSUGGESTIONS"; 2047 } 2048
2049 if ($window.location.href.indexOf("/website-builder") >= 0) { 2050 self.SubSourceCode = "SITEBUILDER_LP"; 2051 } else if ($window.location.href.indexOf("/wordpress-hosting") >= 0) { 2052 self.SubSourceCode = "WORDPRESSHOST_LP"; 2053 } 2054 2055 if (!!data.popularitySortOrder) { 2056 self.SortOrder = data.popularitySortOrder + 50; 2057 } 2058 2059 // Adds domain to cart. If not param specified it will add a NEW registration with default term 2060 // Otherwise, if this domain is subject to special conditions 2061 // (such as premium, special availability, multiple phases selectable, or preorder) 2062 //Params: 2063 // string: actionSource, will act as BillPackage Source 2064 // obj: analytics, must pass this in for analytics purposes 2065 // bool: explicitPremium, must pass this in to acknowledge premium domain 2066 // string: explicitPhase, must pass this in the acknowledge multiple registration phases available, or special registration 2067 //Returns: 2068 // promise from the service 2069 self.AddToCart = addToCart; 2070 self.CanAddToCart = canAddToCart; 2071 self.AddToPreReg = addToPreReg; 2072 self.RemoveFromCart = removeFromCart; 2073 self.addUpsellHostingToCart = addUpsellHostingToCart; 2074 self.removeUpsellHostingFromCart = removeUpsellHostingFromCart; 2075 self.addUpsellWixToCart = addUpsellWixToCart; 2076 self.removeUpsellWixFromCart = removeUpsellWixFromCart; 2077 self.RemoveFromPreReg = removeFromPreReg; 2078 self.NeedsResult = false; 2079 2080 self.Links = { 2081 whoisUrl: $window._SmartURL + 'whois?domain=' + self.data.name, 2082 brokeringUrl: $window._SmartURL + 'domain/domain-brokering?dom=' + self.data.name, 2083 invalidPremiumUrl: "mailto:[email protected]?Subject=Price%20Check%20Request%20for%20" + self.data.name, 2084 reservedMailto: "mailto:[email protected]?Subject=Inquiry%20for%20Reserved%20" + self.data.name, 2085 contactSupportMailto: "mailto:[email protected]?Subject=Inquiry%20for%20" + self.data.name, 2086 managedServicesMailto: "mailto:[email protected]?Subject=Inquiry%20for%20" + self.data.name, 2087 domainMgmtUrl: !!self.data.domainId ? $window._SmartURL + "myaccount/domain_management/general.aspx?domID=" + self.data.domainId : undefined, 2088 preorderMgmtUrl: $window._SmartURL + "myaccount/pre-order/Default.aspx", 2089 preregMgmtUrl: $window._SmartURL + "myaccount/pre-registration/Default.aspx" 2090 } 2091 2092 /****** BACKERS *****/ 2093 function addToCart(actionSource, analytics, explicitRegistrationType, iRegistryTLDPremiumRatingID) { 2094 addToCart_validateBeforeAddToCart(actionSource, analytics, explicitRegistrationType, iRegistryTLDPremiumRatingID); 2095 2096 // re-populate the token list value (which may have been wiped out by a status refresh) 2097 if (self.isNameSuggestion || self.IsExactSuggestion) { 2098 self.data.tokenList = self.TokenList; 2099 } 2100 2101 var sBillType = addToCart_getBillTypeFromPhase(explicitRegistrationType); 2102 2103 // Gets explicit purchase action based on the result of the domain 2104 var purchaseAction = addToCart_getPurchaseAction(); 2105 2106 var deferred = $q.defer(); 2107 if (self.data.domainMgmtState === "REG") { 2108 addToCart_handlePromise(DomainSearchService.AddRenewalToCart(self.data.name, self.data.domainId, actionSource), deferred, analytics); 2109 } else { 2110 addToCart_handlePromise(DomainSearchService.AddDomainToCart(self.data.name, 2111 iRegistryTLDPremiumRatingID, 2112 sBillType, 2113 !!explicitRegistrationType ? explicitRegistrationType.Code : "GENERAL", 2114 actionSource, 2115 self.SubSourceCode, 2116 purchaseAction, 2117 null, 2118 self.data), deferred, analytics); 2119 } 2120 2121 return deferred.promise; 2122 } 2123 2124 function canAddToCart() { 2125 return self.data.result === "AVAIL" || 2126 self.data.result === "AVAIL_P" || 2127 self.data.result === "AVAIL_S" || 2128 self.data.result === "PREBOOK" || 2129 self.data.result === "DPML_BLOCK" || 2130 self.data.result === "AVAIL_WITHHELD_CON" || 2131 self.data.result === "BACKORDER" || 2132 self.data.result === "FORSALE" || 2133 self.data.result === "FORSALEOFFER" || 2134 self.data.domainMgmtState === "REG"; 2135 } 2136 2137 function addToPreReg() { 2138 if (!self.data.result) { 2139 throw "Can't add a domain without a result"; 2140 }
2141 if (self.data.result !== "PRE_REGISTER") { 2142 throw "Can't add a non pre-reg domain to pre-reg"; 2143 } 2144 2145 var deferred = $q.defer(); 2146 self.isAdding = true; 2147 2148 DomainSearchService.AddDomainToPreReg(self.data.name).then(function onSuccess(response) { 2149 self.isAdding = false; 2150 self.data.inCart = true; 2151 2152 CartCounter.PreRegCount++; 2153 2154 deferred.resolve(response.data); 2155 }, function onError(response) { 2156 self.isAdding = false; 2157 deferred.reject(response.data); 2158 }); 2159 return deferred.promise; 2160 } 2161 2162 function removeFromCart(analytics) { 2163 var deferred = $q.defer(); 2164 DomainSearchService.RemoveDomainFromCart(self.data.name).then(function (respData) { 2165 if (respData.success) { 2166 deferred.resolve(respData); 2167 2168 //Handle analytics 2169 GAService.RemoveProductAndSend(analytics.ga.categoryName, analytics.ga.actionName, respData.analytics.ga);
2170 GA4Service.sendRemoveFromCart(respData.analytics.ga4); 2171 CartCounter.UpdateCartCount(respData.billCount); 2172 } else { 2173 deferred.reject(respData); 2174 } 2175 }, 2176 function (respData) { 2177 deferred.reject(respData); 2178 }); 2179 2180 return deferred.promise; 2181 } 2182 2183 function addUpsellHostingToCart() { 2184 var deferred = $q.defer(); 2185 DomainSearchService.addUpsellHostingToCart(self.data.name).then(function (respData) { 2186 if (respData.success) { 2187 deferred.resolve(respData); 2188 } else { 2189 deferred.reject(respData); 2190 } 2191 }, 2192 function (respData) { 2193 deferred.reject(respData); 2194 }); 2195 2196 return deferred.promise; 2197 } 2198 2199 function removeUpsellHostingFromCart() { 2200 var deferred = $q.defer(); 2201 DomainSearchService.removeUpsellHostingFromCart(self.data.name).then(function (respData) { 2202 if (respData.success) { 2203 deferred.resolve(respData); 2204 } else { 2205 deferred.reject(respData); 2206 } 2207 }, 2208 function (respData) { 2209 deferred.reject(respData); 2210 }); 2211 2212 return deferred.promise; 2213 } 2214 2215 function addUpsellWixToCart() { 2216 var deferred = $q.defer(); 2217 DomainSearchService.addUpsellWixToCart(self.data.name).then(function (respData) { 2218 if (respData.success) { 2219 deferred.resolve(respData); 2220 } else { 2221 deferred.reject(respData); 2222 } 2223 }, 2224 function (respData) { 2225 deferred.reject(respData); 2226 }); 2227 2228 return deferred.promise; 2229 } 2230 2231 function removeUpsellWixFromCart() { 2232 var deferred = $q.defer(); 2233 DomainSearchService.removeUpsellWixFromCart(self.data.name).then(function (respData) { 2234 if (respData.success) { 2235 deferred.resolve(respData); 2236 } else { 2237 deferred.reject(respData); 2238 } 2239 }, 2240 function (respData) { 2241 deferred.reject(respData); 2242 }); 2243 2244 return deferred.promise; 2245 } 2246 2247 function removeFromPreReg() { 2248 var deferred = $q.defer(); 2249 self.isRemoving = true; 2250 DomainSearchService.RemoveFromPreRegList(self.data.name).then(function onSuccess(response) { 2251 self.isRemoving = false; 2252 self.data.inCart = false; 2253 deferred.resolve(response.data); 2254 2255 CartCounter.PreRegCount--; 2256 }, function onError(response) { 2257 self.isRemoving = false; 2258 deferred.reject(response.data); 2259 }); 2260 2261 return deferred.promise; 2262 } 2263 2264 function addToCart_validateBeforeAddToCart(actionSource, analytics, explicitRegistrationType, iRegistryTLDPremiumRatingID) { 2265 if (!self.data.result) { 2266 throw "Can not add domain to cart if we do not have a result"; 2267 } 2268 2269 if (self.data.inCart) { 2270 throw "Can not add domain to cart because it's already in cart"; 2271 } 2272 2273 if (!analytics || !_.isObject(analytics)) { 2274 throw "Must include analytics object. Set GAService.GAProperties in your controller!"; 2275 } 2276 2277 if (!analytics.ga) { 2278 throw "Must include analytics for GA at least"; 2279 } 2280 2281 if (!actionSource || typeof (actionSource) !== "string") { 2282 throw "Must specify action source always. Set PackageSourceCode in SelectDomainService!"; 2283 } 2284 2285 if (!!self.data.domainMgmtState && self.data.domainMgmtState === "REG" && !self.data.domainId) {
2286 throw "Need a domainId to add this renewal to the cart."; 2287 } 2288 2289 if (self.data.iRegistryTLDPremiumRatingID && !iRegistryTLDPremiumRatingID) { 2290 throw "Premium domain ackowledgement required! Must pass in 'explicitPremium' as 'true'"; 2291 } 2292 2293 if (!!self.data.lPhaseFamilies && self.data.lPhaseFamilies.length > 1 && typeof (explicitRegistrationType) !== "object") { 2294 throw "Domain has multiple phases! Must specify phase string"; 2295 } 2296 2297 if ((self.data.result === "AVAIL_S" || self.data.result === "PREBOOK") && typeof (explicitRegistrationType) !== "object") { 2298 throw "Domain is " + self.data.result + "! Specific phase string must be passed in for validation."; 2299 } 2300 2301 if (!self.CanAddToCart) { 2302 throw "Cannot add a " + self.data.result + " domain"; 2303 } 2304 } 2305 2306 function addToCart_getBillTypeFromPhase(explicitRegistrationType) { 2307 if (!!explicitRegistrationType) { 2308 return explicitRegistrationType.Type === "Pre" ? "PREBOOKING" : "NORMAL"; 2309 } else if (self.data.result === "BACKORDER") { 2310 return "PREBOOKING"; 2311 } else { 2312 return "NORMAL"; 2313 } 2314 } 2315 2316 function addToCart_getPurchaseAction() { 2317 switch(self.data.result) { 2318 case "DPML_BLOCK": 2319 return "OVERRIDE"; 2320 case "BACKORDER": 2321 return "BACKORDER"; 2322 case "FORSALE": 2323 return "AFTERMARKET"; 2324 case "FORSALEOFFER": 2325 return "AFTERMARKET"; 2326 default: 2327 return null; 2328 } 2329 } 2330 2331 function addToCart_handlePromise(promise, deferred, analytics) { 2332 promise.then(function(respData) { 2333 if (respData.success) { 2334 // Handle Analytics 2335 GAService.AddProductAndSend(analytics.ga.categoryName, analytics.ga.actionName, respData.analytics.ga);
2336 GA4Service.sendAddToCart(respData.analytics.ga4); 2337 2338 CartCounter.UpdateCartCount(respData.billCount); 2339 deferred.resolve(respData); 2340 } else { 2341 deferred.reject(respData); 2342 } 2343 }, 2344 function (respData) { 2345 if (respData) { 2346 if (!!respData.userError) WnErrorHandler.PushError(respData.userError); 2347 if (respData.error === "reserved") self.data.result = "RESV"; 2348 deferred.reject(respData); 2349 } 2350 2351 }); 2352 } 2353 } 2354 2355 return Domain; 2356 } 2357 2358})(); 2359(function() { 2360 "use strict"; 2361 2362 angular.module('DomainSearch') 2363 2364 // Manages a domain search and updates the DomainsDataModel 2365 .service('DomainSearchManager', ['$log', '$q', '$http', '$timeout', '$location', '$window', '$rootScope', 'DomainsDataModel', 'DomainSearchServices', domainSearchManager]); 2366 2367 function domainSearchManager($log, $q, $http, $timeout, $location, $window, $rootScope, DomainsDataModel, DomainSearchServices) { 2368 var service = this; 2369 2370 // Starts a search using a stem and an array of extensions 2371 // Sets up all required variables to complete the search 2372 // Returns: Promise of the original request of the search being kicked off 2373 service.StartSearch = startSearch; 2374 2375 // Similar to StartSearch, but this takes a list of prepared domain names and updates DataModel 2376 service.GetSearchResults = getSearchResults; 2377 2378 service.GetSuggestions = getSuggestions; 2379 2380 service.SearchSingleDomain = searchSingleDomain; 2381 2382 service.CancelSearch = cancelSearch; 2383 2384 service.CheckIfDomainsNeedResults = checkIfDomainsNeedResults; 2385 2386 // Similar to CancelSearch, but resets variables 2387 service.ResetSearch = resetSearch; 2388 service.GetCompleteSearchTerm = getCompleteSearchTerm; 2389 service.DomainStem = null; 2390 service.Extension = null; 2391 service.SpecificDomainIndex = specificDomainIndex; 2392 service.SuggestionsSearched = false; 2393 2394 // The promise that will resolve when the search is fully complete 2395 // Can be rejected if an error occurs 2396 service.SearchingPromise = null; 2397 service.IsAftermarketOnly = false; 2398 service.SetAftermarketOnlyMode = setAftermarketOnlyMode; 2399 service.IsAISearch = false; 2400 2401 service.fnGetDisplayedDomains = null; 2402 2403 /**************** PRIVATES ***************/ 2404 var searchGuid; 2405 var dtTimeout; 2406 var refreshPromise; 2407 var searchingQ; 2408 var defaultRefreshTime = 750; 2409 var defaultClientTimeout = parseInt($location.search().searchTimeout) || 60; 2410 var refreshTime = defaultRefreshTime; 2411 2412 function setAftermarketOnlyMode(bIsAftermarketOnly) { 2413 service.IsAftermarketOnly = bIsAftermarketOnly; 2414 DomainSearchServices.IsAftermarketOnly = bIsAftermarketOnly; 2415 } 2416 2417 function handleSearchResults(deferred, data, domainNamesWithResults) { 2418 searchGuid = data.threadID; 2419 2420 // The search could be cancelled by now! Shut 'er down! 2421 if (!searchingQ) { 2422 //deferred.reject({ error: "Search was cancelled super fast" }); 2423 //postSearchCleanUp("Search was cancelled super fast"); 2424 } else if (searchingQ === deferred) { 2425 // parse our initial result set, and then flag which extensions still need results 2426 DomainsDataModel.ParseDomainData(data.domains); 2427 DomainsDataModel.DomainSuggestions = data.lDomainSuggestions; 2428 2429 // in the case of appending results, these should be marked outside of this 2430 if (!!domainNamesWithResults) { 2431 DomainsDataModel.FlagDomainsNeedingResultsByDomainName(
2431domainNamesWithResults); 2432 } else { 2433 DomainsDataModel.FlagDomainsNeedingResultsByDomainName(_.map(data.domains, 'name')); 2434 } 2435 2436 DomainsDataModel.UpdateBundleDisplay(specificDomainIndex()); 2437 2438 deferred.resolve(data); 2439 2440 if (shouldKeepSearching() ) { // Make sure we even have to keep searching, we could have all the results by now 2441 refreshPromise = $timeout(refreshResults, refreshTime); 2442 } else { 2443 if (!!searchingQ) { 2444 searchingQ.resolve("Search complete without refresh"); 2445 $rootScope.$broadcast('Search complete'); 2446 } 2447 } 2448 } 2449 } 2450 2451 function handlePostSearch() { 2452 // Once the search is FULLY complete, do required promise cleanup 2453 service.SearchingPromise.then(function (reason) { //Resolve 2454 postSearchCleanUp(reason); 2455 }, 2456 function (reason) { //Reject 2457 // Don't call cleanup if the reason is cancellation, it has already called it 2458 if (reason !== "Search cancelled") { 2459 postSearchCleanUp(reason); 2460 } else { 2461 $log.debug("Search cancelled!"); 2462 } 2463 }, 2464 function (reason) { //Notify 2465 $log.debug(reason); 2466 }); 2467 } 2468 2469 // recursively called until we have all the results or we timeout 2470 function refreshResults() { 2471 2472 //get an array of domains where we don't have a result yet and check them again 2473 if (service.fnGetDisplayedDomains) { 2474 _.each(service.fnGetDisplayedDomains(), function (domain) { domain.NeedsResult = !domain.result; }); 2475 } 2476 2477 var lsNeedsResultDomains = _.map(_.filter(DomainsDataModel.domains, function (domain) { return !domain.data.result && domain.NeedsResult }), function (domain) { return domain.data.name }); 2478 2479 lsNeedsResultDomains = _.uniq(lsNeedsResultDomains); 2480 2481 if (lsNeedsResultDomains.length < 1 && !!searchingQ) { 2482 searchingQ.resolve("Search complete"); 2483 $rootScope.$broadcast('Search complete'); 2484 return; 2485 } else if (lsNeedsResultDomains.length < 1) return; 2486 DomainSearchServices.GetUpdatedResults(lsNeedsResultDomains, searchGuid).then(function (data) { 2487 DomainsDataModel.ParseDomainData(data.domains); 2488 2489 DomainsDataModel.UpdateBundleDisplay(specificDomainIndex()); 2490 2491 if (!!searchingQ) { 2492 searchingQ.notify("Search updated"); 2493 } 2494 2495 if (dtTimeout < Date.now()) { 2496 // Timeout happened client side 2497 if (!!searchingQ) { 2498 searchingQ.reject("Timed out"); 2499 } 2500 2501 _.each(DomainsDataModel.domains, function (domain) { 2502 if (!!domain.NeedsResult && !domain.data.result) { 2503 domain.data.result = "ERROR"; 2504 } 2505 }); 2506 } else if (shouldKeepSearching() && searchGuid) { 2507 // slow down our refresh time around the end, we should have most of the results back in the first one or two iterations, 2508 // unless we're gonna time out server side anyway 2509 refreshTime *= (1 + Math.random()); 2510 $log.debug("Bumping refreshTime: " + refreshTime); 2511 // keep track of our current refresh promise 2512 refreshPromise = undefined; 2513 refreshPromise = $timeout(refreshResults, refreshTime); 2514 } else { 2515 if (!!searchingQ) { 2516 searchingQ.resolve("Search complete"); 2517 $rootScope.$broadcast('Search complete'); 2518 } else { 2519 postSearchCleanUp("Backup cleanup"); 2520 } 2521 } 2522 }, 2523 function (data) { 2524 // Unexpected error occurred, reject our promise, let everyone else handle it appropriately 2525 searchingQ.reject(data); 2526 }); 2527 } 2528 2529 function shouldKeepSearching() { 2530 var bShouldKeepSearching = _.any(DomainsDataModel.domains, function (domain) { return !domain.data.result && domain.NeedsResult }); 2531 return bShouldKeepSearching; 2532 } 2533 2534 function postSearchCleanUp(message) { 2535 $log.debug(message); 2536 service.SearchingPromise = null; 2537 searchingQ = null; 2538 refreshPromise = null; 2539 } 2540 2541 /********************** BACKERS ******************/ 2542 function startSearch(searchTerm, suggestedExtension, excludePreReg, aftermarketOptions, OnlyUseTheseTLDs, fnGetDisplayedDomains, bAllowThirdLevelAndCASubdomains, filters, fAIDoneLoadingCallback) { 2543 resetSearch(); 2544 2545 // defer promises 2546 var deferred = $q.defer(); 2547 searchingQ = deferred; 2548 service.SearchingPromise = searchingQ.promise; 2549 2550 service.fnGetDisplayedDomains = fnGetDisplayedDomains; 2551 2552 // set a client timeout (just to be safe) 2553 dtTimeout = new Date(); 2554 dtTimeout.setSeconds(dtTimeout.getSeconds() + defaultClientTimeout); 2555 2556 service.IsAISearch = !!(filters?.useAI); 2557 service.bLastSearchWasAI = service.IsAISearch; 2558 2559 if (service.IsAISearch) { 2560 2561 // OpenAI ChatGPT API is slower (~5s) than EPP domain search (~250m
2561s), so we kick them off in parallel. 2562 2563 // ChatGPT suggestions: 2564 $http.get(`${$window._SmartURL}_/GPTDomainSuggestions?sSearchTerm=${encodeURIComponent(searchTerm)}` + 2565 `&bIncludeOriginal=true` + 2566 `&bIncludeExplanations=${JSON.stringify(filters.bIncludeExplanations)}` + 2567 `&fRandomnessTemperature=${filters.bIncreaseRandomness ? 1.3 : 1.1}`). 2568 then(function onSuccess(response) { 2569 2570 service.IsAISearch = false; 2571 searchGuid = response.data.threadID; 2572 DomainsDataModel.ParseDomainData(response.data.domains, service.SuggestionPanelIndex); 2573 DomainsDataModel.FlagDomainsNeedingResultsByDomainName(response.data.lsDomainsSearched); 2574 2575 fAIDoneLoadingCallback && fAIDoneLoadingCallback(); 2576 2577 if (shouldKeepSearching()) { 2578 refreshPromise = $timeout(refreshResults, refreshTime); 2579 } 2580 2581 deferred.resolve(response.data); 2582 2583 }, function onError(response) { 2584 deferred.reject(response.data); 2585 searchingQ.reject(response.data); 2586 }); 2587 2588 // Regular search: 2589 DomainSearchServices.StartSearch(searchTerm, suggestedExtension, !!excludePreReg, aftermarketOptions, OnlyUseTheseTLDs, bAllowThirdLevelAndCASubdomains, filters, true).then(function (data) { 2590 service.DomainStem = data.sStem; 2591 service.Extension = data.sExtension; 2592 handleSearchResults(deferred, data, data.lsDomainsSearched); 2593 }, 2594 function onError(response) { 2595 deferred.reject(response.data); 2596 searchingQ.reject(response.data); 2597 }); 2598 } else { 2599 // Normal (non-AI) search flow: 2600 DomainSearchServices.StartSearch(searchTerm, suggestedExtension, !!excludePreReg, aftermarketOptions, OnlyUseTheseTLDs, bAllowThirdLevelAndCASubdomains, filters).then(function (data) { 2601 service.DomainStem = data.sStem; 2602 service.Extension = data.sExtension; 2603 handleSearchResults(deferred, data, data.lsDomainsSearched); 2604 }, 2605 function (data) { 2606 deferred.reject(data); //pass it along 2607 searchingQ.reject(data); 2608 }); 2609 } 2610 2611 handlePostSearch(); 2612 return deferred.promise; 2613 } 2614 2615 function getSearchResults(domainNamesWithResults, domainNamesWithoutResults, shouldSort, shouldValidate, bNewSearchSession, aftermarketOptions) { 2616 if (shouldSort === undefined || shouldSort === null) { 2617 shouldSort = true; 2618 }; 2619 2620 if (shouldValidate === undefined || shouldValidate === null) { 2621 shouldValidate = false; 2622 } 2623 2624 // Only meant as a flag to the controller that this is a brand new search session and it should be logged into DomainSearchLog 2625 if (bNewSearchSession === undefined || bNewSearchSession === null) { 2626 bNewSearchSession = false; 2627 } 2628 2629 $log.debug("Appending " + domainNamesWithResults.length + " domain results!"); 2630 refreshTime = defaultRefreshTime; 2631 2632 // defer promises 2633 var deferred = $q.defer(); 2634 searchingQ = deferred; 2635 service.SearchingPromise = searchingQ.promise; 2636 2637 // set a client timeout (just to be safe) 2638 dtTimeout = new Date(); 2639 dtTimeout.setSeconds(dtTimeout.getSeconds() + defaultClientTimeout); 2640 2641 DomainSearchServices.GetSearchResults(domainNamesWithResults, domainNamesWithoutResults, shouldSort, shouldValidate, bNewSearchSession, aftermarketOptions).then(function (data) { 2642 handleSearchResults(deferred, data, domainNamesWithResults); 2643 }, 2644 function (data) { 2645 searchingQ.reject(data); 2646 deferred.reject(data); 2647 }); 2648 2649 handlePostSearch(); 2650 2651 return deferred.promise; 2652 } 2653 2654 function getSuggestions() { 2655 2656 if (!!service.SearchingPromise) return null; 2657 if (!service.DomainStem) return null; 2658 $log.debug("Getting search suggestions"); 2659 refreshTime = defaultRefreshTime; 2660 2661 // defer promises 2662 var deferred = $q.defer(); 2663 searchingQ = deferred; 2664 service.SearchingPromise = searchingQ.promise; 2665 2666 // set a client timeout (just to be safe) 2667 dtTimeout = new Date(); 2668 dtTimeout.setSeconds(dtTimeout.getSeconds() + defaultClientTimeout); 2669 2670 DomainSearchServices.GetSuggestions(service.DomainStem + service.Extension).then(function (data) { 2671 service.SuggestionsSearched = true; 2672 handleSearchResults(deferred, data); 2673 }, function(data) { 2674 searchingQ.reject(data); 2675 deferred.reject(data); 2676 }); 2677 2678 handlePostSearch(); 2679 return deferred.promise; 2680 } 2681 2682 function searchSingleDomain(domainName, isCriticalSearch) { 2683 cancelSearch(); 2684 2685 searchingQ = $q.defer(); 2686 service.SearchingPromise = searchingQ.promise; 2687 2688 DomainSearchServices.GetSingleSearchResult(domainName, !!isCriticalSearch).then(function onSuccess(response) { 2689 searchingQ.resolve(DomainsDataModel.ParseDomainData(response.data)); 2690 }, function onError(response) { 2691 searchingQ.reject(response.data); 2692 }); 2693 2694 handlePostSearch(); 2695 2696 return searchingQ.promise; 2697 } 2698 2699 // Cancel the timeout promise if we're still refreshing results from a previous search 2700 function cancelSearch() { 2701 if (!!refreshPromise) { 2702 $timeout.cancel(refreshPromise); 2703 } 2704 if (!!searchingQ) { 2705 searchingQ.reject("Search cancelled"); 2706 postSearchCleanUp(); 2707 } 2708 return $timeout(function() {}, 300); 2709 } 2710 2711 /** 2712 * Checks against the data model if results need to be updated. Should be called in an $interval loop in implementing controllers. 2713 * See domainSearch.main.controller.js for implementation. 2714 * @param {Array} displayedDomains critical domains currently displayed in the UI (will check these first) 2715 * @param {Array} additionalDomains additional predictive domains (that UI might need next) 2716 * @param {Function} postSearchCallback callback to execute once the entire domain search is finished 2717 * @param {Number} searchBatchLength how many predictive domains to check additionally 2718 * @returns {} 2719 */ 2720 function checkIfDomainsNeedResults(displayedDomains, additionalDomains, postSearchCallback, searchBatchLength) { 2721 if (typeof searchBatchLength === "undefined") searchBatchLength = 50; 2722 // Filtered domains is assigned in the ng-repeater, so we can keep track of what's on the screen 2723 if (!displayedDomains || !!service.SearchingPromise) return; 2724 2725 var domainNamesToSearch = []; 2726 2727 // Check all domains on screen first and populate array 2728 _.each(displayedDomains, function (tempDomain) { 2729 if (!tempDomain.data.result) { 2730 tempDomain.NeedsResult = true; 2731 domainNamesToSearch.push(tempDomain.data.name); 2732 } 2733 }); 2734 2735 // Append some predictive extra domains but only if we're gonna look anyway 2736 if (domainNamesToSearch.length > 0 && domainNamesToSearch.length < searchBatchLength) { 2737 for (var i = 0; domainNamesToSearch.length < searchBatchLength && i < additionalDomains.length; ++i) { 2738 if (!additionalDomains[i].data.result && !_.contains(domainNamesToSearch, additionalDomains[i].data.name)) { 2739 additionalDomains[i].NeedsResult = true; 2740 domainNamesToSearch.push(additionalDomains[i].data.name); 2741 } 2742 } 2743 } 2744 2745 domainNamesToSearch = _.uniq(domainNamesToSearch); 2746 2747 var aftermarketOptions = { 2748 AfterMarketOnly: service.IsAftermarketOnly 2749 } 2750 if (!service.SearchingPromise && domainNamesToSearch.length > 0) { 2751 if (domainNamesToSearch.length > 300) domainNamesToSearch = domainNamesToSearch.slice(0, 300); 2752 service.GetSearchResults(domainNamesToSearch, null, false, false, false, aftermarketOptions)['finally'](function () { 2753 if (!!postSearchCallback) postSearchCallback(); 2754 }); 2755 } 2756 } 2757 2758 function resetSearch() { 2759 // cancel an already in progress search 2760 cancelSearch(); 2761 2762 refreshTime = defaultRefreshTime; 2763 2764 service.SuggestionsSearched = false; 2765 2766 // reset properties 2767 service.DomainStem = null; 2768 service.Extension = null; 2769 service.IsAISearch = false; 2770 service.bLastSearchWasAI = false; 2771 } 2772 2773 function getCompleteSearchTerm() { 2774 if (!!service.DomainStem) { 2775 return service.DomainStem + service.Extension; 2776 } else { 2777 return undefined; 2778 } 2779 } 2780 2781 function specificDomainIndex() { 2782 if (!!service.Extension) { 2783 return _.findIndex(DomainsDataModel.domains, function (domainCandidate) { return domainCandidate.data.name === service.DomainStem + service.Extension }); 2784 } else { 2785 return -1; 2786 } 2787 } 2788 return service; 2789 } 2790})(); 2791/* 2792 Webnames.ca 2015 2793 AngularJS Domain Search App 2794 2795 Service to compliment the domain search app. Handles server side communication. 2796 2797 Author: Akos Sebestyen 2798*/ 2799 2800(function () { 2801 "use strict"; 2802 2803 angular.module('DomainSearch') 2804 // Handles server communication 2805 .service('DomainSearchServices', ['$http', '$q', '$log', '$window', domainSearchServices]); 2806 2807 function domainSearchServices($http, $q, $log, $window) { 2808 var service = this; 2809 2810 var domainSearchControllerUrl = $window._SmartURL + 'domain-registration/search/'; 2811 var cartControllerUrl = $window._SmartURL + 'Cart/'; 2812 2813 service.StartSearch = startSearch; 2814 service.GetUpdatedResults = getUpdatedResults; 2815 service.GetSearchResults = getSearchResults; 2816 service.GetSuggestions = getSuggestions; 2817 service.GetSingleSearchResult = getSingleSearchResult; 2818 service.GetExtensionsByCategories = getExtensionsByCategories; 2819 service.AddDomainToCart = addDomainToCart; 2820 service.AddRenewalToCart = addRenewalToCart; 2821 service.AddDomainToPreReg = addDomainToPreReg; 2822 service.RemoveDomainFromCart = removeDomainFromCart; 2823 service.addUpsellHostingToCart = addUpsellHostingToCart; 2824 service.removeUpsellHostingFromCart = removeUpsellHostingFromCart; 2825 service.addUpsellWixToCart = addUpsellWixToCart; 2826 service.removeUpsellWixFromCart = removeUpsellWixFromCart; 2827 service.RemoveFromPreRegList = removeFromPreRegList; 2828 service.IsAftermarketOnly = false; 2829 service.EnableDisableAISearch = enableDisableAISearch; 2830 2831 // Kick off domain search. Base call for the domain search 2832 function startSearch(searchTerm, suggestedExtension, excludePreReg, aftermarketOptions, OnlyUseTheseTLDs, bAllowThirdLevelAndCASubdomains, filters, bSuppressSuggestions) { 2833 var deferred = $q.defer(); // Use the promise pattern. 2834 2835 var oPostOptions = { 2836 sSearchTerm: searchTerm, 2837 sSuggestedExtension: suggestedExtension, 2838 bExcludePreReg: excludePreReg, 2839 lExtensions: OnlyUseTheseTLDs, 2840 bAllowThirdLevelAndCASubdomains: bAllowThirdLevelAndCASubdomains 2841 }; 2842 2843 if (aftermarketOptions) { 2844 oPostOptions.bAfterMarketOnly = aftermarketOptions.AfterMarketOnly; 2845 oPostOptions.bFastTransfersOnly = aftermarketOptions.FastTransfersOnly; 2846 oPostOptions.dMinPrice = aftermarketOptions.MinPrice; 2847 oPostOptions.dMaxPrice = aftermarketOptions.MaxPrice; 2848 oPostOptions.lExtensions = (oPostOptions.lExtensions || aftermarketOptions.Extensions); 2849 oPostOptions.lCategories = aftermarketOptions.Categories; 2850 oPostOptions.sOwnerGUID = aftermarketOptions.OwnerGUID; 2851 oPostOptions.bSuppressSuggestions = !!bSuppressSuggestions; 2852 } 2853 // Standard search 2854 $http.post(domainSearchControllerUrl + 'GetAppReadyResults', oPostOptions).then(function onSuccess(response) { 2855 deferred.resolve(response.data); // Return our response via promise. 2856 }, function onError(response) { 2857 deferred.reject(response.data); 2858 }); 2859 2860 return deferred.promise; 2861 } 2862 2863 function getUpdatedResults(domainNames, sDomainSearchInstanceID) { 2864 var deferred = $q.defer(); 2865 $http.post(domainSearchControllerUrl + 'GetAppRefreshResults', 2866 { lsDomainNames: domainNames, sDomainSearchInstanceID: sD
2866omainSearchInstanceID, bOwnerSearch: service.IsAftermarketOnly }).then(function onSuccess(response) { 2867 deferred.resolve(response.data); 2868 }, function onError(response) { 2869 deferred.reject(response.data); 2870 }); 2871 return deferred.promise; 2872 } 2873 2874 // Kicks off a new domain search and gets results for it 2875 function getSearchResults(domainNames, domainNamesWithoutResults, shouldSort, shouldValidate, bNewSearchSession, aftermarketOptions) { 2876 var deferred = $q.defer(); 2877 2878 $http.post(domainSearchControllerUrl + 'GetSearchResults', { 2879 lsDomainNames: domainNames, 2880 lsDomainNamesWithoutResults: domainNamesWithoutResults, 2881 bShouldSort: shouldSort, 2882 bShouldValidate: shouldValidate, 2883 bNewSearchSession: bNewSearchSession, 2884 bAfterMarketOnly: (aftermarketOptions? aftermarketOptions.AfterMarketOnly: false), 2885 bFastTransfersOnly: (aftermarketOptions? aftermarketOptions.FastTransfersOnly: false), 2886 dMinPrice: (aftermarketOptions? aftermarketOptions.MinPrice: 0), 2887 dMaxPrice: (aftermarketOptions? aftermarketOptions.MaxPrice: 10000), 2888 lExtensions: (aftermarketOptions? aftermarketOptions.Extensions: null), 2889 lCategories: (aftermarketOptions? aftermarketOptions.Categories: null), 2890 bBulk: (aftermarketOptions ? !!(aftermarketOptions.bulkMode) : false) 2891 }).then(function onSuccess(response) { 2892 deferred.resolve(response.data); 2893 }, function onError(response) { 2894 deferred.reject(response.data); 2895 }); 2896 2897 return deferred.promise; 2898 } 2899 2900 function getSuggestions(searchTerm) { 2901 var deferred = $q.defer(); 2902 2903 $http.post(domainSearchControllerUrl + 'GetSuggestions', { 2904 sSearchTerm: searchTerm 2905 }).then(function onSuccess(response) { 2906 deferred.resolve(response.data); 2907 }, function onError(response) { 2908 deferred.reject(response.data); 2909 }); 2910 2911 return deferred.promise; 2912 } 2913 2914 function getSingleSearchResult(domainName, isCriticalSearch) { 2915 if (isCriticalSearch === undefined) isCriticalSearch = true; 2916 return $http.post(domainSearchControllerUrl + 'GetSingleSearchResult', { sDomainName: domainName, bCriticalSearch: !!isCriticalSearch }); 2917 } 2918 2919 function getExtensionsByCategories(categories) { 2920 return $http.post(domainSearchControllerUrl + 'GetExtensionsByCategories', { categories: categories }); 2921 } 2922 2923 function addDomainToCart(sDomainName, iRegistryTLDPremiumRatingID, sBillType, sPhaseTypeCode, actionSource, subSourceCode, purchaseAction, domainId, oDomainData) { 2924 var defer = $q.defer(); // Use the promise pattern. 2925 2926 $http.post(cartControllerUrl + "AddDomainToCart", 2927 { 2928 sDomainName: sDomainName, 2929 iRegistryTLDPremiumRatingID: iRegistryTLDPremiumRatingID || 0, 2930 sBillType: sBillType, 2931 sPhaseTypeCode: sPhaseTypeCode, 2932 actionSource: actionSource, 2933 sSubSourceCode: subSourceCode, 2934 sPurchaseAction: purchaseAction, 2935 oDomainData: oDomainData 2936 }).then(function onSuccess(response) { 2937 // cart is mvc, no data.d, no JSON parsing needed. 2938 defer.resolve(response.data); // Return our response via promise. 2939 }, function onError(response) { 2940 defer.reject(response.data); 2941 }); 2942 return defer.promise; 2943 } 2944 2945 function addRenewalToCart(domainName, domainId, actionSource) { 2946 var deferred = $q.defer(); 2947 $http.post(cartControllerUrl + "AddRenewalToCart", { 2948 sDomainName: domainName, 2949 iDomainId: domainId, 2950 actionSource: actionSource 2951 }).then(function onSuccess(response) { deferred.resolve(response.data) }, function (response) { deferred.reject(response.data) }); 2952 return deferred.promise; 2953 } 2954 2955 function addDomainToPreReg(sDomainName) { 2956 return $http.post($window._SmartURL + "domain-registration/search/AddToPreRegList", { sDomainName: sDomainName }); 2957 } 2958 2959 function removeDomainFromCart(sDomainName) { 2960 var defer = $q.defer(); // Use the promise pattern. 2961 2962 $http.post(cartControllerUrl + "RemoveDomainFromCart", { sDomainName: sDomainName }).then(function onSuccess(response) { 2963 // cart is mvc, no data.d, no JSON parsing needed. 2964 defer.resolve(response.data); // Return our response via promise. 2965 }, function onError(response) { 2966 defer.reject(response.data); // Return our response via promise. 2967 }); 2968 return defer.promise; 2969 } 2970 2971 function removeFromPreRegList(sDomainName) { 2972 return $http.post(domainSearchControllerUrl + "RemoveFromPreRegList", { sDomainName: sDomainName }); 2973 } 2974 2975 function addUpsellHostingToCart(sDomainName) { 2976 var defer = $q.defer(); 2977 2978 $http.post(cartControllerUrl + "AddUpsellHosting", { sDomainName: sDomainName }).then(function onSuccess(response) { 2979 defer.resolve(response.data); 2980 }, function onError(response) { 2981 defer.reject(response.data); 2982 }); 2983 return defer.promise; 2984 } 2985 2986 function removeUpsellHostingFromCart(sDomainName) { 2987 var defer = $q.defer(); 2988 2989 $http.post(cartControllerUrl + "RemoveUpsellHosting", { sDomainName: sDomainName }).then(function onSuccess(response) { 2990 defer.resolve(response.data); 2991 }, function onError(response) { 2992 defer.reject(response.data); 2993 }); 2994 return defer.promise; 2995 } 2996 2997 function addUpsellWixToCart(sDomainName) { 2998 var defer = $q.defer(); 2999 3000 $http.post(cartControllerUrl + "AddUpsellWix", { sDomainName: sDomainName }).then(function onSuccess(response) { 3001 defer.resolve(response.data); 3002 }, function onError(response) { 3003 defer.reject(response.data); 3004 }); 3005 return defer.promise; 3006 } 3007 3008 function removeUpsellWixFromCart(sDomainName) { 3009 var defer = $q.defer(); 3010 3011 $http.post(cartControllerUrl + "RemoveUpsellWix", { sDomainName: sDomainName }).then(function onSuccess(response) { 3012 defer.resolve(response.data); 3013 }, function onError(response) { 3014 defer.reject(response.data); 3015 }); 3016 return defer.promise; 3017 } 3018 3019 function enableDisableAISearch(bUseAI) { 3020 return $http.post(domainSearchControllerUrl + "UseAISuggestions", { bUseAISuggestions: bUseAI }); 3021 } 3022 } 3023})(); 3024/* 3025 Domain Search Cart Counter 3026 -------------------------------- 3027 Simple data service to maintain the non-Angular 3028 cart counter we have templated on most pages. 3029 3030 Provides functions to update the cart count and to 3031 parse it. 3032 3033 Use CartCounter.ParseCartCount to initialize the service 3034 on a page load. 3035 3036 Author: Akos Sebestyen 2015 3037*/ 3038(function () { 3039 "use strict"; 3040 3041 angular.module('DomainSearch') 3042 3043 // Keeps track of the cart Count and updates #cart-item on the page 3044 .service('CartCounter', ['$rootScope', '$window', '$http', cartCounter]); 3045 3046 function cartCounter($rootScope, $window, $http) { 3047 var model = this; 3048 3049 model.CartCount = 0; 3050 model.PreRegCount = 0; 3051 model.CartUrl = $window._SmartURL + "cart"; 3052
3053 var cartIconElem = $("#cart-icon"); 3054 3055 model.TotalCartCount = function () { 3056 return model.CartCount + model.PreRegCount; 3057 } 3058 3059 model.UpdateCartCount = function (cartCount) { 3060 model.CartCount = cartCount; 3061 cartIconElem.text(model.CartCount.toString()); 3062 } 3063 3064 // Loads the initial cart count into the model from pageload 3065 model.ParseCartCount = function () { 3066 model.CartCount = parseInt(cartIconElem.text()); 3067 } 3068 3069 // Initialize 3070 model.ParseCartCount(); 3071 3072 $http.get($window._SmartURL + "domain-registration/search/GetPreRegCount").then(function onSuccess(response) { 3073 model.PreRegCount = response.data.preRegCount; 3074 }); 3075 3076 // If there are any pre-registrations in the cart, Ninja replace the link of the cart Icon 3077 $rootScope.$watch(function() { 3078 return model.PreRegCount; 3079 }, function (newVal) { 3080 model.CartUrl = newVal > 0 ? $window._SmartURL + "pre-registration/DomainReview.aspx" : $window._SmartURL + "cart"; 3081 cartIconElem.attr('href', model.CartUrl); 3082 }); 3083 3084 } 3085})(); 3086(function () { 3087 "use strict"; 3088 3089 angular.module('DomainSearch') 3090 .service('StatusService', ['$window', function ($window) { 3091 var model = this; 3092 var dpmlURL = $window._SmartURL + "brand-protection-dpml/"; 3093 3094 model.StatusDescriptions = { 3095 AVAIL: { 3096 name: "Available", 3097 nameClass: undefined, 3098 btnText: "Add to Cart", 3099 btnClass: "btn-flat btn-flat-primary", 3100 iconClass: undefined, 3101 specialWrapperClass: "available panel text-base", 3102 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is {{status}} " 3103 }, 3104 AVAIL_S: { 3105 name: "Available", 3106 nameClass: undefined, 3107 btnText: "Add to Cart", 3108 btnClass: "btn-flat btn-flat-primary", 3109 iconClass: undefined, 3110 specialWrapperClass: "available panel text-base", 3111 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is {{status}} " 3112 }, 3113 AVAIL_WITHHELD_CON: { 3114 name: "Available", 3115 nameClass: undefined, 3116 btnText: "Add to Cart", 3117 btnClass: "btn-flat btn-flat-primary", 3118 iconClass: undefined, 3119 specialWrapperClass: "available panel text-base", 3120 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is {{status}} " 3121 }, 3122 AVAIL_WITHHELD: { 3123 name: "Taken", 3124 nameClass: undefined, 3125 btnText: "Let us Help", 3126 btnClass: "btn btn-flat btn-outline link-hover", 3127 iconClass: undefined, 3128 specialWrapperClass: "taken", 3129 specialMessageTemplate: "Sorry, the domain <b>{{domainName}}</b> is {{status}}." 3130 }, 3131 AVAIL_P: { 3132 name: "Available", 3133 nameClass: undefined, 3134 btnText: "Add to Cart", 3135 btnClass: "btn-flat btn-flat-primary", 3136 iconClass: undefined, 3137 specialWrapperClass: "available panel text-base", 3138 specialMessageTemplate: "Available! <br/> The premium domain <b>{{domainName}}</b> is available " 3139 }, 3140 FORSALE: { 3141 name: "For Sale", 3142 nameClass: undefined, 3143 btnText: "Add to Cart", 3144 btnClass: "btn-flat btn-flat-primary", 3145 iconClass: undefined, 3146 specialWrapperClass: "available panel text-base", 3147 specialMessageTemplate: "Congratulations! <br/> The Marketplace domain <b>{{domainName}}</b> is for sale " 3148 }, 3149 FORSALEREQUEST: { 3150 name: "For Sale", 3151 nameClass: undefined, 3152 btnText: "Request Price", 3153 btnClass: "btn-flat btn-flat-primary", 3154 iconClass: undefined, 3155 specialWrapperClass: "available panel text-base", 3156 specialMessageTemplate: "Congratulations! <br/> The Marketplace domain <b>{{domainName}}</b> is for sale." 3157 }, 3158 FORSALEOFFER: { 3159 name: "For Sale", 3160 nameClass: undefined, 3161 btnText: "Make Offer", 3162 btnClass: "btn-flat btn-flat-primary", 3163 iconClass: undefined, 3164 specialWrapperClass: "available panel text-base", 3165 specialMessageTemplate: "Congratulations! <br/> The Marketplace domain <b>{{domainName}}</b> is for sale." 3166 }, 3167 BACKORDER: { 3168 name: "Backorder", 3169 nameClass: undefined, 3170 btnText: "Add to Cart", 3171 btnClass: "btn-flat btn-flat-primary", 3172 iconClass: undefined, 3173 specialWrapperClass: "available panel text-base", 3174 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is available for backorder " 3175 }, 3176 DPML_BLOCK: { 3177 name: "Available for DPML block override", 3178 nameClass: undefined, 3179 btnText: "Add to Cart", 3180 btnClass: "btn-flat btn-flat-primary", 3181 iconClass: undefined, 3182 specialWrapperClass: "available panel text-base", 3183 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is available for <a href=\"" + dpmlURL + "\">DPML</a> block override for Trademark Holders " 3184 }, 3185 INVAL_P: { 3186 name: "Contact Support", 3187 nameClass: undefined, 3188 btnText: "Contact Support", 3189 btnClass: "btn-flat btn-flat-primary", 3190 iconClass: undefined, 3191 specialWrapperClass: "available panel text-base", 3192 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is available. Contact support to confirm price." 3193 }, 3194 PREBOOK: { 3195 name: "Available", 3196 nameClass: undefined, 3197 btnText: "Add to Cart", 3198 btnClass: "btn-flat btn-flat-primary", 3199 iconClass: undefined, 3200 specialWrapperClass: "available panel text-base", 3201 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is available for pre-order." 3202 },
3203 PRE_REGISTER: { 3204 name: "Pre-Register", 3205 nameClass: undefined, 3206 btnText: "Pre-Register", 3207 btnClass: "btn-flat btn-flat-primary", 3208 iconClass: undefined, 3209 specialWrapperClass: "available panel text-base", 3210 specialMessageTemplate: "Available! <br/> The domain <b>{{domainName}}</b> is available for pre-registration." 3211 }, 3212 PRE_REGISTER_CONTACT: { 3213 name: "Contact Managed Services", 3214 nameClass: undefined, 3215 btnText: "Contact", 3216 btnClass: "btn btn-flat btn-outline link-hover", 3217 iconClass: undefined, 3218 specialWrapperClass: "taken", 3219 specialMessageTemplate: "Please contact Managed Services for more information about <b>{{domainName}}</b>." 3220 }, 3221 CONTACT_SUPPORT: { 3222 name: "Contact Support", 3223 nameClass: undefined, 3224 btnText: "Contact", 3225 btnClass: "btn btn-flat btn-outline link-hover", 3226 iconClass: undefined, 3227 specialWrapperClass: "taken", 3228 specialMessageTemplate: "Please contact Support for help registering <b>{{domainName}}</b>." 3229 }, 3230 RESV: { 3231 name: "Reserved", 3232 nameClass: undefined, 3233 btnText: "Contact Support", 3234 btnClass: "btn btn-flat btn-outline link-hover", 3235 iconClass: undefined, 3236 specialWrapperClass: "taken", 3237 specialMessageTemplate: "Sorry, the domain <b>{{domainName}}</b> is {{status}}. Contact support to confirm availability." 3238 }, 3239 NC: { 3240 name: "Reserved", 3241 nameClass: undefined, 3242 btnText: "Contact Support", 3243 btnClass: "btn btn-flat btn-outline link-hover", 3244 iconClass: undefined, 3245 specialWrapperClass: "taken", 3246 specialMessageTemplate: "Sorry, the domain <b>{{domainName}}</b> is {{status}}. Contact support to confirm availability." 3247 }, 3248 UNAVA: { 3249 name: "Taken", 3250 nameClass: undefined, 3251 btnText: "Let us Help", 3252 btnClass: "btn btn-flat btn-outline link-hover", 3253 iconClass: undefined, 3254 specialWrapperClass: "taken", 3255 specialMessageTemplate: "Sorry, the domain <b>{{domainName}}</b> is {{status}}." 3256 }, 3257 UNAVA_P: { 3258 name: "Taken", 3259 nameClass: undefined, 3260 btnText: "Let us Help", 3261 btnClass: "btn btn-flat btn-outline link-hover", 3262 iconClass: undefined, 3263 specialWrapperClass: "taken", 3264 specialMessageTemplate: "Sorry, the domain <b>{{domainName}}</b> is {{status}}." 3265 }, 3266 ERROR: { 3267 name: "Offline", 3268 nameClass: undefined, 3269 btnText: "N/A", 3270 btnClass: "hidden", 3271 iconClass: undefined, 3272 specialWrapperClass: "taken", 3273 specialMessageTemplate: "The registry for this domain extension is currently {{status}}. Please try your search again later." 3274 }, 3275 INVALID: { 3276 name: "Invalid", 3277 nameClass: undefined, 3278 btnText: "Invalid", 3279 btnClass: "hidden", 3280 iconClass: undefined, 3281 specialWrapperClass: "taken", 3282 specialMessageTemplate: "The domain <b>{{domainName}}</b> is {{status}}." 3283 }, 3284 // These statuses are only set if the user is logged in and has the domain under their management in some way. 3285 MGMT_REG: { 3286 name: "In Your Account",
3287 nameClass: undefined, 3288 btnText: "Manage Domain", 3289 btnClass: "btn btn-flat btn-outline link-hover", 3290 iconClass: undefined, 3291 specialWrapperClass: "taken", 3292 specialMessageTemplate: "The domain <b>{{domainName}}</b> is {{status}}." 3293 }, 3294 MGMT_REG_AR: { 3295 name: "In Your Account", 3296 nameClass: undefined, 3297 btnText: "Manage Domain", 3298 btnClass: "btn btn-flat btn-outline link-hover", 3299 iconClass: undefined, 3300 specialWrapperClass: "taken", 3301 specialMessageTemplate: "The domain <b>{{domainName}}</b> is {{status}}." 3302 }, 3303 MGMT_PREORDER: { 3304 name: "In Your Account", 3305 nameClass: undefined, 3306 btnText: "Manage Pre-Order", 3307 btnClass: "btn btn-flat btn-outline link-hover", 3308 iconClass: undefined, 3309 specialWrapperClass: "taken", 3310 specialMessageTemplate: "The domain <b>{{domainName}}</b> is pre-ordered within your account." 3311 }, 3312 MGMT_PREREG: { 3313 name: "In Your Account", 3314 nameClass: undefined, 3315 btnText: "Manage Pre-Reg", 3316 btnClass: "btn btn-flat btn-outline link-hover", 3317 iconClass: undefined, 3318 specialWrapperClass: "taken", 3319 specialMessageTemplate: "The domain <b>{{domainName}}</b> is pre-registered within your account." 3320 } 3321 } 3322 3323 model.GetDomainStatusProperty = getDomainStatusProperty; 3324 model.GetSpecialMessage = getSpecialMessage; 3325 3326 function getDomainStatusProperty(property, domain) { 3327 var result = ""; 3328 if (domain && domain.data) result = domain.data.result; 3329 3330 // if the customer has a pre reg for the domain, but the domain result is no longer pre-reg 3331 // don't worry about the pre reg management option 3332 if (domain && domain.data && domain.data.domainMgmtState && 3333 !(domain.data.domainMgmtState === "PREREG" && !(domain.data.result === "PRE_REGISTER"))) { 3334 var translatedStatus = "MGMT_" + domain.data.domainMgmtState; 3335 if (property === 'status') return translatedStatus; 3336 return model.StatusDescriptions[translatedStatus][property]; 3337 } 3338 3339 if (result) { 3340 // Set new property for contacting managed services 3341 if (result === "PRE_REGISTER" && domain.data.contactManagedServicesForPreReg) result = "PRE_REGISTER_CONTACT"; 3342 if (result === "RESV" && domain.data.ContactSupportForRegistration) result = "CONTACT_SUPPORT"; 3343 3344 if (result === "FORSALEOFFER" && domain.data.ecoSystemCode == "AFTERNIC" && domain.data.price == 0) result = "FORSALEREQUEST"; 3345 if (result === "INVAL_P" || ((result.slice(0, 5) !== "INVAL") && (result !== "UNKNW") && (result !== "ERR"))) { 3346 // keep result the same 3347 } else if (result === "UNKNW" || result === "ERR") { 3348 result = "ERROR"; 3349 } else { 3350 result = "INVALID"; 3351 } 3352 3353 if (property === "status") { 3354 return result; 3355 } else { 3356 return model.StatusDescriptions[result][property]; 3357 } 3358 } else { 3359 return ""; 3360 } 3361 } 3362 3363 function getSpecialMessage(domain) { 3364 if (!!domain && !!domain.data && !!domain.data.result) { 3365 var templatedString = model.GetDomainStatusProperty('specialMessageTemplate', domain); 3366 templatedString = templatedString.replace("{{domainName}}", domain.data.name); 3367 templatedString = templatedString.replace("{{status}}", model.GetDomainStatusProperty('name', domain).toLowerCase()); 3368 return templatedString; 3369 } else { 3370 return ""; 3371 } 3372 } 3373 }]); 3374})(); 3375 3376(function() { 3377 "use strict"; 3378 3379 angular.module('DomainSearch') 3380 .service('SelectDomainService', ['$rootScope', '$log', '$q', '$http', '$window', 'CartCounter', 'GAService', 'ModalService', 'DomainSearchServices', 'DomainsDataModel', function ($rootScope, $log, $q, $http, $window, CartCounter, GAService, ModalService, DomainSearchServices, DomainsDataModel) { 3381 var service = this; 3382 3383 // This should be set from the page calling it 3384 service.PackageSourceCode = undefined; 3385 3386 service.SelectDomain = selectDomain; 3387 service.RemoveDomain = removeDomain; 3388 service.addUpsellHostingToCart = addUpsellHostingToCart; 3389 service.removeUpsellHostingFromCart = removeUpsellHostingFromCart; 3390 service.addUpsellWixToCart = addUpsellWixToCart; 3391 service.removeUpsellWixFromCart = removeUpsellWixFromCart; 3392 service.RemoveDomainByName = removeDomainByName; 3393 service.AddManyDomainsToCart = addManyDomainsToCart; 3394 3395 service.DomainsDataModel = DomainsDataModel; 3396 3397 function selectDomain(domain) { 3398 var deferred = $q.defer(); 3399 3400 var bHasMultipleRegistrationTypes = !!domain.data.lPhaseFamilies && domain.data.lPhaseFamilies.length > 1; 3401 if (!bHasMultipleRegistrationTypes) { 3402 _.each(domain.data.lPhaseFamilies, function(family) { 3403 if (!!family.RegistrationTypes && family.RegistrationTypes.length > 1) { 3404 bHasMultipleRegistrationTypes = true; 3405 } 3406 }); 3407 } 3408 3409 if (bHasMultipleRegistrationTypes || 3410 domain.data.bPremium || 3411 domain.data.result === "PREBOOK" || 3412 domain.data.result === "AVAIL_S" || 3413 domain.data.result === "DPML_BLOCK" || 3414 domain.data.result === "FORSALE" || 3415 domain.data.result === "FORSALEOFFER") 3416 { 3417 ModalService.SelectDomainSpecial.open(domain.data.name).result.then(function (data) {
3418 $log.debug('Modal Close Success', data); 3419 deferred.resolve(data); 3420 }, function (data) { 3421 deferred.reject(data); 3422 $log.debug('Modal Close Failure', data); 3423 }); 3424 } else if (!!domain.data.result && domain.data.result === "PRE_REGISTER") { 3425 domain.AddToPreReg().then(function(data){deferred.resolve(data)}, function(data){deferred.reject(data)}); 3426 } else { 3427 domain.isAdding = true; 3428 3429 // catch client side validation errors 3430 try { 3431 domain.AddToCart(service.PackageSourceCode, 3432 { ga: GAService.GAProperties }).then(function (data) { 3433 domain.isAdding = false; 3434 if (data.success) { 3435 domain.data.upsellAddedWix = data.upsellAddedWix; 3436 domain.data.inCart = true; 3437 CartCounter.UpdateCartCount(data.billCount); 3438 $rootScope.$emit("FreeDomainsFromWix", data.freeDomainsFromWix); 3439 $rootScope.$emit("BillPackageTopDomainNameList", data.BillPackageTopDomainNameList); 3440 deferred.resolve(data); 3441 } 3442 }, 3443 function (data) { 3444 deferred.reject(data); 3445 if (data.error && (data.error + "").indexOf("country limitation") >= 0) { 3446 domain.failedAddDueToCountryRestriction = true; 3447 } 3448 domain.isAdding = false; 3449 }); 3450 } catch (err) { 3451 domain.isAdding = false; 3452 $log.debug(err); 3453 deferred.reject(err); 3454 } 3455 } 3456 3457 return deferred.promise; 3458 } 3459 3460 function removeDomain(domain) { 3461 domain.isRemoving = true; 3462 3463 if (domain.data.result === "PRE_REGISTER") { 3464 domain.RemoveFromPreReg(); 3465 } else { 3466 if (domain.data.upsellAddedWix) { 3467 removeUpsellWixFromCart(domain); 3468 } 3469 return domain.RemoveFromCart({ ga: GAService.GAProperties }).then(function (data) { 3470 domain.isRemoving = false; 3471 if (data.success) { 3472 if (DomainsDataModel?.Bundle && DomainsDataModel.Bundle.Domains.find(d => d.data.name == domain.data.name)) { 3473 DomainsDataModel.Bundle.Added = false; 3474 } 3475 domain.data.inCart = false; 3476 domain.data.upsellAddedWix = data.upsellAddedWix; 3477 $rootScope.$emit("FreeDomainsFromWix", data.freeDomainsFromWix); 3478 $rootScope.$emit("BillPackageTopDomainNameList", data.BillPackageTopDomainNameList); 3479 } 3480 }); 3481 } 3482 3483 return $q.resolve(); 3484 } 3485 3486 function addUpsellHostingToCart(domain) { 3487 domain.data.upsellAdding = true; 3488 domain.addUpsellHostingToCart().then(function (data) { 3489 domain.data.upsellAdding = false; 3490 if (data.success) { 3491 domain.data.upsellAdded = true; 3492 domain.data.upsellPrice = data.upsellPrice 3493 } 3494 }); 3495 } 3496 3497 function removeUpsellHostingFromCart(domain) { 3498 domain.data.upsellRemoving = true; 3499 domain.removeUpsellHostingFromCart().then(function (data) { 3500 domain.data.upsellRemoving = false; 3501 if (data.success) { 3502 domain.data.upsellRemoved = true; 3503 domain.data.upsellAdded = false; 3504 } 3505 }); 3506 } 3507 3508 function addUpsellWixToCart(domain) { 3509 domain.data.upsellAddingWix = true; 3510 domain.addUpsellWixToCart().then(function (data) { 3511 domain.data.upsellAddingWix = false; 3512 if (data.success) { 3513 domain.data.upsellAddedWix = true; 3514 domain.data.upsellRemovedWix = false; 3515 CartCounter.UpdateCartCount(data.billCount); 3516 3517 $rootScope.$emit("FreeDomainsFromWix", data.freeDomainsFromWix); 3518 } 3519 }); 3520 } 3521 3522 function removeUpsellWixFromCart(domain) { 3523 domain.data.upsellRemovingWix = true; 3524 domain.removeUpsellWixFromCart().then(function (data) { 3525 domain.data.upsellRemovingWix = false; 3526 if (data.success) { 3527 domain.data.upsellRemovedWix = true; 3528 domain.data.upsellAddedWix = false; 3529 CartCounter.UpdateCartCount(data.billCount); 3530 3531 $rootScope.$emit("FreeDomainsFromWix", data.freeDomainsFromWix); 3532 } 3533 }); 3534 } 3535 3536 function removeDomainByName(sDomainName) { 3537 3538 DomainSearchServices.RemoveDomainFromCart(sDomainName).then(function (respData) { 3539 3540 CartCounter.UpdateCartCount(respData.billCount); 3541 $rootScope.$emit("BillPackageTopDomainNameList", respData.BillPackageTopDomainNameList); 3542 3543 //Handle analytics 3544 GAService.RemoveProductAndSend(GAService.GAProperties.categoryName, GAService.GAProperties.actionName, respData.analytics.ga);
3545 }); 3546 } 3547 3548 function addManyDomainsToCart(domains) { 3549 var filteredDomains = _.filter(domains, function(domain) { 3550 return domain.data.result === "AVAIL" && !domain.data.inCart; 3551 }); 3552 3553 if (filteredDomains.length < 1) return; 3554 3555 _.each(filteredDomains, function(domain) { domain.isAdding = true }); 3556 3557 $http.post($window._SmartURL + "cart/AddDomainsToCart", 3558 { 3559 domains: _.map(filteredDomains, function(domain) { return domain.data.name }) 3560 }) 3561 .then(function(resp) { 3562 _.each(filteredDomains, function (domain) { domain.isAdding = false; domain.data.inCart = true }); 3563 CartCounter.UpdateCartCount(resp.data.billCount); 3564 $rootScope.$emit("BillPackageTopDomainNameList", resp.data.BillPackageTopDomainNameList); 3565 }, function(resp) { 3566 _.each(filteredDomains, function (domain) { domain.isAdding = false }); 3567 }); 3568 } 3569 }]); 3570})(); 3571(function () { 3572 "use strict"; 3573 3574 angular.module('DomainSearch') 3575 .service('ModalService', ['$uibModal', function ($uibModal) { 3576 var service = this; 3577 3578 service.SelectDomainSpecial = { 3579 open: openSelectDomainSpecial 3580 } 3581 3582 service.SaveShare = { 3583 open: openSaveShare 3584 } 3585 3586 service.InvalidExtensions = { 3587 open: openInvalidExtensions 3588 } 3589 3590 service.LitesiteLearnMore = { 3591 open: openLiteSiteLearnMore 3592 } 3593 3594 function openSelectDomainSpecial(domainName) { 3595 return $uibModal.open({ 3596 templateUrl: 'modalSelectSpecialDomain.html',
3597 controller: [ 3598 '$scope', '$uibModalInstance', '$log', 'DomainsDataModel', 'GAService', 'SelectDomainService', 'domainName', '$window', '$http', function ($scope, $uibModalInstance, $log, DomainsDataModel, GAService, SelectDomainService, domainName, $window, $http) { 3599 $scope.rootURL = $window._SmartURL; 3600 $scope.domain = DomainsDataModel.GetDomainByName(domainName); 3601 $scope.domain.data.isAdditionalAftermarketInfoReady = false; 3602 $scope.addDomain = addDomain; 3603 3604 $scope.cancel = function () { 3605 $uibModalInstance.dismiss('cancel'); 3606 }; 3607 3608 // obtain additional domain properties for Aftermarket domains 3609 if ($scope.domain.data.bAftermarket) 3610 { 3611 var sURL = $window._SmartURL + "domain-registration/search/GetAdditionalAftermarketData/"; 3612 // submit request 3613 $http.post(sURL, $scope.domain.data) 3614 .then(function (response) { 3615 $scope.domain.data = response.data; 3616 }, 3617 function (response) { 3618 $uibModalInstance.close(response.data); 3619 console.debug('GetAdditionalAftermarketData failed.'); 3620 }); 3621 } 3622 3623 function addDomain(registrationType, iRegistryTLDPremiumRatingID) { 3624 // add isAdding prop to registration type because we don't want every item in the modal to update 3625 registrationType.isAdding = true; 3626 $scope.domain.data.errorMessage = null; 3627 try { 3628 $scope.domain.AddToCart(SelectDomainService.PackageSourceCode, 3629 { ga: GAService.GAProperties }, registrationType, iRegistryTLDPremiumRatingID).then(function (data) { 3630 registrationType.isAdding = false; 3631 3632 if (data.success) { 3633 $scope.domain.data.inCart = true; 3634 $uibModalInstance.close(data); 3635 $scope.$emit("BillPackageTopDomainNameList", data.BillPackageTopDomainNameList); 3636 } 3637 }, 3638 function (data) { 3639 registrationType.isAdding = false; 3640 // display error message on popup 3641 if (data.canDisplayError) 3642 $scope.domain.data.errorMessage = data.error; 3643 else 3644 $scope.domain.data.errorMessage = 3645 "<p>We're sorry, but an error has occurred and we are unable to add the selected item to " + 3646 "your shopping cart. To confirm the current availability of your selection, we suggest " + 3647 "searching for your desired domain name again.</p>"; 3648 }); 3649 } catch (err) { 3650 $log.debug(err); 3651 $scope.domain.isAdding = false; 3652 $uibModalInstance.dismiss(err); 3653 } 3654 3655 } 3656 3657 } 3658 ], 3659 size: 'lg', 3660 resolve: { domainName: function () { return domainName } } 3661 }); 3662 } 3663 3664 function openLiteSiteLearnMore(searchTerm, specificDomain) { 3665 return $uibModal.open({ 3666 templateUrl: 'modalLiteSiteLearnMore.html', 3667 controllerAs: 'liteSiteLearnMoreModal', 3668 controller: ['$timeout', '$uibModalInstance', '$log', '$window', 'specificDomain', 'searchTerm', 'DomainSearchManager', 'DomainSearchServices', 'SelectDomainService', 'WnErrorHandler', 3669 function ($timeout, $uibModalInstance, $log, $window, specificDomain, searchTerm, DomainSearchManager, DomainSearchServices, SelectDomainService, WnErrorHandler) { 3670 var vm = this; 3671 vm.specificDomain = specificDomain; 3672 vm.PackageCode = "ONEPAGESITE"; 3673 vm.searchTerm = searchTerm; 3674 3675 vm.cancel = function () { 3676 $uibModalInstance.dismiss('cancel'); 3677 }; 3678 3679 vm.RedirectToBuyPage = function() { 3680 $window.location.href = window._SmartURL + "website-builder/Buy?sPackageCode=" + vm.PackageCode + "&search=" + vm.searchTerm; 3681 }; 3682 3683 vm.AddOPSToCart = function () { 3684 vm.specificDomain.data.bLiteSite = true; 3685 SelectDomainService.SelectDomain(vm.specificDomain, false).then(function () { 3686 $window.location.href = $window._SmartURL + "cart"; 3687 }); 3688 }; 3689 3690 vm.RedirectToCart = function () { 3691 $window.location.href = $window._SmartURL + "cart"; 3692 }; 3693 }], 3694 size: 'lg', 3695 resolve: { searchTerm: function() { return searchTerm }, specificDomain: function() {return specificDomain} } 3696 }); 3697 } 3698 3699 function openSaveShare(domainsToShare) { 3700 return $uibModal.open({ 3701 templateUrl: 'modalSaveShare.html', 3702 controllerAs: 'saveShareModal', 3703 controller: ['$timeout', '$uibModalInstance', '$log', '$window', 'domainsToShare', 'DomainSearchManager', 'DomainSearchServices', 'StatusService', 'WnErrorHandler', function ($timeout, $uibModalInstance, $log, $window, domainsToShare, DomainSearchManager, DomainSearchServices, StatusService, WnErrorHandler) { 3704 var vm = this; 3705 3706 // This is set because the $uibModalInstance can be closed before certain promises are resolved 3707 // Therefore, the sharing should no longer happen 3708 var canStillShare = true; 3709 3710 vm.csvWaitingForSearch = false; 3711 3712 $uibModalInstance.result['finally'](function() { 3713 canStillShare = false; 3714 }); 3715 3716 vm.ok = function () { 3717 $uibModalInstance.close(true); 3718 }; 3719 3720 vm.cancel = function () { 3721 $uibModalInstance.dismiss('cancel'); 3722 }; 3723 3724 vm.canShareEmail = function() { 3725 return !!DomainSearchManager.DomainStem; 3726 } 3727 3728 vm.shareCsv = function () { 3729 if (domainsToShare.length < 1) { 3730 $uibModalInstance.dismiss('cancel'); 3731 WnErrorHandler.PushNotification("You must have some results to save them as CSV file"); 3732 return; 3733 } 3734 3735 if (_.any(domainsToShare, function (domain) { return !domain.data.result }) && !!DomainSearchManager.SearchingPromise) { 3736 vm.csvWaitingForSearch = true; 3737 DomainSearchManager.SearchingPromise.then(function () { 3738 vm.csvWaitingForSearch = false; 3739 shareCsv_exportCsv(); 3740 }); 3741 } else { 3742 shareCsv_exportCsv(); 3743 } 3744 } 3745 3746 vm.emailAddress = ""; 3747 3748 vm.buildMailTo = function () { 3749 var sSubject = "Webnames.ca Domain Search Results"; 3750 var sBody = "Check out the domains I searched \n\r" + window.location.href; // Updated: URL contains all search parameters/filters 3751 var mailTo = "mailto:" + "?Subject=" + encodeURIComponent(sSubject) 3752 + "&Body=" + encodeURIComponent(sBody); 3753 return mailTo; 3754 } 3755 3756 function shareCsv_exportCsv() { 3757 // Make sure the modal has not yet been closed first 3758 if (!canStillShare) return; 3759 3760 var angularDomains = _.map(domainsToShare, function (domain) { 3761 return { 3762 'Domain Name': domain.data.name, 3763 Result: StatusService.GetDomainStatusProperty('name', domain), 3764 Price: domain.data.price, 3765 Currency: domain.data.sPriceCurrencyCode, 3766 'Based on the lowest domain year price*': domain.data.specialPrice 3767 } 3768 }); 3769 3770 // Ninja create a form so we can do a post back to the server 3771 var form = document.createElement("form"); 3772 form.action = window._SmartURL + "domain-registration/search/ShareCsv"; 3773 form.method = "POST"; 3774 form.target = "_self"; 3775 3776 var input = document.createElement("textarea"); 3777 3778 input.name = "jsonDomains"; 3779 input.value = JSON.stringify(angularDomains); 3780 form.appendChild(input); 3781 form.style.display = 'none'; 3782 3783 document.body.appendChild(form); 3784 form.submit(); 3785 document.body.removeChild(form); 3786 } 3787 }], 3788 size: 'lg', 3789 resolve: { domainsToShare: function(){ return domainsToShare }} 3790 }); 3791 } 3792 3793 3794 3795 function openInvalidExtensions() { 3796 return $uibModal.open({ 3797 templateUrl: 'modalInvalidExtensions.html',
3798 controller: [ 3799 '$scope', '$uibModalInstance', '$log', function ($scope, $uibModalInstance, $log) { 3800 $scope.ok = function () { 3801 $uibModalInstance.close(true); 3802 }; 3803 $scope.cancel = function () { 3804 $uibModalInstance.dismiss('cancel'); 3805 }; 3806 } 3807 ], 3808 size: 'lg', 3809 resolve: { } 3810 }); 3811 } 3812 }]); 3813 3814})(); 3815(function() { 3816 "use strict"; 3817 3818 angular.module("DomainSearch") 3819 3820 // Work around to flag HTML as safe and bindable 3821 .filter('trustHtml', ['$sce', trustHtml]) 3822 3823 // Helps format the display string for specific searches 3824 .filter('convertPriceForSentence', ['$filter', convertPriceForSentence]) 3825 3826 // Filter used on search form Typeahead suggestions 3827 .filter('convertExtensionToSuggestions', [convertExtensionToSuggestions]); 3828 3829 // Handy filter to trust html on a page 3830 function trustHtml($sce) { 3831 return function (val) { 3832 return $sce.trustAsHtml(val); 3833 }; 3834 } 3835 3836 function convertPriceForSentence($filter) { 3837 return function (domainData) { 3838 if (!domainData) return ""; 3839 3840 var formattedPriceWithCurrency = domainData.sDisplayedPriceCurrencyCode + " " + $filter('currency')(domainData.displayedPrice); 3841 var formattedMinOfferWithCurrency = domainData.sPriceCurrencyCode + " " + $filter('currency')(domainData.minOffer); 3842 3843 if (domainData.result == 'FORSALEOFFER') 3844 { 3845 return " " + formattedMinOfferWithCurrency + " minimum offer"; 3846 } 3847 else if (domainData.specialPrice) { 3848 return "from " + formattedPriceWithCurrency; 3849 } else { 3850 return "for " + formattedPriceWithCurrency; 3851 } 3852 } 3853 } 3854 3855 function convertExtensionToSuggestions() { 3856 return function (searchableExtensions, searchTerm) { 3857 // Get the indices of the periods in the current search term. This will be used to determine subdomains 3858 var lastPeriodIndex = searchTerm.lastIndexOf('.'); 3859 var secondLastPeriodIndex = searchTerm.substr(0, lastPeriodIndex).lastIndexOf('.'); 3860 3861 var filteredExtensions = []; 3862 3863 var suggestionBag = []; 3864 3865 // Short circuit if the search term is less than 3 characters 3866 if (searchTerm.length < 3) { 3867 return []; 3868 } else if (lastPeriodIndex < 0 && secondLastPeriodIndex < 0) { // No period provided, just append possible extensions 3869 return _.map(searchableExtensions, function (extension) { 3870 return searchTerm + extension; 3871 }); 3872 } else if (lastPeriodIndex > -1 && secondLastPeriodIndex < 0) { // Only one period provided, filter and map 3873 filteredExtensions = _.filter(searchableExtensions, function (extension) { 3874 var tempExtension = searchTerm.substr(lastPeriodIndex, searchTerm.length - 1); 3875 if (tempExtension === ".") { 3876 return true; 3877 } else { 3878 return extension.indexOf(tempExtension) > -1; 3879 } 3880 }); 3881 3882 suggestionBag = _.map(filteredExtensions, function (extension) { 3883 return searchTerm.substr(0, lastPeriodIndex) + extension; 3884 }); 3885 3886 if (filteredExtensions.length === 1 && suggestionBag[0] === searchTerm) { 3887 return []; 3888 } else { 3889 return suggestionBag; 3890 } 3891 } else if (secondLastPeriodIndex > -1 && secondLastPeriodIndex > -1) { // Two periods provided, filter to subdomain extensions and map 3892 filteredExtensions = _.filter(searchableExtensions, function (extension) { 3893 var tempExtension = searchTerm.substr(secondLastPerio
3893dIndex, searchTerm.length - 1); 3894 return extension.indexOf(tempExtension) > -1; 3895 }); 3896 3897 suggestionBag = _.map(filteredExtensions, function (extension) { 3898 return searchTerm.substr(0, secondLastPeriodIndex) + extension; 3899 }); 3900 3901 if (filteredExtensions.length === 1 && suggestionBag[0] === searchTerm) { 3902 return []; 3903 } else { 3904 return suggestionBag; 3905 } 3906 } else { 3907 return []; 3908 } 3909 } 3910 } 3911 3912 3913})(); 3914/* 3915 Domain Search Domain Result Directive 3916 -------------------------------- 3917 The directive specifically for the domain search. 3918 3919 It handles the logic of what should happen when a customer 3920 clicks the call to action and interpolates the status text and 3921 classes to display for various different statuses. 3922 3923 Dependencies 3924 ~ Underscore : Using UnderscoreJS for any array/object work. 3925 3926 Author: Akos Sebestyen 2015 3927*/ 3928(function () { 3929"use strict"; 3930 3931 angular.module('DomainSearch') 3932 .directive('domainResult', [domainResult]); 3933 3934 function domainResult() { 3935 return { 3936 restrict: 'EA', 3937 replace: true, 3938 scope: { 3939 domain: '=', 3940 domainsDataModel: '=', 3941 addBundle: '&', 3942 removeBundle: '&' 3943 }, 3944 templateUrl: function(element, attr) { 3945 return attr.templateUrl || 'domainResult.html'; 3946 }, 3947 controller: [ 3948 '$scope', '$rootScope', '$log', 'StatusService', 'SelectDomainService', function ($scope, $rootScope, $log, StatusService, SelectDomainService) { 3949 $scope.selectDomain = SelectDomainService.SelectDomain; 3950 $scope.removeDomain = SelectDomainService.RemoveDomain; 3951 $scope.addUpsellHostingToCart = SelectDomainService.addUpsellHostingToCart; 3952 $scope.removeUpsellHostingFromCart = SelectDomainService.removeUpsellHostingFromCart; 3953 $scope.addUpsellWixToCart = SelectDomainService.addUpsellWixToCart; 3954 $scope.removeUpsellWixFromCart = SelectDomainService.removeUpsellWixFromCart; 3955 3956 // Gets any property from statusDescriptions 3957 $scope.getDomainStatusProperty = StatusService.GetDomainStatusProperty; 3958 3959 // Interpolates the 'specialMessageTemplate' with the correct properties to return bindable html 3960 $scope.getSpecialMessage = StatusService.GetSpecialMessage; 3961 3962 // Fix casing of scope variables passed in from parent controller: 3963 $scope.DomainsDataModel = $scope.domainsDataModel; 3964 $scope.AddBundle = $scope.addBundle; 3965 $scope.RemoveBundle = $scope.removeBundle; 3966 } 3967 ] 3968 } 3969 } 3970 3971})(); 3972/* 3973 Webnames.ca 2015 3974 AngularJS Domain Search App 3975 3976 Serves as the ng-app module for the AngularJS domain search page 3977 3978 Author: Akos Sebestyen 3979*/ 3980(function () { 3981 "use strict"; 3982 3983 angular.module('DomainSearchApp', [ 3984 'DomainSearch', 3985 'ngAnimate', 3986 'rzModule', 3987 'WixPreviewModal' 3988 ]) 3989 .config(['$logProvider', '$locationProvider', function ($logProvider) { 3990 $logProvider.debugEnabled(true); 3991 }]); 3992})(); 3993/* 3994 Webnames.ca 2015 3995 AngularJS Domain Search App 3996 3997 Contains the main controller that handles majority of the viewstate changes related to domain search 3998 3999 Author: Akos Sebestyen 4000*/ 4001 4002(function() { 4003 "use strict"; 4004 4005 angular.module('DomainSearchApp') 4006 .config(["$locationProvider", function ($locationProvider) { 4007 $locationProvider.html5Mode({ enabled: true, requireBase: false }); 4008 }]) 4009 .controller('MainController', [ 4010 '$scope', 4011 '$rootScope', 4012 '$timeout', 4013 '$interval', 4014 '$filter', 4015 '$log', 4016 '$location', 4017 '$window', 4018 '$q', 4019 // DomainSearch 4020 'StatusService', 4021 'DomainSearchServices', 4022 'SelectDomainService', 4023 'DomainSearchManager', 4024 'DomainsDataModel', 4025 'CartCounter', 4026 'HeaderSearchNinja', 4027 'ModalService', 4028 // WebnamesAnalytics 4029 'GAService', 4030 // WebnamesCommon 4031 'LegacyParam', 4032 'GenericInjector', 4033 // WebnamesError 4034 'WnErrorHandler', 4035 "QueryStringFilterBinder", 4036 'WixPreviewModalService', 4037 function ($scope, $rootScope, $timeout, $interval, $filter, $log, $location, $window, $q, StatusService, DomainSearchServices, SelectDomainService, DomainSearchManager, DomainsDataModel, CartCounter, HeaderSearchNinja, ModalService, GAService, LegacyParam, GenericInjector, WnErrorHandler, QueryStringFilterBinder, WixPreviewModalService) { 4038 $scope.isAfterMarketPage = GenericInjector.PropBag.isAfterMarketPage; 4039 $scope.isViewingOwnListings = GenericInjector.PropBag.isViewingOwnListings; 4040 $scope.BillPackageTopDomainNameList = GenericInjector.PropBag.BillPackageTopDomainNameList; 4041 $scope.lDomainBundles = GenericInjector.PropBag.lDomainBundles; 4042 $scope.DomainsDataModel = DomainsDataModel; 4043 $scope.CartCounter = CartCounter; 4044 $scope.isRetailDefault = GenericInjector.PropBag.isRetailDefault; 4045 $scope.bWixPromoActive = GenericInjector.PropBag.bWixPromoActive; 4046 $scope.bUseAI = GenericInjector.PropBag.bUseAI; 4047 $scope.lsLongSearchWaitMessages = GenericInjector.PropBag.lsLongSearchWaitMessages; 4048 $scope.suggestionPanelIndex = 3; 4049 $scope.DomainSearchManager = DomainSearchManager; 4050 DomainSearchManager.SetAftermarketOnlyMode($scope.isAfterMarketPage); 4051 DomainSearchManager.SuggestionPanelIndex = $scope.suggestionPanelIndex; 4052 var iBulkSearchLimit = ($scope.isAfterMarketPage ? 10 : 300); 4053 var suppressNextSearch = false; 4054 var stepsArray; 4055 if ($scope.isAfterMarketPage) { 4056 stepsArray = [ 4057 { value: 0 }, 4058 { value: 200 }, 4059 { value: 500 }, 4060 { value: 1000 }, 4061 { value: 2000 }, 4062 { value: 5000 }, 4063 { value: 10000 }, 4064 { value: 20000 }, 4065 { value: 50000 }, 4066 { value: 100000 } 4067 ]; 4068 } 4069 else { 4070 stepsArray = [ 4071 { value: 0 }, 4072 { value: 10 }, 4073 { value: 20 }, 4074 { value: 50 }, 4075 { value: 100 }, 4076 { value: 200 }, 4077 { value: 500 }, 4078 { value: 1000 }, 4079 { value: 2000}, 4080 { value: 5000 }, 4081 { value: 10000 }, 4082 { value: 20000 }, 4083 { value: 50000 }, 4084 { value: 100000} 4085 ]; 4086 } 4087 4088 $scope.searchForm = {}; 4089 $scope.specificDomain = {}; 4090 4091 // NOTE: Required to be on the scope, so the filter used by the typeahead can use it decleratively 4092 $scope.searchableExtensions = []; 4093 $scope.availableSortOptions = [ 4094 'Relevance', 4095 'Price: Low to High', 4096 'Price: High to Low', 4097 'Extension: A to Z', 4098 'Extension: Z to A',
4099 'Name: A to Z', 4100 'Name: Z to A', 4101 'Search Term' 4102 ]; 4103 4104 // Main search method. Resets previous search related variables 4105 $scope.doSearch = doSearch; 4106 $scope.showMore = showMore; 4107 4108 /// Domain Result 4109 $scope.selectDomain = function (domain) { return SelectDomainService.SelectDomain(domain) } 4110 $scope.AddBundle = addBundle; 4111 $scope.RemoveBundle = removeBundle; 4112 $scope.removeDomain = SelectDomainService.RemoveDomain; 4113 $scope.addUpsellHostingToCart = SelectDomainService.addUpsellHostingToCart; 4114 $scope.removeUpsellHostingFromCart = SelectDomainService.removeUpsellHostingFromCart; 4115 $scope.addUpsellWixToCart = SelectDomainService.addUpsellWixToCart; 4116 $scope.removeUpsellWixFromCart = SelectDomainService.removeUpsellWixFromCart; 4117 4118 $scope.addAllDomainsToCart = SelectDomainService.AddManyDomainsToCart; 4119 4120 // Gets any property from statusDescriptions 4121 $scope.getDomainStatusProperty = StatusService.GetDomainStatusProperty; 4122 4123 // Interpolates the 'specialMessageTemplate' with the correct properties to return bindable html 4124 $scope.getSpecialMessage = StatusService.GetSpecialMessage; 4125 4126 // Available categories to choose from 4127 $scope.availableCategories = []; 4128 $scope.categoryFilters = { 4129 categoryLimit: 5, 4130 showingMore: false 4131 } 4132 $scope.toggleCategoryCollapse = toggleCategoryCollapse; 4133 $scope.addCategory = addCategory; 4134 $scope.removeCategory = removeCategory; 4135 $scope.clearCategories = clearCategories; 4136 // Available extensions to choose from 4137 $scope.availableExtensions = []; 4138 $scope.extensionFilters = { 4139 extensionLimit: 10, 4140 showingMore: false 4141 } 4142 $scope.toggleExtensionCollapse = toggleExtensionCollapse; 4143 $scope.addExtension = addExtension; 4144 $scope.removeExtension = removeExtension; 4145 $scope.clearExtensions = clearExtensions; 4146 $scope.searchAlternative = searchAlternative; 4147 //$scope.toggleFilters = toggleFilters; 4148 4149 /// Share Results 4150 $scope.saveShare = saveShare; 4151 4152 /// Litesite Popup 4153 $scope.litesiteLearnMore = litesiteLearnMore; 4154 4155 ///Wix Preview popup 4156 $scope.WixPreview = WixPreview; 4157 4158 var defaultInitialResultLength = 25; 4159 var initialResultLength = defaultInitialResultLength; 4160 4161 /// Viewstate bools - sad excuse for a router :) 4162 $scope.viewState = { 4163 isLoading: false, 4164 hasSearched: false, 4165 bulkMode: false, 4166 sortOption: 'Relevance', 4167 resultsLimitLength: initialResultLength, 4168 showingAlternatives: false, 4169 showPopularExtensionAlternatives: false, 4170 showNewExtensionAlternatives: false, 4171 canShowAlternatives: function () { 4172 return !!DomainSearchManager.DomainStem && !$scope.isAfterMarketPage; 4173 }, 4174 ownerUsername: null 4175 } 4176 4177 $scope.showHistory = false; 4178 4179 $scope.toggleBulkMode = function () { 4180 $scope.viewState.bulkMode = !$scope.viewState.bulkMode; 4181 4182 // When switching to bulk mode, if the bulk mode query is blank, copy the query from single search mode: 4183 if ($scope.viewState.bulkMode && 4184 !(($scope.searchForm.bulkSearchTerms || "").trim()) && 4185 !!(($scope.searchForm.searchTerm || "").trim())) 4186 { 4187 $scope.searchForm.bulkSearchTerms = $scope.searchForm.searchTerm + "\n"; 4188 } 4189 4190 if ($scope.viewState.bulkMode) { 4191 $scope.filters.useAI = false; // AI suggestions are not compatible with bulk search. 4192 $scope.filters.bApplyFilters = true; // Enable filters in bulk mode to show all results, even unavailable ones. 4193 } 4194 } 4195 4196 var numTimesScrolled = 0; 4197 var scrollLimit = 3; 4198 $scope.removeAllFilters = removeAllFilters; 4199 4200 $scope.anyAvailabilityFilter = function () { 4201 return !$scope.filters.availableNow || 4202 !$scope.filters.upcoming || 4203 !$scope.filters.unavailable || 4204 !$scope.filters.preReg || 4205 !$scope.filters.forSale; 4206 }; 4207 4208 if ($scope.isAfterMarketPage) { 4209 $scope.filters = { 4210 availableNow: false, 4211 upcoming: false, 4212 unavailable: false, 4213 preReg: false, 4214 forSale: true, 4215 fastTransfersOnly: false, 4216 extensionFilterType: 'extension' 4217 } 4218 } 4219 else { 4220 $scope.filters = { 4221 availableNow: true, 4222 upcoming: true, 4223 unavailable: true, 4224 preReg: true, 4225 forSale: true, 4226 onPromo: true, 4227 fastTransfersOnly: false, 4228 bApplyFilters: false, 4229 useAI: $scope.bUseAI, 4230 bIncludeExplanations: true, 4231 bIncreaseRandomness: false 4232 }; 4233 } 4234 4235 $scope.filters.minPrice = 0; 4236 $scope.filters.maxPrice = 100000; 4237 4238 $scope.parameters = { 4239 bulkMode: false, 4240 domain: null, 4241 bulkSearchTerms: null, 4242 ownerGUID: null 4243 } 4244 4245 $scope.extensions = []; 4246 $scope.categories = []; 4247 4248 $scope.sliderOptions = { 4249 floor: 0, 4250 ceil: 100000,
4251 enforceStep: false, 4252 enforceRange: false, 4253 //noSwitching: true, 4254 mergeRangeLabelsIfSame: true, 4255 showTicks: true, 4256 showTicksValues: false, 4257 pushrange: true, 4258 stepsArray: stepsArray 4259 } 4260 4261 $scope.doSort = function () { $scope.viewState.resultsLimitLength = initialResultLength; } 4262 4263 $scope.totalDomainsInCart = function () { return CartCounter.CartCount } 4264 $scope.totalPreRegCount = function () { return CartCounter.PreRegCount } 4265 $scope.getOwnerSearchLink = function () { 4266 if (!!$scope.parameters.ownerGUID) 4267 return $window._SmartURL + "aftermarket/search?ownerGUID=" + $scope.parameters.ownerGUID; 4268 else 4269 return ''; 4270 } 4271 $scope.getGoToCartLink = function () { return CartCounter.PreRegCount > 0 ? $window._SmartURL + "pre-registration/DomainReview.aspx" : $window._SmartURL + "cart" } 4272 4273 $scope.RemoveDomainFromBasketPreview = function (sDomainName) { 4274 var oDomain = _.find($scope.domains, function (dom) { return dom.data.name === sDomainName; }); 4275 if (oDomain) { 4276 SelectDomainService.RemoveDomain(oDomain); 4277 } 4278 else { 4279 SelectDomainService.RemoveDomainByName(sDomainName); 4280 } 4281 } 4282 4283 $scope.getShowMoreText = function () { 4284 if (numTimesScrolled < scrollLimit) return "Scroll To See More Results"; 4285 return "Click To See More Results"; 4286 } 4287 4288 $scope.$watch('viewState.sortOption', function () { 4289 $timeout(function () { DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); }); 4290 }); 4291 4292 $scope.$watch('filters', function (newVal, oldVal) { 4293 // If any of the availability filters change, rest the result set 4294 if (newVal.availableNow !== oldVal.availableNow || 4295 newVal.upcoming !== oldVal.upcoming || 4296 newVal.unavailable !== oldVal.unavailable || 4297 newVal.forSale !== oldVal.forSale || 4298 newVal.fastTransfersOnly != oldVal.fastTransfersOnly || 4299 newVal.onPromo != oldVal.onPromo || 4300 newVal.preReg !== oldVal.preReg || 4301 newVal.bApplyFilters !== oldVal.bApplyFilters) { 4302 $scope.viewState.resultsLimitLength = initialResultLength; 4303 $timeout(function () { DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); }); 4304 } 4305 }, true); 4306 4307 $scope.$watch('bulkMode', function (newVal) { 4308 // Add sorting by name if bulk mode is enabled 4309 if (newVal) { 4310 $scope.availableSortOptions.push('Search Term'); 4311 } else { 4312 if ($scope.viewState.sortOption === 'Search Term') $scope.viewState.sortOption = 'Relevance'; 4313 $scope.availableSortOptions = _.without($scope.availableSortOptions, 'Search Term'); 4314 } 4315 }); 4316 4317 $scope.$watch('viewState.showingAlternatives', function (newVal, oldVal) { 4318 $timeout(function () { 4319 DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); 4320 }); 4321 }); 4322 4323 $scope.$watch('viewState.showPopularExtensionAlternatives', function (newVal, oldVal) { 4324 $timeout(function () { 4325 DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); 4326 }); 4327 }); 4328 4329 $scope.$watch('viewState.showNewExtensionAlternatives', function (newVal, oldVal) { 4330 $timeout(function () { 4331 DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); 4332 }); 4333 }); 4334 4335 $scope.$watch('filters.extensionFilterType', function (newVal, oldVal) { 4336 if (newVal == 'extension') { 4337 if ($scope.categories.length > 0) $scope.clearCategories(); 4338 } 4339 else if (newVal == 'category') { 4340 if ($scope.extensions.length > 0) $scope.clearExtensions(); 4341 } 4342 }, true); 4343 4344 $scope.$watch('extensions', function (newVal, oldVal) { 4345 // update corresponding extension URL Parameters whenever extension filter changes 4346 4347 // reset everything, then set to what's currently filtered 4348 for (var prop in $scope.urlExtensions) { 4349 if ($scope.urlExtensions.hasOwnProperty(prop)) { 4350 $scope.urlExtensions[prop] = false; 4351 } 4352 } 4353 for (var i = 0; i < newVal.length; i++) { 4354 var extension = newVal[i]; 4355 $scope.urlExtensions[extension['extensionName']] = true; 4356 } 4357 4358 //If the extensions change, reset the limit 4359 $scope.viewState.resultsLimitLength = initialResultLength; 4360 4361 if ($scope.isAfterMarketPage && !$scope.parameters.ownerGUID) { 4362 // filter changes need to re-trigger Aftermarket searches 4363 doDebouncedSearch(); 4364 } 4365 }, true); 4366 4367 $scope.$watch('categories', function (newVal, oldVal) { 4368 // update corresponding category URL Parameters whenever category filter changes 4369 4370 // reset everything, then set to what's currently filtered 4371 for (var prop in $scope.urlCategories) { 4372 if ($scope.urlCategories.hasOwnProperty(prop)) { 4373 $scope.urlCategories[prop] = false; 4374 } 4375 } 4376 for (var i = 0; i < newVal.length; i++) { 4377 var category = newVal[i]; 4378 $scope.urlCategories[category['categoryCode']] = true; 4379 } 4380 4381 //If the categories change, reset the limit 4382 $scope.viewState.resultsLimitLength = initialResultLength; 4383 $timeout(function () { 4384 DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); 4385 }); 4386 4387 if ($scope.isAfterMarketPage && !$scope.parameters.ownerGUID) { 4388 // filter changes need to re-trigger Aftermarket searches 4389 doDebouncedSearch(); 4390 } 4391 }, true); 4392 4393 $scope.$watch('urlExtensions', function (newVal, oldVal) { 4394 // set extension filters whenever extension URL parameters change 4395 4396 // reset everything, then set to what's currently in the URL 4397 $scope.clearExtensions(); 4398 for (var i = 0; i < $scope.availableExtensions.length; i++) { 4399 for (var property in newVal) { 4400 if (newVal.hasOwnProperty(property) && !!newVal[property] 4401 && property == $scope.availableExtensions[i]['extensionName']) { 4402 addExtension(i); 4403 } 4404 } 4405 } 4406 }, true); 4407 4408 $scope.$watch('urlCategories', function (newVal, oldVal) { 4409 // set category filters whenever category URL parameters change 4410 4411 // reset everything, then set to what's currently in the URL 4412 $scope.clearCategories(); 4413 for (var i = 0; i < $scope.availableCategories.length; i++) { 4414 for (var property in newVal) { 4415 if (newVal.hasOwnProperty(property) && !!newVal[property] 4416 && property == $scope.availableCategories[i]['categoryCode']) { 4417 addCategory(i); 4418 } 4419 } 4420 } 4421 }, true); 4422 4423 $scope.$watch('filters.minPrice', function (newVal, oldVal) { 4424 if (!suppressNextSearch) { 4425 if (typeof (newVal) == "undefined") { 4426 $scope.filters.minPrice = oldVal; 4427 suppressNextSearch = true; 4428 } 4429 else if ($scope.isAfterMarketPage && !$scope.parameters.ownerGUID) { 4430 // filter changes need to re-trigger Aftermarket searches 4431 doDebouncedSearch(); 4432 } 4433 } 4434 else { 4435 suppressNextSearch = false; 4436 } 4437 }, true); 4438 4439 $scope.$watch('filters.maxPrice', function (newVal, oldVal) { 4440 if (!suppressNextSearch) { 4441 if (typeof (newVal) == "undefined") { 4442 $scope.filters.maxPrice = oldVal; 4443 suppressNextSearch = true; 4444 } 4445 else if ($scope.isAfterMarketPage && !$scope.parameters.ownerGUID) { 4446 // filter changes need to re-trigger Aftermarket searches 4447 doDebouncedSearch(); 4448 } 4449 } 4450 else { 4451 suppressNextSearch = false; 4452 } 4453 }, true); 4454 4455 // update search mode and bulk/normal search terms whenever either term changes 4456 $scope.$watch('parameters.domain', function (newVal, oldVal) { 4457 if (newVal != $scope.searchForm.searchTerm) { 4458 $scope.viewState.bulkMode = $scope.parameters.bulkMode; 4459 $scope.searchForm.searchTerm = newVal; 4460 $scope.searchForm.bulkSearchTerms = $scope.parameters.bulkSearchTerms; 4461 } 4462 }, true); 4463 4464 $scope.$watch('parameters.bulkSearchTerms', function (newVal, oldVal) { 4465 if (newVal != $scope.searchForm.bulkSearchTerms) { 4466 $scope.viewState.bulkMode = $scope.parameters.bulkMode; 4467 $scope.searchForm.searchTerm = $scope.parameters.domain; 4468 $scope.searchForm.bulkSearchTerms = newVal; 4469 } 4470 }, true); 4471 4472 $scope.$watch('filters.fastTransfersOnly', function (newVal, oldVal) { 4473 // filter changes need to re-trigger Aftermarket searches 4474 if ($scope.isAfterMarketPage && !$scope.parameters.ownerGUID) { 4475 doDebouncedSearch(); 4476 } 4477 }, true); 4478 4479 $rootScope.$on('BillPackageTopDomainNameList', function (event, BillPackageTopDomainNameList) { 4480 $scope.BillPackageTopDomainNameList = BillPackageTopDomainNameList; 4481 }); 4482 4483 $rootScope.$on('FreeDomainsFromWix', function (event, freeDomainsFromWix) { 4484 _.each($scope.domains, function (domain) { 4485 if (freeDomainsFromWix.includes(domain.data.name)) {
4486 domain.data.isWixFreeDomain = true; 4487 } 4488 else { 4489 domain.data.isWixFreeDomain = false; 4490 } 4491 }); 4492 }); 4493 4494 $scope.$on('HeaderSubmit', function (e, form) { 4495 $scope.searchForm.searchTerm = form.searchTerm; 4496 4497 // Must $apply so that the viewState bool gets updated in the digest cycle 4498 $scope.$apply(function() { $location.path("/") }); 4499 $scope.doDebouncedSearch(); 4500 }); 4501 4502 $scope.$on('ScrollTriggered', function () { 4503 if(numTimesScrolled < scrollLimit) $scope.$apply($scope.showMore); 4504 numTimesScrolled++; 4505 }); 4506 4507 /*********** PRIVATES ***********/ 4508 var showMoreSize = 25; 4509 var forceBulkMode = false; 4510 4511 // This can be changed to tweak how many offscreen results we're gonna search 4512 var searchBatchLength = 50; 4513 4514 function determineDomainSearchSource(domain) { 4515 if (domain === $scope.specificDomain) { 4516 return "Specific"; 4517 } 4518 if (domain.IsExactSuggestion) return "DomainSuggestions-Exact"; 4519 if (domain.isNameSuggestion) return "DomainSuggestions-Alternative"; 4520 4521 return "Standard"; 4522 } 4523 4524 function init() { 4525 GAService.GAProperties = { categoryName: "Domain Search", actionName: "Add or Remove From Cart" }; 4526 SelectDomainService.PackageSourceCode = "DomainSearch"; 4527 4528 // Grab all the domain parameters 4529 var paramDomains = LegacyParam.GetDomains(); 4530 if (paramDomains.length > 0) { 4531 var domainsToSearch=[]; 4532 for (var paramDomain in paramDomains) { 4533 domainsToSearch = domainsToSearch.concat(paramDomains[paramDomain].split(/(?:,| |\n|\t)+/)); 4534 } 4535 if (domainsToSearch.length > 1) { 4536 var bulkSearchTerms = ''; 4537 for (var domainToSearch in domainsToSearch) { 4538 bulkSearchTerms += domainsToSearch[domainToSearch] + '\n'; 4539 } 4540 $scope.parameters.bulkSearchTerms = bulkSearchTerms; 4541 $scope.parameters.bulkMode = true; 4542 } 4543 else { 4544 $scope.parameters.domain = domainsToSearch[0]; 4545 $scope.parameters.bulkMode = false; 4546 } 4547 } 4548 4549 // Get data from the injector 4550 $scope.isManagedCustomer = GenericInjector.PropBag.isManagedCustomer; 4551 4552 if (!!GenericInjector.PropBag.ownerUsername && GenericInjector.PropBag.ownerUsername != '') { 4553 $scope.viewState.ownerUsername = GenericInjector.PropBag.ownerUsername; 4554 } 4555 4556 $scope.availableCategories = _.sortBy(angular.copy(GenericInjector.PropBag.lsCategories), function(category) { 4557 return category.sortOrder; 4558 }); 4559 4560 $scope.availableExtensions = angular.copy(GenericInjector.PropBag.lsExtensionsFilter); 4561 4562 $scope.searchableExtensions = GenericInjector.PropBag.lsExtensions; 4563 4564 // available categories now loaded-- instantiate default values for all extension/category URL filters 4565 $scope.urlCategories = {}; 4566 $scope.urlExtensions = {}; 4567 4568 for (var i = 0; i < $scope.availableCategories.length; i++) { 4569 $scope.urlCategories[$scope.availableCategories[i]['categoryCode']] = false; 4570 } 4571 for (var i = 0; i < $scope.availableExtensions.length; i++) { 4572 $scope.urlExtensions[$scope.availableExtensions[i]['extensionName']] = false; 4573 } 4574 var acceptedParameters = Object.assign({}, $scope.parameters, $scope.filters, $scope.urlCategories, $scope.urlExtensions); 4575 4576 // bind extension/category URL filters to URL as parameters 4577 QueryStringFilterBinder.BindFilters($scope.parameters, false, {}, acceptedParameters); 4578 QueryStringFilterBinder.BindFilters($scope.filters, false, $scope.parameters, acceptedParameters); 4579 QueryStringFilterBinder.BindFilters($scope.urlCategories, false, $scope.parameters, acceptedParameters); 4580 QueryStringFilterBinder.BindFilters($scope.urlExtensions, false, $scope.parameters, acceptedParameters); 4581 4582 if (!!GenericInjector.PropBag.currentSearchTerm && (GenericInjector.PropBag.currentSearchTerm == $scope.parameters.domain || !$scope.parameters.domain)) { 4583 $scope.bFromOtherPage = true; 4584 4585 // See if we can split a multi-domain search passed from another page. 4586 var asSplitDomains = GenericInjector.PropBag.currentSearchTerm.trim().split(/[;,\n\t]+/); 4587 if (asSplitDomains.length > 1) { 4588 var bulkSearchTerms = ''; 4589 for (var domainToSearch in domainsToSearch) { 4590 if (domainsToSearch[domainToSearch].indexOf('.') == -1) { 4591 // No extension provided: add a .ca and .com by default: 4592 bulkSearchTerms += domainsToSearch[domainToSearch] + '.ca' + '\n'; 4593 bulkSearchTerms += domainsToSearch[domainToSearch] + '.com' + '\n'; 4594 } else { 4595 bulkSearchTerms += domainsToSearch[domainToSearch] + '\n'; 4596 } 4597 } 4598 $scope.parameters.bulkSearchTerms = bulkSearchTerms; 4599 $scope.searchForm.bulkSearchTerms = bulkSearchTerms; 4600 $scope.parameters.bulkMode = true; 4601 $scope.viewState.bulkMode = true; 4602 } 4603 4604 $scope.parameters.domain = GenericInjector.PropBag.currentSearchTerm; 4605 } 4606 4607 $timeout(function () { 4608 doDebouncedSearch($scope.bFromOtherPage); 4609 }); 4610 } 4611 4612 function GetDisplayedDomains() { 4613 return $scope.displayedDomains; 4614 } 4615 4616 function doSingleSearch() { 4617 if ((!$scope.searchForm.searchTerm || $scope.searchableExtensions.length < 1) && !$scope.parameters.ownerGUID) { 4618 $scope.viewState.hasSearched = false; 4619 $scope.domains = {}; 4620 DomainsDataModel.domains = {}; 4621 //cancel a search if there is one already 4622 DomainSearchManager.ResetSearch(); 4623 return; 4624 } 4625 4626 // Don't reset the ui if the search term has not changed 4627 if (DomainSearchManager.DomainStem + DomainSearchManager.Extension !== $scope.searchForm.searchTerm) { 4628 $scope.viewState.isLoading = true; 4629 $scope.specificDomain = undefined; 4630 $scope.viewState.resultsLimitLength = initialResultLength; 4631 $scope.suggestionsToShowStart = 0; 4632 $scope.suggestionsToShowEnd = 30; 4633 } 4634 4635 DomainsDataModel.Bundle = null; 4636 DomainsDataModel.BundlesProcessed = false; 4637 DomainsDataModel.lDomainBundles = $scope.lDomainBundles; 4638 4639 $scope.resultsLoading = true; 4640 4641 // Show an additional "please wait" message if the search takes a long time (over 3 seconds, usually only for AI searches.) 4642 $scope.bLongSearch = false; 4643 $timeout(function () { 4644 $scope.bLongSearch = DomainSearchManager.IsAISearch; 4645 $scope.sLongSearchWaitMessage = $scope.lsLongSearchWaitMessages[Math.floor(Math.random() * $scope.lsLongSearchWaitMessages.length)];
4646 }, 2000); 4647 4648 //Reset variables 4649 DomainsDataModel.domains = []; 4650 4651 $scope.domains = DomainsDataModel.domains; 4652 $scope.viewState.showingAlternatives = false; 4653 $scope.viewState.showPopularExtensionAlternatives = false; 4654 $scope.viewState.showNewExtensionAlternatives = false; 4655 4656 // Bundle Aftermarket filters 4657 var filteredExtensions =[]; 4658 for (var extension in $scope.extensions) { 4659 filteredExtensions.push($scope.extensions[extension].extensionName); 4660 } 4661 var filteredCategories =[]; 4662 for (var category in $scope.categories) { 4663 filteredCategories.push($scope.categories[category].categoryCode); 4664 } 4665 4666 var aftermarketOptions = { 4667 AfterMarketOnly: $scope.isAfterMarketPage, 4668 FastTransfersOnly: $scope.filters.fastTransfersOnly, 4669 MinPrice: $scope.filters.minPrice, 4670 MaxPrice: $scope.filters.maxPrice, 4671 Extensions: filteredExtensions, 4672 Categories: filteredCategories, 4673 OwnerGUID: $scope.parameters.ownerGUID 4674 } 4675 4676 $scope.viewState.IsAISearchLoading = !!$scope.filters.useAI; 4677 4678 var fAIDoneLoadingCallback = (!$scope.viewState.IsAISearchLoading ? null : function () { 4679 $scope.viewState.IsAISearchLoading = false; 4680 $scope.bLongSearch = false; 4681 }); 4682 4683 DomainSearchManager.StartSearch($scope.searchForm.searchTerm, $location.search().suggestedExtension, false, aftermarketOptions, null, GetDisplayedDomains, null, $scope.filters, fAIDoneLoadingCallback).then(function () { 4684 // Don't reset the specific domain if we already have one and it has a result 4685 // (case if specific extension research) 4686 var theirSpecific = DomainsDataModel.domains[DomainSearchManager.SpecificDomainIndex()]; 4687 if (!(!!$scope.specificDomain && !!$scope.specificDomain.data.result && !!theirSpecific && !theirSpecific.data.result)) { 4688 $scope.specificDomain = DomainsDataModel.domains[DomainSearchManager.SpecificDomainIndex()]; 4689 } 4690 $scope.Suggestions = DomainsDataModel.DomainSuggestions; 4691 4692 // Set search terms 4693 $scope.searchForm.searchTerm = DomainSearchManager.GetCompleteSearchTerm(); 4694 $scope.getCompleteSearchTerm = DomainSearchManager.GetCompleteSearchTerm; 4695 4696 // set URL filters for search mode/term once search has completed 4697 $scope.parameters.domain = $scope.searchForm.searchTerm; 4698 $scope.parameters.bulkMode = $scope.viewState.bulkMode; 4699 4700 $scope.viewState.isLoading = false; 4701 $scope.resultsLoading = false; 4702 $scope.viewState.hasSearched = true; 4703 4704 setUpPostSearchCallbacks(); 4705 }, 4706 function (data) { // if an error ocurred 4707 if (DomainSearchManager.searchingQ && data !== "Search cancelled") { 4708 DomainSearchManager.ResetSearch(); 4709 4710 $scope.viewState.isLoading = false; 4711 $scope.resultsLoading = false; 4712 $scope.viewState.hasSearched = false; 4713 } 4714 4715 if (data && !!data.userError) { 4716 WnErrorHandler.PushError(data.userError); 4717 } else { 4718 setUpPostSearchCallbacks(); 4719 } 4720 })['finally'](function () { 4721 $location.search('suggestedExtension', null) 4722 }); 4723 }; 4724 4725 function doBulkSearch() { 4726 DomainSearchManager.ResetSearch(); 4727 if ($scope.searchForm.bulkSearchTerms === "" || $scope.searchableExtensions.length < 1) { 4728 $scope.viewState.hasSearched = false; 4729 //cancel a search if there is one already 4730 $scope.domains.length = 0; 4731 DomainsDataModel.domains.length = 0; 4732 return; 4733 } 4734 4735 var domainsToSearch =[]; 4736 domainsToSearch = _.compact($scope.searchForm.bulkSearchTerms.split(/(?:,| |\n|\t)+/)); 4737 4738 domainsToSearch = _.map(domainsToSearch, function (tempDomain) { 4739 return tempDomain.trim().toLowerCase() }); 4740 4741 if (domainsToSearch.length < 2 && !forceBulkMode) { 4742 $scope.searchForm.searchTerm = domainsToSearch[0]; 4743 $scope.viewState.bulkMode = false; 4744 doSingleSearch(); 4745 return; 4746 } 4747 4748 if (!$scope.isAfterMarketPage) { 4749 initialResultLength = domainsToSearch.length; 4750 $scope.viewState.resultsLimitLength = initialResultLength; 4751 } 4752 4753 $scope.resultsLoading = true; 4754 4755 $scope.viewState.isLoading = true; 4756 $scope.viewState.hasSearched = true; 4757 4758 //Reset variables 4759 DomainsDataModel.domains =[]; 4760 $scope.domains = DomainsDataModel.domains; 4761 $scope.viewState.showingAlternatives = false; 4762 $scope.viewState.showPopularExtensionAlternatives = false; 4763 $scope.viewState.showNewExtensionAlternatives = false; 4764 4765 $scope.specificDomain = undefined; 4766 4767 if (domainsToSearch.length > iBulkSearchLimit) 4768 { 4769 WnErrorHandler.PushError('Please limit your search terms to ' +iBulkSearchLimit + ' or less.'); 4770 DomainSearchManager.ResetSearch(); 4771 setUpPostSearchCallbacks(); 4772 $scope.resultsLoading = false; 4773 $scope.viewState.hasSearched = true; 4774 $scope.viewState.isLoading = false; 4775 return; 4776 } 4777 4778 // Bundle Aftermarket filters 4779 var filteredExtensions =[]; 4780 for (var extension in $scope.extensions) { 4781 filteredExtensions.push($scope.extensions[extension].extensionName); 4782 } 4783 var filteredCategories =[]; 4784 for (var category in $scope.categories) { 4785 filteredCategories.push($scope.categories[category].categoryCode); 4786 } 4787 4788 var aftermarketOptions = { 4789 AfterMarketOnly: $scope.isAfterMarketPage, 4790 FastTransfersOnly: $scope.filters.fastTransfersOnly, 4791 MinPrice: $scope.filters.minPrice, 4792 MaxPrice: $scope.filters.maxPrice, 4793 Extensions: filteredExtensions, 4794 Categories: filteredCategories, 4795 bulkMode: $scope.viewState.bulkMode 4796 } 4797 4798 DomainSearchManager.GetSearchResults(_.first(domainsToSearch, searchBatchLength), domainsToSearch, true, true, true, aftermarketOptions, GetDisplayedDomains).then(function () { 4799 $scope.viewState.hasSearched = true; 4800 4801 // set URL filters for search mode/term once search has completed 4802 $scope.parameters.bulkSearchTerms = $scope.searchForm.bulkSearchTerms; 4803 $scope.parameters.bulkMode = $scope.viewState.bulkMode; 4804 }, 4805 function (data) { 4806 $log.debug(data); 4807 if (data && data.userError == 'Invalid Extension') { 4808 ModalService.InvalidExtensions.open(); 4809 } 4810 $scope.viewState.hasSearched = false; 4811 DomainSearchManager.ResetSearch(); 4812 })['finally'](function () { 4813 setUpPostSearchCallbacks(); 4814 $scope.viewState.isLoading = false; 4815 $scope.resultsLoading = false; 4816 }); 4817 } 4818 4819 var doDebouncedSearch = function (bFromOtherPage) { 4820 if ((!$scope.viewState.bulkMode && typeof $scope.searchForm.searchTerm != 'undefined') || 4821 ($scope.viewState.bulkMode && typeof $scope.searchForm.bulkSearchTerms != 'undefined') || 4822 $scope.viewState.ownerUsername) 4823 { 4824 doSearch(bFromOtherPage); 4825 } 4826 } 4827 doDebouncedSearch = _.debounce(doDebouncedSearch, 500); 4828 4829 function setUpPostSearchCallbacks() { 4830 if (!DomainSearchManager || !DomainSearchManager.SearchingPromise) return; 4831 // set up callback to handle further domain search errors 4832 DomainSearchManager.SearchingPromise.then(null, 4833 function (data) { 4834 if (data === "Search cancelled" || data === "Timed out") return; 4835 if (data && !!data.userError) WnErrorHandler.PushError(data.userError); 4836 $log.error("Domain search error occurred!!", data); 4837 }); 4838 } 4839 4840 /*********** BACKERS ************/ 4841 function doSearch(bFromOtherPage) { 4842 initialResultLength = defaultInitialResultLength; 4843 WnErrorHandler.ClearAll(); 4844 4845 if (!bFromOtherPage) { 4846 $scope.bFromOtherPage = false; 4847 } 4848 4849 numTimesScrolled = 0; 4850 4851 if ($scope.viewState.bulkMode) { 4852 doBulkSearch(); 4853 } else { 4854 doSingleSearch(); 4855 } 4856 }; 4857 4858 function showMore() { 4859 if ($scope.viewState.resultsLimitLength < $scope.filteredDomains.length) $scope.viewState.resultsLimitLength += showMoreSize; 4860 $timeout(function() { 4861 DomainSearchManager.CheckIfDomainsNeedResults($scope.displayedDomains, $scope.filteredDomains, setUpPostSearchCallbacks, searchBatchLength); 4862 }); 4863 } 4864 4865 function toggleCategoryCollapse() { 4866 if ($scope.categoryFilters.showingMore) { 4867 $scope.categoryFilters.showingMore = false; 4868 $scope.categoryFilters.categoryLimit = 5; 4869 } else { 4870 $scope.categoryFilters.showingMore = true; 4871 $scope.categoryFilters.categoryLimit = $scope.availableCategories.length; 4872 } 4873 } 4874 4875 function addCategory(categoryIndex) { 4876 if ($scope.viewState.isLoading) return; 4877 $scope.viewState.resultsLimitLength = initialResultLength; 4878 4879 $scope.categories.push($scope.availableCategories.splice(categoryIndex, 1)[0]); 4880 4881 // Show all categories, if we have added one 4882 $scope.showingMore = true; 4883 $scope.categoryLimit = $scope.availableCategories.length; 4884 4885 $scope.categories = _.sortBy($scope.categories, function (category) { 4886 return category.sortOrder; 4887 }); 4888 } 4889 4890 function removeCategory(index) { 4891 if ($scope.viewState.isLoading) return; 4892 $scope.viewState.resultsLimitLength = initialResultLength; 4893 $scope.availableCategories.push($scope.categories.splice(index, 1)[0]); 4894 4895 $scope.availableCategories = _.sortBy($scope.availableCategories, function(category) { 4896 return category.sortOrder; 4897 }); 4898 } 4899 4900 function clearCategories() { 4901 if ($scope.viewState.isLoading) return; 4902 $scope.viewState.resultsLimitLength = initialResultLength; 4903 4904 $scope.categories =[]; 4905 $scope.availableCategories = _.sortBy(angular.copy(GenericInjector.PropBag.lsCategories), function (category) { 4906 return category.sortOrder; 4907 }); 4908 if ($scope.showingMore) { 4909 $scope.categoryLimit = $scope.availableCategories.length; 4910 } 4911 } 4912 4913 function toggleExtensionCollapse() { 4914 if ($scope.extensionFilters.showingMore) { 4915 $scope.extensionFilters.showingMore = false; 4916 $scope.extensionFilters.extensionLimit = 10; 4917 } else { 4918 $scope.extensionFilters.showingMore = true; 4919 $scope.extensionFilters.extensionLimit = $scope.availableExtensions.length; 4920 } 4921 } 4922 4923 function addExtension(extensionIndex) { 4924 if ($scope.viewState.isLoading) return; 4925 $scope.viewState.resultsLimitLength = initialResultLength; 4926 4927 $scope.extensions.push($scope.availableExtensions[extensionIndex]); 4928 $scope.availableExtensions[extensionIndex]['visible']= false; 4929 4930 // Show all extensions, if we have added one 4931 $scope.showingMore = true; 4932 $scope.extensionLimit = $scope.availableExtensions.length; 4933 } 4934 4935 function removeExtension(index) { 4936 if ($scope.viewState.isLoading) return; 4937 $scope.viewState.resultsLimitLength = initialResultLength; 4938 4939 var originalIndex = $scope.availableExtensions.findIndex(function (o) { return o.extensionName == $scope.extensions[index].extensionName; }) 4940 $scope.availableExtensions[originalIndex]['visible']= true; 4941 $scope.extensions.splice(index, 1)[0]; 4942 } 4943 4944 function clearExtensions() { 4945 if ($scope.viewState.isLoading) return; 4946 $scope.viewState.resultsLimitLength = initialResultLength; 4947 $scope.extensions =[]; 4948 4949 $scope.availableExtensions = _.sortBy(angular.copy(GenericInjector.PropBag.lsExtensionsFilter), function (extension) { 4950 return !extension.isPopular; 4951 }); 4952 4953 if ($scope.showingMore) { 4954 $scope.extensionLimit = $scope.availableExtensions.length; 4955 } 4956 } 4957 4958 function saveShare() { 4959 ModalService.SaveShare.open($scope.displayedDomains); 4960 } 4961 4962 function litesiteLearnMore() { 4963 ModalService.LitesiteLearnMore.open($scope.searchForm.searchTerm, ($scope.specificDomain || null)); 4964 } 4965 4966 function WixPreview(oDomain) { 4967 WixPreviewModalService.WixPreview.open(null, oDomain.data.name); 4968 } 4969 4970 // user clicked 'more like this' link-- do a non-bulk search of the alternative 4971 function searchAlternative(alternative) { 4972 $scope.searchForm.searchTerm = alternative; 4973 if ($scope.viewState.bulkMode) {
4974 $scope.toggleBulkMode(); 4975 } 4976 doSingleSearch(); 4977 } 4978 4979 $scope.EnableDisableAISearch = function EnableDisableAISearch() { 4980 DomainSearchServices.EnableDisableAISearch($scope.filters.useAI); 4981 DomainSearchManager.ResetSearch(); 4982 doSearch(); 4983 } 4984 4985 function removeAllFilters() { 4986 if($scope.extensions.length > 0) $scope.clearExtensions(); 4987 if($scope.categories.length > 0) $scope.clearCategories(); 4988 if ($scope.isAfterMarketPage) { 4989 $scope.filters.availableNow = false; 4990 $scope.filters.upcoming = false; 4991 $scope.filters.unavailable = false; 4992 $scope.filters.preReg = false; 4993 $scope.filters.forSale = true; 4994 $scope.filters.fastTransfersOnly = false; 4995 $scope.filters.onPromo = true; 4996 $scope.filters.bApplyFilters = false; 4997 } 4998 else { 4999 $scope.filters.availableNow = true; 5000 $scope.filters.upcoming = true; 5001 $scope.filters.unavailable = true; 5002 $scope.filters.preReg = true; 5003 $scope.filters.forSale = true; 5004 $scope.filters.fastTransfersOnly = false; 5005 $scope.filters.onPromo = true; 5006 $scope.filters.bApplyFilters = false; 5007 } 5008 } 5009 5010 function addBundle(oBundle) { 5011 for (const oDomain of oBundle.Domains) { 5012 var oPromise = $q.resolve(); 5013 5014 if (oDomain.data.inCart) { 5015 // Domain already in cart: remove first to ensure bundle price is used. (Yes, this unfortunately loses any package config/addons.) 5016 oPromise = $scope.removeDomain(oDomain); 5017 } 5018 5019 oPromise.then(() => { 5020 oDomain.data.BundlePromoCode = oDomain.PromoCode; 5021 $scope.selectDomain(oDomain); 5022 }); 5023 } 5024 5025 oBundle.Added = true; 5026 } 5027 5028 function removeBundle(oBundle) { 5029 for (const oDomain of oBundle.Domains) { 5030 if (oDomain.data.inCart) { 5031 $scope.removeDomain(oDomain); 5032 } 5033 } 5034 oBundle.Added = false; 5035 } 5036 5037 // Initialize all required values (GET json data, parse url params) 5038 init(); 5039 } 5040 ]); 5041})(); 5042(function() { 5043 5044 angular.module("DomainSearchApp") 5045 .directive("scrollTrigger", ["$window", "$document", function ($window, $document) { 5046 return { 5047 restrict: 'A', 5048 replace: true, 5049 link: function (scope, elem) { 5050 if ($window.addEventListener) { 5051 $window.addEventListener("scroll", sendBroadcast); 5052 } else { 5053 $window.attachEvent("onscroll", sendBroadcast); 5054 } 5055 5056 function sendBroadcast() { 5057 if (isElementInViewport(elem)) scope.$broadcast("ScrollTriggered"); 5058 } 5059 5060 function isElementInViewport(el) { 5061 var rect = $(el)[0].getBoundingClientRect(); 5062 5063 return ( 5064 !!(rect.top || rect.left || rect.bottom || rect.right) && ( // if it's all zero, then it's not visible 5065 rect.top >= 0 && 5066 rect.left >= 0 && 5067 rect.bottom <= ($window.innerHeight || $document.documentElement.clientHeight) && 5068 rect.right <= ($window.innerWidth || $document.documentElement.clientWidth)) 5069 ); 5070 } 5071 } 5072 } 5073 }]); 5074 5075})(); 5076/* 5077 Domain Search Header Search Ninja 5078 -------------------------------- 5079 Service to allow for two way binding with the header search form 5080 which is outside of our Angular app. 5081 5082 Dependency: jQuery 5083 5084 Author: Akos Sebestyen 2015 5085*/ 5086(function () { 5087 "use strict"; 5088 5089 angular.module('DomainSearchApp') 5090 5091 .service('HeaderSearchNinja', ['$rootScope', '$log', headerSearchNinja]); 5092 5093 function headerSearchNinja($rootScope, $log) { 5094 var model = this; 5095 5096 // Two way bindable data model for the search box 5097 model.Form = {}; 5098 5099 var searchElem = $('#headersearchform .domain-search-header'); 5100 var formElem = $('#headersearchform'); 5101 5102 // Trigger a digest cycle to update our Angular components 5103 // and to allow custom watchers to update search box 5104 //searchElem.keyup(function(e) { 5105 // $rootScope.$digest(); 5106 //}); 5107 5108 // Broadcast an event when the for msubmits 5109 formElem.submit(function(e) { 5110 //$rootScope.$digest(); 5111 e.preventDefault(); 5112 model.Form.searchTerm = searchElem.val(); 5113 $rootScope.$broadcast('HeaderSubmit', model.Form); 5114 }); 5115 5116 // Use watchers to synchronize 'bind our model' to the search form 5117 //$rootScope.$watch(function() { 5118 // return model.Form.searchTerm; 5119 // }, 5120 // function(newVal, oldVal) { 5121 // if (newVal !== oldVal) { 5122 // searchElem.val(newVal); 5123 // } 5124 // });
5125 5126 //$rootScope.$watch(function () { 5127 // return searchElem.val(); 5128 // }, 5129 // function (newVal, oldVal) { 5130 // if (newVal !== oldVal) model.Form.searchTerm = newVal; 5131 // }, true); 5132 } 5133})(); 5134/* 5135 Domain Search Filters 5136 -------------------------------- 5137 Handles filtering for the repeater on domain search. 5138 5139*/ 5140 5141(function () { 5142 "use strict"; 5143 5144 angular.module("DomainSearchApp") 5145 // Takes a filter object from domain search 5146 // and filters the domain array it's repeating on based on certain 5147 // rules 5148 .filter('domainAvailabilityFilter', [domainAvailabilityFilter]) 5149 5150 // Filters the categories based on the selected category objects 5151 .filter('domainCategoryFilter', [domainCategoryFilter]) 5152 .filter('domainExtensionFilter', [domainExtensionFilter]) 5153 .filter('domainPriceFilter', [domainPriceFilter]) 5154 5155 // Sorts the domain search on Popularity or Price (for now) 5156 // Remark: also puts an optional specific domain at the top of the list 5157 // when ordered by popularity 5158 .filter('domainSort', ['$filter', '$location', domainSort]) 5159 5160 .filter('domainSuggestions', [domainSuggestions]) 5161 // DR5217 - Applying a default filter if filters are not selected 5162 .filter('defaultFilter', [defaultFilter]) 5163 .filter('nameSuggestionsLimit', [nameSuggestionsLimit]); 5164 5165 function domainAvailabilityFilter() { 5166 return function (domains, filterPredicates, isAfterMarketPage, ownerGUID) { 5167 5168 if (!_.isObject(filterPredicates) || !_.isArray(domains) || !filterPredicates.bApplyFilters) { 5169 return domains; 5170 } 5171 5172 domains = _.filter(domains, function (domain) { 5173 // Short circuit the check if nothing is checked: shouldn't waste searching power 5174 if (!filterPredicates.availableNow && !filterPredicates.upcoming && !filterPredicates.unavailable && !filterPredicates.preReg && !filterPredicates.forSale) { 5175 return false; 5176 } 5177 5178 // Short circuit if the only thing checked is pre reg, we should have all these results 5179 if (filterPredicates.preReg && 5180 !filterPredicates.availableNow && 5181 !filterPredicates.upcoming && 5182 !filterPredicates.forSale && 5183 !filterPredicates.unavailable && 5184 !filterPredicates.onPromo) { 5185 if (!domain.data.result) { 5186 return false; 5187 } else if (domain.data.result === "PRE_REGISTER") { 5188 return true; 5189 } else { 5190 return false; 5191 } 5192 } else { 5193 //The following checks need results, show them if we don't have it 5194 if (!domain.data.result) { 5195 return true; 5196 } 5197 5198 if (!filterPredicates.preReg && ( 5199 domain.data.result === "PRE_REGISTER")) { 5200 return false; 5201 } 5202 5203 if (!filterPredicates.availableNow && ( 5204 domain.data.result === "AVAIL" || 5205 domain.data.result === "AVAIL_P" || 5206 domain.data.result === "DPML_BLOCK")) { 5207 return false; 5208 } 5209 5210 if (!filterPredicates.forSale && (domain.data.result === "FORSALE" || domain.data.result === "FORSALEOFFER")) { 5211 return false; 5212 } 5213 5214 if ((!isAfterMarketPage || ownerGUID) && (domain.data.result === "FORSALE" || domain.data.result === "FORSALEOFFER") && filterPredicates.fastTransfersOnly && !domain.data.isFastTransfer) { 5215 return false; 5216 } 5217 5218 if (!filterPredicates.upcoming && ( 5219 domain.data.result === "PREBOOK" || 5220 domain.data.result === "BACKORDER" 5221 )) { 5222 return false; 5223 } 5224 5225 if (!filterPredicates.upcoming && !filterPredicates.availableNow && 5226 domain.data.result === "AVAIL_S" 5227 ) { 5228 return false; 5229 } 5230 5231 if (!filterPredicates.unavailable && ( 5232 domain.data.result === "UNAVA" || 5233 domain.data.result === "UNAVA_P" || 5234 domain.data.result === "INVAL_P" || 5235 domain.data.result === "RESV" || 5236 domain.data.result === "ERROR" || 5237 domain.data.result.indexOf("INVAL") > -1)) { 5238 return false; 5239 } 5240 5241 if (!filterPredicates.onPromo && domain.data.bPromoPrice) { 5242 return false; 5243 } 5244 5245 return true; 5246 } 5247 }); 5248 5249 5250 return domains; 5251 } 5252 } 5253 5254 /* 5255 * Do not show unavailable results, UNLESS: 5256 The result is for a TLD that was explicitly searched for. E.g.: 5257 If user searches for "googling", don't show "googling.ca" in the results, since it's unavailable, but do show "googling.shop", since the .shop is available. 5258 If user searches for "googling.ca", show "googling.ca" in the specific result even though it's Taken. 5259 The result is for an Aftermarket Buy it Now Fast Transfer domain under $1000 5260 */ 5261 function defaultFilter() { 5262 return function (domains, filters, isAfterMarketPage, searchTerm) { 5263 5264 if (filters.bApplyFilters || !_.isArray(domains) || isAfterMarketPage) return domains; 5265
5266 domains = _.filter(domains, function (domain) { 5267 5268 if (!!domain.data.result && !filters.bApplyFilters && (domain.data.result === "UNAVA" || 5269 domain.data.result === "UNAVA_P" || 5270 domain.data.result === "INVAL_P" || 5271 domain.data.result === "RESV" || 5272 domain.data.result === "ERROR" || 5273 domain.data.result.indexOf("INVAL") > -1) && 5274 domain.data.name !== searchTerm) { 5275 return false; 5276 } 5277 5278 if (!!domain.data.result && !filters.bApplyFilters && (domain.data.isFastTransfer && domain.data.price > 1000)) 5279 return false; 5280 5281 return true; 5282 }); 5283 5284 if (!!searchTerm) { 5285 var index = _.findIndex(domains, function (domain) { return domain.data.name === searchTerm }); 5286 5287 var bContinue = true; 5288 if (domains.length < 1 || index < 0) { 5289 bContinue = false; 5290 } 5291 5292 if (bContinue) { 5293 domains.unshift(domains[index]); 5294 domains.splice(index + 1, 1); 5295 } 5296 } 5297 5298 return domains; 5299 } 5300 } 5301 5302 function domainCategoryFilter() { 5303 return function (domains, filters, categories, hasSearched, specificDomain, isAfterMarketPage, ownerGUID) { 5304 if (!filters.bApplyFilters || categories.length === 0 || !hasSearched || isAfterMarketPage && !ownerGUID) return domains; 5305 5306 var tempDomains = _.filter(domains, function (domain) { 5307 return _.findIndex(categories, function (category) { 5308 return category.categoryCode === domain.data.extensionCategory; 5309 }) > -1; 5310 }); 5311 5312 // add the specific domain if it's not in our list 5313 if (!!specificDomain && _.findIndex(tempDomains, function(domain) { 5314 return domain.data.name === specificDomain.data.name; 5315 }) < 0) { 5316 tempDomains.push(specificDomain); 5317 } 5318 return tempDomains; 5319 } 5320 } 5321 5322 function domainExtensionFilter() { 5323 return function (domains, filters, extensions, hasSearched, specificDomain, isAfterMarketPage, ownerGUID) { 5324 if (!filters.bApplyFilters || extensions.length === 0 || !hasSearched || isAfterMarketPage && !ownerGUID) return domains; 5325 5326 var tempDomains = _.filter(domains, function (domain) { 5327 return _.findIndex(extensions, function (extension) { 5328 return extension.extensionName === domain.data.ext; 5329 }) > -1; 5330 }); 5331 5332 // add the specific domain if it's not in our list 5333 if (!!specificDomain && _.findIndex(tempDomains, function (domain) { 5334 return domain.data.name === specificDomain.data.name; 5335 }) < 0) { 5336 tempDomains.push(specificDomain); 5337 } 5338 return tempDomains; 5339 } 5340 } 5341 5342 function domainPriceFilter() { 5343 return function (domains, filters, minPrice, maxPrice, isAfterMarketPage, ownerGUID) { 5344 if (!filters.bApplyFilters) return domains; 5345 if (isAfterMarketPage && !ownerGUID) return domains; 5346 domains = _.filter(domains, function (domain) { 5347 if (domain.data.price >= minPrice && domain.data.price <= maxPrice) 5348 { 5349 return true; 5350 } 5351 else 5352 { 5353 return false; 5354 } 5355 }); 5356 return domains; 5357 } 5358 } 5359 5360 function domainSort($filter, $location) { 5361 return function (domains, sortOption, specificDomain, showingAlternatives, showPopularExtensionAlternatives, showNewExtensionAlternatives) { 5362 if (!_.isArray(domains)) return domains; 5363 5364 switch (sortOption) {
5365 case 'Price: Low to High': 5366 domains = $filter('orderBy')(domains, [getPriceSortOrder, 'data.ext']); 5367 break; 5368 case 'Price: High to Low': 5369 domains = $filter('orderBy')(domains, [getReversePriceSortOrder, 'data.ext'], true); 5370 break; 5371 case 'Extension: A to Z': 5372 domains = $filter('orderBy')(domains, ['data.ext', 'data.name'], false); 5373 break; 5374 case 'Extension: Z to A': 5375 domains = $filter('orderBy')(domains, ['data.ext', 'data.name'], true); 5376 break; 5377 case 'Name: A to Z': 5378 domains = $filter('orderBy')(domains, 'data.name', false); 5379 break; 5380 case 'Name: Z to A': 5381 domains = $filter('orderBy')(domains, 'data.name', true); 5382 break; 5383 case 'Search Term': 5384 domains = $filter('orderBy')(domains, 'data.searchTermIndex', false); 5385 break; 5386 case 'Popularity': 5387 if (showingAlternatives || showPopularExtensionAlternatives || showNewExtensionAlternatives) { 5388 domains = $filter('orderBy')(domains, [preRegLast, confidenceScore, 'SortOrder'], true); 5389 } else { 5390 domains = $filter('orderBy')(domains, 'SortOrder'); 5391 } 5392 5393 if (!!specificDomain) { 5394 var index = _.findIndex(domains, function (tempDomain) { return tempDomain.data.name === specificDomain.data.name }); 5395 5396 if (domains.length < 1 || index < 0 || $location.search().testing) { 5397 break; 5398 } 5399 5400 domains.unshift(specificDomain); 5401 domains.splice(index + 1, 1); 5402 } 5403 break; 5404 } 5405 5406 return domains; 5407 } 5408 } 5409 5410 function domainSuggestions() { 5411 return function (domains, showSuggestions, showPopularExtensionAlternatives, showNewExtensionAlternatives, isAfterMarketPage, ownerGUID) { 5412 return _.filter(domains, function (domain) { 5413 if (isAfterMarketPage && !ownerGUID) return true; 5414 5415 if (!!showSuggestions && !showPopularExtensionAlternatives && !showNewExtensionAlternatives) return !!domain.IsAlternativeSuggestion; 5416 if (!!showSuggestions && !!showPopularExtensionAlternatives || !!showNewExtensionAlternatives) return !!domain.IsAlternativeSuggestion; 5417 return !domain.IsAlternativeSuggestion; 5418 }); 5419 } 5420 } 5421 function nameSuggestionsLimit() { 5422 return function (domains) { 5423 var iTotal = 1; 5424 return _.filter(domains, function (domain) { 5425 if (!!domain.isNameSuggestion && iTotal <= 3) { 5426 iTotal++; 5427 return true; 5428 } else if (!(!!domain.isNameSuggestion)) { 5429 return true; 5430 } 5431 5432 return false; 5433 }); 5434 } 5435 } 5436 // PRIVATES 5437 function getPriceSortOrder(domain) { 5438 if (!!domain.data.price) { 5439 return domain.data.price; 5440 } else { 5441 return 99999; 5442 } 5443 } 5444 5445 function getReversePriceSortOrder(domain) { 5446 if (!!domain.data.price) { 5447 return domain.data.price; 5448 } else { 5449 return 0; 5450 } 5451 } 5452 5453 function preRegLast(domain) { 5454 if (domain.data.result === "PRE_REGISTER") return 0; 5455 return 1; 5456 } 5457 5458 function confidenceScore(domain) { 5459 if (!!domain.DomainsBotSuggestion) return domain.DomainsBotSuggestion.ConfidenceScore; 5460 return 0; 5461 } 5462})(); 5463 5464(function () { 5465 "use strict"; 5466 angular.module('WixPreviewModal', []) 5467 .service('WixPreviewModalService', ['$uibModal', '$http', function ($uibModal, $http) { 5468 var service = this; 5469 service.WixPreview = { 5470 open: openWixPreview 5471 } 5472 function openWixPreview(sWixPreviewUrl, sDomainNameSearch, sDomainNameSelect) { 5473 return $uibModal.open({ 5474 templateUrl: 'ModalWixPreview.cshtml', 5475 controller: [ 5476 '$scope', '$uibModalInstance', '$log', '$sce', function ($scope, $uibModalInstance, $log, $sce) { 5477 5478 var sDomainName = sDomainNameSearch ?? sDomainNameSelect; 5479 if (!(!!sDomainName)) { 5480 sDomainName = ""; 5481 } 5482 5483 if (!(!!sWixPreviewUrl)) { 5484 $http.get(window._SmartURL + "Helper/GetWixPreviewUrl?sDomainName=" + sDomainName).then(function (resp) { 5485 $scope.sWixPreviewUrl = $sce.trustAsResourceUrl(resp.data); 5486 }); 5487 } else { 5488 $scope.sWixPreviewUrl = $sce.trustAsResourceUrl(sWixPreviewUrl); 5489 } 5490 5491 $scope.ok = function () { 5492 $uibModalInstance.close(true); 5493 }; 5494 $scope.cancel = function () { 5495 $uibModalInstance.dismiss('cancel'); 5496 }; 5497 } 5498 ], 5499 size: 'full',
5500 windowClass: 'modal--wix-preview', 5501 resolve: {} 5502 }); 5503 } 5504 }]); 5505})();
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.