aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorroot2014-10-28 00:52:21 +0100
committerroot2014-10-28 00:52:21 +0100
commit25610c0ccb4c7c99fe0d6d82d6738dbcc40d05e3 (patch)
tree1c4fdcee0fb7b28ca330effbcc3334de3979d555
parentfe229655401abfa5aea2dc6c8830c8b9ed71aa64 (diff)
downloadjungegemeinde-25610c0ccb4c7c99fe0d6d82d6738dbcc40d05e3.tar.gz
v4.2 Sortable table + other improvements.
-rw-r--r--.gitignore2
-rw-r--r--action.php20
-rw-r--r--bootstrap.php4
-rw-r--r--class/moar.php27
-rw-r--r--functions.php48
-rw-r--r--index.php6
-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.js21
-rw-r--r--js/tablesorter.js1901
-rw-r--r--js/tablesorter.min.js5
-rw-r--r--js/upload.js3345
-rwxr-xr-xstatic/footer.php38
-rw-r--r--static/header.php3
-rw-r--r--static/modal-edit-gallery.php2
-rw-r--r--static/modal-new-gallery.html2
-rw-r--r--static/style.css2
-rw-r--r--static/style.min.css2
-rw-r--r--static/tablesorter.css38
-rw-r--r--static/tablesorter.min.css1
20 files changed, 5408 insertions, 60 deletions
diff --git a/.gitignore b/.gitignore
index 957f04e..bb25470 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,8 @@
*.swp
*.tmp
+protected/
+
setup.php
piwik.html
favicon.ico
diff --git a/action.php b/action.php
index f15f776..a4d47f1 100644
--- a/action.php
+++ b/action.php
@@ -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&amp;gallery=<?php echo htmlentities($_GET["gallery"]); ?>" role="tab"><i class="fa fa-download"></i>
+ <li><a class="download" href="/?page=action&amp;task=downloadGallery&amp;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">
diff --git a/index.php b/index.php
index 85f22c7..ba776d9 100644
--- a/index.php
+++ b/index.php
@@ -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('&nbsp;');
+ 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&ouml;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&ouml;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&ouml;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}