From f662b5de8a55e03002dd5338b2dce1e11daa3971 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 21 Mar 2013 23:43:23 -0700 Subject: [PATCH] Added pagination to Missing --- .../Content/Bootstrap/pagination.less | 2 +- .../Content/tablesorter.pager.css | 16 + NzbDrone.Backbone/Index.html | 2 + .../JsLibraries/jquery.tablesorter.js | 1978 ++++++++++------- .../JsLibraries/jquery.tablesorter.pager.js | 575 +++++ .../Missing/MissingCollectionTemplate.html | 25 + .../Missing/MissingCollectionView.js | 53 +- .../Series/Index/SeriesIndexCollectionView.js | 39 +- 8 files changed, 1836 insertions(+), 854 deletions(-) create mode 100644 NzbDrone.Backbone/Content/tablesorter.pager.css create mode 100644 NzbDrone.Backbone/JsLibraries/jquery.tablesorter.pager.js diff --git a/NzbDrone.Backbone/Content/Bootstrap/pagination.less b/NzbDrone.Backbone/Content/Bootstrap/pagination.less index a789db2d2..802213c31 100644 --- a/NzbDrone.Backbone/Content/Bootstrap/pagination.less +++ b/NzbDrone.Backbone/Content/Bootstrap/pagination.less @@ -4,7 +4,7 @@ // Space out pagination from surrounding content .pagination { - margin: @baseLineHeight 0; +// margin: @baseLineHeight 0; } .pagination ul { diff --git a/NzbDrone.Backbone/Content/tablesorter.pager.css b/NzbDrone.Backbone/Content/tablesorter.pager.css new file mode 100644 index 000000000..f94ce778a --- /dev/null +++ b/NzbDrone.Backbone/Content/tablesorter.pager.css @@ -0,0 +1,16 @@ +/*** css used when "updateArrows" option is true ***/ +/* the pager itself gets a disabled class when the number of rows is less than the size */ +.tablesorter-pager.disabled { + display: none; +} +/* hide or fade out pager arrows when the first or last row is visible */ +.tablesorter-pager .disabled { + /* visibility: hidden */ + opacity: 0.5; + filter: alpha(opacity=50); + cursor: default; +} + +.pagedisplay-container { + padding-top: 5px; +} \ No newline at end of file diff --git a/NzbDrone.Backbone/Index.html b/NzbDrone.Backbone/Index.html index fa06da107..519d1e37a 100644 --- a/NzbDrone.Backbone/Index.html +++ b/NzbDrone.Backbone/Index.html @@ -17,6 +17,7 @@ + @@ -111,6 +112,7 @@ + diff --git a/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.js b/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.js index e8e232348..7820a3e66 100644 --- a/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.js +++ b/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.js @@ -1,1031 +1,1383 @@ -/* - * - * TableSorter 2.0 - Client-side table sorting with ease! - * Version 2.0.5b - * @requires jQuery v1.2.3 - * +/*! + * TableSorter 2.7.12 - Client-side table sorting with ease! + * @requires jQuery v1.2.6+ + * * Copyright (c) 2007 Christian Bach * Examples and docs at: http://tablesorter.com * Dual licensed under the MIT and GPL licenses: * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html - * - */ -/** - * - * @description Create a sortable table with multi-column sorting capabilitys - * - * @example $('table').tablesorter(); - * @desc Create a simple tablesorter interface. - * - * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); - * @desc Create a tablesorter interface and sort on the first and secound column column headers. - * - * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); - * - * @desc Create a tablesorter interface and disableing the first and second column headers. - * - * - * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); - * - * @desc Create a tablesorter interface and set a column parser for the first - * and second column. - * - * - * @param Object - * settings An object literal containing key/value pairs to provide - * optional settings. - * - * - * @option String cssHeader (optional) A string of the class name to be appended - * to sortable tr elements in the thead of the table. Default value: - * "header" - * - * @option String cssAsc (optional) A string of the class name to be appended to - * sortable tr elements in the thead on a ascending sort. Default value: - * "headerSortUp" - * - * @option String cssDesc (optional) A string of the class name to be appended - * to sortable tr elements in the thead on a descending sort. Default - * value: "headerSortDown" - * - * @option String sortInitialOrder (optional) A string of the inital sorting - * order can be asc or desc. Default value: "asc" - * - * @option String sortMultisortKey (optional) A string of the multi-column sort - * key. Default value: "shiftKey" - * - * @option String textExtraction (optional) A string of the text-extraction - * method to use. For complex html structures inside td cell set this - * option to "complex", on large tables the complex option can be slow. - * Default value: "simple" - * - * @option Object headers (optional) An array containing the forces sorting - * rules. This option let's you specify a default sorting rule. Default - * value: null - * - * @option Array sortList (optional) An array containing the forces sorting - * rules. This option let's you specify a default sorting rule. Default - * value: null - * - * @option Array sortForce (optional) An array containing forced sorting rules. - * This option let's you specify a default sorting rule, which is - * prepended to user-selected rules. Default value: null - * - * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever - * to use String.localeCampare method or not. Default set to true. - * - * - * @option Array sortAppend (optional) An array containing forced sorting rules. - * This option let's you specify a default sorting rule, which is - * appended to user-selected rules. Default value: null - * - * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter - * should apply fixed widths to the table columns. This is usefull when - * using the pager companion plugin. This options requires the dimension - * jquery plugin. Default value: false - * - * @option Boolean cancelSelection (optional) Boolean flag indicating if - * tablesorter should cancel selection of the table headers text. - * Default value: true - * - * @option Boolean debug (optional) Boolean flag indicating if tablesorter - * should display debuging information usefull for development. - * + * * @type jQuery - * * @name tablesorter - * * @cat Plugins/Tablesorter - * * @author Christian Bach/christian.bach@polyester.se + * @contributor Rob Garrison/https://github.com/Mottie/tablesorter */ - -(function ($) { +/*jshint browser:true, jquery:true, unused:false, expr: true */ +/*global console:false, alert:false */ +!(function($) { + "use strict"; $.extend({ - tablesorter: new - function () { - - var parsers = [], - widgets = []; - - this.defaults = { - cssHeader: "header", - cssAsc: "headerSortUp", - cssDesc: "headerSortDown", - cssChildRow: "expand-child", - sortInitialOrder: "asc", - sortMultiSortKey: "shiftKey", - sortForce: null, - sortAppend: null, - sortLocaleCompare: true, - textExtraction: "simple", - parsers: {}, widgets: [], - widgetZebra: { - css: ["even", "odd"] - }, headers: {}, widthFixed: false, - cancelSelection: true, - sortList: [], + /*jshint supernew:true */ + tablesorter: new function() { + + var ts = this; + + ts.version = "2.7.12"; + + ts.parsers = []; + ts.widgets = []; + ts.defaults = { + + // *** appearance + theme : 'default', // adds tablesorter-{theme} to the table for styling + widthFixed : false, // adds colgroup to fix widths of columns + showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered. + + headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = (class from cssIcon) + onRenderTemplate : null, // function(index, template){ return template; }, (template is a string) + onRenderHeader : null, // function(index){}, (nothing to return) + + // *** functionality + cancelSelection : true, // prevent text selection in the header + dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd" + sortMultiSortKey : 'shiftKey', // key used to select additional columns + sortResetKey : 'ctrlKey', // key used to remove sorting on a column + usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" + delayInit : false, // if false, the parsed table contents will not update until the first sort + serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used. + + // *** sort options + headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. + ignoreCase : true, // ignore case while sorting + sortForce : null, // column(s) first sorted; always applied + sortList : [], // Initial sort order; applied initially; updated when manually sorted + sortAppend : null, // column(s) sorted last; always applied + + sortInitialOrder : 'asc', // sort direction on first click + sortLocaleCompare: false, // replace equivalent character (accented characters) + sortReset : false, // third click on the header will reset column to default - unsorted + sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns + + emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero + stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero + textExtraction : 'simple', // text extraction method/function - function(node, table, cellIndex){} + textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort + + // *** widget options + widgets: [], // method to add widgets, e.g. widgets: ['zebra'] + widgetOptions : { + zebra : [ 'even', 'odd' ] // zebra widget alternating row class names + }, + initWidgets : true, // apply widgets on tablesorter initialization + + // *** callbacks + initialized : null, // function(table){}, + + // *** css class names + tableClass : 'tablesorter', + cssAsc : 'tablesorter-headerAsc', + cssChildRow : 'tablesorter-childRow', // previously "expand-child" + cssDesc : 'tablesorter-headerDesc', + cssHeader : 'tablesorter-header', + cssHeaderRow : 'tablesorter-headerRow', + cssIcon : 'tablesorter-icon', // if this class exists, a will be added to the header automatically + cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name + cssProcessing : 'tablesorter-processing', // processing icon applied to header during sort/filter + + // *** selectors + selectorHeaders : '> thead th, > thead td', + selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort + selectorRemove : '.remove-me', + + // *** advanced + debug : false, + + // *** Internal variables headerList: [], - dateFormat: "us", - decimal: '/\.|\,/g', - onRenderHeader: null, - selectorHeaders: 'thead th', - debug: false - }; + empties: {}, + strings: {}, + parsers: [] - /* debuging utils */ - - function benchmark(s, d) { - log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); - } + // deprecated; but retained for backwards compatibility + // widgetZebra: { css: ["even", "odd"] } - this.benchmark = benchmark; + }; + /* debuging utils */ function log(s) { - if (typeof console != "undefined" && typeof console.debug != "undefined") { + if (typeof console !== "undefined" && typeof console.log !== "undefined") { console.log(s); } else { alert(s); } } - /* parsers utils */ - - function buildParserCache(table, $headers) { - - if (table.config.debug) { - var parsersDebug = ""; - } - - if (table.tBodies.length == 0) return; // In the case of empty tables - var rows = table.tBodies[0].rows; - - if (rows[0]) { - - var list = [], - cells = rows[0].cells, - l = cells.length; - - for (var i = 0; i < l; i++) { - - var p = false; - - if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { - - p = getParserById($($headers[i]).metadata().sorter); - - } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { - - p = getParserById(table.config.headers[i].sorter); - } - if (!p) { - - p = detectParserForColumn(table, rows, -1, i); - } + function benchmark(s, d) { + log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); + } - if (table.config.debug) { - parsersDebug += "column:" + i + " parser:" + p.id + "\n"; - } + ts.benchmark = benchmark; - list.push(p); + function getElementText(table, node, cellIndex) { + if (!node) { return ""; } + var c = table.config, + t = c.textExtraction, text = ""; + if (t === "simple") { + if (c.supportsTextContent) { + text = node.textContent; // newer browsers support this + } else { + text = $(node).text(); + } + } else { + if (typeof(t) === "function") { + text = t(node, table, cellIndex); + } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { + text = t[cellIndex](node, table, cellIndex); + } else { + text = c.supportsTextContent ? node.textContent : $(node).text(); } } - - if (table.config.debug) { - log(parsersDebug); - } - - return list; - }; + return $.trim(text); + } function detectParserForColumn(table, rows, rowIndex, cellIndex) { - var l = parsers.length, + var i, l = ts.parsers.length, node = false, - nodeValue = false, + nodeValue = '', keepLooking = true; - while (nodeValue == '' && keepLooking) { + while (nodeValue === '' && keepLooking) { rowIndex++; if (rows[rowIndex]) { - node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); - nodeValue = trimAndGetNodeText(table.config, node); + node = rows[rowIndex].cells[cellIndex]; + nodeValue = getElementText(table, node, cellIndex); if (table.config.debug) { - log('Checking if value was empty on row:' + rowIndex); + log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': ' + nodeValue); } } else { keepLooking = false; } } - for (var i = 1; i < l; i++) { - if (parsers[i].is(nodeValue, table, node)) { - return parsers[i]; + for (i = 1; i < l; i++) { + if (ts.parsers[i].is && ts.parsers[i].is(nodeValue, table, node)) { + return ts.parsers[i]; } } // 0 is always the generic parser (text) - return parsers[0]; - } - - function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { - return rows[rowIndex].cells[cellIndex]; - } - - function trimAndGetNodeText(config, node) { - return $.trim(getElementText(config, node)); + return ts.parsers[0]; } - function getParserById(name) { - var l = parsers.length; - for (var i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() == name.toLowerCase()) { - return parsers[i]; + function buildParserCache(table) { + var c = table.config, + // update table bodies in case we start with an empty table + tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'), + rows, list, l, i, h, ch, p, parsersDebug = ""; + if ( tb.length === 0) { + return c.debug ? log('*Empty table!* Not building a parser cache') : ''; + } + rows = tb[0].rows; + if (rows[0]) { + list = []; + l = rows[0].cells.length; + for (i = 0; i < l; i++) { + // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! + // More fixes to this selector to work properly in iOS and jQuery 1.8+ (issue #132 & #174) + h = c.$headers.filter(':not([colspan])'); + h = h.add( c.$headers.filter('[colspan="1"]') ) // ie8 fix + .filter('[data-column="' + i + '"]:last'); + ch = c.headers[i]; + // get column parser + p = ts.getParserById( ts.getData(h, ch, 'sorter') ); + // empty cells behaviour - keeping emptyToBottom for backwards compatibility + c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); + // text strings behaviour in numerical sorts + c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; + if (!p) { + p = detectParserForColumn(table, rows, -1, i); + } + if (c.debug) { + parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; + } + list.push(p); } } - return false; + if (c.debug) { + log(parsersDebug); + } + return list; } /* utils */ - function buildCache(table) { - - if (table.config.debug) { - var cacheTime = new Date(); + var b = table.tBodies, + tc = table.config, + totalRows, + totalCells, + parsers = tc.parsers, + t, v, i, j, k, c, cols, cacheTime, colMax = []; + tc.cache = {}; + // if no parsers found, return - it's an empty table. + if (!parsers) { + return tc.debug ? log('*Empty table!* Not building a cache') : ''; } - - var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, - totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, - parsers = table.config.parsers, - cache = { - row: [], - normalized: [] - }; - - for (var i = 0; i < totalRows; ++i) { - - /** Add the table data to main data array */ - var c = $(table.tBodies[0].rows[i]), - cols = []; - - // if this is a child row, add it to the last row's children and - // continue to the next row - if (c.hasClass(table.config.cssChildRow)) { - cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); - // go to the next for loop - continue; - } - - cache.row.push(c); - - for (var j = 0; j < totalCells; ++j) { - cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); - } - - cols.push(cache.normalized.length); // add position for rowCache - cache.normalized.push(cols); - cols = null; - }; - - if (table.config.debug) { - benchmark("Building cache for " + totalRows + " rows:", cacheTime); + if (tc.debug) { + cacheTime = new Date(); } - - return cache; - }; - - function getElementText(config, node) { - - var text = ""; - - if (!node) return ""; - - if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; - - if (config.textExtraction == "simple") { - if (config.supportsTextContent) { - text = node.textContent; - } else { - if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { - text = node.childNodes[0].innerHTML; - } else { - text = node.innerHTML; + // processing icon + if (tc.showProcessing) { + ts.isProcessing(table, true); + } + for (k = 0; k < b.length; k++) { + tc.cache[k] = { row: [], normalized: [] }; + // ignore tbodies with class name from css.cssInfoBlock + if (!$(b[k]).hasClass(tc.cssInfoBlock)) { + totalRows = (b[k] && b[k].rows.length) || 0; + totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; + for (i = 0; i < totalRows; ++i) { + /** Add the table data to main data array */ + c = $(b[k].rows[i]); + cols = []; + // if this is a child row, add it to the last row's children and continue to the next row + if (c.hasClass(tc.cssChildRow)) { + tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); + // go to the next for loop + continue; + } + tc.cache[k].row.push(c); + for (j = 0; j < totalCells; ++j) { + t = getElementText(table, c[0].cells[j], j); + // allow parsing if the string is empty, previously parsing would change it to zero, + // in case the parser needs to extract data from the table cell attributes + v = parsers[j].format(t, table, c[0].cells[j], j); + cols.push(v); + if ((parsers[j].type || '').toLowerCase() === "numeric") { + colMax[j] = Math.max(Math.abs(v), colMax[j] || 0); // determine column max value (ignore sign) + } + } + cols.push(tc.cache[k].normalized.length); // add position for rowCache + tc.cache[k].normalized.push(cols); } - } - } else { - if (typeof(config.textExtraction) == "function") { - text = config.textExtraction(node); - } else { - text = $(node).text(); + tc.cache[k].colMax = colMax; } } - return text; + if (tc.showProcessing) { + ts.isProcessing(table); // remove processing icon + } + if (tc.debug) { + benchmark("Building cache for " + totalRows + " rows", cacheTime); + } } - function appendToTable(table, cache) { - - if (table.config.debug) { - var appendTime = new Date() + // init flag (true) used by pager plugin to prevent widget application + function appendToTable(table, init) { + var c = table.config, + b = table.tBodies, + rows = [], + c2 = c.cache, + r, n, totalRows, checkCell, $bk, $tb, + i, j, k, l, pos, appendTime; + if (!c2[0]) { return; } // empty table - fixes #206 + if (c.debug) { + appendTime = new Date(); } - - var c = cache, - r = c.row, - n = c.normalized, - totalRows = n.length, - checkCell = (n[0].length - 1), - tableBody = $(table.tBodies[0]), - rows = []; - - - for (var i = 0; i < totalRows; i++) { - var pos = n[i][checkCell]; - - rows.push(r[pos]); - - if (!table.config.appender) { - - //var o = ; - var l = r[pos].length; - for (var j = 0; j < l; j++) { - tableBody[0].appendChild(r[pos][j]); + for (k = 0; k < b.length; k++) { + $bk = $(b[k]); + if (!$bk.hasClass(c.cssInfoBlock)) { + // get tbody + $tb = ts.processTbody(table, $bk, true); + r = c2[k].row; + n = c2[k].normalized; + totalRows = n.length; + checkCell = totalRows ? (n[0].length - 1) : 0; + for (i = 0; i < totalRows; i++) { + pos = n[i][checkCell]; + rows.push(r[pos]); + // removeRows used by the pager plugin + if (!c.appender || !c.removeRows) { + l = r[pos].length; + for (j = 0; j < l; j++) { + $tb.append(r[pos][j]); + } + } } - - // + // restore tbody + ts.processTbody(table, $tb, false); } } - - - - if (table.config.appender) { - - table.config.appender(table, rows); + if (c.appender) { + c.appender(table, rows); } - - rows = null; - - if (table.config.debug) { - benchmark("Rebuilt table:", appendTime); + if (c.debug) { + benchmark("Rebuilt table", appendTime); } - // apply table widgets - applyWidget(table); - + if (!init) { ts.applyWidget(table); } // trigger sortend - setTimeout(function () { - $(table).trigger("sortEnd"); - }, 0); - - }; - - function buildHeaders(table) { - - if (table.config.debug) { - var time = new Date(); - } - - var meta = ($.metadata) ? true : false; - - var header_index = computeTableHeaderCellIndexes(table); - - $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { - - this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; - // this.column = index; - this.order = formatSortingOrder(table.config.sortInitialOrder); - - - this.count = this.order; - - if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; - if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); - - if (!this.sortDisabled) { - var $th = $(this).addClass(table.config.cssHeader); - if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); - } - - // add cell to headerList - table.config.headerList[index] = this; - }); - - if (table.config.debug) { - benchmark("Built headers:", time); - log($tableHeaders); - } - - return $tableHeaders; - - }; + $(table).trigger("sortEnd", table); + } - // from: + // computeTableHeaderCellIndexes from: // http://www.javascripttoolbox.com/lib/table/examples.php // http://www.javascripttoolbox.com/temp/table_cellindex.html - - - function computeTableHeaderCellIndexes(t) { - var matrix = []; - var lookup = {}; - var thead = t.getElementsByTagName('THEAD')[0]; - var trs = thead.getElementsByTagName('TR'); - - for (var i = 0; i < trs.length; i++) { - var cells = trs[i].cells; - for (var j = 0; j < cells.length; j++) { - var c = cells[j]; - - var rowIndex = c.parentNode.rowIndex; - var cellId = rowIndex + "-" + c.cellIndex; - var rowSpan = c.rowSpan || 1; - var colSpan = c.colSpan || 1 - var firstAvailCol; - if (typeof(matrix[rowIndex]) == "undefined") { + function computeThIndexes(t) { + var matrix = [], + lookup = {}, + cols = 0, // determine the number of columns + trs = $(t).find('thead:eq(0), tfoot').children('tr'), // children tr in tfoot - see issue #196 + i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; + for (i = 0; i < trs.length; i++) { + cells = trs[i].cells; + for (j = 0; j < cells.length; j++) { + c = cells[j]; + rowIndex = c.parentNode.rowIndex; + cellId = rowIndex + "-" + c.cellIndex; + rowSpan = c.rowSpan || 1; + colSpan = c.colSpan || 1; + if (typeof(matrix[rowIndex]) === "undefined") { matrix[rowIndex] = []; } // Find first available column in the first row - for (var k = 0; k < matrix[rowIndex].length + 1; k++) { - if (typeof(matrix[rowIndex][k]) == "undefined") { + for (k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) === "undefined") { firstAvailCol = k; break; } } lookup[cellId] = firstAvailCol; - for (var k = rowIndex; k < rowIndex + rowSpan; k++) { - if (typeof(matrix[k]) == "undefined") { + cols = Math.max(firstAvailCol, cols); + // add data-column + $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex + for (k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) === "undefined") { matrix[k] = []; } - var matrixrow = matrix[k]; - for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow = matrix[k]; + for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { matrixrow[l] = "x"; } } } } + t.config.columns = cols; // may not be accurate if # header columns !== # tbody columns return lookup; } - function checkCellColSpan(table, rows, row) { - var arr = [], - r = table.tHead.rows, - c = r[row].cells; - - for (var i = 0; i < c.length; i++) { - var cell = c[i]; - - if (cell.colSpan > 1) { - arr = arr.concat(checkCellColSpan(table, headerArr, row++)); - } else { - if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { - arr.push(cell); - } - // headerArr[row] = (i+row); - } - } - return arr; - }; - - function checkHeaderMetadata(cell) { - if (($.metadata) && ($(cell).metadata().sorter === false)) { - return true; - }; - return false; - } - - function checkHeaderOptions(table, i) { - if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { - return true; - }; - return false; - } - - function checkHeaderOptionsSortingLocked(table, i) { - if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; - return false; + function formatSortingOrder(v) { + // look for "d" in "desc" order; return true + return (/^d/i.test(v) || v === 1); } - - function applyWidget(table) { - var c = table.config.widgets; - var l = c.length; - for (var i = 0; i < l; i++) { - getWidgetById(c[i]).format(table); + function buildHeaders(table) { + var header_index = computeThIndexes(table), ch, $t, + h, i, t, lock, time, $tableHeaders, c = table.config; + c.headerList = [], c.headerContent = []; + if (c.debug) { + time = new Date(); } - - } - - function getWidgetById(name) { - var l = widgets.length; - for (var i = 0; i < l; i++) { - if (widgets[i].id.toLowerCase() == name.toLowerCase()) { - return widgets[i]; + i = c.cssIcon ? '' : ''; // add icon if cssIcon option exists + $tableHeaders = $(table).find(c.selectorHeaders).each(function(index) { + $t = $(this); + ch = c.headers[index]; + c.headerContent[index] = this.innerHTML; // save original header content + // set up header template + t = c.headerTemplate.replace(/\{content\}/g, this.innerHTML).replace(/\{icon\}/g, i); + if (c.onRenderTemplate) { + h = c.onRenderTemplate.apply($t, [index, t]); + if (h && typeof h === 'string') { t = h; } // only change t if something is returned } - } - }; + this.innerHTML = '
' + t + '
'; // faster than wrapInner - function formatSortingOrder(v) { - if (typeof(v) != "Number") { - return (v.toLowerCase() == "desc") ? 1 : 0; - } else { - return (v == 1) ? 1 : 0; - } - } + if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } - function isValueInArray(v, a) { - var l = a.length; - for (var i = 0; i < l; i++) { - if (a[i][0] == v) { - return true; + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; + this.count = -1; // set to -1 because clicking on the header automatically adds one + if (ts.getData($t, ch, 'sorter') === 'false') { + this.sortDisabled = true; + $t.addClass('sorter-false'); + } else { + $t.removeClass('sorter-false'); + } + this.lockedOrder = false; + lock = ts.getData($t, ch, 'lockedOrder') || false; + if (typeof(lock) !== 'undefined' && lock !== false) { + this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; } + $t.addClass( (this.sortDisabled ? 'sorter-false ' : ' ') + c.cssHeader ); + // add cell to headerList + c.headerList[index] = this; + // add to parent in case there are multiple rows + $t.parent().addClass(c.cssHeaderRow); + }); + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); } - return false; + return $tableHeaders; } - function setHeadersCss(table, $headers, list, css) { + function setHeadersCss(table) { + var f, i, j, l, + c = table.config, + list = c.sortList, + css = [c.cssAsc, c.cssDesc], + // find the footer + $t = $(table).find('tfoot tr').children().removeClass(css.join(' ')); // remove all header information - $headers.removeClass(css[0]).removeClass(css[1]); - - var h = []; - $headers.each(function (offset) { - if (!this.sortDisabled) { - h[this.column] = $(this); + c.$headers.removeClass(css.join(' ')); + l = list.length; + for (i = 0; i < l; i++) { + // direction = 2 means reset! + if (list[i][1] !== 2) { + // multicolumn sorting updating - choose the :last in case there are nested columns + f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') ); + if (f.length) { + for (j = 0; j < f.length; j++) { + if (!f[j].sortDisabled) { + f.eq(j).addClass(css[list[i][1]]); + // add sorted class to footer, if it exists + if ($t.length) { + $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]); + } + } + } + } } - }); - - var l = list.length; - for (var i = 0; i < l; i++) { - h[list[i][0]].addClass(css[list[i][1]]); } } - function fixColumnWidth(table, $headers) { - var c = table.config; - if (c.widthFixed) { - var colgroup = $(''); - $("tr:first td", table.tBodies[0]).each(function () { - colgroup.append($('').css('width', $(this).width())); + // automatically add col group, and column sizes if set + function fixColumnWidth(table) { + if (table.config.widthFixed && $(table).find('colgroup').length === 0) { + var colgroup = $(''), + overallWidth = $(table).width(); + $(table.tBodies[0]).find("tr:first").children("td").each(function() { + colgroup.append($('').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%')); }); $(table).prepend(colgroup); - }; - } - - function updateHeaderSortCount(table, sortList) { - var c = table.config, - l = sortList.length; - for (var i = 0; i < l; i++) { - var s = sortList[i], - o = c.headerList[s[0]]; - o.count = s[1]; - o.count++; } } - /* sorting methods */ + function updateHeaderSortCount(table, list) { + var s, t, o, c = table.config, + sl = list || c.sortList; + c.sortList = []; + $.each(sl, function(i,v){ + // ensure all sortList values are numeric - fixes #127 + s = [ parseInt(v[0], 10), parseInt(v[1], 10) ]; + // make sure header exists + o = c.headerList[s[0]]; + if (o) { // prevents error if sorton array is wrong + c.sortList.push(s); + t = $.inArray(s[1], o.order); // fixes issue #167 + o.count = t >= 0 ? t : s[1] % (c.sortReset ? 3 : 2); + } + }); + } - function multisort(table, sortList, cache) { + function getCachedSortType(parsers, i) { + return (parsers && parsers[i]) ? parsers[i].type || '' : ''; + } - if (table.config.debug) { - var sortTime = new Date(); + // sort multiple columns + function multisort(table) { /*jshint loopfunc:true */ + var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, + sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length, + sortTime, i, j, k, c, colMax, cache, lc, s, e, order, orgOrderCol; + if (tc.serverSideSorting || !tc.cache[0]) { // empty table - fixes #206 + return; } - - var dynamicExp = "var sortWrapper = function(a,b) {", - l = sortList.length; - - // TODO: inline functions. - for (var i = 0; i < l; i++) { - - var c = sortList[i][0]; - var order = sortList[i][1]; - // var s = (getCachedSortType(table.config.parsers,c) == "text") ? - // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? - // "sortNumeric" : "sortNumericDesc"); - // var s = (table.config.parsers[c].type == "text") ? ((order == 0) - // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? - // makeSortNumeric(c) : makeSortNumericDesc(c)); - var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); - var e = "e" + i; - - dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c - // + "]); "; - dynamicExp += "if(" + e + ") { return " + e + "; } "; - dynamicExp += "else { "; - + if (tc.debug) { sortTime = new Date(); } + for (k = 0; k < bl; k++) { + colMax = tc.cache[k].colMax; + cache = tc.cache[k].normalized; + lc = cache.length; + orgOrderCol = (cache && cache[0]) ? cache[0].length - 1 : 0; + cache.sort(function(a, b) { + // cache is undefined here in IE, so don't use it! + for (i = 0; i < l; i++) { + c = sortList[i][0]; + order = sortList[i][1]; + // fallback to natural sort since it is more robust + s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; + s += order === 0 ? "" : "Desc"; + if (/Numeric/.test(s) && tc.strings[c]) { + // sort strings in numerical columns + if (typeof (tc.string[tc.strings[c]]) === 'boolean') { + dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); + } else { + dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; + } + } + var sort = $.tablesorter["sort" + s](table, a[c], b[c], c, colMax[c], dir); + if (sort) { return sort; } + } + return a[orgOrderCol] - b[orgOrderCol]; + }); } + if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); } + } - // if value is the same keep orignal order - var orgOrderCol = cache.normalized[0].length - 1; - dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; - - for (var i = 0; i < l; i++) { - dynamicExp += "}; "; + function resortComplete($table, callback){ + $table.trigger('updateComplete'); + if (typeof callback === "function") { + callback($table[0]); } + } - dynamicExp += "return 0; "; - dynamicExp += "}; "; - - if (table.config.debug) { - benchmark("Evaling expression:" + dynamicExp, new Date()); + function checkResort($table, flag, callback) { + if (flag !== false) { + $table.trigger("sorton", [$table[0].config.sortList, function(){ + resortComplete($table, callback); + }]); + } else { + resortComplete($table, callback); } + } - eval(dynamicExp); - - cache.normalized.sort(sortWrapper); + /* public methods */ + ts.construct = function(settings) { + return this.each(function() { + // if no thead or tbody, or tablesorter is already present, quit + if (!this.tHead || this.tBodies.length === 0 || this.hasInitialized === true) { + return (this.config && this.config.debug) ? log('stopping initialization! No thead, tbody or tablesorter has already been initialized') : ''; + } + // declare + var $cell, $this = $(this), $t0 = this, + c, i, j, k = '', a, s, o, downTime, + m = $.metadata; + // initialization flag + $t0.hasInitialized = false; + // new blank config object + $t0.config = {}; + // merge and extend + c = $.extend(true, $t0.config, ts.defaults, settings); + // save the settings where they read + $.data($t0, "tablesorter", c); + if (c.debug) { $.data( $t0, 'startoveralltimer', new Date()); } + // constants + c.supportsTextContent = $('x')[0].textContent === 'x'; + c.supportsDataObject = parseFloat($.fn.jquery) >= 1.4; + // digit sort text location; keeping max+/- for backwards compatibility + c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; + // add table theme class only if there isn't already one there + if (!/tablesorter\-/.test($this.attr('class'))) { + k = (c.theme !== '' ? ' tablesorter-' + c.theme : ''); + } + c.$table = $this.addClass(c.tableClass + k); + c.$tbodies = $this.children('tbody:not(.' + c.cssInfoBlock + ')'); + // build headers + c.$headers = buildHeaders($t0); + // fixate columns if the users supplies the fixedWidth option + // do this after theme has been applied + fixColumnWidth($t0); + // try to auto detect column type, and store in tables config + c.parsers = buildParserCache($t0); + // build the cache for the tbody cells + // delayInit will delay building the cache until the user starts a sort + if (!c.delayInit) { buildCache($t0); } + // apply event handling to headers + // this is to big, perhaps break it out? + c.$headers + // http://stackoverflow.com/questions/5312849/jquery-find-self; andSelf() deprecated in jQuery 1.8 + .find('*')[ $.fn.addBack ? 'addBack': 'andSelf' ]().filter(c.selectorSort) + .unbind('mousedown.tablesorter mouseup.tablesorter') + .bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { + // jQuery v1.2.6 doesn't have closest() + var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0]; + // only recognize left clicks + if ((e.which || e.button) !== 1) { return false; } + // set timer on mousedown + if (e.type === 'mousedown') { + downTime = new Date().getTime(); + return e.target.tagName === "INPUT" ? '' : !c.cancelSelection; + } + // ignore long clicks (prevents resizable widget from initializing a sort) + if (external !== true && (new Date().getTime() - downTime > 250)) { return false; } + if (c.delayInit && !c.cache) { buildCache($t0); } + if (!cell.sortDisabled) { + // Only call sortStart if sorting is enabled + $this.trigger("sortStart", $t0); + // store exp, for speed + // $cell = $(this); + k = !e[c.sortMultiSortKey]; + // get current column sort order + cell.count = e[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2); + // reset all sorts on non-current column - issue #30 + if (c.sortRestart) { + i = cell; + c.$headers.each(function() { + // only reset counts on columns that weren't just clicked on and if not included in a multisort + if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { + this.count = -1; + } + }); + } + // get current column index + i = cell.column; + // user only wants to sort on one column + if (k) { + // flush the sort list + c.sortList = []; + if (c.sortForce !== null) { + a = c.sortForce; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // add column to sort list + o = cell.order[cell.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (j = 1; j < cell.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + // multi column sorting + } else { + // get rid of the sortAppend before adding more - fixes issue #115 + if (c.sortAppend && c.sortList.length > 1) { + if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) { + c.sortList.pop(); + } + } + // the user has clicked on an already sorted column + if (ts.isValueInArray(i, c.sortList)) { + // reverse the sorting direction for all tables + for (j = 0; j < c.sortList.length; j++) { + s = c.sortList[j]; + o = c.headerList[s[0]]; + if (s[0] === i) { + s[1] = o.order[o.count]; + if (s[1] === 2) { + c.sortList.splice(j,1); + o.count = -1; + } + } + } + } else { + // add column to sort list array + o = cell.order[cell.count]; + if (o < 2) { + c.sortList.push([i, o]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (j = 1; j < cell.colSpan; j++) { + c.sortList.push([i + j, o]); + } + } + } + } + } + if (c.sortAppend !== null) { + a = c.sortAppend; + for (j = 0; j < a.length; j++) { + if (a[j][0] !== i) { + c.sortList.push(a[j]); + } + } + } + // sortBegin event triggered immediately before the sort + $this.trigger("sortBegin", $t0); + // setTimeout needed so the processing icon shows up + setTimeout(function(){ + // set css for headers + setHeadersCss($t0); + multisort($t0); + appendToTable($t0); + }, 1); + } + }); + if (c.cancelSelection) { + // cancel selection + c.$headers.each(function() { + this.onselectstart = function() { + return false; + }; + }); + } + // apply easy methods that trigger boundd events + $this + .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave '.split(' ').join('.tablesorter ')) + .bind("sortReset.tablesorter", function(){ + c.sortList = []; + setHeadersCss($t0); + multisort($t0); + appendToTable($t0); + }) + .bind("update.tablesorter updateRows.tablesorter", function(e, resort, callback) { + // remove rows/elements before update + $this.find(c.selectorRemove).remove(); + // rebuild parsers + c.parsers = buildParserCache($t0); + // rebuild the cache map + buildCache($t0); + checkResort($this, resort, callback); + }) + .bind("updateCell.tablesorter", function(e, cell, resort, callback) { + $this.find(c.selectorRemove).remove(); + // get position from the dom + var l, row, icell, + $tb = $this.find('tbody'), + // update cache - format: function(s, table, cell, cellIndex) + // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr'); + tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ), + $row = $(cell).parents('tr').filter(':last'); + cell = $(cell)[0]; // in case cell is a jQuery object + // tbody may not exist if update is initialized while tbody is removed for processing + if ($tb.length && tbdy >= 0) { + row = $tb.eq(tbdy).find('tr').index( $row ); + icell = cell.cellIndex; + l = c.cache[tbdy].normalized[row].length - 1; + c.cache[tbdy].row[$t0.config.cache[tbdy].normalized[row][l]] = $row; + c.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText($t0, cell, icell), $t0, cell, icell ); + checkResort($this, resort, callback); + } + }) + .bind("addRows.tablesorter", function(e, $row, resort, callback) { + var i, rows = $row.filter('tr').length, + dat = [], l = $row[0].cells.length, + tbdy = $this.find('tbody').index( $row.closest('tbody') ); + // fixes adding rows to an empty table - see issue #179 + if (!c.parsers) { + c.parsers = buildParserCache($t0); + } + // add each row + for (i = 0; i < rows; i++) { + // add each cell + for (j = 0; j < l; j++) { + dat[j] = c.parsers[j].format( getElementText($t0, $row[i].cells[j], j), $t0, $row[i].cells[j], j ); + } + // add the row index to the end + dat.push(c.cache[tbdy].row.length); + // update cache + c.cache[tbdy].row.push([$row[i]]); + c.cache[tbdy].normalized.push(dat); + dat = []; + } + // resort using current settings + checkResort($this, resort, callback); + }) + .bind("sorton.tablesorter", function(e, list, callback, init) { + $this.trigger("sortStart", this); + // update header count index + updateHeaderSortCount($t0, list); + // set css for headers + setHeadersCss($t0); + // sort the table and append it to the dom + multisort($t0); + appendToTable($t0, init); + if (typeof callback === "function") { + callback($t0); + } + }) + .bind("appendCache.tablesorter", function(e, callback, init) { + appendToTable($t0, init); + if (typeof callback === "function") { + callback($t0); + } + }) + .bind("applyWidgetId.tablesorter", function(e, id) { + ts.getWidgetById(id).format($t0, c, c.widgetOptions); + }) + .bind("applyWidgets.tablesorter", function(e, init) { + // apply widgets + ts.applyWidget($t0, init); + }) + .bind("refreshWidgets.tablesorter", function(e, all, dontapply){ + ts.refreshWidgets($t0, all, dontapply); + }) + .bind("destroy.tablesorter", function(e, c, cb){ + ts.destroy($t0, c, cb); + }); + + // get sort list from jQuery data or metadata + // in jQuery < 1.4, an error occurs when calling $this.data() + if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') { + c.sortList = $this.data().sortlist; + } else if (m && ($this.metadata() && $this.metadata().sortlist)) { + c.sortList = $this.metadata().sortlist; + } + // apply widget init code + ts.applyWidget($t0, true); + // if user has supplied a sort list to constructor + if (c.sortList.length > 0) { + $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); + } else if (c.initWidgets) { + // apply widget format + ts.applyWidget($t0); + } - if (table.config.debug) { - benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); - } + // show processesing icon + if (c.showProcessing) { + $this + .unbind('sortBegin.tablesorter sortEnd.tablesorter') + .bind('sortBegin.tablesorter sortEnd.tablesorter', function(e) { + ts.isProcessing($t0, e.type === 'sortBegin'); + }); + } - return cache; + // initialized + $t0.hasInitialized = true; + if (c.debug) { + ts.benchmark("Overall initialization time", $.data( $t0, 'startoveralltimer')); + } + $this.trigger('tablesorter-initialized', $t0); + if (typeof c.initialized === 'function') { c.initialized($t0); } + }); }; - function makeSortFunction(type, direction, index) { - var a = "a[" + index + "]", - b = "b[" + index + "]"; - if (type == 'text' && direction == 'asc') { - return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; - } else if (type == 'text' && direction == 'desc') { - return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; - } else if (type == 'numeric' && direction == 'asc') { - return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; - } else if (type == 'numeric' && direction == 'desc') { - return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; + // *** Process table *** + // add processing indicator + ts.isProcessing = function(table, toggle, $ths) { + var c = table.config, + // default to all headers + $h = $ths || $(table).find('.' + c.cssHeader); + if (toggle) { + if (c.sortList.length > 0) { + // get headers from the sortList + $h = $h.filter(function(){ + // get data-column from attr to keep compatibility with jQuery 1.2.6 + return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList); + }); + } + $h.addClass(c.cssProcessing); + } else { + $h.removeClass(c.cssProcessing); } }; - function makeSortText(i) { - return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; + // detach tbody but save the position + // don't use tbody because there are portions that look for a tbody index (updateCell) + ts.processTbody = function(table, $tb, getIt){ + var t, holdr; + if (getIt) { + $tb.before(''); + holdr = ($.fn.detach) ? $tb.detach() : $tb.remove(); + return holdr; + } + holdr = $(table).find('span.tablesorter-savemyplace'); + $tb.insertAfter( holdr ); + holdr.remove(); }; - function makeSortTextDesc(i) { - return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; + ts.clearTableBody = function(table) { + table.config.$tbodies.empty(); }; - function makeSortNumeric(i) { - return "a[" + i + "]-b[" + i + "];"; + ts.destroy = function(table, removeClasses, callback){ + if (!table.hasInitialized) { return; } + // remove all widgets + ts.refreshWidgets(table, true, true); + var $t = $(table), c = table.config, + $h = $t.find('thead:first'), + $r = $h.find('tr.' + c.cssHeaderRow).removeClass(c.cssHeaderRow), + $f = $t.find('tfoot:first > tr').children('th, td'); + // remove widget added rows, just in case + $h.find('tr').not($r).remove(); + // disable tablesorter + $t + .removeData('tablesorter') + .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave sortBegin sortEnd '.split(' ').join('.tablesorter ')); + c.$headers.add($f) + .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc) + .removeAttr('data-column'); + $r.find(c.selectorSort).unbind('mousedown.tablesorter mouseup.tablesorter'); + // restore headers + $r.children().each(function(i){ + $(this).html( c.headerContent[i] ); + }); + if (removeClasses !== false) { + $t.removeClass(c.tableClass + ' tablesorter-' + c.theme); + } + // clear flag in case the plugin is initialized again + table.hasInitialized = false; + if (typeof callback === 'function') { + callback(table); + } }; - function makeSortNumericDesc(i) { - return "b[" + i + "]-a[" + i + "];"; + // *** sort functions *** + // regex used in natural sort + ts.regex = [ + /(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // chunk/tokenize numbers & letters + /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date + /^0x[0-9a-f]+$/i // hex + ]; + + // Natural sort - https://github.com/overset/javascript-natural-sort + ts.sortText = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], + r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } + // chunk/tokenize + xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); + // numeric, hex or date detection + xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); + yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; + // first try and sort Hex codes or Dates + if (yD) { + if ( xD < yD ) { return -1; } + if ( xD > yD ) { return 1; } + } + mx = Math.max(xN.length, yN.length); + // natural sorting through split numeric strings and default strings + for (i = 0; i < mx; i++) { + // find floats not starting with '0', string or 0 if not defined + xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0; + yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0; + // handle numeric vs string comparison - number < string - (Kyle Adams) + if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + if (typeof xF !== typeof yF) { + xF += ''; + yF += ''; + } + if (xF < yF) { return -1; } + if (xF > yF) { return 1; } + } + return 0; }; - function sortText(a, b) { - if (table.config.sortLocaleCompare) return a.localeCompare(b); - return ((a < b) ? -1 : ((a > b) ? 1 : 0)); + ts.sortTextDesc = function(table, a, b, col) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } + return ts.sortText(table, b, a); }; - function sortTextDesc(a, b) { - if (table.config.sortLocaleCompare) return b.localeCompare(a); - return ((b < a) ? -1 : ((b > a) ? 1 : 0)); + // return text string value by adding up ascii value + // so the text is somewhat sorted when using a digital sort + // this is NOT an alphanumeric sort + ts.getTextValue = function(a, mx, d) { + if (mx) { + // make sure the text value is greater than the max numerical value (mx) + var i, l = a ? a.length : 0, n = mx + d; + for (i = 0; i < l; i++) { + n += a.charCodeAt(i); + } + return d * n; + } + return 0; }; - function sortNumeric(a, b) { + ts.sortNumeric = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } + if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } + if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } return a - b; }; - function sortNumericDesc(a, b) { + ts.sortNumericDesc = function(table, a, b, col, mx, d) { + if (a === b) { return 0; } + var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; + if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } + if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } + if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } + if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } return b - a; }; - function getCachedSortType(parsers, i) { - return parsers[i].type; - }; /* public methods */ - this.construct = function (settings) { - return this.each(function () { - // if no thead or tbody quit. - if (!this.tHead || !this.tBodies) return; - // declare - var $this, $document, $headers, cache, config, shiftDown = 0, - sortOrder; - // new blank config object - this.config = {}; - // merge and extend. - config = $.extend(this.config, $.tablesorter.defaults, settings); - // store common expression for speed - $this = $(this); - // save the settings where they read - $.data(this, "tablesorter", config); - // build headers - $headers = buildHeaders(this); - // try to auto detect column type, and store in tables config - this.config.parsers = buildParserCache(this, $headers); - // build the cache for the tbody cells - cache = buildCache(this); - // get the css class names, could be done else where. - var sortCSS = [config.cssDesc, config.cssAsc]; - // fixate columns if the users supplies the fixedWidth option - fixColumnWidth(this); - // apply event handling to headers - // this is to big, perhaps break it out? - $headers.click( - - function (e) { - var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; - if (!this.sortDisabled && totalRows > 0) { - // Only call sortStart if sorting is - // enabled. - $this.trigger("sortStart"); - // store exp, for speed - var $cell = $(this); - // get current column index - var i = this.column; - // get current column sort order - this.order = this.count++ % 2; - // always sort on the locked order. - if(this.lockedOrder) this.order = this.lockedOrder; - - // user only whants to sort on one - // column - if (!e[config.sortMultiSortKey]) { - // flush the sort list - config.sortList = []; - if (config.sortForce != null) { - var a = config.sortForce; - for (var j = 0; j < a.length; j++) { - if (a[j][0] != i) { - config.sortList.push(a[j]); - } - } - } - // add column to sort list - config.sortList.push([i, this.order]); - // multi column sorting - } else { - // the user has clicked on an all - // ready sortet column. - if (isValueInArray(i, config.sortList)) { - // revers the sorting direction - // for all tables. - for (var j = 0; j < config.sortList.length; j++) { - var s = config.sortList[j], - o = config.headerList[s[0]]; - if (s[0] == i) { - o.count = s[1]; - o.count++; - s[1] = o.count % 2; - } - } - } else { - // add column to sort list array - config.sortList.push([i, this.order]); - } - }; - setTimeout(function () { - // set css for headers - setHeadersCss($this[0], $headers, config.sortList, sortCSS); - appendToTable( - $this[0], multisort( - $this[0], config.sortList, cache) - ); - }, 1); - // stop normal event by returning false - return false; + // used when replacing accented characters during sorting + ts.characterEquivalents = { + "a" : "\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5", // áàâãäąå + "A" : "\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5", // ÁÀÂÃÄĄÅ + "c" : "\u00e7\u0107\u010d", // çćč + "C" : "\u00c7\u0106\u010c", // ÇĆČ + "e" : "\u00e9\u00e8\u00ea\u00eb\u011b\u0119", // éèêëěę + "E" : "\u00c9\u00c8\u00ca\u00cb\u011a\u0118", // ÉÈÊËĚĘ + "i" : "\u00ed\u00ec\u0130\u00ee\u00ef\u0131", // íìİîïı + "I" : "\u00cd\u00cc\u0130\u00ce\u00cf", // ÍÌİÎÏ + "o" : "\u00f3\u00f2\u00f4\u00f5\u00f6", // óòôõö + "O" : "\u00d3\u00d2\u00d4\u00d5\u00d6", // ÓÒÔÕÖ + "ss": "\u00df", // ß (s sharp) + "SS": "\u1e9e", // ẞ (Capital sharp s) + "u" : "\u00fa\u00f9\u00fb\u00fc\u016f", // úùûüů + "U" : "\u00da\u00d9\u00db\u00dc\u016e" // ÚÙÛÜŮ + }; + ts.replaceAccents = function(s) { + var a, acc = '[', eq = ts.characterEquivalents; + if (!ts.characterRegex) { + ts.characterRegexArray = {}; + for (a in eq) { + if (typeof a === 'string') { + acc += eq[a]; + ts.characterRegexArray[a] = new RegExp('[' + eq[a] + ']', 'g'); } - // cancel selection - }).mousedown(function () { - if (config.cancelSelection) { - this.onselectstart = function () { - return false - }; - return false; + } + ts.characterRegex = new RegExp(acc + ']'); + } + if (ts.characterRegex.test(s)) { + for (a in eq) { + if (typeof a === 'string') { + s = s.replace( ts.characterRegexArray[a], a ); } - }); - // apply easy methods that trigger binded events - $this.bind("update", function () { - var me = this; - setTimeout(function () { - // rebuild parsers. - me.config.parsers = buildParserCache( - me, $headers); - // rebuild the cache map - cache = buildCache(me); - }, 1); - }).bind("updateCell", function (e, cell) { - var config = this.config; - // get position from the dom. - var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; - // update cache - cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( - getElementText(config, cell), cell); - }).bind("sorton", function (e, list) { - $(this).trigger("sortStart"); - config.sortList = list; - // update and store the sortlist - var sortList = config.sortList; - // update header count index - updateHeaderSortCount(this, sortList); - // set css for headers - setHeadersCss(this, $headers, sortList, sortCSS); - // sort the table and append it to the dom - appendToTable(this, multisort(this, sortList, cache)); - }).bind("appendCache", function () { - appendToTable(this, cache); - }).bind("applyWidgetId", function (e, id) { - getWidgetById(id).format(this); - }).bind("applyWidgets", function () { - // apply widgets - applyWidget(this); - }); - if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { - config.sortList = $(this).metadata().sortlist; } - // if user has supplied a sort list to constructor. - if (config.sortList.length > 0) { - $this.trigger("sorton", [config.sortList]); + } + return s; + }; + + // *** utilities *** + ts.isValueInArray = function(v, a) { + var i, l = a.length; + for (i = 0; i < l; i++) { + if (a[i][0] === v) { + return true; } - // apply widgets - applyWidget(this); - }); + } + return false; }; - this.addParser = function (parser) { - var l = parsers.length, - a = true; - for (var i = 0; i < l; i++) { - if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { + + ts.addParser = function(parser) { + var i, l = ts.parsers.length, a = true; + for (i = 0; i < l; i++) { + if (ts.parsers[i].id.toLowerCase() === parser.id.toLowerCase()) { a = false; } } if (a) { - parsers.push(parser); - }; + ts.parsers.push(parser); + } }; - this.addWidget = function (widget) { - widgets.push(widget); + + ts.getParserById = function(name) { + var i, l = ts.parsers.length; + for (i = 0; i < l; i++) { + if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { + return ts.parsers[i]; + } + } + return false; }; - this.formatFloat = function (s) { - var i = parseFloat(s); - return (isNaN(i)) ? 0 : i; + + ts.addWidget = function(widget) { + ts.widgets.push(widget); }; - this.formatInt = function (s) { - var i = parseInt(s); - return (isNaN(i)) ? 0 : i; + + ts.getWidgetById = function(name) { + var i, w, l = ts.widgets.length; + for (i = 0; i < l; i++) { + w = ts.widgets[i]; + if (w && w.hasOwnProperty('id') && w.id.toLowerCase() === name.toLowerCase()) { + return w; + } + } }; - this.isDigit = function (s, config) { - // replace all an wanted chars and match. - return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); + + ts.applyWidget = function(table, init) { + var c = table.config, + wo = c.widgetOptions, + ws = c.widgets.sort().reverse(), // ensure that widgets are always applied in a certain order + time, i, w, l = ws.length; + // make zebra last + i = $.inArray('zebra', c.widgets); + if (i >= 0) { + c.widgets.splice(i,1); + c.widgets.push('zebra'); + } + if (c.debug) { + time = new Date(); + } + // add selected widgets + for (i = 0; i < l; i++) { + w = ts.getWidgetById(ws[i]); + if ( w ) { + if (init === true && w.hasOwnProperty('init')) { + w.init(table, w, c, wo); + } else if (!init && w.hasOwnProperty('format')) { + w.format(table, c, wo); + } + } + } + if (c.debug) { + benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); + } }; - this.clearTableBody = function (table) { - if ($.browser.msie) { - function empty() { - while (this.firstChild) - this.removeChild(this.firstChild); + + ts.refreshWidgets = function(table, doAll, dontapply) { + var i, c = table.config, + cw = c.widgets, + w = ts.widgets, l = w.length; + // remove previous widgets + for (i = 0; i < l; i++){ + if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) { + if (c.debug) { log( 'Refeshing widgets: Removing ' + w[i].id ); } + if (w[i].hasOwnProperty('remove')) { w[i].remove(table, c, c.widgetOptions); } } - empty.apply(table.tBodies[0]); + } + if (dontapply !== true) { + ts.applyWidget(table, doAll); + } + }; + + // get sorter, string, empty, etc options for each column from + // jQuery data, metadata, header option or header class name ("sorter-false") + // priority = jQuery data > meta > headers option > header class name + ts.getData = function(h, ch, key) { + var val = '', $h = $(h), m, cl; + if (!$h.length) { return ''; } + m = $.metadata ? $h.metadata() : false; + cl = ' ' + ($h.attr('class') || ''); + if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){ + // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" + // "data-sort-initial-order" is assigned to "sortInitialOrder" + val += $h.data(key) || $h.data(key.toLowerCase()); + } else if (m && typeof m[key] !== 'undefined') { + val += m[key]; + } else if (ch && typeof ch[key] !== 'undefined') { + val += ch[key]; + } else if (cl !== ' ' && cl.match(' ' + key + '-')) { + // include sorter class name "sorter-text", etc + val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; + } + return $.trim(val); + }; + + ts.formatFloat = function(s, table) { + if (typeof(s) !== 'string' || s === '') { return s; } + // allow using formatFloat without a table; defaults to US number format + var i, + t = table && table.config ? table.config.usNumberFormat !== false : + typeof table !== "undefined" ? table : true; + if (t) { + // US Format - 1,234,567.89 -> 1234567.89 + s = s.replace(/,/g,''); } else { - table.tBodies[0].innerHTML = ""; + // German Format = 1.234.567,89 -> 1234567.89 + // French Format = 1 234 567,89 -> 1234567.89 + s = s.replace(/[\s|\.]/g,'').replace(/,/g,'.'); + } + if(/^\s*\([.\d]+\)/.test(s)) { + // make (#) into a negative number -> (10) = -10 + s = s.replace(/^\s*\(/,'-').replace(/\)/,''); } + i = parseFloat(s); + // return the text instead of zero + return isNaN(i) ? $.trim(s) : i; }; - } - }); - // extend plugin scope - $.fn.extend({ - tablesorter: $.tablesorter.construct + ts.isDigit = function(s) { + // replace all unwanted chars and match + return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true; + }; + + }() }); // make shortcut var ts = $.tablesorter; + // extend plugin scope + $.fn.extend({ + tablesorter: ts.construct + }); + // add default parsers ts.addParser({ id: "text", - is: function (s) { + is: function(s, table, node) { return true; - }, format: function (s) { - return $.trim(s.toLocaleLowerCase()); - }, type: "text" - }); - - ts.addParser({ - id: "digit", - is: function (s, table) { + }, + format: function(s, table, cell, cellIndex) { var c = table.config; - return $.tablesorter.isDigit(s, c); - }, format: function (s) { - return $.tablesorter.formatFloat(s); - }, type: "numeric" + s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); + return c.sortLocaleCompare ? ts.replaceAccents(s) : s; + }, + type: "text" }); ts.addParser({ id: "currency", - is: function (s) { - return /^[£$€?.]/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); - }, type: "numeric" + is: function(s) { + return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test((s || '').replace(/[,. ]/g,'')); // £$€¤¥¢ + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" }); ts.addParser({ id: "ipAddress", - is: function (s) { - return /^\d{2,3}[\.]\d{2,3}[\.]\d{2,3}[\.]\d{2,3}$/.test(s); - }, format: function (s) { - var a = s.split("."), + is: function(s) { + return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); + }, + format: function(s, table) { + var i, a = s.split("."), r = "", l = a.length; - for (var i = 0; i < l; i++) { - var item = a[i]; - if (item.length == 2) { - r += "0" + item; - } else { - r += item; - } + for (i = 0; i < l; i++) { + r += ("00" + a[i]).slice(-3); } - return $.tablesorter.formatFloat(r); - }, type: "numeric" + return ts.formatFloat(r, table); + }, + type: "numeric" }); ts.addParser({ id: "url", - is: function (s) { - return /^(https?|ftp|file):\/\/$/.test(s); - }, format: function (s) { - return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):\/\//), '')); - }, type: "text" + is: function(s) { + return (/^(https?|ftp|file):\/\//).test(s); + }, + format: function(s) { + return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); + }, + type: "text" }); ts.addParser({ id: "isoDate", - is: function (s) { - return /^\d{4}[\/-]\d{1,2}[\/-]\d{1,2}$/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( - new RegExp(/-/g), "/")).getTime() : "0"); - }, type: "numeric" + is: function(s) { + return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s); + }, + format: function(s, table) { + return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); + }, + type: "numeric" }); ts.addParser({ id: "percent", - is: function (s) { - return /\%$/.test($.trim(s)); - }, format: function (s) { - return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); - }, type: "numeric" + is: function(s) { + return (/(\d\s?%|%\s?\d)/).test(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/%/g, ""), table); + }, + type: "numeric" }); ts.addParser({ id: "usLongDate", - is: function (s) { - return s.match(new RegExp(/^[A-Za-z]{3,10}\.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); - }, format: function (s) { - return $.tablesorter.formatFloat(new Date(s).getTime()); - }, type: "numeric" + is: function(s) { + // two digit years are not allowed cross-browser + // Jan 01, 2013 12:34:56 PM or 01 Jan 2013 + return (/^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i).test(s) || (/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table); + }, + type: "numeric" }); ts.addParser({ - id: "shortDate", - is: function (s) { - return /\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}/.test(s); - }, format: function (s, table) { - var c = table.config; - s = s.replace(/\-/g, "/"); - if (c.dateFormat == "us") { - // reformat the string in ISO format - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$1/$2"); - } else if (c.dateFormat == "uk") { - // reformat the string in ISO format - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})/, "$3/$2/$1"); - } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { - s = s.replace(/(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{2})/, "$1/$2/$3"); + id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" + is: function(s) { + // testing for ####-##-####, so it's not perfect + return (/^(\d{1,2}|\d{4})[\/\-\,\.\s+]\d{1,2}[\/\-\.\,\s+](\d{1,2}|\d{4})$/).test(s); + }, + format: function(s, table, cell, cellIndex) { + var c = table.config, ci = c.headerList[cellIndex], + format = ci.shortDateFormat; + if (typeof format === 'undefined') { + // cache header formatting so it doesn't getData for every cell in the column + format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; } - return $.tablesorter.formatFloat(new Date(s).getTime()); - }, type: "numeric" + s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); + if (format === "mmddyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); + } else if (format === "ddmmyyyy") { + s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); + } else if (format === "yyyymmdd") { + s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); + } + return ts.formatFloat( (new Date(s).getTime() || ''), table); + }, + type: "numeric" }); + ts.addParser({ id: "time", - is: function (s) { - return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); - }, format: function (s) { - return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); - }, type: "numeric" + is: function(s) { + return (/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i).test(s); + }, + format: function(s, table) { + return ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table); + }, + type: "numeric" }); + + ts.addParser({ + id: "digit", + is: function(s) { + return ts.isDigit(s); + }, + format: function(s, table) { + return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + }, + type: "numeric" + }); + ts.addParser({ id: "metadata", - is: function (s) { + is: function(s) { return false; - }, format: function (s, table, cell) { + }, + format: function(s, table, cell) { var c = table.config, p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; return $(cell).metadata()[p]; - }, type: "numeric" + }, + type: "numeric" }); + // add default widgets ts.addWidget({ id: "zebra", - format: function (table) { - if (table.config.debug) { - var time = new Date(); + format: function(table, c, wo) { + var $tb, $tv, $tr, row, even, time, k, l, + child = new RegExp(c.cssChildRow, 'i'), + b = c.$tbodies; + if (c.debug) { + time = new Date(); } - var $tr, row = -1, - odd; - // loop through the visible rows - $("tr:visible", table.tBodies[0]).each(function (i) { - $tr = $(this); - // style children rows the same way the parent - // row was styled - if (!$tr.hasClass(table.config.cssChildRow)) row++; - odd = (row % 2 == 0); - $tr.removeClass( - table.config.widgetZebra.css[odd ? 0 : 1]).addClass( - table.config.widgetZebra.css[odd ? 1 : 0]) - }); - if (table.config.debug) { - $.tablesorter.benchmark("Applying Zebra widget", time); + for (k = 0; k < b.length; k++ ) { + // loop through the visible rows + $tb = b.eq(k); + l = $tb.children('tr').length; + if (l > 1) { + row = 0; + $tv = $tb.children('tr:visible'); + // revered back to using jQuery each - strangely it's the fastest method + /*jshint loopfunc:true */ + $tv.each(function(){ + $tr = $(this); + // style children rows the same way the parent row was styled + if (!child.test(this.className)) { row++; } + even = (row % 2 === 0); + $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]); + }); + } + } + if (c.debug) { + ts.benchmark("Applying Zebra widget", time); + } + }, + remove: function(table, c, wo){ + var k, $tb, + b = c.$tbodies, + rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' '); + for (k = 0; k < b.length; k++ ){ + $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody + $tb.children().removeClass(rmv); + $.tablesorter.processTbody(table, $tb, false); // restore tbody } } }); + })(jQuery); \ No newline at end of file diff --git a/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.pager.js b/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.pager.js new file mode 100644 index 000000000..99081c8bf --- /dev/null +++ b/NzbDrone.Backbone/JsLibraries/jquery.tablesorter.pager.js @@ -0,0 +1,575 @@ +/*! + * tablesorter pager plugin + * updated 2/20/2013 + */ +/*jshint browser:true, jquery:true, unused:false */ +;(function($) { + "use strict"; + /*jshint supernew:true */ + $.extend({ tablesorterPager: new function() { + + this.defaults = { + // target the pager markup + container: null, + + // use this format: "http://mydatabase.com?page={page}&size={size}&{sortList:col}&{filterList:fcol}" + // where {page} is replaced by the page number, {size} is replaced by the number of records to show, + // {sortList:col} adds the sortList to the url into a "col" array, and {filterList:fcol} adds + // the filterList to the url into an "fcol" array. + // So a sortList = [[2,0],[3,0]] becomes "&col[2]=0&col[3]=0" in the url + // and a filterList = [[2,Blue],[3,13]] becomes "&fcol[2]=Blue&fcol[3]=13" in the url + ajaxUrl: null, + + // process ajax so that the following information is returned: + // [ total_rows (number), rows (array of arrays), headers (array; optional) ] + // example: + // [ + // 100, // total rows + // [ + // [ "row1cell1", "row1cell2", ... "row1cellN" ], + // [ "row2cell1", "row2cell2", ... "row2cellN" ], + // ... + // [ "rowNcell1", "rowNcell2", ... "rowNcellN" ] + // ], + // [ "header1", "header2", ... "headerN" ] // optional + // ] + ajaxProcessing: function(ajax){ return [ 0, [], null ]; }, + + // output default: '{page}/{totalPages}' + // possible variables: {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} + output: '{startRow} to {endRow} of {totalRows} rows', // '{page}/{totalPages}' + + // apply disabled classname to the pager arrows when the rows at either extreme is visible + updateArrows: true, + + // starting page of the pager (zero based index) + page: 0, + + // Number of visible rows + size: 10, + + // 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 + 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. + removeRows: false, // removing rows in larger tables speeds up the sort + + // css class names of pager arrows + cssFirst: '.first', // go to first page arrow + cssPrev: '.prev', // previous page arrow + cssNext: '.next', // next page arrow + cssLast: '.last', // go to last page arrow + cssGoto: '.gotoPage', // go to page selector - select dropdown that sets the current page + cssPageDisplay: '.pagedisplay', // location of where the "output" is displayed + cssPageSize: '.pagesize', // page size selector - select dropdown that sets the "size" option + cssErrorRow: 'tablesorter-errorRow', // error information row + + // class added to arrows when at the extremes (i.e. prev/first arrows are "disabled" when on the first page) + cssDisabled: 'disabled', // Note there is no period "." in front of this class name + + // stuff not set by the user + totalRows: 0, + totalPages: 0, + filteredRows: 0, + filteredPages: 0 + + }; + + var $this = this, + + // hide arrows at extremes + pagerArrows = function(c, disable) { + var a = 'addClass', + r = 'removeClass', + d = c.cssDisabled, + dis = !!disable, + tp = Math.min( c.totalPages, c.filteredPages ); + if ( c.updateArrows ) { + $(c.cssFirst + ',' + c.cssPrev, c.container)[ ( dis || c.page === 0 ) ? a : r ](d); + $(c.cssNext + ',' + c.cssLast, c.container)[ ( dis || c.page === tp - 1 ) ? a : r ](d); + } + }, + + updatePageDisplay = function(table, c) { + var i, p, s, t, out, f = $(table).hasClass('hasFilters') && !c.ajaxUrl; + c.filteredRows = (f) ? table.config.$tbodies.children('tr:not(.filtered,.remove-me)').length : c.totalRows; + c.filteredPages = (f) ? Math.ceil( c.filteredRows / c.size ) : c.totalPages; + if ( Math.min( c.totalPages, c.filteredPages ) > 0 ) { + t = (c.size * c.page > c.filteredRows); + c.startRow = (t) ? 1 : ( c.size * c.page ) + 1; + c.page = (t) ? 0 : c.page; + c.endRow = Math.min( c.filteredRows, c.totalRows, c.size * ( c.page + 1 ) ); + out = $(c.cssPageDisplay, c.container); + // form the output string + s = c.output.replace(/\{(page|filteredRows|filteredPages|totalPages|startRow|endRow|totalRows)\}/gi, function(m){ + return { + '{page}' : c.page + 1, + '{filteredRows}' : c.filteredRows, + '{filteredPages}' : c.filteredPages, + '{totalPages}' : c.totalPages, + '{startRow}' : c.startRow, + '{endRow}' : c.endRow, + '{totalRows}' : c.totalRows + }[m]; + }); + if (out[0]) { + out[ (out[0].tagName === 'INPUT') ? 'val' : 'html' ](s); + if ( $(c.cssGoto, c.container).length ) { + t = ''; + p = Math.min( c.totalPages, c.filteredPages ); + for ( i = 1; i <= p; i++ ) { + t += ''; + } + $(c.cssGoto, c.container).html(t).val(c.page + 1); + } + } + } + pagerArrows(c); + if (c.initialized) { $(table).trigger('pagerComplete', c); } + }, + + fixHeight = function(table, c) { + var d, h, $b = $(table.tBodies[0]); + if (c.fixedHeight) { + $b.find('tr.pagerSavedHeightSpacer').remove(); + h = $.data(table, 'pagerSavedHeight'); + if (h) { + d = h - $b.height(); + if ( d > 5 && $.data(table, 'pagerLastSize') === c.size && $b.children('tr:visible').length < c.size ) { + $b.append(''); + } + } + } + }, + + changeHeight = function(table, c) { + var $b = $(table.tBodies[0]); + $b.find('tr.pagerSavedHeightSpacer').remove(); + $.data(table, 'pagerSavedHeight', $b.height()); + fixHeight(table, c); + $.data(table, 'pagerLastSize', c.size); + }, + + hideRows = function(table, c){ + if (!c.ajaxUrl) { + var i, + rows = $(table.tBodies).children('tr:not(.' + table.config.cssChildRow + ')'), + l = rows.length, + s = ( c.page * c.size ), + e = s + c.size, + j = 0; // size counter + for ( i = 0; i < l; i++ ){ + if (!/filtered/.test(rows[i].className)) { + rows[i].style.display = ( j >= s && j < e ) ? '' : 'none'; + j++; + } + } + } + }, + + hideRowsSetup = function(table, c){ + c.size = parseInt( $(c.cssPageSize, c.container).find('option[selected]').val(), 10 ) || c.size; + $.data(table, 'pagerLastSize', c.size); + pagerArrows(c); + if ( !c.removeRows ) { + hideRows(table, c); + $(table).bind('sortEnd.pager filterEnd.pager', function(){ + hideRows(table, c); + }); + } + }, + + renderAjax = function(data, table, c, exception){ + // process data + if ( typeof(c.ajaxProcessing) === "function" ) { + // ajaxProcessing result: [ total, rows, headers ] + var i, j, hsh, $f, $sh, + $t = $(table), + tc = table.config, + hl = $t.find('thead th').length, tds = '', + err = '' + + (exception ? exception.message + ' (' + exception.name + ')' : 'No rows found') + '', + result = c.ajaxProcessing(data) || [ 0, [] ], + d = result[1] || [], + l = d.length, + th = result[2]; + if ( l > 0 ) { + for ( i = 0; i < l; i++ ) { + tds += ''; + for ( j = 0; j < d[i].length; j++ ) { + // build tbody cells + tds += '' + d[i][j] + ''; + } + tds += ''; + } + } + // only add new header text if the length matches + if ( th && th.length === hl ) { + hsh = $t.hasClass('hasStickyHeaders'); + $sh = $t.find('.' + ((tc.widgetOptions && tc.widgetOptions.stickyHeaders) || 'tablesorter-stickyheader')); + $f = $t.find('tfoot tr:first').children(); + $t.find('th.' + tc.cssHeader).each(function(j){ + var $t = $(this), icn; + // add new test within the first span it finds, or just in the header + if ( $t.find('.' + tc.cssIcon).length ) { + icn = $t.find('.' + tc.cssIcon).clone(true); + $t.find('.tablesorter-header-inner').html( th[j] ).append(icn); + if ( hsh && $sh.length ) { + icn = $sh.find('th').eq(j).find('.' + tc.cssIcon).clone(true); + $sh.find('th').eq(j).find('.tablesorter-header-inner').html( th[j] ).append(icn); + } + } else { + $t.find('.tablesorter-header-inner').html( th[j] ); + $sh.find('th').eq(j).find('.tablesorter-header-inner').html( th[j] ); + } + $f.eq(j).html( th[j] ); + }); + } + + $t.find('thead tr.' + c.cssErrorRow).remove(); // Clean up any previous error. + if ( exception ) { + // add error row to thead instead of tbody, or clicking on the header will result in a parser error + $t.find('thead').append(err); + } else { + $(table.tBodies[0]).html( tds ); // add rows to first tbody + } + if (tc.showProcessing) { + $.tablesorter.isProcessing(table); // remove loading icon + } + $t.trigger('update'); + c.totalRows = result[0] || 0; + c.totalPages = Math.ceil( c.totalRows / c.size ); + updatePageDisplay(table, c); + fixHeight(table, c); + if (c.initialized) { $t.trigger('pagerChange', c); } + } + if (!c.initialized) { + c.initialized = true; + $(table).trigger('pagerInitialized', c); + } + }, + + getAjax = function(table, c){ + var url = getAjaxUrl(table, c), + tc = table.config; + if ( url !== '' ) { + if (tc.showProcessing) { + $.tablesorter.isProcessing(table, true); // show loading icon + } + $(document).bind('ajaxError.pager', function(e, xhr, settings, exception) { + if (settings.url === url) { + renderAjax(null, table, c, exception); + $(document).unbind('ajaxError.pager'); + } + }); + $.getJSON(url, function(data) { + renderAjax(data, table, c); + $(document).unbind('ajaxError.pager'); + }); + } + }, + + getAjaxUrl = function(table, c) { + var url = (c.ajaxUrl) ? c.ajaxUrl.replace(/\{page\}/g, c.page).replace(/\{size\}/g, c.size) : '', + sl = table.config.sortList, + fl = c.currentFilters || [], + sortCol = url.match(/\{sortList[\s+]?:[\s+]?([^}]*)\}/), + filterCol = url.match(/\{filterList[\s+]?:[\s+]?([^}]*)\}/), + arry = []; + if (sortCol) { + sortCol = sortCol[1]; + $.each(sl, function(i,v){ + arry.push(sortCol + '[' + v[0] + ']=' + v[1]); + }); + // if the arry is empty, just add the col parameter... "&{sortList:col}" becomes "&col" + url = url.replace(/\{sortList[\s+]?:[\s+]?([^\}]*)\}/g, arry.length ? arry.join('&') : sortCol ); + } + if (filterCol) { + filterCol = filterCol[1]; + $.each(fl, function(i,v){ + if (v) { + arry.push(filterCol + '[' + i + ']=' + encodeURIComponent(v)); + } + }); + // if the arry is empty, just add the fcol parameter... "&{filterList:fcol}" becomes "&fcol" + url = url.replace(/\{filterList[\s+]?:[\s+]?([^\}]*)\}/g, arry.length ? arry.join('&') : filterCol ); + } + + return url; + }, + + renderTable = function(table, rows, c) { + c.isDisabled = false; // needed because sorting will change the page and re-enable the pager + var i, j, o, + f = document.createDocumentFragment(), + l = rows.length, + s = ( c.page * c.size ), + e = ( s + c.size ); + if ( l < 1 ) { return; } // empty table, abort! + if (c.initialized) { $(table).trigger('pagerChange', c); } + if ( !c.removeRows ) { + hideRows(table, c); + } else { + if ( e > rows.length ) { + e = rows.length; + } + $(table.tBodies[0]).addClass('tablesorter-hidden'); + $.tablesorter.clearTableBody(table); + for ( i = s; i < e; i++ ) { + o = rows[i]; + l = o.length; + for ( j = 0; j < l; j++ ) { + f.appendChild(o[j]); + } + } + table.tBodies[0].appendChild(f); + $(table.tBodies[0]).removeClass('tablesorter-hidden'); + } + if ( c.page >= c.totalPages ) { + moveToLastPage(table, c); + } + updatePageDisplay(table, c); + if ( !c.isDisabled ) { fixHeight(table, c); } + $(table).trigger('applyWidgets'); + }, + + showAllRows = function(table, c){ + if ( c.ajax ) { + pagerArrows(c, true); + } else { + c.isDisabled = true; + $.data(table, 'pagerLastPage', c.page); + $.data(table, 'pagerLastSize', c.size); + c.page = 0; + c.size = c.totalRows; + c.totalPages = 1; + $('tr.pagerSavedHeightSpacer', table.tBodies[0]).remove(); + renderTable(table, table.config.rowsCopy, c); + } + // disable size selector + $(c.container).find(c.cssPageSize + ',' + c.cssGoto).each(function(){ + $(this).addClass(c.cssDisabled)[0].disabled = true; + }); + }, + + moveToPage = function(table, c) { + if ( c.isDisabled ) { return; } + var p = Math.min( c.totalPages, c.filteredPages ); + if ( c.page < 0 ) { c.page = 0; } + if ( c.page > ( p - 1 ) && p !== 0 ) { c.page = p - 1; } + if (c.ajax) { + getAjax(table, c); + } else if (!c.ajax) { + renderTable(table, table.config.rowsCopy, c); + } + $.data(table, 'pagerLastPage', c.page); + $.data(table, 'pagerUpdateTriggered', true); + if (c.initialized) { $(table).trigger('pageMoved', c); } + }, + + setPageSize = function(table, size, c) { + c.size = size; + $.data(table, 'pagerLastPage', c.page); + $.data(table, 'pagerLastSize', c.size); + c.totalPages = Math.ceil( c.totalRows / c.size ); + moveToPage(table, c); + }, + + moveToFirstPage = function(table, c) { + c.page = 0; + moveToPage(table, c); + }, + + moveToLastPage = function(table, c) { + c.page = ( Math.min( c.totalPages, c.filteredPages ) - 1 ); + moveToPage(table, c); + }, + + moveToNextPage = function(table, c) { + c.page++; + if ( c.page >= ( Math.min( c.totalPages, c.filteredPages ) - 1 ) ) { + c.page = ( Math.min( c.totalPages, c.filteredPages ) - 1 ); + } + moveToPage(table, c); + }, + + moveToPrevPage = function(table, c) { + c.page--; + if ( c.page <= 0 ) { + c.page = 0; + } + moveToPage(table, c); + }, + + destroyPager = function(table, c){ + showAllRows(table, c); + $(c.container).hide(); // hide pager + table.config.appender = null; // remove pager appender function + $(table).unbind('destroy.pager sortEnd.pager filterEnd.pager enable.pager disable.pager'); + }, + + enablePager = function(table, c, triggered){ + var p = $(c.cssPageSize, c.container).removeClass(c.cssDisabled).removeAttr('disabled'); + $(c.container).find(c.cssGoto).removeClass(c.cssDisabled).removeAttr('disabled'); + c.isDisabled = false; + c.page = $.data(table, 'pagerLastPage') || c.page || 0; + c.size = $.data(table, 'pagerLastSize') || parseInt(p.find('option[selected]').val(), 10) || c.size; + p.val(c.size); // set page size + c.totalPages = Math.ceil( Math.min( c.totalPages, c.filteredPages ) / c.size); + if ( triggered ) { + $(table).trigger('update'); + setPageSize(table, c.size, c); + hideRowsSetup(table, c); + fixHeight(table, c); + } + }; + + $this.appender = function(table, rows) { + var c = table.config.pager; + if ( !c.ajax ) { + table.config.rowsCopy = rows; + c.totalRows = rows.length; + c.size = $.data(table, 'pagerLastSize') || c.size; + c.totalPages = Math.ceil(c.totalRows / c.size); + renderTable(table, rows, c); + } + }; + + $this.construct = function(settings) { + return this.each(function() { + // check if tablesorter has initialized + if (!(this.config && this.hasInitialized)) { return; } + var t, ctrls, fxn, + config = this.config, + c = config.pager = $.extend( {}, $.tablesorterPager.defaults, settings ), + table = this, + tc = table.config, + $t = $(table), + pager = $(c.container).addClass('tablesorter-pager').show(); // added in case the pager is reinitialized after being destroyed. + config.appender = $this.appender; + + $t + .unbind('filterStart.pager filterEnd.pager sortEnd.pager disable.pager enable.pager destroy.pager update.pager pageSize.pager') + .bind('filterStart.pager', function(e, filters) { + $.data(table, 'pagerUpdateTriggered', false); + c.currentFilters = filters; + }) + // update pager after filter widget completes + .bind('filterEnd.pager sortEnd.pager', function(e) { + //Prevent infinite event loops from occuring by setting this in all moveToPage calls and catching it here. + if ($.data(table, 'pagerUpdateTriggered')) { + $.data(table, 'pagerUpdateTriggered', false); + return; + } + if (e.type === 'filterEnd') { c.page = 0; } + moveToPage(table, c); + updatePageDisplay(table, c); + fixHeight(table, c); + }) + .bind('disable.pager', function(){ + showAllRows(table, c); + }) + .bind('enable.pager', function(){ + enablePager(table, c, true); + }) + .bind('destroy.pager', function(){ + destroyPager(table, c); + }) + .bind('update.pager', function(){ + hideRows(table, c); + }) + .bind('pageSize.pager', function(e,v){ + c.size = parseInt(v, 10) || 10; + hideRows(table, c); + updatePageDisplay(table, c); + }) + .bind('pageSet.pager', function(e,v){ + c.page = (parseInt(v, 10) || 1) - 1; + moveToPage(table, c); + updatePageDisplay(table, c); + }); + + // clicked controls + ctrls = [c.cssFirst, c.cssPrev, c.cssNext, c.cssLast]; + fxn = [ moveToFirstPage, moveToPrevPage, moveToNextPage, moveToLastPage ]; + + pager.find(ctrls.join(',')) + .unbind('click.pager') + .bind('click.pager', function(e){ + var i, $this = $(this), l = ctrls.length; + if ( !$this.hasClass(c.cssDisabled) ) { + for (i = 0; i < l; i++) { + if ($this.is(ctrls[i])) { + fxn[i](table, c); + break; + } + } + } + return false; + }); + + // goto selector + if ( pager.find(c.cssGoto).length ) { + pager.find(c.cssGoto) + .unbind('change') + .bind('change', function(){ + c.page = $(this).val() - 1; + moveToPage(table, c); + }); + updatePageDisplay(table, c); + } + + // page size selector + t = pager.find(c.cssPageSize); + if ( t.length ) { + t.unbind('change.pager').bind('change.pager', function() { + t.val( $(this).val() ); // in case there are more than one pagers + if ( !$(this).hasClass(c.cssDisabled) ) { + setPageSize(table, parseInt( $(this).val(), 10 ), c); + changeHeight(table, c); + } + return false; + }); + } + + // clear initialized flag + c.initialized = false; + // before initialization event + $t.trigger('pagerBeforeInitialized', c); + + enablePager(table, c, false); + + if ( typeof(c.ajaxUrl) === 'string' ) { + // ajax pager; interact with database + c.ajax = true; + //When filtering with ajax, allow only custom filtering function, disable default filtering since it will be done server side. + tc.widgetOptions.filter_serversideFiltering = true; + tc.serverSideSorting = true; + moveToPage(table, c); + } else { + c.ajax = false; + // Regular pager; all rows stored in memory + $(this).trigger("appendCache", true); + hideRowsSetup(table, c); + } + + changeHeight(table, c); + + // pager initialized + if (!c.ajax) { + c.initialized = true; + $(table).trigger('pagerInitialized', c); + } + }); + }; + + }() + }); +// extend plugin scope + $.fn.extend({ + tablesorterPager: $.tablesorterPager.construct + }); + +})(jQuery); \ No newline at end of file diff --git a/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html b/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html index b56030cbf..a84b6a613 100644 --- a/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html +++ b/NzbDrone.Backbone/Missing/MissingCollectionTemplate.html @@ -10,3 +10,28 @@ +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/NzbDrone.Backbone/Missing/MissingCollectionView.js b/NzbDrone.Backbone/Missing/MissingCollectionView.js index 14ee3cf81..8bfec813e 100644 --- a/NzbDrone.Backbone/Missing/MissingCollectionView.js +++ b/NzbDrone.Backbone/Missing/MissingCollectionView.js @@ -7,7 +7,8 @@ define(['app', 'Missing/MissingItemView'], function (app) { template: 'Missing/MissingCollectionTemplate', ui:{ - table : '.x-missing-table' + table : '.x-missing-table', + pager : '.x-missing-table-pager' }, initialize: function (context, action, query, collection) { @@ -42,34 +43,46 @@ define(['app', 'Missing/MissingItemView'], function (app) { } }); - //Todo: We should extract these common settings out - this.ui.table.find('th.header').each(function(){ - $(this).append(''); + this.ui.table.bind('pagerComplete pagerInitialized', function(event, c){ + c.container.find('.page-number').text(c.page + 1); }); - this.ui.table.bind("sortEnd", function() { - $(this).find('th.header i').each(function(){ - $(this).remove(); - }); - - $(this).find('th.header').each(function () { - if (!$(this).hasClass('headerSortDown') && !$(this).hasClass('headerSortUp')) - $(this).append(''); - }); + this.ui.table.tablesorterPager({ + container: this.ui.pager, + output: 'Displaying {startRow} to {endRow} of {totalRows} episodes' + }); - $(this).find('th.headerSortDown').each(function(){ - $(this).append(''); - }); + this.applySortIcons(); - $(this).find('th.headerSortUp').each(function(){ - $(this).append(''); - }); + this.ui.table.bind("sortEnd", function() { + this.applySortIcons(); }); } else { this.ui.table.trigger('update'); } - } + }, + + //Todo: Remove this from each view that requires it + applySortIcons: function() { + $(this.ui.table).find('th.tablesorter-header .tablesorter-header-inner i').each(function(){ + $(this).remove(); + }); + + $(this.ui.table).find('th.tablesorter-header').each(function () { + if ($(this).hasClass('tablesorter-headerDesc')) + $(this).children('.tablesorter-header-inner').append(''); + + else if ($(this).hasClass('tablesorter-headerAsc')) + $(this).children('.tablesorter-header-inner').append(''); + + else if (!$(this).hasClass('sorter-false')) + $(this).children('.tablesorter-header-inner').append(''); + }); + }, + updatePageNumber: function(event, stuff) { + + } }); }); \ No newline at end of file diff --git a/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js b/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js index 7ce547fbf..ce9d36567 100644 --- a/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js +++ b/NzbDrone.Backbone/Series/Index/SeriesIndexCollectionView.js @@ -57,34 +57,33 @@ define(['app', 'Quality/QualityProfileCollection', 'Series/Index/SeriesItemView' } }); - //Todo: We should extract these common settings out - this.ui.table.find('th.header').each(function(){ - $(this).append(''); - }); + this.applySortIcons(); this.ui.table.bind("sortEnd", function() { - $(this).find('th.header i').each(function(){ - $(this).remove(); - }); - - $(this).find('th.header').each(function () { - if (!$(this).hasClass('headerSortDown') && !$(this).hasClass('headerSortUp')) - $(this).append(''); - }); - - $(this).find('th.headerSortDown').each(function(){ - $(this).append(''); - }); - - $(this).find('th.headerSortUp').each(function(){ - $(this).append(''); - }); + this.applySortIcons(); }); } else { this.ui.table.trigger('update'); } + }, + //Todo: Remove this from each view that requires it + applySortIcons: function() { + $(this.ui.table).find('th.tablesorter-header .tablesorter-header-inner i').each(function(){ + $(this).remove(); + }); + + $(this.ui.table).find('th.tablesorter-header').each(function () { + if ($(this).hasClass('tablesorter-headerDesc')) + $(this).children('.tablesorter-header-inner').append(''); + + else if ($(this).hasClass('tablesorter-headerAsc')) + $(this).children('.tablesorter-header-inner').append(''); + + else if (!$(this).hasClass('sorter-false')) + $(this).children('.tablesorter-header-inner').append(''); + }); } }); });