PageSourceSearch

https://mrnews.ca/shout/web/v6.51.1.R20260831/shout/resources/js/shout.js

js mrnews.ca collected 2026-09-27 09:09:06 UTC 126,378 bytes, 4,153 lines download raw bytes

1(function($, SystemJS, _) {
2
3// Depends on php.js for http_build_query
4
5// Prevent re-initialization
6if (typeof window.Shout != 'undefined')
7{
8    if (typeof window.ShoutConfiguration != 'undefined')
9    {
10        window.Shout.configuration = window.ShoutConfiguration.configuration;
11    }
12
13    return;
14}
15// From jQuery
16// A simple way to check for HTML strings or ID strings
17// (both of which we optimize for)
18var quickExpr = /^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,
19
20// Is it a simple selector
21    isSimple = /^.[^:#\[\.]*$/,
22
23// IsPHPArray
24    phpVariable = /([^\[\]]*)(\[.*\])*/;
25
26var objectCache = [];
27var elementCache = [];
28
29var _maxZ = 1000;
30var _nextId = 0;
31var dropArray = {};
32
33var Shout = window.Shout = function(selector)
34{
35    return new Shout.fn.init(selector);
36};
37
38if(window.console === undefined)
39{
40    window.console = {
41        log:function(){
42            return true;
43        },
44        debug:function(){
45            return true;
46        },
47        info:function(){
48            return true;
49        }
50    };
51}
52
53Shout.fn = Shout.prototype = {
54    nextId: function()
55    {
56        return 'Shout-Gen-' + _nextId++;
57    },
58    init: function(selector) {
59
60        if(!selector)
61        {
62            return this;
63        }
64        else if(selector && selector.nodeEl)
65        {
66            return selector;
67        }
68        else if ( typeof selector == "string" )
69        {
70            // Use jquery expression to determine if we've got
71            // and id string.
72
73            var match = quickExpr.exec( selector );
74
75            if(match && match[3] !== undefined)
76            {
77                if(objectCache[match[3]])
78                {
79                    return objectCache[match[3]];
80                }
81                else
82                {
83                    objectCache[match[3]] = this;
84                }
85            }
86            this.nodeEl = $(selector);
87        }
88        else if(selector)
89        {
90            this.nodeEl = $(selector);
91        }
92        else
93        {
94
95        }
96
97        this.attachParams = null;
98        this.inherittedMove = false;
99        this.shoutId = '';
100        this.attachments = [];
101        this.constraints = {};
102
103        this.dragProxy = null;
104        this.dropProxy = null;
105
106        this.shoutId = this.nextId();
107
108        this.nodeEl.data('shoutObj',this);
109
110        if (typeof window.ShoutConfiguration != 'undefined')
111        {
112            this.configuration = window.ShoutConfiguration.configuration;
113        }
114
115        return this;
116
117    }
118};
119
120// Give the init function the Shout prototype
121
122Shout.fn.init.prototype = Shout.fn;
123
124function _tooltipNormalizeConfig(config)
125{
126    var default_driver;
127
128    if (window.tippy)
129    {
130        default_driver = 'tippy';
131    }
132    else if ($.fn.qtip)
133    {
134        default_driver = 'qtip';
135    }
136    else
137    {
138        throw 'no valid tooltip drivers';
139    }
140
141    var default_config = {
142        driver: default_driver,
143        config: 'sui-tooltip-light',
144        selector_mode: 'jquery',
145        //please limit the use of the driver_config, since it causes us to have less flexibility in changing between tooltip drivers
146        driver_config: {
147            tippy: {
148            },
149            qtip: {
150            },
151        }
152    };
153
154    if (typeof config === 'string')
155    {
156        config = $.extend(default_config, {config: config});
157    }
158    else if (typeof config === 'object')
159    {
160        config = $.extend(default_config, config);
161    }
162    else
163    {
164        config = default_config;
165    }
166
167    var valid_drivers = ['qtip', 'tippy'];
168    if (typeof config.driver !== 'string' || -1 === _.indexOf(valid_drivers, config.driver))
169    {
170        throw 'invalid tooltip driver. Should be one of: '+JSON.stringify(valid_drivers);
171    }
172
173    var valid_configs = ['', 'sui-tooltip-light', 'sui-tooltip-dark', 'sui-sf-tooltip-light', 'sui-sf-tooltip-dark', 'sui-tooltip-interactive', 'sui-tooltip-has_arrow'];
174    if (typeof config.config !== 'string' || _.difference(_.split(config.config, ' '), valid_configs).length > 0)
175    {
176        throw 'invalid tooltip config. Should be combination of: '+JSON.stringify(valid_configs);
177    }
178
179    return config;
180}
181
182// private to Shout obj
183function _tooltip(el, content, config)
184{
185    $el = $(el);
186
187    config = _tooltipNormalizeConfig(config);
188
189    if ('qtip' === config.driver && $.fn.qtip)
190    {
191        var args = {};
192        var default_args = {
193            content: {
194                'text' : ''
195            },
196            position:{
197                corner: {
198                    target: 'topRight',
199                    tooltip: 'bottomLeft'
200                },
201                adjust: {
202                    'screen':true
203                }
204            },
205            show: {
206                delay: 0
207            },
208            hide: {
209                delay: 200,
210                fixed: true
211            },
212            style : {
213                classes: 'qtip-light',
214                width:300,
215                tip: false
216            }
217        };
218
219        switch (true)
220        {
221            case 'sui-tooltip-dark' === config.config:
222                var config_args = {
223                    style: {
224                        classes: 'qtip-dark',
225                    }
226                };
227                default_args = $.extend(default_args, config_args);
228                break;
229
230            case 'sui-sf-tooltip-light' === config.config:
231                break;
232
233            case 'sui-sf-tooltip-dark' === config.config:
234                break;
235
236            case 'sui-tooltip-light' === config.config:
237                break;
238
239            case 'sui-tooltip-has_arrow' === config.config:
240                break;
241
242            default:
243                throw 'unknown tooltip config';
244        }
245
246        if (typeof content === 'object')
247        {
248            args = content;
249        }
250        else if (typeof content === 'string' || typeof content === 'function')
251        {
252            args = {
253                content: {
254                    'text' : content
255                }
256            };
257        }
258        else
259        {
260            throw 'unable to determine tooltip config';
261        }
262
263        args = $.extend(default_args, args);
264
265        $el.qtip(args);
266
267        return;
268    }
269    else if ('tippy' === config.driver && window.tippy)
270    {
271        var $tippyroot = $('> .sui-tippy', document.body);
272
273        if (0 === $tippyroot.length)
274        {
275            $tippyroot = $('<div class="sui-tippy"></div>');
276            $(document.body).append($tippyroot);
277        }
278
279        var args;
280        var default_args = {
281            content: content,
282            duration: 0,
283            theme: 'light sui-tippyshout',
284            // placement: 'top-start',
285            appendTo: $tippyroot.get(0),
286            arrow: false,
287            allowHTML: true
288        };
289
290        config_strings = _.split(config.config, ' ');
291
292        _.forEach(config_strings, function(config_string) {
293
294            switch (true)
295            {
296                case 'sui-tooltip-light' === config_string:
297                    break;
298
299                case 'sui-tooltip-dark' === config_string:
300                    var config_args = {
301                        theme: 'dark sui-tippyshout'
302                    };
303                    default_args = $.extend(default_args, config_args);
304                    break;
305
306                case 'sui-sf-tooltip-dark' === config_string:
307                    var config_args = {
308                        theme: 'sui-sf-tooltip dark sui-sf-tooltip-dark sui-sf-tooltip-leftaligned'
309                    };
310                    default_args = $.extend(default_args, config_args);
311                    break;
312
313                case 'sui-sf-tooltip-light' === config_string:
314                    var config_args = {
315                        theme: 'sui-sf-tooltip light sui-sf-tooltip-light sui-sf-tooltip-leftaligned'
316                    };
317                    default_args = $.extend(default_args, config_args);
318                    break;
319
320                case 'sui-tooltip-interactive' === config_string:
321                    var config_args = {
322                        interactive: true
323                    };
324                    default_args = $.extend(default_args, config_args);
325                    break;
326
327                case 'sui-tooltip-has_arrow' === config_string:
328                    var config_args = {
329                        arrow: true
330                    };
331                    default_args = $.extend(default_args, config_args);
332                    break;
333
334                default:
335                    throw 'unknown tooltip config';
336            }
337
338        });
339
340        if (config.driver_config && config.driver_config[config.driver] && Object.keys(config.driver_config[config.driver]).length)
341        {
342            args = $.extend(default_args, config.driver_config[config.driver]);
343        }
344
345        if (false && typeof result === 'object' && typeof result.then === 'function')
346        {
347            throw 'TODO: promise implementation';
348        }
349        else if (typeof content === 'object')
350        {
351            args = content;
352        }
353        else if (typeof content === 'string')
354        {
355            args = {
356                content: content
357            };
358        }
359        else if (typeof content === 'function')
360        {
361            args = {
362                content: content
363            };
364        }
365        else
366        {
367            throw 'unable to determine tooltip config';
368        }
369
370        args = $.extend(default_args, args);
371
372        if (config.delegate)
373        {
374            if ('native' === config.selector_mode)
375            {
376                args.target = el;
377                console.log(el, config, args);
378                var x = tippy.delegate(config.delegate, args);
379                console.log(x);
380                return;
381            }
382/*
383            if ('jquery' === config.selector_mode)
384            {
385                $el.each(
386                    function ()
387                    {
388                        tippy(this, args);
389                    }
390                );
391                return;
392            }
393*/
394            throw 'invalid tooltip selector mode. Should be [native, jquery, ]';
395        }
396
397        if ('native' === config.selector_mode)
398        {
399            tippy(el, args);
400            return;
401        }
402
403        if ('jquery' === config.selector_mode)
404        {
405            $el.each(
406                function ()
407                {
408                    tippy(this, args);
409                }
410            );
411            return;
412        }
413
414        throw 'invalid tooltip selector mode. Should be [native, jquery, ]';
415    }
416    else
417    {
418        throw 'invalid driver specified';
419    }
420
421    throw 'unable to create tooltip';
422};
423
424function _tooltipify()
425{
426    tippy.delegate('body', {
427      target: 'abbr.sui-abbr-tooltip',
428      content: function (reference) {
429        const title = reference.getAttribute('title');
430        reference.removeAttribute('title');
431        return title;
432      },
433      appendTo: document.body,
434    });
435}
436
437// https://javascript.info/promise-chaining
438// https://davidwalsh.name/javascript-loader
439// https://stackoverflow.com/questions/14644558/call-javascript-function-after-script-is-loaded
440var load = (function() {
441  // Function which returns a function: https://davidwalsh.name/javascript-functions
442  function _load(tag) {
443    return function(url) {
444      // This promise will be used by Promise.all to determine success or failure
445      return new Promise(function(resolve, reject) {
446        var element = document.createElement(tag);
447        var parent = 'body';
448
449        var attr = 'src';
450
451        // Important success and error for the promise
452        element.onload = function() {
453          resolve(url);
454        };
455        element.onerror = function() {
456          reject(url);
457        };
458
459        // Need to set different attributes depending on tag type
460        switch(tag) {
461          case 'script':
462            element.async = true;
463            break;
464
465          case 'link':
466            element.type = 'text/css';
467            element.rel = 'stylesheet';
468            attr = 'href';
469            parent = 'head';
470        }
471
472        // Inject into document to kick off loading
473        element[attr] = url;
474        document[parent].appendChild(element);
475      });
476    };
477  }
478
479  return {
480    css: _load('link'),
481    js: _load('script'),
482    img: _load('img')
483  }
484})();
485
486$.extend(Shout, {
487    formmodified: false,
488
489    scriptsloaded: [],
490
491//    configuration: {},
492    configuration: ((typeof window.ShoutConfiguration != 'undefined') ? window.ShoutConfiguration.configuration : {}),
493
494    configure: function(params)
495    {
496        if (typeof window.ShoutConfiguration != 'undefined')
497        {
498            window.ShoutConfiguration.configure(params);
499        }
500
501        $.extend(true, this.configuration, params);
502    },
503
504    objSerialize: function(fields)
505    {
506        var returnObj = {};
507
508        $.each($(fields),function()
509        {
510            var field = $(this);
511            returnObj[field.attr('name')] = field.val();
512        });
513
514        return returnObj;
515    },
516
517    loadFields: function(region,dataArray,prefix)
518    {
519        var params = [];
520
521        if(dataArray)
522        {
523            $.each(dataArray, function(e,obj) {
524                $.each(obj, function(j, data)
525                {
526                    var match = phpVariable.exec(j);
527                    if(match[2])
528                    {
529                        var str = '['+e+']';
530
531                        if(match[1])
532                        {
533                            str += '['+match[1]+']';
534                        }
535
536                        str += ''+match[2]+'='+data;
537
538                        params.push(str);
539                    }
540                    else
541                    {
542                        params.push('['+e+']['+j+']='+data);
543                    }
544                });
545            });
546        }
547
548        $.each(params,function(i)
549        {
550
551            region.find('[name='+prefix+i+']').val(this);
552        });
553    },
554
555    flattenFields: function(data, prefix,depth)
556    {
557        if(!prefix)
558        {
559            prefix = '';
560        }
561
562        if(!depth)
563        {
564            depth = 1;
565        }
566
567        var returnFields = {};
568
569        var additionalFields = {};
570
571        if($.isArray(data) || typeof data == 'object')
572        {
573            $.each(data,function(i, obj)
574            {
575                if($.isFunction(obj))
576                {
577                    return true;
578                }
579
580                if(obj && ( $.isArray(obj) || typeof obj  == 'object') )
581                {
582                    if(depth > 0)
583                    {
584                        name = prefix + '[' + i + ']';
585                    }
586                    else
587                    {
588                        name = prefix + i;
589                    }
590
591                    $.extend(additionalFields, Shout.flattenFields(obj,name,depth+1) );
592                }
593                else
594                {
595                    if(depth > 0)
596                    {
597                        name = prefix + '[' + i + ']';
598                    }
599                    else
600                    {
601                        name = prefix + i;
602                    }
603
604                    if (typeof obj == 'boolean')
605                    {
606                        if (false === obj)
607                        {
608                            returnFields[name] = 0;
609                        }
610                        else
611                        {
612                            returnFields[name] = 1;
613                        }
614                    }
615                    else
616                    {
617                        returnFields[name] = obj;
618                    }
619                }
620            });
621        }
622        else
623        {
624            return data;
625        }
626
627        $.extend(returnFields,additionalFields);
628
629        return returnFields;
630    },
631
632    phpSerialize: function(dataArray)
633    {
634        var params = [];
635
636
637        if(dataArray.length)
638        {
639            $.each(dataArray, function(e,obj) {
640                $.each(obj, function(j, data)
641                {
642                    var match = phpVariable.exec(j);
643                    if(match[2])
644                    {
645                        var str = 'actions['+e+']';
646
647                        if(match[1])
648                        {
649                            str += '['+match[1]+']';
650                        }
651
652                        str += ''+match[2]+'='+data;
653
654                        params.push(str);
655                    }
656                    else
657                    {
658                        params.push('actions['+e+']['+j+']='+data);
659                    }
660                });
661            });
662        }
663
664        return params.join('&');
665    },
666
667    drawInfoItem: function(content)
668    {
669        var list = [];
670        if($.isArray(content))
671        {
672            $.each(content,function()
673            {
674                list.push('<li class = "ui-state-info ui-corner-all"><span class = "ui-icon ui-icon-lightbulb"></span><span class="sui-content-text">'+this+'</span></li>');
675            });
676        }
677        else
678        {
679            list.push('<li class = "ui-state-info ui-corner-all"><span class = "ui-icon ui-icon-lightbulb"></span><span class="sui-content-text">'+content+'</span></li>');
680        }
681
682        return $(list.join(''));
683    },
684
685    drawInfo: function(content)
686    {
687        return $('<div class = "sui-info"></div>').append($(this.drawInfoItem(content)).wrap('<ul></ul>'));
688    },
689
690    drawSummaryItem: function(content)
691    {
692        var list = [];
693        if($.isArray(content))
694        {
695            $.each(content,function()
696            {
697                list.push('<li class="ui-state-info ui-corner-all"><div class="sui-summary-content">'+this+'</div></li>');
698            });
699        }
700        else
701        {
702            list.push('<li class="ui-state-info ui-corner-all"><div class="sui-summary-content">'+content+'</div></li>');
703        }
704
705        return $(list.join(''));
706    },
707
708    showError: function(selector, content) {
709        Shout.hideUIBlock();
710
711        var exists = false;
712
713        var list;
714
715        if ((list = $(selector).filter('.sui-error').find('> ul')) && list.length)
716        {
717            // If the error already exists, don't re-add it.
718            $(selector).find('> ul > li > .sui-content-text').each(function(){
719                if($(this).html() == content)
720                {
721                    exists = true;
722                    return false;
723                }
724            });
725
726            if (exists) {
727                return;
728            }
729
730            // if you pass in the form element .sui-error container selector i.e. (a .sui-error div/span)
731            list.append(Shout.drawErrorItem(content));
732        }
733        else if ((list = $(selector).filter('.sui-element.sui-field').find('> .sui-error > ul')) && list.length)
734        {
735            // if you pass in the uielement(sui-element) container `id` - hopefully we are consistent in our structure
736            this.showError(list.parent(), content);
737        }
738        else if ((list = $(selector).filter('.sui-field').find('> .sui-strut-messages > .sui-error > ul')) && list.length)
739        {
740            // if you pass in the FormHelper(sui-field) container `class` - hopefully we are consistent in our structure
741            this.showError(list.parent(), content);
742        }
743        else if ((list = $(selector).closest('.sui-field').filter('.sui-element').find('> .sui-error > ul')) && list.length)
744        {
745            // if you pass in the uielement(sui-element) container `id` - unsafe?
746            this.showError(list.parent(), content);
747        }
748        else if ((list = $(selector).closest('.sui-field').not('.sui-element').find('> .sui-strut-messages > .sui-error > ul')) && list.length)
749        {
750            // if you pass in the FormHelper(sui-field) input `id` - unsafe?
751            this.showError(list.parent(), content);
752        }
753    },
754
755    hideError: function(selector, content) {
756
757        var list;
758
759        if ((list = $(selector).filter('.sui-error').find('> ul')) && list.length)
760        {
761            if ('undefined' !== typeof content)
762            {
763                $(selector).find('> ul > li > .sui-content-text').each(function(){
764                    if($(this).html() == content)
765                    {
766                        $(this).closest('li').remove();
767                    }
768                });
769            }
770            else
771            {
772                $(selector).find('> ul').empty();
773            }
774        }
775        else if ((list = $(selector).filter('.sui-element.sui-field').find('> .sui-error > ul')) && list.length)
776        {
777            // if you pass in the uielement(sui-element) container `id` - hopefully we are consistent in our structure
778            this.hideError(list.parent(), content);
779        }
780        else if ((list = $(selector).filter('.sui-field').find('> .sui-strut-messages > .sui-error > ul')) && list.length)
781        {
782            // if you pass in the FormHelper(sui-field) container `class` - hopefully we are consistent in our structure
783            this.hideError(list.parent(), content);
784        }
785        else if ((list = $(selector).closest('.sui-field').filter('.sui-element').find('> .sui-error > ul')) && list.length)
786        {
787            // if you pass in the uielement(sui-element) container `id` - unsafe?
788            this.hideError(list.parent(), content);
789        }
790        else if ((list = $(selector).closest('.sui-field').not('.sui-element').find('> .sui-strut-messages > .sui-error > ul')) && list.length)
791        {
792            // if you pass in the FormHelper(sui-field) input `id` - unsafe?
793            this.hideError(list.parent(), content);
794        }
795    },
796
797    showNotification: function(selector, content, state, dismissable)
798    {
799        var state_id         = '';
800        var state_icon       = '';
801        var state_css        = '';
802
803        var state_global     = 0;
804        var state_identifier = '';
805        var state_vector     = 'js';
806
807        if (undefined == dismissable)
808        {
809            dismissable = true;
810        }
811
812        $.each(Shout.configuration.module.core.notifications.types, function(index, value)
813            {
814                if (value == state)
815                {
816                    state_id = index;
817                }
818            }
819        );
820
821        $.each(Shout.configuration.module.core.notifications.icons, function(index, value)
822            {
823                if (index == state_id)
824                {
825                    state_icon = value;
826                }
827            }
828        );
829
830        $.each(Shout.configuration.module.core.notifications.states, function(index, value)
831            {
832                if (index == state_id)
833                {
834                    state_css = value;
835                }
836            }
837        );
838
839        var dismissable_html = '';
840
841        if (true === dismissable)
842        {
843            dismissable_html =
844                '<div class="sui-block-notification-dismiss"> \
845                <span class="ui-icon ui-icon-close"></span> \
846                </div> \
847                ';
848        }
849
850        var notification_html = '<div class="sui-block-notification-wrapper"\
851                data-global="'+state_global+'"\
852                data-identifier="'+state_identifier+'"\
853                data-origin="js"\
854            > \
855                <div class="ui-widget ui-widget-content ui-corner-all sui-block-notification '+state_css+'"> \
856                    <span class="sui-icon ui-icon '+state_icon+'" style="display:inline-block"></span> \
857                    <span class="sui-description">'+content+'</span> \
858                   '+dismissable_html+' \
859                </div> \
860            </div>';
861
862        $notification_html = $(notification_html);
863
864        $(selector).append($notification_html);
865
866        this.handleNotificationEvents($notification_html);
867    },
868
869    handleNotificationEvents: function($notification_html)
870    {
871        $notification_html.find('.sui-block-notification').click(
872            function(e)
873            {
874                if ($('.sui-block-notification-dismiss', this).length)
875                {
876                    $(this).closest('.sui-block-notification-wrapper').fadeOut('fast');
877                    var data = $(this).closest('.sui-block-notification-wrapper').data();
878                    var searchObj = Shout.buildRequest('Core', 'Shout_Notifier_Util', 'dismiss', {'identifier': data.identifier});
879                    $.post(Shout.configuration.location, searchObj,'json');
880                }
881            }
882        );
883
884        $notification_html.find('.sui-block-notification').hover(
885            function (e)
886            {
887                if ($('.sui-block-notification-dismiss', this).length)
888                {
889                    $(this).find('.sui-block-notification-dismiss .ui-icon').removeClass('ui-icon-close').addClass('ui-icon-circle-close');
890                    $(this).css('cursor','pointer');
891                }
892            },
893            function (e)
894            {
895                if ($('.sui-block-notification-dismiss', this).length)
896                {
897                    $(this).find('.sui-block-notification-dismiss .ui-icon').removeClass('ui-icon-circle-close').addClass('ui-icon-close');
898                    $(this).css('cursor','default');
899                }
900            }
901        );
902
903        $notification_html.find('.sui-block-notification .sui-description').click(
904            function (e)
905            {
906                e.stopPropagation();
907            }
908        );
909
910        $notification_html.find('.sui-block-notification .sui-description').hover(
911            function (e)
912            {
913                if ($(this).closest('.sui-block-notification-wrapper').find('.sui-block-notification-dismiss').length)
914                {
915                    $(this).parent().find('.sui-block-notification-dismiss .ui-icon').removeClass('ui-icon-circle-close').addClass('ui-icon-close');
916                    $(this).parent().css('cursor','default');
917                }
918
919                e.stopPropagation();
920            },
921            function (e)
922            {
923                if ($(this).closest('.sui-block-notification-wrapper').find('.sui-block-notification-dismiss').length)
924                {
925                    $(this).parent().find('.sui-block-notification-dismiss .ui-icon').removeClass('ui-icon-close').addClass('ui-icon-circle-close');
926                    $(this).parent().css('cursor','pointer');
927                }
928
929                e.stopPropagation();
930            }
931        );
932    },
933
934
935    clearNotifications: function(selector)
936    {
937        $(selector).find('.sui-block-notification-wrapper[data-origin="js"]').remove();
938    },
939
940    drawErrorItem: function(content)
941    {
942        var list = [];
943        if($.isArray(content))
944        {
945            $.each(content,function()
946            {
947                list.push('<li class = "ui-state-error ui-corner-all"><span class = "ui-icon ui-icon-alert"></span><span class="sui-content-text">'+this+'</span></li>');
948            });
949        }
950        else
951        {
952            list.push('<li class = "ui-state-error ui-corner-all"><span class = "ui-icon ui-icon-alert"></span><span class="sui-content-text">'+content+'</span></li>');
953        }
954
955        return $(list.join(''));
956    },
957
958    pathToId: function(path)
959    {
960        var pathElements = path.split('/');
961
962        return pathElements.join('-');
963    },
964
965    pathToName: function(path)
966    {
967        var pathElements = path.split('/');
968
969        var elementString = '';
970
971        $.each(pathElements,function()
972        {
973            if(elementString.length == 0)
974            {
975                elementString = this;
976            }
977            else
978            {
979                elementString += '['+this+']';
980            }
981        });
982
983        return elementString;
984    },
985
986    nameToId: function(name)
987    {
988        return Shout.str_replace(['[',']'],['-',''],name);
989    },
990
991    parseCommonParams: function(params)
992    {
993        if(!params)
994        {
995            params = {};
996            return params;
997        }
998
999        results = {};
1000
1001        if(params.value)
1002        {
1003            results.value = "value = \""+params.value+"\"";
1004        }
1005        else
1006        {
1007            results.value = "";
1008        }
1009
1010        if(params.reset != undefined && params.reset == false)
1011        {
1012            results.reset = "";
1013        }
1014
1015        if(params.css)
1016        {
1017            results.css = params.css;
1018        }
1019        else
1020        {
1021            results.css = '';
1022        }
1023
1024        if(params.type)
1025        {
1026            results.type = 'type = "'+params.type+'" ';
1027        }
1028        else
1029        {
1030            results.type = '';
1031        }
1032
1033        if(params.style)
1034        {
1035            results.style = 'style = "'+params.style+'" ';
1036        }
1037        else
1038        {
1039            results.style = '';
1040        }
1041
1042        if(params.multiple)
1043        {
1044            results.multiple = 'multiple';
1045        }
1046        else
1047        {
1048            results.multiple = '';
1049        }
1050
1051        if(params.selected)
1052        {
1053            results.selected = 'selected';
1054        }
1055        else
1056        {
1057            results.selected = '';
1058        }
1059
1060        if(params.checked)
1061        {
1062            results.checked = 'checked';
1063        }
1064        else
1065        {
1066            results.checked = '';
1067        }
1068
1069        if(params.icon)
1070        {
1071            results.icon = '<span class = "sui-icon '+params.icon+'"></span>';
1072        }
1073        else
1074        {
1075            results.icon = '';
1076        }
1077
1078
1079        if(params.href)
1080        {
1081            results.href = "href = \""+params.href+"\"";
1082        }
1083
1084        if(params.target)
1085        {
1086            results.target = "target = \""+params.target+"\"";
1087        }
1088
1089        if(params.title)
1090        {
1091            results.title = "title = \""+params.title+"\"";
1092        }
1093
1094        return results;
1095    },
1096
1097    drawButton: function(path, label, fieldIconClass, params)
1098    {
1099        var fieldId = this.pathToId(path);
1100        var fieldName = this.pathToName(path);
1101
1102        var pp = this.parseCommonParams(params);
1103
1104        if(params && params.type)
1105        {
1106            pp.type = 'hidden';
1107        }
1108        else if(params && params.type == 'submit')
1109        {
1110            // Well, we could be using buttons but IE 7 renders them incorrectly,
1111            // depriving us of a semantically useful element.
1112            var hackScript   = 'onclick = "$(this).next(\'input\').click();"';
1113            pp.type   = 'submit';
1114        }
1115        else
1116        {
1117            pp.type = 'hidden';
1118        }
1119
1120        if(params && params.type == 'submit')
1121        {
1122            pp.name = 'name = "'+fieldName+'"';
1123        }
1124        else
1125        {
1126            pp.name = '';
1127        }
1128
1129        if(params && params.click)
1130        {
1131            $('#'+fieldId).click(pp.click);
1132        }
1133
1134        var HTML = '\
1135        <div class = "sui-control sui-control-button">\
1136            <a id = "'+fieldId+'" class = "ui-state-default ui-corner-all sui-button '+pp.css+'"\
1137            '+pp.style+'  '+pp.href+' '+pp.title+' '+pp.target+'>\
1138        ';
1139
1140        if(fieldIconClass && fieldIconClass.length)
1141        {
1142            HTML += '\
1143                <span class = "ui-icon '+fieldIconClass+'"></span>\
1144            ';
1145        }
1146
1147        if(label.length)
1148        {
1149            HTML += '\
1150                <span class = "sui-button-label">'+label+'</span>\
1151            ';
1152        }
1153
1154        HTML += '\
1155            </a>\
1156            <input type="'+pp.type+'" '+pp.name+' '+pp.value+' style = "display:none" />\
1157        </div>';
1158
1159        return HTML;
1160    },
1161
1162    addHeaderButton: function(element, icon, callback)
1163    {
1164        //$(element).find('ui-dialog')
1165    },
1166
1167    multiPaste: function(content, pasteElement, parentSelector, rowSelector, columnSelector)
1168    {
1169        var rowDelimeter = '\n';
1170        var columnDelimeter = '\t';
1171
1172        var columnQuery = '.sui-form-field input[type!=hidden], .sui-form-field textarea';
1173        var rowQuery = '.sui-tree-node';
1174        var parentQuery = '.sui-tree-root ul > .sui-tree-node';
1175
1176        var rows = content.split(rowDelimeter);
1177
1178        var resultObj = {};
1179
1180        $.each(rows,function(i)
1181        {
1182            resultObj[i] = {};
1183            resultObj[i] = this.split(columnDelimeter);
1184        });
1185
1186        var rowIndex = 0;
1187
1188        var $parent = $(parentSelector);
1189
1190        // In case we're not in the top right corner of a 2d array of input boxes, we offset the pasting from the cursor position.
1191        var rowOffset = -1;
1192        var columnOffset = -1;
1193
1194        $parent.find(rowSelector).each(function(index)
1195        {
1196            var $currentRow = $(this);
1197
1198            if( ( columnOffset = $currentRow.find(columnSelector).index(pasteElement)  ) >= 0 )
1199            {
1200                rowOffset = index;
1201
1202                return false;
1203            }
1204
1205        });
1206
1207        var $rows = null;
1208
1209        if(rowOffset != -1)
1210        {
1211            $rows = $(parentSelector).find(rowSelector).slice(rowOffset);
1212        }
1213        else
1214        {
1215            $rows = $(parentSelector).find(rowSelector);
1216        }
1217
1218        $rows.each(function()
1219        {
1220            var columnIndex = 0;
1221            var $columns = null;
1222
1223            if(columnOffset != -1)
1224            {
1225                $columns = $(this).find(columnSelector).slice(columnOffset);
1226            }
1227            else
1228            {
1229                $columns = $(this).find(columnSelector);
1230            }
1231
1232            $columns.each(function()
1233            {
1234                if(!resultObj[rowIndex])
1235                {
1236                    return false;
1237                }
1238                else if(resultObj[rowIndex] == 'undefined')
1239                {
1240                    return false;
1241                }
1242
1243                $(this).val(resultObj[rowIndex][columnIndex]);
1244
1245                if(columnIndex > resultObj[rowIndex].length)
1246                {
1247                    return false;
1248                }
1249
1250                columnIndex++;
1251            });
1252
1253            rowIndex++;
1254        });
1255    },
1256
1257    expandClass: function(hack)
1258    {
1259        if(hack)
1260        {
1261            return 'sui-expanded-hack';
1262        }
1263        else
1264        {
1265            return 'sui-expanded';
1266        }
1267    },
1268
1269    collapseClass: function(hack)
1270    {
1271        if(hack)
1272        {
1273            return 'sui-collapsed-hack';
1274        }
1275        else
1276        {
1277            return 'sui-collapsed';
1278        }
1279    },
1280
1281    blockToggle: function(blockId, hack)
1282    {
1283        var self = this;
1284
1285        if(self.lastToggle == null )
1286        {
1287            // Keep track of this because on the next subsequent click (what *would* be the double click)
1288            // we don't want to toggle the main level.
1289            self.lastToggle = Shout.toggleCollapse(blockId,hack);
1290
1291            $(blockId+' > .sui-block-header').on('click.dbltoggle',
1292                function()
1293                {
1294
1295                    if(self.lastToggle)
1296                    {
1297                        Shout.expand($(blockId).find('.sui-toggle'),hack);
1298                    }
1299                    else
1300                    {
1301                        Shout.collapse($(blockId).find('.sui-toggle'),hack);
1302                    }
1303                }
1304            );
1305
1306            setTimeout(
1307                function()
1308                {
1309                    $(blockId+ '> .sui-block-header').off('click.dbltoggle');
1310                    self.lastToggle = null;
1311                }
1312            ,350);
1313        }
1314    },
1315    /**
1316     * The hack parameter is used to trigger an alternate implementation that uses
1317     * visibility hidden and position absolute. It was necessitated by plupload. But it turned out
1318     * that still didn't work because changes to the position property also reinitialized the uploader.
1319     */
1320    toggleCollapse: function($el, hack)
1321    {
1322        if(hack === undefined)
1323        {
1324            hack = false;
1325        }
1326
1327        var $el = $($el);
1328
1329        if($el.hasClass(this.expandClass(hack)))
1330        {
1331            Shout.collapse($el,hack);
1332            return false;
1333        }
1334        else if($el.hasClass(this.collapseClass(hack)))
1335        {
1336            Shout.expand($el,hack);
1337            return true;
1338        }
1339    },
1340
1341    collapse: function($el,hack)
1342    {
1343        var $el = $($el);
1344        $el.removeClass(this.expandClass(hack)).addClass(this.collapseClass(hack));
1345        $el.find(' > .sui-toggle-icon').addClass('ui-icon-triangle-1-e').removeClass('ui-icon-triangle-1-se');
1346        // The following should be accomplished via css.
1347        //$el.find(' > .sui-fieldset').hide();
1348    },
1349
1350    expand: function($el,hack)
1351    {
1352        var $el = $($el);
1353        $el.addClass(this.expandClass(hack)).removeClass(this.collapseClass(hack));
1354        $el.find(' > .sui-toggle-icon').removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-se');
1355
1356        // The following should be accomplished via css.
1357        //$el.find(' > .sui-fieldset').show();
1358    },
1359
1360    hideUIBlock: function(container)
1361    {
1362        if (!container)
1363        {
1364            container = '#sui_content_container';
1365        }
1366
1367//        $(container).unblock();
1368        $(container).find(' > .sui_content_container_wait').hide('fade');
1369    },
1370
1371    showUIBlock: function(container)
1372    {
1373        if (!container)
1374        {
1375            container = '#sui_content_container';
1376        }
1377
1378        var $container = $(container);
1379
1380        if (0 === $container.find(' > .sui_content_container_wait').length)
1381        {
1382            var wait_container = '\
1383<div class="sui_content_container_wait">\
1384</div>\
1385';
1386
1387//            $container.prepend(wait_container);
1388            $container.append(wait_container); // this forces the z-index to be higher than elements preceding it
1389        }
1390
1391
1392
1393        var refresh = _.debounce(
1394            function(event)
1395            {
1396                Shout.updateUIBlock(container);
1397            }
1398        );
1399        $(window).off('resize.waitcontainer scroll.waitcontainer', refresh)
1400                 .on('resize.waitcontainer scroll.waitcontainer', refresh)
1401                 ;
1402
1403//        $container.block({message: null})
1404        $container.find(' > .sui_content_container_wait').show('fade');
1405        Shout.updateUIBlock(container);
1406    },
1407
1408    updateUIBlock: function(container)
1409    {
1410        var $container = $(container);
1411
1412        if ($container.length)
1413        {
1414            var $wait_container = $container.find('> .sui_content_container_wait');
1415
1416            $wait_container
1417//                .width($container.outerWidth(true))
1418//                .height($container.outerHeight(true))
1419                .width($container.width())
1420                .height($container.height())
1421                .css('z-index', $container.css( "z-index" ) + 1)
1422                //.css('z-index', 5000)
1423                .css('position', 'absolute')
1424                .position({'my':'left top', 'at':'left top', 'of':$container})
1425                ;
1426
1427            var $bgColor = $wait_container.parents().filter(
1428                function (index, element)
1429                {
1430                    var $element = $(element);
1431
1432                    var property_value = $element.css('background-color');
1433
1434                    if ((property_value !== 'rgba(0, 0, 0, 0)')
1435                    && (property_value !== 'transparent'))
1436                    {
1437                        return true;
1438                    }
1439                    else
1440                    {
1441                        return false;
1442                    }
1443                }
1444            )
1445            .first()
1446            ;
1447
1448            $wait_container
1449                .css('background-color', $bgColor.css("background-color"));
1450        }
1451    },
1452
1453    showWaitIndicator: function($query, msg, progress)
1454    {
1455        Shout.showUIBlock($query);
1456
1457        if (0 === $($query).find('> .sui_content_container_wait > .sui_content_container_wait_message_container').length)
1458        {
1459            var wait_container = '\
1460<div class="rounded-corners sui_content_container_wait_message_container">\
1461    <div class="sui_content_container_wait_message_block">\
1462        <span class="sui_content_container_wait_indicator"><i class="fa-solid fa-spinner fa-fw fa-spin fa-2xl"></i></span>\
1463        <span class="sui_content_container_wait_message">Loading</span>\
1464    </div>\
1465</div>\
1466';
1467
1468            $($query).find('> .sui_content_container_wait').prepend(wait_container);
1469        }
1470
1471        Shout.updateWaitIndicator($query, msg, progress);
1472    },
1473
1474    hideWaitIndicator: function($query)
1475    {
1476        $($query).find('> .sui_content_container_wait > .sui_content_container_wait_message_container').remove();
1477
1478        Shout.hideUIBlock($query);
1479    },
1480
1481    updateWaitIndicator: function($query, message, progress)
1482    {
1483        if (!message)
1484        {
1485            msg = '';
1486        }
1487
1488        Shout.updateUIBlock($query);
1489
1490        $($query).find(' > .sui_content_container_wait .sui_content_container_wait_message').html(message);
1491
1492        if (progress == undefined)
1493        {
1494            $($query).find(' > .sui_content_container_wait .sui_content_container_wait_progress_bar').hide();
1495        }
1496        else
1497        {
1498            $($query).find(' > .sui_content_container_wait .sui_content_container_wait_progress_bar').progressbar({value: progress});
1499        }
1500
1501        Shout.updateUIBlock($query);
1502    },
1503
1504    showLoadIndicator: function($query, msg, progress)
1505    {
1506        Shout.showUIBlock($query);
1507
1508        if (0 === $($query).find('> .sui_content_container_wait > .sui_content_container_wait_message_container').length)
1509        {
1510            var progress_container = '\
1511<div class="rounded-corners sui_content_container_wait_message_container">\
1512    <div class="sui_content_container_wait_message_block">\
1513        <span class="sui_content_container_wait_indicator"><i class="fa-solid fa-spinner fa-fw fa-spin fa-2xl"></i></span>\
1514        <span class="sui_content_container_wait_message">Loading</span>\
1515    </div>\
1516    <div class="sui_content_container_wait_progress_bar"></div>\
1517</div>\
1518';
1519
1520            $($query).find('> .sui_content_container_wait').prepend(progress_container);
1521        }
1522
1523        Shout.updateLoadIndicator($query, msg, progress);
1524    },
1525
1526    hideLoadIndicator: function($query)
1527    {
1528        $($query).find('> .sui_content_container_wait > .sui_content_container_wait_message_container').remove();
1529
1530        Shout.hideUIBlock($query);
1531    },
1532
1533    updateLoadIndicator: function($query, message, progress)
1534    {
1535        if (!message)
1536        {
1537            msg = '';
1538        }
1539
1540        Shout.updateUIBlock($query);
1541
1542        $($query).find(' > .sui_content_container_wait .sui_content_container_wait_message').html(message);
1543
1544        if (progress == undefined)
1545        {
1546            $($query).find(' > .sui_content_container_wait .sui_content_container_wait_progress_bar').hide();
1547        }
1548        else
1549        {
1550            $($query).find(' > .sui_content_container_wait .sui_content_container_wait_progress_bar').progressbar({value: progress});
1551        }
1552
1553        Shout.updateUIBlock($query);
1554    },
1555
1556    tooltip: function($el, content, config)
1557    {
1558        var self = this;
1559
1560        config = _tooltipNormalizeConfig(config);
1561
1562        if ('qtip' === config.driver && $.fn.qtip)
1563        {
1564            if (!(typeof content === 'string'))
1565            {
1566                throw 'content should be a string';
1567            }
1568
1569            var args = {
1570                content: {
1571                    'text' : content
1572                }
1573            };
1574
1575            _tooltip($el, args, config);
1576
1577            return;
1578        }
1579        else if ('tippy' === config.driver && window.tippy)
1580        {
1581            var args = {
1582                content: content
1583            };
1584
1585            _tooltip($el, args, config);
1586
1587            return;
1588        }
1589        else
1590        {
1591            throw 'invalid driver specified';
1592        }
1593
1594        throw 'unable to create tooltip';
1595    },
1596
1597    callbackTooltip: function($el, callback, config)
1598    {
1599        var self = this;
1600
1601        config = _tooltipNormalizeConfig(config);
1602
1603        if ('qtip' === config.driver && $.fn.qtip)
1604        {
1605            var args = {
1606                content: {
1607                    text: function(event, api)
1608                    {
1609                        var result = callback();
1610                        if (!result || typeof result !== 'object' || typeof result.then !== 'function')
1611                        {
1612                            throw 'callback must return a Promise or jQuery.Deferred object';
1613                        }
1614
1615                        result
1616                        .done(
1617                            function(data)
1618                            {
1619                                api.set('content.text', data);
1620                            }
1621                        )
1622                        .fail(
1623                            function(reason)
1624                            {
1625                                api.set('content.text', reason);
1626                            }
1627                        );
1628
1629                        return '<div>loading</div>';
1630                    }
1631                }
1632            };
1633
1634            _tooltip($el, args, config);
1635
1636            return promise;
1637        }
1638        else if ('tippy' === config.driver && window.tippy)
1639        {
1640            var loading = '<i class="far fa-spinner fa-spin"></i>';
1641
1642            var args = {
1643              content: loading,
1644              animateFill: false,
1645              animation: 'fade',
1646              flipOnUpdate: true,
1647              onShow: function(instance) {
1648                // We can monkey-patch the instance's state object with our own state
1649                if (instance.state.callback === undefined)
1650                {
1651                  instance.state.callback =
1652                  {
1653                    isFetching: false,
1654                    canFetch: true,
1655                  }
1656                }
1657
1658                // Now we will avoid initiating a new request unless the old one
1659                // finished (`isFetching`).
1660                // We also only want to initiate a request if the tooltip has been
1661                // reset back to Loading... (`canFetch`).
1662                if (instance.state.callback.isFetching || !instance.state.callback.canFetch)
1663                {
1664                  return;
1665                }
1666
1667                instance.state.isFetching = true;
1668                instance.state.canFetch = false;
1669
1670                var result = callback();
1671                if (!result || typeof result !== 'object' || typeof result.then !== 'function')
1672                {
1673                    throw 'callback must return a Promise or jQuery.Deferred object';
1674                }
1675
1676                result
1677                .then(
1678                    function(html)
1679                    {
1680                        instance.setContent(html);
1681                    },
1682                    function(error)
1683                    {
1684                        instance.setContent(error);
1685                    }
1686                )
1687                .always(
1688                    function()
1689                    {
1690                        instance.state.callback.isFetching = false;
1691                    }
1692                )
1693                ;
1694              },
1695              onHidden: function(instance) {
1696                instance.setContent(loading);
1697                instance.state.callback.canFetch = true;
1698              }
1699            };
1700
1701            _tooltip($el, args, config);
1702
1703            return;
1704        }
1705        else
1706        {
1707            throw 'invalid driver specified';
1708        }
1709
1710        throw 'unable to create tooltip';
1711    },
1712
1713    ajaxTooltip: function($el, url, config)
1714    {
1715        // TODO: it would be nice to handle json responses... maybe we need our own jqueryui object to wrap the tooltip
1716        // allowing us to specify handlers, OR maybe we could use jquery deferred/promises, OR maybe add some properties
1717        // to the config object to handle it
1718        if (!(typeof url === 'string'))
1719        {
1720            throw 'url should be a string';
1721        }
1722
1723        var callback =
1724            function()
1725            {
1726                return $.ajax({
1727                    url: url
1728                })
1729                .done(
1730                    function(data, textStatus, jqXHR)
1731                    {
1732                        var ct = jqXHR.getResponseHeader("content-type") || "";
1733                        if (ct.indexOf('html') <= -1)
1734                        {
1735                            throw 'ajax response must be text/html';
1736                        }
1737
1738                        return data;
1739                    }
1740                )
1741                .fail(
1742                    function(xhr, status, error)
1743                    {
1744                        return status + ': ' + error;
1745                    }
1746                );
1747            }
1748        ;
1749
1750        return Shout.callbackTooltip($el, callback, config);
1751    },
1752
1753    popover: function($el, content, config)
1754    {
1755        var converted_config = $.extend(true, {}, config,
1756                {
1757                    driver_config:
1758                    {
1759                        tippy:
1760                        {
1761                            interactive:true
1762                        }
1763                    }
1764                }
1765        );
1766        return Shout.tooltip($el, content, converted_config);
1767    },
1768
1769    callbackPopover: function($el, callback, config)
1770    {
1771        var converted_config = $.extend(true, {}, config,
1772                {
1773                    driver_config:
1774                    {
1775                        tippy:
1776                        {
1777                            interactive:true
1778                        }
1779                    }
1780                }
1781        );
1782        return Shout.callbackTooltip($el, callback, converted_config);
1783
1784    },
1785
1786    ajaxPopover: function($el, url, config)
1787    {
1788        var converted_config = $.extend(true, {}, config,
1789                {
1790                    driver_config:
1791                    {
1792                        tippy:
1793                        {
1794                            interactive:true
1795                        }
1796                    }
1797                }
1798        );
1799        return Shout.ajaxTooltip($el, url, converted_config);
1800
1801    },
1802
1803    autoSlugify: function($titleField, $slugField, func)
1804    {
1805        if (typeof func === 'undefined')
1806        {
1807            func = this._transformToSlug;
1808        }
1809
1810        if (typeof func !== 'function')
1811        {
1812            throw new TypeError('Expected a function');
1813        }
1814
1815        $($titleField).on('keydown', function()
1816        {
1817            var currentTitle = $(this).val();
1818
1819            var safeTitle = func.call(this, currentTitle, true);
1820
1821            if (safeTitle == $($slugField).val())
1822            {
1823                $($titleField).one('keyup',function()
1824                {
1825                    var currentTitle = $(this).val();
1826
1827                    var safeTitle = func.call(this, currentTitle, true);
1828
1829                    if ($($slugField).attr('maxlength') > 0)
1830                    {
1831                        if ($($slugField).attr('maxlength') >= safeTitle.length)
1832                        {
1833                            $($slugField).val(safeTitle);
1834                        }
1835                        else
1836                        {
1837                            $($slugField).val(safeTitle.substring(0, $($slugField).attr('maxlength')));
1838                        }
1839                    }
1840                    else
1841                    {
1842                        $($slugField).val(safeTitle);
1843                    }
1844                });
1845            }
1846        });
1847
1848        $($slugField).on('keydown', function()
1849        {
1850            $(this).one('keyup', function()
1851            {
1852                var currentSlug = $(this).val();
1853                var safeSlug = func.call(this, currentSlug, false);
1854                $(this).val(safeSlug);
1855            });
1856        });
1857
1858        $($slugField).on('change', function()
1859        {
1860            var currentSlug = $(this).val();
1861            var safeSlug = func.call(this, currentSlug, true);
1862            $(this).val(safeSlug);
1863        });
1864    },
1865
1866    transformToSlug: function(str)
1867    {
1868        return this._transformToSlug(str);
1869    },
1870
1871    _transformToSlug: function(str, trimhyphens)
1872    {
1873        if (typeof trimhyphens === 'undefined')
1874        {
1875            trimhyphens = true;
1876        }
1877
1878        var slug = str;
1879
1880        slug = slug.trim()
1881                  .replace(/[^a-zA-Z0-9-]+/gi,'-')
1882                  .replace(/-+/gi,'-')
1883                  .toLowerCase()
1884                  ;
1885
1886        if (trimhyphens)
1887        {
1888            slug = slug.replace(/^-+|-+$/g,'');
1889        }
1890
1891        return slug;
1892    },
1893
1894    /**
1895     * Used to construct a request for our RPC system.
1896     *
1897     * @param module the primary module
1898     * @param an optional secondary module. Should have a handleAction function.
1899     * @param params Additional parameters. These get flattened into a nested JSON object 'actionParams'.
1900     */
1901    buildRequest: function(module, handler, mode, params)
1902    {
1903        var searchObj = {
1904            'module' : module,
1905            'handler' :handler,
1906            'action': Shout.configuration.action,
1907            'mode' : mode
1908        };
1909
1910        return $.extend(searchObj, Shout.flattenFields(params,'actionParams') );
1911    },
1912
1913    /**
1914     * @param img The original url (relative) to the image.
1915     * @param params The thumbnail configuration. Maps to Image::generateThumb configuration parameters (width, height, watermark, crop).
1916     */
1917    requestThumbnail: function(img, params)
1918    {
1919        if (!params)
1920        {
1921            params = { };
1922        }
1923
1924        if ('' == img)
1925        {
1926            return $.when({});
1927        }
1928
1929        var defaults = {
1930            'img':img,
1931            'v': 3,
1932        };
1933
1934        var data = $.extend(defaults, params);
1935
1936        var url = [
1937            Shout.configuration.absRoot,
1938            '/',
1939            'image.php'
1940        ].join('');
1941
1942        var details = null;
1943
1944        return $.ajax(
1945            {
1946//                headers: {
1947//                    "Accept" : "application/json; charset=utf-8",
1948//                    "Content-Type": "application/json; charset=utf-8"
1949//                },
1950
1951
1952                type: "GET",
1953//                contentType: "application/json; charset=utf-8",
1954
1955                dataType: 'json',
1956//                accepts: {json: 'application/json'},
1957
1958                async: true,
1959
1960                url: url,
1961                data: data
1962            }
1963        );
1964    },
1965    /**
1966     * Used to screen ajax request data for new content. The purpose is to prevent
1967     * repeatedly including scripts and css files that already exist in the page.
1968     *
1969     * These are, by convention, provided regardless. For example, a userbrowser might have an embededded
1970     * GroupBrowser that wasn't present for the initial page load. We'll need to include the scripts initially.
1971     * But subsequently, it's wasteful and leads to odd behaviors.
1972     */
1973    filterContent: function(data, params)
1974    {
1975        if (!params)
1976        {
1977            params = {};
1978        }
1979
1980        var defaults = {
1981            // Filter external sources we've already loaded.
1982            filterExistingExternal: true,
1983            // Filter <script>...</script> tags (non-external).
1984            filterNonExternalScripts: false
1985        };
1986
1987        // Configurable operation.
1988        params = $.extend(defaults, params);
1989
1990        if (!data)
1991        {
1992            return undefined;
1993        }
1994
1995        var $data    = $(data);
1996
1997        var $content = $data.not('script, link');
1998        var $scripts = $data.filter('script');
1999        var $css     = $data.filter('link');
2000
2001        $scripts = $scripts.map(
2002            function()
2003            {
2004                var src = $(this).attr('src');
2005
2006                if (src
2007                && params.filterExistingExternal
2008                && !Shout.isScriptLoaded(src))
2009                {
2010                    return this;
2011                }
2012                else if (!src
2013                     && !params.filterNonExternalScripts)
2014                {
2015                    return this;
2016                }
2017            }
2018        );
2019
2020        $css = $css.map(
2021            function()
2022            {
2023                if (!$('link[href="'+$(this).attr('href')+'"]').length)
2024                {
2025                    return this;
2026                }
2027            }
2028        );
2029
2030        var $filtered_data = $content.add($scripts).add($css);
2031
2032        var filtered_data = $.map($filtered_data, function(val, index) {
2033
2034            var str = '';
2035
2036            if (3 == val.nodeType)
2037            {
2038                str = val.nodeValue;
2039            }
2040            else
2041            {
2042                str = Shout.outerHTML(val);
2043//                str = val.outerHTML;
2044            }
2045
2046            return str;
2047        }).join("");
2048
2049        return filtered_data;
2050    },
2051    /**
2052     * Used to screen ajax request data for new content. The purpose is to prevent
2053     * repeatedly including scripts and css files that already exist in the page.
2054     *
2055     * These are, by convention, provided regardless. For example, a userbrowser might have an embededded
2056     * GroupBrowser that wasn't present for the initial page load. We'll need to include the scripts initially.
2057     * But subsequently, it's wasteful and leads to odd behaviors.
2058     */
2059    handleContent: function(data, params)
2060    {
2061        if (!params)
2062        {
2063            params = {};
2064        }
2065
2066        var defaults = {
2067            debug: false,
2068            use_style_promise: true//,
2069            //excludeLoadedScripts: true,
2070            //excludeLoadedStyles: true,
2071        };
2072
2073        // Configurable operation.
2074        params = $.extend(defaults, params);
2075
2076        var _promise = new Promise(
2077            function (resolve, reject)
2078            {
2079                if (!data)
2080                {
2081                    reject('data is undefined');
2082                }
2083
2084                var $data = $(data);
2085
2086                var $content = $data.not('script, link');
2087                var $scripts = $data.filter('script');
2088                var $css     = $data.filter('link');
2089
2090                var filtered_data = '';
2091
2092                var $new_external_script_uris = $scripts.map(
2093                    function()
2094                    {
2095                        var src = $(this).attr('src');
2096
2097                        if (src)
2098                        {
2099                            return src;
2100                        }
2101                    }
2102                ).map(
2103                    function()
2104                    {
2105                        if (false === Shout.isScriptLoaded(this.valueOf()))
2106                        {
2107                            return this.valueOf();
2108                        }
2109                    }
2110                );
2111
2112                if (true === params.debug)
2113                {
2114                    var z = {};
2115                    z.es = $new_external_script_uris;
2116                }
2117
2118                var $inline_scripts = $scripts.map(
2119                    function()
2120                    {
2121                        var src = $(this).attr('src');
2122
2123                        if (!src)
2124                        {
2125                            return this;
2126                        }
2127                    }
2128                );
2129
2130                if (true === params.debug)
2131                {
2132                    z.is = $inline_scripts.map(
2133                        function()
2134                        {
2135                            return $(this).html();
2136                        }
2137                    );
2138                }
2139
2140                if (true === params.use_style_promise)
2141                {
2142                    var $new_css = $css.map(
2143                        function()
2144                        {
2145                            if (!$('link[href="'+$(this).attr('href')+'"]').length)
2146                            {
2147                                return $(this).attr('href');
2148                            }
2149                        }
2150                    ).get();
2151
2152                    if (true === params.debug)
2153                    {
2154                        z.nc = $new_css
2155                    }
2156
2157                    // https://javascript.info/promise-chaining
2158                    // https://davidwalsh.name/javascript-loader
2159                    var $new_css_promises = $.map($new_css,
2160                        function(val, i)
2161                        {
2162                            var _stylePromise = load.css(val);
2163
2164                            return _stylePromise;
2165                        }
2166                    );
2167
2168                    var _stylesPromise = Promise.all($new_css_promises);
2169                }
2170                else
2171                {
2172                    var $new_css = $css.map(
2173                        function()
2174                        {
2175                            if (!$('link[href="'+$(this).attr('href')+'"]').length)
2176                            {
2177                                return $(this);
2178                            }
2179                        }
2180                    );
2181
2182                    if (true === params.debug)
2183                    {
2184                        z.nc = $new_css.map(
2185                            function()
2186                            {
2187                                return $(this).attr('href');
2188                            }
2189                        );
2190                    }
2191
2192                    var filtered_css = $.map($new_css, function(val, index) {
2193
2194                        var str = '';
2195
2196                        if (3 == val.nodeType)
2197                        {
2198                            str = val.nodeValue;
2199                        }
2200                        else
2201                        {
2202                            str = Shout.outerHTML(val);
2203                        }
2204
2205                        return str;
2206
2207                    }).join("");
2208
2209                    $(document).find('html > head').append(filtered_css);
2210
2211                    var _stylesPromise = new Promise(
2212                        function(resolve, reject)
2213                        {
2214                            resolve();
2215                        }
2216                    );
2217                }
2218
2219                if (params.debug)
2220                {
2221                    console.log(z);
2222                }
2223
2224                var $filtered_data = $content.add($inline_scripts)/*.add($new_css)*/;
2225
2226                var filtered_data = $.map($filtered_data, function(val, index) {
2227
2228                    var str = '';
2229
2230                    if (3 == val.nodeType)
2231                    {
2232                        str = val.nodeValue;
2233                    }
2234                    else
2235                    {
2236                        str = Shout.outerHTML(val);
2237                    }
2238
2239                    return str;
2240
2241                }).join("");
2242
2243                var _scriptsPromise = Shout.getScripts($new_external_script_uris);
2244
2245                Promise.all([
2246                    _stylesPromise,
2247                    _scriptsPromise
2248                ]).then(
2249                    function(result)
2250                    {
2251                        result.filtered_data = filtered_data;
2252
2253                        resolve(result); // resolve _promise
2254                    },
2255                    function(reason)
2256                    {
2257                        reject(reason); // reject _promise
2258                    }
2259                );
2260            }
2261        );
2262
2263        return _promise;
2264    },
2265
2266    filterContentInlineScripts: function(data, params)
2267    {
2268        if (!params)
2269        {
2270            params = {};
2271        }
2272
2273        var defaults = {
2274        };
2275
2276        // Configurable operation.
2277        params = $.extend(defaults, params);
2278
2279        if (!data)
2280        {
2281            return undefined;
2282        }
2283
2284        var $data    = $(data);
2285
2286        var $scripts = $data.filter('script');
2287
2288        $scripts = $scripts.map(
2289            function()
2290            {
2291                var src = $(this).attr('src');
2292
2293                if (!src)
2294                {
2295                    return this;
2296                }
2297            }
2298        );
2299
2300        var filtered_data = $.map($scripts, function(val, index) {
2301
2302            var str = Shout.outerHTML(val);
2303
2304            return str;
2305        }).join("");
2306
2307        return filtered_data;
2308    },
2309
2310    filterContentScripts: function(data, params)
2311    {
2312        if (!params)
2313        {
2314            params = {};
2315        }
2316
2317        var defaults = {
2318            excludeLoadedScripts: true
2319        };
2320
2321        // Configurable operation.
2322        params = $.extend(defaults, params);
2323
2324        if (!data)
2325        {
2326            return undefined;
2327        }
2328
2329        var $data    = $(data);
2330
2331        var $scripts = $data.filter('script');
2332
2333        $scripts = $scripts.map(
2334            function()
2335            {
2336                var src = $(this).attr('src');
2337
2338                if (true === params.excludeLoadedScripts)
2339                {
2340                    if (src
2341                    && !Shout.isScriptLoaded(src))
2342                    {
2343                        return src;
2344                    }
2345                }
2346                else
2347                {
2348                    if (src)
2349                    {
2350                        return src;
2351                    }
2352                }
2353            }
2354        );
2355
2356        return $scripts;
2357    },
2358
2359    //@see: http://stackoverflow.com/questions/11803215/how-to-include-multiple-js-files-using-jquery-getscript-method
2360    //@see: http://stackoverflow.com/questions/23018012/making-multiple-scripts-loader-with-jquery-deferred-object-am-i-doing-it-right
2361    //@see: http://stackoverflow.com/questions/11803215/how-to-include-multiple-js-files-using-jquery-getscript-method
2362    //@see: http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt2-practical-use
2363    getScripts: function(url, params) {
2364
2365        if (!params)
2366        {
2367            params = {};
2368        }
2369
2370        // Configurable operation.
2371        _params = $.extend(params,
2372            {
2373                "path": "",
2374                "sequential": true,
2375                "ajax": {
2376                    'async': true,
2377                    'cache': false
2378                }
2379            }
2380        );
2381
2382        if (url === undefined)
2383        {
2384            return undefined;
2385        }
2386
2387        var _urls = (url instanceof Array || url instanceof jQuery) ? url : [url];
2388        var _promises = [];
2389
2390        // https://promisesaplus.com/
2391        // https://gist.github.com/justinjmoses/18cdb0c5f65314b8e3d
239153f84d2b76b56
2392        // https://developers.google.com/web/fundamentals/primers/promises
2393
2394        // http://bluebirdjs.com/docs/api/deferred-migration.html
2395        var defer = function () {
2396            'use strict';
2397
2398            var result, resolve, reject;
2399
2400            return {
2401                resolve: function (value) {
2402                    if (resolve) {
2403                        resolve(value);
2404                    } else {
2405                        result = result || new Promise(function (r) { r(value); });
2406                    }
2407                },
2408
2409                reject: function (reason) {
2410                    if (reject) {
2411                        reject(reason);
2412                    } else {
2413                        result = result || new Promise(function (_, j) { j(reason); });
2414                    }
2415                },
2416
2417                promise: new Promise(function (r, j) {
2418                    if (result) {
2419                        r(result);
2420                    } else {
2421                        resolve = r;
2422                        reject = j;
2423                    }
2424                })
2425            };
2426        };
2427
2428        var _rootDeferred  = defer();
2429        var _rootPromise = _rootDeferred.promise;
2430
2431        var _promise        = _rootPromise;
2432
2433        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators#Flags_and_bitmasks
2434        var FLAG_A = 1;  // 00001
2435        var FLAG_B = 2;  // 00010
2436        var FLAG_C = 4;  // 00100
2437        var FLAG_D = 8;  // 01000
2438        var FLAG_E = 16; // 10000
2439
2440        var debug = false; // /*FLAG_A | FLAG_B | */FLAG_C | FLAG_D;
2441
2442        _promises.push(_rootPromise);
2443
2444        if (true === _params.sequential)
2445        {
2446            var _prevPromise  = _promise;
2447
2448            $.each(_urls,
2449                function(index, url)
2450                {
2451                    var _resolved_url = (_params.path||"") + url;
2452
2453                    if (!Shout.isScriptLoaded(_resolved_url))
2454                    {
2455                        if (debug & FLAG_E)
2456                        {
2457                            console.groupCollapsed(url);
2458                            console.log('%c_prevPromise%O', "color: silver", _prevPromise);
2459                            console.groupEnd();
2460                        }
2461
2462                        var _promise = _prevPromise.then(
2463                            function(value)
2464                            {
2465                                if (debug & FLAG_A)
2466                                {
2467                                    console.groupCollapsed("%c%i:resolve filter: %s", "color: blue", index, _resolved_url);
2468                                    console.log('this', this);
2469                                    console.log('arguments', arguments);
2470                                    console.groupEnd();
2471                                }
2472
2473                                return Shout.getScript(_resolved_url, _params);
2474                            },
2475                            function(reason)
2476                            {
2477                                if (debug & FLAG_A)
2478                                {
2479                                    console.groupCollapsed("%c%i:resolve filter: %s", "color: blue", index, _resolved_url);
2480                                    console.log('this', this);
2481                                    console.log('arguments', arguments);
2482                                    console.groupEnd();
2483                                }
2484
2485                                console.error(reason);
2486
2487                                return reason;
2488                            }
2489                        );
2490
2491                        if (debug & FLAG_B)
2492                        {
2493                            _promise.then(
2494                                            function()
2495                                            {
2496                                                console.groupCollapsed("%c%i:done: %s", "color: blue", index, _resolved_url);
2497                                                console.log('this', this);
2498                                                console.log('arguments', arguments);
2499                                                console.groupEnd();
2500                                            }
2501                            ).catch(
2502                                            function()
2503                                            {
2504                                                console.groupCollapsed("%c%i:fail: %s", "color: blue", index, _resolved_url);
2505                                                console.log('this', this);
2506                                                console.log('arguments', arguments);
2507                                                console.groupEnd();
2508                                            }
2509                            ).finally(
2510                                            function()
2511                                            {
2512                                                console.groupCollapsed("%c%i:always: %s", "color: blue", index, _resolved_url);
2513                                                console.log('this', this);
2514                                                console.log('arguments', arguments);
2515                                                console.groupEnd();
2516                                            }
2517                            );
2518
2519                            _promise.index = index;
2520                            _promise.url   = _resolved_url;
2521                        }
2522
2523                        // even though some promises might get rejected or resolved and the chain will end we still
2524                        // test via when below to properly return a proper state and promise... maybe we should use
2525                        // Promise.allSettled instead
2526                        _promises.push(_promise);
2527
2528                        _prevPromise = _promise;
2529                    }
2530                }
2531            );
2532        }
2533        else
2534        {
2535            _promises = $.map(_urls,
2536                function(url)
2537                {
2538                    var _resolved_url = (_params.path||"") + url;
2539
2540                    if (!Shout.isScriptLoaded(_resolved_url))
2541                    {
2542                        return Shout.getScript(_resolved_url, _params);
2543                    }
2544                }
2545            );
2546        }
2547
2548        if (debug & FLAG_C)
2549        {
2550            _prevPromise.then(
2551                            function()
2552                            {
2553                                console.groupCollapsed("%c _prevPromise done", "color: green");
2554                                console.log('this', this);
2555                                console.log('arguments', arguments);
2556                                console.groupEnd();
2557                            }
2558            ).catch(
2559                            function()
2560                            {
2561                                console.groupCollapsed("%c _prevPromise fail", "color: green");
2562                                console.log('this', this);
2563                                console.log('arguments', arguments);
2564                                console.groupEnd();
2565                            }
2566            ).finally(
2567                            function()
2568                            {
2569                                console.groupCollapsed("%c _prevPromise always", "color: green");
2570                                console.log('this', this);
2571                                console.log('arguments', arguments);
2572                                console.groupEnd();
2573                            }
2574            );
2575            _rootPromise.then(
2576                function()
2577                {
2578                    console.groupCollapsed("%c _rootPromise done", "color: green");
2579                    console.log('this', this);
2580                    console.log('arguments', arguments);
2581                    console.groupEnd();
2582                }
2583            ).catch(
2584                function()
2585                {
2586                    console.groupCollapsed("%c _rootPromise fail", "color: green");
2587                    console.log('this', this);
2588                    console.log('arguments', arguments);
2589                    console.groupEnd();
2590                }
2591            ).finally(
2592                function()
2593                {
2594                    console.groupCollapsed("%c _rootPromise always", "color: green");
2595                    console.log('this', this);
2596                    console.log('arguments', arguments);
2597                    console.groupEnd();
2598                }
2599            );
2600        }
2601
2602        // ensures dom is ready
2603        _promises.push(
2604            Promise.resolve(
2605                $.Deferred(
2606                    function(deferred)
2607                    {
2608                        $(deferred.resolve);
2609                    }
2610                ).promise()
2611            )
2612        );
2613
2614        // this tests to make sure all the promises are resolved
2615        var _when = Promise.all(_promises);
2616
2617        if (debug & FLAG_D)
2618        {
2619            _when.then(
2620                            function()
2621                            {
2622                                console.groupCollapsed("%c _when done", "color: purple");
2623                                console.log('this', this);
2624                                console.log('arguments', arguments);
2625                                console.groupEnd();
2626                            }
2627            ).catch(
2628                            function()
2629                            {
2630                                console.groupCollapsed("%c _when fail", "color: purple");
2631                                console.log('this', this);
2632                                console.log('arguments', arguments);
2633                                console.groupEnd();
2634                            }
2635            ).finally(
2636                            function()
2637                            {
2638                                console.groupCollapsed("%c _when always", "color: purple");
2639                                console.log('this', this);
2640                                console.log('arguments', arguments);
2641                                console.groupEnd();
2642                            }
2643            );
2644        }
2645
2646        // this delays the start of the ajax calls until all of the promises are in place
2647        _rootDeferred.resolve();
2648
2649        return _when;
2650    },
2651    getScriptsOld: function(url, params) {
2652
2653        if (!params)
2654        {
2655            params = {};
2656        }
2657
2658        // Configurable operation.
2659        _params = $.extend(params,
2660                        {
2661            "path": "",
2662            "sequential": true,
2663            "ajax": {
2664                'async': true,
2665                'cache': false
2666            }
2667                        }
2668        );
2669
2670        if (url === undefined)
2671        {
2672            return undefined;
2673        }
2674
2675        var _urls = (url instanceof Array || url instanceof jQuery) ? url : [url];
2676        var _promises = [];
2677
2678        var _deferred       = new $.Deferred();
2679        var _manualDeferred = _deferred;
2680        var _manualPromise  = _deferred.promise();
2681        var _promise        = _manualPromise;
2682
2683        var debug = false;
2684
2685        _promises.push(_manualPromise);
2686
2687        if (true === _params.sequential)
2688        {
2689            var _prevPromise  = _promise;
2690            var _ajaxPromises = [];
2691
2692            $.each(_urls,
2693                            function(index, url)
2694                            {
2695                var _resolved_url = (_params.path||"") + url;
2696
2697                if (!Shout.isScriptLoaded(_resolved_url))
2698                {
2699                    if (true === debug)
2700                    {
2701                        console.groupCollapsed(url);
2702                        console.log('%c_prevPromise%O', "color: silver", _prevPromise);
2703                        console.groupEnd();
2704                    }
2705
2706                    var _promise = _prevPromise.then(
2707                                    function()
2708                                    {
2709                                        if (true === debug)
2710                                        {
2711                                            console.groupCollapsed("%c%i:resolve filter: %s", "color: blue", index, _resolved_url);
2712                                            console.log('this', this);
2713                                            console.log('arguments', arguments);
2714                                            console.groupEnd();
2715                                        }
2716
2717                                        return Shout.getScript(_resolved_url, _params);
2718                                    },
2719                                    function()
2720                                    {
2721
2722                                        if (true === debug)
2723                                        {
2724                                            console.groupCollapsed("%c%i:reject filter: %s", "color: blue", index, _resolved_url);
2725                                            console.log('this', this);
2726                                            console.log('arguments', arguments);
2727                                            console.groupEnd();
2728                                        }
2729
2730                                        return false;
2731                                    }
2732                    );
2733
2734                    if (true === debug)
2735                    {
2736                        _promise.progress(
2737                                        function()
2738                                        {
2739                                            console.groupCollapsed("%c%i:progress: %s", "color: blue", index, _resolved_url);
2740                                            console.log('this', this);
2741                                            console.log('arguments', arguments);
2742                                            console.groupEnd();
2743                                        }
2744                        )
2745                        .done(
2746                                        function()
2747                                        {
2748                                            console.groupCollapsed("%c%i:done: %s", "color: blue", index, _resolved_url);
2749                                            console.log('this', this);
2750                                            console.log('arguments', arguments);
2751                                            console.groupEnd();
2752                                        }
2753                        ).fail(
2754                                        function()
2755                                        {
2756                                            console.groupCollapsed("%c%i:fail: %s", "color: blue", index, _resolved_url);
2757                                            console.log('this', this);
2758                                            console.log('arguments', arguments);
2759                                            console.groupEnd();
2760                                        }
2761                        ).always(
2762                                        function()
2763                                        {
2764                                            console.groupCollapsed("%c%i:always: %s", "color: blue", index, _resolved_url);
2765                                            console.log('this', this);
2766                                            console.log('arguments', arguments);
2767                                            console.groupEnd();
2768                                        }
2769                        );
2770
2771                        _promise.index = index;
2772                        _promise.url   = _resolved_url;
2773                    }
2774
2775                    _ajaxPromises.push(_promise);
2776
2777                    // even though some promises might get rejected or resolved and the chain will end we still
2778                    // test via when below to properly return a proper state and promise
2779                    _promises.push(_promise);
2780
2781                    _prevPromise = _promise;
2782                }
2783                            }
2784            );
2785
2786            if (true === debug)
2787            {
2788                _prevPromise.progress(
2789                                function()
2790                                {
2791                                    console.groupCollapsed("%cprogress", "color: purple");
2792                                    console.log('this', this);
2793                                    console.log('arguments', arguments);
2794                                    console.groupEnd();
2795                                }
2796                )
2797                .done(
2798                                function()
2799                                {
2800                                    console.groupCollapsed("%cdone", "color: purple");
2801                                    console.log('this', this);
2802                                    console.log('arguments', arguments);
2803                                    console.groupEnd();
2804                                }
2805                ).fail(
2806                                function()
2807                                {
2808                                    console.groupCollapsed("%cfail", "color: purple");
2809                                    console.log('this', this);
2810                                    console.log('arguments', arguments);
2811                                    console.groupEnd();
2812                                }
2813                ).always(
2814                                function()
2815                                {
2816                                    console.groupCollapsed("%calways", "color: purple");
2817                                    console.log('this', this);
2818                                    console.log('arguments', arguments);
2819                                    console.groupEnd();
2820                                }
2821                );
2822
2823                _manualDeferred.progress(
2824                                function()
2825                                {
2826                                    console.groupCollapsed("%cprogress", "color: green");
2827                                    console.log('this', this);
2828                                    console.log('arguments', arguments);
2829                                    console.groupEnd();
2830                                }
2831                )
2832                .done(
2833                                function()
2834                                {
2835                                    console.groupCollapsed("%cdone", "color: green");
2836                                    console.log('this', this);
2837                                    console.log('arguments', arguments);
2838                                    console.groupEnd();
2839                                }
2840                ).fail(
2841                                function()
2842                                {
2843                                    console.groupCollapsed("%cfail", "color: green");
2844                                    console.log('this', this);
2845                                    console.log('arguments', arguments);
2846                                    console.groupEnd();
2847                                }
2848                ).always(
2849                                function()
2850                                {
2851                                    console.groupCollapsed("%calways", "color: green");
2852                                    console.log('this', this);
2853                                    console.log('arguments', arguments);
2854                                    console.groupEnd();
2855                                }
2856                );
2857            }
2858        }
2859        else
2860        {
2861            _promises = $.map(_urls,
2862                            function(url)
2863                            {
2864                var _resolved_url = (_params.path||"") + url;
2865
2866                if (!Shout.isScriptLoaded(_resolved_url))
2867                {
2868                    return Shout.getScript(_resolved_url, _params);
2869                }
2870                            }
2871            );
2872        }
2873
2874        // ensures dom is ready
2875        _promises.push($.Deferred(
2876                        function(deferred)
2877                        {
2878                            $(deferred.resolve);
2879                        }
2880        ).promise()
2881        );
2882
2883        if (true === debug)
2884        {
2885            console.log('%cpromises: %O', "color: red;",_promises);
2886        }
2887
2888        // this tests to make sure all the promises are resolved
2889        var _when = $.when.apply($, _promises);
2890
2891        // this delays the start of the ajax calls until all of the promises are in place
2892        _manualDeferred.resolve();
2893
2894        return _when;
2895    },
2896
2897    getScript: function(url, params)
2898    {
2899        var self = this;
2900
2901        _params = $.extend(params,
2902            {
2903                "ajax": {
2904                    'async': true,
2905                    'cache': false
2906                }
2907            }
2908        );
2909
2910        $method = 'use_internal'; // use_system_js, use_jquery, use_internal
2911
2912        if ('use_system_js' === $method)
2913        {
2914            var _ajaxPromise = SystemJS.import(url)
2915            .then(
2916                function () {
2917                    Shout.scriptsloaded.push(url);
2918                },
2919                function(reason) {
2920                    console.log(reason);
2921                }
2922            );
2923        }
2924        else if ('use_jquery' === $method)
2925        {
2926            var _ajaxPromise = $.ajax(
2927                {
2928                    url: url,
2929                    dataType: "script",
2930                    async: _params.ajax.async,
2931                    cache: _params.ajax.cache
2932                }
2933            );
2934
2935            _ajaxPromise = Promise.resolve(_ajaxPromise);
2936
2937            _ajaxPromise.then(
2938                function (result) {
2939                    Shout.scriptsloaded.push(url);
2940                },
2941                function(reason) {
2942                    console.error(reason);
2943                }
2944            );
2945        }
2946        else if ('use_internal' === $method)
2947        {
2948            _ajaxPromise = load.js(url);
2949
2950            _ajaxPromise.then(
2951                function (result) {
2952                    Shout.scriptsloaded.push(url);
2953                },
2954                function(reason) {
2955                    console.error(reason);
2956                }
2957            );
2958        }
2959        else
2960        {
2961            throw 'invalid getScript method `'+$method+'`';
2962        }
2963
2964
2965        return _ajaxPromise;
2966    },
2967
2968
2969    isScriptLoaded: function(url) {
2970
2971        var self = this;
2972
2973        if (0 <= $.inArray(url, Shout.scriptsloaded))
2974        {
2975            //console.log('already loaded var: '+url);
2976            return true;
2977        }
2978
2979        if ($('script[src="'+url+'"]').length)
2980        {
2981            //console.log('already loaded src: '+url);
2982            return true;
2983        }
2984
2985        //console.log('not loaded: '+url);
2986        return false;
2987    },
2988
2989    outerHTML: function(el) {
2990
2991        var $el = null;
2992
2993        if (el instanceof jQuery)
2994        {
2995            $el = el;
2996        }
2997        else
2998        {
2999            $el = $(el);
3000        }
3001
3002        return (!$el.length)
3003            ? $el
3004            : ($el.get(0).outerHTML || (
3005                  function($el2){
3006                      var div = document.createElement('div');
3007                      div.appendChild($el2.cloneNode(true));
3008                      var contents = div.innerHTML;
3009                      div = null;
3010                      return contents;
3011                })($el.get(0)));
3012    },
3013
3014    /**
3015     * Applies file input styling hackishness. See scripts/walkthrough.php
3016     */
3017    fileStyle: function($el){
3018        var $file = $($el).find('.current_file');
3019        var $cabinet = $($el).find('.cabinet');
3020        var $input = $cabinet.find('input');
3021
3022        var posFile = $file.position();
3023        var posCabinet = $cabinet.position();
3024
3025        var left = (posCabinet.left - posFile.left) * -1;
3026
3027        $input.attr('size', 25);
3028        $input.position({
3029            my:'top left',
3030            at:'top left',
3031            of:$file
3032        });
3033
3034        $input.css('opacity',0);
3035        $input.css('zIndex',2000);
3036        $input.css('left',left);
3037    },
3038
3039    createCookie: function(name,value,days) {
3040        if(typeof days === 'undefined')
3041        {
3042            days = 10000;
3043        }
3044
3045        if (days)
3046        {
3047            var date = new Date();
3048            date.setTime(date.getTime()+(days*24*60*60*1000));
3049            var expires = "; expires="+date.toGMTString();
3050        }
3051        else
3052        {
3053            var expires = "";
3054        }
3055
3056        document.cookie = name+"="+value+expires+"; path=/";
3057    },
3058
3059    readCookie: function(name) {
3060        var nameEQ = name + "=";
3061        var ca = document.cookie.split(';');
3062        for(var i=0;i < ca.length;i++) {
3063            var c = ca[i];
3064            while (c.charAt(0)==' ') c = c.substring(1,c.length);
3065            if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
3066        }
3067        return null;
3068    },
3069
3070    eraseCookie: function(name) {
3071        this.createCookie(name,"",-1);
3072    },
3073
3074    unload: function()
3075    {
3076        if (Shout.formmodified)
3077        {
3078            Shout.formmodified = false;
3079        }
3080        else
3081        {
3082            Shout.showLoadIndicator('#sui_content_container', 'Submitting', 0);
3083        }
3084    },
3085
3086    animatePulse: function($el, duration, start_css, end_css, times)
3087    {
3088        $el.animate(
3089            end_css,
3090            duration,
3091            'linear',
3092            function() {
3093                if (times > 0)
3094                {
3095                    Shout.animatePulse($el, duration, end_css, start_css, --times);
3096                }
3097            }
3098        );
3099    },
3100
3101    loadStart: function(query, msg) {
3102
3103//        if (!Shout.progressTimer)
3104//        {
3105//            Shout.progressTimer = new Array();
3106//        }
3107
3108//        Shout.progressTimer[query] = setTimeout(function(){
3109        Shout.showUIBlock(query);
3110//        });
3111
3112        Shout.showLoadIndicator(query, msg, 0);
3113
3114        $(window).on('load',
3115            function (event)
3116            {
3117                $(document).on('shoutReady', function ()
3118                    {
3119                        Shout.updateLoadIndicator(query, 'Finished', 100);
3120                        Shout.hideLoadIndicator(query);
3121                        Shout.hideUIBlock(query);
3122                    }
3123                );
3124
3125                Shout.isShoutReadyCheck();
3126            }
3127        );
3128    },
3129
3130    isShoutReadyCheck: function ()
3131    {
3132        var poll = 100;
3133
3134        if (!Shout.shoutReady)
3135        {
3136            if (typeof(Wysiwyg_Editor) != 'undefined'
3137            && Wysiwyg_Editor_Manager.editors())
3138            {
3139                Shout.shoutReady = Wysiwyg_Editor_Manager.loaded();
3140            }
3141            else
3142            {
3143                Shout.shoutReady = true;
3144            }
3145
3146            if (!Shout.shoutReady)
3147            {
3148                setTimeout(function(){
3149                    Shout.isShoutReadyCheck();
3150                }, poll);
3151
3152                return false;
3153            }
3154
3155            setTimeout(function(){
3156                Shout.shoutReady = true;
3157                $(document).trigger('shoutReady');
3158            }, poll);
3159
3160            return false;
3161        }
3162
3163        return true;
3164    },
3165
3166    loadProgress: function(query, msg, progress) {
3167        Shout.updateLoadIndicator(query, msg, progress);
3168    },
3169
3170    loadComplete: function(query) {
3171//        if (!Shout.progressTimer)
3172//        {
3173//            Shout.progressTimer = new Array();
3174//        }
3175//        clearTimeout(Shout.progressTimer[query]);
3176
3177        Shout.updateLoadIndicator(query, 'Loading Additional Resources', 75);
3178    },
3179
3180    modifiedFormListener: function(selector)
3181    {
3182        var strategy = 'serializeArray'; // order does not matter
3183
3184        if (typeof window.URLSearchParams !== 'undefined'
3185        && typeof window.FormData !== 'undefined')
3186        {
3187            var strategy = 'URLSearchParamsString'; // modern js but order does matter
3188        }
3189
3190        var equals = function(x, y)
3191        {
3192            var equals = true;
3193
3194            $(x).each(
3195                function()
3196                {
3197                    var found = false;
3198
3199                    var self = this;
3200
3201                    // workaround for the wierd gallery init delay
3202                    if (self.name === 'uploader_count')
3203                    {
3204                        return false;
3205                    }
3206
3207                    if (self.name === '_token')
3208                    {
3209                        return false;
3210                    }
3211
3212                    $(y).each(
3213                        function()
3214                        {
3215                            if (this.name === self.name)
3216                            {
3217                                if (this.value === self.value)
3218                                {
3219                                    found = true;
3220                                    return false;
3221                                }
3222                            }
3223                        }
3224                    );
3225
3226                    if (false === found)
3227                    {
3228                        equals = false;
3229                        return false;
3230                    }
3231                }
3232            );
3233
3234            $(y).each(
3235                function()
3236                {
3237                    var found = false;
3238
3239                    var self = this;
3240
3241                    // workaround for the wierd gallery init delay
3242                    if (self.name === 'uploader_count')
3243                    {
3244                        return;
3245                    }
3246
3247                    if (self.name === '_token')
3248                    {
3249                        return;
3250                    }
3251
3252                    $(x).each(
3253                        function()
3254                        {
3255                            if (this.name === self.name)
3256                            {
3257                                if (this.value === self.value)
3258                                {
3259                                    found = true;
3260                                    return false;
3261                                }
3262                            }
3263                        }
3264                    );
3265
3266                    if (false === found)
3267                    {
3268                        equals = false;
3269                        return false;
3270                    }
3271                }
3272            );
3273
3274            return equals;
3275        };
3276
3277        var getFormData = function(form){
3278            var formData = new FormData(form);
3279            formData.delete('_token');
3280            formData.delete('uploader_count');
3281
3282            return formData;
3283        };
3284
3285        var getFormDataString = function(form){
3286            var formData = getFormData(form);
3287            var urLSearchParams = new URLSearchParams(formData);
3288            var form_data_string = urLSearchParams.toString();
3289
3290            return form_data_string;
3291        };
3292
3293        var detectFormChanges = function(e)
3294        {
3295            var changed = false;
3296
3297            if (window.Wysiwyg_Editor_Manager)
3298            {
3299                Wysiwyg_Editor_Manager.refresh();
3300            }
3301
3302            $forms = e.data.forms;
3303
3304            $forms.each(
3305                function()
3306                {
3307                    $form = $(this);
3308
3309                    if ('serialize' === strategy)
3310                    {
3311                        var currentFormData = $form.serialize();
3312                        var initialFormData = $form.data('initialFormData');
3313
3314                        if (currentFormData != initialFormData)
3315                        {
3316                            changed = true;
3317                        }
3318                    }
3319                    else if ('serializeArray' === strategy)
3320                    {
3321                        var currentFormData = $form.serializeArray();
3322                        var initialFormData = $form.data('initialFormData');
3323
3324                        // console.group();
3325                        // console.log($form.prop('id'));
3326                        // console.log(_.filter(currentFormData, function(v){return v.name === 'core[basics][title]';}));
3327                        // console.log(_.filter(initialFormData, function(v){return v.name === 'core[basics][title]';}));
3328                        // console.groupEnd();
3329
3330                        if (!equals(currentFormData, initialFormData))
3331                        {
3332                            changed = true;
3333                        }
3334                    }
3335                    else if ('URLSearchParamsString' === strategy)
3336                    {
3337                        var currentFormData = getFormDataString(this);
3338                        var initialFormData = $form.data('initialFormData');
3339
3340                        if (currentFormData != initialFormData)
3341                        {
3342                            changed = true;
3343                        }
3344                    }
3345                    else if ('URLSearchParams' === strategy)
3346                    {
3347                        throw 'not implemented';
3348                    }
3349                    else if ('FormData' === strategy)
3350                    {
3351                        throw 'not implemented';
3352                    }
3353                    else
3354                    {
3355                        throw 'invalid modified form detection strategy';
3356                    }
3357                }
3358            );
3359
3360            if (changed)
3361            {
3362                /*e.stopPropagation();*/
3363
3364                console.log('Unsaved Changes');
3365
3366                Shout.formmodified = true;
3367
3368                setTimeout(function(){Shout.hideUIBlock();}, 1000);
3369                Shout.hideUIBlock();
3370
3371                return 'Warning: You have unsaved changes on this page.';
3372            }
3373            else
3374            {
3375                console.log('No Changes');
3376            }
3377        };
3378
3379        $(document).on('shoutReady',
3380            function ()
3381            {
3382                console.log('shoutReady');
3383                console.timeStamp('shoutReady');
3384
3385                var $forms = $('form'+selector);
3386
3387                $forms.each(
3388                    function()
3389                    {
3390                        var $form = $(this);
3391                        if ('serialize' === strategy)
3392                        {
3393                            $form.data('initialFormData', $form.serialize());
3394                        }
3395                        else if ('serializeArray' === strategy)
3396                        {
3397                            $form.data('initialFormData', $form.serializeArray());
3398                        }
3399                        else if ('URLSearchParamsString' === strategy)
3400                        {
3401                            if (typeof window.URLSearchParams !== 'undefined'
3402                            && typeof window.FormData !== 'undefined')
3403                            {
3404                                $form.data('initialFormData', getFormDataString(this));
3405                            }
3406                        }
3407                        else if ('URLSearchParams' === strategy)
3408                        {
3409                            $form.data('initialFormData', new URLSearchParams(getFormData(this)));
3410                        }
3411                        else if ('FormData' === strategy)
3412                        {
3413                            if (typeof window.URLSearchParams !== 'undefined'
3414                            && typeof window.FormData !== 'undefined')
3415                            {
3416                                $form.data('initialFormData', tFormData(this));
3417                            }
3418                        }
3419                        else
3420                        {
3421                            throw 'invalid modified form detection strategy';
3422                        }
3423                    }
3424                ).submit(
3425                    function()
3426                    {
3427                        $(window).off('beforeunload', detectFormChanges);
3428
3429                        window.setTimeout(
3430                            function ()
3431                            {
3432                                $(window).on('beforeunload', {forms: $forms}, detectFormChanges);
3433                            }
3434                        , 2000);
3435                    }
3436                );
3437
3438                $(window).on('beforeunload', {forms: $forms}, detectFormChanges);
3439            }
3440        );
3441    },
3442
3443    removeModifiedRankListener: function()
3444    {
3445        $(window).off('beforeunload.rankmodifiedlistener', detectRankChanges);
3446    },
3447
3448    // works well for simple ranking forms, it does not handle ajax page loads well though since they do not trigger beforeunload
3449    addModifiedRankListener: function(selector)
3450    {
3451        var isRankChanged = function(current_ranks, original_ranks)
3452        {
3453            var changed = false;
3454
3455            var changed = !_.isEqual(original_ranks, current_ranks);
3456            //var changed = !original_ranks.is(current_ranks);
3457
3458            if (changed)
3459            {
3460                return 'You have unsaved rank changes on this page.';
3461            }
3462        };
3463
3464        var detectRankChanges = function(e)
3465        {
3466            var original_ranks = e.data.original_ranks;
3467            var selector = e.data.selector;
3468            var current_ranks = $(selector);
3469
3470            return isRankChanged(current_ranks, original_ranks);
3471        };
3472
3473        var original_ranks = $(selector);
3474        $(window).on('beforeunload.rankmodifiedlistener', {selector: selector, original_ranks: original_ranks}, detectRankChanges);
3475    },
3476
3477    // http://stackoverflow.com/questions/5069464/javascript-string-replace-array
3478    str_replace: function(find, replace, replaceString)
3479    {
3480      for (var i = 0; i < find.length; i++) {
3481        replaceString = replaceString.replace(find[i], replace[i]);
3482      }
3483      return replaceString;
3484    },
3485
3486    // http://thinkclay.com/technology/add-edit-delete-cookies-with-javascript
3487    createCookie: function(name, value, minutes) {
3488        var expires = '';
3489        if (minutes)
3490        {
3491            var date = new Date();
3492            date.setTime(date.getTime() + (minutes * 60 * 1000));
3493            expires = "; expires="+date.toGMTString();
3494        }
3495        else
3496        {
3497            expires = "";
3498        }
3499
3500        document.cookie = name+"="+value+expires;
3501    },
3502
3503    eraseCookie: function(name) {
3504        var self = this;
3505
3506        self.createCookie(name, null, -1);
3507    },
3508
3509//    helpers: {},
3510    helpers: ((typeof window.ShoutConfiguration != 'undefined') ? window.ShoutConfiguration.helpers : {}),
3511
3512    common: {
3513
3514        tooltipify: function()
3515        {
3516            _tooltipify();
3517        },
3518
3519        escapeQuote: function(selector, type)
3520        {
3521            var escaped_selector;
3522
3523            if ("undefined" === typeof type)
3524            {
3525                type = 'double';
3526            }
3527
3528            if ('double' === type)
3529            {
3530                escaped_selector = selector.replace(/["]/g, "\\$&");
3531            }
3532            else if ('single' === type)
3533            {
3534                escaped_selector = selector.replace(/[']/g, "\\$&");
3535            }
3536            else
3537            {
3538                throw 'invalid type';
3539            }
3540
3541            return escaped_selector;
3542        },
3543        escapeCssSelector: function(selector)
3544        {
3545            var escaped_selector;
3546
3547            if ($.escapeSelector)
3548            {
3549                escaped_selector = $.escapeSelector(selector);
3550            }
3551            else if (window.CSS && window.CSS.escape)
3552            {
3553                escaped_selector = CSS.escape(selector);
3554            }
3555            else
3556            {
3557                escaped_selector = selector.replace(/[#;?%&,.+*~\':"!^$[\]()=>|\/@]/g, "\\\\$&");
3558            }
3559
3560            return escaped_selector;
3561        },
3562        checkBoxToggle: function(control, target, prop)
3563        {
3564            if ("undefined" === typeof prop)
3565            {
3566                prop = true;
3567            }
3568
3569            if (prop === $(control).prop('checked'))
3570            {
3571                $(target).show();
3572                Shout.common.enableFormControls(target);
3573            }
3574            else
3575            {
3576                $(target).hide();
3577                Shout.common.disableFormControls(target);
3578            }
3579        },
3580        enableFormControlsOld: function(target)
3581        {
3582            $(target).find('input, select, textarea').each(
3583                function (key, value)
3584                {
3585                    if ("undefined" !== typeof $(value).data('disabled-controls-disabled-state'))
3586                    {
3587                        if (false === $(value).data('disabled-controls-disabled-state'))
3588                        {
3589                            $(value).prop('disabled', false);
3590                        }
3591
3592                        $(value).removeData('disabled-controls-disabled-state');
3593                    }
3594                }
3595            );
3596        },
3597        disableFormControlsOld: function(target)
3598        {
3599            $(target).find('input, select, textarea').each(
3600                function (key, value)
3601                {
3602                    if ("undefined" === typeof $(value).data('disabled-controls-disabled-state'))
3603                    {
3604                        $(value).data('disabled-controls-disabled-state', $(value).prop('disabled'));
3605                    }
3606
3607                    $(value).prop('disabled', true);
3608                }
3609            );
3610        },
3611
3612        enableFormControls: function(target, memento, force)
3613        {
3614            var debug = false;
3615
3616            Shout.common.enableFormControlsMemento(target, memento, force);
3617
3618            if (debug)
3619            {
3620                Shout.common.enableFormControls_redrawCounts(target);
3621                console.log('enableFormControls', {memento: memento, force:force, target:target}, new Error());
3622            }
3623        },
3624
3625        disableFormControls: function(target, memento, force)
3626        {
3627            var debug = false;
3628
3629            Shout.common.disableFormControlsMemento(target, memento, force);
3630
3631            if (debug)
3632            {
3633                Shout.common.enableFormControls_redrawCounts(target);
3634                console.log('disableFormControls', {memento: memento, force:force, target:target}, new Error());
3635            }
3636        },
3637
3638        enableFormControls_2017_12_21: function(target, remember, force)
3639        {
3640            if ("undefined" === typeof remember)
3641            {
3642                remember = true;
3643            }
3644
3645            if ("undefined" === typeof force)
3646            {
3647                force = false;
3648            }
3649
3650            $(target).find('input, select, textarea').each(
3651                function (key, value)
3652                {
3653                    if (true === remember)
3654                    {
3655                        if ("undefined" === typeof $(value).data('disabled-controls-enabled-state'))
3656                        {
3657                            $(value).data('disabled-controls-enabled-state', !$(value).prop('disabled'));
3658                        }
3659                    }
3660
3661                    if ("undefined" !== typeof $(value).data('disabled-controls-disabled-state'))
3662                    {
3663                        if (true === force
3664                        || false === $(value).data('disabled-controls-disabled-state'))
3665                        {
3666                            $(value).prop('disabled', false);
3667                        }
3668
3669                        $(value).removeData('disabled-controls-disabled-state');
3670                    }
3671                    else
3672                    {
3673                        $(value).prop('disabled', false);
3674                    }
3675                }
3676            );
3677        },
3678        disableFormControls_2017_12_21: function(target, remember, force)
3679        {
3680            if ("undefined" === typeof remember)
3681            {
3682                remember = true;
3683            }
3684
3685            if ("undefined" === typeof force)
3686            {
3687                force = false;
3688            }
3689
3690            $(target).find('input, select, textarea').each(
3691                function (key, value)
3692                {
3693                    if (true === remember)
3694                    {
3695                        if ("undefined" === typeof $(value).data('disabled-controls-disabled-state'))
3696                        {
3697                            $(value).data('disabled-controls-disabled-state', $(value).prop('disabled'));
3698                        }
3699                    }
3700
3701                    if ("undefined" !== typeof $(value).data('disabled-controls-enabled-state'))
3702                    {
3703                        if (true === force
3704                        || false === $(value).data('disabled-controls-enabled-state'))
3705                        {
3706                            $(value).prop('disabled', true);
3707                        }
3708
3709                        $(value).removeData('disabled-controls-enabled-state');
3710                    }
3711                    else
3712                    {
3713                        $(value).prop('disabled', true);
3714                    }
3715                }
3716            );
3717        },
3718
3719        _ensureFormControlsMementoInitialized: function(target)
3720        {
3721            $(target).find('input, select, textarea').add(target).each(
3722                function (key, value)
3723                {
3724                    var state = $(value).data('disabled-controls-state');
3725
3726                    if ("undefined" === typeof state)
3727                    {
3728                        $parent = $(value).closest('*[data-disabled-controls-state]');
3729
3730                        if ($parent.length > 0)
3731                        {
3732                            state = Object.assign({}, $parent.data('disabled-controls-state'));
3733                        }
3734                        else
3735                        {
3736                            state = {};
3737                        }
3738
3739                        $(value).data('disabled-controls-state', state);
3740                    }
3741                }
3742            );
3743        },
3744
3745        /**
3746         * Each formControl store a history of what intent has applied disabled state on it and also
3747         * the state of the control before the disabled state was applied.
3748         * @param target: the form container
3749         * @param memento: the intent (e.g. the checkbox) that want to enableFormControls
3750         * @param force: force the form to restore to initial state no matter if other intents has applied disable on the form
3751         */
3752        enableFormControlsMemento: function(target, memento, force)
3753        {
3754            var isDebug = false;
3755
3756            if ("undefined" === typeof memento)
3757            {
3758                memento = 'default';
3759            }
3760
3761            if ("undefined" === typeof force)
3762            {
3763                force = false;
3764            }
3765
3766            Shout.common._ensureFormControlsMementoInitialized(target);
3767
3768            $(target).find('input, select, textarea, *[data-disabled-controls-state]').add(target).each(
3769                function (key, value)
3770                {
3771                    if ($(value).is(target))
3772                    {
3773                        // only process enable/disableFormControl on the target element itself
3774                        // if it has the disabled-controls-state data
3775                        if ('undefined' == typeof $(value).data('disabled-controls-state'))
3776                        {
3777                            return;
3778                        }
3779                    }
3780
3781                    if (force)
3782                    {
3783                        Shout.common.setControlDisabled($(value), false);
3784                    }
3785                    else
3786                    {
3787                        if (isDebug)
3788                        {
3789                            console.log('---- BEGIN ----', value, 'disabled = ', $(value).prop('disabled'));
3790                        }
3791
3792                        // retrieve the saved disabled-state of the control
3793                        var state = $(value).data('disabled-controls-state');
3794                        if ("undefined" === typeof state)
3795                        {
3796                            state = {};
3797                        }
3798
3799                        var disabled = state[memento];
3800
3801                        var initialsize = _.size(state); //brilliant!!!, this is to prevent the enableFormControlsMemento from being called many time on an already enabled control
3802
3803                        delete state[memento];
3804
3805                        $(value).data('disabled-controls-state', state);
3806
3807                        // if all intents have called enableFormControls on this control
3808                        // restore its original state
3809                        if (0 === _.size(state) && 0 < initialsize) // test memento??
3810                        {
3811                            Shout.common.setControlDisabled($(value), disabled === true);
3812                        }
3813
3814                        if (isDebug)
3815                        {
3816                            console.log('disabled = ', disabled, 'initialsize = ', initialsize);
3817
3818                            console.log('---- END ----', value, 'disabled = ', $(value).prop('disabled'));
3819                        }
3820                    }
3821                }
3822            );
3823        },
3824
3825        /**
3826         * Each formControl store a history of what intent has applied disabled state on it and also
3827         * the state of the control before the disabled state was applied.
3828         */
3829        disableFormControlsMemento: function(target, memento, force)
3830        {
3831            var isDebug = false;
3832
3833            if ("undefined" === typeof memento)
3834            {
3835                memento = 'default';
3836            }
3837
3838            if ("undefined" === typeof force)
3839            {
3840                force = false;
3841            }
3842
3843            var state = null;
3844            var newstate = null;
3845
3846            Shout.common._ensureFormControlsMementoInitialized(target);
3847
3848            $(target).find('input, select, textarea, *[data-disabled-controls-state]').add(target).each(
3849                function (key, value)
3850                {
3851                    if ($(value).is(target))
3852                    {
3853                        // only process enable/disableFormControl on the target element itself
3854                        // if it has the disabled-controls-state data
3855                        if ('undefined' == typeof $(value).data('disabled-controls-state'))
3856                        {
3857                            return;
3858                        }
3859                    }
3860
3861                    if (force)
3862                    {
3863                        Shout.common.setControlDisabled($(value), true);
3864                    }
3865                    else
3866                    {
3867                        // save the disabled-state of the control with regard to memento
3868                        if ("undefined" === typeof $(value).data('disabled-controls-state'))
3869                        {
3870                            state = {};
3871                            $(value).data('disabled-controls-state', state);
3872                        }
3873                        else
3874                        {
3875                            state = $(value).data('disabled-controls-state');
3876                        }
3877
3878                        if ("undefined" === typeof state[memento])
3879                        {
3880                            newstate = {};
3881                            newstate[memento] = $(value).prop('disabled');
3882
3883                            $.extend(state, newstate);
3884                            $(value).data('disabled-controls-state', state);
3885                        }
3886
3887                        // disable the control
3888                        Shout.common.setControlDisabled($(value), true);
3889                    }
3890                }
3891            );
3892        },
3893
3894        enableFormControls_redrawCounts: function(target, propName)
3895        {
3896            jQuery(target).find('input,*[data-disabled-controls-state]').add(jQuery('.toggle-block')).add(target).each(function(key, value){
3897                var elm = jQuery(value);
3898                var sib = elm.next();
3899                if (!sib.hasClass('count')){
3900                    sib = jQuery('<div style="display:inline-block;white-space:pre;font-size:10px;vertical-align:middle" class="count"><div>');
3901                    elm.after(sib);
3902                }
3903
3904                var data;
3905                if (propName) {
3906                    data = elm.data(propName);
3907                }
3908                else {
3909                    data = {
3910                        initial: elm.data('initial-disabled-controls-state'),
3911                        current: elm.data('disabled-controls-state')
3912                    };
3913                    //elm.data()
3914                }
3915                var text = '#data contains circular references#';
3916                try{
3917                    text = ('(' + JSON.stringify(data, null, 4) + ')');
3918                }
3919                catch(reason){}
3920                sib.text( text );
3921            });
3922        },
3923
3924        /**
3925         * It would be ideal if all jQuery ui widgets can be enabled/disabled by just calling prop('disabled')
3926         * on the host input field. However, it is not possible without subclassing the widgets.
3927         */
3928        setControlDisabled: function($controls, disabled)
3929        {
3930            $controls.each(function()
3931                {
3932                    var $control = $(this);
3933
3934                    var widgetDefs = {
3935                        'ui-selectmenu' : 'selectmenu'
3936                    };
3937
3938                    $control.prop('disabled', disabled);
3939
3940                    // if the control is a jquery ui widget,
3941                    // set its disabled option accordingly
3942                    for (var jquery_ui_widget_data_key in widgetDefs)
3943                    {
3944                        if ('object' === typeof $control.data(jquery_ui_widget_data_key))
3945                        {
3946                            var jquery_ui_widget_name = widgetDefs[jquery_ui_widget_data_key];
3947                            $control[jquery_ui_widget_name]('option', 'disabled', disabled);
3948                        }
3949                    }
3950                }
3951            );
3952        },
3953
3954        // initialize a checkbox (parentCheckbox) that automatically check/uncheck other checkboxes (childCheckbox)
3955        // TODO: make this dynamic and attach click handlers to window rather i.e. window.on('click', selector, handler)...
3956        // see code/core/src/Core/AddressBook/Display/AddressBook/AbstractGridDisplay.php
3957        // see regex \.(on|bind)\(\s*['"][a-zA-Z0-9 ]+['"],\s*['"][^'"]+['"]\s*,
3958        initSelectAllCheckbox: function($parentCheckbox, $childCheckboxes)
3959        {
3960            $parentCheckbox = $($parentCheckbox);
3961            $childCheckboxes = $($childCheckboxes);
3962
3963            $parentCheckbox.on('click',
3964                function(e)
3965                {
3966                    var self = e.target;
3967                    var $self = $(e.target);
3968
3969                    var is_checked = $self.is(':checked');
3970
3971                    $childCheckboxes.prop('checked', is_checked);
3972                    $parentCheckbox.prop('checked', is_checked);
3973                }
3974            );
3975            $childCheckboxes.on('click',
3976                function(e)
3977                {
3978                    var selected = ($childCheckboxes.length == $childCheckboxes.filter(':checked').length);
3979
3980                    $parentCheckbox.prop('checked', selected);
3981                }
3982            );
3983        }
3984    },
3985
3986    openPopUp: function(url, width, height)
3987    {
3988        empty = window.open(url, 'popup', 'toolbar=0,scrollbars=0,location=0,statusbar=0,menubar=0,resizable=0,width='+width+',height='+height);
3989    },
3990
3991    range: {
3992
3993        _params: {
3994            range_separator: '-',
3995            separator: ',',
3996            sort: true,
3997            uniq: true
3998        },
3999
4000        _isIntergerish: function(value)
4001        {
4002            if (typeof value === 'string')
4003            {
4004                if ('' === value.trim())
4005                {
4006                    return false;
4007                }
4008            }
4009
4010            if (isNaN(value))
4011            {
4012                return false;
4013            }
4014
4015            var num = Number(value);
4016            if (!Number.isInteger(num))
4017            {
4018                return false;
4019            }
4020
4021            return true;
4022        },
4023
4024        extractIntRange: function(expanded_range)
4025        {
4026            var self = this;
4027
4028            var range_separator = self._params.range_separator;
4029            var separator = self._params.separator;
4030
4031            _.forEach(expanded_range, function(value){
4032                if (!self._isIntergerish(value))
4033                {
4034                    throw 'range item is not integerish: `'+value+'`';
4035                }
4036            });
4037
4038            expanded_range = _.map(expanded_range, function(value) {
4039                return _.parseInt(value);
4040            });
4041
4042            if (self._params.sort)
4043            {
4044                expanded_range = expanded_range.sort(function(a,b) {
4045                    return a - b;
4046                });
4047            }
4048
4049            if (self._params.uniq)
4050            {
4051                expanded_range = _.sortedUniq(expanded_range);
4052            }
4053
4054            var len = expanded_range.length;
4055
4056            var i = 0;
4057            var j = 0;
4058
4059            var out = new Array();
4060
4061            for (i = 0; i < len; i = j + 1)
4062            {
4063                // beginning of range or single
4064                out.push(expanded_range[i]);
4065
4066                // find end of range
4067                for (j = (i + 1); (j < len) && ((expanded_range[j]) === ((expanded_range[j-1]) + 1)); j++);
4068                j--;
4069
4070                if (i === j)
4071                {
4072                    // single number
4073                    out.push(separator);
4074                }
4075                else if ((i + 1) === j)
4076                {
4077                    // two numbers
4078                    out.push(separator, (expanded_range[j]), separator);
4079                }
4080                else
4081                {
4082                    // range
4083                    out.push(range_separator, (expanded_range[j]), separator);
4084                }
4085            }
4086
4087            out.pop(); // remove trailing comma
4088
4089            return out.join('');
4090        },
4091
4092        expandIntRange: function(range)
4093        {
4094            var self = this;
4095
4096            var regex = new RegExp('^-?[0-9]+(--?[0-9]+)?(,-?[0-9]+(--?[0-9]+)?)*$');
4097            if (false === regex.test(range))
4098            {
4099                throw 'invalid range string: `'+range+'`';
4100            }
4101
4102            var range_separator = self._params.range_separator;
4103            var separator = self._params.separator;
4104
4105            var lst = new Array();
4106
4107            var ranges = range.split(separator);
4108
4109            _.forEach(ranges, function(subrange) {
4110                var pos = null;
4111                if (-1 === (pos = subrange.indexOf(range_separator, 1)))
4112                {
4113                    lst.push(_.parseInt(subrange));
4114                    return;
4115                }
4116
4117                var a = _.parseInt(subrange.substr(0,pos));
4118                var b = _.parseInt(subrange.substr(pos+1));
4119
4120                for (var j = a; j <= b; j++)
4121                {
4122                    lst.push(j);
4123                }
4124            });
4125
4126            return lst;
4127        }
4128    }
4129});
4130
4131// fix for console.log slipups
4132
4133if(window)
4134{
4135    if (!window.console)
4136    {
4137        window.console = { log: function(){} };
4138    }
4139
4140    if (typeof window.console.timeStamp !== 'function')
4141    {
4142        console.log('console.timeStamp is not supported in this browser!');
4143        window.console.timeStamp = function() {};
4144    }
4145
4146    if (typeof window.console.debug !== 'function')
4147    {
4148        console.log('console.debug is not natively supported in this browser!');
4149        window.console.debug = function(msg) {console.log(msg);};
4150    }
4151}
4152
4153})(jQuery, ((typeof SystemJS !== 'undefined') ? SystemJS : System), _);

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.