diff options
| author | root | 2014-10-28 00:52:21 +0100 |
|---|---|---|
| committer | root | 2014-10-28 00:52:21 +0100 |
| commit | 25610c0ccb4c7c99fe0d6d82d6738dbcc40d05e3 (patch) | |
| tree | 1c4fdcee0fb7b28ca330effbcc3334de3979d555 | |
| parent | fe229655401abfa5aea2dc6c8830c8b9ed71aa64 (diff) | |
| download | jungegemeinde-25610c0ccb4c7c99fe0d6d82d6738dbcc40d05e3.tar.gz | |
v4.2 Sortable table + other improvements.
| -rw-r--r-- | .gitignore | 2 | ||||
| -rw-r--r-- | action.php | 20 | ||||
| -rw-r--r-- | bootstrap.php | 4 | ||||
| -rw-r--r-- | class/moar.php | 27 | ||||
| -rw-r--r-- | functions.php | 48 | ||||
| -rw-r--r-- | index.php | 6 | ||||
| -rw-r--r-- | js/eyecancer.js (renamed from static/eyecancer.js) | 1 | ||||
| -rw-r--r-- | js/eyecancer.min.js (renamed from static/eyecancer.min.js) | 0 | ||||
| -rw-r--r-- | js/functions.js | 21 | ||||
| -rw-r--r-- | js/tablesorter.js | 1901 | ||||
| -rw-r--r-- | js/tablesorter.min.js | 5 | ||||
| -rw-r--r-- | js/upload.js | 3345 | ||||
| -rwxr-xr-x | static/footer.php | 38 | ||||
| -rw-r--r-- | static/header.php | 3 | ||||
| -rw-r--r-- | static/modal-edit-gallery.php | 2 | ||||
| -rw-r--r-- | static/modal-new-gallery.html | 2 | ||||
| -rw-r--r-- | static/style.css | 2 | ||||
| -rw-r--r-- | static/style.min.css | 2 | ||||
| -rw-r--r-- | static/tablesorter.css | 38 | ||||
| -rw-r--r-- | static/tablesorter.min.css | 1 |
20 files changed, 5408 insertions, 60 deletions
@@ -6,6 +6,8 @@ *.swp *.tmp +protected/ + setup.php piwik.html favicon.ico @@ -351,30 +351,32 @@ JG Adlershof"; redirect("foto"); break; case("downloadGallery"): - if ( $_SERVER['REQUEST_METHOD'] != 'POST' ){ + if ( $_SERVER['REQUEST_METHOD'] != 'GET' ){ header($_SERVER["SERVER_PROTOCOL"] . " 405 Method Not Allowed"); ob_clean(); echo "Method not allowed"; exit; } lredirect( "gallery;gallery=".htmlentities($_GET["gallery"]) ); - if ( ! isset($_GET["gallery"]) || $_GET["gallery"] == "" || ! preg_match("/^[0-9]+$", $_GET["gallery"]) ){ + if ( ! isset($_GET["gallery"]) || $_GET["gallery"] == "" || ! preg_match("/^[0-9]+$/", $_GET["gallery"]) ){ redirect( "gallery;gallery=".htmlentities($_SESSION["gallery"]) ); } - $zname = '/tmp/jg_fotoalbum_'.$_GET["gallery"]; - $zip = new ZipArchive; - if ( $zip->open($zname) === TRUE ){ - $images = array_diff( scandir(IMAGE_PATH . $_GET["gallery"]), array('..', '.') ); + $zname = '/tmp/jg_fotoalbum.zip'; + $zip = new ZipArchive(); + if ( $zip->open($zname, ZipArchive::CREATE) == TRUE ){ + $images = array_diff( scandir(IMAGE_PATH . $_GET["gallery"].'/'), array('..', '.') ); foreach( $images as $image){ - if ( is_file($image) ){ - $zip->addFile($image, basename($image)); + if ( is_file(IMAGE_PATH . $_GET["gallery"] . '/' . $image) ){ + //$zip->addFile($image, basename($image)); + $zip->addFile(IMAGE_PATH . $_GET["gallery"] . '/' . $image, $image); } } $zip->close(); ob_end_clean(); + $name = $c->get2(CACHEPREFIX . $_GET["gallery"]); header("Content-Type: application/zip"); header("Content-Length: " . filesize($zname)); - header("Content-Disposition: attachment; filename=jg_fotoalbum_".$_GET["gallery"]); + header("Content-Disposition: attachment; filename=\"".$name.".zip\""); readfile($zname); unlink($zname); exit; diff --git a/bootstrap.php b/bootstrap.php index ed260d9..098288b 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,5 +1,5 @@ <?php -### loads the vfs environment +### loads the jg-environment require_once( dirname(__FILE__) . '/config.php'); @@ -28,6 +28,8 @@ if ( ! defined('DOMAIN') ) # define session name if ( ! defined('SESSION') ) define('SESSION', 'JGSID'); +if ( ! defined('SESSION_LIFETIME') ) + define('SESSION_LIFETIME', 1800); # define include path for class files if ( ! defined('INCLASS') ) diff --git a/class/moar.php b/class/moar.php index 822640f..b917c1d 100644 --- a/class/moar.php +++ b/class/moar.php @@ -17,22 +17,36 @@ class Moar { $this->footer[] = $string; } - public function playHeader(){ + public function playHeader($output = true){ if ( ! empty( $this->header ) ){ + if ( ! $output ) + $buffer = ""; foreach( $this->header as $value ){ - echo $value; + if ( $output ) + echo $value; + else + $buffer .= $value; } } $this->deleteHeader(); + if ( isset($buffer) ) + return $buffer; } - public function playFooter(){ + public function playFooter($output = true){ if ( ! empty( $this->footer ) ){ + if ( ! $output ) + $buffer = ""; foreach( $this->footer as $value ){ - echo $value; + if ( $output ) + echo $value; + else + $buffer .= $value; } } $this->deleteFooter(); + if ( isset($buffer) ) + return $buffer; } public function deleteHeader(){ @@ -45,10 +59,7 @@ class Moar { } public function magicHeader($html){ - ob_start(); - $this->playHeader(); - $header = ob_get_contents(); - ob_end_clean(); + $header = $this->playHeader(false); return preg_replace("/\<\!\-\-%%placeholder\-head%%\-\-\>/", $header, $html); } diff --git a/functions.php b/functions.php index eba19f9..17c2ce3 100644 --- a/functions.php +++ b/functions.php @@ -146,10 +146,31 @@ function print_list($option = false){ lredirect("liste"); global $db; global $c; + global $moar; + + $moar->addHeader('<style>'.file_get_contents("static/tablesorter.min.css").'</style>'); + $moar->addFooter('<script src="/js/tablesorter.min.js" defer></script> + <script> +$(document).ready(function(){ + $(function(){ + $("table").tablesorter(); + }); + + var table = $("table"); + table.bind("sortEnd",function() { + var i = 1; + table.find("tr:gt(0)").each(function(){ + $(this).find("td:eq(0)").text(i); + i++; + }); + }); +}); + </script> + '); $result = $db->doQuery("SELECT * FROM " . DBPREFIX . "member;"); ?> - <h1>Adress Liste</h1> + <h1>Adressliste</h1> <?php if ( $option == "update"){ ?> @@ -191,9 +212,11 @@ function print_list($option = false){ <?php $count = 1; while ( $row = $result->fetch_array(MYSQLI_ASSOC) ){ + $surname = explode(" ", $row['name']); + $surname = $surname[ count($surname) - 1 ]; echo "<tr> <td>$count</td> - <td>".htmlentities($row['name'])."</td> + <td><span class=\"hidden\">".htmlentities($surname)."</span>".htmlentities($row['name'])."</td> <td>".htmlentities($row['adresse'])."</td> <td>".htmlentities($row['telefonnummer'])."</td> <td>".htmlentities($row['handynummer'])."</td> @@ -564,6 +587,9 @@ function print_account($option = false){ </form> <br> <p><strong>Mit * markierte Felder sind Pflichtfelder.</strong></p> +<h3>Log dich aus</h3> +<hr> +<a href="/?page=logout" class="btn btn-danger"><span class="glyphicon glyphicon-off"></span> Logout</a> </div> <?php } @@ -735,7 +761,8 @@ function show_gallery(){ <li><a href="#change" role="tab" onclick="$('#modal-edit-gallery').modal('show');"><span class="glyphicon glyphicon-cog"></span> Ändern</a></li> <li><a href="#new" role="tab" onclick="$('#modal-new-gallery').modal('show')"><span class="fa fa-plus"></span> Neu</a></li> <li><a href="#delete" role="tab" onclick="$('#modal-delete-gallery').modal('show')"><span class="glyphicon glyphicon-trash"></span> Löschen</a></li> - <li><a href="/?page=downloadGallery&gallery=<?php echo htmlentities($_GET["gallery"]); ?>" role="tab"><i class="fa fa-download"></i> + <li><a class="download" href="/?page=action&task=downloadGallery&gallery=<?php echo htmlentities($_GET["gallery"]); ?>" role="tab"><i class="fa fa-download"></i> + <?php if ( isset($row['name']) && ! $c->exists2(CACHEPREFIX . $_GET["gallery"]) ) $c->set2(CACHEPREFIX . $_GET["gallery"], $row['name']); ?> Download</a></li> </ul> <div class="tab-content"> @@ -745,7 +772,7 @@ function show_gallery(){ <!-- Start Tab 'Gallery' --> <div class="tab-pane active effect" id="galerie"> -<div id="blueimp-gallery" class="blueimp-gallery" data-use-bootstrap-modal="false"> +<div id="blueimp-gallery" class="blueimp-gallery blueimp-gallery-controls" data-use-bootstrap-modal="false"> <!-- The container for the modal slides --> <div class="slides"></div> <!-- Controls for the borderless lightbox --> @@ -779,7 +806,7 @@ function show_gallery(){ </div> </div> <?php - if ( $c->exists( CACHEPREFIX . "gallery_imagelinks_" . $_SESSION["gallery"] ) ){ + if ( $c->exists2( CACHEPREFIX . "gallery_imagelinks_" . $_SESSION["gallery"] ) ){ echo $c->get2( CACHEPREFIX . "gallery_imagelinks_" . $_SESSION["gallery"] ); } else { ob_start(); @@ -794,6 +821,11 @@ function show_gallery(){ echo "</div>"; } else { echo "<h4>Keine Bilder in der aktuellen Gallerie vorhanden!</h4>"; + $c->set2(CACHEPREFIX . "gallery_no_download_" . $_SESSION["gallery"], 1); + $moar->addFooter('<script>$(".download").addClass("disabled"); + $("body").on("click", "a.disabled", function(event){ + event.preventDefault(); + });</script>'); } $c->set2( CACHEPREFIX . "gallery_imagelinks_" . $_SESSION["gallery"], ob_get_contents() ); ob_end_flush(); @@ -804,6 +836,12 @@ function show_gallery(){ <?php } elseif ( $_GET["mode"] == "upload" ){ $moar->addFooter('<script src="/js/upload.min.js" defer></script>'); + //$moar->addFooter('<script src="/js/upload.js" defer></script>'); + if ( $c->exists2(CACHEPREFIX . "gallery_no_download_" . $_SESSION["gallery"]) ) + $moar->addFooter('<script>$(".download").addClass("disabled"); + $("body").on("click", "a.disabled", function(event){ + event.preventDefault(); + });</script>'); ?> <!-- Start Tab 'Upload' --> <div class="tab-pane active effect" id="upload"> @@ -2,6 +2,7 @@ require_once( dirname(__FILE__) . '/bootstrap.php'); ob_start('minify'); +session_set_cookie_params(SESSION_LIFETIME, '/', HOST, true, true); session_name(SESSION); session_start(); @@ -121,16 +122,17 @@ require_once 'static/header.php'; <?php require_once 'static/footer.php'; $moar->playFooter(); +include("static/piwik.html"); ?> </body> </html> <?php $html = ob_get_contents(); -ob_end_clean(); - $html = $moar->magicHeader($html); +ob_clean(); echo $html; if ( ! $c->bypassCache && $_SERVER["REQUEST_METHOD"] == "GET" && $_SERVER["REDIRECT_STATUS"] == 200 ) { $c->setPageCache($token, $html, 3600); } +ob_end_flush(); diff --git a/static/eyecancer.js b/js/eyecancer.js index 10c90e0..c2c2168 100644 --- a/static/eyecancer.js +++ b/js/eyecancer.js @@ -33,6 +33,7 @@ function startRandomizer(){ $(".wrapper").append("<img class='gen' id='"+imgid[i]+"' src='/static/img/flare1.png'style='visibility:hidden;'/>"); } $(".navbar").append("<audio src='/static/chu.mp3' autoplay loop></audio>") + $(".wrapper").css({"background-color" : "black"}); window.setInterval(function () { $("#gen").remove(); var newcancer = "#"; diff --git a/static/eyecancer.min.js b/js/eyecancer.min.js index 1483787..1483787 100644 --- a/static/eyecancer.min.js +++ b/js/eyecancer.min.js diff --git a/js/functions.js b/js/functions.js new file mode 100644 index 0000000..6a460ad --- /dev/null +++ b/js/functions.js @@ -0,0 +1,21 @@ +$('#btn-send').click(function () { + var btn = $(this); + btn.button('loading'); +}); +$('.close').click(function () { + $('#btn-send').button('reset'); +}); +$('.modal').draggable({ + handle: ".modal-header" +}); +function loadFancy(){ + document.getElementById("loader").style.backgroundImage="url('/img/loading.gif')"; + document.getElementById("loader").style.visibility="visible"; + document.getElementById("loader-bg").style.visibility="visible"; + + var eyecancer = document.createElement("script"); + eyecancer.type = "text/javascript"; + eyecancer.src = "/js/eyecancer.min.js"; + document.getElementsByTagName("head")[0].appendChild(eyecancer); + return false; +} diff --git a/js/tablesorter.js b/js/tablesorter.js new file mode 100644 index 0000000..08af733 --- /dev/null +++ b/js/tablesorter.js @@ -0,0 +1,1901 @@ +/**! +* TableSorter 2.17.8 - 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 +* +* @type jQuery +* @name tablesorter +* @cat Plugins/Tablesorter +* @author Christian Bach/christian.bach@polyester.se +* @contributor Rob Garrison/https://github.com/Mottie/tablesorter +*/ +/*jshint browser:true, jquery:true, unused:false, expr: true */ +/*global console:false, alert:false */ +!(function($) { + "use strict"; + $.extend({ + /*jshint supernew:true */ + tablesorter: new function() { + + var ts = this; + + ts.version = "2.17.8"; + + 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} = <i/> (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 + tabIndex : true, // add tabindex to header for keyboard accessibility + dateFormat : 'ddmmyyyy', // 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 + sortStable : false, // when sorting two rows with exactly the same content, the original sort order is maintained + + 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 : 'basic', // text extraction method/function - function(node, table, cellIndex){} + textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in textExtraction function) + textSorter : null, // choose overall or specific column sorter function(a, b, direction, table, columnIndex) [alt: ts.sortText] + numberSorter : null, // choose overall numeric sorter function(a, b, direction, maxColumnValue) + + // *** 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){}, + + // *** extra css class names + tableClass : '', + cssAsc : '', + cssDesc : '', + cssNone : '', + cssHeader : '', + cssHeaderRow : '', + cssProcessing : '', // processing icon applied to header during sort/filter + + cssChildRow : 'tablesorter-childRow', // class name indiciating that a row is to be attached to the its parent + cssIcon : 'tablesorter-icon', // if this class exists, a <i> will be added to the header automatically + cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!) + + // *** 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: [], + empties: {}, + strings: {}, + parsers: [] + + // deprecated; but retained for backwards compatibility + // widgetZebra: { css: ["even", "odd"] } + + }; + + // internal css classes - these will ALWAYS be added to + // the table and MUST only contain one class name - fixes #381 + ts.css = { + table : 'tablesorter', + cssHasChild: 'tablesorter-hasChildRow', + childRow : 'tablesorter-childRow', + header : 'tablesorter-header', + headerRow : 'tablesorter-headerRow', + headerIn : 'tablesorter-header-inner', + icon : 'tablesorter-icon', + info : 'tablesorter-infoOnly', + processing : 'tablesorter-processing', + sortAsc : 'tablesorter-headerAsc', + sortDesc : 'tablesorter-headerDesc', + sortNone : 'tablesorter-headerUnSorted' + }; + + // labels applied to sortable headers for accessibility (aria) support + ts.language = { + sortAsc : 'Ascending sort applied, ', + sortDesc : 'Descending sort applied, ', + sortNone : 'No sort applied, ', + nextAsc : 'activate to apply an ascending sort', + nextDesc : 'activate to apply a descending sort', + nextNone : 'activate to remove the sort' + }; + + /* debuging utils */ + function log() { + var a = arguments[0], + s = arguments.length > 1 ? Array.prototype.slice.call(arguments) : a; + if (typeof console !== "undefined" && typeof console.log !== "undefined") { + console[ /error/i.test(a) ? 'error' : /warn/i.test(a) ? 'warn' : 'log' ](s); + } else { + alert(s); + } + } + + function benchmark(s, d) { + log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); + } + + ts.log = log; + ts.benchmark = benchmark; + + // $.isEmptyObject from jQuery v1.4 + function isEmptyObject(obj) { + /*jshint forin: false */ + for (var name in obj) { + return false; + } + return true; + } + + function getElementText(table, node, cellIndex) { + if (!node) { return ""; } + var te, c = table.config, + t = c.textExtraction || '', + text = ""; + if (t === "basic") { + // check data-attribute first + text = $(node).attr(c.textAttribute) || node.textContent || node.innerText || $(node).text() || ""; + } else { + if (typeof(t) === "function") { + text = t(node, table, cellIndex); + } else if (typeof (te = ts.getColumnData( table, t, cellIndex )) === 'function') { + text = te(node, table, cellIndex); + } else { + // previous "simple" method + text = node.textContent || node.innerText || $(node).text() || ""; + } + } + return $.trim(text); + } + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var cur, + i = ts.parsers.length, + node = false, + nodeValue = '', + keepLooking = true; + while (nodeValue === '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = rows[rowIndex].cells[cellIndex]; + nodeValue = getElementText(table, node, cellIndex); + if (table.config.debug) { + log('Checking if value was empty on row ' + rowIndex + ', column: ' + cellIndex + ': "' + nodeValue + '"'); + } + } else { + keepLooking = false; + } + } + while (--i >= 0) { + cur = ts.parsers[i]; + // ignore the default text parser because it will always be true + if (cur && cur.id !== 'text' && cur.is && cur.is(nodeValue, table, node)) { + return cur; + } + } + // nothing found, return the generic parser (text) + return ts.getParserById('text'); + } + + 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, np, p, e, time, + j = 0, + parsersDebug = "", + len = tb.length; + if ( len === 0) { + return c.debug ? log('Warning: *Empty table!* Not building a parser cache') : ''; + } else if (c.debug) { + time = new Date(); + log('Detecting parsers for each column'); + } + list = { + extractors: [], + parsers: [] + }; + while (j < len) { + rows = tb[j].rows; + if (rows[j]) { + l = c.columns; // rows[j].cells.length; + for (i = 0; i < l; i++) { + h = c.$headers.filter('[data-column="' + i + '"]:last'); + // get column indexed table cell + ch = ts.getColumnData( table, c.headers, i ); + // get column parser/extractor + e = ts.getParserById( ts.getData(h, ch, 'extractor') ); + p = ts.getParserById( ts.getData(h, ch, 'sorter') ); + np = ts.getData(h, ch, 'parser') === 'false'; + // empty cells behaviour - keeping emptyToBottom for backwards compatibility + c.empties[i] = ( ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase(); + // text strings behaviour in numerical sorts + c.strings[i] = ( ts.getData(h, ch, 'string') || c.stringTo || 'max' ).toLowerCase(); + if (np) { + p = ts.getParserById('no-parser'); + } + if (!e) { + // For now, maybe detect someday + e = false; + } + if (!p) { + p = detectParserForColumn(table, rows, -1, i); + } + if (c.debug) { + parsersDebug += "column:" + i + "; extractor:" + e.id + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; + } + list.parsers[i] = p; + list.extractors[i] = e; + } + } + j += (list.parsers.length) ? len : 1; + } + if (c.debug) { + log(parsersDebug ? parsersDebug : "No parsers detected"); + benchmark("Completed detecting parsers", time); + } + c.parsers = list.parsers; + c.extractors = list.extractors; + } + + /* utils */ + function buildCache(table) { + var cc, t, tx, v, i, j, k, $row, rows, cols, cacheTime, + totalRows, rowData, colMax, + c = table.config, + $tb = c.$table.children('tbody'), + extractors = c.extractors, + parsers = c.parsers; + c.cache = {}; + c.totalRows = 0; + // if no parsers found, return - it's an empty table. + if (!parsers) { + return c.debug ? log('Warning: *Empty table!* Not building a cache') : ''; + } + if (c.debug) { + cacheTime = new Date(); + } + // processing icon + if (c.showProcessing) { + ts.isProcessing(table, true); + } + for (k = 0; k < $tb.length; k++) { + colMax = []; // column max value per tbody + cc = c.cache[k] = { + normalized: [] // array of normalized row data; last entry contains "rowData" above + // colMax: # // added at the end + }; + + // ignore tbodies with class name from c.cssInfoBlock + if (!$tb.eq(k).hasClass(c.cssInfoBlock)) { + totalRows = ($tb[k] && $tb[k].rows.length) || 0; + for (i = 0; i < totalRows; ++i) { + rowData = { + // order: original row order # + // $row : jQuery Object[] + child: [] // child row text (filter widget) + }; + /** Add the table data to main data array */ + $row = $($tb[k].rows[i]); + rows = [ new Array(c.columns) ]; + cols = []; + // if this is a child row, add it to the last row's children and continue to the next row + // ignore child row class, if it is the first row + if ($row.hasClass(c.cssChildRow) && i !== 0) { + t = cc.normalized.length - 1; + cc.normalized[t][c.columns].$row = cc.normalized[t][c.columns].$row.add($row); + // add "hasChild" class name to parent row + if (!$row.prev().hasClass(c.cssChildRow)) { + $row.prev().addClass(ts.css.cssHasChild); + } + // save child row content (un-parsed!) + rowData.child[t] = $.trim( $row[0].textContent || $row[0].innerText || $row.text() || "" ); + // go to the next for loop + continue; + } + rowData.$row = $row; + rowData.order = i; // add original row position to rowCache + for (j = 0; j < c.columns; ++j) { + if (typeof parsers[j] === 'undefined') { + if (c.debug) { + log('No parser found for cell:', $row[0].cells[j], 'does it have a header?'); + } + continue; + } + t = getElementText(table, $row[0].cells[j], j); + // do extract before parsing if there is one + if (typeof extractors[j].id === 'undefined') { + tx = t; + } else { + tx = extractors[j].format(t, table, $row[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].id === 'no-parser' ? '' : parsers[j].format(tx, table, $row[0].cells[j], j); + cols.push( c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v ); + if ((parsers[j].type || '').toLowerCase() === "numeric") { + // determine column max value (ignore sign) + colMax[j] = Math.max(Math.abs(v) || 0, colMax[j] || 0); + } + } + // ensure rowData is always in the same location (after the last column) + cols[c.columns] = rowData; + cc.normalized.push(cols); + } + cc.colMax = colMax; + // total up rows, not including child rows + c.totalRows += cc.normalized.length; + } + } + if (c.showProcessing) { + ts.isProcessing(table); // remove processing icon + } + if (c.debug) { + benchmark("Building cache for " + totalRows + " rows", cacheTime); + } + } + + // init flag (true) used by pager plugin to prevent widget application + function appendToTable(table, init) { + var c = table.config, + wo = c.widgetOptions, + b = table.tBodies, + rows = [], + cc = c.cache, + n, totalRows, $bk, $tb, + i, k, appendTime; + // empty table - fixes #206/#346 + if (isEmptyObject(cc)) { + // run pager appender in case the table was just emptied + return c.appender ? c.appender(table, rows) : + table.isUpdating ? c.$table.trigger("updateComplete", table) : ''; // Fixes #532 + } + if (c.debug) { + appendTime = new Date(); + } + for (k = 0; k < b.length; k++) { + $bk = $(b[k]); + if ($bk.length && !$bk.hasClass(c.cssInfoBlock)) { + // get tbody + $tb = ts.processTbody(table, $bk, true); + n = cc[k].normalized; + totalRows = n.length; + for (i = 0; i < totalRows; i++) { + rows.push(n[i][c.columns].$row); + // removeRows used by the pager plugin; don't render if using ajax - fixes #411 + if (!c.appender || (c.pager && (!c.pager.removeRows || !wo.pager_removeRows) && !c.pager.ajax)) { + $tb.append(n[i][c.columns].$row); + } + } + // restore tbody + ts.processTbody(table, $tb, false); + } + } + if (c.appender) { + c.appender(table, rows); + } + if (c.debug) { + benchmark("Rebuilt table", appendTime); + } + // apply table widgets; but not before ajax completes + if (!init && !c.appender) { ts.applyWidget(table); } + if (table.isUpdating) { + c.$table.trigger("updateComplete", table); + } + } + + function formatSortingOrder(v) { + // look for "d" in "desc" order; return true + return (/^d/i.test(v) || v === 1); + } + + function buildHeaders(table) { + var ch, $t, + h, i, t, lock, time, + c = table.config; + c.headerList = []; + c.headerContent = []; + if (c.debug) { + time = new Date(); + } + // children tr in tfoot - see issue #196 & #547 + c.columns = ts.computeColumnIndex( c.$table.children('thead, tfoot').children('tr') ); + // add icon if cssIcon option exists + i = c.cssIcon ? '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : ''; + // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683 + c.$headers = $(table).find(c.selectorHeaders).each(function(index) { + $t = $(this); + // make sure to get header cell & not column indexed cell + ch = ts.getColumnData( table, c.headers, index, true ); + // save original header content + c.headerContent[index] = $(this).html(); + // if headerTemplate is empty, don't reformat the header cell + if ( c.headerTemplate !== '' ) { + // set up header template + t = c.headerTemplate.replace(/\{content\}/g, $(this).html()).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).html('<div class="' + ts.css.headerIn + '">' + t + '</div>'); // faster than wrapInner + } + if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } + this.column = parseInt( $(this).attr('data-column'), 10); + 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 + 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(ts.css.header + ' ' + c.cssHeader); + // add cell to headerList + c.headerList[index] = this; + // add to parent in case there are multiple rows + $t.parent().addClass(ts.css.headerRow + ' ' + c.cssHeaderRow).attr('role', 'row'); + // allow keyboard cursor to focus on element + if (c.tabIndex) { $t.attr("tabindex", 0); } + }).attr({ + scope: 'col', + role : 'columnheader' + }); + // enable/disable sorting + updateHeader(table); + if (c.debug) { + benchmark("Built headers:", time); + log(c.$headers); + } + } + + function commonUpdate(table, resort, callback) { + var c = table.config; + // remove rows/elements before update + c.$table.find(c.selectorRemove).remove(); + // rebuild parsers + buildParserCache(table); + // rebuild the cache map + buildCache(table); + checkResort(c.$table, resort, callback); + } + + function updateHeader(table) { + var s, $th, col, + c = table.config; + c.$headers.each(function(index, th){ + $th = $(th); + col = ts.getColumnData( table, c.headers, index, true ); + // add "sorter-false" class if "parser-false" is set + s = ts.getData( th, col, 'sorter' ) === 'false' || ts.getData( th, col, 'parser' ) === 'false'; + th.sortDisabled = s; + $th[ s ? 'addClass' : 'removeClass' ]('sorter-false').attr('aria-disabled', '' + s); + // aria-controls - requires table ID + if (table.id) { + if (s) { + $th.removeAttr('aria-controls'); + } else { + $th.attr('aria-controls', table.id); + } + } + }); + } + + function setHeadersCss(table) { + var f, i, j, + c = table.config, + list = c.sortList, + len = list.length, + none = ts.css.sortNone + ' ' + c.cssNone, + css = [ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc], + aria = ['ascending', 'descending'], + // find the footer + $t = $(table).find('tfoot tr').children().add(c.$extraHeaders).removeClass(css.join(' ')); + // remove all header information + c.$headers + .removeClass(css.join(' ')) + .addClass(none).attr('aria-sort', 'none'); + for (i = 0; i < len; 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] + '"]' + (len === 1 ? ':last' : '') ); + if (f.length) { + for (j = 0; j < f.length; j++) { + if (!f[j].sortDisabled) { + f.eq(j).removeClass(none).addClass(css[list[i][1]]).attr('aria-sort', aria[list[i][1]]); + } + } + // add sorted class to footer & extra headers, if they exist + if ($t.length) { + $t.filter('[data-column="' + list[i][0] + '"]').removeClass(none).addClass(css[list[i][1]]); + } + } + } + } + // add verbose aria labels + c.$headers.not('.sorter-false').each(function(){ + var $this = $(this), + nextSort = this.order[(this.count + 1) % (c.sortReset ? 3 : 2)], + txt = $this.text() + ': ' + + ts.language[ $this.hasClass(ts.css.sortAsc) ? 'sortAsc' : $this.hasClass(ts.css.sortDesc) ? 'sortDesc' : 'sortNone' ] + + ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ]; + $this.attr('aria-label', txt ); + }); + } + + // automatically add col group, and column sizes if set + function fixColumnWidth(table) { + var colgroup, overallWidth, + c = table.config; + if (c.widthFixed && c.$table.find('colgroup').length === 0) { + colgroup = $('<colgroup>'); + overallWidth = $(table).width(); + // only add col for visible columns - fixes #371 + $(table.tBodies).not('.' + c.cssInfoBlock).find("tr:first").children(":visible").each(function() { + colgroup.append($('<col>').css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%')); + }); + c.$table.prepend(colgroup); + } + } + + function updateHeaderSortCount(table, list) { + var s, t, o, col, primary, + c = table.config, + sl = list || c.sortList; + c.sortList = []; + $.each(sl, function(i,v){ + // ensure all sortList values are numeric - fixes #127 + col = parseInt(v[0], 10); + // make sure header exists + o = c.$headers.filter('[data-column="' + col + '"]:last')[0]; + if (o) { // prevents error if sorton array is wrong + // o.count = o.count + 1; + t = ('' + v[1]).match(/^(1|d|s|o|n)/); + t = t ? t[0] : ''; + // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext + switch(t) { + case '1': case 'd': // descending + t = 1; + break; + case 's': // same direction (as primary column) + // if primary sort is set to "s", make it ascending + t = primary || 0; + break; + case 'o': + s = o.order[(primary || 0) % (c.sortReset ? 3 : 2)]; + // opposite of primary column; but resets if primary resets + t = s === 0 ? 1 : s === 1 ? 0 : 2; + break; + case 'n': + o.count = o.count + 1; + t = o.order[(o.count) % (c.sortReset ? 3 : 2)]; + break; + default: // ascending + t = 0; + break; + } + primary = i === 0 ? t : primary; + s = [ col, parseInt(t, 10) || 0 ]; + 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 getCachedSortType(parsers, i) { + return (parsers && parsers[i]) ? parsers[i].type || '' : ''; + } + + function initSort(table, cell, event){ + if (table.isUpdating) { + // let any updates complete before initializing a sort + return setTimeout(function(){ initSort(table, cell, event); }, 50); + } + var arry, indx, col, order, s, + c = table.config, + key = !event[c.sortMultiSortKey], + $table = c.$table; + // Only call sortStart if sorting is enabled + $table.trigger("sortStart", table); + // get current column sort order + cell.count = event[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2); + // reset all sorts on non-current column - issue #30 + if (c.sortRestart) { + indx = 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 !== indx && (key || !$(this).is('.' + ts.css.sortDesc + ',.' + ts.css.sortAsc))) { + this.count = -1; + } + }); + } + // get current column index + indx = cell.column; + // user only wants to sort on one column + if (key) { + // flush the sort list + c.sortList = []; + if (c.sortForce !== null) { + arry = c.sortForce; + for (col = 0; col < arry.length; col++) { + if (arry[col][0] !== indx) { + c.sortList.push(arry[col]); + } + } + } + // add column to sort list + order = cell.order[cell.count]; + if (order < 2) { + c.sortList.push([indx, order]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (col = 1; col < cell.colSpan; col++) { + c.sortList.push([indx + col, order]); + } + } + } + // multi column sorting + } else { + // get rid of the sortAppend before adding more - fixes issue #115 & #523 + if (c.sortAppend && c.sortList.length > 1) { + for (col = 0; col < c.sortAppend.length; col++) { + s = ts.isValueInArray(c.sortAppend[col][0], c.sortList); + if (s >= 0) { + c.sortList.splice(s,1); + } + } + } + // the user has clicked on an already sorted column + if (ts.isValueInArray(indx, c.sortList) >= 0) { + // reverse the sorting direction + for (col = 0; col < c.sortList.length; col++) { + s = c.sortList[col]; + order = c.$headers.filter('[data-column="' + s[0] + '"]:last')[0]; + if (s[0] === indx) { + // order.count seems to be incorrect when compared to cell.count + s[1] = order.order[cell.count]; + if (s[1] === 2) { + c.sortList.splice(col,1); + order.count = -1; + } + } + } + } else { + // add column to sort list array + order = cell.order[cell.count]; + if (order < 2) { + c.sortList.push([indx, order]); + // add other columns if header spans across multiple + if (cell.colSpan > 1) { + for (col = 1; col < cell.colSpan; col++) { + c.sortList.push([indx + col, order]); + } + } + } + } + } + if (c.sortAppend !== null) { + arry = c.sortAppend; + for (col = 0; col < arry.length; col++) { + if (arry[col][0] !== indx) { + c.sortList.push(arry[col]); + } + } + } + // sortBegin event triggered immediately before the sort + $table.trigger("sortBegin", table); + // setTimeout needed so the processing icon shows up + setTimeout(function(){ + // set css for headers + setHeadersCss(table); + multisort(table); + appendToTable(table); + $table.trigger("sortEnd", table); + }, 1); + } + + // sort multiple columns + function multisort(table) { /*jshint loopfunc:true */ + var i, k, num, col, sortTime, colMax, + cache, order, sort, x, y, + dir = 0, + c = table.config, + cts = c.textSorter || '', + sortList = c.sortList, + l = sortList.length, + bl = table.tBodies.length; + if (c.serverSideSorting || isEmptyObject(c.cache)) { // empty table - fixes #206/#346 + return; + } + if (c.debug) { sortTime = new Date(); } + for (k = 0; k < bl; k++) { + colMax = c.cache[k].colMax; + cache = c.cache[k].normalized; + + cache.sort(function(a, b) { + // cache is undefined here in IE, so don't use it! + for (i = 0; i < l; i++) { + col = sortList[i][0]; + order = sortList[i][1]; + // sort direction, true = asc, false = desc + dir = order === 0; + + if (c.sortStable && a[col] === b[col] && l === 1) { + return a[c.columns].order - b[c.columns].order; + } + + // fallback to natural sort since it is more robust + num = /n/i.test(getCachedSortType(c.parsers, col)); + if (num && c.strings[col]) { + // sort strings in numerical columns + if (typeof (c.string[c.strings[col]]) === 'boolean') { + num = (dir ? 1 : -1) * (c.string[c.strings[col]] ? -1 : 1); + } else { + num = (c.strings[col]) ? c.string[c.strings[col]] || 0 : 0; + } + // fall back to built-in numeric sort + // var sort = $.tablesorter["sort" + s](table, a[c], b[c], c, colMax[c], dir); + sort = c.numberSorter ? c.numberSorter(a[col], b[col], dir, colMax[col], table) : + ts[ 'sortNumeric' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], num, colMax[col], col, table); + } else { + // set a & b depending on sort direction + x = dir ? a : b; + y = dir ? b : a; + // text sort function + if (typeof(cts) === 'function') { + // custom OVERALL text sorter + sort = cts(x[col], y[col], dir, col, table); + } else if (typeof(cts) === 'object' && cts.hasOwnProperty(col)) { + // custom text sorter for a SPECIFIC COLUMN + sort = cts[col](x[col], y[col], dir, col, table); + } else { + // fall back to natural sort + sort = ts[ 'sortNatural' + (dir ? 'Asc' : 'Desc') ](a[col], b[col], col, table, c); + } + } + if (sort) { return sort; } + } + return a[c.columns].order - b[c.columns].order; + }); + } + if (c.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); } + } + + function resortComplete($table, callback){ + var table = $table[0]; + if (table.isUpdating) { + $table.trigger('updateComplete', table); + } + if ($.isFunction(callback)) { + callback($table[0]); + } + } + + function checkResort($table, flag, callback) { + var sl = $table[0].config.sortList; + // don't try to resort if the table is still processing + // this will catch spamming of the updateCell method + if (flag !== false && !$table[0].isProcessing && sl.length) { + $table.trigger("sorton", [sl, function(){ + resortComplete($table, callback); + }, true]); + } else { + resortComplete($table, callback); + ts.applyWidget($table[0], false); + } + } + + function bindMethods(table){ + var c = table.config, + $table = c.$table; + // apply easy methods that trigger bound events + $table + .unbind('sortReset update updateRows updateCell updateAll addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave '.split(' ').join(c.namespace + ' ')) + .bind("sortReset" + c.namespace, function(e, callback){ + e.stopPropagation(); + c.sortList = []; + setHeadersCss(table); + multisort(table); + appendToTable(table); + if ($.isFunction(callback)) { + callback(table); + } + }) + .bind("updateAll" + c.namespace, function(e, resort, callback){ + e.stopPropagation(); + table.isUpdating = true; + ts.refreshWidgets(table, true, true); + ts.restoreHeaders(table); + buildHeaders(table); + ts.bindEvents(table, c.$headers, true); + bindMethods(table); + commonUpdate(table, resort, callback); + }) + .bind("update" + c.namespace + " updateRows" + c.namespace, function(e, resort, callback) { + e.stopPropagation(); + table.isUpdating = true; + // update sorting (if enabled/disabled) + updateHeader(table); + commonUpdate(table, resort, callback); + }) + .bind("updateCell" + c.namespace, function(e, cell, resort, callback) { + e.stopPropagation(); + table.isUpdating = true; + $table.find(c.selectorRemove).remove(); + // get position from the dom + var v, t, row, icell, + $tb = $table.find('tbody'), + $cell = $(cell), + // 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( $.fn.closest ? $cell.closest('tbody') : $cell.parents('tbody').filter(':first') ), + $row = $.fn.closest ? $cell.closest('tr') : $cell.parents('tr').filter(':first'); + 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.index(); + c.cache[tbdy].normalized[row][c.columns].$row = $row; + if (typeof c.extractors[icell].id === 'undefined') { + t = getElementText(table, cell, icell); + } else { + t = c.extractors[icell].format( getElementText(table, cell, icell), table, cell, icell ); + } + v = c.parsers[icell].id === 'no-parser' ? '' : + c.parsers[icell].format( t, table, cell, icell ); + c.cache[tbdy].normalized[row][icell] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v; + if ((c.parsers[icell].type || '').toLowerCase() === "numeric") { + // update column max value (ignore sign) + c.cache[tbdy].colMax[icell] = Math.max(Math.abs(v) || 0, c.cache[tbdy].colMax[icell] || 0); + } + checkResort($table, resort, callback); + } + }) + .bind("addRows" + c.namespace, function(e, $row, resort, callback) { + e.stopPropagation(); + table.isUpdating = true; + if (isEmptyObject(c.cache)) { + // empty table, do an update instead - fixes #450 + updateHeader(table); + commonUpdate(table, resort, callback); + } else { + $row = $($row).attr('role', 'row'); // make sure we're using a jQuery object + var i, j, l, t, v, rowData, cells, + rows = $row.filter('tr').length, + tbdy = $table.find('tbody').index( $row.parents('tbody').filter(':first') ); + // fixes adding rows to an empty table - see issue #179 + if (!(c.parsers && c.parsers.length)) { + buildParserCache(table); + } + // add each row + for (i = 0; i < rows; i++) { + l = $row[i].cells.length; + cells = []; + rowData = { + child: [], + $row : $row.eq(i), + order: c.cache[tbdy].normalized.length + }; + // add each cell + for (j = 0; j < l; j++) { + if (typeof c.extractors[j].id === 'undefined') { + t = getElementText(table, $row[i].cells[j], j); + } else { + t = c.extractors[j].format( getElementText(table, $row[i].cells[j], j), table, $row[i].cells[j], j ); + } + v = c.parsers[j].id === 'no-parser' ? '' : + c.parsers[j].format( t, table, $row[i].cells[j], j ); + cells[j] = c.ignoreCase && typeof v === 'string' ? v.toLowerCase() : v; + if ((c.parsers[j].type || '').toLowerCase() === "numeric") { + // update column max value (ignore sign) + c.cache[tbdy].colMax[j] = Math.max(Math.abs(cells[j]) || 0, c.cache[tbdy].colMax[j] || 0); + } + } + // add the row data to the end + cells.push(rowData); + // update cache + c.cache[tbdy].normalized.push(cells); + } + // resort using current settings + checkResort($table, resort, callback); + } + }) + .bind("updateComplete" + c.namespace, function(){ + table.isUpdating = false; + }) + .bind("sorton" + c.namespace, function(e, list, callback, init) { + var c = table.config; + e.stopPropagation(); + $table.trigger("sortStart", this); + // update header count index + updateHeaderSortCount(table, list); + // set css for headers + setHeadersCss(table); + // fixes #346 + if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); } + $table.trigger("sortBegin", this); + // sort the table and append it to the dom + multisort(table); + appendToTable(table, init); + $table.trigger("sortEnd", this); + ts.applyWidget(table); + if ($.isFunction(callback)) { + callback(table); + } + }) + .bind("appendCache" + c.namespace, function(e, callback, init) { + e.stopPropagation(); + appendToTable(table, init); + if ($.isFunction(callback)) { + callback(table); + } + }) + .bind("updateCache" + c.namespace, function(e, callback){ + // rebuild parsers + if (!(c.parsers && c.parsers.length)) { + buildParserCache(table); + } + // rebuild the cache map + buildCache(table); + if ($.isFunction(callback)) { + callback(table); + } + }) + .bind("applyWidgetId" + c.namespace, function(e, id) { + e.stopPropagation(); + ts.getWidgetById(id).format(table, c, c.widgetOptions); + }) + .bind("applyWidgets" + c.namespace, function(e, init) { + e.stopPropagation(); + // apply widgets + ts.applyWidget(table, init); + }) + .bind("refreshWidgets" + c.namespace, function(e, all, dontapply){ + e.stopPropagation(); + ts.refreshWidgets(table, all, dontapply); + }) + .bind("destroy" + c.namespace, function(e, c, cb){ + e.stopPropagation(); + ts.destroy(table, c, cb); + }) + .bind("resetToLoadState" + c.namespace, function(){ + // remove all widgets + ts.refreshWidgets(table, true, true); + // restore original settings; this clears out current settings, but does not clear + // values saved to storage. + c = $.extend(true, ts.defaults, c.originalSettings); + table.hasInitialized = false; + // setup the entire table again + ts.setup( table, c ); + }); + } + + /* public methods */ + ts.construct = function(settings) { + return this.each(function() { + var table = this, + // merge & extend config options + c = $.extend(true, {}, ts.defaults, settings); + // save initial settings + c.originalSettings = settings; + // create a table from data (build table widget) + if (!table.hasInitialized && ts.buildTable && this.tagName !== 'TABLE') { + // return the table (in case the original target is the table's container) + ts.buildTable(table, c); + } else { + ts.setup(table, c); + } + }); + }; + + ts.setup = function(table, c) { + // if no thead or tbody, or tablesorter is already present, quit + if (!table || !table.tHead || table.tBodies.length === 0 || table.hasInitialized === true) { + return c.debug ? log('ERROR: stopping initialization! No table, thead, tbody or tablesorter has already been initialized') : ''; + } + + var k = '', + $table = $(table), + m = $.metadata; + // initialization flag + table.hasInitialized = false; + // table is being processed flag + table.isProcessing = true; + // make sure to store the config object + table.config = c; + // save the settings where they read + $.data(table, "tablesorter", c); + if (c.debug) { $.data( table, 'startoveralltimer', new Date()); } + + // removing this in version 3 (only supports jQuery 1.7+) + c.supportsDataObject = (function(version) { + version[0] = parseInt(version[0], 10); + return (version[0] > 1) || (version[0] === 1 && parseInt(version[1], 10) >= 4); + })($.fn.jquery.split(".")); + // digit sort text location; keeping max+/- for backwards compatibility + c.string = { 'max': 1, 'min': -1, 'emptymin': 1, 'emptymax': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; + // ensure case insensitivity + c.emptyTo = c.emptyTo.toLowerCase(); + c.stringTo = c.stringTo.toLowerCase(); + // add table theme class only if there isn't already one there + if (!/tablesorter\-/.test($table.attr('class'))) { + k = (c.theme !== '' ? ' tablesorter-' + c.theme : ''); + } + c.table = table; + c.$table = $table + .addClass(ts.css.table + ' ' + c.tableClass + k) + .attr('role', 'grid'); + c.$headers = $table.find(c.selectorHeaders); + + // give the table a unique id, which will be used in namespace binding + if (!c.namespace) { + c.namespace = '.tablesorter' + Math.random().toString(16).slice(2); + } else { + // make sure namespace starts with a period & doesn't have weird characters + c.namespace = '.' + c.namespace.replace(/\W/g,''); + } + + c.$table.children().children('tr').attr('role', 'row'); + c.$tbodies = $table.children('tbody:not(.' + c.cssInfoBlock + ')').attr({ + 'aria-live' : 'polite', + 'aria-relevant' : 'all' + }); + if (c.$table.find('caption').length) { + c.$table.attr('aria-labelledby', 'theCaption'); + } + c.widgetInit = {}; // keep a list of initialized widgets + // change textExtraction via data-attribute + c.textExtraction = c.$table.attr('data-text-extraction') || c.textExtraction || 'basic'; + // build headers + buildHeaders(table); + // fixate columns if the users supplies the fixedWidth option + // do this after theme has been applied + fixColumnWidth(table); + // try to auto detect column type, and store in tables config + buildParserCache(table); + // start total row count at zero + c.totalRows = 0; + // build the cache for the tbody cells + // delayInit will delay building the cache until the user starts a sort + if (!c.delayInit) { buildCache(table); } + // bind all header events and methods + ts.bindEvents(table, c.$headers, true); + bindMethods(table); + // get sort list from jQuery data or metadata + // in jQuery < 1.4, an error occurs when calling $table.data() + if (c.supportsDataObject && typeof $table.data().sortlist !== 'undefined') { + c.sortList = $table.data().sortlist; + } else if (m && ($table.metadata() && $table.metadata().sortlist)) { + c.sortList = $table.metadata().sortlist; + } + // apply widget init code + ts.applyWidget(table, true); + // if user has supplied a sort list to constructor + if (c.sortList.length > 0) { + $table.trigger("sorton", [c.sortList, {}, !c.initWidgets, true]); + } else { + setHeadersCss(table); + if (c.initWidgets) { + // apply widget format + ts.applyWidget(table, false); + } + } + + // show processesing icon + if (c.showProcessing) { + $table + .unbind('sortBegin' + c.namespace + ' sortEnd' + c.namespace) + .bind('sortBegin' + c.namespace + ' sortEnd' + c.namespace, function(e) { + clearTimeout(c.processTimer); + ts.isProcessing(table); + if (e.type === 'sortBegin') { + c.processTimer = setTimeout(function(){ + ts.isProcessing(table, true); + }, 500); + } + }); + } + + // initialized + table.hasInitialized = true; + table.isProcessing = false; + if (c.debug) { + ts.benchmark("Overall initialization time", $.data( table, 'startoveralltimer')); + } + $table.trigger('tablesorter-initialized', table); + if (typeof c.initialized === 'function') { c.initialized(table); } + }; + + ts.getColumnData = function(table, obj, indx, getCell){ + if (typeof obj === 'undefined' || obj === null) { return; } + table = $(table)[0]; + var result, $h, k, + c = table.config; + if (obj[indx]) { + return getCell ? obj[indx] : obj[c.$headers.index( c.$headers.filter('[data-column="' + indx + '"]:last') )]; + } + for (k in obj) { + if (typeof k === 'string') { + if (getCell) { + // get header cell + $h = c.$headers.eq(indx).filter(k); + } else { + // get column indexed cell + $h = c.$headers.filter('[data-column="' + indx + '"]:last').filter(k); + } + if ($h.length) { + return obj[k]; + } + } + } + return result; + }; + + // computeTableHeaderCellIndexes from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + ts.computeColumnIndex = function(trs) { + var matrix = [], + lookup = {}, + cols = 0, // determine the number of columns + i, j, k, l, $cell, cell, 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++) { + cell = cells[j]; + $cell = $(cell); + rowIndex = cell.parentNode.rowIndex; + cellId = rowIndex + "-" + $cell.index(); + rowSpan = cell.rowSpan || 1; + colSpan = cell.colSpan || 1; + if (typeof(matrix[rowIndex]) === "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) === "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + cols = Math.max(firstAvailCol, cols); + // add data-column + $cell.attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex + for (k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) === "undefined") { + matrix[k] = []; + } + matrixrow = matrix[k]; + for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + // may not be accurate if # header columns !== # tbody columns + return cols + 1; // add one because it's a zero-based index + }; + + // *** Process table *** + // add processing indicator + ts.isProcessing = function(table, toggle, $ths) { + table = $(table); + var c = table[0].config, + // default to all headers + $h = $ths || table.find('.' + ts.css.header); + if (toggle) { + // don't use sortList if custom $ths used + if (typeof $ths !== 'undefined' && 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) >= 0; + }); + } + table.add($h).addClass(ts.css.processing + ' ' + c.cssProcessing); + } else { + table.add($h).removeClass(ts.css.processing + ' ' + c.cssProcessing); + } + }; + + // 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){ + table = $(table)[0]; + var holdr; + if (getIt) { + table.isProcessing = true; + $tb.before('<span class="tablesorter-savemyplace"/>'); + holdr = ($.fn.detach) ? $tb.detach() : $tb.remove(); + return holdr; + } + holdr = $(table).find('span.tablesorter-savemyplace'); + $tb.insertAfter( holdr ); + holdr.remove(); + table.isProcessing = false; + }; + + ts.clearTableBody = function(table) { + $(table)[0].config.$tbodies.children().detach(); + }; + + ts.bindEvents = function(table, $headers, core){ + table = $(table)[0]; + var downTime, + c = table.config; + if (core !== true) { + c.$extraHeaders = c.$extraHeaders ? c.$extraHeaders.add($headers) : $headers; + } + // apply event handling to headers and/or additional headers (stickyheaders, scroller, etc) + $headers + // http://stackoverflow.com/questions/5312849/jquery-find-self; + .find(c.selectorSort).add( $headers.filter(c.selectorSort) ) + .unbind('mousedown mouseup sort keyup '.split(' ').join(c.namespace + ' ')) + .bind('mousedown mouseup sort keyup '.split(' ').join(c.namespace + ' '), function(e, external) { + var cell, type = e.type; + // only recognize left clicks or enter + if ( ((e.which || e.button) !== 1 && !/sort|keyup/.test(type)) || (type === 'keyup' && e.which !== 13) ) { + return; + } + // ignore long clicks (prevents resizable widget from initializing a sort) + if (type === 'mouseup' && external !== true && (new Date().getTime() - downTime > 250)) { return; } + // set timer on mousedown + if (type === 'mousedown') { + downTime = new Date().getTime(); + return /(input|select|button|textarea)/i.test(e.target.tagName) ? '' : !c.cancelSelection; + } + if (c.delayInit && isEmptyObject(c.cache)) { buildCache(table); } + // jQuery v1.2.6 doesn't have closest() + cell = $.fn.closest ? $(this).closest('th, td')[0] : /TH|TD/.test(this.tagName) ? this : $(this).parents('th, td')[0]; + // reference original table headers and find the same cell + cell = c.$headers[ $headers.index( cell ) ]; + if (!cell.sortDisabled) { + initSort(table, cell, e); + } + }); + if (c.cancelSelection) { + // cancel selection + $headers + .attr('unselectable', 'on') + .bind('selectstart', false) + .css({ + 'user-select': 'none', + 'MozUserSelect': 'none' // not needed for jQuery 1.8+ + }); + } + }; + + // restore headers + ts.restoreHeaders = function(table){ + var c = $(table)[0].config; + // don't use c.$headers here in case header cells were swapped + c.$table.find(c.selectorHeaders).each(function(i){ + // only restore header cells if it is wrapped + // because this is also used by the updateAll method + if ($(this).find('.' + ts.css.headerIn).length){ + $(this).html( c.headerContent[i] ); + } + }); + }; + + ts.destroy = function(table, removeClasses, callback){ + table = $(table)[0]; + 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.' + ts.css.headerRow).removeClass(ts.css.headerRow + ' ' + c.cssHeaderRow), + $f = $t.find('tfoot:first > tr').children('th, td'); + if (removeClasses === false && $.inArray('uitheme', c.widgets) >= 0) { + // reapply uitheme classes, in case we want to maintain appearance + $t.trigger('applyWidgetId', ['uitheme']); + $t.trigger('applyWidgetId', ['zebra']); + } + // remove widget added rows, just in case + $h.find('tr').not($r).remove(); + // disable tablesorter + $t + .removeData('tablesorter') + .unbind('sortReset update updateAll updateRows updateCell addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd resetToLoadState '.split(' ').join(c.namespace + ' ')); + c.$headers.add($f) + .removeClass( [ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone].join(' ') ) + .removeAttr('data-column') + .removeAttr('aria-label') + .attr('aria-disabled', 'true'); + $r.find(c.selectorSort).unbind('mousedown mouseup keypress '.split(' ').join(c.namespace + ' ')); + ts.restoreHeaders(table); + $t.toggleClass(ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false); + // clear flag in case the plugin is initialized again + table.hasInitialized = false; + delete table.config.cache; + if (typeof callback === 'function') { + callback(table); + } + }; + + // *** sort functions *** + // regex used in natural sort + ts.regex = { + chunk : /(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, // chunk/tokenize numbers & letters + chunks: /(^\\0|\\0$)/, // replace chunks @ ends + hex: /^0x[0-9a-f]+$/i // hex + }; + + // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed) + // this function will only accept strings, or you'll see "TypeError: undefined is not a function" + // I could add a = a.toString(); b = b.toString(); but it'll slow down the sort overall + ts.sortNatural = function(a, b) { + if (a === b) { return 0; } + var xN, xD, yN, yD, xF, yF, i, mx, + r = ts.regex; + // first try and sort Hex codes + if (r.hex.test(b)) { + xD = parseInt(a.match(r.hex), 16); + yD = parseInt(b.match(r.hex), 16); + if ( xD < yD ) { return -1; } + if ( xD > yD ) { return 1; } + } + // chunk/tokenize + xN = a.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0'); + yN = b.replace(r.chunk, '\\0$1\\0').replace(r.chunks, '').split('\\0'); + 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; + }; + + ts.sortNaturalAsc = function(a, b, col, table, c) { + if (a === b) { return 0; } + var 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; } + return ts.sortNatural(a, b); + }; + + ts.sortNaturalDesc = function(a, b, col, table, c) { + if (a === b) { return 0; } + var 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; } + return ts.sortNatural(b, a); + }; + + // basic alphabetical sort + ts.sortText = function(a, b) { + return a > b ? 1 : (a < b ? -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, num, mx) { + if (mx) { + // make sure the text value is greater than the max numerical value (mx) + var i, l = a ? a.length : 0, n = mx + num; + for (i = 0; i < l; i++) { + n += a.charCodeAt(i); + } + return num * n; + } + return 0; + }; + + ts.sortNumericAsc = function(a, b, num, mx, col, table) { + 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, num, mx); } + if (isNaN(b)) { b = ts.getTextValue(b, num, mx); } + return a - b; + }; + + ts.sortNumericDesc = function(a, b, num, mx, col, table) { + 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, num, mx); } + if (isNaN(b)) { b = ts.getTextValue(b, num, mx); } + return b - a; + }; + + ts.sortNumeric = function(a, b) { + return a - b; + }; + + // 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'); + } + } + 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 ); + } + } + } + return s; + }; + + // *** utilities *** + ts.isValueInArray = function(column, arry) { + var indx, len = arry.length; + for (indx = 0; indx < len; indx++) { + if (arry[indx][0] === column) { + return indx; + } + } + return -1; + }; + + 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) { + ts.parsers.push(parser); + } + }; + + ts.getParserById = function(name) { + /*jshint eqeqeq:false */ + if (name == 'false') { return false; } + 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; + }; + + ts.addWidget = function(widget) { + ts.widgets.push(widget); + }; + + ts.hasWidget = function(table, name){ + table = $(table); + return table.length && table[0].config && table[0].config.widgetInit[name] || false; + }; + + 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; + } + } + }; + + ts.applyWidget = function(table, init) { + table = $(table)[0]; // in case this is called externally + var c = table.config, + wo = c.widgetOptions, + widgets = [], + time, w, wd; + // prevent numerous consecutive widget applications + if (init !== false && table.hasInitialized && (table.isApplyingWidgets || table.isUpdating)) { return; } + if (c.debug) { time = new Date(); } + if (c.widgets.length) { + table.isApplyingWidgets = true; + // ensure unique widget ids + c.widgets = $.grep(c.widgets, function(v, k){ + return $.inArray(v, c.widgets) === k; + }); + // build widget array & add priority as needed + $.each(c.widgets || [], function(i,n){ + wd = ts.getWidgetById(n); + if (wd && wd.id) { + // set priority to 10 if not defined + if (!wd.priority) { wd.priority = 10; } + widgets[i] = wd; + } + }); + // sort widgets by priority + widgets.sort(function(a, b){ + return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1; + }); + // add/update selected widgets + $.each(widgets, function(i,w){ + if (w) { + if (init || !(c.widgetInit[w.id])) { + // set init flag first to prevent calling init more than once (e.g. pager) + c.widgetInit[w.id] = true; + if (w.hasOwnProperty('options')) { + wo = table.config.widgetOptions = $.extend( true, {}, w.options, wo ); + } + if (w.hasOwnProperty('init')) { + w.init(table, w, c, wo); + } + } + if (!init && w.hasOwnProperty('format')) { + w.format(table, c, wo, false); + } + } + }); + } + setTimeout(function(){ + table.isApplyingWidgets = false; + }, 0); + if (c.debug) { + w = c.widgets.length; + benchmark("Completed " + (init === true ? "initializing " : "applying ") + w + " widget" + (w !== 1 ? "s" : ""), time); + } + }; + + ts.refreshWidgets = function(table, doAll, dontapply) { + table = $(table)[0]; // see issue #243 + 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 + '"' ); } + // only remove widgets that have been initialized - fixes #442 + if (w[i].hasOwnProperty('remove') && c.widgetInit[w[i].id]) { + w[i].remove(table, c, c.widgetOptions); + c.widgetInit[w[i].id] = false; + } + } + } + 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; now works with "sorter-my-custom-parser" + val = cl.match( new RegExp('\\s' + 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 { + // 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*\(([.\d]+)\)/, '-$1'); + } + i = parseFloat(s); + // return the text instead of zero + return isNaN(i) ? $.trim(s) : i; + }; + + 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: 'no-parser', + is: function() { + return false; + }, + format: function() { + return ''; + }, + type: 'text' + }); + + ts.addParser({ + id: "text", + is: function() { + return true; + }, + format: function(s, table) { + var c = table.config; + if (s) { + s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); + s = c.sortLocaleCompare ? ts.replaceAccents(s) : s; + } + return s; + }, + type: "text" + }); + + ts.addParser({ + id: "digit", + is: function(s) { + return ts.isDigit(s); + }, + format: function(s, table) { + var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ""), table); + return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "currency", + is: function(s) { + return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test((s || '').replace(/[+\-,. ]/g,'')); // £$€¤¥¢ + }, + format: function(s, table) { + var n = ts.formatFloat((s || '').replace(/[^\w,. \-()]/g, ""), table); + return s && typeof n === 'number' ? n : s ? $.trim( s && table.config.ignoreCase ? s.toLocaleLowerCase() : s ) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + 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 ? s.split(".") : '', + r = "", + l = a.length; + for (i = 0; i < l; i++) { + r += ("00" + a[i]).slice(-3); + } + return s ? ts.formatFloat(r, table) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function(s) { + return (/^(https?|ftp|file):\/\//).test(s); + }, + format: function(s) { + return s ? $.trim(s.replace(/(https?|ftp|file):\/\//, '')) : s; + }, + parsed : true, // filter widget flag + type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function(s) { + return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s); + }, + format: function(s, table) { + return s ? ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || s) : "", table) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function(s) { + return (/(\d\s*?%|%\s*?\d)/).test(s) && s.length < 15; + }, + format: function(s, table) { + return s ? ts.formatFloat(s.replace(/%/g, ""), table) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + 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 s ? ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || s), table) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" + is: function(s) { + // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included + return (/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/).test((s || '').replace(/\s+/g," ").replace(/[\-.,]/g, "/")); + }, + format: function(s, table, cell, cellIndex) { + if (s) { + var c = table.config, + ci = c.$headers.filter('[data-column=' + cellIndex + ']:last'), + format = ci.length && ci[0].dateFormat || ts.getData( ci, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat') || c.dateFormat; + s = s.replace(/\s+/g," ").replace(/[\-.,]/g, "/"); // escaped - because JSHint in Firefox was showing it as an error + 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 s ? ts.formatFloat( (new Date(s).getTime() || s), table) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "time", + 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 s ? ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || s), table) : s; + }, + type: "numeric" + }); + + ts.addParser({ + id: "metadata", + is: function() { + return false; + }, + format: function(s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, + type: "numeric" + }); + + // add default widgets + ts.addWidget({ + id: "zebra", + priority: 90, + format: function(table, c, wo) { + var $tb, $tv, $tr, row, even, time, k, + child = new RegExp(c.cssChildRow, 'i'), + b = c.$tbodies; + if (c.debug) { + time = new Date(); + } + for (k = 0; k < b.length; k++ ) { + // loop through the visible rows + row = 0; + $tb = b.eq(k); + $tv = $tb.children('tr:visible').not(c.selectorRemove); + // revered back to using jQuery each - strangely it's the fastest method + /*jshint loopfunc:true */ + $tv.each(function(){ + $tr = $(this); + // style child 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 = (wo.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); diff --git a/js/tablesorter.min.js b/js/tablesorter.min.js new file mode 100644 index 0000000..42d535d --- /dev/null +++ b/js/tablesorter.min.js @@ -0,0 +1,5 @@ +/*! +* TableSorter 2.17.8 min - Client-side table sorting with ease! +* Copyright (c) 2007 Christian Bach +*/ +!function(h){h.extend({tablesorter:new function(){function d(){var b=arguments[0],a=1<arguments.length?Array.prototype.slice.call(arguments):b;if("undefined"!==typeof console&&"undefined"!==typeof console.log)console[/error/i.test(b)?"error":/warn/i.test(b)?"warn":"log"](a);else alert(a)}function q(b,a){d(b+" ("+((new Date).getTime()-a.getTime())+"ms)")}function p(b){for(var a in b)return!1;return!0}function r(b,a,c){if(!a)return"";var f,e=b.config,l=e.textExtraction||"",d="",d="basic"===l?h(a).attr(e.textAttribute)|| a.textContent||a.innerText||h(a).text()||"":"function"===typeof l?l(a,b,c):"function"===typeof(f=g.getColumnData(b,l,c))?f(a,b,c):a.textContent||a.innerText||h(a).text()||"";return h.trim(d)}function v(b){var a,c,f=b.config,e=f.$tbodies=f.$table.children("tbody:not(."+f.cssInfoBlock+")"),l,x,k,h,m,B,u,s,t,p=0,v="",w=e.length;if(0===w)return f.debug?d("Warning: *Empty table!* Not building a parser cache"):"";f.debug&&(t=new Date,d("Detecting parsers for each column"));a=[];for(c=[];p<w;){l=e[p].rows; if(l[p])for(x=f.columns,k=0;k<x;k++){h=f.$headers.filter('[data-column="'+k+'"]:last');m=g.getColumnData(b,f.headers,k);s=g.getParserById(g.getData(h,m,"extractor"));u=g.getParserById(g.getData(h,m,"sorter"));B="false"===g.getData(h,m,"parser");f.empties[k]=(g.getData(h,m,"empty")||f.emptyTo||(f.emptyToBottom?"bottom":"top")).toLowerCase();f.strings[k]=(g.getData(h,m,"string")||f.stringTo||"max").toLowerCase();B&&(u=g.getParserById("no-parser"));s||(s=!1);if(!u)a:{h=b;m=l;B=-1;u=k;for(var A=void 0, K=g.parsers.length,G=!1,z="",A=!0;""===z&&A;)B++,m[B]?(G=m[B].cells[u],z=r(h,G,u),h.config.debug&&d("Checking if value was empty on row "+B+", column: "+u+': "'+z+'"')):A=!1;for(;0<=--K;)if((A=g.parsers[K])&&"text"!==A.id&&A.is&&A.is(z,h,G)){u=A;break a}u=g.getParserById("text")}f.debug&&(v+="column:"+k+"; extractor:"+s.id+"; parser:"+u.id+"; string:"+f.strings[k]+"; empty: "+f.empties[k]+"\n");c[k]=u;a[k]=s}p+=c.length?w:1}f.debug&&(d(v?v:"No parsers detected"),q("Completed detecting parsers",t)); f.parsers=c;f.extractors=a}function w(b){var a,c,f,e,l,x,k,n,m,p,u,s=b.config,t=s.$table.children("tbody"),v=s.extractors,w=s.parsers;s.cache={};s.totalRows=0;if(!w)return s.debug?d("Warning: *Empty table!* Not building a cache"):"";s.debug&&(n=new Date);s.showProcessing&&g.isProcessing(b,!0);for(l=0;l<t.length;l++)if(u=[],a=s.cache[l]={normalized:[]},!t.eq(l).hasClass(s.cssInfoBlock)){m=t[l]&&t[l].rows.length||0;for(f=0;f<m;++f)if(p={child:[]},x=h(t[l].rows[f]),k=[],x.hasClass(s.cssChildRow)&&0!== f)c=a.normalized.length-1,a.normalized[c][s.columns].$row=a.normalized[c][s.columns].$row.add(x),x.prev().hasClass(s.cssChildRow)||x.prev().addClass(g.css.cssHasChild),p.child[c]=h.trim(x[0].textContent||x[0].innerText||x.text()||"");else{p.$row=x;p.order=f;for(e=0;e<s.columns;++e)"undefined"===typeof w[e]?s.debug&&d("No parser found for cell:",x[0].cells[e],"does it have a header?"):(c=r(b,x[0].cells[e],e),c="undefined"===typeof v[e].id?c:v[e].format(c,b,x[0].cells[e],e),c="no-parser"===w[e].id? "":w[e].format(c,b,x[0].cells[e],e),k.push(s.ignoreCase&&"string"===typeof c?c.toLowerCase():c),"numeric"===(w[e].type||"").toLowerCase()&&(u[e]=Math.max(Math.abs(c)||0,u[e]||0)));k[s.columns]=p;a.normalized.push(k)}a.colMax=u;s.totalRows+=a.normalized.length}s.showProcessing&&g.isProcessing(b);s.debug&&q("Building cache for "+m+" rows",n)}function z(b,a){var c=b.config,f=c.widgetOptions,e=b.tBodies,l=[],d=c.cache,k,n,m,r,u,s;if(p(d))return c.appender?c.appender(b,l):b.isUpdating?c.$table.trigger("updateComplete", b):"";c.debug&&(s=new Date);for(u=0;u<e.length;u++)if(k=h(e[u]),k.length&&!k.hasClass(c.cssInfoBlock)){m=g.processTbody(b,k,!0);k=d[u].normalized;n=k.length;for(r=0;r<n;r++)l.push(k[r][c.columns].$row),c.appender&&(!c.pager||c.pager.removeRows&&f.pager_removeRows||c.pager.ajax)||m.append(k[r][c.columns].$row);g.processTbody(b,m,!1)}c.appender&&c.appender(b,l);c.debug&&q("Rebuilt table",s);a||c.appender||g.applyWidget(b);b.isUpdating&&c.$table.trigger("updateComplete",b)}function D(b){return/^d/i.test(b)|| 1===b}function E(b){var a,c,f,e,l,x,k,n=b.config;n.headerList=[];n.headerContent=[];n.debug&&(k=new Date);n.columns=g.computeColumnIndex(n.$table.children("thead, tfoot").children("tr"));e=n.cssIcon?'<i class="'+(n.cssIcon===g.css.icon?g.css.icon:n.cssIcon+" "+g.css.icon)+'"></i>':"";n.$headers=h(b).find(n.selectorHeaders).each(function(k){c=h(this);a=g.getColumnData(b,n.headers,k,!0);n.headerContent[k]=h(this).html();""!==n.headerTemplate&&(l=n.headerTemplate.replace(/\{content\}/g,h(this).html()).replace(/\{icon\}/g, e),n.onRenderTemplate&&(f=n.onRenderTemplate.apply(c,[k,l]))&&"string"===typeof f&&(l=f),h(this).html('<div class="'+g.css.headerIn+'">'+l+"</div>"));n.onRenderHeader&&n.onRenderHeader.apply(c,[k]);this.column=parseInt(h(this).attr("data-column"),10);this.order=D(g.getData(c,a,"sortInitialOrder")||n.sortInitialOrder)?[1,0,2]:[0,1,2];this.count=-1;this.lockedOrder=!1;x=g.getData(c,a,"lockedOrder")||!1;"undefined"!==typeof x&&!1!==x&&(this.order=this.lockedOrder=D(x)?[1,1,1]:[0,0,0]);c.addClass(g.css.header+ " "+n.cssHeader);n.headerList[k]=this;c.parent().addClass(g.css.headerRow+" "+n.cssHeaderRow).attr("role","row");n.tabIndex&&c.attr("tabindex",0)}).attr({scope:"col",role:"columnheader"});H(b);n.debug&&(q("Built headers:",k),d(n.$headers))}function C(b,a,c){var f=b.config;f.$table.find(f.selectorRemove).remove();v(b);w(b);I(f.$table,a,c)}function H(b){var a,c,f,e=b.config;e.$headers.each(function(l,d){c=h(d);f=g.getColumnData(b,e.headers,l,!0);a="false"===g.getData(d,f,"sorter")||"false"===g.getData(d, f,"parser");d.sortDisabled=a;c[a?"addClass":"removeClass"]("sorter-false").attr("aria-disabled",""+a);b.id&&(a?c.removeAttr("aria-controls"):c.attr("aria-controls",b.id))})}function F(b){var a,c,f=b.config,e=f.sortList,l=e.length,d=g.css.sortNone+" "+f.cssNone,k=[g.css.sortAsc+" "+f.cssAsc,g.css.sortDesc+" "+f.cssDesc],n=["ascending","descending"],m=h(b).find("tfoot tr").children().add(f.$extraHeaders).removeClass(k.join(" "));f.$headers.removeClass(k.join(" ")).addClass(d).attr("aria-sort","none"); for(a=0;a<l;a++)if(2!==e[a][1]&&(b=f.$headers.not(".sorter-false").filter('[data-column="'+e[a][0]+'"]'+(1===l?":last":"")),b.length)){for(c=0;c<b.length;c++)b[c].sortDisabled||b.eq(c).removeClass(d).addClass(k[e[a][1]]).attr("aria-sort",n[e[a][1]]);m.length&&m.filter('[data-column="'+e[a][0]+'"]').removeClass(d).addClass(k[e[a][1]])}f.$headers.not(".sorter-false").each(function(){var b=h(this),a=this.order[(this.count+1)%(f.sortReset?3:2)],a=b.text()+": "+g.language[b.hasClass(g.css.sortAsc)?"sortAsc": b.hasClass(g.css.sortDesc)?"sortDesc":"sortNone"]+g.language[0===a?"nextAsc":1===a?"nextDesc":"nextNone"];b.attr("aria-label",a)})}function O(b){var a,c,f=b.config;f.widthFixed&&0===f.$table.find("colgroup").length&&(a=h("<colgroup>"),c=h(b).width(),h(b.tBodies).not("."+f.cssInfoBlock).find("tr:first").children(":visible").each(function(){a.append(h("<col>").css("width",parseInt(h(this).width()/c*1E3,10)/10+"%"))}),f.$table.prepend(a))}function P(b,a){var c,f,e,l,g,k=b.config,d=a||k.sortList;k.sortList= [];h.each(d,function(b,a){l=parseInt(a[0],10);if(e=k.$headers.filter('[data-column="'+l+'"]:last')[0]){f=(f=(""+a[1]).match(/^(1|d|s|o|n)/))?f[0]:"";switch(f){case "1":case "d":f=1;break;case "s":f=g||0;break;case "o":c=e.order[(g||0)%(k.sortReset?3:2)];f=0===c?1:1===c?0:2;break;case "n":e.count+=1;f=e.order[e.count%(k.sortReset?3:2)];break;default:f=0}g=0===b?f:g;c=[l,parseInt(f,10)||0];k.sortList.push(c);f=h.inArray(c[1],e.order);e.count=0<=f?f:c[1]%(k.sortReset?3:2)}})}function Q(b,a){return b&& b[a]?b[a].type||"":""}function L(b,a,c){if(b.isUpdating)return setTimeout(function(){L(b,a,c)},50);var f,e,l,d,k=b.config,n=!c[k.sortMultiSortKey],m=k.$table;m.trigger("sortStart",b);a.count=c[k.sortResetKey]?2:(a.count+1)%(k.sortReset?3:2);k.sortRestart&&(e=a,k.$headers.each(function(){this===e||!n&&h(this).is("."+g.css.sortDesc+",."+g.css.sortAsc)||(this.count=-1)}));e=a.column;if(n){k.sortList=[];if(null!==k.sortForce)for(f=k.sortForce,l=0;l<f.length;l++)f[l][0]!==e&&k.sortList.push(f[l]);f=a.order[a.count]; if(2>f&&(k.sortList.push([e,f]),1<a.colSpan))for(l=1;l<a.colSpan;l++)k.sortList.push([e+l,f])}else{if(k.sortAppend&&1<k.sortList.length)for(l=0;l<k.sortAppend.length;l++)d=g.isValueInArray(k.sortAppend[l][0],k.sortList),0<=d&&k.sortList.splice(d,1);if(0<=g.isValueInArray(e,k.sortList))for(l=0;l<k.sortList.length;l++)d=k.sortList[l],f=k.$headers.filter('[data-column="'+d[0]+'"]:last')[0],d[0]===e&&(d[1]=f.order[a.count],2===d[1]&&(k.sortList.splice(l,1),f.count=-1));else if(f=a.order[a.count],2>f&& (k.sortList.push([e,f]),1<a.colSpan))for(l=1;l<a.colSpan;l++)k.sortList.push([e+l,f])}if(null!==k.sortAppend)for(f=k.sortAppend,l=0;l<f.length;l++)f[l][0]!==e&&k.sortList.push(f[l]);m.trigger("sortBegin",b);setTimeout(function(){F(b);J(b);z(b);m.trigger("sortEnd",b)},1)}function J(b){var a,c,f,e,l,d,k,h,m,r,u,s=0,t=b.config,v=t.textSorter||"",w=t.sortList,y=w.length,z=b.tBodies.length;if(!t.serverSideSorting&&!p(t.cache)){t.debug&&(l=new Date);for(c=0;c<z;c++)d=t.cache[c].colMax,k=t.cache[c].normalized, k.sort(function(c,l){for(a=0;a<y;a++){e=w[a][0];h=w[a][1];s=0===h;if(t.sortStable&&c[e]===l[e]&&1===y)break;(f=/n/i.test(Q(t.parsers,e)))&&t.strings[e]?(f="boolean"===typeof t.string[t.strings[e]]?(s?1:-1)*(t.string[t.strings[e]]?-1:1):t.strings[e]?t.string[t.strings[e]]||0:0,m=t.numberSorter?t.numberSorter(c[e],l[e],s,d[e],b):g["sortNumeric"+(s?"Asc":"Desc")](c[e],l[e],f,d[e],e,b)):(r=s?c:l,u=s?l:c,m="function"===typeof v?v(r[e],u[e],s,e,b):"object"===typeof v&&v.hasOwnProperty(e)?v[e](r[e],u[e], s,e,b):g["sortNatural"+(s?"Asc":"Desc")](c[e],l[e],e,b,t));if(m)return m}return c[t.columns].order-l[t.columns].order});t.debug&&q("Sorting on "+w.toString()+" and dir "+h+" time",l)}}function M(b,a){var c=b[0];c.isUpdating&&b.trigger("updateComplete",c);h.isFunction(a)&&a(b[0])}function I(b,a,c){var f=b[0].config.sortList;!1!==a&&!b[0].isProcessing&&f.length?b.trigger("sorton",[f,function(){M(b,c)},!0]):(M(b,c),g.applyWidget(b[0],!1))}function N(b){var a=b.config,c=a.$table;c.unbind("sortReset update updateRows updateCell updateAll addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave ".split(" ").join(a.namespace+ " ")).bind("sortReset"+a.namespace,function(f,e){f.stopPropagation();a.sortList=[];F(b);J(b);z(b);h.isFunction(e)&&e(b)}).bind("updateAll"+a.namespace,function(f,e,c){f.stopPropagation();b.isUpdating=!0;g.refreshWidgets(b,!0,!0);g.restoreHeaders(b);E(b);g.bindEvents(b,a.$headers,!0);N(b);C(b,e,c)}).bind("update"+a.namespace+" updateRows"+a.namespace,function(a,e,c){a.stopPropagation();b.isUpdating=!0;H(b);C(b,e,c)}).bind("updateCell"+a.namespace,function(f,e,l,g){f.stopPropagation();b.isUpdating= !0;c.find(a.selectorRemove).remove();var d,n,m;n=c.find("tbody");m=h(e);f=n.index(h.fn.closest?m.closest("tbody"):m.parents("tbody").filter(":first"));d=h.fn.closest?m.closest("tr"):m.parents("tr").filter(":first");e=m[0];n.length&&0<=f&&(n=n.eq(f).find("tr").index(d),m=m.index(),a.cache[f].normalized[n][a.columns].$row=d,d="undefined"===typeof a.extractors[m].id?r(b,e,m):a.extractors[m].format(r(b,e,m),b,e,m),e="no-parser"===a.parsers[m].id?"":a.parsers[m].format(d,b,e,m),a.cache[f].normalized[n][m]= a.ignoreCase&&"string"===typeof e?e.toLowerCase():e,"numeric"===(a.parsers[m].type||"").toLowerCase()&&(a.cache[f].colMax[m]=Math.max(Math.abs(e)||0,a.cache[f].colMax[m]||0)),I(c,l,g))}).bind("addRows"+a.namespace,function(f,e,l,g){f.stopPropagation();b.isUpdating=!0;if(p(a.cache))H(b),C(b,l,g);else{e=h(e).attr("role","row");var d,n,m,q,u,s=e.filter("tr").length,t=c.find("tbody").index(e.parents("tbody").filter(":first"));a.parsers&&a.parsers.length||v(b);for(f=0;f<s;f++){n=e[f].cells.length;u=[]; q={child:[],$row:e.eq(f),order:a.cache[t].normalized.length};for(d=0;d<n;d++)m="undefined"===typeof a.extractors[d].id?r(b,e[f].cells[d],d):a.extractors[d].format(r(b,e[f].cells[d],d),b,e[f].cells[d],d),m="no-parser"===a.parsers[d].id?"":a.parsers[d].format(m,b,e[f].cells[d],d),u[d]=a.ignoreCase&&"string"===typeof m?m.toLowerCase():m,"numeric"===(a.parsers[d].type||"").toLowerCase()&&(a.cache[t].colMax[d]=Math.max(Math.abs(u[d])||0,a.cache[t].colMax[d]||0));u.push(q);a.cache[t].normalized.push(u)}I(c, l,g)}}).bind("updateComplete"+a.namespace,function(){b.isUpdating=!1}).bind("sorton"+a.namespace,function(a,e,d,x){var k=b.config;a.stopPropagation();c.trigger("sortStart",this);P(b,e);F(b);k.delayInit&&p(k.cache)&&w(b);c.trigger("sortBegin",this);J(b);z(b,x);c.trigger("sortEnd",this);g.applyWidget(b);h.isFunction(d)&&d(b)}).bind("appendCache"+a.namespace,function(a,e,c){a.stopPropagation();z(b,c);h.isFunction(e)&&e(b)}).bind("updateCache"+a.namespace,function(c,e){a.parsers&&a.parsers.length||v(b); w(b);h.isFunction(e)&&e(b)}).bind("applyWidgetId"+a.namespace,function(c,e){c.stopPropagation();g.getWidgetById(e).format(b,a,a.widgetOptions)}).bind("applyWidgets"+a.namespace,function(a,c){a.stopPropagation();g.applyWidget(b,c)}).bind("refreshWidgets"+a.namespace,function(a,c,d){a.stopPropagation();g.refreshWidgets(b,c,d)}).bind("destroy"+a.namespace,function(a,c,d){a.stopPropagation();g.destroy(b,c,d)}).bind("resetToLoadState"+a.namespace,function(){g.refreshWidgets(b,!0,!0);a=h.extend(!0,g.defaults, a.originalSettings);b.hasInitialized=!1;g.setup(b,a)})}var g=this;g.version="2.17.8";g.parsers=[];g.widgets=[];g.defaults={theme:"default",widthFixed:!1,showProcessing:!1,headerTemplate:"{content}",onRenderTemplate:null,onRenderHeader:null,cancelSelection:!0,tabIndex:!0,dateFormat:"ddmmyyyy",sortMultiSortKey:"shiftKey",sortResetKey:"ctrlKey",usNumberFormat:!0,delayInit:!1,serverSideSorting:!1,headers:{},ignoreCase:!0,sortForce:null,sortList:[],sortAppend:null,sortStable:!1,sortInitialOrder:"asc", sortLocaleCompare:!1,sortReset:!1,sortRestart:!1,emptyTo:"bottom",stringTo:"max",textExtraction:"basic",textAttribute:"data-text",textSorter:null,numberSorter:null,widgets:[],widgetOptions:{zebra:["even","odd"]},initWidgets:!0,initialized:null,tableClass:"",cssAsc:"",cssDesc:"",cssNone:"",cssHeader:"",cssHeaderRow:"",cssProcessing:"",cssChildRow:"tablesorter-childRow",cssIcon:"tablesorter-icon",cssInfoBlock:"tablesorter-infoOnly",selectorHeaders:"> thead th, > thead td",selectorSort:"th, td",selectorRemove:".remove-me", debug:!1,headerList:[],empties:{},strings:{},parsers:[]};g.css={table:"tablesorter",cssHasChild:"tablesorter-hasChildRow",childRow:"tablesorter-childRow",header:"tablesorter-header",headerRow:"tablesorter-headerRow",headerIn:"tablesorter-header-inner",icon:"tablesorter-icon",info:"tablesorter-infoOnly",processing:"tablesorter-processing",sortAsc:"tablesorter-headerAsc",sortDesc:"tablesorter-headerDesc",sortNone:"tablesorter-headerUnSorted"};g.language={sortAsc:"Ascending sort applied, ",sortDesc:"Descending sort applied, ", sortNone:"No sort applied, ",nextAsc:"activate to apply an ascending sort",nextDesc:"activate to apply a descending sort",nextNone:"activate to remove the sort"};g.log=d;g.benchmark=q;g.construct=function(b){return this.each(function(){var a=h.extend(!0,{},g.defaults,b);a.originalSettings=b;!this.hasInitialized&&g.buildTable&&"TABLE"!==this.tagName?g.buildTable(this,a):g.setup(this,a)})};g.setup=function(b,a){if(!b||!b.tHead||0===b.tBodies.length||!0===b.hasInitialized)return a.debug?d("ERROR: stopping initialization! No table, thead, tbody or tablesorter has already been initialized"): "";var c="",f=h(b),e=h.metadata;b.hasInitialized=!1;b.isProcessing=!0;b.config=a;h.data(b,"tablesorter",a);a.debug&&h.data(b,"startoveralltimer",new Date);a.supportsDataObject=function(a){a[0]=parseInt(a[0],10);return 1<a[0]||1===a[0]&&4<=parseInt(a[1],10)}(h.fn.jquery.split("."));a.string={max:1,min:-1,emptymin:1,emptymax:-1,zero:0,none:0,"null":0,top:!0,bottom:!1};a.emptyTo=a.emptyTo.toLowerCase();a.stringTo=a.stringTo.toLowerCase();/tablesorter\-/.test(f.attr("class"))||(c=""!==a.theme?" tablesorter-"+ a.theme:"");a.table=b;a.$table=f.addClass(g.css.table+" "+a.tableClass+c).attr("role","grid");a.$headers=f.find(a.selectorHeaders);a.namespace=a.namespace?"."+a.namespace.replace(/\W/g,""):".tablesorter"+Math.random().toString(16).slice(2);a.$table.children().children("tr").attr("role","row");a.$tbodies=f.children("tbody:not(."+a.cssInfoBlock+")").attr({"aria-live":"polite","aria-relevant":"all"});a.$table.find("caption").length&&a.$table.attr("aria-labelledby","theCaption");a.widgetInit={};a.textExtraction= a.$table.attr("data-text-extraction")||a.textExtraction||"basic";E(b);O(b);v(b);a.totalRows=0;a.delayInit||w(b);g.bindEvents(b,a.$headers,!0);N(b);a.supportsDataObject&&"undefined"!==typeof f.data().sortlist?a.sortList=f.data().sortlist:e&&f.metadata()&&f.metadata().sortlist&&(a.sortList=f.metadata().sortlist);g.applyWidget(b,!0);0<a.sortList.length?f.trigger("sorton",[a.sortList,{},!a.initWidgets,!0]):(F(b),a.initWidgets&&g.applyWidget(b,!1));a.showProcessing&&f.unbind("sortBegin"+a.namespace+" sortEnd"+ a.namespace).bind("sortBegin"+a.namespace+" sortEnd"+a.namespace,function(c){clearTimeout(a.processTimer);g.isProcessing(b);"sortBegin"===c.type&&(a.processTimer=setTimeout(function(){g.isProcessing(b,!0)},500))});b.hasInitialized=!0;b.isProcessing=!1;a.debug&&g.benchmark("Overall initialization time",h.data(b,"startoveralltimer"));f.trigger("tablesorter-initialized",b);"function"===typeof a.initialized&&a.initialized(b)};g.getColumnData=function(b,a,c,f){if("undefined"!==typeof a&&null!==a){b=h(b)[0]; var e,d=b.config;if(a[c])return f?a[c]:a[d.$headers.index(d.$headers.filter('[data-column="'+c+'"]:last'))];for(e in a)if("string"===typeof e&&(b=f?d.$headers.eq(c).filter(e):d.$headers.filter('[data-column="'+c+'"]:last').filter(e),b.length))return a[e]}};g.computeColumnIndex=function(b){var a=[],c=0,f,e,d,g,k,n,m,p,q,s;for(f=0;f<b.length;f++)for(k=b[f].cells,e=0;e<k.length;e++){d=k[e];g=h(d);n=d.parentNode.rowIndex;g.index();m=d.rowSpan||1;p=d.colSpan||1;"undefined"===typeof a[n]&&(a[n]=[]);for(d= 0;d<a[n].length+1;d++)if("undefined"===typeof a[n][d]){q=d;break}c=Math.max(q,c);g.attr({"data-column":q});for(d=n;d<n+m;d++)for("undefined"===typeof a[d]&&(a[d]=[]),s=a[d],g=q;g<q+p;g++)s[g]="x"}return c+1};g.isProcessing=function(b,a,c){b=h(b);var f=b[0].config,e=c||b.find("."+g.css.header);a?("undefined"!==typeof c&&0<f.sortList.length&&(e=e.filter(function(){return this.sortDisabled?!1:0<=g.isValueInArray(parseFloat(h(this).attr("data-column")),f.sortList)})),b.add(e).addClass(g.css.processing+ " "+f.cssProcessing)):b.add(e).removeClass(g.css.processing+" "+f.cssProcessing)};g.processTbody=function(b,a,c){b=h(b)[0];if(c)return b.isProcessing=!0,a.before('<span class="tablesorter-savemyplace"/>'),c=h.fn.detach?a.detach():a.remove();c=h(b).find("span.tablesorter-savemyplace");a.insertAfter(c);c.remove();b.isProcessing=!1};g.clearTableBody=function(b){h(b)[0].config.$tbodies.children().detach()};g.bindEvents=function(b,a,c){b=h(b)[0];var f,e=b.config;!0!==c&&(e.$extraHeaders=e.$extraHeaders? e.$extraHeaders.add(a):a);a.find(e.selectorSort).add(a.filter(e.selectorSort)).unbind(["mousedown","mouseup","sort","keyup",""].join(e.namespace+" ")).bind(["mousedown","mouseup","sort","keyup",""].join(e.namespace+" "),function(c,d){var g;g=c.type;if(!(1!==(c.which||c.button)&&!/sort|keyup/.test(g)||"keyup"===g&&13!==c.which||"mouseup"===g&&!0!==d&&250<(new Date).getTime()-f)){if("mousedown"===g)return f=(new Date).getTime(),/(input|select|button|textarea)/i.test(c.target.tagName)?"":!e.cancelSelection; e.delayInit&&p(e.cache)&&w(b);g=h.fn.closest?h(this).closest("th, td")[0]:/TH|TD/.test(this.tagName)?this:h(this).parents("th, td")[0];g=e.$headers[a.index(g)];g.sortDisabled||L(b,g,c)}});e.cancelSelection&&a.attr("unselectable","on").bind("selectstart",!1).css({"user-select":"none",MozUserSelect:"none"})};g.restoreHeaders=function(b){var a=h(b)[0].config;a.$table.find(a.selectorHeaders).each(function(b){h(this).find("."+g.css.headerIn).length&&h(this).html(a.headerContent[b])})};g.destroy=function(b, a,c){b=h(b)[0];if(b.hasInitialized){g.refreshWidgets(b,!0,!0);var f=h(b),e=b.config,d=f.find("thead:first"),q=d.find("tr."+g.css.headerRow).removeClass(g.css.headerRow+" "+e.cssHeaderRow),k=f.find("tfoot:first > tr").children("th, td");!1===a&&0<=h.inArray("uitheme",e.widgets)&&(f.trigger("applyWidgetId",["uitheme"]),f.trigger("applyWidgetId",["zebra"]));d.find("tr").not(q).remove();f.removeData("tablesorter").unbind("sortReset update updateAll updateRows updateCell addRows updateComplete sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave keypress sortBegin sortEnd resetToLoadState ".split(" ").join(e.namespace+ " "));e.$headers.add(k).removeClass([g.css.header,e.cssHeader,e.cssAsc,e.cssDesc,g.css.sortAsc,g.css.sortDesc,g.css.sortNone].join(" ")).removeAttr("data-column").removeAttr("aria-label").attr("aria-disabled","true");q.find(e.selectorSort).unbind(["mousedown","mouseup","keypress",""].join(e.namespace+" "));g.restoreHeaders(b);f.toggleClass(g.css.table+" "+e.tableClass+" tablesorter-"+e.theme,!1===a);b.hasInitialized=!1;delete b.config.cache;"function"===typeof c&&c(b)}};g.regex={chunk:/(^([+\-]?(?:0|[1-9]\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, chunks:/(^\\0|\\0$)/,hex:/^0x[0-9a-f]+$/i};g.sortNatural=function(b,a){if(b===a)return 0;var c,f,e,d,h,k;f=g.regex;if(f.hex.test(a)){c=parseInt(b.match(f.hex),16);e=parseInt(a.match(f.hex),16);if(c<e)return-1;if(c>e)return 1}c=b.replace(f.chunk,"\\0$1\\0").replace(f.chunks,"").split("\\0");f=a.replace(f.chunk,"\\0$1\\0").replace(f.chunks,"").split("\\0");k=Math.max(c.length,f.length);for(h=0;h<k;h++){e=isNaN(c[h])?c[h]||0:parseFloat(c[h])||0;d=isNaN(f[h])?f[h]||0:parseFloat(f[h])||0;if(isNaN(e)!== isNaN(d))return isNaN(e)?1:-1;typeof e!==typeof d&&(e+="",d+="");if(e<d)return-1;if(e>d)return 1}return 0};g.sortNaturalAsc=function(b,a,c,f,e){if(b===a)return 0;c=e.string[e.empties[c]||e.emptyTo];return""===b&&0!==c?"boolean"===typeof c?c?-1:1:-c||-1:""===a&&0!==c?"boolean"===typeof c?c?1:-1:c||1:g.sortNatural(b,a)};g.sortNaturalDesc=function(b,a,c,f,e){if(b===a)return 0;c=e.string[e.empties[c]||e.emptyTo];return""===b&&0!==c?"boolean"===typeof c?c?-1:1:c||1:""===a&&0!==c?"boolean"===typeof c?c? 1:-1:-c||-1:g.sortNatural(a,b)};g.sortText=function(b,a){return b>a?1:b<a?-1:0};g.getTextValue=function(b,a,c){if(c){var f=b?b.length:0,e=c+a;for(c=0;c<f;c++)e+=b.charCodeAt(c);return a*e}return 0};g.sortNumericAsc=function(b,a,c,f,e,d){if(b===a)return 0;d=d.config;e=d.string[d.empties[e]||d.emptyTo];if(""===b&&0!==e)return"boolean"===typeof e?e?-1:1:-e||-1;if(""===a&&0!==e)return"boolean"===typeof e?e?1:-1:e||1;isNaN(b)&&(b=g.getTextValue(b,c,f));isNaN(a)&&(a=g.getTextValue(a,c,f));return b-a};g.sortNumericDesc= function(b,a,c,f,e,d){if(b===a)return 0;d=d.config;e=d.string[d.empties[e]||d.emptyTo];if(""===b&&0!==e)return"boolean"===typeof e?e?-1:1:e||1;if(""===a&&0!==e)return"boolean"===typeof e?e?1:-1:-e||-1;isNaN(b)&&(b=g.getTextValue(b,c,f));isNaN(a)&&(a=g.getTextValue(a,c,f));return a-b};g.sortNumeric=function(b,a){return b-a};g.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",SS:"\u1e9e",u:"\u00fa\u00f9\u00fb\u00fc\u016f",U:"\u00da\u00d9\u00db\u00dc\u016e"};g.replaceAccents=function(b){var a,c="[",d=g.characterEquivalents;if(!g.characterRegex){g.characterRegexArray={};for(a in d)"string"===typeof a&&(c+=d[a],g.characterRegexArray[a]=new RegExp("["+d[a]+"]","g"));g.characterRegex= new RegExp(c+"]")}if(g.characterRegex.test(b))for(a in d)"string"===typeof a&&(b=b.replace(g.characterRegexArray[a],a));return b};g.isValueInArray=function(b,a){var c,d=a.length;for(c=0;c<d;c++)if(a[c][0]===b)return c;return-1};g.addParser=function(b){var a,c=g.parsers.length,d=!0;for(a=0;a<c;a++)g.parsers[a].id.toLowerCase()===b.id.toLowerCase()&&(d=!1);d&&g.parsers.push(b)};g.getParserById=function(b){if("false"==b)return!1;var a,c=g.parsers.length;for(a=0;a<c;a++)if(g.parsers[a].id.toLowerCase()=== b.toString().toLowerCase())return g.parsers[a];return!1};g.addWidget=function(b){g.widgets.push(b)};g.hasWidget=function(b,a){b=h(b);return b.length&&b[0].config&&b[0].config.widgetInit[a]||!1};g.getWidgetById=function(b){var a,c,d=g.widgets.length;for(a=0;a<d;a++)if((c=g.widgets[a])&&c.hasOwnProperty("id")&&c.id.toLowerCase()===b.toLowerCase())return c};g.applyWidget=function(b,a){b=h(b)[0];var c=b.config,d=c.widgetOptions,e=[],l,p,k;!1!==a&&b.hasInitialized&&(b.isApplyingWidgets||b.isUpdating)|| (c.debug&&(l=new Date),c.widgets.length&&(b.isApplyingWidgets=!0,c.widgets=h.grep(c.widgets,function(a,b){return h.inArray(a,c.widgets)===b}),h.each(c.widgets||[],function(a,b){(k=g.getWidgetById(b))&&k.id&&(k.priority||(k.priority=10),e[a]=k)}),e.sort(function(a,b){return a.priority<b.priority?-1:a.priority===b.priority?0:1}),h.each(e,function(e,g){if(g){if(a||!c.widgetInit[g.id])c.widgetInit[g.id]=!0,g.hasOwnProperty("options")&&(d=b.config.widgetOptions=h.extend(!0,{},g.options,d)),g.hasOwnProperty("init")&& g.init(b,g,c,d);!a&&g.hasOwnProperty("format")&&g.format(b,c,d,!1)}})),setTimeout(function(){b.isApplyingWidgets=!1},0),c.debug&&(p=c.widgets.length,q("Completed "+(!0===a?"initializing ":"applying ")+p+" widget"+(1!==p?"s":""),l)))};g.refreshWidgets=function(b,a,c){b=h(b)[0];var f,e=b.config,l=e.widgets,q=g.widgets,k=q.length;for(f=0;f<k;f++)q[f]&&q[f].id&&(a||0>h.inArray(q[f].id,l))&&(e.debug&&d('Refeshing widgets: Removing "'+q[f].id+'"'),q[f].hasOwnProperty("remove")&&e.widgetInit[q[f].id]&&(q[f].remove(b, e,e.widgetOptions),e.widgetInit[q[f].id]=!1));!0!==c&&g.applyWidget(b,a)};g.getData=function(b,a,c){var d="";b=h(b);var e,g;if(!b.length)return"";e=h.metadata?b.metadata():!1;g=" "+(b.attr("class")||"");"undefined"!==typeof b.data(c)||"undefined"!==typeof b.data(c.toLowerCase())?d+=b.data(c)||b.data(c.toLowerCase()):e&&"undefined"!==typeof e[c]?d+=e[c]:a&&"undefined"!==typeof a[c]?d+=a[c]:" "!==g&&g.match(" "+c+"-")&&(d=g.match(new RegExp("\\s"+c+"-([\\w-]+)"))[1]||"");return h.trim(d)};g.formatFloat= function(b,a){if("string"!==typeof b||""===b)return b;var c;b=(a&&a.config?!1!==a.config.usNumberFormat:"undefined"!==typeof a?a:1)?b.replace(/,/g,""):b.replace(/[\s|\.]/g,"").replace(/,/g,".");/^\s*\([.\d]+\)/.test(b)&&(b=b.replace(/^\s*\(([.\d]+)\)/,"-$1"));c=parseFloat(b);return isNaN(c)?h.trim(b):c};g.isDigit=function(b){return isNaN(b)?/^[\-+(]?\d+[)]?$/.test(b.toString().replace(/[,.'"\s]/g,"")):!0}}});var r=h.tablesorter;h.fn.extend({tablesorter:r.construct});r.addParser({id:"no-parser",is:function(){return!1}, format:function(){return""},type:"text"});r.addParser({id:"text",is:function(){return!0},format:function(d,q){var p=q.config;d&&(d=h.trim(p.ignoreCase?d.toLocaleLowerCase():d),d=p.sortLocaleCompare?r.replaceAccents(d):d);return d},type:"text"});r.addParser({id:"digit",is:function(d){return r.isDigit(d)},format:function(d,q){var p=r.formatFloat((d||"").replace(/[^\w,. \-()]/g,""),q);return d&&"number"===typeof p?p:d?h.trim(d&&q.config.ignoreCase?d.toLocaleLowerCase():d):d},type:"numeric"});r.addParser({id:"currency", is:function(d){return/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/.test((d||"").replace(/[+\-,. ]/g,""))},format:function(d,q){var p=r.formatFloat((d||"").replace(/[^\w,. \-()]/g,""),q);return d&&"number"===typeof p?p:d?h.trim(d&&q.config.ignoreCase?d.toLocaleLowerCase():d):d},type:"numeric"});r.addParser({id:"ipAddress",is:function(d){return/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/.test(d)},format:function(d,h){var p,y=d?d.split("."):"",v="",w=y.length; for(p=0;p<w;p++)v+=("00"+y[p]).slice(-3);return d?r.formatFloat(v,h):d},type:"numeric"});r.addParser({id:"url",is:function(d){return/^(https?|ftp|file):\/\//.test(d)},format:function(d){return d?h.trim(d.replace(/(https?|ftp|file):\/\//,"")):d},parsed:!0,type:"text"});r.addParser({id:"isoDate",is:function(d){return/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/.test(d)},format:function(d,h){return d?r.formatFloat(""!==d?(new Date(d.replace(/-/g,"/"))).getTime()||d:"",h):d},type:"numeric"});r.addParser({id:"percent", is:function(d){return/(\d\s*?%|%\s*?\d)/.test(d)&&15>d.length},format:function(d,h){return d?r.formatFloat(d.replace(/%/g,""),h):d},type:"numeric"});r.addParser({id:"usLongDate",is:function(d){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(d)||/^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i.test(d)},format:function(d,h){return d?r.formatFloat((new Date(d.replace(/(\S)([AP]M)$/i,"$1 $2"))).getTime()||d,h):d},type:"numeric"});r.addParser({id:"shortDate",is:function(d){return/(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/.test((d|| "").replace(/\s+/g," ").replace(/[\-.,]/g,"/"))},format:function(d,h,p,y){if(d){p=h.config;var v=p.$headers.filter("[data-column="+y+"]:last");y=v.length&&v[0].dateFormat||r.getData(v,r.getColumnData(h,p.headers,y),"dateFormat")||p.dateFormat;d=d.replace(/\s+/g," ").replace(/[\-.,]/g,"/");"mmddyyyy"===y?d=d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/,"$3/$1/$2"):"ddmmyyyy"===y?d=d.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/,"$3/$2/$1"):"yyyymmdd"===y&&(d=d.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"))}return d?r.formatFloat((new Date(d)).getTime()||d,h):d},type:"numeric"});r.addParser({id:"time",is:function(d){return/^(([0-2]?\d:[0-5]\d)|([0-1]?\d:[0-5]\d\s?([AP]M)))$/i.test(d)},format:function(d,h){return d?r.formatFloat((new Date("2000/01/01 "+d.replace(/(\S)([AP]M)$/i,"$1 $2"))).getTime()||d,h):d},type:"numeric"});r.addParser({id:"metadata",is:function(){return!1},format:function(d,q,p){d=q.config;d=d.parserMetadataName?d.parserMetadataName:"sortValue";return h(p).metadata()[d]}, type:"numeric"});r.addWidget({id:"zebra",priority:90,format:function(d,q,p){var y,v,w,z,D,E=new RegExp(q.cssChildRow,"i"),C=q.$tbodies;q.debug&&(D=new Date);for(d=0;d<C.length;d++)w=0,y=C.eq(d),y=y.children("tr:visible").not(q.selectorRemove),y.each(function(){v=h(this);E.test(this.className)||w++;z=0===w%2;v.removeClass(p.zebra[z?1:0]).addClass(p.zebra[z?0:1])});q.debug&&r.benchmark("Applying Zebra widget",D)},remove:function(d,q,p){var r;q=q.$tbodies;var v=(p.zebra||["even","odd"]).join(" ");for(p= 0;p<q.length;p++)r=h.tablesorter.processTbody(d,q.eq(p),!0),r.children().removeClass(v),h.tablesorter.processTbody(d,r,!1)}})}(jQuery); diff --git a/js/upload.js b/js/upload.js new file mode 100644 index 0000000..ffcf296 --- /dev/null +++ b/js/upload.js @@ -0,0 +1,3345 @@ +/* + * jQuery File Upload Audio Preview Plugin 1.0.3 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadAudio', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableAudioPreview' + }, + { + action: 'setAudio', + name: '@audioPreviewName', + disabled: '@disableAudioPreview' + } + ); + + // The File Upload Audio Preview plugin extends the fileupload widget + // with audio preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of audio files to load, + // matched against the file type: + loadAudioFileTypes: /^audio\/.*$/ + }, + + _audioElement: document.createElement('audio'), + + processActions: { + + // Loads the audio file given via data.files and data.index + // as audio element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadAudio: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + audio; + if (this._audioElement.canPlayType && + this._audioElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + audio = this._audioElement.cloneNode(false); + audio.src = url; + audio.controls = true; + data.audio = audio; + return data; + } + } + return data; + }, + + // Sets the audio element as a property of the file object: + setAudio: function (data, options) { + if (data.audio && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.audio; + } + return data; + } + + } + + }); + +})); +/* + * jQuery File Upload Image Preview & Resize Plugin 1.7.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window, Blob */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + 'load-image-meta', + 'load-image-exif', + 'load-image-ios', + 'canvas-to-blob', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadImageMetaData', + disableImageHead: '@', + disableExif: '@', + disableExifThumbnail: '@', + disableExifSub: '@', + disableExifGps: '@', + disabled: '@disableImageMetaDataLoad' + }, + { + action: 'loadImage', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + noRevoke: '@', + disabled: '@disableImageLoad' + }, + { + action: 'resizeImage', + // Use "image" as prefix for the "@" options: + prefix: 'image', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + forceResize: '@', + disabled: '@disableImageResize' + }, + { + action: 'saveImage', + quality: '@imageQuality', + type: '@imageType', + disabled: '@disableImageResize' + }, + { + action: 'saveImageMetaData', + disabled: '@disableImageMetaDataSave' + }, + { + action: 'resizeImage', + // Use "preview" as prefix for the "@" options: + prefix: 'preview', + maxWidth: '@', + maxHeight: '@', + minWidth: '@', + minHeight: '@', + crop: '@', + orientation: '@', + thumbnail: '@', + canvas: '@', + disabled: '@disableImagePreview' + }, + { + action: 'setImage', + name: '@imagePreviewName', + disabled: '@disableImagePreview' + }, + { + action: 'deleteImageReferences', + disabled: '@disableImageReferencesDeletion' + } + ); + + // The File Upload Resize plugin extends the fileupload widget + // with image resize functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of images to load: + // matched against the file type: + loadImageFileTypes: /^image\/(gif|jpeg|png|svg\+xml)$/, + // The maximum file size of images to load: + loadImageMaxFileSize: 10000000, // 10MB + // The maximum width of resized images: + imageMaxWidth: 1920, + // The maximum height of resized images: + imageMaxHeight: 1080, + // Defines the image orientation (1-8) or takes the orientation + // value from Exif data if set to true: + imageOrientation: false, + // Define if resized images should be cropped or only scaled: + imageCrop: false, + // Disable the resize image functionality by default: + disableImageResize: true, + // The maximum width of the preview images: + previewMaxWidth: 80, + // The maximum height of the preview images: + previewMaxHeight: 80, + // Defines the preview orientation (1-8) or takes the orientation + // value from Exif data if set to true: + previewOrientation: true, + // Create the preview using the Exif data thumbnail: + previewThumbnail: true, + // Define if preview images should be cropped or only scaled: + previewCrop: false, + // Define if preview images should be resized as canvas elements: + previewCanvas: true + }, + + processActions: { + + // Loads the image given via data.files and data.index + // as img element, if the browser supports the File API. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadImage: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (($.type(options.maxFileSize) === 'number' && + file.size > options.maxFileSize) || + (options.fileTypes && + !options.fileTypes.test(file.type)) || + !loadImage( + file, + function (img) { + if (img.src) { + data.img = img; + } + dfd.resolveWith(that, [data]); + }, + options + )) { + return data; + } + return dfd.promise(); + }, + + // Resizes the image given as data.canvas or data.img + // and updates data.canvas or data.img with the resized image. + // Also stores the resized image as preview property. + // Accepts the options maxWidth, maxHeight, minWidth, + // minHeight, canvas and crop: + resizeImage: function (data, options) { + if (options.disabled || !(data.canvas || data.img)) { + return data; + } + options = $.extend({canvas: true}, options); + var that = this, + dfd = $.Deferred(), + img = (options.canvas && data.canvas) || data.img, + resolve = function (newImg) { + if (newImg && (newImg.width !== img.width || + newImg.height !== img.height || + options.forceResize)) { + data[newImg.getContext ? 'canvas' : 'img'] = newImg; + } + data.preview = newImg; + dfd.resolveWith(that, [data]); + }, + thumbnail; + if (data.exif) { + if (options.orientation === true) { + options.orientation = data.exif.get('Orientation'); + } + if (options.thumbnail) { + thumbnail = data.exif.get('Thumbnail'); + if (thumbnail) { + loadImage(thumbnail, resolve, options); + return dfd.promise(); + } + } + // Prevent orienting the same image twice: + if (data.orientation) { + delete options.orientation; + } else { + data.orientation = options.orientation; + } + } + if (img) { + resolve(loadImage.scale(img, options)); + return dfd.promise(); + } + return data; + }, + + // Saves the processed image given as data.canvas + // inplace at data.index of data.files: + saveImage: function (data, options) { + if (!data.canvas || options.disabled) { + return data; + } + var that = this, + file = data.files[data.index], + dfd = $.Deferred(); + if (data.canvas.toBlob) { + data.canvas.toBlob( + function (blob) { + if (!blob.name) { + if (file.type === blob.type) { + blob.name = file.name; + } else if (file.name) { + blob.name = file.name.replace( + /\..+$/, + '.' + blob.type.substr(6) + ); + } + } + // Don't restore invalid meta data: + if (file.type !== blob.type) { + delete data.imageHead; + } + // Store the created blob at the position + // of the original file in the files list: + data.files[data.index] = blob; + dfd.resolveWith(that, [data]); + }, + options.type || file.type, + options.quality + ); + } else { + return data; + } + return dfd.promise(); + }, + + loadImageMetaData: function (data, options) { + if (options.disabled) { + return data; + } + var that = this, + dfd = $.Deferred(); + loadImage.parseMetaData(data.files[data.index], function (result) { + $.extend(data, result); + dfd.resolveWith(that, [data]); + }, options); + return dfd.promise(); + }, + + saveImageMetaData: function (data, options) { + if (!(data.imageHead && data.canvas && + data.canvas.toBlob && !options.disabled)) { + return data; + } + var file = data.files[data.index], + blob = new Blob([ + data.imageHead, + // Resized images always have a head size of 20 bytes, + // including the JPEG marker and a minimal JFIF header: + this._blobSlice.call(file, 20) + ], {type: file.type}); + blob.name = file.name; + data.files[data.index] = blob; + return data; + }, + + // Sets the resized version of the image as a property of the + // file object, must be called after "saveImage": + setImage: function (data, options) { + if (data.preview && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.preview; + } + return data; + }, + + deleteImageReferences: function (data, options) { + if (!options.disabled) { + delete data.img; + delete data.canvas; + delete data.preview; + delete data.imageHead; + } + return data; + } + + } + + }); + +})); +/* + * jQuery File Upload jQuery UI Plugin 8.7.1 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery', './jquery.fileupload-ui'], factory); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + processdone: function (e, data) { + data.context.find('.start').button('enable'); + }, + progress: function (e, data) { + if (data.context) { + data.context.find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ); + } + }, + progressall: function (e, data) { + var $this = $(this); + $this.find('.fileupload-progress') + .find('.progress').progressbar( + 'option', + 'value', + parseInt(data.loaded / data.total * 100, 10) + ).end() + .find('.progress-extended').each(function () { + $(this).html( + ($this.data('blueimp-fileupload') || + $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + }); + } + }, + + _renderUpload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.progress').empty().progressbar(); + node.find('.start').button({ + icons: {primary: 'ui-icon-circle-arrow-e'}, + text: showIconText + }); + node.find('.cancel').button({ + icons: {primary: 'ui-icon-cancel'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _renderDownload: function (func, files) { + var node = this._super(func, files), + showIconText = $(window).width() > 480; + node.find('.delete').button({ + icons: {primary: 'ui-icon-trash'}, + text: showIconText + }); + if (node.hasClass('fade')) { + node.hide(); + } + return node; + }, + + _startHandler: function (e) { + $(e.currentTarget).button('disable'); + this._super(e); + }, + + _transition: function (node) { + var deferred = $.Deferred(); + if (node.hasClass('fade')) { + node.fadeToggle( + this.options.transitionDuration, + this.options.transitionEasing, + function () { + deferred.resolveWith(node); + } + ); + } else { + deferred.resolveWith(node); + } + return deferred; + }, + + _create: function () { + this._super(); + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button({icons: {primary: 'ui-icon-plusthick'}}) + .append(input); + }) + .end().find('.start') + .button({icons: {primary: 'ui-icon-circle-arrow-e'}}) + .end().find('.cancel') + .button({icons: {primary: 'ui-icon-cancel'}}) + .end().find('.delete') + .button({icons: {primary: 'ui-icon-trash'}}) + .end().find('.progress').progressbar(); + }, + + _destroy: function () { + this.element + .find('.fileupload-buttonbar') + .find('.fileinput-button').each(function () { + var input = $(this).find('input:file').detach(); + $(this) + .button('destroy') + .append(input); + }) + .end().find('.start') + .button('destroy') + .end().find('.cancel') + .button('destroy') + .end().find('.delete') + .button('destroy') + .end().find('.progress').progressbar('destroy'); + this._super(); + } + + }); + +})); +/* + * jQuery File Upload Processing Plugin 1.3.0 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2012, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + var originalAdd = $.blueimp.fileupload.prototype.options.add; + + // The File Upload Processing plugin extends the fileupload widget + // with file processing functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The list of processing actions: + processQueue: [ + /* + { + action: 'log', + type: 'debug' + } + */ + ], + add: function (e, data) { + var $this = $(this); + data.process(function () { + return $this.fileupload('process', data); + }); + originalAdd.call(this, e, data); + } + }, + + processActions: { + /* + log: function (data, options) { + console[options.type]( + 'Processing "' + data.files[data.index].name + '"' + ); + } + */ + }, + + _processFile: function (data, originalData) { + var that = this, + dfd = $.Deferred().resolveWith(that, [data]), + chain = dfd.promise(); + this._trigger('process', null, data); + $.each(data.processQueue, function (i, settings) { + var func = function (data) { + if (originalData.errorThrown) { + return $.Deferred() + .rejectWith(that, [originalData]).promise(); + } + return that.processActions[settings.action].call( + that, + data, + settings + ); + }; + chain = chain.pipe(func, settings.always && func); + }); + chain + .done(function () { + that._trigger('processdone', null, data); + that._trigger('processalways', null, data); + }) + .fail(function () { + that._trigger('processfail', null, data); + that._trigger('processalways', null, data); + }); + return chain; + }, + + // Replaces the settings of each processQueue item that + // are strings starting with an "@", using the remaining + // substring as key for the option map, + // e.g. "@autoUpload" is replaced with options.autoUpload: + _transformProcessQueue: function (options) { + var processQueue = []; + $.each(options.processQueue, function () { + var settings = {}, + action = this.action, + prefix = this.prefix === true ? action : this.prefix; + $.each(this, function (key, value) { + if ($.type(value) === 'string' && + value.charAt(0) === '@') { + settings[key] = options[ + value.slice(1) || (prefix ? prefix + + key.charAt(0).toUpperCase() + key.slice(1) : key) + ]; + } else { + settings[key] = value; + } + + }); + processQueue.push(settings); + }); + options.processQueue = processQueue; + }, + + // Returns the number of files currently in the processsing queue: + processing: function () { + return this._processing; + }, + + // Processes the files given as files property of the data parameter, + // returns a Promise object that allows to bind callbacks: + process: function (data) { + var that = this, + options = $.extend({}, this.options, data); + if (options.processQueue && options.processQueue.length) { + this._transformProcessQueue(options); + if (this._processing === 0) { + this._trigger('processstart'); + } + $.each(data.files, function (index) { + var opts = index ? $.extend({}, options) : options, + func = function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return that._processFile(opts, data); + }; + opts.index = index; + that._processing += 1; + that._processingQueue = that._processingQueue.pipe(func, func) + .always(function () { + that._processing -= 1; + if (that._processing === 0) { + that._trigger('processstop'); + } + }); + }); + } + return this._processingQueue; + }, + + _create: function () { + this._super(); + this._processing = 0; + this._processingQueue = $.Deferred().resolveWith(this) + .promise(); + } + + }); + +})); +/* + * jQuery File Upload User Interface Plugin 9.6.0 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'tmpl', + './jquery.fileupload-image', + './jquery.fileupload-audio', + './jquery.fileupload-video', + './jquery.fileupload-validate' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.tmpl + ); + } +}(function ($, tmpl) { + 'use strict'; + + $.blueimp.fileupload.prototype._specialOptions.push( + 'filesContainer', + 'uploadTemplateId', + 'downloadTemplateId' + ); + + // The UI version extends the file upload widget + // and adds complete user interface interaction: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // By default, files added to the widget are uploaded as soon + // as the user clicks on the start buttons. To enable automatic + // uploads, set the following option to true: + autoUpload: false, + // The ID of the upload template: + uploadTemplateId: 'template-upload', + // The ID of the download template: + downloadTemplateId: 'template-download', + // The container for the list of files. If undefined, it is set to + // an element with class "files" inside of the widget element: + filesContainer: undefined, + // By default, files are appended to the files container. + // Set the following option to true, to prepend files instead: + prependFiles: false, + // The expected data type of the upload response, sets the dataType + // option of the $.ajax upload requests: + dataType: 'json', + + // Error and info messages: + messages: { + unknownError: 'Unknown error' + }, + + // Function returning the current number of files, + // used by the maxNumberOfFiles validation: + getNumberOfFiles: function () { + return this.filesContainer.children() + .not('.processing').length; + }, + + // Callback to retrieve the list of files from the server response: + getFilesFromResponse: function (data) { + if (data.result && $.isArray(data.result.files)) { + return data.result.files; + } + return []; + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop or add API call). + // See the basic file upload widget for more information: + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + that = $this.data('blueimp-fileupload') || + $this.data('fileupload'), + options = that.options; + data.context = that._renderUpload(data.files) + .data('data', data) + .addClass('processing'); + options.filesContainer[ + options.prependFiles ? 'prepend' : 'append' + ](data.context); + that._forceReflow(data.context); + that._transition(data.context); + data.process(function () { + return $this.fileupload('process', data); + }).always(function () { + data.context.each(function (index) { + $(this).find('.size').text( + that._formatFileSize(data.files[index].size) + ); + }).removeClass('processing'); + that._renderPreviews(data); + }).done(function () { + data.context.find('.start').prop('disabled', false); + if ((that._trigger('added', e, data) !== false) && + (options.autoUpload || data.autoUpload) && + data.autoUpload !== false) { + data.submit(); + } + }).fail(function () { + if (data.files.error) { + data.context.each(function (index) { + var error = data.files[index].error; + if (error) { + $(this).find('.error').text(error); + } + }); + } + }); + }, + // Callback for the start of each file upload request: + send: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + if (data.context && data.dataType && + data.dataType.substr(0, 6) === 'iframe') { + // Iframe Transport does not support progress events. + // In lack of an indeterminate progress bar, we set + // the progress to 100%, showing the full animated bar: + data.context + .find('.progress').addClass( + !$.support.transition && 'progress-animated' + ) + .attr('aria-valuenow', 100) + .children().first().css( + 'width', + '100%' + ); + } + return that._trigger('sent', e, data); + }, + // Callback for successful uploads: + done: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + getFilesFromResponse = data.getFilesFromResponse || + that.options.getFilesFromResponse, + files = getFilesFromResponse(data), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + var file = files[index] || + {error: 'Empty file upload result'}; + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + }); + } else { + template = that._renderDownload(files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer); + that._forceReflow(template); + deferred = that._addFinishedDeferreds(); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('completed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }, + // Callback for failed (abort or error) uploads: + fail: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + template, + deferred; + if (data.context) { + data.context.each(function (index) { + if (data.errorThrown !== 'abort') { + var file = data.files[index]; + file.error = file.error || data.errorThrown || + data.i18n('unknownError'); + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + var node = $(this); + template = that._renderDownload([file]) + .replaceAll(node); + that._forceReflow(template); + that._transition(template).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + ); + } else { + deferred = that._addFinishedDeferreds(); + that._transition($(this)).done( + function () { + $(this).remove(); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } + }); + } else if (data.errorThrown !== 'abort') { + data.context = that._renderUpload(data.files)[ + that.options.prependFiles ? 'prependTo' : 'appendTo' + ](that.options.filesContainer) + .data('data', data); + that._forceReflow(data.context); + deferred = that._addFinishedDeferreds(); + that._transition(data.context).done( + function () { + data.context = $(this); + that._trigger('failed', e, data); + that._trigger('finished', e, data); + deferred.resolve(); + } + ); + } else { + that._trigger('failed', e, data); + that._trigger('finished', e, data); + that._addFinishedDeferreds().resolve(); + } + }, + // Callback for upload progress events: + progress: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var progress = Math.floor(data.loaded / data.total * 100); + if (data.context) { + data.context.each(function () { + $(this).find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }); + } + }, + // Callback for global upload progress events: + progressall: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var $this = $(this), + progress = Math.floor(data.loaded / data.total * 100), + globalProgressNode = $this.find('.fileupload-progress'), + extendedProgressNode = globalProgressNode + .find('.progress-extended'); + if (extendedProgressNode.length) { + extendedProgressNode.html( + ($this.data('blueimp-fileupload') || $this.data('fileupload')) + ._renderExtendedProgress(data) + ); + } + globalProgressNode + .find('.progress') + .attr('aria-valuenow', progress) + .children().first().css( + 'width', + progress + '%' + ); + }, + // Callback for uploads start, equivalent to the global ajaxStart event: + start: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'); + that._resetFinishedDeferreds(); + that._transition($(this).find('.fileupload-progress')).done( + function () { + that._trigger('started', e); + } + ); + }, + // Callback for uploads stop, equivalent to the global ajaxStop event: + stop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + deferred = that._addFinishedDeferreds(); + $.when.apply($, that._getFinishedDeferreds()) + .done(function () { + that._trigger('stopped', e); + }); + that._transition($(this).find('.fileupload-progress')).done( + function () { + $(this).find('.progress') + .attr('aria-valuenow', '0') + .children().first().css('width', '0%'); + $(this).find('.progress-extended').html(' '); + deferred.resolve(); + } + ); + }, + processstart: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).addClass('fileupload-processing'); + }, + processstop: function (e) { + if (e.isDefaultPrevented()) { + return false; + } + $(this).removeClass('fileupload-processing'); + }, + // Callback for file deletion: + destroy: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + var that = $(this).data('blueimp-fileupload') || + $(this).data('fileupload'), + removeNode = function () { + that._transition(data.context).done( + function () { + $(this).remove(); + that._trigger('destroyed', e, data); + } + ); + }; + if (data.url) { + data.dataType = data.dataType || that.options.dataType; + $.ajax(data).done(removeNode).fail(function () { + that._trigger('destroyfailed', e, data); + }); + } else { + removeNode(); + } + } + }, + + _resetFinishedDeferreds: function () { + this._finishedUploads = []; + }, + + _addFinishedDeferreds: function (deferred) { + if (!deferred) { + deferred = $.Deferred(); + } + this._finishedUploads.push(deferred); + return deferred; + }, + + _getFinishedDeferreds: function () { + return this._finishedUploads; + }, + + // Link handler, that allows to download files + // by drag & drop of the links to the desktop: + _enableDragToDesktop: function () { + var link = $(this), + url = link.prop('href'), + name = link.prop('download'), + type = 'application/octet-stream'; + link.bind('dragstart', function (e) { + try { + e.originalEvent.dataTransfer.setData( + 'DownloadURL', + [type, name, url].join(':') + ); + } catch (ignore) {} + }); + }, + + _formatFileSize: function (bytes) { + if (typeof bytes !== 'number') { + return ''; + } + if (bytes >= 1000000000) { + return (bytes / 1000000000).toFixed(2) + ' GB'; + } + if (bytes >= 1000000) { + return (bytes / 1000000).toFixed(2) + ' MB'; + } + return (bytes / 1000).toFixed(2) + ' KB'; + }, + + _formatBitrate: function (bits) { + if (typeof bits !== 'number') { + return ''; + } + if (bits >= 1000000000) { + return (bits / 1000000000).toFixed(2) + ' Gbit/s'; + } + if (bits >= 1000000) { + return (bits / 1000000).toFixed(2) + ' Mbit/s'; + } + if (bits >= 1000) { + return (bits / 1000).toFixed(2) + ' kbit/s'; + } + return bits.toFixed(2) + ' bit/s'; + }, + + _formatTime: function (seconds) { + var date = new Date(seconds * 1000), + days = Math.floor(seconds / 86400); + days = days ? days + 'd ' : ''; + return days + + ('0' + date.getUTCHours()).slice(-2) + ':' + + ('0' + date.getUTCMinutes()).slice(-2) + ':' + + ('0' + date.getUTCSeconds()).slice(-2); + }, + + _formatPercentage: function (floatValue) { + return (floatValue * 100).toFixed(2) + ' %'; + }, + + _renderExtendedProgress: function (data) { + return this._formatBitrate(data.bitrate) + ' | ' + + this._formatTime( + (data.total - data.loaded) * 8 / data.bitrate + ) + ' | ' + + this._formatPercentage( + data.loaded / data.total + ) + ' | ' + + this._formatFileSize(data.loaded) + ' / ' + + this._formatFileSize(data.total); + }, + + _renderTemplate: function (func, files) { + if (!func) { + return $(); + } + var result = func({ + files: files, + formatFileSize: this._formatFileSize, + options: this.options + }); + if (result instanceof $) { + return result; + } + return $(this.options.templatesContainer).html(result).children(); + }, + + _renderPreviews: function (data) { + data.context.find('.preview').each(function (index, elm) { + $(elm).append(data.files[index].preview); + }); + }, + + _renderUpload: function (files) { + return this._renderTemplate( + this.options.uploadTemplate, + files + ); + }, + + _renderDownload: function (files) { + return this._renderTemplate( + this.options.downloadTemplate, + files + ).find('a[download]').each(this._enableDragToDesktop).end(); + }, + + _startHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget), + template = button.closest('.template-upload'), + data = template.data('data'); + button.prop('disabled', true); + if (data && data.submit) { + data.submit(); + } + }, + + _cancelHandler: function (e) { + e.preventDefault(); + var template = $(e.currentTarget) + .closest('.template-upload,.template-download'), + data = template.data('data') || {}; + data.context = data.context || template; + if (data.abort) { + data.abort(); + } else { + data.errorThrown = 'abort'; + this._trigger('fail', e, data); + } + }, + + _deleteHandler: function (e) { + e.preventDefault(); + var button = $(e.currentTarget); + this._trigger('destroy', e, $.extend({ + context: button.closest('.template-download'), + type: 'DELETE' + }, button.data())); + }, + + _forceReflow: function (node) { + return $.support.transition && node.length && + node[0].offsetWidth; + }, + + _transition: function (node) { + var dfd = $.Deferred(); + if ($.support.transition && node.hasClass('fade') && node.is(':visible')) { + node.bind( + $.support.transition.end, + function (e) { + // Make sure we don't respond to other transitions events + // in the container element, e.g. from button elements: + if (e.target === node[0]) { + node.unbind($.support.transition.end); + dfd.resolveWith(node); + } + } + ).toggleClass('in'); + } else { + node.toggleClass('in'); + dfd.resolveWith(node); + } + return dfd; + }, + + _initButtonBarEventHandlers: function () { + var fileUploadButtonBar = this.element.find('.fileupload-buttonbar'), + filesList = this.options.filesContainer; + this._on(fileUploadButtonBar.find('.start'), { + click: function (e) { + e.preventDefault(); + filesList.find('.start').click(); + } + }); + this._on(fileUploadButtonBar.find('.cancel'), { + click: function (e) { + e.preventDefault(); + filesList.find('.cancel').click(); + } + }); + this._on(fileUploadButtonBar.find('.delete'), { + click: function (e) { + e.preventDefault(); + filesList.find('.toggle:checked') + .closest('.template-download') + .find('.delete').click(); + fileUploadButtonBar.find('.toggle') + .prop('checked', false); + } + }); + this._on(fileUploadButtonBar.find('.toggle'), { + change: function (e) { + filesList.find('.toggle').prop( + 'checked', + $(e.currentTarget).is(':checked') + ); + } + }); + }, + + _destroyButtonBarEventHandlers: function () { + this._off( + this.element.find('.fileupload-buttonbar') + .find('.start, .cancel, .delete'), + 'click' + ); + this._off( + this.element.find('.fileupload-buttonbar .toggle'), + 'change.' + ); + }, + + _initEventHandlers: function () { + this._super(); + this._on(this.options.filesContainer, { + 'click .start': this._startHandler, + 'click .cancel': this._cancelHandler, + 'click .delete': this._deleteHandler + }); + this._initButtonBarEventHandlers(); + }, + + _destroyEventHandlers: function () { + this._destroyButtonBarEventHandlers(); + this._off(this.options.filesContainer, 'click'); + this._super(); + }, + + _enableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', false) + .parent().removeClass('disabled'); + }, + + _disableFileInputButton: function () { + this.element.find('.fileinput-button input') + .prop('disabled', true) + .parent().addClass('disabled'); + }, + + _initTemplates: function () { + var options = this.options; + options.templatesContainer = this.document[0].createElement( + options.filesContainer.prop('nodeName') + ); + if (tmpl) { + if (options.uploadTemplateId) { + options.uploadTemplate = tmpl(options.uploadTemplateId); + } + if (options.downloadTemplateId) { + options.downloadTemplate = tmpl(options.downloadTemplateId); + } + } + }, + + _initFilesContainer: function () { + var options = this.options; + if (options.filesContainer === undefined) { + options.filesContainer = this.element.find('.files'); + } else if (!(options.filesContainer instanceof $)) { + options.filesContainer = $(options.filesContainer); + } + }, + + _initSpecialOptions: function () { + this._super(); + this._initFilesContainer(); + this._initTemplates(); + }, + + _create: function () { + this._super(); + this._resetFinishedDeferreds(); + if (!$.support.fileInput) { + this._disableFileInputButton(); + } + }, + + enable: function () { + var wasDisabled = false; + if (this.options.disabled) { + wasDisabled = true; + } + this._super(); + if (wasDisabled) { + this.element.find('input, button').prop('disabled', false); + this._enableFileInputButton(); + } + }, + + disable: function () { + if (!this.options.disabled) { + this.element.find('input, button').prop('disabled', true); + this._disableFileInputButton(); + } + this._super(); + } + + }); + +})); +/* + * jQuery File Upload Validation Plugin 1.1.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, window */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery + ); + } +}(function ($) { + 'use strict'; + + // Append to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.push( + { + action: 'validate', + // Always trigger this action, + // even if the previous action was rejected: + always: true, + // Options taken from the global options map: + acceptFileTypes: '@', + maxFileSize: '@', + minFileSize: '@', + maxNumberOfFiles: '@', + disabled: '@disableValidation' + } + ); + + // The File Upload Validation plugin extends the fileupload widget + // with file validation functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + /* + // The regular expression for allowed file types, matches + // against either file type or file name: + acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, + // The maximum allowed file size in bytes: + maxFileSize: 10000000, // 10 MB + // The minimum allowed file size in bytes: + minFileSize: undefined, // No minimal file size + // The limit of files to be uploaded: + maxNumberOfFiles: 10, + */ + + // Function returning the current number of files, + // has to be overriden for maxNumberOfFiles validation: + getNumberOfFiles: $.noop, + + // Error and info messages: + messages: { + maxNumberOfFiles: 'Maximum number of files exceeded', + acceptFileTypes: 'File type not allowed', + maxFileSize: 'File is too large', + minFileSize: 'File is too small' + } + }, + + processActions: { + + validate: function (data, options) { + if (options.disabled) { + return data; + } + var dfd = $.Deferred(), + settings = this.options, + file = data.files[data.index], + fileSize; + if (options.minFileSize || options.maxFileSize) { + fileSize = file.size; + } + if ($.type(options.maxNumberOfFiles) === 'number' && + (settings.getNumberOfFiles() || 0) + data.files.length > + options.maxNumberOfFiles) { + file.error = settings.i18n('maxNumberOfFiles'); + } else if (options.acceptFileTypes && + !(options.acceptFileTypes.test(file.type) || + options.acceptFileTypes.test(file.name))) { + file.error = settings.i18n('acceptFileTypes'); + } else if (fileSize > options.maxFileSize) { + file.error = settings.i18n('maxFileSize'); + } else if ($.type(fileSize) === 'number' && + fileSize < options.minFileSize) { + file.error = settings.i18n('minFileSize'); + } else { + delete file.error; + } + if (file.error || data.files.error) { + data.files.error = true; + dfd.rejectWith(this, [data]); + } else { + dfd.resolveWith(this, [data]); + } + return dfd.promise(); + } + + } + + }); + +})); +/* + * jQuery File Upload Video Preview Plugin 1.0.3 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2013, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'load-image', + './jquery.fileupload-process' + ], factory); + } else { + // Browser globals: + factory( + window.jQuery, + window.loadImage + ); + } +}(function ($, loadImage) { + 'use strict'; + + // Prepend to the default processQueue: + $.blueimp.fileupload.prototype.options.processQueue.unshift( + { + action: 'loadVideo', + // Use the action as prefix for the "@" options: + prefix: true, + fileTypes: '@', + maxFileSize: '@', + disabled: '@disableVideoPreview' + }, + { + action: 'setVideo', + name: '@videoPreviewName', + disabled: '@disableVideoPreview' + } + ); + + // The File Upload Video Preview plugin extends the fileupload widget + // with video preview functionality: + $.widget('blueimp.fileupload', $.blueimp.fileupload, { + + options: { + // The regular expression for the types of video files to load, + // matched against the file type: + loadVideoFileTypes: /^video\/.*$/ + }, + + _videoElement: document.createElement('video'), + + processActions: { + + // Loads the video file given via data.files and data.index + // as video element if the browser supports playing it. + // Accepts the options fileTypes (regular expression) + // and maxFileSize (integer) to limit the files to load: + loadVideo: function (data, options) { + if (options.disabled) { + return data; + } + var file = data.files[data.index], + url, + video; + if (this._videoElement.canPlayType && + this._videoElement.canPlayType(file.type) && + ($.type(options.maxFileSize) !== 'number' || + file.size <= options.maxFileSize) && + (!options.fileTypes || + options.fileTypes.test(file.type))) { + url = loadImage.createObjectURL(file); + if (url) { + video = this._videoElement.cloneNode(false); + video.src = url; + video.controls = true; + data.video = video; + return data; + } + } + return data; + }, + + // Sets the video element as a property of the file object: + setVideo: function (data, options) { + if (data.video && !options.disabled) { + data.files[data.index][options.name || 'preview'] = data.video; + } + return data; + } + + } + + }); + +})); +/* + * jQuery File Upload Plugin 5.42.0 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2010, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* jshint nomen:false */ +/* global define, window, document, location, Blob, FormData */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define([ + 'jquery', + 'jquery.ui.widget' + ], factory); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Detect file input support, based on + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/ + $.support.fileInput = !(new RegExp( + // Handle devices which give false positives for the feature detection: + '(Android (1\\.[0156]|2\\.[01]))' + + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' + + '|(w(eb)?OSBrowser)|(webOS)' + + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))' + ).test(window.navigator.userAgent) || + // Feature detection for all other devices: + $('<input type="file">').prop('disabled')); + + // The FileReader API is not actually used, but works as feature detection, + // as some Safari versions (5?) support XHR file uploads via the FormData API, + // but not non-multipart XHR file uploads. + // window.XMLHttpRequestUpload is not available on IE10, so we check for + // window.ProgressEvent instead to detect XHR2 file upload capability: + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader); + $.support.xhrFormDataFileUpload = !!window.FormData; + + // Detect support for Blob slicing (required for chunked uploads): + $.support.blobSlice = window.Blob && (Blob.prototype.slice || + Blob.prototype.webkitSlice || Blob.prototype.mozSlice); + + // Helper function to create drag handlers for dragover/dragenter/dragleave: + function getDragHandler(type) { + var isDragOver = type === 'dragover'; + return function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var dataTransfer = e.dataTransfer; + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 && + this._trigger( + type, + $.Event(type, {delegatedEvent: e}) + ) !== false) { + e.preventDefault(); + if (isDragOver) { + dataTransfer.dropEffect = 'copy'; + } + } + }; + } + + // The fileupload widget listens for change events on file input fields defined + // via fileInput setting and paste or drop events of the given dropZone. + // In addition to the default jQuery Widget methods, the fileupload widget + // exposes the "add" and "send" methods, to add or directly send files using + // the fileupload API. + // By default, files added via file input selection, paste, drag & drop or + // "add" method are uploaded immediately, but it is possible to override + // the "add" callback option to queue file uploads. + $.widget('blueimp.fileupload', { + + options: { + // The drop target element(s), by the default the complete document. + // Set to null to disable drag & drop support: + dropZone: $(document), + // The paste target element(s), by the default undefined. + // Set to a DOM node or jQuery object to enable file pasting: + pasteZone: undefined, + // The file input field(s), that are listened to for change events. + // If undefined, it is set to the file input fields inside + // of the widget element on plugin initialization. + // Set to null to disable the change listener. + fileInput: undefined, + // By default, the file input field is replaced with a clone after + // each input field change event. This is required for iframe transport + // queues and allows change events to be fired for the same file + // selection, but can be disabled by setting the following option to false: + replaceFileInput: true, + // The parameter name for the file form data (the request argument name). + // If undefined or empty, the name property of the file input field is + // used, or "files[]" if the file input name property is also empty, + // can be a string or an array of strings: + paramName: undefined, + // By default, each file of a selection is uploaded using an individual + // request for XHR type uploads. Set to false to upload file + // selections in one request each: + singleFileUploads: true, + // To limit the number of files uploaded with one XHR request, + // set the following option to an integer greater than 0: + limitMultiFileUploads: undefined, + // The following option limits the number of files uploaded with one + // XHR request to keep the request size under or equal to the defined + // limit in bytes: + limitMultiFileUploadSize: undefined, + // Multipart file uploads add a number of bytes to each uploaded file, + // therefore the following option adds an overhead for each file used + // in the limitMultiFileUploadSize configuration: + limitMultiFileUploadSizeOverhead: 512, + // Set the following option to true to issue all file upload requests + // in a sequential order: + sequentialUploads: false, + // To limit the number of concurrent uploads, + // set the following option to an integer greater than 0: + limitConcurrentUploads: undefined, + // Set the following option to true to force iframe transport uploads: + forceIframeTransport: false, + // Set the following option to the location of a redirect url on the + // origin server, for cross-domain iframe transport uploads: + redirect: undefined, + // The parameter name for the redirect url, sent as part of the form + // data and set to 'redirect' if this option is empty: + redirectParamName: undefined, + // Set the following option to the location of a postMessage window, + // to enable postMessage transport uploads: + postMessage: undefined, + // By default, XHR file uploads are sent as multipart/form-data. + // The iframe transport is always using multipart/form-data. + // Set to false to enable non-multipart XHR uploads: + multipart: true, + // To upload large files in smaller chunks, set the following option + // to a preferred maximum chunk size. If set to 0, null or undefined, + // or the browser does not support the required Blob API, files will + // be uploaded as a whole. + maxChunkSize: undefined, + // When a non-multipart upload or a chunked multipart upload has been + // aborted, this option can be used to resume the upload by setting + // it to the size of the already uploaded bytes. This option is most + // useful when modifying the options object inside of the "add" or + // "send" callbacks, as the options are cloned for each file upload. + uploadedBytes: undefined, + // By default, failed (abort or error) file uploads are removed from the + // global progress calculation. Set the following option to false to + // prevent recalculating the global progress data: + recalculateProgress: true, + // Interval in milliseconds to calculate and trigger progress events: + progressInterval: 100, + // Interval in milliseconds to calculate progress bitrate: + bitrateInterval: 500, + // By default, uploads are started automatically when adding files: + autoUpload: true, + + // Error and info messages: + messages: { + uploadedBytes: 'Uploaded bytes exceed file size' + }, + + // Translation function, gets the message key to be translated + // and an object with context specific data as arguments: + i18n: function (message, context) { + message = this.messages[message] || message.toString(); + if (context) { + $.each(context, function (key, value) { + message = message.replace('{' + key + '}', value); + }); + } + return message; + }, + + // Additional form data to be sent along with the file uploads can be set + // using this option, which accepts an array of objects with name and + // value properties, a function returning such an array, a FormData + // object (for XHR file uploads), or a simple object. + // The form of the first fileInput is given as parameter to the function: + formData: function (form) { + return form.serializeArray(); + }, + + // The add callback is invoked as soon as files are added to the fileupload + // widget (via file input selection, drag & drop, paste or add API call). + // If the singleFileUploads option is enabled, this callback will be + // called once for each file in the selection for XHR file uploads, else + // once for each file selection. + // + // The upload starts when the submit method is invoked on the data parameter. + // The data object contains a files property holding the added files + // and allows you to override plugin options as well as define ajax settings. + // + // Listeners for this callback can also be bound the following way: + // .bind('fileuploadadd', func); + // + // data.submit() returns a Promise object and allows to attach additional + // handlers using jQuery's Deferred callbacks: + // data.submit().done(func).fail(func).always(func); + add: function (e, data) { + if (e.isDefaultPrevented()) { + return false; + } + if (data.autoUpload || (data.autoUpload !== false && + $(this).fileupload('option', 'autoUpload'))) { + data.process().done(function () { + data.submit(); + }); + } + }, + + // Other callbacks: + + // Callback for the submit event of each file upload: + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func); + + // Callback for the start of each file upload request: + // send: function (e, data) {}, // .bind('fileuploadsend', func); + + // Callback for successful uploads: + // done: function (e, data) {}, // .bind('fileuploaddone', func); + + // Callback for failed (abort or error) uploads: + // fail: function (e, data) {}, // .bind('fileuploadfail', func); + + // Callback for completed (success, abort or error) requests: + // always: function (e, data) {}, // .bind('fileuploadalways', func); + + // Callback for upload progress events: + // progress: function (e, data) {}, // .bind('fileuploadprogress', func); + + // Callback for global upload progress events: + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func); + + // Callback for uploads start, equivalent to the global ajaxStart event: + // start: function (e) {}, // .bind('fileuploadstart', func); + + // Callback for uploads stop, equivalent to the global ajaxStop event: + // stop: function (e) {}, // .bind('fileuploadstop', func); + + // Callback for change events of the fileInput(s): + // change: function (e, data) {}, // .bind('fileuploadchange', func); + + // Callback for paste events to the pasteZone(s): + // paste: function (e, data) {}, // .bind('fileuploadpaste', func); + + // Callback for drop events of the dropZone(s): + // drop: function (e, data) {}, // .bind('fileuploaddrop', func); + + // Callback for dragover events of the dropZone(s): + // dragover: function (e) {}, // .bind('fileuploaddragover', func); + + // Callback for the start of each chunk upload request: + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func); + + // Callback for successful chunk uploads: + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func); + + // Callback for failed (abort or error) chunk uploads: + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func); + + // Callback for completed (success, abort or error) chunk upload requests: + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func); + + // The plugin options are used as settings object for the ajax calls. + // The following are jQuery ajax settings required for the file uploads: + processData: false, + contentType: false, + cache: false + }, + + // A list of options that require reinitializing event listeners and/or + // special initialization code: + _specialOptions: [ + 'fileInput', + 'dropZone', + 'pasteZone', + 'multipart', + 'forceIframeTransport' + ], + + _blobSlice: $.support.blobSlice && function () { + var slice = this.slice || this.webkitSlice || this.mozSlice; + return slice.apply(this, arguments); + }, + + _BitrateTimer: function () { + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime()); + this.loaded = 0; + this.bitrate = 0; + this.getBitrate = function (now, loaded, interval) { + var timeDiff = now - this.timestamp; + if (!this.bitrate || !interval || timeDiff > interval) { + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8; + this.loaded = loaded; + this.timestamp = now; + } + return this.bitrate; + }; + }, + + _isXHRUpload: function (options) { + return !options.forceIframeTransport && + ((!options.multipart && $.support.xhrFileUpload) || + $.support.xhrFormDataFileUpload); + }, + + _getFormData: function (options) { + var formData; + if ($.type(options.formData) === 'function') { + return options.formData(options.form); + } + if ($.isArray(options.formData)) { + return options.formData; + } + if ($.type(options.formData) === 'object') { + formData = []; + $.each(options.formData, function (name, value) { + formData.push({name: name, value: value}); + }); + return formData; + } + return []; + }, + + _getTotal: function (files) { + var total = 0; + $.each(files, function (index, file) { + total += file.size || 1; + }); + return total; + }, + + _initProgressObject: function (obj) { + var progress = { + loaded: 0, + total: 0, + bitrate: 0 + }; + if (obj._progress) { + $.extend(obj._progress, progress); + } else { + obj._progress = progress; + } + }, + + _initResponseObject: function (obj) { + var prop; + if (obj._response) { + for (prop in obj._response) { + if (obj._response.hasOwnProperty(prop)) { + delete obj._response[prop]; + } + } + } else { + obj._response = {}; + } + }, + + _onProgress: function (e, data) { + if (e.lengthComputable) { + var now = ((Date.now) ? Date.now() : (new Date()).getTime()), + loaded; + if (data._time && data.progressInterval && + (now - data._time < data.progressInterval) && + e.loaded !== e.total) { + return; + } + data._time = now; + loaded = Math.floor( + e.loaded / e.total * (data.chunkSize || data._progress.total) + ) + (data.uploadedBytes || 0); + // Add the difference from the previously loaded state + // to the global loaded counter: + this._progress.loaded += (loaded - data._progress.loaded); + this._progress.bitrate = this._bitrateTimer.getBitrate( + now, + this._progress.loaded, + data.bitrateInterval + ); + data._progress.loaded = data.loaded = loaded; + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate( + now, + loaded, + data.bitrateInterval + ); + // Trigger a custom progress event with a total data property set + // to the file size(s) of the current upload and a loaded data + // property calculated accordingly: + this._trigger( + 'progress', + $.Event('progress', {delegatedEvent: e}), + data + ); + // Trigger a global progress event for all current file uploads, + // including ajax calls queued for sequential file uploads: + this._trigger( + 'progressall', + $.Event('progressall', {delegatedEvent: e}), + this._progress + ); + } + }, + + _initProgressListener: function (options) { + var that = this, + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr(); + // Accesss to the native XHR object is required to add event listeners + // for the upload progress event: + if (xhr.upload) { + $(xhr.upload).bind('progress', function (e) { + var oe = e.originalEvent; + // Make sure the progress event properties get copied over: + e.lengthComputable = oe.lengthComputable; + e.loaded = oe.loaded; + e.total = oe.total; + that._onProgress(e, options); + }); + options.xhr = function () { + return xhr; + }; + } + }, + + _isInstanceOf: function (type, obj) { + // Cross-frame instanceof check + return Object.prototype.toString.call(obj) === '[object ' + type + ']'; + }, + + _initXHRData: function (options) { + var that = this, + formData, + file = options.files[0], + // Ignore non-multipart setting if not supported: + multipart = options.multipart || !$.support.xhrFileUpload, + paramName = $.type(options.paramName) === 'array' ? + options.paramName[0] : options.paramName; + options.headers = $.extend({}, options.headers); + if (options.contentRange) { + options.headers['Content-Range'] = options.contentRange; + } + if (!multipart || options.blob || !this._isInstanceOf('File', file)) { + options.headers['Content-Disposition'] = 'attachment; filename="' + + encodeURI(file.name) + '"'; + } + if (!multipart) { + options.contentType = file.type || 'application/octet-stream'; + options.data = options.blob || file; + } else if ($.support.xhrFormDataFileUpload) { + if (options.postMessage) { + // window.postMessage does not allow sending FormData + // objects, so we just add the File/Blob objects to + // the formData array and let the postMessage window + // create the FormData object out of this array: + formData = this._getFormData(options); + if (options.blob) { + formData.push({ + name: paramName, + value: options.blob + }); + } else { + $.each(options.files, function (index, file) { + formData.push({ + name: ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + value: file + }); + }); + } + } else { + if (that._isInstanceOf('FormData', options.formData)) { + formData = options.formData; + } else { + formData = new FormData(); + $.each(this._getFormData(options), function (index, field) { + formData.append(field.name, field.value); + }); + } + if (options.blob) { + formData.append(paramName, options.blob, file.name); + } else { + $.each(options.files, function (index, file) { + // This check allows the tests to run with + // dummy objects: + if (that._isInstanceOf('File', file) || + that._isInstanceOf('Blob', file)) { + formData.append( + ($.type(options.paramName) === 'array' && + options.paramName[index]) || paramName, + file, + file.uploadName || file.name + ); + } + }); + } + } + options.data = formData; + } + // Blob reference is not needed anymore, free memory: + options.blob = null; + }, + + _initIframeSettings: function (options) { + var targetHost = $('<a></a>').prop('href', options.url).prop('host'); + // Setting the dataType to iframe enables the iframe transport: + options.dataType = 'iframe ' + (options.dataType || ''); + // The iframe transport accepts a serialized array as form data: + options.formData = this._getFormData(options); + // Add redirect url to form data on cross-domain uploads: + if (options.redirect && targetHost && targetHost !== location.host) { + options.formData.push({ + name: options.redirectParamName || 'redirect', + value: options.redirect + }); + } + }, + + _initDataSettings: function (options) { + if (this._isXHRUpload(options)) { + if (!this._chunkedUpload(options, true)) { + if (!options.data) { + this._initXHRData(options); + } + this._initProgressListener(options); + } + if (options.postMessage) { + // Setting the dataType to postmessage enables the + // postMessage transport: + options.dataType = 'postmessage ' + (options.dataType || ''); + } + } else { + this._initIframeSettings(options); + } + }, + + _getParamName: function (options) { + var fileInput = $(options.fileInput), + paramName = options.paramName; + if (!paramName) { + paramName = []; + fileInput.each(function () { + var input = $(this), + name = input.prop('name') || 'files[]', + i = (input.prop('files') || [1]).length; + while (i) { + paramName.push(name); + i -= 1; + } + }); + if (!paramName.length) { + paramName = [fileInput.prop('name') || 'files[]']; + } + } else if (!$.isArray(paramName)) { + paramName = [paramName]; + } + return paramName; + }, + + _initFormSettings: function (options) { + // Retrieve missing options from the input field and the + // associated form, if available: + if (!options.form || !options.form.length) { + options.form = $(options.fileInput.prop('form')); + // If the given file input doesn't have an associated form, + // use the default widget file input's form: + if (!options.form.length) { + options.form = $(this.options.fileInput.prop('form')); + } + } + options.paramName = this._getParamName(options); + if (!options.url) { + options.url = options.form.prop('action') || location.href; + } + // The HTTP request method must be "POST" or "PUT": + options.type = (options.type || + ($.type(options.form.prop('method')) === 'string' && + options.form.prop('method')) || '' + ).toUpperCase(); + if (options.type !== 'POST' && options.type !== 'PUT' && + options.type !== 'PATCH') { + options.type = 'POST'; + } + if (!options.formAcceptCharset) { + options.formAcceptCharset = options.form.attr('accept-charset'); + } + }, + + _getAJAXSettings: function (data) { + var options = $.extend({}, this.options, data); + this._initFormSettings(options); + this._initDataSettings(options); + return options; + }, + + // jQuery 1.6 doesn't provide .state(), + // while jQuery 1.8+ removed .isRejected() and .isResolved(): + _getDeferredState: function (deferred) { + if (deferred.state) { + return deferred.state(); + } + if (deferred.isResolved()) { + return 'resolved'; + } + if (deferred.isRejected()) { + return 'rejected'; + } + return 'pending'; + }, + + // Maps jqXHR callbacks to the equivalent + // methods of the given Promise object: + _enhancePromise: function (promise) { + promise.success = promise.done; + promise.error = promise.fail; + promise.complete = promise.always; + return promise; + }, + + // Creates and returns a Promise object enhanced with + // the jqXHR methods abort, success, error and complete: + _getXHRPromise: function (resolveOrReject, context, args) { + var dfd = $.Deferred(), + promise = dfd.promise(); + context = context || this.options.context || promise; + if (resolveOrReject === true) { + dfd.resolveWith(context, args); + } else if (resolveOrReject === false) { + dfd.rejectWith(context, args); + } + promise.abort = dfd.promise; + return this._enhancePromise(promise); + }, + + // Adds convenience methods to the data callback argument: + _addConvenienceMethods: function (e, data) { + var that = this, + getPromise = function (args) { + return $.Deferred().resolveWith(that, args).promise(); + }; + data.process = function (resolveFunc, rejectFunc) { + if (resolveFunc || rejectFunc) { + data._processQueue = this._processQueue = + (this._processQueue || getPromise([this])).pipe( + function () { + if (data.errorThrown) { + return $.Deferred() + .rejectWith(that, [data]).promise(); + } + return getPromise(arguments); + } + ).pipe(resolveFunc, rejectFunc); + } + return this._processQueue || getPromise([this]); + }; + data.submit = function () { + if (this.state() !== 'pending') { + data.jqXHR = this.jqXHR = + (that._trigger( + 'submit', + $.Event('submit', {delegatedEvent: e}), + this + ) !== false) && that._onSend(e, this); + } + return this.jqXHR || that._getXHRPromise(); + }; + data.abort = function () { + if (this.jqXHR) { + return this.jqXHR.abort(); + } + this.errorThrown = 'abort'; + that._trigger('fail', null, this); + return that._getXHRPromise(false); + }; + data.state = function () { + if (this.jqXHR) { + return that._getDeferredState(this.jqXHR); + } + if (this._processQueue) { + return that._getDeferredState(this._processQueue); + } + }; + data.processing = function () { + return !this.jqXHR && this._processQueue && that + ._getDeferredState(this._processQueue) === 'pending'; + }; + data.progress = function () { + return this._progress; + }; + data.response = function () { + return this._response; + }; + }, + + // Parses the Range header from the server response + // and returns the uploaded bytes: + _getUploadedBytes: function (jqXHR) { + var range = jqXHR.getResponseHeader('Range'), + parts = range && range.split('-'), + upperBytesPos = parts && parts.length > 1 && + parseInt(parts[1], 10); + return upperBytesPos && upperBytesPos + 1; + }, + + // Uploads a file in multiple, sequential requests + // by splitting the file up in multiple blob chunks. + // If the second parameter is true, only tests if the file + // should be uploaded in chunks, but does not invoke any + // upload requests: + _chunkedUpload: function (options, testOnly) { + options.uploadedBytes = options.uploadedBytes || 0; + var that = this, + file = options.files[0], + fs = file.size, + ub = options.uploadedBytes, + mcs = options.maxChunkSize || fs, + slice = this._blobSlice, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + upload; + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) || + options.data) { + return false; + } + if (testOnly) { + return true; + } + if (ub >= fs) { + file.error = options.i18n('uploadedBytes'); + return this._getXHRPromise( + false, + options.context, + [null, 'error', file.error] + ); + } + // The chunk upload method: + upload = function () { + // Clone the options object for each chunk upload: + var o = $.extend({}, options), + currentLoaded = o._progress.loaded; + o.blob = slice.call( + file, + ub, + ub + mcs, + file.type + ); + // Store the current chunk size, as the blob itself + // will be dereferenced after data processing: + o.chunkSize = o.blob.size; + // Expose the chunk bytes position range: + o.contentRange = 'bytes ' + ub + '-' + + (ub + o.chunkSize - 1) + '/' + fs; + // Process the upload data (the blob and potential form data): + that._initXHRData(o); + // Add progress listeners for this chunk upload: + that._initProgressListener(o); + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) || + that._getXHRPromise(false, o.context)) + .done(function (result, textStatus, jqXHR) { + ub = that._getUploadedBytes(jqXHR) || + (ub + o.chunkSize); + // Create a progress event if no final progress event + // with loaded equaling total has been triggered + // for this chunk: + if (currentLoaded + o.chunkSize - o._progress.loaded) { + that._onProgress($.Event('progress', { + lengthComputable: true, + loaded: ub - o.uploadedBytes, + total: ub - o.uploadedBytes + }), o); + } + options.uploadedBytes = o.uploadedBytes = ub; + o.result = result; + o.textStatus = textStatus; + o.jqXHR = jqXHR; + that._trigger('chunkdone', null, o); + that._trigger('chunkalways', null, o); + if (ub < fs) { + // File upload not yet complete, + // continue with the next chunk: + upload(); + } else { + dfd.resolveWith( + o.context, + [result, textStatus, jqXHR] + ); + } + }) + .fail(function (jqXHR, textStatus, errorThrown) { + o.jqXHR = jqXHR; + o.textStatus = textStatus; + o.errorThrown = errorThrown; + that._trigger('chunkfail', null, o); + that._trigger('chunkalways', null, o); + dfd.rejectWith( + o.context, + [jqXHR, textStatus, errorThrown] + ); + }); + }; + this._enhancePromise(promise); + promise.abort = function () { + return jqXHR.abort(); + }; + upload(); + return promise; + }, + + _beforeSend: function (e, data) { + if (this._active === 0) { + // the start callback is triggered when an upload starts + // and no other uploads are currently running, + // equivalent to the global ajaxStart event: + this._trigger('start'); + // Set timer for global bitrate progress calculation: + this._bitrateTimer = new this._BitrateTimer(); + // Reset the global progress values: + this._progress.loaded = this._progress.total = 0; + this._progress.bitrate = 0; + } + // Make sure the container objects for the .response() and + // .progress() methods on the data object are available + // and reset to their initial state: + this._initResponseObject(data); + this._initProgressObject(data); + data._progress.loaded = data.loaded = data.uploadedBytes || 0; + data._progress.total = data.total = this._getTotal(data.files) || 1; + data._progress.bitrate = data.bitrate = 0; + this._active += 1; + // Initialize the global progress values: + this._progress.loaded += data.loaded; + this._progress.total += data.total; + }, + + _onDone: function (result, textStatus, jqXHR, options) { + var total = options._progress.total, + response = options._response; + if (options._progress.loaded < total) { + // Create a progress event if no final progress event + // with loaded equaling total has been triggered: + this._onProgress($.Event('progress', { + lengthComputable: true, + loaded: total, + total: total + }), options); + } + response.result = options.result = result; + response.textStatus = options.textStatus = textStatus; + response.jqXHR = options.jqXHR = jqXHR; + this._trigger('done', null, options); + }, + + _onFail: function (jqXHR, textStatus, errorThrown, options) { + var response = options._response; + if (options.recalculateProgress) { + // Remove the failed (error or abort) file upload from + // the global progress calculation: + this._progress.loaded -= options._progress.loaded; + this._progress.total -= options._progress.total; + } + response.jqXHR = options.jqXHR = jqXHR; + response.textStatus = options.textStatus = textStatus; + response.errorThrown = options.errorThrown = errorThrown; + this._trigger('fail', null, options); + }, + + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) { + // jqXHRorResult, textStatus and jqXHRorError are added to the + // options object via done and fail callbacks + this._trigger('always', null, options); + }, + + _onSend: function (e, data) { + if (!data.submit) { + this._addConvenienceMethods(e, data); + } + var that = this, + jqXHR, + aborted, + slot, + pipe, + options = that._getAJAXSettings(data), + send = function () { + that._sending += 1; + // Set timer for bitrate progress calculation: + options._bitrateTimer = new that._BitrateTimer(); + jqXHR = jqXHR || ( + ((aborted || that._trigger( + 'send', + $.Event('send', {delegatedEvent: e}), + options + ) === false) && + that._getXHRPromise(false, options.context, aborted)) || + that._chunkedUpload(options) || $.ajax(options) + ).done(function (result, textStatus, jqXHR) { + that._onDone(result, textStatus, jqXHR, options); + }).fail(function (jqXHR, textStatus, errorThrown) { + that._onFail(jqXHR, textStatus, errorThrown, options); + }).always(function (jqXHRorResult, textStatus, jqXHRorError) { + that._onAlways( + jqXHRorResult, + textStatus, + jqXHRorError, + options + ); + that._sending -= 1; + that._active -= 1; + if (options.limitConcurrentUploads && + options.limitConcurrentUploads > that._sending) { + // Start the next queued upload, + // that has not been aborted: + var nextSlot = that._slots.shift(); + while (nextSlot) { + if (that._getDeferredState(nextSlot) === 'pending') { + nextSlot.resolve(); + break; + } + nextSlot = that._slots.shift(); + } + } + if (that._active === 0) { + // The stop callback is triggered when all uploads have + // been completed, equivalent to the global ajaxStop event: + that._trigger('stop'); + } + }); + return jqXHR; + }; + this._beforeSend(e, options); + if (this.options.sequentialUploads || + (this.options.limitConcurrentUploads && + this.options.limitConcurrentUploads <= this._sending)) { + if (this.options.limitConcurrentUploads > 1) { + slot = $.Deferred(); + this._slots.push(slot); + pipe = slot.pipe(send); + } else { + this._sequence = this._sequence.pipe(send, send); + pipe = this._sequence; + } + // Return the piped Promise object, enhanced with an abort method, + // which is delegated to the jqXHR object of the current upload, + // and jqXHR callbacks mapped to the equivalent Promise methods: + pipe.abort = function () { + aborted = [undefined, 'abort', 'abort']; + if (!jqXHR) { + if (slot) { + slot.rejectWith(options.context, aborted); + } + return send(); + } + return jqXHR.abort(); + }; + return this._enhancePromise(pipe); + } + return send(); + }, + + _onAdd: function (e, data) { + var that = this, + result = true, + options = $.extend({}, this.options, data), + files = data.files, + filesLength = files.length, + limit = options.limitMultiFileUploads, + limitSize = options.limitMultiFileUploadSize, + overhead = options.limitMultiFileUploadSizeOverhead, + batchSize = 0, + paramName = this._getParamName(options), + paramNameSet, + paramNameSlice, + fileSet, + i, + j = 0; + if (limitSize && (!filesLength || files[0].size === undefined)) { + limitSize = undefined; + } + if (!(options.singleFileUploads || limit || limitSize) || + !this._isXHRUpload(options)) { + fileSet = [files]; + paramNameSet = [paramName]; + } else if (!(options.singleFileUploads || limitSize) && limit) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i += limit) { + fileSet.push(files.slice(i, i + limit)); + paramNameSlice = paramName.slice(i, i + limit); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + } + } else if (!options.singleFileUploads && limitSize) { + fileSet = []; + paramNameSet = []; + for (i = 0; i < filesLength; i = i + 1) { + batchSize += files[i].size + overhead; + if (i + 1 === filesLength || + ((batchSize + files[i + 1].size + overhead) > limitSize) || + (limit && i + 1 - j >= limit)) { + fileSet.push(files.slice(j, i + 1)); + paramNameSlice = paramName.slice(j, i + 1); + if (!paramNameSlice.length) { + paramNameSlice = paramName; + } + paramNameSet.push(paramNameSlice); + j = i + 1; + batchSize = 0; + } + } + } else { + paramNameSet = paramName; + } + data.originalFiles = files; + $.each(fileSet || files, function (index, element) { + var newData = $.extend({}, data); + newData.files = fileSet ? element : [element]; + newData.paramName = paramNameSet[index]; + that._initResponseObject(newData); + that._initProgressObject(newData); + that._addConvenienceMethods(e, newData); + result = that._trigger( + 'add', + $.Event('add', {delegatedEvent: e}), + newData + ); + return result; + }); + return result; + }, + + _replaceFileInput: function (data) { + var input = data.fileInput, + inputClone = input.clone(true); + // Add a reference for the new cloned file input to the data argument: + data.fileInputClone = inputClone; + $('<form></form>').append(inputClone)[0].reset(); + // Detaching allows to insert the fileInput on another form + // without loosing the file input value: + input.after(inputClone).detach(); + // Avoid memory leaks with the detached file input: + $.cleanData(input.unbind('remove')); + // Replace the original file input element in the fileInput + // elements set with the clone, which has been copied including + // event handlers: + this.options.fileInput = this.options.fileInput.map(function (i, el) { + if (el === input[0]) { + return inputClone[0]; + } + return el; + }); + // If the widget has been initialized on the file input itself, + // override this.element with the file input clone: + if (input[0] === this.element[0]) { + this.element = inputClone; + } + }, + + _handleFileTreeEntry: function (entry, path) { + var that = this, + dfd = $.Deferred(), + errorHandler = function (e) { + if (e && !e.entry) { + e.entry = entry; + } + // Since $.when returns immediately if one + // Deferred is rejected, we use resolve instead. + // This allows valid files and invalid items + // to be returned together in one set: + dfd.resolve([e]); + }, + successHandler = function (entries) { + that._handleFileTreeEntries( + entries, + path + entry.name + '/' + ).done(function (files) { + dfd.resolve(files); + }).fail(errorHandler); + }, + readEntries = function () { + dirReader.readEntries(function (results) { + if (!results.length) { + successHandler(entries); + } else { + entries = entries.concat(results); + readEntries(); + } + }, errorHandler); + }, + dirReader, entries = []; + path = path || ''; + if (entry.isFile) { + if (entry._file) { + // Workaround for Chrome bug #149735 + entry._file.relativePath = path; + dfd.resolve(entry._file); + } else { + entry.file(function (file) { + file.relativePath = path; + dfd.resolve(file); + }, errorHandler); + } + } else if (entry.isDirectory) { + dirReader = entry.createReader(); + readEntries(); + } else { + // Return an empy list for file system items + // other than files or directories: + dfd.resolve([]); + } + return dfd.promise(); + }, + + _handleFileTreeEntries: function (entries, path) { + var that = this; + return $.when.apply( + $, + $.map(entries, function (entry) { + return that._handleFileTreeEntry(entry, path); + }) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _getDroppedFiles: function (dataTransfer) { + dataTransfer = dataTransfer || {}; + var items = dataTransfer.items; + if (items && items.length && (items[0].webkitGetAsEntry || + items[0].getAsEntry)) { + return this._handleFileTreeEntries( + $.map(items, function (item) { + var entry; + if (item.webkitGetAsEntry) { + entry = item.webkitGetAsEntry(); + if (entry) { + // Workaround for Chrome bug #149735: + entry._file = item.getAsFile(); + } + return entry; + } + return item.getAsEntry(); + }) + ); + } + return $.Deferred().resolve( + $.makeArray(dataTransfer.files) + ).promise(); + }, + + _getSingleFileInputFiles: function (fileInput) { + fileInput = $(fileInput); + var entries = fileInput.prop('webkitEntries') || + fileInput.prop('entries'), + files, + value; + if (entries && entries.length) { + return this._handleFileTreeEntries(entries); + } + files = $.makeArray(fileInput.prop('files')); + if (!files.length) { + value = fileInput.prop('value'); + if (!value) { + return $.Deferred().resolve([]).promise(); + } + // If the files property is not available, the browser does not + // support the File API and we add a pseudo File object with + // the input value as name with path information removed: + files = [{name: value.replace(/^.*\\/, '')}]; + } else if (files[0].name === undefined && files[0].fileName) { + // File normalization for Safari 4 and Firefox 3: + $.each(files, function (index, file) { + file.name = file.fileName; + file.size = file.fileSize; + }); + } + return $.Deferred().resolve(files).promise(); + }, + + _getFileInputFiles: function (fileInput) { + if (!(fileInput instanceof $) || fileInput.length === 1) { + return this._getSingleFileInputFiles(fileInput); + } + return $.when.apply( + $, + $.map(fileInput, this._getSingleFileInputFiles) + ).pipe(function () { + return Array.prototype.concat.apply( + [], + arguments + ); + }); + }, + + _onChange: function (e) { + var that = this, + data = { + fileInput: $(e.target), + form: $(e.target.form) + }; + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + if (that.options.replaceFileInput) { + that._replaceFileInput(data); + } + if (that._trigger( + 'change', + $.Event('change', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + }, + + _onPaste: function (e) { + var items = e.originalEvent && e.originalEvent.clipboardData && + e.originalEvent.clipboardData.items, + data = {files: []}; + if (items && items.length) { + $.each(items, function (index, item) { + var file = item.getAsFile && item.getAsFile(); + if (file) { + data.files.push(file); + } + }); + if (this._trigger( + 'paste', + $.Event('paste', {delegatedEvent: e}), + data + ) !== false) { + this._onAdd(e, data); + } + } + }, + + _onDrop: function (e) { + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer; + var that = this, + dataTransfer = e.dataTransfer, + data = {}; + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { + e.preventDefault(); + this._getDroppedFiles(dataTransfer).always(function (files) { + data.files = files; + if (that._trigger( + 'drop', + $.Event('drop', {delegatedEvent: e}), + data + ) !== false) { + that._onAdd(e, data); + } + }); + } + }, + + _onDragOver: getDragHandler('dragover'), + + _onDragEnter: getDragHandler('dragenter'), + + _onDragLeave: getDragHandler('dragleave'), + + _initEventHandlers: function () { + if (this._isXHRUpload(this.options)) { + this._on(this.options.dropZone, { + dragover: this._onDragOver, + drop: this._onDrop, + // event.preventDefault() on dragenter is required for IE10+: + dragenter: this._onDragEnter, + // dragleave is not required, but added for completeness: + dragleave: this._onDragLeave + }); + this._on(this.options.pasteZone, { + paste: this._onPaste + }); + } + if ($.support.fileInput) { + this._on(this.options.fileInput, { + change: this._onChange + }); + } + }, + + _destroyEventHandlers: function () { + this._off(this.options.dropZone, 'dragenter dragleave dragover drop'); + this._off(this.options.pasteZone, 'paste'); + this._off(this.options.fileInput, 'change'); + }, + + _setOption: function (key, value) { + var reinit = $.inArray(key, this._specialOptions) !== -1; + if (reinit) { + this._destroyEventHandlers(); + } + this._super(key, value); + if (reinit) { + this._initSpecialOptions(); + this._initEventHandlers(); + } + }, + + _initSpecialOptions: function () { + var options = this.options; + if (options.fileInput === undefined) { + options.fileInput = this.element.is('input[type="file"]') ? + this.element : this.element.find('input[type="file"]'); + } else if (!(options.fileInput instanceof $)) { + options.fileInput = $(options.fileInput); + } + if (!(options.dropZone instanceof $)) { + options.dropZone = $(options.dropZone); + } + if (!(options.pasteZone instanceof $)) { + options.pasteZone = $(options.pasteZone); + } + }, + + _getRegExp: function (str) { + var parts = str.split('/'), + modifiers = parts.pop(); + parts.shift(); + return new RegExp(parts.join('/'), modifiers); + }, + + _isRegExpOption: function (key, value) { + return key !== 'url' && $.type(value) === 'string' && + /^\/.*\/[igm]{0,3}$/.test(value); + }, + + _initDataAttributes: function () { + var that = this, + options = this.options, + clone = $(this.element[0].cloneNode(false)); + // Initialize options set via HTML5 data-attributes: + $.each( + clone.data(), + function (key, value) { + var dataAttributeName = 'data-' + + // Convert camelCase to hyphen-ated key: + key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); + if (clone.attr(dataAttributeName)) { + if (that._isRegExpOption(key, value)) { + value = that._getRegExp(value); + } + options[key] = value; + } + } + ); + }, + + _create: function () { + this._initDataAttributes(); + this._initSpecialOptions(); + this._slots = []; + this._sequence = this._getXHRPromise(true); + this._sending = this._active = 0; + this._initProgressObject(this); + this._initEventHandlers(); + }, + + // This method is exposed to the widget API and allows to query + // the number of active uploads: + active: function () { + return this._active; + }, + + // This method is exposed to the widget API and allows to query + // the widget upload progress. + // It returns an object with loaded, total and bitrate properties + // for the running uploads: + progress: function () { + return this._progress; + }, + + // This method is exposed to the widget API and allows adding files + // using the fileupload API. The data parameter accepts an object which + // must have a files property and can contain additional options: + // .fileupload('add', {files: filesList}); + add: function (data) { + var that = this; + if (!data || this.options.disabled) { + return; + } + if (data.fileInput && !data.files) { + this._getFileInputFiles(data.fileInput).always(function (files) { + data.files = files; + that._onAdd(null, data); + }); + } else { + data.files = $.makeArray(data.files); + this._onAdd(null, data); + } + }, + + // This method is exposed to the widget API and allows sending files + // using the fileupload API. The data parameter accepts an object which + // must have a files or fileInput property and can contain additional options: + // .fileupload('send', {files: filesList}); + // The method returns a Promise object for the file upload call. + send: function (data) { + if (data && !this.options.disabled) { + if (data.fileInput && !data.files) { + var that = this, + dfd = $.Deferred(), + promise = dfd.promise(), + jqXHR, + aborted; + promise.abort = function () { + aborted = true; + if (jqXHR) { + return jqXHR.abort(); + } + dfd.reject(null, 'abort', 'abort'); + return promise; + }; + this._getFileInputFiles(data.fileInput).always( + function (files) { + if (aborted) { + return; + } + if (!files.length) { + dfd.reject(); + return; + } + data.files = files; + jqXHR = that._onSend(null, data); + jqXHR.then( + function (result, textStatus, jqXHR) { + dfd.resolve(result, textStatus, jqXHR); + }, + function (jqXHR, textStatus, errorThrown) { + dfd.reject(jqXHR, textStatus, errorThrown); + } + ); + } + ); + return this._enhancePromise(promise); + } + data.files = $.makeArray(data.files); + if (data.files.length) { + return this._onSend(null, data); + } + } + return this._getXHRPromise(false, data && data.context); + } + + }); + +})); +/* + * jQuery Iframe Transport Plugin 1.8.2 + * https://github.com/blueimp/jQuery-File-Upload + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * http://www.opensource.org/licenses/MIT + */ + +/* global define, window, document */ + +(function (factory) { + 'use strict'; + if (typeof define === 'function' && define.amd) { + // Register as an anonymous AMD module: + define(['jquery'], factory); + } else { + // Browser globals: + factory(window.jQuery); + } +}(function ($) { + 'use strict'; + + // Helper variable to create unique names for the transport iframes: + var counter = 0; + + // The iframe transport accepts four additional options: + // options.fileInput: a jQuery collection of file input fields + // options.paramName: the parameter name for the file form data, + // overrides the name property of the file input field(s), + // can be a string or an array of strings. + // options.formData: an array of objects with name and value properties, + // equivalent to the return data of .serializeArray(), e.g.: + // [{name: 'a', value: 1}, {name: 'b', value: 2}] + // options.initialIframeSrc: the URL of the initial iframe src, + // by default set to "javascript:false;" + $.ajaxTransport('iframe', function (options) { + if (options.async) { + // javascript:false as initial iframe src + // prevents warning popups on HTTPS in IE6: + /*jshint scripturl: true */ + var initialIframeSrc = options.initialIframeSrc || 'javascript:false;', + /*jshint scripturl: false */ + form, + iframe, + addParamChar; + return { + send: function (_, completeCallback) { + form = $('<form style="display:none;"></form>'); + form.attr('accept-charset', options.formAcceptCharset); + addParamChar = /\?/.test(options.url) ? '&' : '?'; + // XDomainRequest only supports GET and POST: + if (options.type === 'DELETE') { + options.url = options.url + addParamChar + '_method=DELETE'; + options.type = 'POST'; + } else if (options.type === 'PUT') { + options.url = options.url + addParamChar + '_method=PUT'; + options.type = 'POST'; + } else if (options.type === 'PATCH') { + options.url = options.url + addParamChar + '_method=PATCH'; + options.type = 'POST'; + } + // IE versions below IE8 cannot set the name property of + // elements that have already been added to the DOM, + // so we set the name along with the iframe HTML markup: + counter += 1; + iframe = $( + '<iframe src="' + initialIframeSrc + + '" name="iframe-transport-' + counter + '"></iframe>' + ).bind('load', function () { + var fileInputClones, + paramNames = $.isArray(options.paramName) ? + options.paramName : [options.paramName]; + iframe + .unbind('load') + .bind('load', function () { + var response; + // Wrap in a try/catch block to catch exceptions thrown + // when trying to access cross-domain iframe contents: + try { + response = iframe.contents(); + // Google Chrome and Firefox do not throw an + // exception when calling iframe.contents() on + // cross-domain requests, so we unify the response: + if (!response.length || !response[0].firstChild) { + throw new Error(); + } + } catch (e) { + response = undefined; + } + // The complete callback returns the + // iframe content document as response object: + completeCallback( + 200, + 'success', + {'iframe': response} + ); + // Fix for IE endless progress bar activity bug + // (happens on form submits to iframe targets): + $('<iframe src="' + initialIframeSrc + '"></iframe>') + .appendTo(form); + window.setTimeout(function () { + // Removing the form in a setTimeout call + // allows Chrome's developer tools to display + // the response result + form.remove(); + }, 0); + }); + form + .prop('target', iframe.prop('name')) + .prop('action', options.url) + .prop('method', options.type); + if (options.formData) { + $.each(options.formData, function (index, field) { + $('<input type="hidden"/>') + .prop('name', field.name) + .val(field.value) + .appendTo(form); + }); + } + if (options.fileInput && options.fileInput.length && + options.type === 'POST') { + fileInputClones = options.fileInput.clone(); + // Insert a clone for each file input field: + options.fileInput.after(function (index) { + return fileInputClones[index]; + }); + if (options.paramName) { + options.fileInput.each(function (index) { + $(this).prop( + 'name', + paramNames[index] || options.paramName + ); + }); + } + // Appending the file input fields to the hidden form + // removes them from their original location: + form + .append(options.fileInput) + .prop('enctype', 'multipart/form-data') + // enctype must be set as encoding for IE: + .prop('encoding', 'multipart/form-data'); + // Remove the HTML5 form attribute from the input(s): + options.fileInput.removeAttr('form'); + } + form.submit(); + // Insert the file input fields at their original location + // by replacing the clones with the originals: + if (fileInputClones && fileInputClones.length) { + options.fileInput.each(function (index, input) { + var clone = $(fileInputClones[index]); + // Restore the original name and form properties: + $(input) + .prop('name', clone.prop('name')) + .attr('form', clone.attr('form')); + clone.replaceWith(input); + }); + } + }); + form.append(iframe).appendTo(document.body); + }, + abort: function () { + if (iframe) { + // javascript:false as iframe src aborts the request + // and prevents warning popups on HTTPS in IE6. + // concat is used to avoid the "Script URL" JSLint error: + iframe + .unbind('load') + .prop('src', initialIframeSrc); + } + if (form) { + form.remove(); + } + } + }; + } + }); + + // The iframe transport returns the iframe content document as response. + // The following adds converters from iframe to text, json, html, xml + // and script. + // Please note that the Content-Type for JSON responses has to be text/plain + // or text/html, if the browser doesn't include application/json in the + // Accept header, else IE will show a download dialog. + // The Content-Type for XML responses on the other hand has to be always + // application/xml or text/xml, so IE properly parses the XML response. + // See also + // https://github.com/blueimp/jQuery-File-Upload/wiki/Setup#content-type-negotiation + $.ajaxSetup({ + converters: { + 'iframe text': function (iframe) { + return iframe && $(iframe[0].body).text(); + }, + 'iframe json': function (iframe) { + return iframe && $.parseJSON($(iframe[0].body).text()); + }, + 'iframe html': function (iframe) { + return iframe && $(iframe[0].body).html(); + }, + 'iframe xml': function (iframe) { + var xmlDoc = iframe && iframe[0]; + return xmlDoc && $.isXMLDoc(xmlDoc) ? xmlDoc : + $.parseXML((xmlDoc.XMLDocument && xmlDoc.XMLDocument.xml) || + $(xmlDoc.body).html()); + }, + 'iframe script': function (iframe) { + return iframe && $.globalEval($(iframe[0].body).text()); + } + } + }); + +})); diff --git a/static/footer.php b/static/footer.php index 06d4295..7c05018 100755 --- a/static/footer.php +++ b/static/footer.php @@ -1,13 +1,14 @@ </div> - <div class="footer random-bg"> + <div class="footer random-bg "> <div class="container"> - <div class="row"> + <!--div class="row"> <div class="col-md-6"> - <p class="effect">v4.1 Built with <a href="http://getbootstrap.com" title="Twitter Bootstrap" class="footer-a">Bootstrap</a>, - <a href="http://redis.io" title="Redis.io" class="footer-a">Redis</a> and <a href="http://mariadb.org" title="MariaDB.org" class="footer-a">MariaDB</a>.</p> + <p class="effect"><span class="pull-right"><span class="fa fa-copyright"></span> Copyright 2014 <a class="footer-a" href="//www.moehm.org/" target="_blank" title="www.moehm.org">Maximilian Möhring</a></span></p> </div> - <div class="col-md-6"> - <p class="effect"><span class="pull-right"><span class="fa fa-copyright"></span> Copyright 2014 <a class="footer-a" href="//www.moehm.org/" target="_blank">Maximilian Möhring</a></span></p> + </div--> + <div class="row"> + <div class="text-right"> + <p class="effect">v4.2 <span class="fa fa-copyright"> Copyright 2014 <a class="footer-a" id="copyright-text" href="//www.moehm.org/" target="_blank" title="https://www.moehm.org/">Maximilian Möhring</a></p> </div> </div> </div> @@ -15,27 +16,4 @@ <script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <script src="//code.jquery.com/ui/1.11.1/jquery-ui.min.js" defer></script> <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js" defer></script> - <?php include("static/piwik.html"); ?> - <script>function loadFancy(){ - document.getElementById("loader").style.visibility="visible"; - document.getElementById("loader-bg").style.visibility="visible"; - - var eyecancer = document.createElement("script"); - eyecancer.type = "text/javascript"; - eyecancer.src = "/static/eyecancer.min.js"; - document.getElementsByTagName("head")[0].appendChild(eyecancer); - - return false; - }</script> -<script> - $('#btn-send').click(function () { - var btn = $(this) - btn.button('loading') - $.ajax().always(function () { - }); - }); - $('.close').click(function () { - $('#btn-send').button('reset'); - }); -</script> - + <script src="/js/functions.js" defer></script> diff --git a/static/header.php b/static/header.php index 1fabb3f..69a9c4b 100644 --- a/static/header.php +++ b/static/header.php @@ -19,7 +19,8 @@ <a href="/?page=liste" title="Liste aller JG-Mitglieder"><span class="glyphicon glyphicon-th-list"></span> Adressen <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <li><a href="/?page=download&task=download&type=plain" title="Download: text/plain"><span class="glyphicon glyphicon-download"></span> Download als Text</a></li> - <li><a href="/?page=download&task=download&type=csv" title="Download: text/csv"><span class="glyphicon glyphicon-arrow-down"></span> Download als CSV</a></li> + <!--li><a href="/?page=download&task=download&type=csv" title="Download: text/csv"><span class="glyphicon glyphicon-arrow-down"></span> Download als CSV</a></li--> + <li><a href="/?page=download&task=download&type=csv" title="Download: text/csv"><span class="fa fa-download"></span> Download als CSV</a></li> </ul> </li> <li> diff --git a/static/modal-edit-gallery.php b/static/modal-edit-gallery.php index e5a2c48..d16e580 100644 --- a/static/modal-edit-gallery.php +++ b/static/modal-edit-gallery.php @@ -14,7 +14,7 @@ <div class="form-group"> <label class="col-md-4 control-label" for="name">Galerie Name</label> <div class="col-md-6"> - <input id="name" name="name" value="<?php echo htmlentities($row["name"]); ?>" placeholder="Wie heißt die neue Foto Galerie? (Pflicht)" class="form-control input-md" required="" type="text"> + <input id="name" name="name" value="<?php echo htmlentities($row["name"]); ?>" placeholder="Wie heißt die Fotogalerie? (Pflicht)" class="form-control input-md" required="" type="text"> </div> </div> diff --git a/static/modal-new-gallery.html b/static/modal-new-gallery.html index 88e0f18..3d0b3f9 100644 --- a/static/modal-new-gallery.html +++ b/static/modal-new-gallery.html @@ -14,7 +14,7 @@ <div class="form-group"> <label class="col-md-4 control-label" for="name">Galerie Name</label> <div class="col-md-6"> - <input id="name" name="name" placeholder="Wie heißt die neue Foto Galerie? (Pflicht)" class="form-control input-md" required="" type="text"> + <input id="name" name="name" placeholder="Wie heißt die neue Fotogalerie? (Pflicht)" class="form-control input-md" required="" type="text"> </div> </div> diff --git a/static/style.css b/static/style.css index 62f0c50..f6319cc 100644 --- a/static/style.css +++ b/static/style.css @@ -99,7 +99,7 @@ a { position: absolute; left: 50%; top: 50%; - background-image: url(/static/img/loading.gif); + background-image: url(/img/loading.gif); background-repeat: no-repeat; background-position: center; -webkit-background-size: cover; diff --git a/static/style.min.css b/static/style.min.css index 9fca366..138e391 100644 --- a/static/style.min.css +++ b/static/style.min.css @@ -1 +1 @@ -html{position:relative;min-height:100%}body{margin-bottom:60px}a{color:#3083D6}.navbar-default{border-color:#3083D6;background:#3083D6}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-nav>li>a{color:#fff}.footer{border-color:#3083D6;background:#3083D6;color:#fff;position:absolute;bottom:0;width:100%}.footer-a{color:#fff}.footer-a:hover{color:#fff;text-decoration:underline}.noscript{background-color:#dd5148;color:#fff}.table-center{margin:0 auto!important;float:none!important}.disabled{color:#5E5E5E;text-decoration:line-through}.random-bg,.random-font{transition:all 1500ms}.wrapper{overflow:hidden}#loader-bg{position:fixed;background-color:#fefefe;z-index:99999;height:100%;width:100%;overflow:hidden!important;visibility:hidden}#loader{width:40px;height:40px;position:absolute;left:50%;top:50%;background-image:url(/static/img/loading.gif);background-repeat:no-repeat;background-position:center;-webkit-background-size:cover;background-size:cover;margin:-20px 0 0 -20px;visibility:hidden}.fa-external-link{font-size:.5em}.a-black{color:#000}.a-restore:hover{text-decoration:none}.desc{color:#666}.font-small{font-size:.8em}.nav-tabs{margin:20px} +html{position:relative;min-height:100%}body{margin-bottom:60px}a{color:#3083D6}.navbar-default{border-color:#3083D6;background:#3083D6}.navbar-default .navbar-brand{color:#fff}.navbar-default .navbar-nav>li>a{color:#fff}.footer{border-color:#3083D6;background:#3083D6;color:#fff;position:absolute;bottom:0;width:100%}.footer-a{color:#fff}.footer-a:hover{color:#fff;text-decoration:underline}.noscript{background-color:#dd5148;color:#fff}.table-center{margin:0 auto!important;float:none!important}.disabled{color:#5E5E5E;text-decoration:line-through}.random-bg,.random-font{transition:all 1500ms}.wrapper{overflow:hidden}#loader-bg{position:fixed;background-color:#fefefe;z-index:99999;height:100%;width:100%;overflow:hidden!important;visibility:hidden}#loader{width:40px;height:40px;position:absolute;left:50%;top:50%;background-repeat:no-repeat;background-position:center;-webkit-background-size:cover;background-size:cover;margin:-20px 0 0 -20px;visibility:hidden}.fa-external-link{font-size:.5em}.a-black{color:#000}.a-restore:hover{text-decoration:none}.desc{color:#666}.font-small{font-size:.8em}.nav-tabs{margin:20px} diff --git a/static/tablesorter.css b/static/tablesorter.css new file mode 100644 index 0000000..ac9dfda --- /dev/null +++ b/static/tablesorter.css @@ -0,0 +1,38 @@ +th { + padding-left: 20px !important; +} +th:hover { + background-color: #f9f9f9; + transition: 1s; +} + +.tablesorter-default { + width: 100%; +} + +.tablesorter-default .header, +.tablesorter-default .tablesorter-header { + background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); + background-position: center left; + background-repeat: no-repeat; + cursor: pointer; + white-space: normal; + padding: 4px 20px 0px 4px; +} +.tablesorter-default thead .headerSortUp, +.tablesorter-default thead .tablesorter-headerSortUp, +.tablesorter-default thead .tablesorter-headerAsc { + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); + border-bottom: #3083D6 2px solid; +} +.tablesorter-default thead .headerSortDown, +.tablesorter-default thead .tablesorter-headerSortDown, +.tablesorter-default thead .tablesorter-headerDesc { + background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); + border-bottom: #3083D6 2px solid; +} +.tablesorter-default thead .sorter-false { + background-image: none; + cursor: default; + padding: 4px; +} diff --git a/static/tablesorter.min.css b/static/tablesorter.min.css new file mode 100644 index 0000000..e42578e --- /dev/null +++ b/static/tablesorter.min.css @@ -0,0 +1 @@ +th{padding-left:20px!important}th:hover{background-color:#f9f9f9;transition:1s}.tablesorter-default{width:100%}.tablesorter-default .header,.tablesorter-default .tablesorter-header{background-image:url(data:image/gif;base64,R0lGODlhFQAJAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==);background-position:center left;background-repeat:no-repeat;cursor:pointer;white-space:normal;padding:4px 20px 0 4px}.tablesorter-default thead .headerSortUp,.tablesorter-default thead .tablesorter-headerAsc,.tablesorter-default thead .tablesorter-headerSortUp{background-image:url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7);border-bottom:#3083D6 2px solid}.tablesorter-default thead .headerSortDown,.tablesorter-default thead .tablesorter-headerDesc,.tablesorter-default thead .tablesorter-headerSortDown{background-image:url(data:image/gif;base64,R0lGODlhFQAEAIAAACMtMP///yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7);border-bottom:#3083D6 2px solid}.tablesorter-default thead .sorter-false{background-image:none;cursor:default;padding:4px} |
