PageSourceSearch

https://www.webnames.ca/jsbundles/domainsearch?v=MgVB-LITGfqy0rOmD1vzkcfKun8x6TU5_BvUF5HRmtQ1

js webnames.ca collected 2026-10-02 02:54:20 UTC 237,247 bytes, 5,505 lines download raw bytes

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>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</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>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
613&lt;/div&gt;</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>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</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("&lt;p style=\"color:blue\"&gt;an html\n" +
642              "&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
643              "snippet&lt;/p&gt;");
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 &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
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, '&amp;').
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, '&lt;').
1096      replace(/>/g, '&gt;');
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>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</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>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_blank'"&gt;<br>&lt;/div&gt;</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>&lt;div ng-bind-html="snippetWithSingleURL | linky:'_self':{rel: 'nofollow'}"&gt;<br>&lt;/div&gt;</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>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</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, '&quot;'),
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.