/**
 * uf-table plugin.  Sets up a Tablesorter table with sorting, pagination, and search, and fetches data from a JSON API.
 *
 * This plugin depends on query-string.js, which is used to convert a query string into a JSON object.
 *
 * jQuery plugin template adapted from https://gist.github.com/Air-Craft/1300890
 *
 * === USAGE ===
 *
 * Create a container element, and within it place your table, paging controls, and Handlebars templates for rendering the cells.
 *
 * - Your table should have a unique id, and your paging controls should be wrapped in an element with the `.js-uf-table-pager` class.
 * - Create a button with the `.js-uf-table-download` class, and it will be automatically bound to trigger an AJAX request for downloading the table (CSV, etc).
 *
    <div id="widget-users">
       <table id="table-users" class="tablesorter table table-bordered table-hover table-striped" data-sortlist="[[0, 0]]">
           <thead>
               <tr>
                   <th class="sorter-metatext" data-column-name="name" data-column-template="#user-table-column-info">User <i class="fas fa-sort"></i></th>
                   <th class="sorter-metanum" data-column-name="last_activity" data-column-template="#user-table-column-last-activity">Last Activity <i class="fas fa-sort"></i></th>
               </tr>
           </thead>
           <tbody>
           </tbody>
       </table>

       <script id="user-table-column-info" type="text/x-handlebars-template">
           <td data-text="{{row.last_name}}">
               <strong>
                   <a href="{{site.uri.public}}/users/u/{{row.user_name}}">{{row.first_name}} {{row.last_name}} ({{row.user_name}})</a>
               </strong>
               <div>
                   <i class="fas fa-envelope"></i> <a href="mailto:{{row.email}}">{{row.email}}</a>
               </div>
           </td>
       </script>

       <script id="user-table-column-last-activity" type="text/x-handlebars-template">
           {{#if row.last_activity_at }}
           <td data-num="{{dateFormat row.last_activity_at format='x'}}">
               {{dateFormat row.last_activity_at format="dddd"}}<br>{{dateFormat row.last_activity_at format="MMM Do, YYYY h:mm a"}}
               <br>
               <i>{{row.last_activity.description}}</i>
           </td>
           {{ else }}
           <td data-num="0">
                   <i>Unknown</i>
           </td>
           {{/if }}
       </script>

       <div class="pager pager-lg tablesorter-pager js-uf-table-pager">
           <span class="pager-control first" title="First page"><i class="fas fa-angle-double-left"></i></span>
           <span class="pager-control prev" title="Previous page"><i class="fas fa-angle-left"></i></span>
           <span class="pagedisplay"></span>
           <span class="pager-control next" title="Next page"><i class="fas fa-angle-right"></i></span>
           <span class="pager-control last" title= "Last page"><i class="fas fa-angle-double-right"></i></span>
           <br><br>
           Jump to Page: <select class="gotoPage"></select> &bull; Show:
           <select class="pagesize">
               <option value="5">5</option>
               <option value="10">10</option>
           </select>
       </div>

       <button class="btn btn-sm btn-default js-uf-table-download">Download CSV</button>
   </div>
 *
 * Initialize ufTable on your container object:
 *
 * $("#widget-users").ufTable(options);
 *
 * `options` is an object containing any of the following parameters:
 * @param {string} dataUrl The absolute URL from which to fetch table data.
 * @param {mixed} addParams An object containing any additional key-value pairs that you want appended to the AJAX requests.
 * @param {mixed} tablesorter An object containing tablesorter's configuration options (https://mottie.github.io/tablesorter/docs/#Configuration)
 * @param {mixed} pager An object containing tablesorter's paging options (https://mottie.github.io/tablesorter/docs/#pager)
 *
 * == EVENTS ==
 *
 * ufTable triggers the following events:
 *
 * `pagerComplete.ufTable`: triggered when the tablesorter pager plugin has completed rendering of the table.
 *
 * == METHODS ==
 *
 * `getTableStateVars( table )`: fetches the current page size, page number, sort order, sort field, and column filters.
 * `refresh`: reload the table data
 *
 * UserFrosting https://www.userfrosting.com
 * @author Alexander Weissman <https://alexanderweissman.com>
 */
;(function($, window, document, undefined) {
    'use strict';

    // Define plugin name and defaults.
    var pluginName = 'ufTable',
        defaults = {
            DEBUG                : false,
            site                 : site, // global site variables
            dataUrl              : '',
            msgTarget            : $('#alerts-page'),
            addParams            : {},
            filterAllField       : '_all',
            useLoadingTransition : true,
            rowTemplate          : null,
            columnTemplates      : {},
            tablesorter     : {
                debug: false,
                theme     : 'bootstrap',
                widthFixed: true,
                // Set up pagination of data via an AJAX source
                // See http://jsfiddle.net/Mottie/uwZc2/
                // Also see https://mottie.github.io/tablesorter/docs/example-pager-ajax.html
                widgets: ['saveSort', 'sort2Hash', 'filter', 'pager', 'columnSelector', 'reflow2'],
                widgetOptions : {
                    columnSelector_layout : '<label><input type="checkbox"> <span>{name}</span></label>',
                    filter_cssFilter: 'form-control',
                    filter_saveFilters : true,
                    filter_serversideFiltering : true,
                    filter_selectSource : {
                        '.filter-select' : function() { return null; }
                    },

                    // apply disabled class name to the pager arrows when the rows at either extreme is visible
                    pager_updateArrows: true,

                    // starting page of the pager (zero based index)
                    pager_startPage: 0,

                    // Number of visible rows
                    pager_size: 10,

                    // Save pager page & size if the storage script is loaded (requires $.tablesorter.storage in jquery.tablesorter.widgets.js)
                    pager_savePages: true,

                    // if true, the table will remain the same height no matter how many records are displayed. The space is made up by an empty
                    // table row set to a height to compensate; default is false
                    pager_fixedHeight: false,

                    // remove rows from the table to speed up the sort of large tables.
                    // setting this to false, only hides the non-visible rows; needed if you plan to add/remove rows with the pager enabled.
                    pager_removeRows: false, // removing rows in larger tables speeds up the sort

                    // target the pager markup - see the HTML block below
                    pager_css: {
                        errorRow    : 'uf-table-error-row', // error information row
                        disabled    : 'disabled' // Note there is no period "." in front of this class name
                    },

                    // Must be initialized with a 'data' key
                    pager_ajaxObject: {
                        data: {},
                        dataType: 'json'
                    },

                    // hash prefix
                    sort2Hash_hash              : '#',
                    // don't '#' or '=' here
                    sort2Hash_separator         : '|',
                    // this option > table ID > table index on page
                    sort2Hash_tableId           : null,
                    // if true, show header cell text instead of a zero-based column index
                    sort2Hash_headerTextAttr    : 'data-column-name',
                    // direction text shown in the URL e.g. [ 'asc', 'desc' ]
                    sort2Hash_directionText     : [ 'asc', 'desc' ], // default values
                    // if true, override saveSort widget sort, if used & stored sort is available
                    sort2Hash_overrideSaveSort  : true, // default = false
                }
            }
        };

    // Constructor
    function Plugin (element, options) {
        this.element = element[0];
        this.$element = $(this.element);

        var lateDefaults = {
            download: {
                button: this.$element.find('.js-uf-table-download'),
                callback: $.proxy(this._onDownload, this)
            },
            info: {
                container: this.$element.find('.js-uf-table-info'),
                callback: $.proxy(this._renderInfoMessages, this)
            },
            overlay: {
                container: this.$element.find('.js-uf-table-overlay')
            },
            tableElement: this.$element.find('.tablesorter'),
            tablesorter: {
                widgetOptions: {
                    columnSelector_container : this.$element.find('.js-uf-table-cs-options'),
                    filter_external          : this.$element.find('.js-uf-table-search input'),

                    // Pager selectors
                    pager_selectors: {
                        container   : this.$element.find('.js-uf-table-pager'),
                        first       : '.first',       // go to first page arrow
                        prev        : '.prev',        // previous page arrow
                        next        : '.next',        // next page arrow
                        last        : '.last',        // go to last page arrow
                        gotoPage    : '.gotoPage',    // go to page selector - select dropdown that sets the current page
                        pageDisplay : '.pagedisplay', // location of where the "output" is displayed
                        pageSize    : '.pagesize'     // page size selector - select dropdown that sets the "size" option
                    },
                    // We need to use $.proxy to properly bind the context for callbacks that will be called by Tablesorter

                    // Generate the URL for the AJAX request, with the relevant parameters
                    pager_customAjaxUrl: $.proxy(this._generateUrl, this),

                    // Callback to process the response from the AJAX request
                    pager_ajaxProcessing: $.proxy(this._processAjax, this),

                    // Callback to display errors
                    pager_ajaxError: $.proxy(this._pagerAjaxError, this),

                    sort2Hash_encodeHash: $.proxy(this._encodeHash, this),

                    sort2Hash_decodeHash: $.proxy(this._decodeHash, this),

                    sort2Hash_cleanHash: $.proxy(this._cleanHash, this)
                }
            }
        };
        this.settings = $.extend(true, {}, defaults, lateDefaults, options);
        this._defaults = defaults;
        this._name = pluginName;
        this._debugAjax = (typeof this.settings.site !== 'undefined') && this.settings.site.debug.ajax;

        // Fall back to attributes from data-*, default values if not specified in options
        var pagerContainer = this.settings.tablesorter.widgetOptions.pager_selectors.container;
        var infoContainer = this.settings.info.container;
        var dataAttributeDefaults = {
            info: {
                messageEmptyRows: infoContainer.data('message-empty-rows') ?
                                  infoContainer.data('message-empty-rows') :
                                  "Sorry, we've got nothing here."
            },
            tablesorter: {
                widgetOptions: {
                    // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows}
                    // also {page:input} & {startRow:input} will add a modifiable input in place of the value
                    pager_output: pagerContainer.data('output-template') ?
                                  pagerContainer.data('output-template') :
                                  '{startRow} to {endRow} of {filteredRows} ({totalRows})' // default if not set on data-* attribute
                }
            }
        };

        this.settings = $.extend(true, {}, dataAttributeDefaults, this.settings);

        // Check that tableElement exists
        var tableElement = this.settings.tableElement;
        if (!tableElement.length) {
            if (window.console && console.error) {
                console.error('ufTable could not be initialized: wrapper element does not exist, or does not contain a matched tableElement (see https://learn.userfrosting.com/client-side-code/components/tables )');
            }
            return;
        }

        // Copy over dataUrl to pager_ajaxUrl
        this.settings.tablesorter.widgetOptions.pager_ajaxUrl = this.settings.dataUrl;

        // Set up 'loading' overlays
        if (this.settings.useLoadingTransition) {
            var overlay = this.settings.overlay.container;
            tableElement.bind('sortStart filterStart pageMoved', function() {
                overlay.removeClass('hidden');
            }).bind('pagerComplete updateComplete', function() {
                overlay.addClass('hidden');
            });
        }

        // Set up tablesorter and pager
        this.ts = tableElement.tablesorter(this.settings.tablesorter);

        // Map default column template selectors based on data-column-template attribute in each column header
        var columns = this.ts[0].config.$headerIndexed;
        var columnTemplates = {};
        for (var col = 0; col < columns.length; col++) {
            var columnName = columns[col].data('column-name');
            if (!columnName && this.settings.DEBUG) {
                console.error('Column number ' + col + ' is missing a data-column-name attribute.');
            }
            columnTemplates[columnName] = columns[col].data('column-template');
        }

        // Merge in any column template selectors that were set in the ctor options
        columnTemplates = $.extend(true, columnTemplates, this.settings.columnTemplates);

        // Locate and compile templates for any string-identified column renderers
        // At the same time, build out a numerically indexed array of templates
        this.columnTemplatesIndexed = [];
        for (var col = 0; col < columns.length; col++) {
            var columnName = columns[col].data('column-name');
            if (!columnTemplates[columnName] && this.settings.DEBUG) {
                console.error("No template found for column '" + columnName + "'.");
            }
            var columnTemplate = columnTemplates[columnName];
            if (typeof columnTemplate === 'string') {
                this.columnTemplatesIndexed.push(Handlebars.compile($(columnTemplate).html()));
            } else {
                this.columnTemplatesIndexed.push(columnTemplate);
            }
        }

        // Locate and compile row template
        this.rowTemplate = Handlebars.compile('<tr>');
        // If rowTemplateSelector is set, then find the DOM element that it references, which contains the template
        if (this.settings.rowTemplate) {
            var rowTemplate = this.settings.rowTemplate;
            if (typeof rowTemplate === 'string') {
                this.rowTemplate = Handlebars.compile($(this.settings.rowTemplate).html());
            } else {
                this.rowTemplate = rowTemplate;
            }
        }

        // Link CSV download button
        this.settings.download.button.on('click', this.settings.download.callback);

        // Allow clicking on the labels in the table menu without closing the menu
        $(this.settings.tablesorter.widgetOptions.columnSelector_container).find('label').on('click', function(e) {
            e.stopPropagation();
        });

        // Propagate our own pagerComplete event
        this.ts.on('pagerComplete', $.proxy(function () {
            this.$element.trigger('pagerComplete.ufTable');
        }, this));

        // Show info messages when there are no rows/no results
        this.ts.on('filterEnd filterReset pagerComplete', this.settings.info.callback);

        // Detect changes to element attributes
        this.$element.attrchange({
            callback: function (event) {
                this.element = event.target;
            }.bind(this)
        });

        return this;
    }

    /**
     * Get state variables for this table, as required by the AJAX data source: sorts, filters, size, page
     */
    Plugin.prototype.getTableStateVars = function(table) {
        var base = this;

        // Get sort column and order
        var sortOrders = {
            '0': 'asc',
            '1': 'desc'
        };

        // Set sorts in URL.  Assumes each th has a data-column-name attribute that corresponds to the name in the API
        var sortList = table.config.sortList;
        var sorts = {};
        for (var i = 0; i < sortList.length; i++) {
            var columnIndex = sortList[i][0];
            var columnDirection = sortOrders[sortList[i][1]];   // Converts to 'asc' or 'desc'
            if (sortList[i]) {
                var columnName = table.config.$headerIndexed[columnIndex].data('column-name');
                sorts[columnName] = columnDirection;
            }
        }

        // Set filters in URL.  Assumes each th has a data-column-name attribute that corresponds to the name in the API
        var filterList = base.getSavedFilters(table);
        var filters = {};
        for (i = 0; i < filterList.length; i++) {
            if (filterList[i]) {
                var columnName = base.settings.filterAllField;

                if (table.config.$headerIndexed[i]) {
                    columnName = table.config.$headerIndexed[i].data('column-name');
                }

                filters[columnName] = filterList[i];
            }
        }

        var state = {
            size: table.config.pager.size,
            page: table.config.pager.page,
            sorts: sorts,
            filters: filters
        };

        return state;
    };

    /**
     * Get saved filters from the browser local storage. Those should always be up to date
     */
    Plugin.prototype.getSavedFilters = function(table) {

        // Fallback to `getFilters` or empty in case of failure
        var filterList = $.tablesorter.getFilters(table) || [];

        // Overwrite list with saved filter for filter-select not setup by ts
        var isArray, saved,
            wo = table.config.widgetOptions;
        if ( wo.filter_saveFilters && $.tablesorter.storage ) {
            saved = $.tablesorter.storage( table, 'tablesorter-filters' ) || [];
            isArray = $.isArray( saved );
            // make sure we're not just getting an empty array
            if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) {
                filterList = $.tablesorter.filter.processFilters( saved );
            }
        }

        return filterList;
    };

    /**
     * Refresh the table
     */
    Plugin.prototype.refresh = function() {
        this.ts.trigger('pagerUpdate');
    };

    /**
     * Generate the AJAX url.
     * Used as the default callback for pager_customAjaxUrl
     * @private
     */
    Plugin.prototype._generateUrl = function(table, url) {
        var tableState = this.getTableStateVars(table);

        if (this.settings.DEBUG) {
            console.log(tableState);
        }

        $.extend(table.config.pager.ajaxObject.data, tableState);

        // Merge in any additional parameters
        $.extend(true, table.config.pager.ajaxObject.data, this.settings.addParams);

        return url;
    };

    /**
     * Process data returned from the AJAX request and rendering the table cells.
     * Used as the default callback for pager_ajaxProcessing
     * @private
     */
    Plugin.prototype._processAjax = function(data) {
        var ts = this.ts[0];
        var json = {},
            rows = '';

        if (data) {
            var size = data.rows.length;

            // Render table rows and cells via Handlebars
            for (var row = 0; row < size; row++) {
                var cellData = {
                    rownum: row,
                    row   : data.rows[row],       // It is safe to use the data from the API because Handlebars escapes HTML
                    site  : this.settings.site
                };

                rows += this.rowTemplate(cellData);

                for (var col = 0; col < this.columnTemplatesIndexed.length; col++) {
                    rows += this.columnTemplatesIndexed[col](cellData);
                }

                rows += '</tr>';
            }

            // Initialize any dropdown filters
            var columns = ts.config.$headerIndexed;
            this._ajaxInitFilterSelects(columns, data.listable);

            json.total = data.count;  // Get total rows without pagination
            json.filteredRows = data.count_filtered; // no filtering
            json.rows = $(rows);
            json.output = data.output;
        } else {
            json.total = 0;
            json.filteredRows = 0;
            json.rows = '';
        }

        return json;
    };

    /**
     * Initialize filter select menus using the ajax `listable` values
     * @private
     */
    Plugin.prototype._ajaxInitFilterSelects = function(columns, listable) {
        var ts = this.ts[0];
        var filters = this.getSavedFilters(ts);
        // Find columns with `.filter-select` and match them to column numbers based on their data-column-name
        for (var col = 0; col < columns.length; col++) {
            var column = columns[col];
            // If the column is designated for filter-select, get the listables from the data and recreate it
            if (column.hasClass('filter-select')) {
                var columnName = column.data('column-name');
                if (listable[columnName]) {
                    $.tablesorter.filter.buildSelect(ts, col, listable[columnName], true);
                    // If there is a filter actually set for this column, update the selected option.
                    if (filters[col]) {
                        var selectControl = $(ts).find(".tablesorter-filter[data-column='" + col + "']");
                        selectControl.val(filters[col]);
                    }
                }
            }
        }
    };

    /**
     * Implements handler for the "download CSV" button.
     * Default callback for download.callback
     * @private
     */
    Plugin.prototype._onDownload = function () {
        var tableState = this.getTableStateVars(this.ts[0]);
        tableState.format = 'csv';
        delete tableState.page;
        delete tableState.size;

        // Merge in any additional request parameters
        $.extend(true, tableState, this.settings.addParams);

        // Causes download to begin
        window.location = this.settings.dataUrl + '?' + $.param(tableState);
    };

    /**
     * Handle pager ajax errors.
     * @private
     */
    Plugin.prototype._pagerAjaxError = function(c, jqXHR, settings, exception) {
        this._ajaxError(jqXHR);

        // Let TS handle the in-table error message
        return '';
    };

    /**
     * Handle ajax error
     * @private
     */
    Plugin.prototype._ajaxError = function(jqXHR) {
        if (typeof jqXHR === 'object') {
            // Error messages
            if (this._debugAjax && jqXHR.responseText) {
                document.write(jqXHR.responseText);
                document.close();
            } else {
                if (this.settings.DEBUG) {
                    console.log('Error (' + jqXHR.status + '): ' + jqXHR.responseText );
                }
                // Display errors on failure
                // TODO: ufAlerts widget should have a 'destroy' method
                if (!this.settings.msgTarget.data('ufAlerts')) {
                    this.settings.msgTarget.ufAlerts();
                } else {
                    this.settings.msgTarget.ufAlerts('clear');
                }

                this.settings.msgTarget.ufAlerts('fetch').ufAlerts('render');
            }
        }
    };

    /**
     * Render info messages, such as when there are no results.
     * Default callback for info.callback
     * @private
     */
    Plugin.prototype._renderInfoMessages = function () {
        var table = this.ts[0];
        var infoMessages = this.settings.info.container;
        if (table.config.pager) {
            infoMessages.html('');
            var fr = table.config.pager.filteredRows;
            if (fr === 0) {
                infoMessages.html(this.settings.info.messageEmptyRows);
            }
        }
    };

    /**
     * Encode the current table state variables into a URL hash.
     * Default callback for sort2Hash_encodeHash
     * @private
     */
    Plugin.prototype._encodeHash = function(config, tableId, component, value, rawValue) {
        var wo = config.widgetOptions;
        if ( component === 'filter' ) {
            // rawValue is an array of filter values, numerically indexed
            var encodedFilters = '';
            var len = rawValue.length;
            for (var index = 0; index < len; index++) {
                if (rawValue[index]) {
                    var columnName = this.settings.filterAllField;
                    if (config.$headerIndexed[index]) {
                        columnName = $(config.$headerIndexed[index][0]).attr(wo.sort2Hash_headerTextAttr);
                    }
                    encodedFilters += '&filter[' + tableId + '][' + columnName + ']=' + encodeURIComponent(rawValue[index]);
                }
            }
            return encodedFilters;
        } else if ( component === 'sort' ) {
            // rawValue is an array of sort pairs [columnNum, sortDirection]
            var encodedFilters = '';
            var len = rawValue.length;
            for (var index = 0; index < len; index++) {
                var columnNum = rawValue[index][0];
                var sortDirection = rawValue[index][1];
                var columnName = $(config.$headerIndexed[columnNum][0]).attr(wo.sort2Hash_headerTextAttr);
                encodedFilters += '&sort[' + tableId + '][' + columnName + ']=' + wo.sort2Hash_directionText[sortDirection];
            }
            return encodedFilters;
        }
        return false;
    };

    /**
     * Decode the current table state variables from the URL hash.
     * Default callback for sort2Hash_decodeHash
     * @private
     */
    Plugin.prototype._decodeHash = function(config, tableId, component) {
        var wo = config.widgetOptions;
        var result;
        // Convert hash into JSON object
        var urlObject = $.String.deparam(window.location.hash);
        delete urlObject[wo.sort2Hash_hash];  // Remove hash character
        if (component === 'filter') {
            var decodedFilters = [];
            // Extract filter names and values for the specified table
            var pageFilters = urlObject.filter ? urlObject.filter : [];
            if (pageFilters[tableId]) {
                var tableFilters = pageFilters[tableId];
                // Build a numerically indexed array of filter values
                var len = config.$headerIndexed.length;
                for (var index = 0; index < len; index++) {
                    var columnName = $(config.$headerIndexed[index][0]).attr(wo.sort2Hash_headerTextAttr);
                    if (tableFilters[columnName] && tableFilters[columnName] != this.settings.filterAllField) {
                        decodedFilters.push(tableFilters[columnName]);
                    } else {
                        decodedFilters.push('');
                    }
                }
                // Convert array of filter values to a delimited string
                result = decodedFilters.join(wo.sort2Hash_separator);
                // make sure to use decodeURIComponent on the result
                return decodeURIComponent(result);
            } else {
                return '';
            }
        }
        return false;
    };

    /**
     * Clean up URL hash.
     * Default callback for sort2Hash_cleanHash
     * @private
     */
    Plugin.prototype._cleanHash = function(config, tableId, component, hash) {
        var wo = config.widgetOptions;
        // Convert hash to JSON object
        var urlObject = $.String.deparam(hash);
        delete urlObject[wo.sort2Hash_hash];  // Remove hash character
        // Remove specified component for specified table
        if (urlObject[component]) {
            if (urlObject[component][tableId]) {
                delete urlObject[component][tableId];
            }
            // Delete entire component if no other tables remaining
            if (jQuery.isEmptyObject(urlObject[component])) {
                delete urlObject[component];
            }
        }
        // Convert modified JSON object back into serialized representation
        var result = decodeURIComponent(jQuery.param(urlObject));
        return result.length ? result : '';
    };

    // Handles instantiation and access to non-private methods.
    $.fn[pluginName] = function(methodOrOptions) {
        // Grab plugin instance
        var instance = $(this).data(pluginName);
        // If undefined or object, initialize plugin.
        if (methodOrOptions === undefined || typeof methodOrOptions === 'object') {
            // Only initialize if not previously done.
            if (!instance) {
                $(this).data(pluginName, new Plugin(this, methodOrOptions));
            }
            return this;
        // Otherwise ensure first parameter is a valid string, and is the name of an actual function.
        } else if (typeof methodOrOptions === 'string' && typeof instance[methodOrOptions] === 'function') {
            // Ensure not a private function
            if (methodOrOptions.indexOf('_') !== 0) {
                return instance[methodOrOptions]( Array.prototype.slice.call(arguments, 1));
            }
            else {
                console.warn( 'Method ' +  methodOrOptions + ' is private!' );
            }
        } else {
            console.warn( 'Method ' +  methodOrOptions + ' does not exist.' );
        }
    };
})(jQuery, window, document);
