diff --git a/CHANGELOG.markdown b/CHANGELOG.markdown deleted file mode 100644 index 9b05922..0000000 --- a/CHANGELOG.markdown +++ /dev/null @@ -1,74 +0,0 @@ -Changelog -=== -#### v1.4.1 - -* Upgrade tablesorter to v2.7.5 - -#### v1.4.0 - -* Upgrade tablesorter to v2.7.3 - -#### v1.3.0 - -* Upgrade tablesorter to v2.6.2 - -#### V1.2.0 - -* Upgrade tablesorter to v2.5.2 - -#### V1.1.0 - -* Upgrade tablesorter to v2.4.6 - -#### v1.0.5 - -* Upgrade tablesorter to V2.3.11 - -#### v1.0.4 - -* Upgrade tablesorter to V2.3.10 - -#### v1.0.3 - -* Fixes #3 pager is gone after upgrade to [Mottie's fork of tablesorter] - -#### V1.0.2 - -* Upgrade tablesorter to V2.3.8 - -#### V1.0.1 - -* Upgrade tablesorter to V2.3.7 - -#### V1.0.0 - -* BIG CHANGE: Use [Mottie's fork of tablesorter], V2.3.4 - -#### v0.0.5 - -* FIX: now require pager plugin as default. -* FIX: move assets files from app to vendor & cleanup. -* FIX: remove dependency on `jquery-rails` -* FIX: remove development dependency on `sqlite3` - -#### v0.0.4 - -* FIX: update gemspec to be compatible with Rails 3.2, Thanks to [derekprior]. - -#### v0.0.3 - -* NEW: added pagenation plugin, use `require jquery-tablesorter/pager` to require -* FIX: use `require jquery-tablesorter` instead of `require jquery-tablesorter/jquery-tablesorter`, the old way to require will still works, but will be removed in future. - -#### v0.0.2 - -* FIX: test issues. - -#### v0.0.1 - -* NEW: added jquery-tablesorter plugin, use `require jquery-tablesorter/jquery-tablesorter` to require javascript and `require jquery-tablesorter/` to require stylesheet. - - -[Mottie's fork of tablesorter]: https://github.com/Mottie/tablesorter -[derekprior]: https://github.com/derekprior - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7031e0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,569 @@ +Changelog +=== + +#### v1.27.2 (2020-04-16) + +* Upgrade tablesorter to v2.31.3 +* Update Readme +* Update copyright + +#### v1.27.1 (2019-10-07) + +* Fix installing in Rails < 6, remove railties limit constraint for now (fix github#10) + +#### v1.27.0 (2019-08-18) + +* Bump rails dependency to support Rails 6.0.0 +* Add some metadata to gemspec +* Update Readme +* Update copyright + +#### v1.26.1 (2018-11-22) + +* Upgrade tablesorter to v2.31.1 + +#### v1.26.0 (2018-09-03) + +* Upgrade tablesorter to v2.31.0 + +#### v1.25.5 (2018-08-20) + +* Upgrade tablesorter to v2.30.7 + +#### v1.25.4 (2018-06-17) + +* Upgrade tablesorter to v2.30.6 + +#### v1.25.3 (2018-06-01) + +* Upgrade tablesorter to v2.30.5 + +#### v1.25.2 (2018-05-21) + +* Upgrade tablesorter to v2.30.4 + +#### v1.25.1 (2018-04-01) + +* Upgrade tablesorter to v2.30.3 + +#### v1.25.0 (2018-03-22) + +* Upgrade tablesorter to v2.30.1 + +#### v1.24.5 (2018-02-25) + +* Upgrade tablesorter to v2.29.6 + +#### v1.24.4 (2018-01-31) + +* Upgrade tablesorter to v2.29.5 + +#### v1.24.3 (2018-01-19) + +* Upgrade tablesorter to v2.29.4 + +#### v1.24.2 (2018-01-11) + +* Upgrade tablesorter to v2.29.3 + +#### v1.24.1 (2017-12-15) + +* Upgrade tablesorter to v2.29.2 + +#### v1.24.0 (2017-10-09) + +* Upgrade tablesorter to v2.29.0 + +#### v1.23.15 (2017-07-09) + +* Hotfix for adding v2.28.14 instead of v2.28.15 asset data in previous release + +#### v1.23.14 (2017-07-09) + +* Upgrade tablesorter to v2.28.15 + +#### v1.23.13 (2017-06-06) + +* Upgrade tablesorter to v2.28.13 + +#### v1.23.12 (2017-05-29) + +* Upgrade tablesorter to v2.28.12 + +#### v1.23.11 (2017-05-25) + +* Upgrade tablesorter to v2.28.11 + +#### v1.23.10 (2017-05-17) + +* Upgrade tablesorter to v2.28.10 + +#### v1.23.9 (2017-05-05) + +* Upgrade tablesorter to v2.28.9 + +#### v1.23.8 (2017-04-23) + +* Upgrade tablesorter to v2.28.8 +* Readme: Updated ruby version + +#### v1.23.7 (2017-04-09) + +* Upgrade tablesorter to v2.28.7 + +#### v1.23.6 (2017-04-02) + +* Upgrade tablesorter to v2.28.6 + +#### v1.23.5 (2017-01-10) + +* Upgrade tablesorter to v2.28.5 + +#### v1.23.4 (2017-01-08) + +* Upgrade tablesorter to v2.28.4 + +#### v1.23.3 (2016-12-19) + +* Upgrade tablesorter to v2.28.3 + +#### v1.23.2 (2016-12-09) + +* Upgrade tablesorter to v2.28.1 + +#### v1.23.1 (2016-11-29) + +* Add actual files of v2.28.0 (facepalm!) + +#### v1.23.0 (2016-11-28) + +* Upgrade tablesorter to v2.28.0 + +#### v1.22.7 (2016-09-29) + +* Upgrade tablesorter to v2.27.8 + +#### v1.22.6 (2016-09-28) + +* Include beta-testing files from tablesorter + +#### v1.22.5 (2016-09-23) + +* Upgrade tablesorter to v2.27.7 + +#### v1.22.4 (2016-09-01) + +* Upgrade tablesorter to v2.27.6 + +#### v1.22.3 (2016-08-22) + +* Upgrade tablesorter to v2.27.5 + +#### v1.22.2 (2016-08-17) + +* Upgrade tablesorter to v2.27.3 + +#### v1.22.1 (2016-08-02) + +* Upgrade tablesorter to v2.27.2 + +#### v1.22.0 (2016-08-01) + +* Upgrade tablesorter to v2.27.1 + +#### v1.21.4 (2016-07-12) + +* Upgrade tablesorter to v2.26.6 + +#### v1.21.3 (2016-06-28) + +* Upgrade tablesorter to v2.26.5 + +#### v1.21.2 (2016-06-16) + +* Upgrade tablesorter to v2.26.4 + +#### v1.21.1 (2016-05-18) + +* Upgrade tablesorter to v2.26.1 + +#### v1.21.0 (2016-05-08) + +* Upgrade tablesorter to v2.26.0 + +#### v1.20.8 (2016-04-13) + +* Upgrade tablesorter to v2.25.8 + +#### v1.20.7 (2016-04-01) + +* Upgrade tablesorter to v2.25.7 + +#### v1.20.6 (2016-03-21) + +* Upgrade tablesorter to v2.25.6 + +#### v1.20.5 (2016-03-02) + +* Upgrade tablesorter to v2.25.5 + +#### v1.20.4 (2016-02-18) + +* Upgrade tablesorter to v2.25.4 + +#### v1.20.3 (2016-01-22) + +* Upgrade tablesorter to v2.25.3 + +#### v1.20.2 (2016-01-15) + +* Upgrade tablesorter to v2.25.2 + +#### v1.20.1 (2016-01-11) + +* Upgrade tablesorter to v2.25.1 +* Bump railties dependency to support v5.x + +#### v1.20.0 (2015-12-14) + +* Upgrade tablesorter to v2.25.0 + +#### v1.19.4 (2015-11-23) + +* Upgrade tablesorter to v2.24.6 + +#### v1.19.3 (2015-11-12) + +* Upgrade tablesorter to v2.24.5 + +#### v1.19.2 (2015-11-11) + +* Upgrade tablesorter to v2.24.4 + +#### v1.19.1 (2015-11-06) + +* Upgrade tablesorter to v2.24.3 + +#### v1.19.0 (2015-11-03) + +* Upgrade tablesorter to v2.24.2 + +#### v1.18.5 (2015-10-04) + +* Upgrade tablesorter to v2.23.5 + +#### v1.18.4 (2015-09-23) + +* Upgrade tablesorter to v2.23.4 + +#### v1.18.3 (2015-09-01) + +* Upgrade tablesorter to v2.23.3 + +#### v1.18.2 (2015-08-24) + +* Upgrade tablesorter to v2.23.2 + +#### v1.18.1 (2015-08-20) + +* Upgrade tablesorter to v2.23.1 + +#### v1.18.0 (2015-08-18) + +* Upgrade tablesorter to v2.23.0 + +#### v1.17.4 (2015-07-29) + +* Upgrade tablesorter to v2.22.5 + +#### v1.17.3 (2015-07-28) + +* Upgrade tablesorter to v2.22.4 + +#### v1.17.2 (2015-07-01) + +* Upgrade tablesorter to v2.22.3 + +#### v1.17.1 (2015-05-18) + +* Upgrade tablesorter to v2.22.1 + +#### v1.17.0 (2015-05-17) + +* Upgrade tablesorter to v2.22.0 + +#### v1.16.4 (2015-04-08) + +* Upgrade tablesorter to v2.21.5 + +#### v1.16.3 (2015-03-30) + +* Upgrade tablesorter to v2.21.4 +* Readme: Clarified how to require JS files + +#### v1.16.2 (2015-03-26) + +* Upgrade tablesorter to v2.21.3 + +#### v1.16.2 (2015-03-15) + +* Upgrade tablesorter to v2.21.2 + +#### v1.16.1 (2015-03-10) + +* Upgrade tablesorter to v2.21.1 + +#### v1.16.0 (2015-03-05) + +* Upgrade tablesorter to v2.21.0 + +#### v1.15.0 (2015-02-21) + +* Upgrade tablesorter to v2.20.1 +* Increased minimum required railties version to 3.2 + +#### v1.14.1 (2015-02-10) + +* Upgrade tablesorter to v2.19.1 + +#### v1.14.0 (2015-02-07) + +* Upgrade tablesorter to v2.19.0 + +#### v1.13.4 (2014-12-23) + +* Upgrade tablesorter to v2.18.4 + +#### v1.13.3 (2014-11-10) + +* Upgrade tablesorter to v2.18.3 + +#### v1.13.2 (2014-11-04) + +* Upgrade tablesorter to v2.18.2 + +#### v1.13.1 (2014-11-03) + +* Upgrade tablesorter to v2.18.1 + +#### v1.13.0 (2014-10-27) + +* Upgrade tablesorter to v2.18.0 + +#### v1.12.8 (2014-09-17) + +* Upgrade tablesorter to v2.17.8 +* Set required Ruby version to >= 1.9.3 + +#### v1.12.7 (2014-08-10) + +* Upgrade tablesorter to v2.17.7 + +#### v1.12.6 (2014-08-03) + +* Upgrade tablesorter to v2.17.6 + +#### v1.12.5 (2014-07-20) + +* Upgrade tablesorter to v2.17.5 + +#### v1.12.4 (2014-07-06) + +* Upgrade tablesorter to v2.17.4 + +#### v1.12.3 (2014-06-30) + +* Upgrade tablesorter to v2.17.3 + +#### v1.12.2 (2014-06-19) + +* Upgrade tablesorter to v2.17.2 + +#### v1.12.1 (2014-05-29) + +* Upgrade tablesorter to v2.17.1 + +#### v1.12.0 (2014-05-22) + +* Upgrade tablesorter to v2.17.0 + +#### v1.11.2 (2014-05-01) + +* Upgrade tablesorter to v2.16.3 + +#### v1.11.1 (2014-04-28) + +* Upgrade tablesorter to v2.16.2 + +#### v1.11.0 (2014-04-24) + +* Upgrade tablesorter to v2.16.1 + +#### v1.10.10 (2014-04-10) + +* Upgrade tablesorter to v2.15.14 + +#### v1.10.9 (2014-04-03) + +* Upgrade tablesorter to v2.15.13 + +#### v1.10.8 (2014-03-31) + +* Upgrade tablesorter to v2.15.12 +* Remove all vendor files before updating from tablesorter repository +** Removed unused images +* Some minor code changes and optimizations, updated Readme + +#### v1.10.7 (2014-03-11) + +* Upgrade tablesorter to v2.15.11 + +#### v1.10.6 (2014-03-11) + +* Upgrade tablesorter to v2.15.10 + +#### v1.10.5 (2014-03-11) + +* Upgrade tablesorter to v2.15.7 + +#### v1.10.4 (2014-03-08) + +* Upgrade tablesorter to v2.15.6 + +#### v1.10.3 (2014-02-23) + +* Upgrade tablesorter to v2.15.5 +* FIX: Added accidentally missing parsers and widgets to gem +* Minor structural code changes in Rakefile, updated Readme + +#### v1.10.2 (2014-02-22) + +* Upgrade tablesorter to v2.15.4 + +#### v1.10.1 (2014-02-20) + +* Upgrade tablesorter to v2.15.1 + +#### v1.10.0 (2014-02-19) + +* Upgrade tablesorter to v2.15.0 +* Updated copyright year, license information and added note about Bootstrap 2 theme in Readme + +#### v1.9.5 (2013-12-17) + +* Upgrade tablesorter to v2.14.5 + +#### v1.9.4 (2013-12-15) + +* Upgrade tablesorter to v2.14.4 + +#### v1.9.3 (2013-12-04) + +* Upgrade tablesorter to v2.14.3 + +#### v1.9.2 (2013-11-25) + +* Upgrade tablesorter to v2.14.2 + +#### v1.9.1 (2013-11-23) + +* Upgrade tablesorter to v2.14.1 + +#### v1.9.0 (2013-11-20) + +* Upgrade tablesorter to v2.14.0 + +#### v1.8.1 (2013-11-10) + +* Upgrade tablesorter to v2.13.3 + +#### v1.8.0 + +* Upgrade tablesorter to v2.13.2 + +#### v1.7.0 + +* Upgrade tablesorter to v2.12 + +#### v1.6.0 + +* Upgrade tablesorter to v2.11.1 + +#### v1.5.0 + +* Upgrade tablesorter to v2.10.8 +* Rails 4 compatibility +* Gem is now maintained by Erik-B. Ernst (@themilkman). Special thanks to Jun Lin (@linjunpop) for his work! + +#### v1.4.1 + +* Upgrade tablesorter to v2.7.5 + +#### v1.4.0 + +* Upgrade tablesorter to v2.7.3 + +#### v1.3.0 + +* Upgrade tablesorter to v2.6.2 + +#### V1.2.0 + +* Upgrade tablesorter to v2.5.2 + +#### V1.1.0 + +* Upgrade tablesorter to v2.4.6 + +#### v1.0.5 + +* Upgrade tablesorter to V2.3.11 + +#### v1.0.4 + +* Upgrade tablesorter to V2.3.10 + +#### v1.0.3 + +* Fixes #3 pager is gone after upgrade to [Mottie's fork of tablesorter] + +#### V1.0.2 + +* Upgrade tablesorter to V2.3.8 + +#### V1.0.1 + +* Upgrade tablesorter to V2.3.7 + +#### V1.0.0 + +* BIG CHANGE: Use [Mottie's fork of tablesorter], V2.3.4 + +#### v0.0.5 + +* FIX: now require pager plugin as default. +* FIX: move assets files from app to vendor & cleanup. +* FIX: remove dependency on `jquery-rails` +* FIX: remove development dependency on `sqlite3` + +#### v0.0.4 + +* FIX: update gemspec to be compatible with Rails 3.2, Thanks to [derekprior]. + +#### v0.0.3 + +* NEW: added pagenation plugin, use `require jquery-tablesorter/pager` to require +* FIX: use `require jquery-tablesorter` instead of `require jquery-tablesorter/jquery-tablesorter`, the old way to require will still works, but will be removed in future. + +#### v0.0.2 + +* FIX: test issues. + +#### v0.0.1 + +* NEW: added jquery-tablesorter plugin, use `require jquery-tablesorter/jquery-tablesorter` to require javascript and `require jquery-tablesorter/` to require stylesheet. + + +[Mottie's fork of tablesorter]: https://github.com/Mottie/tablesorter +[derekprior]: https://github.com/derekprior + diff --git a/MIT-LICENSE b/MIT-LICENSE index ae244e8..f7e7530 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright 2013 Jun Lin +Copyright 2020 Jun Lin, Erik-B. Ernst Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.markdown b/README.md similarity index 50% rename from README.markdown rename to README.md index 791e183..20370e0 100644 --- a/README.markdown +++ b/README.md @@ -1,14 +1,12 @@ -# jQuery Table Sorter plugin for Rails +# jQuery tablesorter plugin for Rails -[![Still Maintained](http://stillmaintained.com/linjunpop/jquery-tablesorter-rails.png)](http://stillmaintained.com/linjunpop/jquery-tablesorter-rails) [![Gem Version](https://badge.fury.io/rb/jquery-tablesorter.png)](http://badge.fury.io/rb/jquery-tablesorter) -[![endorse](http://api.coderwall.com/linjunpop/endorsecount.png)](http://coderwall.com/linjunpop) -Simple integration of jquery-tablesorter into the asset pipeline. +Simple integration of jQuery tablesorter ([Mottie's fork]) into the asset pipeline. -Current tablesorter version: 2.7.5 (2/5/2013), [documentation] +Current tablesorter version: 2.31.3 (2020-03-03) [documentation] -Any issue associate with the js/css files, please report to [Mottie's fork]. +Any issue associated with the js/css files, please report to [Mottie's fork]. ## Installation @@ -26,7 +24,8 @@ Or install it yourself as: ## Requirements -Rails 3.1 and higher +It should work with Rails 3.2 and higher as well as with ruby 1.9.3 - 2.7.x. +Each release is always tested with the latest version of both. ## Usage @@ -38,7 +37,7 @@ In your `application.js` //= require jquery-tablesorter ``` -This will require all jquery-tablesorter files (exclude addons). +This will require all core jquery-tablesorter files. Please note: This loads only the core-widgets and will neither include the extracted widgets nor any files from the addons and extras directories. Those files must be required manually as shown below. Or you can include single file with: @@ -47,8 +46,14 @@ Or you can include single file with: //= require jquery-tablesorter/jquery.tablesorter //= require jquery-tablesorter/jquery.tablesorter.widgets //= require jquery-tablesorter/addons/pager/jquery.tablesorter.pager +//= require jquery-tablesorter/widgets/widget-repeatheaders +//= require jquery-tablesorter/parsers/parser-metric +//= require jquery-tablesorter/extras/jquery.quicksearch +//= require jquery-tablesorter/beta-testing/pager-custom-controls ``` +Please note that files in the beta-testing directory might move into the stable tablesorter or get renamed with upcoming tablesorter releases and thus could have to be required in an updated way after upgrading this gem. + ### Stylesheet files In your `application.css` @@ -64,15 +69,17 @@ Avaliable theme names: * theme.black-ice * theme.blue * theme.bootstrap +* theme.bootstrap_2 * theme.dark * theme.default * theme.dropbox * theme.green * theme.grey -* theme.ice +* theme.ice.css (file extension required due to sprockets, see [issue #3](https://github.com/themilkman/jquery-tablesorter-rails/issues/3)) * theme.jui +* theme.metro-dark -pager theme: +Pager theme: ```css /* @@ -94,8 +101,12 @@ pager theme: 2. Run `rake jquery_tablesorter:update` 3. Run `rake jquery_tablesorter:sanitize_image_paths` 4. Update `README.md` and `CHANGELOG.md` - + + +### Licensing + +* Licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) license. +* Original jquery-tablesorter code is dual licensed under the [MIT](http://www.opensource.org/licenses/mit-license.php) and [GPL](http://www.gnu.org/licenses/gpl.html) licenses (see [Mottie's fork]). [Mottie's fork]: https://github.com/Mottie/tablesorter [documentation]: http://mottie.github.com/tablesorter/docs/index.html - diff --git a/Rakefile b/Rakefile index 6621e31..2a3babe 100755 --- a/Rakefile +++ b/Rakefile @@ -4,58 +4,68 @@ require 'bundler' Bundler::GemHelper.install_tasks namespace :jquery_tablesorter do - desc 'update tablesorter' + + desc 'Update tablesorter files' task :update do + # javascripts # - javascript_dir = 'vendor/assets/javascripts/jquery-tablesorter' - FileUtils.mkdir_p(javascript_dir) - Dir.glob('tablesorter/js/*.js').reject{ |file| file =~ /.min.js\Z/}.each do |file| - FileUtils.cp file, javascript_dir, :verbose => true - end + javascript_dir = File.join('vendor', 'assets', 'javascripts', 'jquery-tablesorter') + copy_files(Dir.glob(File.join('tablesorter', 'js', '*.js')).reject{|file| file =~ /.min.js\Z/}, javascript_dir) # stylesheets # - stylesheet_dir = 'vendor/assets/stylesheets/jquery-tablesorter' - FileUtils.mkdir_p(stylesheet_dir) - Dir.glob('tablesorter/css/*.css').each do |file| - FileUtils.cp file, stylesheet_dir, :verbose => true - end + stylesheet_dir = File.join('vendor', 'assets', 'stylesheets', 'jquery-tablesorter') + copy_files(Dir.glob(File.join('tablesorter', 'css', '*.css')), stylesheet_dir) # images # - images_dir = 'vendor/assets/images/jquery-tablesorter' - FileUtils.mkdir_p(images_dir) - Dir.glob('tablesorter/css/images/*').each do |file| - FileUtils.cp file, images_dir, :verbose => true - end + images_dir = File.join('vendor', 'assets', 'images', 'jquery-tablesorter') + copy_files(Dir.glob(File.join('tablesorter', 'css', 'images', '*')), images_dir) # addons # ## pager - pager_stylesheet_dir = stylesheet_dir + '/addons/pager' - FileUtils.mkdir_p(pager_stylesheet_dir) - FileUtils.cp 'tablesorter/addons/pager/jquery.tablesorter.pager.css', - pager_stylesheet_dir, - :verbose => true - - pager_javascript_dir = javascript_dir + '/addons/pager' - FileUtils.mkdir_p(pager_javascript_dir) - FileUtils.cp 'tablesorter/addons/pager/jquery.tablesorter.pager.js', - pager_javascript_dir, - :verbose => true - - pager_images_dir = images_dir + '/addons/pager' - FileUtils.mkdir_p(pager_images_dir) - FileUtils.cp_r 'tablesorter/addons/pager/icons', pager_images_dir, - :verbose => true + pager_stylesheet_dir = File.join(stylesheet_dir, 'addons', 'pager') + copy_files([File.join('tablesorter', 'addons', 'pager', 'jquery.tablesorter.pager.css')], pager_stylesheet_dir) + + pager_javascript_dir = File.join(javascript_dir, 'addons', 'pager') + copy_files([File.join('tablesorter', 'addons', 'pager', 'jquery.tablesorter.pager.js')], pager_javascript_dir) + + pager_images_dir = File.join(images_dir, 'addons', 'pager', 'icons') + copy_files(Dir.glob(File.join('tablesorter', 'addons', 'pager', 'icons', '*')), pager_images_dir) + + + # parsers, widgets and extras + # + %w(parsers widgets extras).each do |folder| + folder_javascript_dir = File.join(javascript_dir, folder) + files = Dir.glob(File.join('tablesorter', 'js', folder, '*.js')).reject{|file| file =~ /.min.js\Z/} + copy_files(files, folder_javascript_dir) + end + + # beta-testing + # + beta_dir = File.join(javascript_dir, 'beta-testing') + beta_files = Dir.glob(File.join('tablesorter', 'beta-testing', '*.js')).reject{|file| file =~ /.min.js\Z/} + copy_files(beta_files, beta_dir) + + end + + def copy_files(files, target_dir) + FileUtils.mkdir_p(target_dir) + FileUtils.rm_rf("#{target_dir}/.", :secure => true) + + files.each do |file| + FileUtils.cp(file, target_dir, :verbose => true) + end end desc 'Sanitize image paths' task :sanitize_image_paths do - Dir.glob('vendor/assets/stylesheets/jquery-tablesorter/*.css').each do |file_path| - content = File.read(file_path).gsub(/url\(images\//, "url(/assets/jquery-tablesorter/") - File.open(file_path, "w") {|file| file.write content} + Dir.glob(File.join('vendor', 'assets', 'stylesheets', 'jquery-tablesorter', '*.css')).each do |file_path| + content = File.read(file_path).gsub(/url\(images\//, 'url(/assets/jquery-tablesorter/') + File.open(file_path, 'w') {|file| file.write content} end end end diff --git a/jquery-tablesorter.gemspec b/jquery-tablesorter.gemspec index b41d2d4..0a1761d 100644 --- a/jquery-tablesorter.gemspec +++ b/jquery-tablesorter.gemspec @@ -1,19 +1,26 @@ -$:.push File.expand_path("../lib", __FILE__) +$:.push File.expand_path('../lib', __FILE__) # Maintain your gem's version: -require "jquery-tablesorter/version" +require 'jquery-tablesorter/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "jquery-tablesorter" + s.name = 'jquery-tablesorter' s.version = JqueryTablesorter::VERSION - s.authors = ["Jun Lin"] - s.email = ["linjunpop@gmail.com"] - s.homepage = "https://github.com/linjunpop/jquery-tablesorter-rails" - s.summary = "Simple integration of jquery-tablesorter into the asset pipeline." - s.description = "Simple integration of jquery-tablesorter into the asset pipeline." + s.authors = ['Jun Lin', 'Erik-B. Ernst'] + s.email = ['github@black-milk.de'] + s.homepage = 'https://github.com/themilkman/jquery-tablesorter-rails' + s.summary = "Simple integration of jquery-tablesorter (Mottie's fork) into the Rails asset pipeline." + s.description = "Simple integration of jquery-tablesorter (Mottie's fork) into the Rails asset pipeline." + s.license = 'MIT' + s.metadata = { + 'bug_tracker_uri' => 'https://github.com/themilkman/jquery-tablesorter-rails/issues', + 'changelog_uri' => 'https://github.com/themilkman/jquery-tablesorter-rails/blob/master/CHANGELOG.md', + 'source_code_uri' => 'https://github.com/themilkman/jquery-tablesorter-rails' + } - s.files = Dir["{vendor,lib}/**/*"] + ["MIT-LICENSE", "Rakefile", "README.markdown"] + s.files = Dir['{vendor,lib}/**/*'] + %w[MIT-LICENSE Rakefile README.md] - s.add_dependency "railties", ">= 3.1, < 5" + s.required_ruby_version = '>= 1.9.3' + s.add_dependency 'railties', '>= 3.2' end diff --git a/lib/jquery-tablesorter.rb b/lib/jquery-tablesorter.rb index 8edba7a..57b3680 100644 --- a/lib/jquery-tablesorter.rb +++ b/lib/jquery-tablesorter.rb @@ -1,4 +1,4 @@ -require "jquery-tablesorter/engine" +require 'jquery-tablesorter/engine' module JqueryTablesorter end diff --git a/lib/jquery-tablesorter/version.rb b/lib/jquery-tablesorter/version.rb index 6e5f231..2e88c77 100644 --- a/lib/jquery-tablesorter/version.rb +++ b/lib/jquery-tablesorter/version.rb @@ -1,3 +1,7 @@ module JqueryTablesorter - VERSION = "1.4.1" + MAJOR = 1 + MINOR = 27 + TINY = 2 + + VERSION = [MAJOR, MINOR, TINY].compact.join('.') end diff --git a/tablesorter b/tablesorter index 197960a..7202d5f 160000 --- a/tablesorter +++ b/tablesorter @@ -1 +1 @@ -Subproject commit 197960a791fc8463f1c55038d5bb152ba3af87c0 +Subproject commit 7202d5faf8105a5ecd1a2b7a653777618713ffe5 diff --git a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/first.png b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/first.png index 6f11fcb..7e505d6 100644 Binary files a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/first.png and b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/first.png differ diff --git a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/last.png b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/last.png index 7207935..41e248c 100644 Binary files a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/last.png and b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/last.png differ diff --git a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/next.png b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/next.png index 4a2f9d4..aebf14d 100644 Binary files a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/next.png and b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/next.png differ diff --git a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/prev.png b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/prev.png index 15d1584..7d1d049 100644 Binary files a/vendor/assets/images/jquery-tablesorter/addons/pager/icons/prev.png and b/vendor/assets/images/jquery-tablesorter/addons/pager/icons/prev.png differ diff --git a/vendor/assets/images/jquery-tablesorter/bootstrap-black-unsorted.png b/vendor/assets/images/jquery-tablesorter/bootstrap-black-unsorted.png new file mode 100644 index 0000000..3190f29 Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/bootstrap-black-unsorted.png differ diff --git a/vendor/assets/images/jquery-tablesorter/bootstrap-white-unsorted.png b/vendor/assets/images/jquery-tablesorter/bootstrap-white-unsorted.png new file mode 100644 index 0000000..368c66d Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/bootstrap-white-unsorted.png differ diff --git a/vendor/assets/images/jquery-tablesorter/dragtable-handle.png b/vendor/assets/images/jquery-tablesorter/dragtable-handle.png new file mode 100644 index 0000000..52a1a56 Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/dragtable-handle.png differ diff --git a/vendor/assets/images/jquery-tablesorter/dragtable-handle.svg b/vendor/assets/images/jquery-tablesorter/dragtable-handle.svg new file mode 100644 index 0000000..041ec1d --- /dev/null +++ b/vendor/assets/images/jquery-tablesorter/dragtable-handle.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-asc-hovered.png b/vendor/assets/images/jquery-tablesorter/dropbox-asc-hovered.png index bc45223..4e625e0 100644 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-asc-hovered.png and b/vendor/assets/images/jquery-tablesorter/dropbox-asc-hovered.png differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-asc.png b/vendor/assets/images/jquery-tablesorter/dropbox-asc.png index 0d6ee15..7b6615b 100644 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-asc.png and b/vendor/assets/images/jquery-tablesorter/dropbox-asc.png differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-asc1.png b/vendor/assets/images/jquery-tablesorter/dropbox-asc1.png deleted file mode 100644 index 0bce65a..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-asc1.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-asc2.png b/vendor/assets/images/jquery-tablesorter/dropbox-asc2.png deleted file mode 100644 index 4930942..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-asc2.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-desc-hovered.png b/vendor/assets/images/jquery-tablesorter/dropbox-desc-hovered.png index 4930942..806707d 100644 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-desc-hovered.png and b/vendor/assets/images/jquery-tablesorter/dropbox-desc-hovered.png differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-desc.png b/vendor/assets/images/jquery-tablesorter/dropbox-desc.png index 0bce65a..868a37c 100644 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-desc.png and b/vendor/assets/images/jquery-tablesorter/dropbox-desc.png differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-desc1.png b/vendor/assets/images/jquery-tablesorter/dropbox-desc1.png deleted file mode 100644 index 0d6ee15..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-desc1.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/dropbox-desc2.png b/vendor/assets/images/jquery-tablesorter/dropbox-desc2.png deleted file mode 100644 index bc45223..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/dropbox-desc2.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/green-asc.png b/vendor/assets/images/jquery-tablesorter/green-asc.png deleted file mode 100644 index 1367ba9..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/green-asc.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/green-desc.png b/vendor/assets/images/jquery-tablesorter/green-desc.png deleted file mode 100644 index d32a3aa..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/green-desc.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/green-header.png b/vendor/assets/images/jquery-tablesorter/green-header.png deleted file mode 100644 index ccf62ac..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/green-header.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/green-unsorted.png b/vendor/assets/images/jquery-tablesorter/green-unsorted.png deleted file mode 100644 index 0afbf71..0000000 Binary files a/vendor/assets/images/jquery-tablesorter/green-unsorted.png and /dev/null differ diff --git a/vendor/assets/images/jquery-tablesorter/metro-black-asc.png b/vendor/assets/images/jquery-tablesorter/metro-black-asc.png new file mode 100644 index 0000000..61c4f80 Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/metro-black-asc.png differ diff --git a/vendor/assets/images/jquery-tablesorter/metro-black-desc.png b/vendor/assets/images/jquery-tablesorter/metro-black-desc.png new file mode 100644 index 0000000..fc2188c Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/metro-black-desc.png differ diff --git a/vendor/assets/images/jquery-tablesorter/metro-loading.gif b/vendor/assets/images/jquery-tablesorter/metro-loading.gif new file mode 100644 index 0000000..ae274c6 Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/metro-loading.gif differ diff --git a/vendor/assets/images/jquery-tablesorter/metro-unsorted.png b/vendor/assets/images/jquery-tablesorter/metro-unsorted.png new file mode 100644 index 0000000..e67ab2a Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/metro-unsorted.png differ diff --git a/vendor/assets/images/jquery-tablesorter/metro-white-asc.png b/vendor/assets/images/jquery-tablesorter/metro-white-asc.png new file mode 100644 index 0000000..a850fbf Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/metro-white-asc.png differ diff --git a/vendor/assets/images/jquery-tablesorter/metro-white-desc.png b/vendor/assets/images/jquery-tablesorter/metro-white-desc.png new file mode 100644 index 0000000..fc05607 Binary files /dev/null and b/vendor/assets/images/jquery-tablesorter/metro-white-desc.png differ diff --git a/vendor/assets/javascripts/jquery-tablesorter.js b/vendor/assets/javascripts/jquery-tablesorter.js index 6c6d4ec..42c6aa8 100644 --- a/vendor/assets/javascripts/jquery-tablesorter.js +++ b/vendor/assets/javascripts/jquery-tablesorter.js @@ -1,3 +1,3 @@ -//= require jquery-tablesorter/jquery.metadata +//= require jquery-tablesorter/extras/jquery.metadata //= require jquery-tablesorter/jquery.tablesorter //= require jquery-tablesorter/jquery.tablesorter.widgets \ No newline at end of file diff --git a/vendor/assets/javascripts/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.js b/vendor/assets/javascripts/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.js index 8d86772..1d67c1b 100644 --- a/vendor/assets/javascripts/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.js +++ b/vendor/assets/javascripts/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.js @@ -1,570 +1,1265 @@ /*! - * tablesorter pager plugin - * updated 1/29/2013 - */ +* tablesorter (FORK) pager plugin +* updated 2020-03-03 (v2.31.3) +*/ /*jshint browser:true, jquery:true, unused:false */ ;(function($) { - "use strict"; + 'use strict'; /*jshint supernew:true */ - $.extend({ tablesorterPager: new function() { - - this.defaults = { - // target the pager markup - container: null, - - // use this format: "http://mydatabase.com?page={page}&size={size}&{sortList:col}&{filterList:fcol}" - // where {page} is replaced by the page number, {size} is replaced by the number of records to show, - // {sortList:col} adds the sortList to the url into a "col" array, and {filterList:fcol} adds - // the filterList to the url into an "fcol" array. - // So a sortList = [[2,0],[3,0]] becomes "&col[2]=0&col[3]=0" in the url - // and a filterList = [[2,Blue],[3,13]] becomes "&fcol[2]=Blue&fcol[3]=13" in the url - ajaxUrl: null, - - // process ajax so that the following information is returned: - // [ total_rows (number), rows (array of arrays), headers (array; optional) ] - // example: - // [ - // 100, // total rows - // [ - // [ "row1cell1", "row1cell2", ... "row1cellN" ], - // [ "row2cell1", "row2cell2", ... "row2cellN" ], - // ... - // [ "rowNcell1", "rowNcell2", ... "rowNcellN" ] - // ], - // [ "header1", "header2", ... "headerN" ] // optional - // ] - ajaxProcessing: function(ajax){ return [ 0, [], null ]; }, - - // output default: '{page}/{totalPages}' - // possible variables: {page}, {totalPages}, {filteredPages}, {startRow}, {endRow}, {filteredRows} and {totalRows} - output: '{startRow} to {endRow} of {totalRows} rows', // '{page}/{totalPages}' - - // apply disabled classname to the pager arrows when the rows at either extreme is visible - updateArrows: true, - - // starting page of the pager (zero based index) - page: 0, - - // Number of visible rows - size: 10, - - // if true, the table will remain the same height no matter how many records are displayed. The space is made up by an empty - // table row set to a height to compensate; default is false - fixedHeight: false, - - // remove rows from the table to speed up the sort of large tables. - // setting this to false, only hides the non-visible rows; needed if you plan to add/remove rows with the pager enabled. - removeRows: false, // removing rows in larger tables speeds up the sort - - // css class names of pager arrows - cssFirst: '.first', // go to first page arrow - cssPrev: '.prev', // previous page arrow - cssNext: '.next', // next page arrow - cssLast: '.last', // go to last page arrow - cssGoto: '.gotoPage', // go to page selector - select dropdown that sets the current page - cssPageDisplay: '.pagedisplay', // location of where the "output" is displayed - cssPageSize: '.pagesize', // page size selector - select dropdown that sets the "size" option - cssErrorRow: 'tablesorter-errorRow', // error information row - - // class added to arrows when at the extremes (i.e. prev/first arrows are "disabled" when on the first page) - cssDisabled: 'disabled', // Note there is no period "." in front of this class name - - // stuff not set by the user - totalRows: 0, - totalPages: 0, - filteredRows: 0, - filteredPages: 0 - - }; - - var $this = this, - - // hide arrows at extremes - pagerArrows = function(c, disable) { - var a = 'addClass', - r = 'removeClass', - d = c.cssDisabled, - dis = !!disable, - tp = Math.min( c.totalPages, c.filteredPages ); - if ( c.updateArrows ) { - $(c.cssFirst + ',' + c.cssPrev, c.container)[ ( dis || c.page === 0 ) ? a : r ](d); - $(c.cssNext + ',' + c.cssLast, c.container)[ ( dis || c.page === tp - 1 ) ? a : r ](d); - } - }, - - updatePageDisplay = function(table, c) { - var i, p, s, t, out, f = $(table).hasClass('hasFilters') && !c.ajaxUrl; - c.filteredRows = (f) ? table.config.$tbodies.children('tr:not(.filtered,.remove-me)').length : c.totalRows; - c.filteredPages = (f) ? Math.ceil( c.filteredRows / c.size ) : c.totalPages; - if ( Math.min( c.totalPages, c.filteredPages ) > 0 ) { - t = (c.size * c.page > c.filteredRows); - c.startRow = (t) ? 1 : ( c.size * c.page ) + 1; - c.page = (t) ? 0 : c.page; - c.endRow = Math.min( c.filteredRows, c.totalRows, c.size * ( c.page + 1 ) ); - out = $(c.cssPageDisplay, c.container); - // form the output string - s = c.output.replace(/\{(page|filteredRows|filteredPages|totalPages|startRow|endRow|totalRows)\}/gi, function(m){ - return { - '{page}' : c.page + 1, - '{filteredRows}' : c.filteredRows, - '{filteredPages}' : c.filteredPages, - '{totalPages}' : c.totalPages, - '{startRow}' : c.startRow, - '{endRow}' : c.endRow, - '{totalRows}' : c.totalRows - }[m]; - }); - if (out[0]) { - out[ (out[0].tagName === 'INPUT') ? 'val' : 'html' ](s); - if ( $(c.cssGoto, c.container).length ) { + var ts = $.tablesorter; + + $.extend({ + tablesorterPager: new function() { + + this.defaults = { + // target the pager markup + container: null, + + // use this format: "http://mydatabase.com?page={page}&size={size}&{sortList:col}&{filterList:fcol}" + // where {page} is replaced by the page number, {size} is replaced by the number of records to show, + // {sortList:col} adds the sortList to the url into a "col" array, and {filterList:fcol} adds + // the filterList to the url into an "fcol" array. + // So a sortList = [[2,0],[3,0]] becomes "&col[2]=0&col[3]=0" in the url + // and a filterList = [[2,Blue],[3,13]] becomes "&fcol[2]=Blue&fcol[3]=13" in the url + ajaxUrl: null, + + // modify the url after all processing has been applied + customAjaxUrl: function(table, url) { return url; }, + + // ajax error callback from $.tablesorter.showError function + // ajaxError: function( config, xhr, settings, exception ) { return exception; }; + // returning false will abort the error message + ajaxError: null, + + // modify the $.ajax object to allow complete control over your ajax requests + ajaxObject: { + dataType: 'json' + }, + + // set this to false if you want to block ajax loading on init + processAjaxOnInit: true, + + // process ajax so that the following information is returned: + // [ total_rows (number), rows (array of arrays), headers (array; optional) ] + // example: + // [ + // 100, // total rows + // [ + // [ "row1cell1", "row1cell2", ... "row1cellN" ], + // [ "row2cell1", "row2cell2", ... "row2cellN" ], + // ... + // [ "rowNcell1", "rowNcell2", ... "rowNcellN" ] + // ], + // [ "header1", "header2", ... "headerN" ] // optional + // ] + ajaxProcessing: function(data) { return data; }, + + // output default: '{page}/{totalPages}' + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, + // {endRow}, {filteredRows} and {totalRows} + output: '{startRow} to {endRow} of {totalRows} rows', // '{page}/{totalPages}' + + // apply disabled classname to the pager arrows when the rows at either extreme is visible + updateArrows: true, + + // starting page of the pager (zero based index) + page: 0, + + // reset pager after filtering; set to desired page # + // set to false to not change page at filter start + pageReset: 0, + + // Number of visible rows + size: 10, + + // Number of options to include in the pager number selector + maxOptionSize: 20, + + // Save pager page & size if the storage script is loaded (requires $.tablesorter.storage in jquery.tablesorter.widgets.js) + savePages: true, + + // defines custom storage key + storageKey: 'tablesorter-pager', + + // if true, the table will remain the same height no matter how many records are displayed. The space is made up by an empty + // table row set to a height to compensate; default is false + fixedHeight: false, + + // count child rows towards the set page size? (set true if it is a visible table row within the pager) + // if true, child row(s) may not appear to be attached to its parent row, may be split across pages or + // may distort the table if rowspan or cellspans are included. + countChildRows: false, + + // remove rows from the table to speed up the sort of large tables. + // setting this to false, only hides the non-visible rows; needed if you plan to add/remove rows with the pager enabled. + removeRows: false, // removing rows in larger tables speeds up the sort + + // css class names of pager arrows + cssFirst: '.first', // go to first page arrow + cssPrev: '.prev', // previous page arrow + cssNext: '.next', // next page arrow + cssLast: '.last', // go to last page arrow + cssGoto: '.gotoPage', // go to page selector - select dropdown that sets the current page + cssPageDisplay: '.pagedisplay', // location of where the "output" is displayed + cssPageSize: '.pagesize', // page size selector - select dropdown that sets the "size" option + cssErrorRow: 'tablesorter-errorRow', // error information row + + // class added to arrows when at the extremes (i.e. prev/first arrows are "disabled" when on the first page) + cssDisabled: 'disabled', // Note there is no period "." in front of this class name + + // stuff not set by the user + totalRows: 0, + totalPages: 0, + filteredRows: 0, + filteredPages: 0, + ajaxCounter: 0, + currentFilters: [], + startRow: 0, + endRow: 0, + $size: null, + last: {} + + }; + + var pagerEvents = 'filterInit filterStart filterEnd sortEnd disablePager enablePager destroyPager updateComplete ' + + 'pageSize pageSet pageAndSize pagerUpdate refreshComplete ', + + $this = this, + + // hide arrows at extremes + pagerArrows = function( table, p, disable ) { + var tmp, + a = 'addClass', + r = 'removeClass', + d = p.cssDisabled, + dis = !!disable, + first = ( dis || p.page === 0 ), + tp = getTotalPages( table, p ), + last = ( dis || (p.page === tp - 1) || tp === 0 ); + if ( p.updateArrows ) { + tmp = p.$container.find(p.cssFirst + ',' + p.cssPrev); + tmp[ first ? a : r ](d); // toggle disabled class + tmp.each(function() { + this.ariaDisabled = first; + }); + tmp = p.$container.find(p.cssNext + ',' + p.cssLast); + tmp[ last ? a : r ](d); + tmp.each(function() { + this.ariaDisabled = last; + }); + } + }, + + calcFilters = function(table, p) { + var normalized, indx, len, + c = table.config, + hasFilters = c.$table.hasClass('hasFilters'); + if (hasFilters && !p.ajax) { + if (ts.isEmptyObject(c.cache)) { + // delayInit: true so nothing is in the cache + p.filteredRows = p.totalRows = c.$tbodies.eq(0).children('tr').not( p.countChildRows ? '' : '.' + c.cssChildRow ).length; + } else { + p.filteredRows = 0; + normalized = c.cache[0].normalized; + len = normalized.length; + for (indx = 0; indx < len; indx++) { + p.filteredRows += p.regexRows.test(normalized[indx][c.columns].$row[0].className) ? 0 : 1; + } + } + } else if (!hasFilters) { + p.filteredRows = p.totalRows; + } + }, + + updatePageDisplay = function(table, p, completed) { + if ( p.initializing ) { return; } + var s, t, $out, $el, indx, len, options, output, + c = table.config, + namespace = c.namespace + 'pager', + sz = parsePageSize( p, p.size, 'get' ); // don't allow dividing by zero + if (sz === 'all') { sz = p.totalRows; } + if (p.countChildRows) { t[ t.length ] = c.cssChildRow; } + p.totalPages = Math.ceil( p.totalRows / sz ); // needed for "pageSize" method + c.totalRows = p.totalRows; + parsePageNumber( table, p ); + calcFilters(table, p); + c.filteredRows = p.filteredRows; + p.filteredPages = Math.ceil( p.filteredRows / sz ) || 0; + if ( getTotalPages( table, p ) >= 0 ) { + t = (sz * p.page > p.filteredRows) && completed; + p.page = (t) ? p.pageReset || 0 : p.page; + p.startRow = (t) ? sz * p.page + 1 : (p.filteredRows === 0 ? 0 : sz * p.page + 1); + p.endRow = Math.min( p.filteredRows, p.totalRows, sz * ( p.page + 1 ) ); + $out = p.$container.find(p.cssPageDisplay); + + // Output param can be callback for custom rendering or string + if (typeof p.output === 'function') { + s = p.output(table, p); + } else { + output = $out + // get output template from data-pager-output or data-pager-output-filtered + .attr('data-pager-output' + (p.filteredRows < p.totalRows ? '-filtered' : '')) || + p.output; + // form the output string (can now get a new output string from the server) + s = ( p.ajaxData && p.ajaxData.output ? p.ajaxData.output || output : output ) + // {page} = one-based index; {page+#} = zero based index +/- value + .replace(/\{page([\-+]\d+)?\}/gi, function(m, n) { + return p.totalPages ? p.page + (n ? parseInt(n, 10) : 1) : 0; + }) + // {totalPages}, {extra}, {extra:0} (array) or {extra : key} (object) + .replace(/\{\w+(\s*:\s*\w+)?\}/gi, function(m) { + var len, indx, + str = m.replace(/[{}\s]/g, ''), + extra = str.split(':'), + data = p.ajaxData, + // return zero for default page/row numbers + deflt = /(rows?|pages?)$/i.test(str) ? 0 : ''; + if (/(startRow|page)/.test(extra[0]) && extra[1] === 'input') { + len = ('' + (extra[0] === 'page' ? p.totalPages : p.totalRows)).length; + indx = extra[0] === 'page' ? p.page + 1 : p.startRow; + return ''; + } + return extra.length > 1 && data && data[extra[0]] ? data[extra[0]][extra[1]] : p[str] || (data ? data[str] : deflt) || deflt; + }); + } + $el = p.$container.find(p.cssGoto); + if ( $el.length ) { t = ''; - p = Math.min( c.totalPages, c.filteredPages ); - for ( i = 1; i <= p; i++ ) { - t += ''; + options = buildPageSelect( table, p ); + len = options.length; + for (indx = 0; indx < len; indx++) { + t += ''; } - $(c.cssGoto, c.container).html(t).val(c.page + 1); + // innerHTML doesn't work in IE9 - http://support2.microsoft.com/kb/276228 + $el.html(t).val( p.page + 1 ); + } + if ($out.length) { + $out[ ($out[0].nodeName === 'INPUT') ? 'val' : 'html' ](s); + // rebind startRow/page inputs + $out.find('.ts-startRow, .ts-page').unbind('change' + namespace).bind('change' + namespace, function() { + var v = $(this).val(), + pg = $(this).hasClass('ts-startRow') ? Math.floor( v / sz ) + 1 : v; + c.$table.triggerHandler('pageSet' + namespace, [ pg ]); + }); } } - } - pagerArrows(c); - if (c.initialized) { $(table).trigger('pagerComplete', c); } - }, + pagerArrows( table, p ); + fixHeight(table, p); + if (p.initialized && completed !== false) { + if (ts.debug(c, 'pager')) { + console.log('Pager >> Triggering pagerComplete'); + } + c.$table.triggerHandler('pagerComplete', p); + // save pager info to storage + if (p.savePages && ts.storage) { + ts.storage(table, p.storageKey, { + page : p.page, + size : sz === p.totalRows ? 'all' : sz + }); + } + } + }, - fixHeight = function(table, c) { - var d, h, $b = $(table.tBodies[0]); - if (c.fixedHeight) { - $b.find('tr.pagerSavedHeightSpacer').remove(); - h = $.data(table, 'pagerSavedHeight'); - if (h) { - d = h - $b.height(); - if ( d > 5 && $.data(table, 'pagerLastSize') === c.size && $b.children('tr:visible').length < c.size ) { - $b.append(''); + buildPageSelect = function( table, p ) { + // Filter the options page number link array if it's larger than 'maxOptionSize' + // as large page set links will slow the browser on large dom inserts + var i, central_focus_size, focus_option_pages, insert_index, option_length, focus_length, + pg = getTotalPages( table, p ) || 1, + // make skip set size multiples of 5 + skip_set_size = Math.ceil( ( pg / p.maxOptionSize ) / 5 ) * 5, + large_collection = pg > p.maxOptionSize, + current_page = p.page + 1, + start_page = skip_set_size, + end_page = pg - skip_set_size, + option_pages = [ 1 ], + // construct default options pages array + option_pages_start_page = (large_collection) ? skip_set_size : 1; + + for ( i = option_pages_start_page; i <= pg; ) { + option_pages[ option_pages.length ] = i; + i = i + ( large_collection ? skip_set_size : 1 ); + } + option_pages[ option_pages.length ] = pg; + if (large_collection) { + focus_option_pages = []; + // don't allow central focus size to be > 5 on either side of current page + central_focus_size = Math.max( Math.floor( p.maxOptionSize / skip_set_size ) - 1, 5 ); + + start_page = current_page - central_focus_size; + if (start_page < 1) { start_page = 1; } + end_page = current_page + central_focus_size; + if (end_page > pg) { end_page = pg; } + // construct an array to get a focus set around the current page + for (i = start_page; i <= end_page ; i++) { + focus_option_pages[ focus_option_pages.length ] = i; } + + // keep unique values + option_pages = $.grep(option_pages, function(value, indx) { + return $.inArray(value, option_pages) === indx; + }); + + option_length = option_pages.length; + focus_length = focus_option_pages.length; + + // make sure at all option_pages aren't replaced + if (option_length - focus_length > skip_set_size / 2 && option_length + focus_length > p.maxOptionSize ) { + insert_index = Math.floor(option_length / 2) - Math.floor(focus_length / 2); + Array.prototype.splice.apply(option_pages, [ insert_index, focus_length ]); + } + option_pages = option_pages.concat(focus_option_pages); + } - } - }, - - changeHeight = function(table, c) { - var $b = $(table.tBodies[0]); - $b.find('tr.pagerSavedHeightSpacer').remove(); - $.data(table, 'pagerSavedHeight', $b.height()); - fixHeight(table, c); - $.data(table, 'pagerLastSize', c.size); - }, - - hideRows = function(table, c){ - if (!c.ajaxUrl) { - var i, - rows = $(table.tBodies).children('tr:not(.' + table.config.cssChildRow + ')'), - l = rows.length, - s = ( c.page * c.size ), - e = s + c.size, - j = 0; // size counter - for ( i = 0; i < l; i++ ){ - if (!/filtered/.test(rows[i].className)) { - rows[i].style.display = ( j >= s && j < e ) ? '' : 'none'; - j++; + + // keep unique values again + option_pages = $.grep(option_pages, function(value, indx) { + return $.inArray(value, option_pages) === indx; + }) + .sort(function(a, b) { return a - b; }); + + return option_pages; + }, + + fixHeight = function(table, p) { + var d, h, bs, + c = table.config, + $b = c.$tbodies.eq(0); + $b.find('tr.pagerSavedHeightSpacer').remove(); + if (p.fixedHeight && !p.isDisabled) { + h = $.data(table, 'pagerSavedHeight'); + if (h) { + bs = 0; + if ($(table).css('border-spacing').split(' ').length > 1) { + bs = $(table).css('border-spacing').split(' ')[1].replace(/[^-\d\.]/g, ''); + } + d = h - $b.height() + (bs * p.size) - bs; + if ( + d > 5 && $.data(table, 'pagerLastSize') === p.size && + $b.children('tr:visible').length < (p.size === 'all' ? p.totalRows : p.size) + ) { + $b.append(''); + } } } - } - }, - - hideRowsSetup = function(table, c){ - c.size = parseInt( $(c.cssPageSize, c.container).find('option[selected]').val(), 10 ) || c.size; - $.data(table, 'pagerLastSize', c.size); - pagerArrows(c); - if ( !c.removeRows ) { - hideRows(table, c); - $(table).bind('sortEnd.pager filterEnd.pager', function(){ - hideRows(table, c); - }); - } - }, + }, - renderAjax = function(data, table, c, exception){ - // process data - if ( typeof(c.ajaxProcessing) === "function" ) { - // ajaxProcessing result: [ total, rows, headers ] - var i, j, hsh, $f, $sh, - $t = $(table), - tc = table.config, - hl = $t.find('thead th').length, tds = '', - err = '' + - (exception ? exception.message + ' (' + exception.name + ')' : 'No rows found') + '', - result = c.ajaxProcessing(data) || [ 0, [] ], - d = result[1] || [], - l = d.length, - th = result[2]; - if ( l > 0 ) { + changeHeight = function(table, p) { + var h, + c = table.config, + $b = c.$tbodies.eq(0); + $b.find('tr.pagerSavedHeightSpacer').remove(); + if (!$b.children('tr:visible').length) { + $b.append(' '); + } + h = $b.children('tr').eq(0).height() * (p.size === 'all' ? p.totalRows : p.size); + $.data(table, 'pagerSavedHeight', h); + fixHeight(table, p); + $.data(table, 'pagerLastSize', p.size); + }, + + hideRows = function(table, p) { + if (!p.ajaxUrl) { + var i, + lastIndex = 0, + c = table.config, + rows = c.$tbodies.eq(0).children('tr'), + l = rows.length, + sz = p.size === 'all' ? p.totalRows : p.size, + s = ( p.page * sz ), + e = s + sz, + last = -1, // for cache indexing + j = 0; // size counter + p.cacheIndex = []; for ( i = 0; i < l; i++ ) { - tds += ''; - for ( j = 0; j < d[i].length; j++ ) { - // build tbody cells - tds += '' + d[i][j] + ''; - } - tds += ''; - } - } - // only add new header text if the length matches - if ( th && th.length === hl ) { - hsh = $t.hasClass('hasStickyHeaders'); - $sh = $t.find('.' + ((tc.widgetOptions && tc.widgetOptions.stickyHeaders) || 'tablesorter-stickyheader')); - $f = $t.find('tfoot tr:first').children(); - $t.find('th.' + tc.cssHeader).each(function(j){ - var $t = $(this), icn; - // add new test within the first span it finds, or just in the header - if ( $t.find('.' + tc.cssIcon).length ) { - icn = $t.find('.' + tc.cssIcon).clone(true); - $t.find('.tablesorter-header-inner').html( th[j] ).append(icn); - if ( hsh && $sh.length ) { - icn = $sh.find('th').eq(j).find('.' + tc.cssIcon).clone(true); - $sh.find('th').eq(j).find('.tablesorter-header-inner').html( th[j] ).append(icn); + if ( !p.regexFiltered.test(rows[i].className) ) { + if (j === s && rows[i].className.match(c.cssChildRow)) { + // hide child rows @ start of pager (if already visible) + rows[i].style.display = 'none'; + } else { + rows[i].style.display = ( j >= s && j < e ) ? '' : 'none'; + if (last !== j && j >= s && j < e) { + p.cacheIndex[ p.cacheIndex.length ] = i; + last = j; + } + // don't count child rows + j += rows[i].className.match(c.cssChildRow + '|' + c.selectorRemove.slice(1)) && !p.countChildRows ? 0 : 1; + if ( j === e && rows[i].style.display !== 'none' && rows[i].className.match(ts.css.cssHasChild) ) { + lastIndex = i; + } } + } + } + // add any attached child rows to last row of pager. Fixes part of issue #396 + if ( lastIndex > 0 && rows[lastIndex].className.match(ts.css.cssHasChild) ) { + while ( ++lastIndex < l && rows[lastIndex].className.match(c.cssChildRow) ) { + rows[lastIndex].style.display = ''; + } + } + } + }, + + hideRowsSetup = function(table, p) { + p.size = parsePageSize( p, p.$container.find(p.cssPageSize).val(), 'get' ); + setPageSize( table, p.size, p ); + pagerArrows( table, p ); + if ( !p.removeRows ) { + hideRows(table, p); + $(table).bind('sortEnd filterEnd '.split(' ').join(table.config.namespace + 'pager '), function() { + hideRows(table, p); + }); + } + }, + + renderAjax = function(data, table, p, xhr, settings, exception) { + // process data + if ( typeof p.ajaxProcessing === 'function' ) { + + // in case nothing is returned by ajax, empty out the table; see #1032 + // but do it before calling pager_ajaxProcessing because that function may add content + // directly to the table + table.config.$tbodies.eq(0).empty(); + + // ajaxProcessing result: [ total, rows, headers ] + var i, j, t, hsh, $f, $sh, $headers, $h, icon, th, d, l, rr_count, len, sz, + c = table.config, + $table = c.$table, + tds = '', + result = p.ajaxProcessing(data, table, xhr) || [ 0, [] ]; + // Clean up any previous error. + ts.showError( table ); + + if ( exception ) { + if (ts.debug(c, 'pager')) { + console.error('Pager >> Ajax Error', xhr, settings, exception); + } + ts.showError( table, xhr, settings, exception ); + c.$tbodies.eq(0).children('tr').detach(); + p.totalRows = 0; + } else { + // process ajax object + if (!$.isArray(result)) { + p.ajaxData = result; + c.totalRows = p.totalRows = result.total; + c.filteredRows = p.filteredRows = typeof result.filteredRows !== 'undefined' ? result.filteredRows : result.total; + th = result.headers; + d = result.rows || []; } else { - $t.find('.tablesorter-header-inner').html( th[j] ); - $sh.find('th').eq(j).find('.tablesorter-header-inner').html( th[j] ); + // allow [ total, rows, headers ] or [ rows, total, headers ] + t = isNaN(result[0]) && !isNaN(result[1]); + // ensure a zero returned row count doesn't fail the logical || + rr_count = result[t ? 1 : 0]; + p.totalRows = isNaN(rr_count) ? p.totalRows || 0 : rr_count; + // can't set filtered rows when returning an array + c.totalRows = c.filteredRows = p.filteredRows = p.totalRows; + // set row data to empty array if nothing found - see http://stackoverflow.com/q/30875583/145346 + d = p.totalRows === 0 ? [] : result[t ? 0 : 1] || []; // row data + th = result[2]; // headers + } + l = d && d.length; + if (d instanceof $) { + if (p.processAjaxOnInit) { + // append jQuery object + c.$tbodies.eq(0).empty(); + c.$tbodies.eq(0).append(d); + } + } else if (l) { + // build table from array + for ( i = 0; i < l; i++ ) { + tds += ''; + for ( j = 0; j < d[i].length; j++ ) { + // build tbody cells; watch for data containing HTML markup - see #434 + tds += /^\s*' + d[i][j] + ''; + } + tds += ''; + } + // add rows to first tbody + if (p.processAjaxOnInit) { + c.$tbodies.eq(0).html( tds ); + } + } + p.processAjaxOnInit = true; + // update new header text + if ( th ) { + hsh = $table.hasClass('hasStickyHeaders'); + $sh = hsh ? + c.widgetOptions.$sticky.children('thead:first').children('tr:not(.' + c.cssIgnoreRow + ')').children() : + ''; + $f = $table.find('tfoot tr:first').children(); + // don't change td headers (may contain pager) + $headers = c.$headers.filter( 'th ' ); + len = $headers.length; + for ( j = 0; j < len; j++ ) { + $h = $headers.eq( j ); + // add new test within the first span it finds, or just in the header + if ( $h.find('.' + ts.css.icon).length ) { + icon = $h.find('.' + ts.css.icon).clone(true); + $h.find('.' + ts.css.headerIn).html( th[j] ).append(icon); + if ( hsh && $sh.length ) { + icon = $sh.eq(j).find('.' + ts.css.icon).clone(true); + $sh.eq(j).find('.' + ts.css.headerIn).html( th[j] ).append(icon); + } + } else { + $h.find('.' + ts.css.headerIn).html( th[j] ); + if (hsh && $sh.length) { + // add sticky header to container just in case it contains pager controls + p.$container = p.$container.add( c.widgetOptions.$sticky ); + $sh.eq(j).find('.' + ts.css.headerIn).html( th[j] ); + } + } + $f.eq(j).html( th[j] ); + } + } + } + if (c.showProcessing) { + ts.isProcessing(table); // remove loading icon + } + sz = parsePageSize( p, p.size, 'get' ); + // make sure last pager settings are saved, prevents multiple server side calls with + // the same parameters + p.totalPages = sz === 'all' ? 1 : Math.ceil( p.totalRows / sz ); + p.last.totalRows = p.totalRows; + p.last.currentFilters = p.currentFilters; + p.last.sortList = (c.sortList || []).join(','); + updatePageDisplay(table, p, false); + // tablesorter core updateCache (not pager) + ts.updateCache( c, function() { + if (p.initialized) { + // apply widgets after table has rendered & after a delay to prevent + // multiple applyWidget blocking code from blocking this trigger + setTimeout(function() { + if (ts.debug(c, 'pager')) { + console.log('Pager >> Triggering pagerChange'); + } + $table.triggerHandler( 'pagerChange', p ); + ts.applyWidget( table ); + updatePageDisplay(table, p, true); + }, 0); } - $f.eq(j).html( th[j] ); }); + + } + if (!p.initialized) { + pagerInitialized(table, p); + } + }, + + getAjax = function(table, p) { + var url = getAjaxUrl(table, p), + $doc = $(document), + counter, + c = table.config, + namespace = c.namespace + 'pager'; + if ( url !== '' ) { + if (c.showProcessing) { + ts.isProcessing(table, true); // show loading icon + } + $doc.bind('ajaxError' + namespace, function(e, xhr, settings, exception) { + renderAjax(null, table, p, xhr, settings, exception); + $doc.unbind('ajaxError' + namespace); + }); + + counter = ++p.ajaxCounter; + + p.last.ajaxUrl = url; // remember processed url + p.ajaxObject.url = url; // from the ajaxUrl option and modified by customAjaxUrl + p.ajaxObject.success = function(data, status, jqxhr) { + // Refuse to process old ajax commands that were overwritten by new ones - see #443 + if (counter < p.ajaxCounter) { + return; + } + renderAjax(data, table, p, jqxhr); + $doc.unbind('ajaxError' + namespace); + if (typeof p.oldAjaxSuccess === 'function') { + p.oldAjaxSuccess(data); + } + }; + if (ts.debug(c, 'pager')) { + console.log('Pager >> Ajax initialized', p.ajaxObject); + } + $.ajax(p.ajaxObject); + } + }, + + getAjaxUrl = function(table, p) { + var indx, len, + c = table.config, + url = (p.ajaxUrl) ? p.ajaxUrl + // allow using "{page+1}" in the url string to switch to a non-zero based index + .replace(/\{page([\-+]\d+)?\}/, function(s, n) { return p.page + (n ? parseInt(n, 10) : 0); }) + // this will pass "all" to server when size is set to "all" + .replace(/\{size\}/g, p.size) : '', + sortList = c.sortList, + filterList = p.currentFilters || $(table).data('lastSearch') || [], + sortCol = url.match(/\{\s*sort(?:List)?\s*:\s*(\w*)\s*\}/), + filterCol = url.match(/\{\s*filter(?:List)?\s*:\s*(\w*)\s*\}/), + arry = []; + if (sortCol) { + sortCol = sortCol[1]; + len = sortList.length; + for (indx = 0; indx < len; indx++) { + arry[ arry.length ] = sortCol + '[' + sortList[indx][0] + ']=' + sortList[indx][1]; + } + // if the arry is empty, just add the col parameter... "&{sortList:col}" becomes "&col" + url = url.replace(/\{\s*sort(?:List)?\s*:\s*(\w*)\s*\}/g, arry.length ? arry.join('&') : sortCol ); + arry = []; + } + if (filterCol) { + filterCol = filterCol[1]; + len = filterList.length; + for (indx = 0; indx < len; indx++) { + if (filterList[indx]) { + arry[ arry.length ] = filterCol + '[' + indx + ']=' + encodeURIComponent( filterList[indx] ); + } + } + // if the arry is empty, just add the fcol parameter... "&{filterList:fcol}" becomes "&fcol" + url = url.replace(/\{\s*filter(?:List)?\s*:\s*(\w*)\s*\}/g, arry.length ? arry.join('&') : filterCol ); + p.currentFilters = filterList; + } + if ( typeof p.customAjaxUrl === 'function' ) { + url = p.customAjaxUrl(table, url); + } + if (ts.debug(c, 'pager')) { + console.log('Pager >> Ajax url = ' + url); + } + return url; + }, + + renderTable = function(table, rows, p) { + var $tb, index, count, added, + $t = $(table), + c = table.config, + debug = ts.debug(c, 'pager'), + f = c.$table.hasClass('hasFilters'), + l = rows && rows.length || 0, // rows may be undefined + e = p.size === 'all' ? p.totalRows : p.size, + s = ( p.page * e ); + if ( l < 1 ) { + if (debug) { + console.warn('Pager >> No rows for pager to render'); + } + // empty table, abort! + return; + } + if ( p.page >= p.totalPages ) { + // lets not render the table more than once + moveToLastPage(table, p); + } + p.cacheIndex = []; + p.isDisabled = false; // needed because sorting will change the page and re-enable the pager + if (p.initialized) { + if (debug) { + console.log('Pager >> Triggering pagerChange'); + } + $t.triggerHandler( 'pagerChange', p ); } - - $t.find('thead tr.' + c.cssErrorRow).remove(); // Clean up any previous error. - if ( exception ) { - // add error row to thead instead of tbody, or clicking on the header will result in a parser error - $t.find('thead').append(err); + if ( !p.removeRows ) { + hideRows(table, p); } else { - $(table.tBodies[0]).html( tds ); // add rows to first tbody + ts.clearTableBody(table); + $tb = ts.processTbody(table, c.$tbodies.eq(0), true); + // not filtered, start from the calculated starting point (s) + // if filtered, start from zero + index = f ? 0 : s; + count = f ? 0 : s; + added = 0; + while (added < e && index < rows.length) { + if (!f || !p.regexFiltered.test(rows[index][0].className)) { + count++; + if (count > s && added <= e) { + added++; + p.cacheIndex[ p.cacheIndex.length ] = index; + $tb.append(rows[index]); + } + } + index++; + } + ts.processTbody(table, $tb, false); } - if (tc.showProcessing) { - $.tablesorter.isProcessing(table); // remove loading icon + updatePageDisplay(table, p); + if (table.isUpdating) { + if (debug) { + console.log('Pager >> Triggering updateComplete'); + } + $t.triggerHandler('updateComplete', [ table, true ]); } - $t.trigger('update'); - c.totalRows = result[0] || 0; - c.totalPages = Math.ceil( c.totalRows / c.size ); - updatePageDisplay(table, c); - fixHeight(table, c); - if (c.initialized) { $t.trigger('pagerChange', c); } - } - if (!c.initialized) { - c.initialized = true; - $(table).trigger('pagerInitialized', c); - } - }, + }, - getAjax = function(table, c){ - var url = getAjaxUrl(table, c), - tc = table.config; - if ( url !== '' ) { - if (tc.showProcessing) { - $.tablesorter.isProcessing(table, true); // show loading icon + showAllRows = function(table, p) { + var index, $controls, len; + if ( p.ajax ) { + pagerArrows( table, p, true ); + } else { + $.data(table, 'pagerLastPage', p.page); + $.data(table, 'pagerLastSize', p.size); + p.page = 0; + p.size = p.totalRows; + p.totalPages = 1; + $(table) + .addClass('pagerDisabled') + .removeAttr('aria-describedby') + .find('tr.pagerSavedHeightSpacer').remove(); + renderTable(table, table.config.rowsCopy, p); + p.isDisabled = true; + ts.applyWidget( table ); + if (ts.debug(table.config, 'pager')) { + console.log('Pager >> Disabled'); + } + } + // disable size selector + $controls = p.$container.find( p.cssGoto + ',' + p.cssPageSize + ', .ts-startRow, .ts-page' ); + len = $controls.length; + for ( index = 0; index < len; index++ ) { + $controls.eq( index ).addClass( p.cssDisabled )[0].disabled = true; + $controls[ index ].ariaDisabled = true; } - $(document).bind('ajaxError.pager', function(e, xhr, settings, exception) { - if (settings.url === url) { - renderAjax(null, table, c, exception); - $(document).unbind('ajaxError.pager'); + }, + + // updateCache if delayInit: true + updateCache = function(table) { + var c = table.config, + p = c.pager; + // tablesorter core updateCache (not pager) + ts.updateCache( c, function() { + var i, + rows = [], + n = table.config.cache[0].normalized; + p.totalRows = n.length; + for (i = 0; i < p.totalRows; i++) { + rows[ rows.length ] = n[i][c.columns].$row; } + c.rowsCopy = rows; + moveToPage(table, p, true); }); - $.getJSON(url, function(data) { - renderAjax(data, table, c); - $(document).unbind('ajaxError.pager'); - }); - } - }, - - getAjaxUrl = function(table, c) { - var url = (c.ajaxUrl) ? c.ajaxUrl.replace(/\{page\}/g, c.page).replace(/\{size\}/g, c.size) : '', - sl = table.config.sortList, - fl = c.currentFilters || [], - sortCol = url.match(/\{sortList[\s+]?:[\s+]?([^}]*)\}/), - filterCol = url.match(/\{filterList[\s+]?:[\s+]?([^}]*)\}/), - arry = []; - if (sortCol) { - sortCol = sortCol[1]; - $.each(sl, function(i,v){ - arry.push(sortCol + '[' + v[0] + ']=' + v[1]); - }); - // if the arry is empty, just add the col parameter... "&{sortList:col}" becomes "&col" - url = url.replace(/\{sortList[\s+]?:[\s+]?([^\}]*)\}/g, arry.length ? arry.join('&') : sortCol ); - } - if (filterCol) { - filterCol = filterCol[1]; - $.each(fl, function(i,v){ - if (v) { - arry.push(filterCol + '[' + i + ']=' + encodeURIComponent(v)); + }, + + moveToPage = function(table, p, pageMoved) { + if ( p.isDisabled ) { return; } + var tmp, + c = table.config, + debug = ts.debug(c, 'pager'), + $t = $(table), + l = p.last; + if ( pageMoved !== false && p.initialized && ts.isEmptyObject(c.cache)) { + return updateCache(table); + } + // abort page move if the table has filters and has not been initialized + if (p.ajax && ts.hasWidget(table, 'filter') && !c.widgetOptions.filter_initialized) { return; } + parsePageNumber( table, p ); + calcFilters(table, p); + // fixes issue where one currentFilter is [] and the other is ['','',''], + // making the next if comparison think the filters are different (joined by commas). Fixes #202. + l.currentFilters = (l.currentFilters || []).join('') === '' ? [] : l.currentFilters; + p.currentFilters = (p.currentFilters || []).join('') === '' ? [] : p.currentFilters; + // don't allow rendering multiple times on the same page/size/totalRows/filters/sorts + if ( l.page === p.page && l.size === p.size && l.totalRows === p.totalRows && + (l.currentFilters || []).join(',') === (p.currentFilters || []).join(',') && + // check for ajax url changes see #730 + (l.ajaxUrl || '') === (p.ajaxObject.url || '') && + // & ajax url option changes (dynamically add/remove/rename sort & filter parameters) + (l.optAjaxUrl || '') === (p.ajaxUrl || '') && + l.sortList === (c.sortList || []).join(',') ) { return; } + if (debug) { + console.log('Pager >> Changing to page ' + p.page); + } + p.last = { + page : p.page, + size : p.size, + // fixes #408; modify sortList otherwise it auto-updates + sortList : (c.sortList || []).join(','), + totalRows : p.totalRows, + currentFilters : p.currentFilters || [], + ajaxUrl : p.ajaxObject.url || '', + optAjaxUrl : p.ajaxUrl || '' + }; + if (p.ajax) { + if ( !p.processAjaxOnInit && !ts.isEmptyObject(p.initialRows) ) { + p.processAjaxOnInit = true; + tmp = p.initialRows; + p.totalRows = typeof tmp.total !== 'undefined' ? tmp.total : + ( debug ? console.error('Pager >> No initial total page set!') || 0 : 0 ); + p.filteredRows = typeof tmp.filtered !== 'undefined' ? tmp.filtered : + ( debug ? console.error('Pager >> No initial filtered page set!') || 0 : 0 ); + pagerInitialized( table, p ); + } else { + getAjax(table, p); } - }); - // if the arry is empty, just add the fcol parameter... "&{filterList:fcol}" becomes "&fcol" - url = url.replace(/\{filterList[\s+]?:[\s+]?([^\}]*)\}/g, arry.length ? arry.join('&') : filterCol ); - } - - return url; - }, - - renderTable = function(table, rows, c) { - c.isDisabled = false; // needed because sorting will change the page and re-enable the pager - var i, j, o, - f = document.createDocumentFragment(), - l = rows.length, - s = ( c.page * c.size ), - e = ( s + c.size ); - if ( l < 1 ) { return; } // empty table, abort! - if (c.initialized) { $(table).trigger('pagerChange', c); } - if ( !c.removeRows ) { - hideRows(table, c); - } else { - if ( e > rows.length ) { - e = rows.length; + } else if (!p.ajax) { + renderTable(table, c.rowsCopy, p); } - $(table.tBodies[0]).addClass('tablesorter-hidden'); - $.tablesorter.clearTableBody(table); - for ( i = s; i < e; i++ ) { - o = rows[i]; - l = o.length; - for ( j = 0; j < l; j++ ) { - f.appendChild(o[j]); + $.data(table, 'pagerLastPage', p.page); + if (p.initialized && pageMoved !== false) { + if (debug) { + console.log('Pager >> Triggering pageMoved'); + } + $t.triggerHandler('pageMoved', p); + ts.applyWidget( table ); + if (table.isUpdating) { + if (debug) { + console.log('Pager >> Triggering updateComplete'); + } + $t.triggerHandler('updateComplete', [ table, true ]); } } - table.tBodies[0].appendChild(f); - $(table.tBodies[0]).removeClass('tablesorter-hidden'); - } - if ( c.page >= c.totalPages ) { - moveToLastPage(table, c); - } - updatePageDisplay(table, c); - if ( !c.isDisabled ) { fixHeight(table, c); } - $(table).trigger('applyWidgets'); - }, - - showAllRows = function(table, c){ - if ( c.ajax ) { - pagerArrows(c, true); - } else { - c.isDisabled = true; - $.data(table, 'pagerLastPage', c.page); - $.data(table, 'pagerLastSize', c.size); - c.page = 0; - c.size = c.totalRows; - c.totalPages = 1; - $('tr.pagerSavedHeightSpacer', table.tBodies[0]).remove(); - renderTable(table, table.config.rowsCopy, c); - } - // disable size selector - $(c.container).find(c.cssPageSize + ',' + c.cssGoto).each(function(){ - $(this).addClass(c.cssDisabled)[0].disabled = true; - }); - }, + }, - moveToPage = function(table, c) { - if ( c.isDisabled ) { return; } - var p = Math.min( c.totalPages, c.filteredPages ); - if ( c.page < 0 || c.page > ( p - 1 ) ) { - c.page = 0; - } - if (c.ajax) { - getAjax(table, c); - } else if (!c.ajax) { - renderTable(table, table.config.rowsCopy, c); - } - $.data(table, 'pagerLastPage', c.page); - $.data(table, 'pagerUpdateTriggered', true); - if (c.initialized) { $(table).trigger('pageMoved', c); } - }, - - setPageSize = function(table, size, c) { - c.size = size; - $.data(table, 'pagerLastPage', c.page); - $.data(table, 'pagerLastSize', c.size); - c.totalPages = Math.ceil( c.totalRows / c.size ); - moveToPage(table, c); - }, - - moveToFirstPage = function(table, c) { - c.page = 0; - moveToPage(table, c); - }, - - moveToLastPage = function(table, c) { - c.page = ( Math.min( c.totalPages, c.filteredPages ) - 1 ); - moveToPage(table, c); - }, - - moveToNextPage = function(table, c) { - c.page++; - if ( c.page >= ( Math.min( c.totalPages, c.filteredPages ) - 1 ) ) { - c.page = ( Math.min( c.totalPages, c.filteredPages ) - 1 ); - } - moveToPage(table, c); - }, + getTotalPages = function( table, p ) { + return ts.hasWidget( table, 'filter' ) ? + Math.min( p.totalPages, p.filteredPages ) : + p.totalPages; + }, - moveToPrevPage = function(table, c) { - c.page--; - if ( c.page <= 0 ) { - c.page = 0; - } - moveToPage(table, c); - }, - - destroyPager = function(table, c){ - showAllRows(table, c); - $(c.container).hide(); // hide pager - table.config.appender = null; // remove pager appender function - $(table).unbind('destroy.pager sortEnd.pager filterEnd.pager enable.pager disable.pager'); - }, - - enablePager = function(table, c, triggered){ - var p = $(c.cssPageSize, c.container).removeClass(c.cssDisabled).removeAttr('disabled'); - $(c.container).find(c.cssGoto).removeClass(c.cssDisabled).removeAttr('disabled'); - c.isDisabled = false; - c.page = $.data(table, 'pagerLastPage') || c.page || 0; - c.size = $.data(table, 'pagerLastSize') || parseInt(p.find('option[selected]').val(), 10) || c.size; - p.val(c.size); // set page size - c.totalPages = Math.ceil( Math.min( c.totalPages, c.filteredPages ) / c.size); - if ( triggered ) { - $(table).trigger('update'); - setPageSize(table, c.size, c); - hideRowsSetup(table, c); - fixHeight(table, c); - } - }; - - $this.appender = function(table, rows) { - var c = table.config.pager; - if ( !c.ajax ) { - table.config.rowsCopy = rows; - c.totalRows = rows.length; - c.size = $.data(table, 'pagerLastSize') || c.size; - c.totalPages = Math.ceil(c.totalRows / c.size); - renderTable(table, rows, c); - } - }; - - $this.construct = function(settings) { - return this.each(function() { - // check if tablesorter has initialized - if (!(this.config && this.hasInitialized)) { return; } - var t, ctrls, fxn, - config = this.config, - c = config.pager = $.extend( {}, $.tablesorterPager.defaults, settings ), - table = this, - tc = table.config, - $t = $(table), - pager = $(c.container).addClass('tablesorter-pager').show(); // added in case the pager is reinitialized after being destroyed. - config.appender = $this.appender; + parsePageNumber = function( table, p ) { + var min = getTotalPages( table, p ) - 1; + p.page = parseInt( p.page, 10 ); + if ( p.page < 0 || isNaN( p.page ) ) { p.page = 0; } + if ( p.page > min && min >= 0 ) { p.page = min; } + return p.page; + }, + + // set to either set or get value + parsePageSize = function( p, size, mode ) { + var s = parseInt( size, 10 ) || p.size || p.settings.size || 10; + if (p.initialized && (/all/i.test( s + ' ' + size ) || s === p.totalRows)) { + // Fixing #1364 & #1366 + return p.$container.find(p.cssPageSize + ' option[value="all"]').length ? + 'all' : p.totalRows; + } + // "get" to get `p.size` or "set" to set `pageSize.val()` + return mode === 'get' ? s : p.size; + }, + + setPageSize = function(table, size, p) { + // "all" size is only returned if an "all" option exists - fixes #1366 + p.size = parsePageSize( p, size, 'get' ); + p.$container.find( p.cssPageSize ).val( p.size ); + $.data(table, 'pagerLastPage', parsePageNumber( table, p ) ); + $.data(table, 'pagerLastSize', p.size); + p.totalPages = p.size === 'all' ? 1 : Math.ceil( p.totalRows / p.size ); + p.filteredPages = p.size === 'all' ? 1 : Math.ceil( p.filteredRows / p.size ); + }, + + moveToFirstPage = function(table, p) { + p.page = 0; + moveToPage(table, p); + }, + + moveToLastPage = function(table, p) { + p.page = getTotalPages( table, p ) - 1; + moveToPage(table, p); + }, + + moveToNextPage = function(table, p) { + p.page++; + var last = getTotalPages( table, p ) - 1; + if ( p.page >= last ) { + p.page = last; + } + moveToPage(table, p); + }, + + moveToPrevPage = function(table, p) { + p.page--; + if ( p.page <= 0 ) { + p.page = 0; + } + moveToPage(table, p); + }, + + pagerInitialized = function(table, p) { + p.initialized = true; + p.initializing = false; + if (ts.debug(table.config, 'pager')) { + console.log('Pager >> Triggering pagerInitialized'); + } + $(table).triggerHandler( 'pagerInitialized', p ); + ts.applyWidget( table ); + updatePageDisplay(table, p); + }, + + resetState = function(table, p) { + var c = table.config; + c.pager = $.extend( true, {}, $.tablesorterPager.defaults, p.settings ); + init(table, p.settings); + }, + + destroyPager = function(table, p) { + var c = table.config, + namespace = c.namespace + 'pager', + ctrls = [ p.cssFirst, p.cssPrev, p.cssNext, p.cssLast, p.cssGoto, p.cssPageSize ].join( ',' ); + showAllRows(table, p); + p.$container + // hide pager controls + .hide() + // unbind + .find( ctrls ) + .unbind( namespace ); + c.appender = null; // remove pager appender function + c.$table.unbind( namespace ); + if (ts.storage) { + ts.storage(table, p.storageKey, ''); + } + delete c.pager; + delete c.rowsCopy; + }, + + enablePager = function(table, p, triggered) { + var info, size, $el, + c = table.config; + p.$container.find(p.cssGoto + ',' + p.cssPageSize + ',.ts-startRow, .ts-page') + .removeClass(p.cssDisabled) + .removeAttr('disabled') + .each(function() { + this.ariaDisabled = false; + }); + p.isDisabled = false; + p.page = $.data(table, 'pagerLastPage') || p.page || 0; + $el = p.$container.find(p.cssPageSize); + size = $el.find('option[selected]').val(); + p.size = $.data(table, 'pagerLastSize') || parsePageSize( p, size, 'get' ); + p.totalPages = p.size === 'all' ? 1 : Math.ceil( getTotalPages( table, p ) / p.size ); + setPageSize(table, p.size, p); // set page size + // if table id exists, include page display with aria info + if ( table.id && !c.$table.attr( 'aria-describedby' ) ) { + $el = p.$container.find( p.cssPageDisplay ); + info = $el.attr( 'id' ); + if ( !info ) { + // only add pageDisplay id if it doesn't exist - see #1288 + info = table.id + '_pager_info'; + $el.attr( 'id', info ); + } + c.$table.attr( 'aria-describedby', info ); + } + changeHeight(table, p); + if ( triggered ) { + // tablesorter core update table + ts.update( c ); + setPageSize(table, p.size, p); + moveToPage(table, p); + hideRowsSetup(table, p); + if (ts.debug(c, 'pager')) { + console.log('Pager >> Enabled'); + } + } + }, + + init = function(table, settings) { + var t, ctrls, fxn, $el, + c = table.config, + wo = c.widgetOptions, + debug = ts.debug(c, 'pager'), + p = c.pager = $.extend( true, {}, $.tablesorterPager.defaults, settings ), + $t = c.$table, + namespace = c.namespace + 'pager', + // added in case the pager is reinitialized after being destroyed. + pager = p.$container = $(p.container).addClass('tablesorter-pager').show(); + // save a copy of the original settings + p.settings = $.extend( true, {}, $.tablesorterPager.defaults, settings ); + if (debug) { + console.log('Pager >> Initializing'); + } + p.oldAjaxSuccess = p.oldAjaxSuccess || p.ajaxObject.success; + c.appender = $this.appender; + p.initializing = true; + if (p.savePages && ts.storage) { + t = ts.storage(table, p.storageKey) || {}; // fixes #387 + p.page = isNaN(t.page) ? p.page : t.page; + p.size = t.size === 'all' ? t.size : ( isNaN( t.size ) ? p.size : t.size ) || p.setSize || 10; + setPageSize(table, p.size, p); + } + // skipped rows + p.regexRows = new RegExp('(' + (wo.filter_filteredRow || 'filtered') + '|' + c.selectorRemove.slice(1) + '|' + c.cssChildRow + ')'); + p.regexFiltered = new RegExp(wo.filter_filteredRow || 'filtered'); $t - .unbind('filterStart.pager filterEnd.pager sortEnd.pager disable.pager enable.pager destroy.pager update.pager pageSize.pager') - .bind('filterStart.pager', function(e, filters) { - $.data(table, 'pagerUpdateTriggered', false); - c.currentFilters = filters; - }) - // update pager after filter widget completes - .bind('filterEnd.pager sortEnd.pager', function(e) { - //Prevent infinite event loops from occuring by setting this in all moveToPage calls and catching it here. - if ($.data(table, 'pagerUpdateTriggered')) { - $.data(table, 'pagerUpdateTriggered', false); - return; + // .unbind( namespace ) adding in jQuery 1.4.3 ( I think ) + .unbind( pagerEvents.split(' ').join(namespace + ' ').replace(/\s+/g, ' ') ) + .bind('filterInit filterStart '.split(' ').join(namespace + ' '), function(e, filters) { + p.currentFilters = $.isArray(filters) ? filters : c.$table.data('lastSearch'); + var filtersEqual; + if (p.ajax && e.type === 'filterInit') { + // ensure pager ajax is called after filter widget has initialized + return moveToPage( table, p, false ); + } + if (ts.filter.equalFilters) { + filtersEqual = ts.filter.equalFilters(c, c.lastSearch, p.currentFilters); + } else { + // will miss filter changes of the same value in a different column, see #1363 + filtersEqual = (c.lastSearch || []).join('') !== (p.currentFilters || []).join(''); + } + // don't change page if filters are the same (pager updating, etc) + if (e.type === 'filterStart' && p.pageReset !== false && !filtersEqual) { + p.page = p.pageReset; // fixes #456 & #565 + } + }) + // update pager after filter widget completes + .bind('filterEnd sortEnd '.split(' ').join(namespace + ' '), function() { + p.currentFilters = c.$table.data('lastSearch'); + if (p.initialized || p.initializing) { + if (c.delayInit && c.rowsCopy && c.rowsCopy.length === 0) { + // make sure we have a copy of all table rows once the cache has been built + updateCache(table); } - if (e.type === 'filterEnd') { c.page = 0; } - updatePageDisplay(table, c); - moveToPage(table, c); - fixHeight(table, c); - }) - .bind('disable.pager', function(){ - showAllRows(table, c); - }) - .bind('enable.pager', function(){ - enablePager(table, c, true); - }) - .bind('destroy.pager', function(){ - destroyPager(table, c); - }) - .bind('update.pager', function(){ - hideRows(table, c); - }) - .bind('pageSize.pager', function(e,v){ - c.size = parseInt(v, 10) || 10; - hideRows(table, c); - updatePageDisplay(table, c); - }); + updatePageDisplay(table, p, false); + moveToPage(table, p, false); + ts.applyWidget( table ); + } + }) + .bind('disablePager' + namespace, function(e) { + e.stopPropagation(); + showAllRows(table, p); + }) + .bind('enablePager' + namespace, function(e) { + e.stopPropagation(); + enablePager(table, p, true); + }) + .bind('destroyPager' + namespace, function(e) { + e.stopPropagation(); + destroyPager(table, p); + }) + .bind('resetToLoadState' + namespace, function(e) { + e.stopPropagation(); + resetState(table, p); + }) + .bind('updateComplete' + namespace, function(e, table, triggered) { + e.stopPropagation(); + // table can be unintentionally undefined in tablesorter v2.17.7 and earlier + // don't recalculate total rows/pages if using ajax + if ( !table || triggered || p.ajax ) { return; } + var $rows = c.$tbodies.eq(0).children('tr').not(c.selectorRemove); + p.totalRows = $rows.length - ( p.countChildRows ? 0 : $rows.filter('.' + c.cssChildRow).length ); + p.totalPages = p.size === 'all' ? 1 : Math.ceil( p.totalRows / p.size ); + if ($rows.length && c.rowsCopy && c.rowsCopy.length === 0) { + // make a copy of all table rows once the cache has been built + updateCache(table); + } + if ( p.page >= p.totalPages ) { + moveToLastPage(table, p); + } + hideRows(table, p); + changeHeight(table, p); + updatePageDisplay(table, p, true); + }) + .bind('pageSize refreshComplete '.split(' ').join(namespace + ' '), function(e, size) { + e.stopPropagation(); + setPageSize(table, parsePageSize( p, size, 'get' ), p); + moveToPage(table, p); + hideRows(table, p); + updatePageDisplay(table, p, false); + }) + .bind('pageSet pagerUpdate '.split(' ').join(namespace + ' '), function(e, num) { + e.stopPropagation(); + // force pager refresh + if (e.type === 'pagerUpdate') { + num = typeof num === 'undefined' ? p.page + 1 : num; + p.last.page = true; + } + p.page = (parseInt(num, 10) || 1) - 1; + moveToPage(table, p, true); + updatePageDisplay(table, p, false); + }) + .bind('pageAndSize' + namespace, function(e, page, size) { + e.stopPropagation(); + p.page = (parseInt(page, 10) || 1) - 1; + setPageSize(table, parsePageSize( p, size, 'get' ), p); + moveToPage(table, p, true); + hideRows(table, p); + updatePageDisplay(table, p, false); + }); // clicked controls - ctrls = [c.cssFirst, c.cssPrev, c.cssNext, c.cssLast]; + ctrls = [ p.cssFirst, p.cssPrev, p.cssNext, p.cssLast ]; fxn = [ moveToFirstPage, moveToPrevPage, moveToNextPage, moveToLastPage ]; + if (debug && !pager.length) { + console.warn('Pager >> "container" not found'); + } pager.find(ctrls.join(',')) - .unbind('click.pager') - .bind('click.pager', function(e){ - var i, $this = $(this), l = ctrls.length; - if ( !$this.hasClass(c.cssDisabled) ) { - for (i = 0; i < l; i++) { - if ($this.is(ctrls[i])) { - fxn[i](table, c); - break; - } + .attr('tabindex', 0) + .unbind('click' + namespace) + .bind('click' + namespace, function(e) { + e.stopPropagation(); + var i, $t = $(this), l = ctrls.length; + if ( !$t.hasClass(p.cssDisabled) ) { + for (i = 0; i < l; i++) { + if ($t.is(ctrls[i])) { + fxn[i](table, p); + break; } } - return false; - }); + } + }); // goto selector - if ( pager.find(c.cssGoto).length ) { - pager.find(c.cssGoto) - .unbind('change') - .bind('change', function(){ - c.page = $(this).val() - 1; - moveToPage(table, c); - }); - updatePageDisplay(table, c); + $el = pager.find(p.cssGoto); + if ( $el.length ) { + $el + .unbind('change' + namespace) + .bind('change' + namespace, function() { + p.page = $(this).val() - 1; + moveToPage(table, p, true); + updatePageDisplay(table, p, false); + }); + } else if (debug) { + console.warn('Pager >> "goto" selector not found'); } - // page size selector - t = pager.find(c.cssPageSize); - if ( t.length ) { - t.unbind('change.pager').bind('change.pager', function() { - t.val( $(this).val() ); // in case there are more than one pagers - if ( !$(this).hasClass(c.cssDisabled) ) { - setPageSize(table, parseInt( $(this).val(), 10 ), c); - changeHeight(table, c); + $el = pager.find(p.cssPageSize); + if ( $el.length ) { + // setting an option as selected appears to cause issues with initial page size + $el.find('option').removeAttr('selected'); + $el.unbind('change' + namespace).bind('change' + namespace, function() { + if ( !$(this).hasClass(p.cssDisabled) ) { + var size = $(this).val(); + // in case there are more than one pager + setPageSize(table, size, p); + moveToPage(table, p); + changeHeight(table, p); } return false; }); + } else if (debug) { + console.warn('Pager >> "size" selector not found'); } // clear initialized flag - c.initialized = false; + p.initialized = false; // before initialization event - $t.trigger('pagerBeforeInitialized', c); - - enablePager(table, c, false); + $t.triggerHandler('pagerBeforeInitialized', p); - if ( typeof(c.ajaxUrl) === 'string' ) { + enablePager(table, p, false); + if ( typeof p.ajaxUrl === 'string' ) { // ajax pager; interact with database - c.ajax = true; - //When filtering with ajax, allow only custom filtering function, disable default filtering since it will be done server side. - tc.widgetOptions.filter_serversideFiltering = true; - tc.serverSideSorting = true; - moveToPage(table, c); + p.ajax = true; + // When filtering with ajax, allow only custom filtering function, disable default + // filtering since it will be done server side. + c.widgetOptions.filter_serversideFiltering = true; + c.serverSideSorting = true; + moveToPage(table, p); } else { - c.ajax = false; + p.ajax = false; // Regular pager; all rows stored in memory - $(this).trigger("appendCache", true); - hideRowsSetup(table, c); + ts.appendCache( c, true ); // true = don't apply widgets + hideRowsSetup(table, p); } - changeHeight(table, c); - // pager initialized - if (!c.ajax) { - c.initialized = true; - $(table).trigger('pagerInitialized', c); + if (!p.ajax && !p.initialized) { + p.initializing = false; + p.initialized = true; + // update page size on init + setPageSize(table, p.size, p); + moveToPage(table, p); + if (debug) { + console.log('Pager >> Triggering pagerInitialized'); + } + c.$table.triggerHandler( 'pagerInitialized', p ); + if ( !( c.widgetOptions.filter_initialized && ts.hasWidget(table, 'filter') ) ) { + updatePageDisplay(table, p, false); + } } + + // make the hasWidget function think that the pager widget is being used + c.widgetInit.pager = true; + }; + + $this.appender = function(table, rows) { + var c = table.config, + p = c.pager; + if ( !p.ajax ) { + c.rowsCopy = rows; + p.totalRows = p.countChildRows ? c.$tbodies.eq(0).children('tr').length : rows.length; + p.size = $.data(table, 'pagerLastSize') || p.size || p.settings.size || 10; + p.totalPages = p.size === 'all' ? 1 : Math.ceil( p.totalRows / p.size ); + renderTable(table, rows, p); + // update display here in case all rows are removed + updatePageDisplay(table, p, false); + } + }; + + $this.construct = function(settings) { + return this.each(function() { + // check if tablesorter has initialized + if (!(this.config && this.hasInitialized)) { return; } + init(this, settings); + }); + }; + + }() + }); + + // see #486 + ts.showError = function( table, xhr, settings, exception ) { + var $table = $( table ), + c = $table[0].config, + wo = c && c.widgetOptions, + errorRow = c.pager && c.pager.cssErrorRow || + wo && wo.pager_css && wo.pager_css.errorRow || + 'tablesorter-errorRow', + typ = typeof xhr, + valid = true, + message = '', + removeRow = function() { + c.$table.find( 'thead' ).find( c.selectorRemove ).remove(); + }; + + if ( !$table.length ) { + console.error('tablesorter showError: no table parameter passed'); + return; + } + + // ajaxError callback for plugin or widget - see #992 + if ( typeof c.pager.ajaxError === 'function' ) { + valid = c.pager.ajaxError( c, xhr, settings, exception ); + if ( valid === false ) { + return removeRow(); + } else { + message = valid; + } + } else if ( typeof wo.pager_ajaxError === 'function' ) { + valid = wo.pager_ajaxError( c, xhr, settings, exception ); + if ( valid === false ) { + return removeRow(); + } else { + message = valid; + } + } + + if ( message === '' ) { + if ( typ === 'object' ) { + message = + xhr.status === 0 ? 'Not connected, verify Network' : + xhr.status === 404 ? 'Requested page not found [404]' : + xhr.status === 500 ? 'Internal Server Error [500]' : + exception === 'parsererror' ? 'Requested JSON parse failed' : + exception === 'timeout' ? 'Time out error' : + exception === 'abort' ? 'Ajax Request aborted' : + 'Uncaught error: ' + xhr.statusText + ' [' + xhr.status + ']'; + } else if ( typ === 'string' ) { + // keep backward compatibility (external usage just passes a message string) + message = xhr; + } else { + // remove all error rows + return removeRow(); + } + } + + // allow message to include entire row HTML! + $( /tr\>/.test(message) ? message : '' + message + '' ) + .click( function() { + $( this ).remove(); + }) + // add error row to thead instead of tbody, or clicking on the header will result in a parser error + .appendTo( c.$table.find( 'thead:first' ) ) + .addClass( errorRow + ' ' + c.selectorRemove.slice(1) ) + .attr({ + role : 'alert', + 'aria-live' : 'assertive' }); - }; - }() -}); -// extend plugin scope -$.fn.extend({ - tablesorterPager: $.tablesorterPager.construct -}); + }; + + // extend plugin scope + $.fn.extend({ + tablesorterPager: $.tablesorterPager.construct + }); -})(jQuery); \ No newline at end of file +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/beta-testing/pager-custom-controls.js b/vendor/assets/javascripts/jquery-tablesorter/beta-testing/pager-custom-controls.js new file mode 100644 index 0000000..e6ba676 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/beta-testing/pager-custom-controls.js @@ -0,0 +1,151 @@ +/*! + * custom pager controls (beta) for Tablesorter - updated 9/28/2016 (v2.27.8) + initialize custom pager script BEFORE initializing tablesorter/tablesorter pager + custom pager looks like this: + 1 | 2 … 5 | 6 | 7 … 99 | 100 + _ _ _ _ adjacentSpacer + _ _ distanceSpacer + _____ ________ ends (2 default) + _________ aroundCurrent (1 default) + + */ +/*jshint browser:true, jquery:true, unused:false, loopfunc:true */ +/*global jQuery: false */ + +;(function($) { +'use strict'; + +$.tablesorter = $.tablesorter || {}; + +$.tablesorter.customPagerControls = function(settings) { + var defaults = { + table : 'table', + pager : '.pager', + pageSize : '.left a', + currentPage : '.right a', + ends : 2, // number of pages to show of either end + aroundCurrent : 1, // number of pages surrounding the current page + link : '{page}', // page element; use {page} to include the page number + currentClass : 'current', // current page class name + adjacentSpacer : ' | ', // spacer for page numbers next to each other + distanceSpacer : '', // spacer for page numbers away from each other (ellipsis) + addKeyboard : true, // use left,right,up,down,pageUp,pageDown,home, or end to change current page + pageKeyStep : 10 // page step to use for pageUp and pageDown + }, + options = $.extend({}, defaults, settings), + $table = $(options.table), + $pager = $(options.pager), + focusOnPager = false; + + $table + .on('filterStart', function() { + focusOnPager = false; + }) + .on('pagerInitialized pagerComplete', function (e, c) { + var indx, + p = c.pager ? c.pager : c, // using widget + pages = $('
'), + cur = p.page + 1, + pageArray = [], + max = p.filteredPages, + around = options.aroundCurrent; + for (indx = -around; indx <= around; indx++) { + if (cur + indx >= 1 && cur + indx <= max) { + pageArray.push(cur + indx); + } + } + if (pageArray.length) { + // include first and last pages (ends) in the pagination + for (indx = 0; indx < options.ends; indx++) { + if ((indx + 1 <= max) && $.inArray(indx + 1, pageArray) === -1) { + pageArray.push(indx + 1); + } + if ((max - indx > 0) && $.inArray(max - indx, pageArray) === -1) { + pageArray.push(max - indx); + } + } + // sort the list + pageArray = pageArray.sort(function(a, b) { return a - b; }); + // only include unique pages + pageArray = $.grep(pageArray, function(value, key) { + return $.inArray(value, pageArray) === key; + }); + // make links and spacers + if (pageArray.length) { + max = pageArray.length - 1; + $.each(pageArray, function(indx, value) { + pages + .append( + $(options.link.replace(/\{page\}/g, value)) + .toggleClass(options.currentClass, value === cur) + .attr('data-page', value) + ) + .append((indx < max && (pageArray[ indx + 1 ] - 1 !== value) ? + options.distanceSpacer : + (indx >= max ? '' : options.adjacentSpacer) + )); + }); + } + } + $pager.find('.pagecount').html(pages.html()); + if (focusOnPager) { + // don't focus on pager when using filter - fixes #1296 + $pager.find('.' + options.currentClass).focus(); + } + }); + + // set up pager controls + $pager + .find(options.pageSize) + .on('click', function () { + $(this) + .addClass(options.currentClass) + .siblings() + .removeClass(options.currentClass); + $table.trigger('pageSize', $(this).html()); + return false; + }) + .end() + .on('click', options.currentPage, function() { + focusOnPager = true; + var $el = $(this); + $el + .addClass(options.currentClass) + .siblings() + .removeClass(options.currentClass); + $table.trigger('pageSet', $el.attr('data-page')); + return false; + }); + + // make right/left arrow keys work + if (options.addKeyboard) { + $(document).on('keydown', function(events) { + // ignore arrows inside form elements + if (/input|select|textarea/i.test(events.target.nodeName) || + !(events.which > 32 && events.which < 41)) { + focusOnPager = false; + return; + } + // only allow keyboard use if element inside of pager is focused + if ($(document.activeElement).closest(options.pager).is($pager)) { + events.preventDefault(); + focusOnPager = true; + var key = events.which, + max = $table[0].config.totalRows, + $el = $pager.find(options.currentPage).filter('.' + options.currentClass), + page = $el.length ? parseInt($el.attr('data-page'), 10) : null; + if (page) { + if (key === 33) { page -= options.pageKeyStep; } // pageUp + if (key === 34) { page += options.pageKeyStep; } // pageDown + if (key === 35) { page = max; } // end + if (key === 36) { page = 1; } // home + if (key === 37 || key === 38) { page -= 1; } // left/up + if (key === 39 || key === 40) { page += 1; } // right/down + $table.trigger('pageSet', page); + } + } + }); + } +}; + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/beta-testing/widget-reorder.js b/vendor/assets/javascripts/jquery-tablesorter/beta-testing/widget-reorder.js new file mode 100644 index 0000000..09b6bab --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/beta-testing/widget-reorder.js @@ -0,0 +1,181 @@ +/*! tablesorter column reorder - beta testing +* Requires tablesorter v2.8+ and jQuery 1.7+ +* by Rob Garrison +*/ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + +$.tablesorter.addWidget({ + id: 'reorder', + priority: 70, + options : { + reorder_axis : 'xy', // x or xy + reorder_delay : 300, + reorder_helperClass : 'tablesorter-reorder-helper', + reorder_helperBar : 'tablesorter-reorder-helper-bar', + reorder_noReorder : 'reorder-false', + reorder_blocked : 'reorder-block-left reorder-block-end', + reorder_complete : null // callback + }, + init: function(table, thisWidget, c, wo) { + var i, timer, $helper, $bar, clickOffset, + lastIndx = -1, + endIndex = -1, + startIndex = -1, + t = wo.reorder_blocked.split(' '), + noReorderLeft = t[0] || 'reorder-block-left', + noReorderLast = t[1] || 'reorder-block-end', + lastOffset = c.$headers.not('.' + noReorderLeft).first(), + offsets = c.$headers.map(function() { + var s, $t = $(this); + if ($t.hasClass(noReorderLeft)) { + s = lastOffset; + $t = s; + //lastOffset = $t; + } + lastOffset = $t; + return $t.offset().left; + }).get(), + len = offsets.length, + startReorder = function(e, $th) { + var p = $th.position(), + r = $th.parent().position(), + i = startIndex = $th.index(); + clickOffset = [ e.pageX - p.left, e.pageY - r.top ]; + $helper = c.$table.clone(); + $helper.find('> thead > tr:first').children('[data-column!=' + i + ']').remove(); + $helper.find('thead tr:gt(0), caption, colgroup, tbody, tfoot').remove(); + $helper + .css({ + position: 'absolute', + zIndex : 1, + left: p.left - clickOffset[0], + top: r.top - clickOffset[1], + width: $th.outerWidth() + }) + .appendTo('head') + .find('th, td').addClass(wo.reorder_helperClass); + $bar = $('
') + .css({ + position : 'absolute', + top : c.$table.find('thead').offset().top, + height : $th.closest('thead').outerHeight() + c.$table.find('tbody').height() + }) + .appendTo('head'); + positionBar(e); + lastIndx = endIndex; + }, + positionBar = function(e) { + for (i = 0; i <= len; i++) { + if ( i > 0 && e.pageX < offsets[i-1] + (offsets[i] - offsets[i-1])/2 && !c.$headers.eq(i).hasClass(noReorderLeft) ) { + endIndex = i - 1; + // endIndex = offsets.lastIndexOf( offsets[i-1] ); // lastIndexOf not supported by IE8 and older + if (endIndex >= 0 && lastIndx === endIndex) { return false; } + lastIndx = endIndex; + if (c.debug) { + console.log( endIndex === 0 ? 'target before column 0' : endIndex === len ? 'target after last column' : 'target between columns ' + startIndex + ' and ' + endIndex); + } + $bar.css('left', offsets[i-1]); + return false; + } + } + if (endIndex < 0) { + endIndex = len; + $bar.css('left', offsets[len]); + } + }, + finishReorder = function() { + $helper.remove(); + $bar.remove(); + // finish reorder + var adj, s = startIndex, + rows = c.$table.find('tr'), + cols; + startIndex = -1; // stop mousemove updates + if ( s > -1 && endIndex > -1 && s !== endIndex && s + 1 !== endIndex ) { + adj = endIndex !== 0; + if (c.debug) { + console.log( 'Inserting column ' + s + (adj ? ' after' : ' before') + ' column ' + (endIndex - adj ? 1 : 0) ); + } + rows.each(function() { + cols = $(this).children(); + cols.eq(s)[ adj ? 'insertAfter' : 'insertBefore' ]( cols.eq( endIndex - (adj ? 1 : 0) ) ); + }); + cols = []; + // stored header info needs to be modified too! + for (i = 0; i < len; i++) { + if (i === s) { continue; } + if (i === endIndex - (adj ? 1 : 0)) { + if (!adj) { cols.push(c.headerContent[s]); } + cols.push(c.headerContent[i]); + if (adj) { cols.push(c.headerContent[s]); } + } else { + cols.push(c.headerContent[i]); + } + } + c.headerContent = cols; + // cols = c.headerContent.splice(s, 1); + // c.headerContent.splice(endIndex - (adj ? 1 : 0), 0, cols); + c.$table.trigger('updateAll', [ true, wo.reorder_complete ]); + } + endIndex = -1; + }, + mdown = function(e, el) { + var $t = $(el), evt = e; + if ($t.hasClass(wo.reorder_noReorder)) { return; } + timer = setTimeout(function() { + $t.addClass('tablesorter-reorder'); + startReorder(evt, $t); + }, wo.reorder_delay); + }; + + console.log( c.$headers.last().hasClass(noReorderLast) ); + + if ( c.$headers.last().hasClass(noReorderLast) ) { + offsets.push( offsets[ offsets.length - 1 ] ); + } else { + offsets.push( c.$table.offset().left + c.$table.outerWidth() ); + } + + c.$headers.not('.' + wo.reorder_noReorder).bind('mousedown.reorder', function(e) { + mdown(e, this); + }); + + $(document) + .bind('mousemove.reorder', function(e) { + if (startIndex !== -1) { + var c = { left : e.pageX - clickOffset[0] }; + endIndex = -1; + if (/y/.test(wo.reorder_axis)) { + c.top = e.pageY - clickOffset[1]; + } + $helper.css(c); + positionBar(e); + } + }) + .add( c.$headers ) + .bind('mouseup.reorder', function() { + clearTimeout(timer); + if (startIndex !== -1 && endIndex !== -1) { + finishReorder(); + } else { + startIndex = -1; + } + }); + + // has sticky headers? + c.$table.bind('stickyHeadersInit', function() { + wo.$sticky.find('thead').children().not('.' + wo.reorder_noReorder).bind('mousedown.reorder', function(e) { + mdown(e, this); + }); + }); + + } +}); + +// add mouse coordinates +$x = $('#main h1:last'); $(document).mousemove(function(e) { $x.html( e.pageX ); }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.dragtable.mod.js b/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.dragtable.mod.js new file mode 100644 index 0000000..fe86683 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.dragtable.mod.js @@ -0,0 +1,602 @@ +/*! Dragtable Mod for TableSorter - updated 10/31/2015 (v2.24.0) *//* + * Requires + * tablesorter v2.8+ + * jQuery 1.7+ + * jQuery UI (Core, Widget, Mouse & Sortable) + * Dragtable by Akottr (https://github.com/akottr) modified by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function( $ ) { +'use strict'; + var undef, + ts = $.tablesorter; + + ts.dragtable = { + create : function( _this ) { + var hasAccept, + $table = _this.originalTable.el, + handle = _this.options.dragHandle.replace('.', ''); + $table.children('thead').children().children('th,td').each(function(){ + var $this = $(this); + if ( !$this.find( _this.options.dragHandle + ',.' + handle + '-disabled' ).length ) { + hasAccept = _this.options.dragaccept ? $this.hasClass( _this.options.dragaccept.replace('.', '') ) : true; + $this + // sortClass includes a "." to match the tablesorter selectorSort option - for consistency + .wrapInner('
') + // add handle class + "-disabled" to drag-disabled columns + .prepend('
'); + } + }); + }, + start : function( table ) { + table = $( table )[0]; + if ( table && table.config ) { + table.config.widgetOptions.dragtableLast = { + search : $( table ).data( 'lastSearch' ), + order : ts.dragtable.getOrder( table ) + }; + } + }, + update : function( _this ) { + var t, list, val, + dragTable = _this.originalTable, + table = dragTable.el[ 0 ], + $table = $( table ), + c = table.config, + wo = c && c.widgetOptions, + startIndex = dragTable.startIndex - 1, + endIndex = dragTable.endIndex - 1, + columnOrder = ts.dragtable.getOrder( table ) || [], + hasFilters = ts.hasWidget( $table, 'filter' ) || false, + last = wo && wo.dragtableLast || {}, + // update moved filters + filters = []; + + // only trigger updateAll if column order changed + if ( ( last.order || [] ).join( '' ) !== columnOrder.join( '' ) ) { + + if ( c.sortList.length ) { + // must deep extend (nested arrays) to prevent list from changing with c.sortList + list = $.extend( true, [], c.sortList ); + $.each( columnOrder, function( indx, value ) { + val = ts.isValueInArray( parseInt( value, 10 ), list ); + if ( value !== last.order[ indx ] && val >= 0 ) { + c.sortList[ val ][ 0 ] = indx; + } + }); + } + + // update filter widget + if ( hasFilters ) { + $.each( last.search || [], function( indx ) { + filters[ indx ] = last.search[ columnOrder[ indx ] ]; + }); + } + + // update preset editable widget columns + t = ( ts.hasWidget( c.$table, 'editable' ) || false ) ? wo.editable_columnsArray : false; + if ( t ) { + c.widgetOptions.editable_columnsArray = ts.dragtable.reindexArrayItem( t, startIndex, endIndex ); + } + // update ignore math columns + t = ( ts.hasWidget( c.$table, 'math' ) || false ) ? wo.math_ignore : false; + if ( t ) { + c.widgetOptions.math_ignore = ts.dragtable.reindexArrayItem( t, startIndex, endIndex ); + } + // update preset resizable widget widths + t = ( ts.hasWidget( c.$table, 'resizable' ) || false ) ? wo.resizable_widths : false; + if ( t ) { + // use zero-based indexes in the array + wo.resizable_widths = ts.dragtable.moveArrayItem( t, startIndex, endIndex ); + } + /* + // chart widget WIP - there are other options that need to be rearranged! + t = ( ts.hasWidget( c.$table, 'chart' ) || false ) ? wo.chart_ignoreColumns : false; + if ( t ) { + // use zero-based indexes in the array + wo.chart_ignoreColumns = ts.dragtable.moveArrayItem( t, startIndex, endIndex ); + } + */ + + ts.updateAll( c, false, function() { + if ( hasFilters ) { + setTimeout( function() { + // just update the filter values + c.lastCombinedFilter = null; + c.$table.data('lastSearch', filters); + ts.setFilters( $table, filters ); + if ($.isFunction(_this.options.tablesorterComplete)) { + _this.options.tablesorterComplete( c.table ); + } + }, 10 ); + } + }); + } + }, + getOrder : function( table ) { + return $( table ).children( 'thead' ).children( '.' + ts.css.headerRow ).children().map( function() { + return $( this ).attr( 'data-column' ); + }).get() || []; + }, + // bubble the moved col left or right + startColumnMove : function( dragTable ) { + var $cols, + c = dragTable.el[ 0 ].config, + startIndex = dragTable.startIndex - 1, + endIndex = dragTable.endIndex - 1, + cols = c.columns - 1, + pos = endIndex === cols ? false : endIndex <= startIndex, + $rows = c.$table.children().children( 'tr' ); + if ( c.debug ) { + console.log( 'Inserting column ' + startIndex + ( pos ? ' before' : ' after' ) + ' column ' + endIndex ); + } + $rows.each( function() { + $cols = $( this ).children(); + $cols.eq( startIndex )[ pos ? 'insertBefore' : 'insertAfter' ]( $cols.eq( endIndex ) ); + }); + // rearrange col in colgroup + $cols = c.$table.children( 'colgroup' ).children(); + $cols.eq( startIndex )[ pos ? 'insertBefore' : 'insertAfter' ]( $cols.eq( endIndex ) ); + }, + swapNodes : function( a, b ) { + var indx, aparent, asibling, + len = a.length; + for ( indx = 0; indx < len; indx++ ) { + aparent = a[ indx ].parentNode; + asibling = a[ indx ].nextSibling === b[ indx ] ? a[ indx ] : a[ indx ].nextSibling; + b[ indx ].parentNode.insertBefore( a[ indx ], b[ indx ] ); + aparent.insertBefore( b[ indx ], asibling ); + } + }, + // http://stackoverflow.com/a/5306832/145346 + moveArrayItem : function( array, oldIndex, newIndex ) { + var indx, len = array.length; + if ( newIndex >= len ) { + indx = newIndex - len; + while ( ( indx-- ) + 1 ) { + array.push( undef ); + } + } + array.splice( newIndex, 0, array.splice( oldIndex, 1 )[ 0 ] ); + return array; + }, + reindexArrayItem : function( array, oldIndex, newIndex ) { + var nIndx = $.inArray( newIndex, array ), + oIndx = $.inArray( oldIndex, array ), + max = Math.max.apply( Math, array ), + arry = []; + // columns in the array were swapped so return original array + if ( nIndx >= 0 && oIndx >= 0 ) { + return array; + } + // columns not in the array were moved + $.each( array, function( indx, value ) { + // column (not in array) inserted between indexes + if ( newIndex < oldIndex ) { + // ( [ 0,1,2,3 ], 5, 1 ) -> column inserted between 0 & 1 => [ 0,2,3,4 ] + if ( value >= newIndex ) { + // 5 -> 1 [ 0, 2, 3 ] then 1 -> 0 [ 1, 2, 3 ] + arry.push( value + ( value < oldIndex ? 1 : 0 ) ); + } else { + arry.push( value ); + } + } else if ( newIndex > oldIndex ) { + // ( [ 0,1,2,3 ], 1, 5 ) -> column in array moved outside => [ 0,1,2,5 ] + if ( value === oldIndex ) { + arry.push( newIndex ); + } else if ( value < newIndex && value >= oldIndex ) { + arry.push( value - 1 ); + } else if ( value <= newIndex ) { + arry.push( value ); + } else if ( value > oldIndex ) { + arry.push( value + ( value < newIndex ? 0 : 1 ) ); + } + } + }); + return arry.sort(); + } + }; + +/*! dragtable v2.0.14 Mod *//* + * _____ _ + * | |___ _| | + * | | | | . | . | + * |_|_|_|___|___| + * + * Copyright (c) 2010-2013, Andres akottr@gmail.com + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * Inspired by the the dragtable from Dan Vanderkam (danvk.org/dragtable/) + * Thanks to the jquery and jqueryui comitters + * + * Any comment, bug report, feature-request is welcome + * Feel free to contact me. + */ + +/* TOKNOW: + * For IE7 you need this css rule: + * table { + * border-collapse: collapse; + * } + * Or take a clean reset.css (see http://meyerweb.com/eric/tools/css/reset/) + */ + +/* TODO: investigate + * Does not work properly with css rule: + * html { + * overflow: -moz-scrollbars-vertical; + * } + * Workaround: + * Fixing Firefox issues by scrolling down the page + * http://stackoverflow.com/questions/2451528/jquery-ui-sortable-scroll-helper-element-offset-firefox-issue + * + * var start = $.noop; + * var beforeStop = $.noop; + * if($.browser.mozilla) { + * var start = function (event, ui) { + * if( ui.helper !== undefined ) + * ui.helper.css('position','absolute').css('margin-top', $(window).scrollTop() ); + * } + * var beforeStop = function (event, ui) { + * if( ui.offset !== undefined ) + * ui.helper.css('margin-top', 0); + * } + * } + * + * and pass this as start and stop function to the sortable initialisation + * start: start, + * beforeStop: beforeStop + */ +/* + * Special thx to all pull requests comitters + */ + + $.widget("akottr.dragtable", { + options: { + revert: false, // smooth revert + dragHandle: '.table-handle', // handle for moving cols, if not exists the whole 'th' is the handle + maxMovingRows: 40, // 1 -> only header. 40 row should be enough, the rest is usually not in the viewport + excludeFooter: false, // excludes the footer row(s) while moving other columns. Make sense if there is a footer with a colspan. */ + onlyHeaderThreshold: 100, // TODO: not implemented yet, switch automatically between entire col moving / only header moving + dragaccept: null, // draggable cols -> default all + persistState: null, // url or function -> plug in your custom persistState function right here. function call is persistState(originalTable) + restoreState: null, // JSON-Object or function: some kind of experimental aka Quick-Hack TODO: do it better + exact: true, // removes pixels, so that the overlay table width fits exactly the original table width + clickDelay: 10, // ms to wait before rendering sortable list and delegating click event + containment: null, // @see http://api.jqueryui.com/sortable/#option-containment, use it if you want to move in 2 dimesnions (together with axis: null) + cursor: 'move', // @see http://api.jqueryui.com/sortable/#option-cursor + cursorAt: false, // @see http://api.jqueryui.com/sortable/#option-cursorAt + distance: 0, // @see http://api.jqueryui.com/sortable/#option-distance, for immediate feedback use "0" + tolerance: 'pointer', // @see http://api.jqueryui.com/sortable/#option-tolerance + axis: 'x', // @see http://api.jqueryui.com/sortable/#option-axis, Only vertical moving is allowed. Use 'x' or null. Use this in conjunction with the 'containment' setting + beforeStart: $.noop, // returning FALSE will stop the execution chain. + beforeMoving: $.noop, + beforeReorganize: $.noop, + beforeStop: $.noop, + // new options + tablesorterComplete: null, + sortClass : '.sorter' + }, + originalTable: { + el: null, + selectedHandle: null, + sortOrder: null, + startIndex: 0, + endIndex: 0 + }, + sortableTable: { + el: $(), + selectedHandle: $(), + movingRow: $() + }, + persistState: function() { + var _this = this; + this.originalTable.el.find('th').each(function(i) { + if (this.id !== '') { + _this.originalTable.sortOrder[this.id] = i; + } + }); + $.ajax({ + url: this.options.persistState, + data: this.originalTable.sortOrder + }); + }, + /* + * persistObj looks like + * {'id1':'2','id3':'3','id2':'1'} + * table looks like + * | id2 | id1 | id3 | + */ + _restoreState: function(persistObj) { + for (var n in persistObj) { + if (n in persistObj) { + this.originalTable.startIndex = $('#' + n).closest('th').prevAll().length + 1; + this.originalTable.endIndex = parseInt(persistObj[n], 10) + 1; + this._bubbleCols(); + } + } + }, + // bubble the moved col left or right + _bubbleCols: function() { + ts.dragtable.startColumnMove(this.originalTable); + }, + _rearrangeTableBackroundProcessing: function() { + var _this = this; + return function() { + _this._bubbleCols(); + _this.options.beforeStop(_this.originalTable); + _this.sortableTable.el.remove(); + restoreTextSelection(); + ts.dragtable.update(_this); + // persist state if necessary + if ($.isFunction(_this.options.persistState)) { + _this.options.persistState(_this.originalTable); + } else { + _this.persistState(); + } + + }; + }, + _rearrangeTable: function() { + var _this = this; + return function() { + // remove handler-class -> handler is now finished + _this.originalTable.selectedHandle.removeClass('dragtable-handle-selected'); + // add disabled class -> reorgorganisation starts soon + _this.sortableTable.el.sortable("disable"); + _this.sortableTable.el.addClass('dragtable-disabled'); + _this.options.beforeReorganize(_this.originalTable, _this.sortableTable); + // do reorganisation asynchronous + // for chrome a little bit more than 1 ms because we want to force a rerender + _this.originalTable.endIndex = _this.sortableTable.movingRow.prevAll().length + 1; + setTimeout(_this._rearrangeTableBackroundProcessing(), 50); + }; + }, + /* + * Disrupts the table. The original table stays the same. + * But on a layer above the original table we are constructing a list (ul > li) + * each li with a separate table representig a single col of the original table. + */ + _generateSortable: function(e) { + if (e.cancelBubble) { + e.cancelBubble = true; + } else { + e.stopPropagation(); + } + var _this = this; + // table attributes + var attrs = this.originalTable.el[0].attributes; + var tableAttrsString = ''; + for (var i = 0; i < attrs.length; i++) { + if ( (attrs[i].value || attrs[i].nodeValue) && attrs[i].nodeName != 'id' && attrs[i].nodeName != 'width') { + tableAttrsString += attrs[i].nodeName + '="' + ( attrs[i].value || attrs[i].nodeValue ) + '" '; + } + } + // row attributes + var rowAttrsArr = []; + //compute height, special handling for ie needed :-( + var heightArr = []; + + // don't save tfoot attributes because it messes up indexing + _this.originalTable.el.children('thead, tbody').children('tr:visible').slice(0, _this.options.maxMovingRow).each(function() { + // row attributes + var attrs = this.attributes; + var attrsString = ''; + for (var j = 0; j < attrs.length; j++) { + if ( (attrs[j].value || attrs[j].nodeValue ) && attrs[j].nodeName != 'id') { + attrsString += ' ' + attrs[j].nodeName + '="' + ( attrs[j].value || attrs[j].nodeValue ) + '"'; + } + } + rowAttrsArr.push(attrsString); + heightArr.push($(this).height()); + }); + + // compute width, no special handling for ie needed :-) + var widthArr = []; + // compute total width, needed for not wrapping around after the screen ends (floating) + var totalWidth = 0; + /* Find children thead and tbody. + * Only to process the immediate tr-children. Bugfix for inner tables + */ + var thtb = _this.originalTable.el.children(); + var headerRows = thtb.filter('thead').children('tr:visible'); + var visibleRows = thtb.filter('tbody').children('tr:visible'); + + headerRows.eq(0).children('th, td').filter(':visible').each(function() { + var w = $(this).outerWidth(); + widthArr.push(w); + totalWidth += w; + }); + if(_this.options.exact) { + var difference = totalWidth - _this.originalTable.el.outerWidth(); + widthArr[0] -= difference; + } + // one extra px on right and left side + totalWidth += 2; + + var captionHeight = 0; + thtb.filter('caption').each(function(){ + captionHeight += $(this).outerHeight(); + }); + + var sortableHtml = '
    '; + var sortableColumn = []; + // assemble the needed html + // build list + var rowIndex, + columns = headerRows.eq(0).children('th, td').length; + /*jshint loopfunc:true */ + for (i = 0; i < columns; i++) { + var row = headerRows.children(':nth-child(' + (i + 1) + ')'); + if (row.is(':visible')) { + rowIndex = 0; + sortableColumn[i] = '
  • ' + + '' + + ( captionHeight ? '' : '' ) + + ''; + // thead + headerRows.each(function(j){ + sortableColumn[i] += '' + + row[j].outerHTML + ''; + }); + sortableColumn[i] += ''; + // tbody + row = visibleRows.children(':nth-child(' + (i + 1) + ')'); + if (_this.options.maxMovingRows > 1) { + row = row.add(visibleRows.children(':nth-child(' + (i + 1) + ')').slice(0, _this.options.maxMovingRows - 1)); + } + row.each(function(j) { + sortableColumn[i] += '' + + this.outerHTML + ''; + }); + sortableColumn[i] += ''; + + // add footer to end of max Rows + if (!_this.options.excludeFooter) { + sortableColumn[i] += '' + + thtb.filter('tfoot').children('tr:visible').children()[i].outerHTML + ''; + } + sortableColumn[i] += '
  • '; + } + } + sortableHtml += sortableColumn.join('') + '
'; + this.sortableTable.el = this.originalTable.el.before(sortableHtml).prev(); + // set width if necessary + this.sortableTable.el.find('> li > table').each(function(i) { + $(this).css('width', widthArr[i] + 'px'); + }); + + // assign this.sortableTable.selectedHandle + this.sortableTable.selectedHandle = this.sortableTable.el.find('th .dragtable-handle-selected'); + + var items = !this.options.dragaccept ? 'li' : 'li:has(' + this.options.dragaccept + ')'; + this.sortableTable.el.sortable({ + items: items, + stop: this._rearrangeTable(), + // pass thru options for sortable widget + revert: this.options.revert, + tolerance: this.options.tolerance, + containment: this.options.containment, + cursor: this.options.cursor, + cursorAt: this.options.cursorAt, + distance: this.options.distance, + axis: this.options.axis + }); + + // assign start index + this.originalTable.startIndex = $(e.target).closest('th,td').prevAll().length + 1; + this.options.beforeMoving(this.originalTable, this.sortableTable); + // Start moving by delegating the original event to the new sortable table + this.sortableTable.movingRow = this.sortableTable.el.children('li:nth-child(' + this.originalTable.startIndex + ')'); + + // prevent the user from drag selecting "highlighting" surrounding page elements + disableTextSelection(); + // clone the initial event and trigger the sort with it + this.sortableTable.movingRow.trigger($.extend($.Event(e.type), { + which: 1, + clientX: e.clientX, + clientY: e.clientY, + pageX: e.pageX, + pageY: e.pageY, + screenX: e.screenX, + screenY: e.screenY + })); + + // Some inner divs to deliver the posibillity to style the placeholder more sophisticated + var placeholder = this.sortableTable.el.find('.ui-sortable-placeholder'); + if(placeholder.height() > 0) { + placeholder.css('height', this.sortableTable.el.find('.ui-sortable-helper').height()); + } + + placeholder.html('
'); + }, + bindTo: {}, + _create: function() { + var _this = this; + _this.originalTable = { + el: _this.element, + selectedHandle: $(), + sortOrder: {}, + startIndex: 0, + endIndex: 0 + }; + ts.dragtable.create( _this ); + // filter only the cols that are accepted + _this.bindTo = '> thead > tr > ' + ( _this.options.dragaccept || 'th, td' ); + // bind draggable to handle if exists + if (_this.element.find(_this.bindTo).find(_this.options.dragHandle).length) { + _this.bindTo += ' ' + _this.options.dragHandle; + } + // restore state if necessary + if ($.isFunction(_this.options.restoreState)) { + _this.options.restoreState(_this.originalTable); + } else { + _this._restoreState(_this.options.restoreState); + } + _this.originalTable.el.on( 'mousedown.dragtable', _this.bindTo, function(evt) { + // listen only to left mouse click + if (evt.which!==1) return; + ts.dragtable.start( _this.originalTable.el ); + if (_this.options.beforeStart(_this.originalTable) === false) { + return; + } + clearTimeout(_this.downTimer); + _this.downTimer = setTimeout(function() { + _this.originalTable.selectedHandle = $(_this); + _this.originalTable.selectedHandle.addClass('dragtable-handle-selected'); + _this._generateSortable(evt); + }, _this.options.clickDelay); + }).on( 'mouseup.dragtable', _this.options.dragHandle,function() { + clearTimeout(_this.downTimer); + }); + }, + redraw: function(){ + this.destroy(); + this._create(); + }, + destroy: function() { + this.originalTable.el.off('mousedown.dragtable mouseup.dragtable', this.bindTo); + $.Widget.prototype.destroy.apply(this, arguments); // default destroy + // now do other stuff particular to this widget + } + }); + + /** closure-scoped "private" functions **/ + var body_onselectstart_save = $(document.body).attr('onselectstart'), + body_unselectable_save = $(document.body).attr('unselectable'); + + // css properties to disable user-select on the body tag by appending a '); + $(document.head).append($style); + $(document.body).attr('onselectstart', 'return false;').attr('unselectable', 'on'); + if (window.getSelection) { + window.getSelection().removeAllRanges(); + } else { + document.selection.empty(); // MSIE http://msdn.microsoft.com/en-us/library/ms535869%28v=VS.85%29.aspx + } + } + + // remove the '; + $('head').append(s); + }); + + ts.resizable = { + init : function( c, wo ) { + if ( c.$table.hasClass( 'hasResizable' ) ) { return; } + c.$table.addClass( 'hasResizable' ); + + var noResize, $header, column, storedSizes, tmp, + $table = c.$table, + $parent = $table.parent(), + marginTop = parseInt( $table.css( 'margin-top' ), 10 ), + + // internal variables + vars = wo.resizable_vars = { + useStorage : ts.storage && wo.resizable !== false, + $wrap : $parent, + mouseXPosition : 0, + $target : null, + $next : null, + overflow : $parent.css('overflow') === 'auto' || + $parent.css('overflow') === 'scroll' || + $parent.css('overflow-x') === 'auto' || + $parent.css('overflow-x') === 'scroll', + storedSizes : [] + }; + + // set default widths + ts.resizableReset( c.table, true ); + + // now get measurements! + vars.tableWidth = $table.width(); + // attempt to autodetect + vars.fullWidth = Math.abs( $parent.width() - vars.tableWidth ) < 20; + + /* + // Hacky method to determine if table width is set to 'auto' + // http://stackoverflow.com/a/20892048/145346 + if ( !vars.fullWidth ) { + tmp = $table.width(); + $header = $table.wrap('').parent(); // temp variable + storedSizes = parseInt( $table.css( 'margin-left' ), 10 ) || 0; + $table.css( 'margin-left', storedSizes + 50 ); + vars.tableWidth = $header.width() > tmp ? 'auto' : tmp; + $table.css( 'margin-left', storedSizes ? storedSizes : '' ); + $header = null; + $table.unwrap(''); + } + */ + + if ( vars.useStorage && vars.overflow ) { + // save table width + ts.storage( c.table, 'tablesorter-table-original-css-width', vars.tableWidth ); + tmp = ts.storage( c.table, 'tablesorter-table-resized-width' ) || 'auto'; + ts.resizable.setWidth( $table, tmp, true ); + } + wo.resizable_vars.storedSizes = storedSizes = ( vars.useStorage ? + ts.storage( c.table, ts.css.resizableStorage ) : + [] ) || []; + ts.resizable.setWidths( c, wo, storedSizes ); + ts.resizable.updateStoredSizes( c, wo ); + + wo.$resizable_container = $( '
' ) + .css({ top : marginTop }) + .insertBefore( $table ); + // add container + for ( column = 0; column < c.columns; column++ ) { + $header = c.$headerIndexed[ column ]; + tmp = ts.getColumnData( c.table, c.headers, column ); + noResize = ts.getData( $header, tmp, 'resizable' ) === 'false'; + if ( !noResize ) { + $( '
' ) + .appendTo( wo.$resizable_container ) + .attr({ + 'data-column' : column, + 'unselectable' : 'on' + }) + .data( 'header', $header ) + .bind( 'selectstart', false ); + } + } + ts.resizable.bindings( c, wo ); + }, + + updateStoredSizes : function( c, wo ) { + var column, $header, + len = c.columns, + vars = wo.resizable_vars; + vars.storedSizes = []; + for ( column = 0; column < len; column++ ) { + $header = c.$headerIndexed[ column ]; + vars.storedSizes[ column ] = $header.is(':visible') ? $header.width() : 0; + } + }, + + setWidth : function( $el, width, overflow ) { + // overflow tables need min & max width set as well + $el.css({ + 'width' : width, + 'min-width' : overflow ? width : '', + 'max-width' : overflow ? width : '' + }); + }, + + setWidths : function( c, wo, storedSizes ) { + var column, $temp, + vars = wo.resizable_vars, + $extra = $( c.namespace + '_extra_headers' ), + $col = c.$table.children( 'colgroup' ).children( 'col' ); + storedSizes = storedSizes || vars.storedSizes || []; + // process only if table ID or url match + if ( storedSizes.length ) { + for ( column = 0; column < c.columns; column++ ) { + // set saved resizable widths + ts.resizable.setWidth( c.$headerIndexed[ column ], storedSizes[ column ], vars.overflow ); + if ( $extra.length ) { + // stickyHeaders needs to modify min & max width as well + $temp = $extra.eq( column ).add( $col.eq( column ) ); + ts.resizable.setWidth( $temp, storedSizes[ column ], vars.overflow ); + } + } + $temp = $( c.namespace + '_extra_table' ); + if ( $temp.length && !ts.hasWidget( c.table, 'scroller' ) ) { + ts.resizable.setWidth( $temp, c.$table.outerWidth(), vars.overflow ); + } + } + }, + + setHandlePosition : function( c, wo ) { + var startPosition, + tableHeight = c.$table.height(), + $handles = wo.$resizable_container.children(), + handleCenter = Math.floor( $handles.width() / 2 ); + + if ( ts.hasWidget( c.table, 'scroller' ) ) { + tableHeight = 0; + c.$table.closest( '.' + ts.css.scrollerWrap ).children().each(function() { + var $this = $(this); + // center table has a max-height set + tableHeight += $this.filter('[style*="height"]').length ? $this.height() : $this.children('table').height(); + }); + } + + if ( !wo.resizable_includeFooter && c.$table.children('tfoot').length ) { + tableHeight -= c.$table.children('tfoot').height(); + } + // subtract out table left position from resizable handles. Fixes #864 + // jQuery v3.3.0+ appears to include the start position with the $header.position().left; see #1544 + startPosition = parseFloat($.fn.jquery) >= 3.3 ? 0 : c.$table.position().left; + $handles.each( function() { + var $this = $(this), + column = parseInt( $this.attr( 'data-column' ), 10 ), + columns = c.columns - 1, + $header = $this.data( 'header' ); + if ( !$header ) { return; } // see #859 + if ( + !$header.is(':visible') || + ( !wo.resizable_addLastColumn && ts.resizable.checkVisibleColumns(c, column) ) + ) { + $this.hide(); + } else if ( column < columns || column === columns && wo.resizable_addLastColumn ) { + $this.css({ + display: 'inline-block', + height : tableHeight, + left : $header.position().left - startPosition + $header.outerWidth() - handleCenter + }); + } + }); + }, + + // Fixes #1485 + checkVisibleColumns: function( c, column ) { + var i, + len = 0; + for ( i = column + 1; i < c.columns; i++ ) { + len += c.$headerIndexed[i].is( ':visible' ) ? 1 : 0; + } + return len === 0; + }, + + // prevent text selection while dragging resize bar + toggleTextSelection : function( c, wo, toggle ) { + var namespace = c.namespace + 'tsresize'; + wo.resizable_vars.disabled = toggle; + $( 'body' ).toggleClass( ts.css.resizableNoSelect, toggle ); + if ( toggle ) { + $( 'body' ) + .attr( 'unselectable', 'on' ) + .bind( 'selectstart' + namespace, false ); + } else { + $( 'body' ) + .removeAttr( 'unselectable' ) + .unbind( 'selectstart' + namespace ); + } + }, + + bindings : function( c, wo ) { + var namespace = c.namespace + 'tsresize'; + wo.$resizable_container.children().bind( 'mousedown', function( event ) { + // save header cell and mouse position + var column, + vars = wo.resizable_vars, + $extras = $( c.namespace + '_extra_headers' ), + $header = $( event.target ).data( 'header' ); + + column = parseInt( $header.attr( 'data-column' ), 10 ); + vars.$target = $header = $header.add( $extras.filter('[data-column="' + column + '"]') ); + vars.target = column; + + // if table is not as wide as it's parent, then resize the table + vars.$next = event.shiftKey || wo.resizable_targetLast ? + $header.parent().children().not( '.resizable-false' ).filter( ':last' ) : + $header.nextAll( ':not(.resizable-false)' ).eq( 0 ); + + column = parseInt( vars.$next.attr( 'data-column' ), 10 ); + vars.$next = vars.$next.add( $extras.filter('[data-column="' + column + '"]') ); + vars.next = column; + + vars.mouseXPosition = event.pageX; + ts.resizable.updateStoredSizes( c, wo ); + ts.resizable.toggleTextSelection(c, wo, true ); + }); + + $( document ) + .bind( 'mousemove' + namespace, function( event ) { + var vars = wo.resizable_vars; + // ignore mousemove if no mousedown + if ( !vars.disabled || vars.mouseXPosition === 0 || !vars.$target ) { return; } + if ( wo.resizable_throttle ) { + clearTimeout( vars.timer ); + vars.timer = setTimeout( function() { + ts.resizable.mouseMove( c, wo, event ); + }, isNaN( wo.resizable_throttle ) ? 5 : wo.resizable_throttle ); + } else { + ts.resizable.mouseMove( c, wo, event ); + } + }) + .bind( 'mouseup' + namespace, function() { + if (!wo.resizable_vars.disabled) { return; } + ts.resizable.toggleTextSelection( c, wo, false ); + ts.resizable.stopResize( c, wo ); + ts.resizable.setHandlePosition( c, wo ); + }); + + // resizeEnd event triggered by scroller widget + $( window ).bind( 'resize' + namespace + ' resizeEnd' + namespace, function() { + ts.resizable.setHandlePosition( c, wo ); + }); + + // right click to reset columns to default widths + c.$table + .bind( 'columnUpdate pagerComplete resizableUpdate '.split( ' ' ).join( namespace + ' ' ), function() { + ts.resizable.setHandlePosition( c, wo ); + }) + .bind( 'resizableReset' + namespace, function() { + ts.resizableReset( c.table ); + }) + .find( 'thead:first' ) + .add( $( c.namespace + '_extra_table' ).find( 'thead:first' ) ) + .bind( 'contextmenu' + namespace, function() { + // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset + var allowClick = wo.resizable_vars.storedSizes.length === 0; + ts.resizableReset( c.table ); + ts.resizable.setHandlePosition( c, wo ); + wo.resizable_vars.storedSizes = []; + return allowClick; + }); + + }, + + mouseMove : function( c, wo, event ) { + if ( wo.resizable_vars.mouseXPosition === 0 || !wo.resizable_vars.$target ) { return; } + // resize columns + var column, + total = 0, + vars = wo.resizable_vars, + $next = vars.$next, + tar = vars.storedSizes[ vars.target ], + leftEdge = event.pageX - vars.mouseXPosition; + if ( vars.overflow ) { + if ( tar + leftEdge > 0 ) { + vars.storedSizes[ vars.target ] += leftEdge; + ts.resizable.setWidth( vars.$target, vars.storedSizes[ vars.target ], true ); + // update the entire table width + for ( column = 0; column < c.columns; column++ ) { + total += vars.storedSizes[ column ]; + } + ts.resizable.setWidth( c.$table.add( $( c.namespace + '_extra_table' ) ), total ); + } + if ( !$next.length ) { + // if expanding right-most column, scroll the wrapper + vars.$wrap[0].scrollLeft = c.$table.width(); + } + } else if ( vars.fullWidth ) { + vars.storedSizes[ vars.target ] += leftEdge; + vars.storedSizes[ vars.next ] -= leftEdge; + ts.resizable.setWidths( c, wo ); + } else { + vars.storedSizes[ vars.target ] += leftEdge; + ts.resizable.setWidths( c, wo ); + } + vars.mouseXPosition = event.pageX; + // dynamically update sticky header widths + c.$table.triggerHandler('stickyHeadersUpdate'); + }, + + stopResize : function( c, wo ) { + var vars = wo.resizable_vars; + ts.resizable.updateStoredSizes( c, wo ); + if ( vars.useStorage ) { + // save all column widths + ts.storage( c.table, ts.css.resizableStorage, vars.storedSizes ); + ts.storage( c.table, 'tablesorter-table-resized-width', c.$table.width() ); + } + vars.mouseXPosition = 0; + vars.$target = vars.$next = null; + // will update stickyHeaders, just in case, see #912 + c.$table.triggerHandler('stickyHeadersUpdate'); + c.$table.triggerHandler('resizableComplete'); + } + }; + + // this widget saves the column widths if + // $.tablesorter.storage function is included + // ************************** + ts.addWidget({ + id: 'resizable', + priority: 40, + options: { + resizable : true, // save column widths to storage + resizable_addLastColumn : false, + resizable_includeFooter: true, + resizable_widths : [], + resizable_throttle : false, // set to true (5ms) or any number 0-10 range + resizable_targetLast : false + }, + init: function(table, thisWidget, c, wo) { + ts.resizable.init( c, wo ); + }, + format: function( table, c, wo ) { + ts.resizable.setHandlePosition( c, wo ); + }, + remove: function( table, c, wo, refreshing ) { + if (wo.$resizable_container) { + var namespace = c.namespace + 'tsresize'; + c.$table.add( $( c.namespace + '_extra_table' ) ) + .removeClass('hasResizable') + .children( 'thead' ) + .unbind( 'contextmenu' + namespace ); + + wo.$resizable_container.remove(); + ts.resizable.toggleTextSelection( c, wo, false ); + ts.resizableReset( table, refreshing ); + $( document ).unbind( 'mousemove' + namespace + ' mouseup' + namespace ); + } + } + }); + + ts.resizableReset = function( table, refreshing ) { + $( table ).each(function() { + var index, $t, + c = this.config, + wo = c && c.widgetOptions, + vars = wo.resizable_vars; + if ( table && c && c.$headerIndexed.length ) { + // restore the initial table width + if ( vars.overflow && vars.tableWidth ) { + ts.resizable.setWidth( c.$table, vars.tableWidth, true ); + if ( vars.useStorage ) { + ts.storage( table, 'tablesorter-table-resized-width', vars.tableWidth ); + } + } + for ( index = 0; index < c.columns; index++ ) { + $t = c.$headerIndexed[ index ]; + if ( wo.resizable_widths && wo.resizable_widths[ index ] ) { + ts.resizable.setWidth( $t, wo.resizable_widths[ index ], vars.overflow ); + } else if ( !$t.hasClass( 'resizable-false' ) ) { + // don't clear the width of any column that is not resizable + ts.resizable.setWidth( $t, '', vars.overflow ); + } + } + + // reset stickyHeader widths + c.$table.triggerHandler( 'stickyHeadersUpdate' ); + if ( ts.storage && !refreshing ) { + ts.storage( this, ts.css.resizableStorage, [] ); + } + } + }); + }; + +})( jQuery, window ); + +/*! Widget: saveSort - updated 2018-03-19 (v2.30.1) *//* +* Requires tablesorter v2.16+ +* by Rob Garrison +*/ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + function getStoredSortList(c) { + var stored = ts.storage( c.table, 'tablesorter-savesort' ); + return (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : []; + } + + function sortListChanged(c, sortList) { + return (sortList || getStoredSortList(c)).join(',') !== c.sortList.join(','); + } + + // this widget saves the last sort only if the + // saveSort widget option is true AND the + // $.tablesorter.storage function is included + // ************************** + ts.addWidget({ + id: 'saveSort', + priority: 20, + options: { + saveSort : true + }, + init: function(table, thisWidget, c, wo) { + // run widget format before all other widgets are applied to the table + thisWidget.format(table, c, wo, true); + }, + format: function(table, c, wo, init) { + var time, + $table = c.$table, + saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true + sortList = { 'sortList' : c.sortList }, + debug = ts.debug(c, 'saveSort'); + if (debug) { + time = new Date(); + } + if ($table.hasClass('hasSaveSort')) { + if (saveSort && table.hasInitialized && ts.storage && sortListChanged(c)) { + ts.storage( table, 'tablesorter-savesort', sortList ); + if (debug) { + console.log('saveSort >> Saving last sort: ' + c.sortList + ts.benchmark(time)); + } + } + } else { + // set table sort on initial run of the widget + $table.addClass('hasSaveSort'); + sortList = ''; + // get data + if (ts.storage) { + sortList = getStoredSortList(c); + if (debug) { + console.log('saveSort >> Last sort loaded: "' + sortList + '"' + ts.benchmark(time)); + } + $table.bind('saveSortReset', function(event) { + event.stopPropagation(); + ts.storage( table, 'tablesorter-savesort', '' ); + }); + } + // init is true when widget init is run, this will run this widget before all other widgets have initialized + // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. + if (init && sortList && sortList.length > 0) { + c.sortList = sortList; + } else if (table.hasInitialized && sortList && sortList.length > 0) { + // update sort change + if (sortListChanged(c, sortList)) { + ts.sortOn(c, sortList); + } + } + } + }, + remove: function(table, c) { + c.$table.removeClass('hasSaveSort'); + // clear storage + if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); } + } + }); + +})(jQuery); +return jQuery.tablesorter;})); diff --git a/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js b/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js index e399f15..fe1baaa 100644 --- a/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js +++ b/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.js @@ -1,1392 +1,2914 @@ -/*! -* TableSorter 2.7.5 - Client-side table sorting with ease! +/*! TableSorter (FORK) v2.31.3 *//* +* Client-side table sorting with ease! * @requires jQuery v1.2.6+ * * Copyright (c) 2007 Christian Bach -* Examples and docs at: http://tablesorter.com +* fork maintained by Rob Garrison +* +* Examples and original 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 +* @name tablesorter (FORK) * @cat Plugins/Tablesorter -* @author Christian Bach/christian.bach@polyester.se -* @contributor Rob Garrison/https://github.com/Mottie/tablesorter +* @author Christian Bach - christian.bach@polyester.se +* @contributor Rob Garrison - https://github.com/Mottie/tablesorter +* @docs (fork) - https://mottie.github.io/tablesorter/docs/ */ /*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.7.5"; - - ts.parsers = []; - ts.widgets = []; - ts.defaults = { - - // *** appearance - theme : 'default', // adds tablesorter-{theme} to the table for styling - widthFixed : false, // adds colgroup to fix widths of columns - showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered. - - headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = (class from cssIcon) - onRenderTemplate : null, // function(index, template){ return template; }, (template is a string) - onRenderHeader : null, // function(index){}, (nothing to return) - - // *** functionality - cancelSelection : true, // prevent text selection in the header - dateFormat : 'mmddyyyy', // other options: "ddmmyyy" or "yyyymmdd" - sortMultiSortKey : 'shiftKey', // key used to select additional columns - sortResetKey : 'ctrlKey', // key used to remove sorting on a column - usNumberFormat : true, // false for German "1.234.567,89" or French "1 234 567,89" - delayInit : false, // if false, the parsed table contents will not update until the first sort - serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used. - - // *** sort options - headers : {}, // set sorter, string, empty, locked order, sortInitialOrder, filter, etc. - ignoreCase : true, // ignore case while sorting - sortForce : null, // column(s) first sorted; always applied - sortList : [], // Initial sort order; applied initially; updated when manually sorted - sortAppend : null, // column(s) sorted last; always applied - - sortInitialOrder : 'asc', // sort direction on first click - sortLocaleCompare: false, // replace equivalent character (accented characters) - sortReset : false, // third click on the header will reset column to default - unsorted - sortRestart : false, // restart sort to "sortInitialOrder" when clicking on previously unsorted columns - - emptyTo : 'bottom', // sort empty cell to bottom, top, none, zero - stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero - textExtraction : 'simple', // text extraction method/function - function(node, table, cellIndex){} - textSorter : null, // use custom text sorter - function(a,b){ return a.sort(b); } // basic sort - - // *** widget options - widgets: [], // method to add widgets, e.g. widgets: ['zebra'] - widgetOptions : { - zebra : [ 'even', 'odd' ] // zebra widget alternating row class names - }, - initWidgets : true, // apply widgets on tablesorter initialization - - // *** callbacks - initialized : null, // function(table){}, - - // *** css class names - tableClass : 'tablesorter', - cssAsc : 'tablesorter-headerAsc', - cssChildRow : 'tablesorter-childRow', // previously "expand-child" - cssDesc : 'tablesorter-headerDesc', - cssHeader : 'tablesorter-header', - cssHeaderRow : 'tablesorter-headerRow', - cssIcon : 'tablesorter-icon', // if this class exists, a will be added to the header automatically - cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name - cssProcessing : 'tablesorter-processing', // processing icon applied to header during sort/filter - - // *** selectors - selectorHeaders : '> thead th, > thead td', - selectorSort : 'th, td', // jQuery selector of content within selectorHeaders that is clickable to trigger a sort - selectorRemove : '.remove-me', - - // *** advanced - debug : false, - - // *** Internal variables - headerList: [], - empties: {}, - strings: {}, - parsers: [] +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter = { + + version : '2.31.3', + + parsers : [], + widgets : [], + defaults : { + + // *** appearance + theme : 'default', // adds tablesorter-{theme} to the table for styling + widthFixed : false, // adds colgroup to fix widths of columns + showProcessing : false, // show an indeterminate timer icon in the header when the table is sorted or filtered. + + headerTemplate : '{content}',// header layout template (HTML ok); {content} = innerHTML, {icon} = // class from cssIcon + onRenderTemplate : null, // function( index, template ) { return template; }, // template is a string + onRenderHeader : null, // function( index ) {}, // nothing to return + + // *** functionality + cancelSelection : true, // prevent text selection in the header + tabIndex : true, // add tabindex to header for keyboard accessibility + dateFormat : 'mmddyyyy', // other options: 'ddmmyyy' or 'yyyymmdd' + sortMultiSortKey : 'shiftKey', // key used to select additional columns + sortResetKey : 'ctrlKey', // key used to remove sorting on a column + usNumberFormat : true, // false for German '1.234.567,89' or French '1 234 567,89' + delayInit : false, // if false, the parsed table contents will not update until the first sort + serverSideSorting: false, // if true, server-side sorting should be performed because client-side sorting will be disabled, but the ui and events will still be used. + resort : true, // default setting to trigger a resort after an 'update', 'addRows', 'updateCell', etc has completed + + // *** 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, emptyMax, emptyMin + stringTo : 'max', // sort strings in numerical column as max, min, top, bottom, zero + duplicateSpan : true, // colspan cells in the tbody will have duplicated content in the cache for each spanned column + textExtraction : 'basic', // text extraction method/function - function( node, table, cellIndex ) {} + textAttribute : 'data-text',// data-attribute that contains alternate cell text (used in default 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 + initWidgets : true, // apply widgets on tablesorter initialization + widgetClass : 'widget-{name}', // table class name template to match to include a widget + widgets : [], // method to add widgets, e.g. widgets: ['zebra'] + widgetOptions : { + zebra : [ 'even', 'odd' ] // zebra widget alternating row class names + }, + + // *** 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 its parent + cssInfoBlock : 'tablesorter-infoOnly', // don't sort tbody with this class name (only one class name allowed here!) + cssNoSort : 'tablesorter-noSort', // class name added to element inside header; clicking on it won't cause a sort + cssIgnoreRow : 'tablesorter-ignoreRow',// header row to ignore; cells within this row will not be added to c.$headers + + cssIcon : 'tablesorter-icon', // if this class does not exist, the {icon} will not be added from the headerTemplate + cssIconNone : '', // class name added to the icon when there is no column sort + cssIconAsc : '', // class name added to the icon when the column has an ascending sort + cssIconDesc : '', // class name added to the icon when the column has a descending sort + cssIconDisabled : '', // class name added to the icon when the column has a disabled sort + + // *** events + pointerClick : 'click', + pointerDown : 'mousedown', + pointerUp : 'mouseup', + + // *** 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: [], + + // *** parser options for validator; values must be falsy! + globalize: 0, + imgAttr: 0 + + // removed: widgetZebra: { css: ['even', 'odd'] } - // 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 + css : { + table : 'tablesorter', + cssHasChild: 'tablesorter-hasChildRow', + childRow : 'tablesorter-childRow', + colgroup : 'tablesorter-colgroup', + header : 'tablesorter-header', + headerRow : 'tablesorter-headerRow', + headerIn : 'tablesorter-header-inner', + icon : 'tablesorter-icon', + processing : 'tablesorter-processing', + sortAsc : 'tablesorter-headerAsc', + sortDesc : 'tablesorter-headerDesc', + sortNone : 'tablesorter-headerUnSorted' + }, - /* debuging utils */ - function log(s) { - if (typeof console !== "undefined" && typeof console.log !== "undefined") { - console.log(s); - } else { - alert(s); + // labels applied to sortable headers for accessibility (aria) support + language : { + sortAsc : 'Ascending sort applied, ', + sortDesc : 'Descending sort applied, ', + sortNone : 'No sort applied, ', + sortDisabled : 'sorting is disabled', + nextAsc : 'activate to apply an ascending sort', + nextDesc : 'activate to apply a descending sort', + nextNone : 'activate to remove the sort' + }, + + regex : { + templateContent : /\{content\}/g, + templateIcon : /\{icon\}/g, + templateName : /\{name\}/i, + spaces : /\s+/g, + nonWord : /\W/g, + formElements : /(input|select|button|textarea)/i, + + // *** sort functions *** + // regex used in natural sort + // chunk/tokenize numbers & letters + chunk : /(^([+\-]?(?:\d*)(?:\.\d*)?(?:[eE][+\-]?\d+)?)?$|^0x[0-9a-f]+$|\d+)/gi, + // replace chunks @ ends + chunks : /(^\\0|\\0$)/, + hex : /^0x[0-9a-f]+$/i, + + // *** formatFloat *** + comma : /,/g, + digitNonUS : /[\s|\.]/g, + digitNegativeTest : /^\s*\([.\d]+\)/, + digitNegativeReplace : /^\s*\(([.\d]+)\)/, + + // *** isDigit *** + digitTest : /^[\-+(]?\d+[)]?$/, + digitReplace : /[,.'"\s]/g + + }, + + // digit sort, text location + string : { + max : 1, + min : -1, + emptymin : 1, + emptymax : -1, + zero : 0, + none : 0, + 'null' : 0, + top : true, + bottom : false + }, + + keyCodes : { + enter : 13 + }, + + // placeholder date parser data (globalize) + dates : {}, + + // These methods can be applied on table.config instance + instanceMethods : {}, + + /* + ▄█████ ██████ ██████ ██ ██ █████▄ + ▀█▄ ██▄▄ ██ ██ ██ ██▄▄██ + ▀█▄ ██▀▀ ██ ██ ██ ██▀▀▀ + █████▀ ██████ ██ ▀████▀ ██ + */ + + 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 ) { + if ( ts.debug(c, 'core') ) { + if ( table.hasInitialized ) { + console.warn( 'Stopping initialization. Tablesorter has already been initialized' ); + } else { + console.error( 'Stopping initialization! No table, thead or tbody', table ); + } } + return; } - function benchmark(s, d) { - log(s + " (" + (new Date().getTime() - d.getTime()) + "ms)"); + var tmp = '', + $table = $( table ), + meta = $.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 ( ts.debug(c, 'core') ) { + console[ console.group ? 'group' : 'log' ]( 'Initializing tablesorter v' + ts.version ); + $.data( table, 'startoveralltimer', new Date() ); } - ts.benchmark = benchmark; + // 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( '.' ) ); + // ensure case insensitivity + c.emptyTo = c.emptyTo.toLowerCase(); + c.stringTo = c.stringTo.toLowerCase(); + c.last = { sortList : [], clickedIndex : -1 }; + // add table theme class only if there isn't already one there + if ( !/tablesorter\-/.test( $table.attr( 'class' ) ) ) { + tmp = ( c.theme !== '' ? ' tablesorter-' + c.theme : '' ); + } - function getElementText(table, node, cellIndex) { - if (!node) { return ""; } - var c = table.config, - t = c.textExtraction, text = ""; - if (t === "simple") { - if (c.supportsTextContent) { - text = node.textContent; // newer browsers support this - } else { - text = $(node).text(); - } - } else { - if (typeof(t) === "function") { - text = t(node, table, cellIndex); - } else if (typeof(t) === "object" && t.hasOwnProperty(cellIndex)) { - text = t[cellIndex](node, table, cellIndex); - } else { - text = c.supportsTextContent ? node.textContent : $(node).text(); - } + // 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( ts.regex.nonWord, '' ); + } + + c.table = table; + c.$table = $table + // add namespace to table to allow bindings on extra elements to target + // the parent table (e.g. parser-input-select) + .addClass( ts.css.table + ' ' + c.tableClass + tmp + ' ' + c.namespace.slice(1) ) + .attr( 'role', 'grid' ); + c.$headers = $table.find( c.selectorHeaders ); + + 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.children( 'caption' ).length ) { + tmp = c.$table.children( 'caption' )[ 0 ]; + if ( !tmp.id ) { tmp.id = c.namespace.slice( 1 ) + 'caption'; } + c.$table.attr( 'aria-labelledby', tmp.id ); + } + 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 + ts.buildHeaders( c ); + // fixate columns if the users supplies the fixedWidth option + // do this after theme has been applied + ts.fixColumnWidth( table ); + // add widgets from class name + ts.addWidgetFromClass( table ); + // add widget options before parsing (e.g. grouping widget has parser settings) + ts.applyWidgetOptions( table ); + // try to auto detect column type, and store in tables config + ts.setupParsers( c ); + // start total row count at zero + c.totalRows = 0; + // only validate options while debugging. See #1528 + if (c.debug) { + ts.validateOptions( c ); + } + // build the cache for the tbody cells + // delayInit will delay building the cache until the user starts a sort + if ( !c.delayInit ) { ts.buildCache( c ); } + // bind all header events and methods + ts.bindEvents( table, c.$headers, true ); + ts.bindMethods( c ); + // 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 ( meta && ( $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 ) { + // save sortList before any sortAppend is added + c.last.sortList = c.sortList; + ts.sortOn( c, c.sortList, {}, !c.initWidgets ); + } else { + ts.setHeadersCss( c ); + if ( c.initWidgets ) { + // apply widget format + ts.applyWidget( table, false ); } - return $.trim(text); } - function detectParserForColumn(table, rows, rowIndex, cellIndex) { - var i, l = 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); - } + // 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.timerProcessing ); + ts.isProcessing( table ); + if ( e.type === 'sortBegin' ) { + c.timerProcessing = setTimeout( function() { + ts.isProcessing( table, true ); + }, 500 ); + } + }); + } + + // initialized + table.hasInitialized = true; + table.isProcessing = false; + if ( ts.debug(c, 'core') ) { + console.log( 'Overall initialization time:' + ts.benchmark( $.data( table, 'startoveralltimer' ) ) ); + if ( ts.debug(c, 'core') && console.groupEnd ) { console.groupEnd(); } + } + $table.triggerHandler( 'tablesorter-initialized', table ); + if ( typeof c.initialized === 'function' ) { + c.initialized( table ); + } + }, + + bindMethods : function( c ) { + var $table = c.$table, + namespace = c.namespace, + events = ( 'sortReset update updateRows updateAll updateHeaders addRows updateCell updateComplete ' + + 'sorton appendCache updateCache applyWidgetId applyWidgets refreshWidgets destroy mouseup ' + + 'mouseleave ' ).split( ' ' ) + .join( namespace + ' ' ); + // apply easy methods that trigger bound events + $table + .unbind( events.replace( ts.regex.spaces, ' ' ) ) + .bind( 'sortReset' + namespace, function( e, callback ) { + e.stopPropagation(); + // using this.config to ensure functions are getting a non-cached version of the config + ts.sortReset( this.config, function( table ) { + if (table.isApplyingWidgets) { + // multiple triggers in a row... filterReset, then sortReset - see #1361 + // wait to update widgets + setTimeout( function() { + ts.applyWidget( table, '', callback ); + }, 100 ); } else { - keepLooking = false; + ts.applyWidget( table, '', callback ); } + }); + }) + .bind( 'updateAll' + namespace, function( e, resort, callback ) { + e.stopPropagation(); + ts.updateAll( this.config, resort, callback ); + }) + .bind( 'update' + namespace + ' updateRows' + namespace, function( e, resort, callback ) { + e.stopPropagation(); + ts.update( this.config, resort, callback ); + }) + .bind( 'updateHeaders' + namespace, function( e, callback ) { + e.stopPropagation(); + ts.updateHeaders( this.config, callback ); + }) + .bind( 'updateCell' + namespace, function( e, cell, resort, callback ) { + e.stopPropagation(); + ts.updateCell( this.config, cell, resort, callback ); + }) + .bind( 'addRows' + namespace, function( e, $row, resort, callback ) { + e.stopPropagation(); + ts.addRows( this.config, $row, resort, callback ); + }) + .bind( 'updateComplete' + namespace, function() { + this.isUpdating = false; + }) + .bind( 'sorton' + namespace, function( e, list, callback, init ) { + e.stopPropagation(); + ts.sortOn( this.config, list, callback, init ); + }) + .bind( 'appendCache' + namespace, function( e, callback, init ) { + e.stopPropagation(); + ts.appendCache( this.config, init ); + if ( $.isFunction( callback ) ) { + callback( this ); } - for (i = 1; i < l; i++) { - if (ts.parsers[i].is && ts.parsers[i].is(nodeValue, table, node)) { - return ts.parsers[i]; - } + }) + // $tbodies variable is used by the tbody sorting widget + .bind( 'updateCache' + namespace, function( e, callback, $tbodies ) { + e.stopPropagation(); + ts.updateCache( this.config, callback, $tbodies ); + }) + .bind( 'applyWidgetId' + namespace, function( e, id ) { + e.stopPropagation(); + ts.applyWidgetId( this, id ); + }) + .bind( 'applyWidgets' + namespace, function( e, callback ) { + e.stopPropagation(); + // apply widgets (false = not initializing) + ts.applyWidget( this, false, callback ); + }) + .bind( 'refreshWidgets' + namespace, function( e, all, dontapply ) { + e.stopPropagation(); + ts.refreshWidgets( this, all, dontapply ); + }) + .bind( 'removeWidget' + namespace, function( e, name, refreshing ) { + e.stopPropagation(); + ts.removeWidget( this, name, refreshing ); + }) + .bind( 'destroy' + namespace, function( e, removeClasses, callback ) { + e.stopPropagation(); + ts.destroy( this, removeClasses, callback ); + }) + .bind( 'resetToLoadState' + namespace, function( e ) { + e.stopPropagation(); + // remove all widgets + ts.removeWidget( this, true, false ); + var tmp = $.extend( true, {}, c.originalSettings ); + // restore original settings; this clears out current settings, but does not clear + // values saved to storage. + c = $.extend( true, {}, ts.defaults, tmp ); + c.originalSettings = tmp; + this.hasInitialized = false; + // setup the entire table again + ts.setup( this, c ); + }); + }, + + bindEvents : function( table, $headers, core ) { + table = $( table )[ 0 ]; + var tmp, + c = table.config, + namespace = c.namespace, + downTarget = null; + if ( core !== true ) { + $headers.addClass( namespace.slice( 1 ) + '_extra_headers' ); + tmp = ts.getClosest( $headers, 'table' ); + if ( tmp.length && tmp[ 0 ].nodeName === 'TABLE' && tmp[ 0 ] !== table ) { + $( tmp[ 0 ] ).addClass( namespace.slice( 1 ) + '_extra_table' ); } - // 0 is always the generic parser (text) - return ts.parsers[0]; } + tmp = ( c.pointerDown + ' ' + c.pointerUp + ' ' + c.pointerClick + ' sort keyup ' ) + .replace( ts.regex.spaces, ' ' ) + .split( ' ' ) + .join( namespace + ' ' ); + // 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( tmp ) + .bind( tmp, function( e, external ) { + var $cell, cell, temp, + $target = $( e.target ), + // wrap event type in spaces, so the match doesn't trigger on inner words + type = ' ' + e.type + ' '; + // only recognize left clicks + if ( ( ( e.which || e.button ) !== 1 && !type.match( ' ' + c.pointerClick + ' | sort | keyup ' ) ) || + // allow pressing enter + ( type === ' keyup ' && e.which !== ts.keyCodes.enter ) || + // allow triggering a click event (e.which is undefined) & ignore physical clicks + ( type.match( ' ' + c.pointerClick + ' ' ) && typeof e.which !== 'undefined' ) ) { + return; + } + // ignore mouseup if mousedown wasn't on the same target + if ( type.match( ' ' + c.pointerUp + ' ' ) && downTarget !== e.target && external !== true ) { + return; + } + // set target on mousedown + if ( type.match( ' ' + c.pointerDown + ' ' ) ) { + downTarget = e.target; + // preventDefault needed or jQuery v1.3.2 and older throws an + // "Uncaught TypeError: handler.apply is not a function" error + temp = $target.jquery.split( '.' ); + if ( temp[ 0 ] === '1' && temp[ 1 ] < 4 ) { e.preventDefault(); } + return; + } + downTarget = null; + $cell = ts.getClosest( $( this ), '.' + ts.css.header ); + // prevent sort being triggered on form elements + if ( ts.regex.formElements.test( e.target.nodeName ) || + // nosort class name, or elements within a nosort container + $target.hasClass( c.cssNoSort ) || $target.parents( '.' + c.cssNoSort ).length > 0 || + // disabled cell directly clicked + $cell.hasClass( 'sorter-false' ) || + // elements within a button + $target.parents( 'button' ).length > 0 ) { + return !c.cancelSelection; + } + if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { + ts.buildCache( c ); + } + // use column index from data-attribute or index of current row; fixes #1116 + c.last.clickedIndex = $cell.attr( 'data-column' ) || $cell.index(); + cell = c.$headerIndexed[ c.last.clickedIndex ][0]; + if ( cell && !cell.sortDisabled ) { + ts.initSort( c, 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+ + }); + } + }, - function buildParserCache(table) { - var c = table.config, - // update table bodies in case we start with an empty table - tb = c.$tbodies = c.$table.children('tbody:not(.' + c.cssInfoBlock + ')'), - rows, list, l, i, h, ch, p, parsersDebug = ""; - if ( tb.length === 0) { - return c.debug ? log('*Empty table!* Not building a parser cache') : ''; - } - rows = tb[0].rows; - if (rows[0]) { - list = []; - l = rows[0].cells.length; - for (i = 0; i < l; i++) { - // tons of thanks to AnthonyM1229 for working out the following selector (issue #74) to make this work in IE8! - // More fixes to this selector to work properly in iOS and jQuery 1.8+ (issue #132 & #174) - h = c.$headers.filter(':not([colspan])'); - h = h.add( c.$headers.filter('[colspan="1"]') ) // ie8 fix - .filter('[data-column="' + i + '"]:last'); - ch = c.headers[i]; - // get column parser - p = ts.getParserById( ts.getData(h, ch, 'sorter') ); - // empty cells behaviour - keeping emptyToBottom for backwards compatibility - c.empties[i] = ts.getData(h, ch, 'empty') || c.emptyTo || (c.emptyToBottom ? 'bottom' : 'top' ); - // text strings behaviour in numerical sorts - c.strings[i] = ts.getData(h, ch, 'string') || c.stringTo || 'max'; - if (!p) { - p = detectParserForColumn(table, rows, -1, i); - } - if (c.debug) { - parsersDebug += "column:" + i + "; parser:" + p.id + "; string:" + c.strings[i] + '; empty: ' + c.empties[i] + "\n"; + buildHeaders : function( c ) { + var $temp, icon, timer, indx; + c.headerList = []; + c.headerContent = []; + c.sortVars = []; + if ( ts.debug(c, 'core') ) { + timer = new Date(); + } + // children tr in tfoot - see issue #196 & #547 + // don't pass table.config to computeColumnIndex here - widgets (math) pass it to "quickly" index tbody cells + c.columns = ts.computeColumnIndex( c.$table.children( 'thead, tfoot' ).children( 'tr' ) ); + // add icon if cssIcon option exists + icon = c.cssIcon ? + '' : + ''; + // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683 + c.$headers = $( $.map( c.$table.find( c.selectorHeaders ), function( elem, index ) { + var configHeaders, header, column, template, tmp, + $elem = $( elem ); + // ignore cell (don't add it to c.$headers) if row has ignoreRow class + if ( ts.getClosest( $elem, 'tr' ).hasClass( c.cssIgnoreRow ) ) { return; } + // transfer data-column to element if not th/td - #1459 + if ( !/(th|td)/i.test( elem.nodeName ) ) { + tmp = ts.getClosest( $elem, 'th, td' ); + $elem.attr( 'data-column', tmp.attr( 'data-column' ) ); + } + // make sure to get header cell & not column indexed cell + configHeaders = ts.getColumnData( c.table, c.headers, index, true ); + // save original header content + c.headerContent[ index ] = $elem.html(); + // if headerTemplate is empty, don't reformat the header cell + if ( c.headerTemplate !== '' && !$elem.find( '.' + ts.css.headerIn ).length ) { + // set up header template + template = c.headerTemplate + .replace( ts.regex.templateContent, $elem.html() ) + .replace( ts.regex.templateIcon, $elem.find( '.' + ts.css.icon ).length ? '' : icon ); + if ( c.onRenderTemplate ) { + header = c.onRenderTemplate.apply( $elem, [ index, template ] ); + // only change t if something is returned + if ( header && typeof header === 'string' ) { + template = header; } - list.push(p); } + $elem.html( '
' + template + '
' ); // faster than wrapInner + } + if ( c.onRenderHeader ) { + c.onRenderHeader.apply( $elem, [ index, c, c.$table ] ); } - if (c.debug) { - log(parsersDebug); - } - return list; - } - - /* utils */ - function buildCache(table) { - var b = table.tBodies, - tc = table.config, - totalRows, - totalCells, - parsers = tc.parsers, - t, v, i, j, k, c, cols, cacheTime, colMax = []; - tc.cache = {}; - // if no parsers found, return - it's an empty table. - if (!parsers) { - return tc.debug ? log('*Empty table!* Not building a cache') : ''; - } - if (tc.debug) { - cacheTime = new Date(); - } - // processing icon - if (tc.showProcessing) { - ts.isProcessing(table, true); - } - for (k = 0; k < b.length; k++) { - tc.cache[k] = { row: [], normalized: [] }; - // ignore tbodies with class name from css.cssInfoBlock - if (!$(b[k]).hasClass(tc.cssInfoBlock)) { - totalRows = (b[k] && b[k].rows.length) || 0; - totalCells = (b[k].rows[0] && b[k].rows[0].cells.length) || 0; - for (i = 0; i < totalRows; ++i) { - /** Add the table data to main data array */ - c = $(b[k].rows[i]); - cols = []; - // if this is a child row, add it to the last row's children and continue to the next row - if (c.hasClass(tc.cssChildRow)) { - tc.cache[k].row[tc.cache[k].row.length - 1] = tc.cache[k].row[tc.cache[k].row.length - 1].add(c); - // go to the next for loop - continue; + column = parseInt( $elem.attr( 'data-column' ), 10 ); + elem.column = column; + tmp = ts.getOrder( ts.getData( $elem, configHeaders, 'sortInitialOrder' ) || c.sortInitialOrder ); + // this may get updated numerous times if there are multiple rows + c.sortVars[ column ] = { + count : -1, // set to -1 because clicking on the header automatically adds one + order : tmp ? + ( c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ] ) : // desc, asc, unsorted + ( c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ] ), // asc, desc, unsorted + lockedOrder : false, + sortedBy : '' + }; + tmp = ts.getData( $elem, configHeaders, 'lockedOrder' ) || false; + if ( typeof tmp !== 'undefined' && tmp !== false ) { + c.sortVars[ column ].lockedOrder = true; + c.sortVars[ column ].order = ts.getOrder( tmp ) ? [ 1, 1 ] : [ 0, 0 ]; + } + // add cell to headerList + c.headerList[ index ] = elem; + $elem.addClass( ts.css.header + ' ' + c.cssHeader ); + // add to parent in case there are multiple rows + ts.getClosest( $elem, 'tr' ) + .addClass( ts.css.headerRow + ' ' + c.cssHeaderRow ) + .attr( 'role', 'row' ); + // allow keyboard cursor to focus on element + if ( c.tabIndex ) { + $elem.attr( 'tabindex', 0 ); + } + return elem; + }) ); + // cache headers per column + c.$headerIndexed = []; + for ( indx = 0; indx < c.columns; indx++ ) { + // colspan in header making a column undefined + if ( ts.isEmptyObject( c.sortVars[ indx ] ) ) { + c.sortVars[ indx ] = {}; + } + // Use c.$headers.parent() in case selectorHeaders doesn't point to the th/td + $temp = c.$headers.filter( '[data-column="' + indx + '"]' ); + // target sortable column cells, unless there are none, then use non-sortable cells + // .last() added in jQuery 1.4; use .filter(':last') to maintain compatibility with jQuery v1.2.6 + c.$headerIndexed[ indx ] = $temp.length ? + $temp.not( '.sorter-false' ).length ? + $temp.not( '.sorter-false' ).filter( ':last' ) : + $temp.filter( ':last' ) : + $(); + } + c.$table.find( c.selectorHeaders ).attr({ + scope: 'col', + role : 'columnheader' + }); + // enable/disable sorting + ts.updateHeader( c ); + if ( ts.debug(c, 'core') ) { + console.log( 'Built headers:' + ts.benchmark( timer ) ); + console.log( c.$headers ); + } + }, + + // Use it to add a set of methods to table.config which will be available for all tables. + // This should be done before table initialization + addInstanceMethods : function( methods ) { + $.extend( ts.instanceMethods, methods ); + }, + + /* + █████▄ ▄████▄ █████▄ ▄█████ ██████ █████▄ ▄█████ + ██▄▄██ ██▄▄██ ██▄▄██ ▀█▄ ██▄▄ ██▄▄██ ▀█▄ + ██▀▀▀ ██▀▀██ ██▀██ ▀█▄ ██▀▀ ██▀██ ▀█▄ + ██ ██ ██ ██ ██ █████▀ ██████ ██ ██ █████▀ + */ + setupParsers : function( c, $tbodies ) { + var rows, list, span, max, colIndex, indx, header, configHeaders, + noParser, parser, extractor, time, tbody, len, + table = c.table, + tbodyIndex = 0, + debug = ts.debug(c, 'core'), + debugOutput = {}; + // update table bodies in case we start with an empty table + c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ); + tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies; + len = tbody.length; + if ( len === 0 ) { + return debug ? console.warn( 'Warning: *Empty table!* Not building a parser cache' ) : ''; + } else if ( debug ) { + time = new Date(); + console[ console.group ? 'group' : 'log' ]( 'Detecting parsers for each column' ); + } + list = { + extractors: [], + parsers: [] + }; + while ( tbodyIndex < len ) { + rows = tbody[ tbodyIndex ].rows; + if ( rows.length ) { + colIndex = 0; + max = c.columns; + for ( indx = 0; indx < max; indx++ ) { + header = c.$headerIndexed[ colIndex ]; + if ( header && header.length ) { + // get column indexed table cell; adding true parameter fixes #1362 but + // it would break backwards compatibility... + configHeaders = ts.getColumnData( table, c.headers, colIndex ); // , true ); + // get column parser/extractor + extractor = ts.getParserById( ts.getData( header, configHeaders, 'extractor' ) ); + parser = ts.getParserById( ts.getData( header, configHeaders, 'sorter' ) ); + noParser = ts.getData( header, configHeaders, 'parser' ) === 'false'; + // empty cells behaviour - keeping emptyToBottom for backwards compatibility + c.empties[colIndex] = ( + ts.getData( header, configHeaders, 'empty' ) || + c.emptyTo || ( c.emptyToBottom ? 'bottom' : 'top' ) ).toLowerCase(); + // text strings behaviour in numerical sorts + c.strings[colIndex] = ( + ts.getData( header, configHeaders, 'string' ) || + c.stringTo || + 'max' ).toLowerCase(); + if ( noParser ) { + parser = ts.getParserById( 'no-parser' ); + } + if ( !extractor ) { + // For now, maybe detect someday + extractor = false; + } + if ( !parser ) { + parser = ts.detectParserForColumn( c, rows, -1, colIndex ); } - tc.cache[k].row.push(c); - for (j = 0; j < totalCells; ++j) { - t = getElementText(table, c[0].cells[j], j); - // allow parsing if the string is empty, previously parsing would change it to zero, - // in case the parser needs to extract data from the table cell attributes - v = parsers[j].format(t, table, c[0].cells[j], j); - cols.push(v); - if ((parsers[j].type || '').toLowerCase() === "numeric") { - colMax[j] = Math.max(Math.abs(v), colMax[j] || 0); // determine column max value (ignore sign) + if ( debug ) { + debugOutput[ '(' + colIndex + ') ' + header.text() ] = { + parser : parser.id, + extractor : extractor ? extractor.id : 'none', + string : c.strings[ colIndex ], + empty : c.empties[ colIndex ] + }; + } + list.parsers[ colIndex ] = parser; + list.extractors[ colIndex ] = extractor; + span = header[ 0 ].colSpan - 1; + if ( span > 0 ) { + colIndex += span; + max += span; + while ( span + 1 > 0 ) { + // set colspan columns to use the same parsers & extractors + list.parsers[ colIndex - span ] = parser; + list.extractors[ colIndex - span ] = extractor; + span--; } } - cols.push(tc.cache[k].normalized.length); // add position for rowCache - tc.cache[k].normalized.push(cols); } - tc.cache[k].colMax = colMax; + colIndex++; } } - if (tc.showProcessing) { - ts.isProcessing(table); // remove processing icon + tbodyIndex += ( list.parsers.length ) ? len : 1; + } + if ( debug ) { + if ( !ts.isEmptyObject( debugOutput ) ) { + console[ console.table ? 'table' : 'log' ]( debugOutput ); + } else { + console.warn( ' No parsers detected!' ); + } + console.log( 'Completed detecting parsers' + ts.benchmark( time ) ); + if ( console.groupEnd ) { console.groupEnd(); } + } + c.parsers = list.parsers; + c.extractors = list.extractors; + }, + + addParser : function( parser ) { + var indx, + len = ts.parsers.length, + add = true; + for ( indx = 0; indx < len; indx++ ) { + if ( ts.parsers[ indx ].id.toLowerCase() === parser.id.toLowerCase() ) { + add = false; } - if (tc.debug) { - benchmark("Building cache for " + totalRows + " rows", cacheTime); + } + if ( add ) { + ts.parsers[ ts.parsers.length ] = parser; + } + }, + + getParserById : function( name ) { + /*jshint eqeqeq:false */ // eslint-disable-next-line eqeqeq + if ( name == 'false' ) { return false; } + var indx, + len = ts.parsers.length; + for ( indx = 0; indx < len; indx++ ) { + if ( ts.parsers[ indx ].id.toLowerCase() === ( name.toString() ).toLowerCase() ) { + return ts.parsers[ indx ]; } } + return false; + }, - // init flag (true) used by pager plugin to prevent widget application - function appendToTable(table, init) { - var c = table.config, - b = table.tBodies, - rows = [], - c2 = c.cache, - r, n, totalRows, checkCell, $bk, $tb, - i, j, k, l, pos, appendTime; - if (!c2[0]) { return; } // empty table - fixes #206 - if (c.debug) { - appendTime = new Date(); - } - for (k = 0; k < b.length; k++) { - $bk = $(b[k]); - if (!$bk.hasClass(c.cssInfoBlock)) { - // get tbody - $tb = ts.processTbody(table, $bk, true); - r = c2[k].row; - n = c2[k].normalized; - totalRows = n.length; - checkCell = totalRows ? (n[0].length - 1) : 0; - for (i = 0; i < totalRows; i++) { - pos = n[i][checkCell]; - rows.push(r[pos]); - // removeRows used by the pager plugin - if (!c.appender || !c.removeRows) { - l = r[pos].length; - for (j = 0; j < l; j++) { - $tb.append(r[pos][j]); - } - } + detectParserForColumn : function( c, rows, rowIndex, cellIndex ) { + var cur, $node, row, + indx = ts.parsers.length, + node = false, + nodeValue = '', + debug = ts.debug(c, 'core'), + keepLooking = true; + while ( nodeValue === '' && keepLooking ) { + rowIndex++; + row = rows[ rowIndex ]; + // stop looking after 50 empty rows + if ( row && rowIndex < 50 ) { + if ( row.className.indexOf( ts.cssIgnoreRow ) < 0 ) { + node = rows[ rowIndex ].cells[ cellIndex ]; + nodeValue = ts.getElementText( c, node, cellIndex ); + $node = $( node ); + if ( debug ) { + console.log( 'Checking if value was empty on row ' + rowIndex + ', column: ' + + cellIndex + ': "' + nodeValue + '"' ); } - // restore tbody - ts.processTbody(table, $tb, false); } + } else { + keepLooking = false; + } + } + while ( --indx >= 0 ) { + cur = ts.parsers[ indx ]; + // ignore the default text parser because it will always be true + if ( cur && cur.id !== 'text' && cur.is && cur.is( nodeValue, c.table, node, $node ) ) { + return cur; + } + } + // nothing found, return the generic parser (text) + return ts.getParserById( 'text' ); + }, + + getElementText : function( c, node, cellIndex ) { + if ( !node ) { return ''; } + var tmp, + extract = c.textExtraction || '', + // node could be a jquery object + // http://jsperf.com/jquery-vs-instanceof-jquery/2 + $node = node.jquery ? node : $( node ); + if ( typeof extract === 'string' ) { + // check data-attribute first when set to 'basic'; don't use node.innerText - it's really slow! + // http://www.kellegous.com/j/2013/02/27/innertext-vs-textcontent/ + if ( extract === 'basic' && typeof ( tmp = $node.attr( c.textAttribute ) ) !== 'undefined' ) { + return $.trim( tmp ); + } + return $.trim( node.textContent || $node.text() ); + } else { + if ( typeof extract === 'function' ) { + return $.trim( extract( $node[ 0 ], c.table, cellIndex ) ); + } else if ( typeof ( tmp = ts.getColumnData( c.table, extract, cellIndex ) ) === 'function' ) { + return $.trim( tmp( $node[ 0 ], c.table, cellIndex ) ); } - if (c.appender) { - c.appender(table, rows); - } - if (c.debug) { - benchmark("Rebuilt table", appendTime); - } - // apply table widgets - if (!init) { ts.applyWidget(table); } - // trigger sortend - $(table).trigger("sortEnd", table); - } - - // computeTableHeaderCellIndexes from: - // http://www.javascripttoolbox.com/lib/table/examples.php - // http://www.javascripttoolbox.com/temp/table_cellindex.html - function computeThIndexes(t) { - var matrix = [], - lookup = {}, - cols = 0, // determine the number of columns - trs = $(t).find('thead:eq(0), tfoot').children('tr'), // children tr in tfoot - see issue #196 - i, j, k, l, c, cells, rowIndex, cellId, rowSpan, colSpan, firstAvailCol, matrixrow; - for (i = 0; i < trs.length; i++) { - cells = trs[i].cells; - for (j = 0; j < cells.length; j++) { - c = cells[j]; - rowIndex = c.parentNode.rowIndex; - cellId = rowIndex + "-" + c.cellIndex; - rowSpan = c.rowSpan || 1; - colSpan = c.colSpan || 1; - if (typeof(matrix[rowIndex]) === "undefined") { - matrix[rowIndex] = []; + } + // fallback + return $.trim( $node[ 0 ].textContent || $node.text() ); + }, + + // centralized function to extract/parse cell contents + getParsedText : function( c, cell, colIndex, txt ) { + if ( typeof txt === 'undefined' ) { + txt = ts.getElementText( c, cell, colIndex ); + } + // if no parser, make sure to return the txt + var val = '' + txt, + parser = c.parsers[ colIndex ], + extractor = c.extractors[ colIndex ]; + if ( parser ) { + // do extract before parsing, if there is one + if ( extractor && typeof extractor.format === 'function' ) { + txt = extractor.format( txt, c.table, cell, colIndex ); + } + // 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 + val = parser.id === 'no-parser' ? '' : + // make sure txt is a string (extractor may have converted it) + parser.format( '' + txt, c.table, cell, colIndex ); + if ( c.ignoreCase && typeof val === 'string' ) { + val = val.toLowerCase(); + } + } + return val; + }, + + /* + ▄████▄ ▄████▄ ▄████▄ ██ ██ ██████ + ██ ▀▀ ██▄▄██ ██ ▀▀ ██▄▄██ ██▄▄ + ██ ▄▄ ██▀▀██ ██ ▄▄ ██▀▀██ ██▀▀ + ▀████▀ ██ ██ ▀████▀ ██ ██ ██████ + */ + buildCache : function( c, callback, $tbodies ) { + var cache, val, txt, rowIndex, colIndex, tbodyIndex, $tbody, $row, + cols, $cells, cell, cacheTime, totalRows, rowData, prevRowData, + colMax, span, cacheIndex, hasParser, max, len, index, + table = c.table, + parsers = c.parsers, + debug = ts.debug(c, 'core'); + // update tbody variable + c.$tbodies = c.$table.children( 'tbody:not(.' + c.cssInfoBlock + ')' ); + $tbody = typeof $tbodies === 'undefined' ? c.$tbodies : $tbodies, + c.cache = {}; + c.totalRows = 0; + // if no parsers found, return - it's an empty table. + if ( !parsers ) { + return debug ? console.warn( 'Warning: *Empty table!* Not building a cache' ) : ''; + } + if ( debug ) { + cacheTime = new Date(); + } + // processing icon + if ( c.showProcessing ) { + ts.isProcessing( table, true ); + } + for ( tbodyIndex = 0; tbodyIndex < $tbody.length; tbodyIndex++ ) { + colMax = []; // column max value per tbody + cache = c.cache[ tbodyIndex ] = { + normalized: [] // array of normalized row data; last entry contains 'rowData' above + // colMax: # // added at the end + }; + + totalRows = ( $tbody[ tbodyIndex ] && $tbody[ tbodyIndex ].rows.length ) || 0; + for ( rowIndex = 0; rowIndex < totalRows; ++rowIndex ) { + rowData = { + // order: original row order # + // $row : jQuery Object[] + child: [], // child row text (filter widget) + raw: [] // original row text + }; + /** Add the table data to main data array */ + $row = $( $tbody[ tbodyIndex ].rows[ rowIndex ] ); + cols = []; + // ignore "remove-me" rows + if ( $row.hasClass( c.selectorRemove.slice(1) ) ) { + continue; + } + // 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 ) && rowIndex !== 0 ) { + len = cache.normalized.length - 1; + prevRowData = cache.normalized[ len ][ c.columns ]; + prevRowData.$row = prevRowData.$row.add( $row ); + // add 'hasChild' class name to parent row + if ( !$row.prev().hasClass( c.cssChildRow ) ) { + $row.prev().addClass( ts.css.cssHasChild ); } - // 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; + // save child row content (un-parsed!) + $cells = $row.children( 'th, td' ); + len = prevRowData.child.length; + prevRowData.child[ len ] = []; + // child row content does not account for colspans/rowspans; so indexing may be off + cacheIndex = 0; + max = c.columns; + for ( colIndex = 0; colIndex < max; colIndex++ ) { + cell = $cells[ colIndex ]; + if ( cell ) { + prevRowData.child[ len ][ colIndex ] = ts.getParsedText( c, cell, colIndex ); + span = $cells[ colIndex ].colSpan - 1; + if ( span > 0 ) { + cacheIndex += span; + max += span; + } } + cacheIndex++; } - lookup[cellId] = firstAvailCol; - cols = Math.max(firstAvailCol, cols); - // add data-column - $(c).attr({ 'data-column' : firstAvailCol }); // 'data-row' : rowIndex - for (k = rowIndex; k < rowIndex + rowSpan; k++) { - if (typeof(matrix[k]) === "undefined") { - matrix[k] = []; + // go to the next for loop + continue; + } + rowData.$row = $row; + rowData.order = rowIndex; // add original row position to rowCache + cacheIndex = 0; + max = c.columns; + for ( colIndex = 0; colIndex < max; ++colIndex ) { + cell = $row[ 0 ].cells[ colIndex ]; + if ( cell && cacheIndex < c.columns ) { + hasParser = typeof parsers[ cacheIndex ] !== 'undefined'; + if ( !hasParser && debug ) { + console.warn( 'No parser found for row: ' + rowIndex + ', column: ' + colIndex + + '; cell containing: "' + $(cell).text() + '"; does it have a header?' ); } - matrixrow = matrix[k]; - for (l = firstAvailCol; l < firstAvailCol + colSpan; l++) { - matrixrow[l] = "x"; + val = ts.getElementText( c, cell, cacheIndex ); + rowData.raw[ cacheIndex ] = val; // save original row text + // save raw column text even if there is no parser set + txt = ts.getParsedText( c, cell, cacheIndex, val ); + cols[ cacheIndex ] = txt; + if ( hasParser && ( parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { + // determine column max value (ignore sign) + colMax[ cacheIndex ] = Math.max( Math.abs( txt ) || 0, colMax[ cacheIndex ] || 0 ); + } + // allow colSpan in tbody + span = cell.colSpan - 1; + if ( span > 0 ) { + index = 0; + while ( index <= span ) { + // duplicate text (or not) to spanned columns + // instead of setting duplicate span to empty string, use textExtraction to try to get a value + // see http://stackoverflow.com/q/36449711/145346 + txt = c.duplicateSpan || index === 0 ? + txt : + typeof c.textExtraction !== 'string' ? + ts.getElementText( c, cell, cacheIndex + index ) || '' : + ''; + rowData.raw[ cacheIndex + index ] = txt; + cols[ cacheIndex + index ] = txt; + index++; + } + cacheIndex += span; + max += span; } } + cacheIndex++; } + // ensure rowData is always in the same location (after the last column) + cols[ c.columns ] = rowData; + cache.normalized[ cache.normalized.length ] = cols; } - t.config.columns = cols; // may not be accurate if # header columns !== # tbody columns - return lookup; - } + cache.colMax = colMax; + // total up rows, not including child rows + c.totalRows += cache.normalized.length; - function formatSortingOrder(v) { - // look for "d" in "desc" order; return true - return (/^d/i.test(v) || v === 1); } - - function buildHeaders(table) { - var header_index = computeThIndexes(table), ch, $t, - h, i, t, lock, time, $tableHeaders, c = table.config; - c.headerList = [], c.headerContent = []; - if (c.debug) { - time = new Date(); - } - i = c.cssIcon ? '' : ''; // add icon if cssIcon option exists - $tableHeaders = $(table).find(c.selectorHeaders).each(function(index) { - $t = $(this); - ch = c.headers[index]; - c.headerContent[index] = this.innerHTML; // save original header content - // set up header template - t = c.headerTemplate.replace(/\{content\}/g, this.innerHTML).replace(/\{icon\}/g, i); - if (c.onRenderTemplate) { - h = c.onRenderTemplate.apply($t, [index, t]); - if (h && typeof h === 'string') { t = h; } // only change t if something is returned + if ( c.showProcessing ) { + ts.isProcessing( table ); // remove processing icon + } + if ( debug ) { + len = Math.min( 5, c.cache[ 0 ].normalized.length ); + console[ console.group ? 'group' : 'log' ]( 'Building cache for ' + c.totalRows + + ' rows (showing ' + len + ' rows in log) and ' + c.columns + ' columns' + + ts.benchmark( cacheTime ) ); + val = {}; + for ( colIndex = 0; colIndex < c.columns; colIndex++ ) { + for ( cacheIndex = 0; cacheIndex < len; cacheIndex++ ) { + if ( !val[ 'row: ' + cacheIndex ] ) { + val[ 'row: ' + cacheIndex ] = {}; + } + val[ 'row: ' + cacheIndex ][ c.$headerIndexed[ colIndex ].text() ] = + c.cache[ 0 ].normalized[ cacheIndex ][ colIndex ]; } - this.innerHTML = '
' + t + '
'; // faster than wrapInner - - if (c.onRenderHeader) { c.onRenderHeader.apply($t, [index]); } + } + console[ console.table ? 'table' : 'log' ]( val ); + if ( console.groupEnd ) { console.groupEnd(); } + } + if ( $.isFunction( callback ) ) { + callback( table ); + } + }, - this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; - this.order = formatSortingOrder( ts.getData($t, ch, 'sortInitialOrder') || c.sortInitialOrder ) ? [1,0,2] : [0,1,2]; - this.count = -1; // set to -1 because clicking on the header automatically adds one - if (ts.getData($t, ch, 'sorter') === 'false') { - this.sortDisabled = true; - $t.addClass('sorter-false'); - } else { - $t.removeClass('sorter-false'); - } - this.lockedOrder = false; - lock = ts.getData($t, ch, 'lockedOrder') || false; - if (typeof(lock) !== 'undefined' && lock !== false) { - this.order = this.lockedOrder = formatSortingOrder(lock) ? [1,1,1] : [0,0,0]; + getColumnText : function( table, column, callback, rowFilter ) { + table = $( table )[0]; + var tbodyIndex, rowIndex, cache, row, tbodyLen, rowLen, raw, parsed, $cell, result, + hasCallback = typeof callback === 'function', + allColumns = column === 'all', + data = { raw : [], parsed: [], $cell: [] }, + c = table.config; + if ( ts.isEmptyObject( c ) ) { + if ( ts.debug(c, 'core') ) { + console.warn( 'No cache found - aborting getColumnText function!' ); + } + } else { + tbodyLen = c.$tbodies.length; + for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) { + cache = c.cache[ tbodyIndex ].normalized; + rowLen = cache.length; + for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) { + row = cache[ rowIndex ]; + if ( rowFilter && !row[ c.columns ].$row.is( rowFilter ) ) { + continue; + } + result = true; + parsed = ( allColumns ) ? row.slice( 0, c.columns ) : row[ column ]; + row = row[ c.columns ]; + raw = ( allColumns ) ? row.raw : row.raw[ column ]; + $cell = ( allColumns ) ? row.$row.children() : row.$row.children().eq( column ); + if ( hasCallback ) { + result = callback({ + tbodyIndex : tbodyIndex, + rowIndex : rowIndex, + parsed : parsed, + raw : raw, + $row : row.$row, + $cell : $cell + }); + } + if ( result !== false ) { + data.parsed[ data.parsed.length ] = parsed; + data.raw[ data.raw.length ] = raw; + data.$cell[ data.$cell.length ] = $cell; + } } - $t.addClass( (this.sortDisabled ? 'sorter-false ' : ' ') + c.cssHeader ); - // add cell to headerList - c.headerList[index] = this; - // add to parent in case there are multiple rows - $t.parent().addClass(c.cssHeaderRow); - }); - if (table.config.debug) { - benchmark("Built headers:", time); - log($tableHeaders); } - return $tableHeaders; + // return everything + return data; } + }, - function setHeadersCss(table) { - var f, i, j, l, - c = table.config, - list = c.sortList, - css = [c.cssAsc, c.cssDesc], - // find the footer - $t = $(table).find('tfoot tr').children().removeClass(css.join(' ')); + /* + ██ ██ █████▄ █████▄ ▄████▄ ██████ ██████ + ██ ██ ██▄▄██ ██ ██ ██▄▄██ ██ ██▄▄ + ██ ██ ██▀▀▀ ██ ██ ██▀▀██ ██ ██▀▀ + ▀████▀ ██ █████▀ ██ ██ ██ ██████ + */ + setHeadersCss : function( c ) { + var indx, column, + list = c.sortList, + len = list.length, + none = ts.css.sortNone + ' ' + c.cssNone, + css = [ ts.css.sortAsc + ' ' + c.cssAsc, ts.css.sortDesc + ' ' + c.cssDesc ], + cssIcon = [ c.cssIconAsc, c.cssIconDesc, c.cssIconNone ], + aria = [ 'ascending', 'descending' ], + updateColumnSort = function($el, index) { + $el + .removeClass( none ) + .addClass( css[ index ] ) + .attr( 'aria-sort', aria[ index ] ) + .find( '.' + ts.css.icon ) + .removeClass( cssIcon[ 2 ] ) + .addClass( cssIcon[ index ] ); + }, + // find the footer + $extras = c.$table + .find( 'tfoot tr' ) + .children( 'td, th' ) + .add( $( c.namespace + '_extra_headers' ) ) + .removeClass( css.join( ' ' ) ), // remove all header information - c.$headers.removeClass(css.join(' ')); - l = list.length; - for (i = 0; i < l; i++) { - // direction = 2 means reset! - if (list[i][1] !== 2) { - // multicolumn sorting updating - choose the :last in case there are nested columns - f = c.$headers.not('.sorter-false').filter('[data-column="' + list[i][0] + '"]' + (l === 1 ? ':last' : '') ); - if (f.length) { - for (j = 0; j < f.length; j++) { - if (!f[j].sortDisabled) { - f.eq(j).addClass(css[list[i][1]]); - // add sorted class to footer, if it exists - if ($t.length) { - $t.filter('[data-column="' + list[i][0] + '"]').eq(j).addClass(css[list[i][1]]); - } - } + $sorted = c.$headers + .add( $( 'thead ' + c.namespace + '_extra_headers' ) ) + .removeClass( css.join( ' ' ) ) + .addClass( none ) + .attr( 'aria-sort', 'none' ) + .find( '.' + ts.css.icon ) + .removeClass( cssIcon.join( ' ' ) ) + .end(); + // add css none to all sortable headers + $sorted + .not( '.sorter-false' ) + .find( '.' + ts.css.icon ) + .addClass( cssIcon[ 2 ] ); + // add disabled css icon class + if ( c.cssIconDisabled ) { + $sorted + .filter( '.sorter-false' ) + .find( '.' + ts.css.icon ) + .addClass( c.cssIconDisabled ); + } + for ( indx = 0; indx < len; indx++ ) { + // direction = 2 means reset! + if ( list[ indx ][ 1 ] !== 2 ) { + // multicolumn sorting updating - see #1005 + // .not(function() {}) needs jQuery 1.4 + // filter(function(i, el) {}) <- el is undefined in jQuery v1.2.6 + $sorted = c.$headers.filter( function( i ) { + // only include headers that are in the sortList (this includes colspans) + var include = true, + $el = c.$headers.eq( i ), + col = parseInt( $el.attr( 'data-column' ), 10 ), + end = col + ts.getClosest( $el, 'th, td' )[0].colSpan; + for ( ; col < end; col++ ) { + include = include ? include || ts.isValueInArray( col, c.sortList ) > -1 : false; + } + return include; + }); + + // choose the :last in case there are nested columns + $sorted = $sorted + .not( '.sorter-false' ) + .filter( '[data-column="' + list[ indx ][ 0 ] + '"]' + ( len === 1 ? ':last' : '' ) ); + if ( $sorted.length ) { + for ( column = 0; column < $sorted.length; column++ ) { + if ( !$sorted[ column ].sortDisabled ) { + updateColumnSort( $sorted.eq( column ), list[ indx ][ 1 ] ); } } } + // add sorted class to footer & extra headers, if they exist + if ( $extras.length ) { + updateColumnSort( $extras.filter( '[data-column="' + list[ indx ][ 0 ] + '"]' ), list[ indx ][ 1 ] ); + } } } + // add verbose aria labels + len = c.$headers.length; + for ( indx = 0; indx < len; indx++ ) { + ts.setColumnAriaLabel( c, c.$headers.eq( indx ) ); + } + }, - // automatically add col group, and column sizes if set - function fixColumnWidth(table) { - var $c, c = table.config, - $cg = $(''), - $cgo = c.$table.find('colgroup'), - n = c.columns.length, - overallWidth = c.$table.width(); - $("tr:first td", table.tBodies[0]).each(function(i) { - $c = $(''); - if (c.widthFixed) { - $c.css('width', parseInt(($(this).width()/overallWidth)*1000, 10)/10 + '%'); - } - $cg.append($c); - }); - // replace colgroup contents - if ($cgo.length) { - $cgo.html( $cg.html() ); + getClosest : function( $el, selector ) { + // jQuery v1.2.6 doesn't have closest() + if ( $.fn.closest ) { + return $el.closest( selector ); + } + return $el.is( selector ) ? + $el : + $el.parents( selector ).filter( ':first' ); + }, + + // nextSort (optional), lets you disable next sort text + setColumnAriaLabel : function( c, $header, nextSort ) { + if ( $header.length ) { + var column = parseInt( $header.attr( 'data-column' ), 10 ), + vars = c.sortVars[ column ], + tmp = $header.hasClass( ts.css.sortAsc ) ? + 'sortAsc' : + $header.hasClass( ts.css.sortDesc ) ? 'sortDesc' : 'sortNone', + txt = $.trim( $header.text() ) + ': ' + ts.language[ tmp ]; + if ( $header.hasClass( 'sorter-false' ) || nextSort === false ) { + txt += ts.language.sortDisabled; } else { - c.$table.prepend( $cg ); + tmp = ( vars.count + 1 ) % vars.order.length; + nextSort = vars.order[ tmp ]; + // if nextSort + txt += ts.language[ nextSort === 0 ? 'nextAsc' : nextSort === 1 ? 'nextDesc' : 'nextNone' ]; + } + $header.attr( 'aria-label', txt ); + if (vars.sortedBy) { + $header.attr( 'data-sortedBy', vars.sortedBy ); + } else { + $header.removeAttr('data-sortedBy'); } } + }, - function updateHeaderSortCount(table, list) { - var s, t, o, c = table.config, - sl = list || c.sortList; - c.sortList = []; - $.each(sl, function(i,v){ - // ensure all sortList values are numeric - fixes #127 - s = [ parseInt(v[0], 10), parseInt(v[1], 10) ]; - // make sure header exists - o = c.headerList[s[0]]; - if (o) { // prevents error if sorton array is wrong - c.sortList.push(s); - t = $.inArray(s[1], o.order); // fixes issue #167 - o.count = t >= 0 ? t : s[1] % (c.sortReset ? 3 : 2); - } - }); + updateHeader : function( c ) { + var index, isDisabled, $header, col, + table = c.table, + len = c.$headers.length; + for ( index = 0; index < len; index++ ) { + $header = c.$headers.eq( index ); + col = ts.getColumnData( table, c.headers, index, true ); + // add 'sorter-false' class if 'parser-false' is set + isDisabled = ts.getData( $header, col, 'sorter' ) === 'false' || ts.getData( $header, col, 'parser' ) === 'false'; + ts.setColumnSort( c, $header, isDisabled ); } + }, - function getCachedSortType(parsers, i) { - return (parsers && parsers[i]) ? parsers[i].type || '' : ''; + setColumnSort : function( c, $header, isDisabled ) { + var id = c.table.id; + $header[ 0 ].sortDisabled = isDisabled; + $header[ isDisabled ? 'addClass' : 'removeClass' ]( 'sorter-false' ) + .attr( 'aria-disabled', '' + isDisabled ); + // disable tab index on disabled cells + if ( c.tabIndex ) { + if ( isDisabled ) { + $header.removeAttr( 'tabindex' ); + } else { + $header.attr( 'tabindex', '0' ); + } + } + // aria-controls - requires table ID + if ( id ) { + if ( isDisabled ) { + $header.removeAttr( 'aria-controls' ); + } else { + $header.attr( 'aria-controls', id ); + } } + }, - // sort multiple columns - function multisort(table) { /*jshint loopfunc:true */ - var dynamicExp, sortWrapper, col, mx = 0, dir = 0, tc = table.config, - sortList = tc.sortList, l = sortList.length, bl = table.tBodies.length, - sortTime, i, j, k, c, colMax, cache, lc, s, e, order, orgOrderCol; - if (tc.serverSideSorting || !tc.cache[0]) { // empty table - fixes #206 - return; + updateHeaderSortCount : function( c, list ) { + var col, dir, group, indx, primary, temp, val, order, + sortList = list || c.sortList, + len = sortList.length; + c.sortList = []; + for ( indx = 0; indx < len; indx++ ) { + val = sortList[ indx ]; + // ensure all sortList values are numeric - fixes #127 + col = parseInt( val[ 0 ], 10 ); + // prevents error if sorton array is wrong + if ( col < c.columns ) { + + // set order if not already defined - due to colspan header without associated header cell + // adding this check prevents a javascript error + if ( !c.sortVars[ col ].order ) { + if ( ts.getOrder( c.sortInitialOrder ) ) { + order = c.sortReset ? [ 1, 0, 2 ] : [ 1, 0 ]; + } else { + order = c.sortReset ? [ 0, 1, 2 ] : [ 0, 1 ]; + } + c.sortVars[ col ].order = order; + c.sortVars[ col ].count = 0; + } + + order = c.sortVars[ col ].order; + dir = ( '' + val[ 1 ] ).match( /^(1|d|s|o|n)/ ); + dir = dir ? dir[ 0 ] : ''; + // 0/(a)sc (default), 1/(d)esc, (s)ame, (o)pposite, (n)ext + switch ( dir ) { + case '1' : case 'd' : // descending + dir = 1; + break; + case 's' : // same direction (as primary column) + // if primary sort is set to 's', make it ascending + dir = primary || 0; + break; + case 'o' : + temp = order[ ( primary || 0 ) % order.length ]; + // opposite of primary column; but resets if primary resets + dir = temp === 0 ? 1 : temp === 1 ? 0 : 2; + break; + case 'n' : + dir = order[ ( ++c.sortVars[ col ].count ) % order.length ]; + break; + default : // ascending + dir = 0; + break; + } + primary = indx === 0 ? dir : primary; + group = [ col, parseInt( dir, 10 ) || 0 ]; + c.sortList[ c.sortList.length ] = group; + dir = $.inArray( group[ 1 ], order ); // fixes issue #167 + c.sortVars[ col ].count = dir >= 0 ? dir : group[ 1 ] % order.length; } - if (tc.debug) { sortTime = new Date(); } - for (k = 0; k < bl; k++) { - colMax = tc.cache[k].colMax; - cache = tc.cache[k].normalized; - lc = cache.length; - orgOrderCol = (cache && cache[0]) ? cache[0].length - 1 : 0; - cache.sort(function(a, b) { - // cache is undefined here in IE, so don't use it! - for (i = 0; i < l; i++) { - c = sortList[i][0]; - order = sortList[i][1]; - // fallback to natural sort since it is more robust - s = /n/i.test(getCachedSortType(tc.parsers, c)) ? "Numeric" : "Text"; - s += order === 0 ? "" : "Desc"; - if (/Numeric/.test(s) && tc.strings[c]) { - // sort strings in numerical columns - if (typeof (tc.string[tc.strings[c]]) === 'boolean') { - dir = (order === 0 ? 1 : -1) * (tc.string[tc.strings[c]] ? -1 : 1); - } else { - dir = (tc.strings[c]) ? tc.string[tc.strings[c]] || 0 : 0; - } - } - var sort = $.tablesorter["sort" + s](table, a[c], b[c], c, colMax[c], dir); - if (sort) { return sort; } + } + }, + + updateAll : function( c, resort, callback ) { + var table = c.table; + table.isUpdating = true; + ts.refreshWidgets( table, true, true ); + ts.buildHeaders( c ); + ts.bindEvents( table, c.$headers, true ); + ts.bindMethods( c ); + ts.commonUpdate( c, resort, callback ); + }, + + update : function( c, resort, callback ) { + var table = c.table; + table.isUpdating = true; + // update sorting (if enabled/disabled) + ts.updateHeader( c ); + ts.commonUpdate( c, resort, callback ); + }, + + // simple header update - see #989 + updateHeaders : function( c, callback ) { + c.table.isUpdating = true; + ts.buildHeaders( c ); + ts.bindEvents( c.table, c.$headers, true ); + ts.resortComplete( c, callback ); + }, + + updateCell : function( c, cell, resort, callback ) { + // updateCell for child rows is a mess - we'll ignore them for now + // eventually I'll break out the "update" row cache code to make everything consistent + if ( $( cell ).closest( 'tr' ).hasClass( c.cssChildRow ) ) { + console.warn('Tablesorter Warning! "updateCell" for child row content has been disabled, use "update" instead'); + return; + } + if ( ts.isEmptyObject( c.cache ) ) { + // empty table, do an update instead - fixes #1099 + ts.updateHeader( c ); + ts.commonUpdate( c, resort, callback ); + return; + } + c.table.isUpdating = true; + c.$table.find( c.selectorRemove ).remove(); + // get position from the dom + var tmp, indx, row, icell, cache, len, + $tbodies = c.$tbodies, + $cell = $( cell ), + // update cache - format: function( s, table, cell, cellIndex ) + // no closest in jQuery v1.2.6 + tbodyIndex = $tbodies.index( ts.getClosest( $cell, 'tbody' ) ), + tbcache = c.cache[ tbodyIndex ], + $row = ts.getClosest( $cell, 'tr' ); + 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 ( $tbodies.length && tbodyIndex >= 0 ) { + row = $tbodies.eq( tbodyIndex ).find( 'tr' ).not( '.' + c.cssChildRow ).index( $row ); + cache = tbcache.normalized[ row ]; + len = $row[ 0 ].cells.length; + if ( len !== c.columns ) { + // colspan in here somewhere! + icell = 0; + tmp = false; + for ( indx = 0; indx < len; indx++ ) { + if ( !tmp && $row[ 0 ].cells[ indx ] !== cell ) { + icell += $row[ 0 ].cells[ indx ].colSpan; + } else { + tmp = true; } - return a[orgOrderCol] - b[orgOrderCol]; - }); + } + } else { + icell = $cell.index(); } - if (tc.debug) { benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time", sortTime); } + tmp = ts.getElementText( c, cell, icell ); // raw + cache[ c.columns ].raw[ icell ] = tmp; + tmp = ts.getParsedText( c, cell, icell, tmp ); + cache[ icell ] = tmp; // parsed + if ( ( c.parsers[ icell ].type || '' ).toLowerCase() === 'numeric' ) { + // update column max value (ignore sign) + tbcache.colMax[ icell ] = Math.max( Math.abs( tmp ) || 0, tbcache.colMax[ icell ] || 0 ); + } + tmp = resort !== 'undefined' ? resort : c.resort; + if ( tmp !== false ) { + // widgets will be reapplied + ts.checkResort( c, tmp, callback ); + } else { + // don't reapply widgets is resort is false, just in case it causes + // problems with element focus + ts.resortComplete( c, callback ); + } + } else { + if ( ts.debug(c, 'core') ) { + console.error( 'updateCell aborted, tbody missing or not within the indicated table' ); + } + c.table.isUpdating = false; } + }, - function resortComplete($table, callback){ - $table.trigger('updateComplete'); - if (typeof callback === "function") { - callback($table[0]); + addRows : function( c, $row, resort, callback ) { + var txt, val, tbodyIndex, rowIndex, rows, cellIndex, len, order, + cacheIndex, rowData, cells, cell, span, + // allow passing a row string if only one non-info tbody exists in the table + valid = typeof $row === 'string' && c.$tbodies.length === 1 && / 0 ) { + cacheIndex += span; + } + cacheIndex++; + } + // add the row data to the end + cells[ c.columns ] = rowData; + // update cache + c.cache[ tbodyIndex ].normalized[ order ] = cells; + } + // resort using current settings + ts.checkResort( c, resort, callback ); + } + }, - function checkResort($table, flag, callback) { - if (flag !== false) { - $table.trigger("sorton", [$table[0].config.sortList, function(){ - resortComplete($table, callback); - }]); - } else { - resortComplete($table, callback); + updateCache : function( c, callback, $tbodies ) { + // rebuild parsers + if ( !( c.parsers && c.parsers.length ) ) { + ts.setupParsers( c, $tbodies ); + } + // rebuild the cache map + ts.buildCache( c, callback, $tbodies ); + }, + + // init flag (true) used by pager plugin to prevent widget application + // renamed from appendToTable + appendCache : function( c, init ) { + var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime, + table = c.table, + $tbodies = c.$tbodies, + rows = [], + cache = c.cache; + // empty table - fixes #206/#346 + if ( ts.isEmptyObject( cache ) ) { + // run pager appender in case the table was just emptied + return c.appender ? c.appender( table, rows ) : + table.isUpdating ? c.$table.triggerHandler( 'updateComplete', table ) : ''; // Fixes #532 + } + if ( ts.debug(c, 'core') ) { + appendTime = new Date(); + } + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = $tbodies.eq( tbodyIndex ); + if ( $tbody.length ) { + // detach tbody for manipulation + $curTbody = ts.processTbody( table, $tbody, true ); + parsed = cache[ tbodyIndex ].normalized; + totalRows = parsed.length; + for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) { + rows[rows.length] = parsed[ rowIndex ][ 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 && !c.pager.ajax ) ) { + $curTbody.append( parsed[ rowIndex ][ c.columns ].$row ); + } + } + // restore tbody + ts.processTbody( table, $curTbody, false ); } } + if ( c.appender ) { + c.appender( table, rows ); + } + if ( ts.debug(c, 'core') ) { + console.log( 'Rebuilt table' + ts.benchmark( appendTime ) ); + } + // apply table widgets; but not before ajax completes + if ( !init && !c.appender ) { + ts.applyWidget( table ); + } + if ( table.isUpdating ) { + c.$table.triggerHandler( 'updateComplete', table ); + } + }, + + commonUpdate : function( c, resort, callback ) { + // remove rows/elements before update + c.$table.find( c.selectorRemove ).remove(); + // rebuild parsers + ts.setupParsers( c ); + // rebuild the cache map + ts.buildCache( c ); + ts.checkResort( c, resort, callback ); + }, + + /* + ▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄ + ▀█▄ ██ ██ ██▄▄██ ██ ██ ██ ██ ██ ▄▄▄ + ▀█▄ ██ ██ ██▀██ ██ ██ ██ ██ ██ ▀██ + █████▀ ▀████▀ ██ ██ ██ ██ ██ ██ ▀████▀ + */ + initSort : function( c, cell, event ) { + if ( c.table.isUpdating ) { + // let any updates complete before initializing a sort + return setTimeout( function() { + ts.initSort( c, cell, event ); + }, 50 ); + } - /* public methods */ - ts.construct = function(settings) { - return this.each(function() { - // if no thead or tbody, or tablesorter is already present, quit - if (!this.tHead || this.tBodies.length === 0 || this.hasInitialized === true) { - return (this.config.debug) ? log('stopping initialization! No thead, tbody or tablesorter has already been initialized') : ''; + var arry, indx, headerIndx, dir, temp, tmp, $header, + notMultiSort = !event[ c.sortMultiSortKey ], + table = c.table, + len = c.$headers.length, + th = ts.getClosest( $( cell ), 'th, td' ), + col = parseInt( th.attr( 'data-column' ), 10 ), + sortedBy = event.type === 'mouseup' ? 'user' : event.type, + order = c.sortVars[ col ].order; + th = th[0]; + // Only call sortStart if sorting is enabled + c.$table.triggerHandler( 'sortStart', table ); + // get current column sort order + tmp = ( c.sortVars[ col ].count + 1 ) % order.length; + c.sortVars[ col ].count = event[ c.sortResetKey ] ? 2 : tmp; + // reset all sorts on non-current column - issue #30 + if ( c.sortRestart ) { + for ( headerIndx = 0; headerIndx < len; headerIndx++ ) { + $header = c.$headers.eq( headerIndx ); + tmp = parseInt( $header.attr( 'data-column' ), 10 ); + // only reset counts on columns that weren't just clicked on and if not included in a multisort + if ( col !== tmp && ( notMultiSort || $header.hasClass( ts.css.sortNone ) ) ) { + c.sortVars[ tmp ].count = -1; } - // declare - var $cell, $this = $(this), $t0 = this, - c, i, j, k = '', a, s, o, downTime, - m = $.metadata; - // initialization flag - $t0.hasInitialized = false; - // new blank config object - $t0.config = {}; - // merge and extend - c = $.extend(true, $t0.config, ts.defaults, settings); - // save the settings where they read - $.data($t0, "tablesorter", c); - if (c.debug) { $.data( $t0, 'startoveralltimer', new Date()); } - // constants - c.supportsTextContent = $('x')[0].textContent === 'x'; - c.supportsDataObject = parseFloat($.fn.jquery) >= 1.4; - // digit sort text location; keeping max+/- for backwards compatibility - c.string = { 'max': 1, 'min': -1, 'max+': 1, 'max-': -1, 'zero': 0, 'none': 0, 'null': 0, 'top': true, 'bottom': false }; - // add table theme class only if there isn't already one there - if (!/tablesorter\-/.test($this.attr('class'))) { - k = (c.theme !== '' ? ' tablesorter-' + c.theme : ''); + } + } + // user only wants to sort on one column + if ( notMultiSort ) { + $.each( c.sortVars, function( i ) { + c.sortVars[ i ].sortedBy = ''; + }); + // flush the sort list + c.sortList = []; + c.last.sortList = []; + if ( c.sortForce !== null ) { + arry = c.sortForce; + for ( indx = 0; indx < arry.length; indx++ ) { + if ( arry[ indx ][ 0 ] !== col ) { + c.sortList[ c.sortList.length ] = arry[ indx ]; + c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortForce'; + } } - c.$table = $this.addClass(c.tableClass + k); - c.$tbodies = $this.children('tbody:not(.' + c.cssInfoBlock + ')'); - // build headers - c.$headers = buildHeaders($t0); - // fixate columns if the users supplies the fixedWidth option - // do this after theme has been applied - fixColumnWidth($t0); - // try to auto detect column type, and store in tables config - c.parsers = buildParserCache($t0); - // build the cache for the tbody cells - // delayInit will delay building the cache until the user starts a sort - if (!c.delayInit) { buildCache($t0); } - // apply event handling to headers - // this is to big, perhaps break it out? - c.$headers - // http://stackoverflow.com/questions/5312849/jquery-find-self - .find('*').andSelf().filter(c.selectorSort) - .unbind('mousedown.tablesorter mouseup.tablesorter') - .bind('mousedown.tablesorter mouseup.tablesorter', function(e, external) { - // jQuery v1.2.6 doesn't have closest() - var $cell = this.tagName.match('TH|TD') ? $(this) : $(this).parents('th, td').filter(':last'), cell = $cell[0]; - // only recognize left clicks - if ((e.which || e.button) !== 1) { return false; } - // set timer on mousedown - if (e.type === 'mousedown') { - downTime = new Date().getTime(); - return e.target.tagName === "INPUT" ? '' : !c.cancelSelection; + } + // add column to sort list + dir = order[ c.sortVars[ col ].count ]; + if ( dir < 2 ) { + c.sortList[ c.sortList.length ] = [ col, dir ]; + c.sortVars[ col ].sortedBy = sortedBy; + // add other columns if header spans across multiple + if ( th.colSpan > 1 ) { + for ( indx = 1; indx < th.colSpan; indx++ ) { + c.sortList[ c.sortList.length ] = [ col + indx, dir ]; + // update count on columns in colSpan + c.sortVars[ col + indx ].count = $.inArray( dir, order ); + c.sortVars[ col + indx ].sortedBy = sortedBy; } - // ignore long clicks (prevents resizable widget from initializing a sort) - if (external !== true && (new Date().getTime() - downTime > 250)) { return false; } - if (c.delayInit && !c.cache) { buildCache($t0); } - if (!cell.sortDisabled) { - // Only call sortStart if sorting is enabled - $this.trigger("sortStart", $t0); - // store exp, for speed - // $cell = $(this); - k = !e[c.sortMultiSortKey]; - // get current column sort order - cell.count = e[c.sortResetKey] ? 2 : (cell.count + 1) % (c.sortReset ? 3 : 2); - // reset all sorts on non-current column - issue #30 - if (c.sortRestart) { - i = cell; - c.$headers.each(function() { - // only reset counts on columns that weren't just clicked on and if not included in a multisort - if (this !== i && (k || !$(this).is('.' + c.cssDesc + ',.' + c.cssAsc))) { - this.count = -1; - } - }); - } - // get current column index - i = cell.column; - // user only wants to sort on one column - if (k) { - // flush the sort list - c.sortList = []; - if (c.sortForce !== null) { - a = c.sortForce; - for (j = 0; j < a.length; j++) { - if (a[j][0] !== i) { - c.sortList.push(a[j]); - } - } - } - // add column to sort list - o = cell.order[cell.count]; - if (o < 2) { - c.sortList.push([i, o]); - // add other columns if header spans across multiple - if (cell.colSpan > 1) { - for (j = 1; j < cell.colSpan; j++) { - c.sortList.push([i + j, o]); - } - } - } - // multi column sorting - } else { - // get rid of the sortAppend before adding more - fixes issue #115 - if (c.sortAppend && c.sortList.length > 1) { - if (ts.isValueInArray(c.sortAppend[0][0], c.sortList)) { - c.sortList.pop(); - } - } - // the user has clicked on an already sorted column - if (ts.isValueInArray(i, c.sortList)) { - // reverse the sorting direction for all tables - for (j = 0; j < c.sortList.length; j++) { - s = c.sortList[j]; - o = c.headerList[s[0]]; - if (s[0] === i) { - s[1] = o.order[o.count]; - if (s[1] === 2) { - c.sortList.splice(j,1); - o.count = -1; - } - } - } - } else { - // add column to sort list array - o = cell.order[cell.count]; - if (o < 2) { - c.sortList.push([i, o]); - // add other columns if header spans across multiple - if (cell.colSpan > 1) { - for (j = 1; j < cell.colSpan; j++) { - c.sortList.push([i + j, o]); - } - } - } - } - } - if (c.sortAppend !== null) { - a = c.sortAppend; - for (j = 0; j < a.length; j++) { - if (a[j][0] !== i) { - c.sortList.push(a[j]); - } - } + } + } + // multi column sorting + } else { + // get rid of the sortAppend before adding more - fixes issue #115 & #523 + c.sortList = $.extend( [], c.last.sortList ); + + // the user has clicked on an already sorted column + if ( ts.isValueInArray( col, c.sortList ) >= 0 ) { + // reverse the sorting direction + c.sortVars[ col ].sortedBy = sortedBy; + for ( indx = 0; indx < c.sortList.length; indx++ ) { + tmp = c.sortList[ indx ]; + if ( tmp[ 0 ] === col ) { + // order.count seems to be incorrect when compared to cell.count + tmp[ 1 ] = order[ c.sortVars[ col ].count ]; + if ( tmp[1] === 2 ) { + c.sortList.splice( indx, 1 ); + c.sortVars[ col ].count = -1; } - // sortBegin event triggered immediately before the sort - $this.trigger("sortBegin", $t0); - // setTimeout needed so the processing icon shows up - setTimeout(function(){ - // set css for headers - setHeadersCss($t0); - multisort($t0); - appendToTable($t0); - }, 1); } - }); - if (c.cancelSelection) { - // cancel selection - c.$headers.each(function() { - this.onselectstart = function() { - return false; - }; - }); } - // apply easy methods that trigger binded events - $this - .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave') - .bind("sortReset", function(){ - c.sortList = []; - setHeadersCss($t0); - multisort($t0); - appendToTable($t0); - }) - .bind("update updateRows", function(e, resort, callback) { - // remove rows/elements before update - $(c.selectorRemove, $t0).remove(); - // rebuild parsers - c.parsers = buildParserCache($t0); - // rebuild the cache map - buildCache($t0); - checkResort($this, resort, callback); - }) - .bind("updateCell", function(e, cell, resort, callback) { - // get position from the dom - var l, row, icell, - $tb = $this.find('tbody'), - // update cache - format: function(s, table, cell, cellIndex) - // no closest in jQuery v1.2.6 - tbdy = $tb.index( $(cell).closest('tbody') ),$row = $(cell).closest('tr'); - tbdy = $tb.index( $(cell).parents('tbody').filter(':last') ), - $row = $(cell).parents('tr').filter(':last'); - cell = $(cell)[0]; // in case cell is a jQuery object - // tbody may not exist if update is initialized while tbody is removed for processing - if ($tb.length && tbdy >= 0) { - row = $tb.eq(tbdy).find('tr').index( $row ); - icell = cell.cellIndex; - l = $t0.config.cache[tbdy].normalized[row].length - 1; - $t0.config.cache[tbdy].row[$t0.config.cache[tbdy].normalized[row][l]] = $row; - $t0.config.cache[tbdy].normalized[row][icell] = c.parsers[icell].format( getElementText($t0, cell, icell), $t0, cell, icell ); - checkResort($this, resort, callback); - } - }) - .bind("addRows", function(e, $row, resort, callback) { - var i, rows = $row.filter('tr').length, - dat = [], l = $row[0].cells.length, - tbdy = $this.find('tbody').index( $row.closest('tbody') ); - // fixes adding rows to an empty table - see issue #179 - if (!c.parsers) { - c.parsers = buildParserCache($t0); - } - // add each row - for (i = 0; i < rows; i++) { - // add each cell - for (j = 0; j < l; j++) { - dat[j] = c.parsers[j].format( getElementText($t0, $row[i].cells[j], j), $t0, $row[i].cells[j], j ); + } else { + // add column to sort list array + dir = order[ c.sortVars[ col ].count ]; + c.sortVars[ col ].sortedBy = sortedBy; + if ( dir < 2 ) { + c.sortList[ c.sortList.length ] = [ col, dir ]; + // add other columns if header spans across multiple + if ( th.colSpan > 1 ) { + for ( indx = 1; indx < th.colSpan; indx++ ) { + c.sortList[ c.sortList.length ] = [ col + indx, dir ]; + // update count on columns in colSpan + c.sortVars[ col + indx ].count = $.inArray( dir, order ); + c.sortVars[ col + indx ].sortedBy = sortedBy; } - // add the row index to the end - dat.push(c.cache[tbdy].row.length); - // update cache - c.cache[tbdy].row.push([$row[i]]); - c.cache[tbdy].normalized.push(dat); - dat = []; - } - // resort using current settings - checkResort($this, resort, callback); - }) - .bind("sorton", function(e, list, callback, init) { - $this.trigger("sortStart", this); - // update header count index - updateHeaderSortCount($t0, list); - // set css for headers - setHeadersCss($t0); - // sort the table and append it to the dom - multisort($t0); - appendToTable($t0, init); - if (typeof callback === "function") { - callback($t0); - } - }) - .bind("appendCache", function(e, callback, init) { - appendToTable($t0, init); - if (typeof callback === "function") { - callback($t0); } - }) - .bind("applyWidgetId", function(e, id) { - ts.getWidgetById(id).format($t0, c, c.widgetOptions); - }) - .bind("applyWidgets", function(e, init) { - // apply widgets - ts.applyWidget($t0, init); - }) - .bind("refreshWidgets", function(e, all, dontapply){ - ts.refreshWidgets($t0, all, dontapply); - }) - .bind("destroy", function(e, c, cb){ - ts.destroy($t0, c, cb); - }); - - // get sort list from jQuery data or metadata - // in jQuery < 1.4, an error occurs when calling $this.data() - if (c.supportsDataObject && typeof $this.data().sortlist !== 'undefined') { - c.sortList = $this.data().sortlist; - } else if (m && ($this.metadata() && $this.metadata().sortlist)) { - c.sortList = $this.metadata().sortlist; } - // apply widget init code - ts.applyWidget($t0, true); - // if user has supplied a sort list to constructor - if (c.sortList.length > 0) { - $this.trigger("sorton", [c.sortList, {}, !c.initWidgets]); - } else if (c.initWidgets) { - // apply widget format - ts.applyWidget($t0); + } + } + // save sort before applying sortAppend + c.last.sortList = $.extend( [], c.sortList ); + if ( c.sortList.length && c.sortAppend ) { + arry = $.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ]; + if ( !ts.isEmptyObject( arry ) ) { + for ( indx = 0; indx < arry.length; indx++ ) { + if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) { + dir = arry[ indx ][ 1 ]; + temp = ( '' + dir ).match( /^(a|d|s|o|n)/ ); + if ( temp ) { + tmp = c.sortList[ 0 ][ 1 ]; + switch ( temp[ 0 ] ) { + case 'd' : + dir = 1; + break; + case 's' : + dir = tmp; + break; + case 'o' : + dir = tmp === 0 ? 1 : 0; + break; + case 'n' : + dir = ( tmp + 1 ) % order.length; + break; + default: + dir = 0; + break; + } + } + c.sortList[ c.sortList.length ] = [ arry[ indx ][ 0 ], dir ]; + c.sortVars[ arry[ indx ][ 0 ] ].sortedBy = 'sortAppend'; + } } + } + } + // sortBegin event triggered immediately before the sort + c.$table.triggerHandler( 'sortBegin', table ); + // setTimeout needed so the processing icon shows up + setTimeout( function() { + // set css for headers + ts.setHeadersCss( c ); + ts.multisort( c ); + ts.appendCache( c ); + c.$table.triggerHandler( 'sortBeforeEnd', table ); + c.$table.triggerHandler( 'sortEnd', table ); + }, 1 ); + }, - // show processesing icon - if (c.showProcessing) { - $this - .unbind('sortBegin sortEnd') - .bind('sortBegin sortEnd', function(e) { - ts.isProcessing($t0, e.type === 'sortBegin'); - }); + // sort multiple columns + multisort : function( c ) { /*jshint loopfunc:true */ + var tbodyIndex, sortTime, colMax, rows, tmp, + table = c.table, + sorter = [], + dir = 0, + textSorter = c.textSorter || '', + sortList = c.sortList, + sortLen = sortList.length, + len = c.$tbodies.length; + if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) { + // empty table - fixes #206/#346 + return; + } + if ( ts.debug(c, 'core') ) { sortTime = new Date(); } + // cache textSorter to optimize speed + if ( typeof textSorter === 'object' ) { + colMax = c.columns; + while ( colMax-- ) { + tmp = ts.getColumnData( table, textSorter, colMax ); + if ( typeof tmp === 'function' ) { + sorter[ colMax ] = tmp; } + } + } + for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) { + colMax = c.cache[ tbodyIndex ].colMax; + rows = c.cache[ tbodyIndex ].normalized; + + rows.sort( function( a, b ) { + var sortIndex, num, col, order, sort, x, y; + // rows is undefined here in IE, so don't use it! + for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) { + col = sortList[ sortIndex ][ 0 ]; + order = sortList[ sortIndex ][ 1 ]; + // sort direction, true = asc, false = desc + dir = order === 0; + + if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) { + return a[ c.columns ].order - b[ c.columns ].order; + } - // initialized - $t0.hasInitialized = true; - if (c.debug) { - ts.benchmark("Overall initialization time", $.data( $t0, 'startoveralltimer')); + // fallback to natural sort since it is more robust + num = /n/i.test( ts.getSortType( c.parsers, col ) ); + if ( num && c.strings[ col ] ) { + // sort strings in numerical columns + if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) { + num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 ); + } else { + num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0; + } + // fall back to built-in numeric sort + // var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table ); + 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, c ); + } else { + // set a & b depending on sort direction + x = dir ? a : b; + y = dir ? b : a; + // text sort function + if ( typeof textSorter === 'function' ) { + // custom OVERALL text sorter + sort = textSorter( x[ col ], y[ col ], dir, col, table ); + } else if ( typeof sorter[ col ] === 'function' ) { + // custom text sorter for a SPECIFIC COLUMN + sort = sorter[ 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, c ); + } + } + if ( sort ) { return sort; } } - $this.trigger('tablesorter-initialized', $t0); - if (typeof c.initialized === 'function') { c.initialized($t0); } + return a[ c.columns ].order - b[ c.columns ].order; }); - }; + } + if ( ts.debug(c, 'core') ) { + console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) ); + } + }, - // *** Process table *** - // add processing indicator - ts.isProcessing = function(table, toggle, $ths) { - var c = table.config, - // default to all headers - $h = $ths || $(table).find('.' + c.cssHeader); - if (toggle) { - if (c.sortList.length > 0) { - // get headers from the sortList - $h = $h.filter(function(){ - // get data-column from attr to keep compatibility with jQuery 1.2.6 - return this.sortDisabled ? false : ts.isValueInArray( parseFloat($(this).attr('data-column')), c.sortList); - }); - } - $h.addClass(c.cssProcessing); + resortComplete : function( c, callback ) { + if ( c.table.isUpdating ) { + c.$table.triggerHandler( 'updateComplete', c.table ); + } + if ( $.isFunction( callback ) ) { + callback( c.table ); + } + }, + + checkResort : function( c, resort, callback ) { + var sortList = $.isArray( resort ) ? resort : c.sortList, + // if no resort parameter is passed, fallback to config.resort (true by default) + resrt = typeof resort === 'undefined' ? c.resort : resort; + // don't try to resort if the table is still processing + // this will catch spamming of the updateCell method + if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) { + if ( sortList.length ) { + ts.sortOn( c, sortList, function() { + ts.resortComplete( c, callback ); + }, true ); } else { - $h.removeClass(c.cssProcessing); + ts.sortReset( c, function() { + ts.resortComplete( c, callback ); + ts.applyWidget( c.table, false ); + } ); } - }; + } else { + ts.resortComplete( c, callback ); + ts.applyWidget( c.table, false ); + } + }, - // detach tbody but save the position - // don't use tbody because there are portions that look for a tbody index (updateCell) - ts.processTbody = function(table, $tb, getIt){ - var t, holdr; - if (getIt) { - $tb.before(''); - holdr = ($.fn.detach) ? $tb.detach() : $tb.remove(); - return holdr; - } - holdr = $(table).find('span.tablesorter-savemyplace'); - $tb.insertAfter( holdr ); - holdr.remove(); - }; + sortOn : function( c, list, callback, init ) { + var indx, + table = c.table; + c.$table.triggerHandler( 'sortStart', table ); + for (indx = 0; indx < c.columns; indx++) { + c.sortVars[ indx ].sortedBy = ts.isValueInArray( indx, list ) > -1 ? 'sorton' : ''; + } + // update header count index + ts.updateHeaderSortCount( c, list ); + // set css for headers + ts.setHeadersCss( c ); + // fixes #346 + if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { + ts.buildCache( c ); + } + c.$table.triggerHandler( 'sortBegin', table ); + // sort the table and append it to the dom + ts.multisort( c ); + ts.appendCache( c, init ); + c.$table.triggerHandler( 'sortBeforeEnd', table ); + c.$table.triggerHandler( 'sortEnd', table ); + ts.applyWidget( table ); + if ( $.isFunction( callback ) ) { + callback( table ); + } + }, - ts.clearTableBody = function(table) { - table.config.$tbodies.empty(); - }; + sortReset : function( c, callback ) { + c.sortList = []; + var indx; + for (indx = 0; indx < c.columns; indx++) { + c.sortVars[ indx ].count = -1; + c.sortVars[ indx ].sortedBy = ''; + } + ts.setHeadersCss( c ); + ts.multisort( c ); + ts.appendCache( c ); + if ( $.isFunction( callback ) ) { + callback( c.table ); + } + }, - ts.destroy = function(table, removeClasses, callback){ - if (!table.hasInitialized) { return; } - // remove all widgets - ts.refreshWidgets(table, true, true); - var $t = $(table), c = table.config, - $h = $t.find('thead:first'), - $r = $h.find('tr.' + c.cssHeaderRow).removeClass(c.cssHeaderRow), - $f = $t.find('tfoot:first > tr').children('th, td'); - // remove widget added rows, just in case - $h.find('tr').not($r).remove(); - // disable tablesorter - $t - .removeData('tablesorter') - .unbind('sortReset update updateCell addRows sorton appendCache applyWidgetId applyWidgets refreshWidgets destroy mouseup mouseleave'); - c.$headers.add($f) - .removeClass(c.cssHeader + ' ' + c.cssAsc + ' ' + c.cssDesc) - .removeAttr('data-column'); - $r.find(c.selectorSort).unbind('mousedown.tablesorter mouseup.tablesorter'); - // restore headers - $r.children().each(function(i){ - $(this).html( c.headerContent[i] ); - }); - if (removeClasses !== false) { - $t.removeClass(c.tableClass + ' tablesorter-' + c.theme); - } - // clear flag in case the plugin is initialized again - table.hasInitialized = false; - if (typeof callback === 'function') { - callback(table); + getSortType : function( parsers, column ) { + return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : ''; + }, + + getOrder : function( val ) { + // look for 'd' in 'desc' order; return true + return ( /^d/i.test( val ) || val === 1 ); + }, + + // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed) + sortNatural : function( a, b ) { + if ( a === b ) { return 0; } + a = ( a || '' ).toString(); + b = ( b || '' ).toString(); + var aNum, bNum, aFloat, bFloat, indx, max, + regex = ts.regex; + // first try and sort Hex codes + if ( regex.hex.test( b ) ) { + aNum = parseInt( a.match( regex.hex ), 16 ); + bNum = parseInt( b.match( regex.hex ), 16 ); + if ( aNum < bNum ) { return -1; } + if ( aNum > bNum ) { return 1; } + } + // chunk/tokenize + aNum = a.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); + bNum = b.replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); + max = Math.max( aNum.length, bNum.length ); + // natural sorting through split numeric strings and default strings + for ( indx = 0; indx < max; indx++ ) { + // find floats not starting with '0', string or 0 if not defined + aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0; + bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0; + // handle numeric vs string comparison - number < string - (Kyle Adams) + if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + if ( typeof aFloat !== typeof bFloat ) { + aFloat += ''; + bFloat += ''; } - }; + if ( aFloat < bFloat ) { return -1; } + if ( aFloat > bFloat ) { return 1; } + } + return 0; + }, - // *** sort functions *** - // regex used in natural sort - ts.regex = [ - /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi, // chunk/tokenize numbers & letters - /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/, //date - /^0x[0-9a-f]+$/i // hex - ]; - - // Natural sort - https://github.com/overset/javascript-natural-sort - ts.sortText = function(table, a, b, col) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ], - r = ts.regex, xN, xD, yN, yD, xF, yF, i, mx; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } - if (typeof c.textSorter === 'function') { return c.textSorter(a, b, table, col); } - // chunk/tokenize - xN = a.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); - yN = b.replace(r[0], '\\0$1\\0').replace(/\\0$/, '').replace(/^\\0/, '').split('\\0'); - // numeric, hex or date detection - xD = parseInt(a.match(r[2]),16) || (xN.length !== 1 && a.match(r[1]) && Date.parse(a)); - yD = parseInt(b.match(r[2]),16) || (xD && b.match(r[1]) && Date.parse(b)) || null; - // first try and sort Hex codes or Dates - if (yD) { - if ( xD < yD ) { return -1; } - if ( xD > yD ) { return 1; } - } - mx = Math.max(xN.length, yN.length); - // natural sorting through split numeric strings and default strings - for (i = 0; i < mx; i++) { - // find floats not starting with '0', string or 0 if not defined - xF = isNaN(xN[i]) ? xN[i] || 0 : parseFloat(xN[i]) || 0; - yF = isNaN(yN[i]) ? yN[i] || 0 : parseFloat(yN[i]) || 0; - // handle numeric vs string comparison - number < string - (Kyle Adams) - if (isNaN(xF) !== isNaN(yF)) { return (isNaN(xF)) ? 1 : -1; } - // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' - if (typeof xF !== typeof yF) { - xF += ''; - yF += ''; - } - if (xF < yF) { return -1; } - if (xF > yF) { return 1; } + sortNaturalAsc : function( a, b, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } + return ts.sortNatural( a, b ); + }, + + sortNaturalDesc : function( a, b, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } + return ts.sortNatural( b, a ); + }, + + // basic alphabetical sort + 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 + getTextValue : function( val, num, max ) { + if ( max ) { + // make sure the text value is greater than the max numerical value (max) + var indx, + len = val ? val.length : 0, + n = max + num; + for ( indx = 0; indx < len; indx++ ) { + n += val.charCodeAt( indx ); } - return 0; - }; + return num * n; + } + return 0; + }, - ts.sortTextDesc = function(table, a, b, col) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } - if (typeof c.textSorter === 'function') { return c.textSorter(b, a, table, col); } - return ts.sortText(table, b, a); - }; + sortNumericAsc : function( a, b, num, max, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } + if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } + if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } + return a - b; + }, - // return text string value by adding up ascii value - // so the text is somewhat sorted when using a digital sort - // this is NOT an alphanumeric sort - ts.getTextValue = function(a, mx, d) { - if (mx) { - // make sure the text value is greater than the max numerical value (mx) - var i, l = a.length, n = mx + d; - for (i = 0; i < l; i++) { - n += a.charCodeAt(i); - } - return d * n; + sortNumericDesc : function( a, b, num, max, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } + if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } + if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } + return b - a; + }, + + sortNumeric : function( a, b ) { + return a - b; + }, + + /* + ██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████ + ██ ██ ██ ██ ██ ██ ██ ▄▄▄ ██▄▄ ██ ▀█▄ + ██ ██ ██ ██ ██ ██ ██ ▀██ ██▀▀ ██ ▀█▄ + ███████▀ ██ █████▀ ▀████▀ ██████ ██ █████▀ + */ + addWidget : function( widget ) { + if ( widget.id && !ts.isEmptyObject( ts.getWidgetById( widget.id ) ) ) { + console.warn( '"' + widget.id + '" widget was loaded more than once!' ); + } + ts.widgets[ ts.widgets.length ] = widget; + }, + + hasWidget : function( $table, name ) { + $table = $( $table ); + return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false; + }, + + getWidgetById : function( name ) { + var indx, widget, + len = ts.widgets.length; + for ( indx = 0; indx < len; indx++ ) { + widget = ts.widgets[ indx ]; + if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) { + return widget; } - return 0; - }; + } + }, - ts.sortNumeric = function(table, a, b, col, mx, d) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : -e || -1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : e || 1; } - if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } - if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } - return a - b; - }; + applyWidgetOptions : function( table ) { + var indx, widget, wo, + c = table.config, + len = c.widgets.length; + if ( len ) { + for ( indx = 0; indx < len; indx++ ) { + widget = ts.getWidgetById( c.widgets[ indx ] ); + if ( widget && widget.options ) { + wo = $.extend( true, {}, widget.options ); + c.widgetOptions = $.extend( true, wo, c.widgetOptions ); + // add widgetOptions to defaults for option validator + $.extend( true, ts.defaults.widgetOptions, widget.options ); + } + } + } + }, - ts.sortNumericDesc = function(table, a, b, col, mx, d) { - if (a === b) { return 0; } - var c = table.config, e = c.string[ (c.empties[col] || c.emptyTo ) ]; - if (a === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? -1 : 1) : e || 1; } - if (b === '' && e !== 0) { return (typeof(e) === 'boolean') ? (e ? 1 : -1) : -e || -1; } - if (isNaN(a)) { a = ts.getTextValue(a, mx, d); } - if (isNaN(b)) { b = ts.getTextValue(b, mx, d); } - return b - a; - }; + addWidgetFromClass : function( table ) { + var len, indx, + c = table.config, + // look for widgets to apply from table class + // don't match from 'ui-widget-content'; use \S instead of \w to include widgets + // with dashes in the name, e.g. "widget-test-2" extracts out "test-2" + regex = '^' + c.widgetClass.replace( ts.regex.templateName, '(\\S+)+' ) + '$', + widgetClass = new RegExp( regex, 'g' ), + // split up table class (widget id's can include dashes) - stop using match + // otherwise only one widget gets extracted, see #1109 + widgets = ( table.className || '' ).split( ts.regex.spaces ); + if ( widgets.length ) { + len = widgets.length; + for ( indx = 0; indx < len; indx++ ) { + if ( widgets[ indx ].match( widgetClass ) ) { + c.widgets[ c.widgets.length ] = widgets[ indx ].replace( widgetClass, '$1' ); + } + } + } + }, - // 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'); + applyWidgetId : function( table, id, init ) { + table = $(table)[0]; + var applied, time, name, + c = table.config, + wo = c.widgetOptions, + debug = ts.debug(c, 'core'), + widget = ts.getWidgetById( id ); + if ( widget ) { + name = widget.id; + applied = false; + // add widget name to option list so it gets reapplied after sorting, filtering, etc + if ( $.inArray( name, c.widgets ) < 0 ) { + c.widgets[ c.widgets.length ] = name; + } + if ( debug ) { time = new Date(); } + + if ( init || !( c.widgetInit[ name ] ) ) { + // set init flag first to prevent calling init more than once (e.g. pager) + c.widgetInit[ name ] = true; + if ( table.hasInitialized ) { + // don't reapply widget options on tablesorter init + ts.applyWidgetOptions( table ); + } + if ( typeof widget.init === 'function' ) { + applied = true; + if ( debug ) { + console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' ); } + widget.init( table, widget, c, wo ); } - 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 ); - } + if ( !init && typeof widget.format === 'function' ) { + applied = true; + if ( debug ) { + console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' ); } + widget.format( table, c, wo, false ); } - return s; - }; - - // *** utilities *** - ts.isValueInArray = function(v, a) { - var i, l = a.length; - for (i = 0; i < l; i++) { - if (a[i][0] === v) { - return true; + if ( debug ) { + if ( applied ) { + console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) ); + if ( console.groupEnd ) { console.groupEnd(); } } } - return false; - }; + } + }, - 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; + applyWidget : function( table, init, callback ) { + table = $( table )[ 0 ]; // in case this is called externally + var indx, len, names, widget, time, + c = table.config, + debug = ts.debug(c, 'core'), + widgets = []; + // prevent numerous consecutive widget applications + if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) { + return; + } + if ( debug ) { time = new Date(); } + ts.addWidgetFromClass( table ); + // prevent "tablesorter-ready" from firing multiple times in a row + clearTimeout( c.timerReady ); + if ( c.widgets.length ) { + table.isApplyingWidgets = true; + // ensure unique widget ids + c.widgets = $.grep( c.widgets, function( val, index ) { + return $.inArray( val, c.widgets ) === index; + }); + names = c.widgets || []; + len = names.length; + // build widget array & add priority as needed + for ( indx = 0; indx < len; indx++ ) { + widget = ts.getWidgetById( names[ indx ] ); + if ( widget && widget.id ) { + // set priority to 10 if not defined + if ( !widget.priority ) { widget.priority = 10; } + widgets[ indx ] = widget; + } else if ( debug ) { + console.warn( '"' + names[ indx ] + '" was enabled, but the widget code has not been loaded!' ); } } - if (a) { - ts.parsers.push(parser); + // 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 + len = widgets.length; + if ( debug ) { + console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' ); + } + for ( indx = 0; indx < len; indx++ ) { + widget = widgets[ indx ]; + if ( widget && widget.id ) { + ts.applyWidgetId( table, widget.id, init ); + } } - }; + if ( debug && console.groupEnd ) { console.groupEnd(); } + } + c.timerReady = setTimeout( function() { + table.isApplyingWidgets = false; + $.data( table, 'lastWidgetApplication', new Date() ); + c.$table.triggerHandler( 'tablesorter-ready' ); + // callback executed on init only + if ( !init && typeof callback === 'function' ) { + callback( table ); + } + if ( debug ) { + widget = c.widgets.length; + console.log( 'Completed ' + + ( init === true ? 'initializing ' : 'applying ' ) + widget + + ' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) ); + } + }, 10 ); + }, - ts.getParserById = function(name) { - var i, l = ts.parsers.length; - for (i = 0; i < l; i++) { - if (ts.parsers[i].id.toLowerCase() === (name.toString()).toLowerCase()) { - return ts.parsers[i]; + removeWidget : function( table, name, refreshing ) { + table = $( table )[ 0 ]; + var index, widget, indx, len, + c = table.config; + // if name === true, add all widgets from $.tablesorter.widgets + if ( name === true ) { + name = []; + len = ts.widgets.length; + for ( indx = 0; indx < len; indx++ ) { + widget = ts.widgets[ indx ]; + if ( widget && widget.id ) { + name[ name.length ] = widget.id; + } + } + } else { + // name can be either an array of widgets names, + // or a space/comma separated list of widget names + name = ( $.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ ); + } + len = name.length; + for ( index = 0; index < len; index++ ) { + widget = ts.getWidgetById( name[ index ] ); + indx = $.inArray( name[ index ], c.widgets ); + // don't remove the widget from config.widget if refreshing + if ( indx >= 0 && refreshing !== true ) { + c.widgets.splice( indx, 1 ); + } + if ( widget && widget.remove ) { + if ( ts.debug(c, 'core') ) { + console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' ); } + widget.remove( table, c, c.widgetOptions, refreshing ); + c.widgetInit[ name[ index ] ] = false; } + } + c.$table.triggerHandler( 'widgetRemoveEnd', table ); + }, + + refreshWidgets : function( table, doAll, dontapply ) { + table = $( table )[ 0 ]; // see issue #243 + var indx, widget, + c = table.config, + curWidgets = c.widgets, + widgets = ts.widgets, + len = widgets.length, + list = [], + callback = function( table ) { + $( table ).triggerHandler( 'refreshComplete' ); + }; + // remove widgets not defined in config.widgets, unless doAll is true + for ( indx = 0; indx < len; indx++ ) { + widget = widgets[ indx ]; + if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) { + list[ list.length ] = widget.id; + } + } + ts.removeWidget( table, list.join( ',' ), true ); + if ( dontapply !== true ) { + // call widget init if + ts.applyWidget( table, doAll || false, callback ); + if ( doAll ) { + // apply widget format + ts.applyWidget( table, false, callback ); + } + } else { + callback( table ); + } + }, + + /* + ██ ██ ██████ ██ ██ ██ ██████ ██ ██████ ▄█████ + ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ▀█▄ + ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀█▄ + ▀████▀ ██ ██ ██████ ██ ██ ██ ██████ █████▀ + */ + benchmark : function( diff ) { + return ( ' (' + ( new Date().getTime() - diff.getTime() ) + ' ms)' ); + }, + // deprecated ts.log + log : function() { + console.log( arguments ); + }, + debug : function(c, name) { + return c && ( + c.debug === true || + typeof c.debug === 'string' && c.debug.indexOf(name) > -1 + ); + }, + + // $.isEmptyObject from jQuery v1.4 + isEmptyObject : function( obj ) { + /*jshint forin: false */ + for ( var name in obj ) { return false; - }; + } + return true; + }, - ts.addWidget = function(widget) { - ts.widgets.push(widget); - }; + isValueInArray : function( column, arry ) { + var indx, + len = arry && arry.length || 0; + for ( indx = 0; indx < len; indx++ ) { + if ( arry[ indx ][ 0 ] === column ) { + return indx; + } + } + return -1; + }, - 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; + formatFloat : function( str, table ) { + if ( typeof str !== 'string' || str === '' ) { return str; } + // allow using formatFloat without a table; defaults to US number format + var num, + usFormat = table && table.config ? table.config.usNumberFormat !== false : + typeof table !== 'undefined' ? table : true; + if ( usFormat ) { + // US Format - 1,234,567.89 -> 1234567.89 + str = str.replace( ts.regex.comma, '' ); + } else { + // German Format = 1.234.567,89 -> 1234567.89 + // French Format = 1 234 567,89 -> 1234567.89 + str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' ); + } + if ( ts.regex.digitNegativeTest.test( str ) ) { + // make (#) into a negative number -> (10) = -10 + str = str.replace( ts.regex.digitNegativeReplace, '-$1' ); + } + num = parseFloat( str ); + // return the text instead of zero + return isNaN( num ) ? $.trim( str ) : num; + }, + + isDigit : function( str ) { + // replace all unwanted chars and match + return isNaN( str ) ? + ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) : + str !== ''; + }, + + // computeTableHeaderCellIndexes from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + computeColumnIndex : function( $rows, c ) { + var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol, + // total columns has been calculated, use it to set the matrixrow + columns = c && c.columns || 0, + matrix = [], + matrixrow = new Array( columns ); + for ( i = 0; i < $rows.length; i++ ) { + cells = $rows[ i ].cells; + for ( j = 0; j < cells.length; j++ ) { + cell = cells[ j ]; + rowIndex = i; + 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; + } + } + // jscs:disable disallowEmptyBlocks + if ( columns && cell.cellIndex === firstAvailCol ) { + // don't to anything + } else if ( cell.setAttribute ) { + // jscs:enable disallowEmptyBlocks + // add data-column (setAttribute = IE8+) + cell.setAttribute( 'data-column', firstAvailCol ); + } else { + // remove once we drop support for IE7 - 1/12/2016 + $( cell ).attr( 'data-column', firstAvailCol ); + } + 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'; + } } } - }; + } + ts.checkColumnCount($rows, matrix, matrixrow.length); + return matrixrow.length; + }, - ts.applyWidget = function(table, init) { - var c = table.config, - wo = c.widgetOptions, - ws = c.widgets.sort().reverse(), // ensure that widgets are always applied in a certain order - time, i, w, l = ws.length; - // make zebra last - i = $.inArray('zebra', c.widgets); - if (i >= 0) { - c.widgets.splice(i,1); - c.widgets.push('zebra'); - } - if (c.debug) { - time = new Date(); - } - // add selected widgets - for (i = 0; i < l; i++) { - w = ts.getWidgetById(ws[i]); - if ( w ) { - if (init === true && w.hasOwnProperty('init')) { - w.init(table, w, c, wo); - } else if (!init && w.hasOwnProperty('format')) { - w.format(table, c, wo); - } + checkColumnCount : function($rows, matrix, columns) { + // this DOES NOT report any tbody column issues, except for the math and + // and column selector widgets + var i, len, + valid = true, + cells = []; + for ( i = 0; i < matrix.length; i++ ) { + // some matrix entries are undefined when testing the footer because + // it is using the rowIndex property + if ( matrix[i] ) { + len = matrix[i].length; + if ( matrix[i].length !== columns ) { + valid = false; + break; } } - if (c.debug) { - benchmark("Completed " + (init === true ? "initializing" : "applying") + " widgets", time); + } + if ( !valid ) { + $rows.each( function( indx, el ) { + var cell = el.parentElement.nodeName; + if ( cells.indexOf( cell ) < 0 ) { + cells.push( cell ); + } + }); + console.error( + 'Invalid or incorrect number of columns in the ' + + cells.join( ' or ' ) + '; expected ' + columns + + ', but found ' + len + ' columns' + ); + } + }, + + // automatically add a colgroup with col elements set to a percentage width + fixColumnWidth : function( table ) { + table = $( table )[ 0 ]; + var overallWidth, percent, $tbodies, len, index, + c = table.config, + $colgroup = c.$table.children( 'colgroup' ); + // remove plugin-added colgroup, in case we need to refresh the widths + if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) { + $colgroup.remove(); + } + if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) { + $colgroup = $( '' ); + overallWidth = c.$table.width(); + // only add col for visible columns - fixes #371 + $tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' ); + len = $tbodies.length; + for ( index = 0; index < len; index++ ) { + percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%'; + $colgroup.append( $( '' ).css( 'width', percent ) ); } - }; + c.$table.prepend( $colgroup ); + } + }, - ts.refreshWidgets = function(table, doAll, dontapply) { - var i, c = table.config, - cw = c.widgets, - w = ts.widgets, l = w.length; - // remove previous widgets - for (i = 0; i < l; i++){ - if ( w[i] && w[i].id && (doAll || $.inArray( w[i].id, cw ) < 0) ) { - if (c.debug) { log( 'Refeshing widgets: Removing ' + w[i].id ); } - if (w[i].hasOwnProperty('remove')) { w[i].remove(table, c, c.widgetOptions); } + // 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 + getData : function( header, configHeader, key ) { + var meta, cl4ss, + val = '', + $header = $( header ); + if ( !$header.length ) { return ''; } + meta = $.metadata ? $header.metadata() : false; + cl4ss = ' ' + ( $header.attr( 'class' ) || '' ); + if ( typeof $header.data( key ) !== 'undefined' || + typeof $header.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 += $header.data( key ) || $header.data( key.toLowerCase() ); + } else if ( meta && typeof meta[ key ] !== 'undefined' ) { + val += meta[ key ]; + } else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) { + val += configHeader[ key ]; + } else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) { + // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser' + val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || ''; + } + return $.trim( val ); + }, + + getColumnData : function( table, obj, indx, getCell, $headers ) { + if ( typeof obj !== 'object' || obj === null ) { + return obj; + } + table = $( table )[ 0 ]; + var $header, key, + c = table.config, + $cells = ( $headers || c.$headers ), + // c.$headerIndexed is not defined initially + $cell = c.$headerIndexed && c.$headerIndexed[ indx ] || + $cells.find( '[data-column="' + indx + '"]:last' ); + if ( typeof obj[ indx ] !== 'undefined' ) { + return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ]; + } + for ( key in obj ) { + if ( typeof key === 'string' ) { + $header = $cell + // header cell with class/id + .filter( key ) + // find elements within the header cell with cell/id + .add( $cell.find( key ) ); + if ( $header.length ) { + return obj[ key ]; } } - if (dontapply !== true) { - ts.applyWidget(table, doAll); + } + return; + }, + + // *** Process table *** + // add processing indicator + isProcessing : function( $table, toggle, $headers ) { + $table = $( $table ); + var c = $table[ 0 ].config, + // default to all headers + $header = $headers || $table.find( '.' + ts.css.header ); + if ( toggle ) { + // don't use sortList if custom $headers used + if ( typeof $headers !== 'undefined' && c.sortList.length > 0 ) { + // get headers from the sortList + $header = $header.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( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing ); + } else { + $table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing ); + } + }, - // get sorter, string, empty, etc options for each column from - // jQuery data, metadata, header option or header class name ("sorter-false") - // priority = jQuery data > meta > headers option > header class name - ts.getData = function(h, ch, key) { - var val = '', $h = $(h), m, cl; - if (!$h.length) { return ''; } - m = $.metadata ? $h.metadata() : false; - cl = ' ' + ($h.attr('class') || ''); - if (typeof $h.data(key) !== 'undefined' || typeof $h.data(key.toLowerCase()) !== 'undefined'){ - // "data-lockedOrder" is assigned to "lockedorder"; but "data-locked-order" is assigned to "lockedOrder" - // "data-sort-initial-order" is assigned to "sortInitialOrder" - val += $h.data(key) || $h.data(key.toLowerCase()); - } else if (m && typeof m[key] !== 'undefined') { - val += m[key]; - } else if (ch && typeof ch[key] !== 'undefined') { - val += ch[key]; - } else if (cl !== ' ' && cl.match(' ' + key + '-')) { - // include sorter class name "sorter-text", etc - val = cl.match( new RegExp(' ' + key + '-(\\w+)') )[1] || ''; - } - return $.trim(val); - }; + // detach tbody but save the position + // don't use tbody because there are portions that look for a tbody index (updateCell) + processTbody : function( table, $tb, getIt ) { + table = $( table )[ 0 ]; + if ( getIt ) { + table.isProcessing = true; + $tb.before( '' ); + return $.fn.detach ? $tb.detach() : $tb.remove(); + } + var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' ); + $tb.insertAfter( holdr ); + holdr.remove(); + table.isProcessing = false; + }, - 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,'.'); + clearTableBody : function( table ) { + $( table )[ 0 ].config.$tbodies.children().detach(); + }, + + // used when replacing accented characters during sorting + 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\u014d', // óòôõöō + 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ + 'ss': '\u00df', // ß (s sharp) + 'SS': '\u1e9e', // ẞ (Capital sharp s) + 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů + 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ + }, + + replaceAccents : function( str ) { + var chr, + acc = '[', + eq = ts.characterEquivalents; + if ( !ts.characterRegex ) { + ts.characterRegexArray = {}; + for ( chr in eq ) { + if ( typeof chr === 'string' ) { + acc += eq[ chr ]; + ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' ); + } } - if(/^\s*\([.\d]+\)/.test(s)) { - // make (#) into a negative number -> (10) = -10 - s = s.replace(/^\s*\(/,'-').replace(/\)/,''); + ts.characterRegex = new RegExp( acc + ']' ); + } + if ( ts.characterRegex.test( str ) ) { + for ( chr in eq ) { + if ( typeof chr === 'string' ) { + str = str.replace( ts.characterRegexArray[ chr ], chr ); + } } - i = parseFloat(s); - // return the text instead of zero - return isNaN(i) ? $.trim(s) : i; - }; + } + return str; + }, - ts.isDigit = function(s) { - // replace all unwanted chars and match - return isNaN(s) ? (/^[\-+(]?\d+[)]?$/).test(s.toString().replace(/[,.'"\s]/g, '')) : true; - }; + validateOptions : function( c ) { + var setting, setting2, typ, timer, + // ignore options containing an array + ignore = 'headers sortForce sortList sortAppend widgets'.split( ' ' ), + orig = c.originalSettings; + if ( orig ) { + if ( ts.debug(c, 'core') ) { + timer = new Date(); + } + for ( setting in orig ) { + typ = typeof ts.defaults[setting]; + if ( typ === 'undefined' ) { + console.warn( 'Tablesorter Warning! "table.config.' + setting + '" option not recognized' ); + } else if ( typ === 'object' ) { + for ( setting2 in orig[setting] ) { + typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2]; + if ( $.inArray( setting, ignore ) < 0 && typ === 'undefined' ) { + console.warn( 'Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized' ); + } + } + } + } + if ( ts.debug(c, 'core') ) { + console.log( 'validate options time:' + ts.benchmark( timer ) ); + } + } + }, - }() - }); + // restore headers + restoreHeaders : function( table ) { + var index, $cell, + c = $( table )[ 0 ].config, + $headers = c.$table.find( c.selectorHeaders ), + len = $headers.length; + // don't use c.$headers here in case header cells were swapped + for ( index = 0; index < len; index++ ) { + $cell = $headers.eq( index ); + // only restore header cells if it is wrapped + // because this is also used by the updateAll method + if ( $cell.find( '.' + ts.css.headerIn ).length ) { + $cell.html( c.headerContent[ index ] ); + } + } + }, + + destroy : function( table, removeClasses, callback ) { + table = $( table )[ 0 ]; + if ( !table.hasInitialized ) { return; } + // remove all widgets + ts.removeWidget( table, true, false ); + var events, + $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.triggerHandler( 'applyWidgetId', [ 'uitheme' ] ); + $t.triggerHandler( 'applyWidgetId', [ 'zebra' ] ); + } + // remove widget added rows, just in case + $h.find( 'tr' ).not( $r ).remove(); + // disable tablesorter - not using .unbind( namespace ) because namespacing was + // added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/ + events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' + + 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' + + 'keypress sortBegin sortEnd resetToLoadState '.split( ' ' ) + .join( c.namespace + ' ' ); + $t + .removeData( 'tablesorter' ) + .unbind( events.replace( ts.regex.spaces, ' ' ) ); + 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 + ' ' ) ).replace( ts.regex.spaces, ' ' ) ); + ts.restoreHeaders( table ); + $t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false ); + $t.removeClass(c.namespace.slice(1)); + // clear flag in case the plugin is initialized again + table.hasInitialized = false; + delete table.config.cache; + if ( typeof callback === 'function' ) { + callback( table ); + } + if ( ts.debug(c, 'core') ) { + console.log( 'tablesorter has been removed' ); + } + } - // make shortcut - var ts = $.tablesorter; + }; + + $.fn.tablesorter = function( settings ) { + return this.each( function() { + var table = this, + // merge & extend config options + c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods ); + // save initial settings + c.originalSettings = settings; + // create a table from data (build table widget) + if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) { + // return the table (in case the original target is the table's container) + ts.buildTable( table, c ); + } else { + ts.setup( table, c ); + } + }); + }; + + // set up debug logs + if ( !( window.console && window.console.log ) ) { + // access $.tablesorter.logs for browsers that don't have a console... + ts.logs = []; + /*jshint -W020 */ + console = {}; + console.log = console.warn = console.error = console.table = function() { + var arg = arguments.length > 1 ? arguments : arguments[0]; + ts.logs[ ts.logs.length ] = { date: Date.now(), log: arg }; + }; + } - // 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' }); - // add default parsers ts.addParser({ - id: "text", - is: function(s, table, node) { + id : 'text', + is : function() { return true; }, - format: function(s, table, cell, cellIndex) { + format : function( str, table ) { var c = table.config; - s = $.trim( c.ignoreCase ? s.toLocaleLowerCase() : s ); - return c.sortLocaleCompare ? ts.replaceAccents(s) : s; + if ( str ) { + str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str ); + str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str; + } + return str; }, - type: "text" + type : 'text' }); + ts.regex.nondigit = /[^\w,. \-()]/g; ts.addParser({ - id: "currency", - is: function(s) { - return (/^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/).test(s); // £$€¤¥¢ + id : 'digit', + is : function( str ) { + return ts.isDigit( str ); }, - format: function(s, table) { - return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + format : function( str, table ) { + var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); + return str && typeof num === 'number' ? num : + str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; }, - type: "numeric" + type : 'numeric' }); + ts.regex.currencyReplace = /[+\-,. ]/g; + ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/; ts.addParser({ - id: "ipAddress", - is: function(s) { - return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); + id : 'currency', + is : function( str ) { + str = ( str || '' ).replace( ts.regex.currencyReplace, '' ); + // test for £$€¤¥¢ + return ts.regex.currencyTest.test( str ); }, - format: function(s, table) { - var i, a = s.split("."), - r = "", - l = a.length; - for (i = 0; i < l; i++) { - r += ("00" + a[i]).slice(-3); - } - return ts.formatFloat(r, table); + format : function( str, table ) { + var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); + return str && typeof num === 'number' ? num : + str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; }, - type: "numeric" + type : 'numeric' }); + // too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme + // now, this regex can be updated before initialization + ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//; + ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/; ts.addParser({ - id: "url", - is: function(s) { - return (/^(https?|ftp|file):\/\//).test(s); + id : 'url', + is : function( str ) { + return ts.regex.urlProtocolTest.test( str ); }, - format: function(s) { - return $.trim(s.replace(/(https?|ftp|file):\/\//, '')); + format : function( str ) { + return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str; }, - type: "text" + type : 'text' }); + ts.regex.dash = /-/g; + ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/; ts.addParser({ - id: "isoDate", - is: function(s) { - return (/^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/).test(s); + id : 'isoDate', + is : function( str ) { + return ts.regex.isoDate.test( str ); }, - format: function(s, table) { - return ts.formatFloat((s !== "") ? (new Date(s.replace(/-/g, "/")).getTime() || "") : "", table); + format : function( str ) { + var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str; + return date instanceof Date && isFinite( date ) ? date.getTime() : str; }, - type: "numeric" + type : 'numeric' }); + ts.regex.percent = /%/g; + ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/; ts.addParser({ - id: "percent", - is: function(s) { - return (/(\d\s?%|%\s?\d)/).test(s); + id : 'percent', + is : function( str ) { + return ts.regex.percentTest.test( str ) && str.length < 15; }, - format: function(s, table) { - return ts.formatFloat(s.replace(/%/g, ""), table); + format : function( str, table ) { + return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str; }, - type: "numeric" + type : 'numeric' }); + // added image parser to core v2.17.9 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); + id : 'image', + is : function( str, table, node, $node ) { + return $node.find( 'img' ).length > 0; }, - format: function(s, table) { - return ts.formatFloat( (new Date(s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ''), table); + format : function( str, table, cell ) { + return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str; }, - type: "numeric" + parsed : true, // filter widget flag + type : 'text' }); + ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser + ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i; + ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i; ts.addParser({ - id: "shortDate", // "mmddyyyy", "ddmmyyyy" or "yyyymmdd" - is: function(s) { - // testing for ####-##-####, so it's not perfect - return (/^(\d{1,2}|\d{4})[\/\-\,\.\s+]\d{1,2}[\/\-\.\,\s+](\d{1,2}|\d{4})$/).test(s); - }, - format: function(s, table, cell, cellIndex) { - var c = table.config, ci = c.headerList[cellIndex], - format = ci.shortDateFormat; - if (typeof format === 'undefined') { - // cache header formatting so it doesn't getData for every cell in the column - format = ci.shortDateFormat = ts.getData( ci, c.headers[cellIndex], 'dateFormat') || c.dateFormat; - } - s = s.replace(/\s+/g," ").replace(/[\-|\.|\,]/g, "/"); - if (format === "mmddyyyy") { - s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$1/$2"); - } else if (format === "ddmmyyyy") { - s = s.replace(/(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, "$3/$2/$1"); - } else if (format === "yyyymmdd") { - s = s.replace(/(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/, "$1/$2/$3"); - } - return ts.formatFloat( (new Date(s).getTime() || ''), table); - }, - type: "numeric" + id : 'usLongDate', + is : function( str ) { + // two digit years are not allowed cross-browser + // Jan 01, 2013 12:34:56 PM or 01 Jan 2013 + return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str ); + }, + format : function( str ) { + var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str; + return date instanceof Date && isFinite( date ) ? date.getTime() : str; + }, + type : 'numeric' }); + // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included + ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/; + // escaped "-" because JSHint in Firefox was showing it as an error + ts.regex.shortDateReplace = /[\-.,]/g; + // XXY covers MDY & DMY formats + ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/; + ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/; + ts.convertFormat = function( dateString, format ) { + dateString = ( dateString || '' ) + .replace( ts.regex.spaces, ' ' ) + .replace( ts.regex.shortDateReplace, '/' ); + if ( format === 'mmddyyyy' ) { + dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' ); + } else if ( format === 'ddmmyyyy' ) { + dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' ); + } else if ( format === 'yyyymmdd' ) { + dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' ); + } + var date = new Date( dateString ); + return date instanceof Date && isFinite( date ) ? date.getTime() : ''; + }; + 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); + id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd' + is : function( str ) { + str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' ); + return ts.regex.shortDateTest.test( str ); }, - format: function(s, table) { - return ts.formatFloat( (new Date("2000/01/01 " + s.replace(/(\S)([AP]M)$/i, "$1 $2")).getTime() || ""), table); + format : function( str, table, cell, cellIndex ) { + if ( str ) { + var c = table.config, + $header = c.$headerIndexed[ cellIndex ], + format = $header.length && $header.data( 'dateFormat' ) || + ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) || + c.dateFormat; + // save format because getData can be slow... + if ( $header.length ) { + $header.data( 'dateFormat', format ); + } + return ts.convertFormat( str, format ) || str; + } + return str; }, - type: "numeric" + type : 'numeric' }); + // match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk + ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i; + ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i; ts.addParser({ - id: "digit", - is: function(s) { - return ts.isDigit(s); + id : 'time', + is : function( str ) { + return ts.regex.timeTest.test( str ); }, - format: function(s, table) { - return ts.formatFloat(s.replace(/[^\w,. \-()]/g, ""), table); + format : function( str ) { + // isolate time... ignore month, day and year + var temp, + timePart = ( str || '' ).match( ts.regex.timeMatch ), + orig = new Date( str ), + // no time component? default to 00:00 by leaving it out, but only if str is defined + time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ), + date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time; + if ( date instanceof Date && isFinite( date ) ) { + temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0; + // if original string was a valid date, add it to the decimal so the column sorts in some kind of order + // luckily new Date() ignores the decimals + return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime(); + } + return str; }, - type: "numeric" + type : 'numeric' }); ts.addParser({ - id: "metadata", - is: function(s) { + id : 'metadata', + is : function() { return false; }, - format: function(s, table, cell) { + format : function( str, table, cell ) { var c = table.config, - p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; - return $(cell).metadata()[p]; + p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName; + return $( cell ).metadata()[ p ]; }, - type: "numeric" + type : 'numeric' }); + /* + ██████ ██████ █████▄ █████▄ ▄████▄ + ▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██ + ▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██ + ██████ ██████ █████▀ ██ ██ ██ ██ + */ // add default widgets ts.addWidget({ - id: "zebra", - format: function(table, c, wo) { - var $tb, $tv, $tr, row, even, time, k, l, - child = new RegExp(c.cssChildRow, 'i'), - b = c.$tbodies; - if (c.debug) { - time = new Date(); - } - for (k = 0; k < b.length; k++ ) { + id : 'zebra', + priority : 90, + format : function( table, c, wo ) { + var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len, + child = new RegExp( c.cssChildRow, 'i' ), + $tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) ); + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { // loop through the visible rows - $tb = b.eq(k); - l = $tb.children('tr').length; - if (l > 1) { - row = 0; - $tv = $tb.children('tr:visible'); - // revered back to using jQuery each - strangely it's the fastest method - /*jshint loopfunc:true */ - $tv.each(function(){ - $tr = $(this); - // style children rows the same way the parent row was styled - if (!child.test(this.className)) { row++; } - even = (row % 2 === 0); - $tr.removeClass(wo.zebra[even ? 1 : 0]).addClass(wo.zebra[even ? 0 : 1]); - }); + count = 0; + $visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove ); + len = $visibleRows.length; + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + $row = $visibleRows.eq( rowIndex ); + // style child rows the same way the parent row was styled + if ( !child.test( $row[ 0 ].className ) ) { count++; } + isEven = ( count % 2 === 0 ); + $row + .removeClass( wo.zebra[ isEven ? 1 : 0 ] ) + .addClass( wo.zebra[ isEven ? 0 : 1 ] ); } } - if (c.debug) { - ts.benchmark("Applying Zebra widget", time); - } }, - remove: function(table, c, wo){ - var k, $tb, - b = c.$tbodies, - rmv = (c.widgetOptions.zebra || [ "even", "odd" ]).join(' '); - for (k = 0; k < b.length; k++ ){ - $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody - $tb.children().removeClass(rmv); - $.tablesorter.processTbody(table, $tb, false); // restore tbody + remove : function( table, c, wo, refreshing ) { + if ( refreshing ) { return; } + var tbodyIndex, $tbody, + $tbodies = c.$tbodies, + toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' ); + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody + $tbody.children().removeClass( toRemove ); + ts.processTbody( table, $tbody, false ); // restore tbody } } }); -})(jQuery); \ No newline at end of file +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js b/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js index 49749eb..567374e 100644 --- a/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js +++ b/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.widgets.js @@ -1,981 +1,3184 @@ -/*! tableSorter 2.4+ widgets - updated 1/29/2013 - * - * Column Styles - * Column Filters - * Column Resizing - * Sticky Header - * UI Theme (generalized) - * Save Sort - * ["zebra", "uitheme", "stickyHeaders", "filter", "columns"] - */ -/*jshint browser:true, jquery:true, unused:false, loopfunc:true */ -/*global jQuery: false, localStorage: false, navigator: false */ -;(function($){ -"use strict"; -$.tablesorter = $.tablesorter || {}; - -$.tablesorter.themes = { - "bootstrap" : { - table : 'table table-bordered table-striped', - header : 'bootstrap-header', // give the header a gradient background - footerRow : '', - footerCells: '', - icons : '', // add "icon-white" to make them white; this icon class is added to the in the header - sortNone : 'bootstrap-icon-unsorted', - sortAsc : 'icon-chevron-up', - sortDesc : 'icon-chevron-down', - active : '', // applied when column is sorted - hover : '', // use custom css here - bootstrap class may not override it - filterRow : '', // filter row class - even : '', // even row zebra striping - odd : '' // odd row zebra striping - }, - "jui" : { - table : 'ui-widget ui-widget-content ui-corner-all', // table classes - header : 'ui-widget-header ui-corner-all ui-state-default', // header classes - footerRow : '', - footerCells: '', - icons : 'ui-icon', // icon class added to the in the header - sortNone : 'ui-icon-carat-2-n-s', - sortAsc : 'ui-icon-carat-1-n', - sortDesc : 'ui-icon-carat-1-s', - active : 'ui-state-active', // applied when column is sorted - hover : 'ui-state-hover', // hover class - filterRow : '', - even : 'ui-widget-content', // even row zebra striping - odd : 'ui-state-default' // odd row zebra striping - } -}; - -// *** Store data in local storage, with a cookie fallback *** -/* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) - if you need it, then include https://github.com/douglascrockford/JSON-js - - $.parseJSON is not available is jQuery versions older than 1.4.1, using older - versions will only allow storing information for one page at a time - - // *** Save data (JSON format only) *** - // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid - var val = { "mywidget" : "data1" }; // valid JSON uses double quotes - // $.tablesorter.storage(table, key, val); - $.tablesorter.storage(table, 'tablesorter-mywidget', val); - - // *** Get data: $.tablesorter.storage(table, key); *** - v = $.tablesorter.storage(table, 'tablesorter-mywidget'); - // val may be empty, so also check for your data - val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; - alert(val); // "data1" if saved, or "" if not +/*** This file is dynamically generated *** +█████▄ ▄████▄ █████▄ ▄████▄ ██████ ███████▄ ▄████▄ █████▄ ██ ██████ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ██▄▄██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀▀▀██ +█████▀ ▀████▀ ██ ██ ▀████▀ ██ ██ ██ ██ ▀████▀ █████▀ ██ ██ █████▀ */ -$.tablesorter.storage = function(table, key, val){ - var d, k, ls = false, v = {}, - id = table.id || $('.tablesorter').index( $(table) ), - url = window.location.pathname; - try { ls = !!(localStorage.getItem); } catch(e) {} - // *** get val *** - if ($.parseJSON){ - if (ls){ - v = $.parseJSON(localStorage[key]) || {}; - } else { - k = document.cookie.split(/[;\s|=]/); // cookie - d = $.inArray(key, k) + 1; // add one to get from the key to the value - v = (d !== 0) ? $.parseJSON(k[d]) || {} : {}; - } - } - // allow val to be an empty string to - if ((val || val === '') && window.JSON && JSON.hasOwnProperty('stringify')){ - // add unique identifiers = url pathname > table ID/index on page > data - if (!v[url]) { - v[url] = {}; - } - v[url][id] = val; - // *** set val *** - if (ls){ - localStorage[key] = JSON.stringify(v); - } else { - d = new Date(); - d.setTime(d.getTime() + (31536e+6)); // 365 days - document.cookie = key + '=' + (JSON.stringify(v)).replace(/\"/g,'\"') + '; expires=' + d.toGMTString() + '; path=/'; +/*! tablesorter (FORK) - updated 2020-03-03 (v2.31.3)*/ +/* Includes widgets ( storage,uitheme,columns,filter,stickyHeaders,resizable,saveSort ) */ +(function(factory){if (typeof define === 'function' && define.amd){define(['jquery'], factory);} else if (typeof module === 'object' && typeof module.exports === 'object'){module.exports = factory(require('jquery'));} else {factory(jQuery);}}(function(jQuery) { +/*! Widget: storage - updated 2018-03-18 (v2.30.0) */ +/*global JSON:false */ +;(function ($, window, document) { + 'use strict'; + + var ts = $.tablesorter || {}; + + // update defaults for validator; these values must be falsy! + $.extend(true, ts.defaults, { + fixedUrl: '', + widgetOptions: { + storage_fixedUrl: '', + storage_group: '', + storage_page: '', + storage_storageType: '', + storage_tableId: '', + storage_useSessionStorage: '' } - } else { - return v && v[url] ? v[url][id] : {}; - } -}; - -// Widget: General UI theme -// "uitheme" option in "widgetOptions" -// ************************** -$.tablesorter.addWidget({ - id: "uitheme", - format: function(table){ - var time, klass, $el, $tar, - t = $.tablesorter.themes, - $t = $(table), + }); + + // *** Store data in local storage, with a cookie fallback *** + /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) + if you need it, then include https://github.com/douglascrockford/JSON-js + + $.parseJSON is not available is jQuery versions older than 1.4.1, using older + versions will only allow storing information for one page at a time + + // *** Save data (JSON format only) *** + // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid + var val = { "mywidget" : "data1" }; // valid JSON uses double quotes + // $.tablesorter.storage(table, key, val); + $.tablesorter.storage(table, 'tablesorter-mywidget', val); + + // *** Get data: $.tablesorter.storage(table, key); *** + v = $.tablesorter.storage(table, 'tablesorter-mywidget'); + // val may be empty, so also check for your data + val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; + alert(val); // 'data1' if saved, or '' if not + */ + ts.storage = function(table, key, value, options) { + table = $(table)[0]; + var cookieIndex, cookies, date, + hasStorage = false, + values = {}, c = table.config, - wo = c.widgetOptions, - theme = c.theme !== 'default' ? c.theme : wo.uitheme || 'jui', // default uitheme is 'jui' - o = t[ t[theme] ? theme : t[wo.uitheme] ? wo.uitheme : 'jui'], - $h = $(c.headerList), - sh = 'tr.' + (wo.stickyHeaders || 'tablesorter-stickyHeader'), - rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc; - if (c.debug) { time = new Date(); } - if (!$t.hasClass('tablesorter-' + theme) || c.theme === theme || !table.hasInitialized){ - // update zebra stripes - if (o.even !== '') { wo.zebra[0] += ' ' + o.even; } - if (o.odd !== '') { wo.zebra[1] += ' ' + o.odd; } - // add table/footer class names - t = $t - // remove other selected themes; use widgetOptions.theme_remove - .removeClass( c.theme === '' ? '' : 'tablesorter-' + c.theme ) - .addClass('tablesorter-' + theme + ' ' + o.table) // add theme widget class name - .find('tfoot'); - if (t.length) { - t - .find('tr').addClass(o.footerRow) - .children('th, td').addClass(o.footerCells); - } - // update header classes - $h - .addClass(o.header) - .filter(':not(.sorter-false)') - .hover(function(){ - $(this).addClass(o.hover); - }, function(){ - $(this).removeClass(o.hover); - }); - if (!$h.find('.tablesorter-wrapper').length) { - // Firefox needs this inner div to position the resizer correctly - $h.wrapInner('
'); - } - if (c.cssIcon){ - // if c.cssIcon is '', then no is added to the header - $h.find('.' + c.cssIcon).addClass(o.icons); + wo = c && c.widgetOptions, + debug = ts.debug(c, 'storage'), + storageType = ( + ( options && options.storageType ) || ( wo && wo.storage_storageType ) + ).toString().charAt(0).toLowerCase(), + // deprecating "useSessionStorage"; any storageType setting overrides it + session = storageType ? '' : + ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ), + $table = $(table), + // id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId, + // (4) table ID, then (5) table index + id = options && options.id || + $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') || + wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ), + // url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl, + // (4) table.config.fixedUrl (deprecated), then (5) window location path + url = options && options.url || + $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') || + wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname; + + // skip if using cookies + if (storageType !== 'c') { + storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage'; + // https://gist.github.com/paulirish/5558557 + if (storageType in window) { + try { + window[storageType].setItem('_tmptest', 'temp'); + hasStorage = true; + window[storageType].removeItem('_tmptest'); + } catch (error) { + console.warn( storageType + ' is not supported in this browser' ); + } } - if ($t.hasClass('hasFilters')){ - $h.find('.tablesorter-filter-row').addClass(o.filterRow); + } + if (debug) { + console.log('Storage >> Using', hasStorage ? storageType : 'cookies'); + } + // *** get value *** + if ($.parseJSON) { + if (hasStorage) { + values = $.parseJSON( window[storageType][key] || 'null' ) || {}; + } else { + // old browser, using cookies + cookies = document.cookie.split(/[;\s|=]/); + // add one to get from the key to the value + cookieIndex = $.inArray(key, cookies) + 1; + values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {}; } } - $.each($h, function(i){ - $el = $(this); - $tar = (c.cssIcon) ? $el.find('.' + c.cssIcon) : $el; - if (this.sortDisabled){ - // no sort arrows for disabled columns! - $el.removeClass(rmv); - $tar.removeClass(rmv + ' tablesorter-icon ' + o.icons); + // allow value to be an empty string too + if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) { + // add unique identifiers = url pathname > table ID/index on page > data + if (!values[url]) { + values[url] = {}; + } + values[url][id] = value; + // *** set value *** + if (hasStorage) { + window[storageType][key] = JSON.stringify(values); } else { - t = ($t.hasClass('hasStickyHeaders')) ? $t.find(sh).find('th').eq(i).add($el) : $el; - klass = ($el.hasClass(c.cssAsc)) ? o.sortAsc : ($el.hasClass(c.cssDesc)) ? o.sortDesc : $el.hasClass(c.cssHeader) ? o.sortNone : ''; - $el[klass === o.sortNone ? 'removeClass' : 'addClass'](o.active); - $tar.removeClass(rmv).addClass(klass); + date = new Date(); + date.setTime(date.getTime() + (31536e+6)); // 365 days + document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; } - }); - if (c.debug){ - $.tablesorter.benchmark("Applying " + theme + " theme", time); + } else { + return values && values[url] ? values[url][id] : ''; } - }, - remove: function(table, c, wo){ - var $t = $(table), - theme = typeof wo.uitheme === 'object' ? 'jui' : wo.uitheme || 'jui', - o = typeof wo.uitheme === 'object' ? wo.uitheme : $.tablesorter.themes[ $.tablesorter.themes.hasOwnProperty(theme) ? theme : 'jui'], - $h = $t.children('thead').children(), - rmv = o.sortNone + ' ' + o.sortDesc + ' ' + o.sortAsc; - $t - .removeClass('tablesorter-' + theme + ' ' + o.table) - .find(c.cssHeader).removeClass(o.header); - $h - .unbind('mouseenter mouseleave') // remove hover - .removeClass(o.hover + ' ' + rmv + ' ' + o.active) - .find('.tablesorter-filter-row').removeClass(o.filterRow); - $h.find('.tablesorter-icon').removeClass(o.icons); - } -}); - -// Widget: Column styles -// "columns", "columns_thead" (true) and -// "columns_tfoot" (true) options in "widgetOptions" -// ************************** -$.tablesorter.addWidget({ - id: "columns", - format: function(table){ - var $tb, $tr, $td, $t, time, last, rmv, i, k, l, - $tbl = $(table), - c = table.config, - wo = c.widgetOptions, - b = c.$tbodies, - list = c.sortList, - len = list.length, - css = [ "primary", "secondary", "tertiary" ]; // default options - // keep backwards compatibility, for now - css = (c.widgetColumns && c.widgetColumns.hasOwnProperty('css')) ? c.widgetColumns.css || css : - (wo && wo.hasOwnProperty('columns')) ? wo.columns || css : css; - last = css.length-1; - rmv = css.join(' '); - if (c.debug){ - time = new Date(); + }; + +})(jQuery, window, document); + +/*! Widget: uitheme - updated 2018-03-18 (v2.30.0) */ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + ts.themes = { + 'bootstrap' : { + table : 'table table-bordered table-striped', + caption : 'caption', + // header class names + header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css) + sortNone : '', + sortAsc : '', + sortDesc : '', + active : '', // applied when column is sorted + hover : '', // custom css required - a defined bootstrap style may not override other classes + // icon class names + icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the in the header + iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted + iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort + iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort + filterRow : '', // filter row class + footerRow : '', + footerCells : '', + even : '', // even row zebra striping + odd : '' // odd row zebra striping + }, + 'jui' : { + table : 'ui-widget ui-widget-content ui-corner-all', // table classes + caption : 'ui-widget-content', + // header class names + header : 'ui-widget-header ui-corner-all ui-state-default', // header classes + sortNone : '', + sortAsc : '', + sortDesc : '', + active : 'ui-state-active', // applied when column is sorted + hover : 'ui-state-hover', // hover class + // icon class names + icons : 'ui-icon', // icon class added to the in the header + iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted + iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort + iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort + filterRow : '', + footerRow : '', + footerCells : '', + even : 'ui-widget-content', // even row zebra striping + odd : 'ui-state-default' // odd row zebra striping } - // check if there is a sort (on initialization there may not be one) - for (k = 0; k < b.length; k++ ){ - $tb = $.tablesorter.processTbody(table, b.eq(k), true); // detach tbody - $tr = $tb.children('tr'); - l = $tr.length; - // loop through the visible rows - $tr.each(function(){ - $t = $(this); - if (this.style.display !== 'none'){ - // remove all columns class names - $td = $t.children().removeClass(rmv); - // add appropriate column class names - if (list && list[0]){ - // primary sort column class - $td.eq(list[0][0]).addClass(css[0]); - if (len > 1){ - for (i = 1; i < len; i++){ - // secondary, tertiary, etc sort column classes - $td.eq(list[i][0]).addClass( css[i] || css[last] ); - } + }; + + $.extend(ts.css, { + wrapper : 'tablesorter-wrapper' // ui theme & resizable + }); + + ts.addWidget({ + id: 'uitheme', + priority: 10, + format: function(table, c, wo) { + var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme, + themesAll = ts.themes, + $table = c.$table.add( $( c.namespace + '_extra_table' ) ), + $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ), + theme = c.theme || 'jui', + themes = themesAll[theme] || {}, + remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ), + iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) ), + debug = ts.debug(c, 'uitheme'); + if (debug) { time = new Date(); } + // initialization code - run once + if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) { + wo.uitheme_applied = true; + oldtheme = themesAll[c.appliedTheme] || {}; + hasOldTheme = !$.isEmptyObject(oldtheme); + oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : ''; + oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : ''; + if (hasOldTheme) { + wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') ); + wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') ); + c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') ); + } + // update zebra stripes + if (themes.even) { wo.zebra[0] += ' ' + themes.even; } + if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; } + // add caption style + $table.children('caption') + .removeClass(oldtheme.caption || '') + .addClass(themes.caption); + // add table/footer class names + $tfoot = $table + // remove other selected themes + .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') ) + .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name + .children('tfoot'); + c.appliedTheme = c.theme; + + if ($tfoot.length) { + $tfoot + // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed + .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow) + .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells); + } + // update header classes + $headers + .removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' ) + .addClass(themes.header) + .not('.sorter-false') + .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') + .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { + // toggleClass with switch added in jQuery 1.3 + $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || ''); + }); + + $headers.each(function() { + var $this = $(this); + if (!$this.find('.' + ts.css.wrapper).length) { + // Firefox needs this inner div to position the icon & resizer correctly + $this.wrapInner('
'); + } + }); + if (c.cssIcon) { + // if c.cssIcon is '', then no is added to the header + $headers + .find('.' + ts.css.icon) + .removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '') + .addClass(themes.icons || ''); + } + // filter widget initializes after uitheme + if (ts.hasWidget( c.table, 'filter' )) { + tmp = function() { + $table.children('thead').children('.' + ts.css.filterRow) + .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '') + .addClass(themes.filterRow || ''); + }; + if (wo.filter_initialized) { + tmp(); + } else { + $table.one('filterInit', function() { + tmp(); + }); + } + } + } + for (i = 0; i < c.columns; i++) { + $header = c.$headers + .add($(c.namespace + '_extra_headers')) + .not('.sorter-false') + .filter('[data-column="' + i + '"]'); + $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $(); + $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last'); + if ($h.length) { + $header.removeClass(remove); + $icon.removeClass(iconRmv); + if ($h[0].sortDisabled) { + // no sort arrows for disabled columns! + $icon.removeClass(themes.icons || ''); + } else { + hdr = themes.sortNone; + icon = themes.iconSortNone; + if ($h.hasClass(ts.css.sortAsc)) { + hdr = [ themes.sortAsc, themes.active ].join(' '); + icon = themes.iconSortAsc; + } else if ($h.hasClass(ts.css.sortDesc)) { + hdr = [ themes.sortDesc, themes.active ].join(' '); + icon = themes.iconSortDesc; } + $header.addClass(hdr); + $icon.addClass(icon || ''); } } - }); - $.tablesorter.processTbody(table, $tb, false); - } - // add classes to thead and tfoot - $tr = wo.columns_thead !== false ? 'thead tr' : ''; - if (wo.columns_tfoot !== false) { - $tr += ($tr === '' ? '' : ',') + 'tfoot tr'; + } + if (debug) { + console.log('uitheme >> Applied ' + theme + ' theme' + ts.benchmark(time)); + } + }, + remove: function(table, c, wo, refreshing) { + if (!wo.uitheme_applied) { return; } + var $table = c.$table, + theme = c.appliedTheme || 'jui', + themes = ts.themes[ theme ] || ts.themes.jui, + $headers = $table.children('thead').children(), + remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc, + iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc; + $table.removeClass('tablesorter-' + theme + ' ' + themes.table); + wo.uitheme_applied = false; + if (refreshing) { return; } + $table.find(ts.css.header).removeClass(themes.header); + $headers + .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover + .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) + .filter('.' + ts.css.filterRow) + .removeClass(themes.filterRow); + $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv); } - if ($tr.length) { - $t = $tbl.find($tr).children().removeClass(rmv); - if (list && list[0]){ - // primary sort column class - $t.filter('[data-column="' + list[0][0] + '"]').addClass(css[0]); - if (len > 1){ - for (i = 1; i < len; i++){ - // secondary, tertiary, etc sort column classes - $t.filter('[data-column="' + list[i][0] + '"]').addClass(css[i] || css[last]); + }); + +})(jQuery); + +/*! Widget: columns - updated 5/24/2017 (v2.28.11) */ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + ts.addWidget({ + id: 'columns', + priority: 65, + options : { + columns : [ 'primary', 'secondary', 'tertiary' ] + }, + format: function(table, c, wo) { + var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, + $table = c.$table, + $tbodies = c.$tbodies, + sortList = c.sortList, + len = sortList.length, + // removed c.widgetColumns support + css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ], + last = css.length - 1; + remove = css.join(' '); + // check if there is a sort (on initialization there may not be one) + for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody + $rows = $tbody.children('tr'); + // loop through the visible rows + $rows.each(function() { + $row = $(this); + if (this.style.display !== 'none') { + // remove all columns class names + $cells = $row.children().removeClass(remove); + // add appropriate column class names + if (sortList && sortList[0]) { + // primary sort column class + $cells.eq(sortList[0][0]).addClass(css[0]); + if (len > 1) { + for (indx = 1; indx < len; indx++) { + // secondary, tertiary, etc sort column classes + $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); + } + } + } + } + }); + ts.processTbody(table, $tbody, false); + } + // add classes to thead and tfoot + rows = wo.columns_thead !== false ? [ 'thead tr' ] : []; + if (wo.columns_tfoot !== false) { + rows.push('tfoot tr'); + } + if (rows.length) { + $rows = $table.find( rows.join(',') ).children().removeClass(remove); + if (len) { + for (indx = 0; indx < len; indx++) { + // add primary. secondary, tertiary, etc sort column classes + $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); } } } + }, + remove: function(table, c, wo) { + var tbodyIndex, $tbody, + $tbodies = c.$tbodies, + remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' '); + c.$headers.removeClass(remove); + c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); + for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody + $tbody.children('tr').each(function() { + $(this).children().removeClass(remove); + }); + ts.processTbody(table, $tbody, false); // restore tbody + } } - if (c.debug){ - $.tablesorter.benchmark("Applying Columns widget", time); - } - }, - remove: function(table, c, wo){ - var k, $tb, - b = c.$tbodies, - rmv = (c.widgetOptions.columns || [ "primary", "secondary", "tertiary" ]).join(' '); - c.$headers.removeClass(rmv); - $(table).children('tfoot').children('tr').children('th, td').removeClass(rmv); - for (k = 0; k < b.length; k++ ){ - $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody - $tb.children('tr').each(function(){ - $(this).children().removeClass(rmv); - }); - $.tablesorter.processTbody(table, $tb, false); // restore tbody + }); + +})(jQuery); + +/*! Widget: filter - updated 2018-03-18 (v2.30.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +;( function ( $ ) { + 'use strict'; + var tsf, tsfRegex, + ts = $.tablesorter || {}, + tscss = ts.css, + tskeyCodes = ts.keyCodes; + + $.extend( tscss, { + filterRow : 'tablesorter-filter-row', + filter : 'tablesorter-filter', + filterDisabled : 'disabled', + filterRowHide : 'hideme' + }); + + $.extend( tskeyCodes, { + backSpace : 8, + escape : 27, + space : 32, + left : 37, + down : 40 + }); + + ts.addWidget({ + id: 'filter', + priority: 50, + options : { + filter_cellFilter : '', // css class name added to the filter cell ( string or array ) + filter_childRows : false, // if true, filter includes child row content in the search + filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped + filter_childWithSibs : true, // if true, include matching child row siblings + filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query ) + filter_columnFilters : true, // if true, a filter will be added to the top of each table column + filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added ) + filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value + filter_defaultFilter : {}, // add a default column filter type '~{query}' to make fuzzy searches default; '{q1} AND {q2}' to make all searches use a logical AND. + filter_excludeFilter : {}, // filters to exclude, per column + filter_external : '', // jQuery selector string ( or jQuery object ) of external filters + filter_filteredRow : 'filtered', // class added to filtered rows; define in css with "display:none" to hide the filtered-out rows + filter_filterLabel : 'Filter "{{label}}" column by...', // Aria-label added to filter input/select; see #1495 + filter_formatter : null, // add custom filter elements to the filter row + filter_functions : null, // add custom filter functions using this option + filter_hideEmpty : true, // hide filter row when table is empty + filter_hideFilters : false, // collapse filter row when mouse leaves the area + filter_ignoreCase : true, // if true, make all searches case-insensitive + filter_liveSearch : true, // if true, search column content while the user types ( with a delay ) + filter_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class + filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available ( visible ) options within the drop down + filter_placeholder : { search : '', select : '' }, // default placeholder text ( overridden by any header 'data-placeholder' setting ) + filter_reset : null, // jQuery selector string of an element used to reset the filters + filter_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers + filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters + filter_searchDelay : 300, // typing delay in milliseconds before starting a search + filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true + filter_selectSource : null, // include a function to return an array of values to be added to the column filter select + filter_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text + filter_serversideFiltering : false, // if true, must perform server-side filtering b/c client-side filtering is disabled, but the ui and events will still be used. + filter_startsWith : false, // if true, filter start from the beginning of the cell contents + filter_useParsedData : false // filter all data using parsed content + }, + format: function( table, c, wo ) { + if ( !c.$table.hasClass( 'hasFilters' ) ) { + tsf.init( table, c, wo ); + } + }, + remove: function( table, c, wo, refreshing ) { + var tbodyIndex, $tbody, + $table = c.$table, + $tbodies = c.$tbodies, + events = ( + 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + + 'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit ' + ).split( ' ' ).join( c.namespace + 'filter ' ); + $table + .removeClass( 'hasFilters' ) + // add filter namespace to all BUT search + .unbind( events.replace( ts.regex.spaces, ' ' ) ) + // remove the filter row even if refreshing, because the column might have been moved + .find( '.' + tscss.filterRow ).remove(); + wo.filter_initialized = false; + if ( refreshing ) { return; } + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody + $tbody.children().removeClass( wo.filter_filteredRow ).show(); + ts.processTbody( table, $tbody, false ); // restore tbody + } + if ( wo.filter_reset ) { + $( document ).undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ); + } } - } -}); - -/* Widget: filter - widgetOptions: - filter_childRows : false // if true, filter includes child row content in the search - filter_columnFilters : true // if true, a filter will be added to the top of each table column - filter_cssFilter : 'tablesorter-filter' // css class name added to the filter row & each input in the row - filter_functions : null // add custom filter functions using this option - filter_hideFilters : false // collapse filter row when mouse leaves the area - filter_ignoreCase : true // if true, make all searches case-insensitive - filter_reset : null // jQuery selector string of an element used to reset the filters - filter_searchDelay : 300 // typing delay in milliseconds before starting a search - filter_startsWith : false // if true, filter start from the beginning of the cell contents - filter_useParsedData : false // filter all data using parsed content - filter_serversideFiltering : false // if true, server-side filtering should be performed because client-side filtering will be disabled, but the ui and events will still be used. - **************************/ -$.tablesorter.addWidget({ - id: "filter", - format: function(table){ - if (table.config.parsers && !$(table).hasClass('hasFilters')){ - var i, j, k, l, val, ff, x, xi, st, sel, str, - ft, ft2, $th, rg, s, t, dis, col, - last = '', // save last filter search - ts = $.tablesorter, - c = table.config, - $ths = $(c.headerList), - wo = c.widgetOptions, - css = wo.filter_cssFilter || 'tablesorter-filter', - $t = $(table).addClass('hasFilters'), - b = c.$tbodies, - cols = c.parsers.length, - reg = [ // regex used in filter "check" functions - /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // 0 = regex to test for regex - new RegExp(c.cssChildRow), // 1 = child row - /undefined|number/, // 2 = check type - /(^[\"|\'|=])|([\"|\'|=]$)/, // 3 = exact match - /[\"\'=]/g, // 4 = replace exact match flags - /[^\w,. \-()]/g, // 5 = replace non-digits (from digit & currency parser) - /[<>=]/g // 6 = replace operators - ], - parsed = $ths.map(function(i){ - return (ts.getData) ? ts.getData($ths.filter('[data-column="' + i + '"]:last'), c.headers[i], 'filter') === 'parsed' : $(this).hasClass('filter-parsed'); - }).get(), - time, timer, - - // dig fer gold - checkFilters = function(filter){ - var arry = $.isArray(filter), - $inpts = $t.find('thead').eq(0).children('tr').find('select.' + css + ', input.' + css), - v = (arry) ? filter : $inpts.map(function(){ - return $(this).val() || ''; - }).get(), - cv = (v || []).join(''); // combined filter values - // add filter array back into inputs - if (arry) { - $inpts.each(function(i,el){ - $(el).val(filter[i] || ''); - }); - } - if (wo.filter_hideFilters === true){ - // show/hide filter row as needed - $t.find('.tablesorter-filter-row').trigger( cv === '' ? 'mouseleave' : 'mouseenter' ); - } - // return if the last search is the same; but filter === false when updating the search - // see example-widget-filter.html filter toggle buttons - if (last === cv && filter !== false) { return; } - $t.trigger('filterStart', [v]); - if (c.showProcessing) { - // give it time for the processing icon to kick in - setTimeout(function(){ - findRows(filter, v, cv); - return false; - }, 30); - } else { - findRows(filter, v, cv); - return false; + }); + + tsf = ts.filter = { + + // regex used in filter 'check' functions - not for general use and not documented + regex: { + regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // regex to test for regex + child : /tablesorter-childRow/, // child row class name; this gets updated in the script + filtered : /filtered/, // filtered (hidden) row class name; updated in the script + type : /undefined|number/, // check type + exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==') + operators : /[<>=]/g, // replace operators + query : '(q|query)', // replace filter queries + wild01 : /\?/g, // wild card match 0 or 1 + wild0More : /\*/g, // wild care match 0 or more + quote : /\"/g, + isNeg1 : /(>=?\s*-\d)/, + isNeg2 : /(<=?\s*\d)/ + }, + // function( c, data ) { } + // c = table.config + // data.$row = jQuery object of the row currently being processed + // data.$cells = jQuery object of all cells within the current row + // data.filters = array of filters for all columns ( some may be undefined ) + // data.filter = filter for the current column + // data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true ) + // data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string ) + // data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string ) + // data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true ) + // data.cacheArray = An array of parsed content from each table cell in the row being processed + // data.index = column index; table = table element ( DOM ) + // data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class ) + types: { + or : function( c, data, vars ) { + // look for "|", but not if it is inside of a regular expression + if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) && + // this test for regex has potential to slow down the overall search + !tsfRegex.regex.test( data.filter ) ) { + var indx, filterMatched, query, regex, + // duplicate data but split filter + data2 = $.extend( {}, data ), + filter = data.filter.split( tsfRegex.orSplit ), + iFilter = data.iFilter.split( tsfRegex.orSplit ), + len = filter.length; + for ( indx = 0; indx < len; indx++ ) { + data2.nestedFilters = true; + data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); + data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); + query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')'; + try { + // use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search, + // e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group + regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); + // filterMatched = data2.filter === '' && indx > 0 ? true + // look for an exact match with the 'or' unless the 'filter-match' class is found + filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ); + if ( filterMatched ) { + return filterMatched; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; } + return null; }, - findRows = function(filter, v, cv){ - var $tb, $tr, $td, cr, r, l, ff, time, arry; - if (c.debug) { time = new Date(); } - - for (k = 0; k < b.length; k++ ){ - $tb = $.tablesorter.processTbody(table, b.eq(k), true); - $tr = $tb.children('tr'); - l = $tr.length; - if (cv === '' || wo.filter_serversideFiltering){ - $tr.show().removeClass('filtered'); - } else { - // loop through the rows - for (j = 0; j < l; j++){ - // skip child rows - if (reg[1].test($tr[j].className)) { continue; } - r = true; - cr = $tr.eq(j).nextUntil('tr:not(.' + c.cssChildRow + ')'); - // so, if "table.config.widgetOptions.filter_childRows" is true and there is - // a match anywhere in the child row, then it will make the row visible - // checked here so the option can be changed dynamically - t = (cr.length && (wo && wo.hasOwnProperty('filter_childRows') && - typeof wo.filter_childRows !== 'undefined' ? wo.filter_childRows : true)) ? cr.text() : ''; - t = wo.filter_ignoreCase ? t.toLocaleLowerCase() : t; - $td = $tr.eq(j).children('td'); - for (i = 0; i < cols; i++){ - // ignore if filter is empty or disabled - if (v[i]){ - // check if column data should be from the cell or from parsed data - if (wo.filter_useParsedData || parsed[i]){ - x = c.cache[k].normalized[j][i]; - } else { - // using older or original tablesorter - x = $.trim($td.eq(i).text()); - } - xi = !reg[2].test(typeof x) && wo.filter_ignoreCase ? x.toLocaleLowerCase() : x; - ff = r; // if r is true, show that row - // val = case insensitive, v[i] = case sensitive - val = wo.filter_ignoreCase ? v[i].toLocaleLowerCase() : v[i]; - if (wo.filter_functions && wo.filter_functions[i]){ - if (wo.filter_functions[i] === true){ - // default selector; no "filter-select" class - ff = ($ths.filter('[data-column="' + i + '"]:last').hasClass('filter-match')) ? xi.search(val) >= 0 : v[i] === x; - } else if (typeof wo.filter_functions[i] === 'function'){ - // filter callback( exact cell content, parser normalized content, filter input value, column index ) - ff = wo.filter_functions[i](x, c.cache[k].normalized[j][i], v[i], i); - } else if (typeof wo.filter_functions[i][v[i]] === 'function'){ - // selector option function - ff = wo.filter_functions[i][v[i]](x, c.cache[k].normalized[j][i], v[i], i); - } - // Look for regex - } else if (reg[0].test(val)){ - rg = reg[0].exec(val); - try { - ff = new RegExp(rg[1], rg[2]).test(xi); - } catch (err){ - ff = false; - } - // Look for quotes or equals to get an exact match - } else if (reg[3].test(val) && xi === val.replace(reg[4], '')){ - ff = true; - // Look for a not match - } else if (/^\!/.test(val)){ - val = val.replace('!',''); - s = xi.search($.trim(val)); - ff = val === '' ? true : !(wo.filter_startsWith ? s === 0 : s >= 0); - // Look for operators >, >=, < or <= - } else if (/^[<>]=?/.test(val)){ - // xi may be numeric - see issue #149 - rg = isNaN(xi) ? $.tablesorter.formatFloat(xi.replace(reg[5], ''), table) : $.tablesorter.formatFloat(xi, table); - s = $.tablesorter.formatFloat(val.replace(reg[5], '').replace(reg[6],''), table); - if (/>/.test(val)) { ff = />=/.test(val) ? rg >= s : rg > s; } - if (/= 0) || (wo.filter_startsWith && x === 0) ); - } - r = (ff) ? (r ? true : false) : false; - } + // Look for an AND or && operator ( logical and ) + and : function( c, data, vars ) { + if ( tsfRegex.andTest.test( data.filter ) ) { + var indx, filterMatched, result, query, regex, + // duplicate data but split filter + data2 = $.extend( {}, data ), + filter = data.filter.split( tsfRegex.andSplit ), + iFilter = data.iFilter.split( tsfRegex.andSplit ), + len = filter.length; + for ( indx = 0; indx < len; indx++ ) { + data2.nestedFilters = true; + data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); + data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); + query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' ) + // replace wild cards since /(a*)/i will match anything + .replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ); + try { + // use try/catch just in case RegExp is invalid + regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); + // look for an exact match with the 'and' unless the 'filter-match' class is found + result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) ); + if ( indx === 0 ) { + filterMatched = result; + } else { + filterMatched = filterMatched && result; } - $tr[j].style.display = (r ? '' : 'none'); - $tr.eq(j)[r ? 'removeClass' : 'addClass']('filtered'); - if (cr.length) { cr[r ? 'show' : 'hide'](); } + } catch ( error ) { + return null; } } - $.tablesorter.processTbody(table, $tb, false); - } - - last = cv; // save last search - if (c.debug){ - ts.benchmark("Completed filter widget search", time); + // may be null from processing types + return filterMatched || false; } - $t.trigger('applyWidgets'); // make sure zebra widget is applied - $t.trigger('filterEnd'); + return null; }, - buildSelect = function(i, updating){ - var o, arry = []; - i = parseInt(i, 10); - o = ''; - for (k = 0; k < b.length; k++ ){ - l = c.cache[k].row.length; - // loop through the rows - for (j = 0; j < l; j++){ - // get non-normalized cell content - if (wo.filter_useParsedData){ - arry.push( '' + c.cache[k].normalized[j][i] ); - } else { - t = c.cache[k].row[j][0].cells[i]; - if (t){ - arry.push( $.trim(c.supportsTextContent ? t.textContent : $(t).text()) ); - } + // Look for regex + regex: function( c, data ) { + if ( tsfRegex.regex.test( data.filter ) ) { + var matches, + // cache regex per column for optimal speed + regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ), + isRegex = regex instanceof RegExp; + try { + if ( !isRegex ) { + // force case insensitive search if ignoreCase option set? + // if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; } + data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] ); } + matches = regex.test( data.exact ); + } catch ( error ) { + matches = false; } + return matches; } - - // get unique elements and sort the list - // if $.tablesorter.sortText exists (not in the original tablesorter), - // then natural sort the list otherwise use a basic sort - arry = $.grep(arry, function(v, k){ - return $.inArray(v ,arry) === k; - }); - arry = (ts.sortText) ? arry.sort(function(a,b){ return ts.sortText(table, a, b, i); }) : arry.sort(true); - - // build option list - for (k = 0; k < arry.length; k++){ - o += ''; + return null; + }, + // Look for operators >, >=, < or <= + operators: function( c, data ) { + // ignore empty strings... because '' < 10 is true + if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) { + var cachedValue, result, txt, + table = c.table, + parsed = data.parsed[ data.index ], + query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ), + parser = c.parsers[ data.index ] || {}, + savedSearch = query; + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || parser.type === 'numeric' ) { + txt = $.trim( '' + data.iFilter.replace( tsfRegex.operators, '' ) ); + result = tsf.parseFilter( c, txt, data, true ); + query = ( typeof result === 'number' && result !== '' && !isNaN( result ) ) ? result : query; + } + // iExact may be numeric - see issue #149; + // check if cached is defined, because sometimes j goes out of range? ( numeric columns ) + if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) && + typeof data.cache !== 'undefined' ) { + cachedValue = data.cache; + } else { + txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + cachedValue = ts.formatFloat( txt, table ); + } + if ( tsfRegex.gtTest.test( data.iFilter ) ) { + result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query; + } else if ( tsfRegex.ltTest.test( data.iFilter ) ) { + result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query; + } + // keep showing all rows if nothing follows the operator + if ( !result && savedSearch === '' ) { + result = true; + } + return result; } - $t.find('thead').find('select.' + css + '[data-column="' + i + '"]')[ updating ? 'html' : 'append' ](o); + return null; }, - buildDefault = function(updating){ - // build default select dropdown - for (i = 0; i < cols; i++){ - t = $ths.filter('[data-column="' + i + '"]:last'); - // look for the filter-select class; build/update it if found - if ((t.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[i] === true) && !t.hasClass('filter-false')){ - if (!wo.filter_functions) { wo.filter_functions = {}; } - wo.filter_functions[i] = true; // make sure this select gets processed by filter_functions - buildSelect(i, updating); + // Look for a not match + notMatch: function( c, data ) { + if ( tsfRegex.notTest.test( data.iFilter ) ) { + var indx, + txt = data.iFilter.replace( '!', '' ), + filter = tsf.parseFilter( c, txt, data ) || ''; + if ( tsfRegex.exact.test( filter ) ) { + // look for exact not matches - see #628 + filter = filter.replace( tsfRegex.exact, '' ); + return filter === '' ? true : $.trim( filter ) !== data.iExact; + } else { + indx = data.iExact.search( $.trim( filter ) ); + return filter === '' ? true : + // return true if not found + data.anyMatch ? indx < 0 : + // return false if found + !( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0 ); } } - }; + return null; + }, + // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric + exact: function( c, data ) { + /*jshint eqeqeq:false */ + if ( tsfRegex.exact.test( data.iFilter ) ) { + var txt = data.iFilter.replace( tsfRegex.exact, '' ), + filter = tsf.parseFilter( c, txt, data ) || ''; + // eslint-disable-next-line eqeqeq + return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact; + } + return null; + }, + // Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu! + range : function( c, data ) { + if ( tsfRegex.toTest.test( data.iFilter ) ) { + var result, tmp, range1, range2, + table = c.table, + index = data.index, + parsed = data.parsed[index], + // make sure the dash is for a range and not indicating a negative number + query = data.iFilter.split( tsfRegex.toSplit ); - if (c.debug){ - time = new Date(); - } - wo.filter_ignoreCase = wo.filter_ignoreCase !== false; // set default filter_ignoreCase to true - wo.filter_useParsedData = wo.filter_useParsedData === true; // default is false - // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156 - if (wo.filter_columnFilters !== false && $ths.filter('.filter-false').length !== $ths.length){ - t = ''; // build filter row - for (i = 0; i < cols; i++){ - dis = false; - $th = $ths.filter('[data-column="' + i + '"]:last'); // assuming last cell of a column is the main column - sel = (wo.filter_functions && wo.filter_functions[i] && typeof wo.filter_functions[i] !== 'function') || $th.hasClass('filter-select'); - t += ''; - if (sel){ - t += '' : '>') + ''; + return patternIndx === pattern.length; } - $t.find('thead').eq(0).append(t += ''); + return null; + } + }, + init: function( table ) { + // filter language options + ts.language = $.extend( true, {}, { + to : 'to', + or : 'or', + and : 'and' + }, ts.language ); + + var options, string, txt, $header, column, val, fxn, noSelect, + c = table.config, + wo = c.widgetOptions, + processStr = function(prefix, str, suffix) { + str = str.trim(); + // don't include prefix/suffix if str is empty + return str === '' ? '' : (prefix || '') + str + (suffix || ''); + }; + c.$table.addClass( 'hasFilters' ); + c.lastSearch = []; + + // define timers so using clearTimeout won't cause an undefined error + wo.filter_searchTimer = null; + wo.filter_initTimer = null; + wo.filter_formatterCount = 0; + wo.filter_formatterInit = []; + wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]'; + wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]'; + + val = '\\{' + tsfRegex.query + '\\}'; + $.extend( tsfRegex, { + child : new RegExp( c.cssChildRow ), + filtered : new RegExp( wo.filter_filteredRow ), + alreadyFiltered : new RegExp( '(\\s+(-' + processStr('|', ts.language.or) + processStr('|', ts.language.to) + ')\\s+)', 'i' ), + toTest : new RegExp( '\\s+(-' + processStr('|', ts.language.to) + ')\\s+', 'i' ), + toSplit : new RegExp( '(?:\\s+(?:-' + processStr('|', ts.language.to) + ')\\s+)', 'gi' ), + andTest : new RegExp( '\\s+(' + processStr('', ts.language.and, '|') + '&&)\\s+', 'i' ), + andSplit : new RegExp( '(?:\\s+(?:' + processStr('', ts.language.and, '|') + '&&)\\s+)', 'gi' ), + orTest : new RegExp( '(\\|' + processStr('|\\s+', ts.language.or, '\\s+') + ')', 'i' ), + orSplit : new RegExp( '(?:\\|' + processStr('|\\s+(?:', ts.language.or, ')\\s+') + ')', 'gi' ), + iQuery : new RegExp( val, 'i' ), + igQuery : new RegExp( val, 'ig' ), + operTest : /^[<>]=?/, + gtTest : />/, + gteTest : />=/, + ltTest : /= 37 && e.which <=40)) { return; } - // skip delay - if (typeof filter !== 'undefined'){ - checkFilters(filter); - return false; + // Add filterAndSortReset - see #1361 + if ( event.type === 'filterReset' || event.type === 'filterAndSortReset' ) { + c.$table.find( '.' + tscss.filter ).add( wo.filter_$externalFilters ).val( '' ); + if ( event.type === 'filterAndSortReset' ) { + ts.sortReset( this.config, function() { + tsf.searching( table, [] ); + }); + } else { + tsf.searching( table, [] ); + } + } else if ( event.type === 'filterResetSaved' ) { + ts.storage( table, 'tablesorter-filters', '' ); + } else if ( event.type === 'filterEnd' ) { + tsf.buildDefault( table, true ); + } else { + // send false argument to force a new search; otherwise if the filter hasn't changed, + // it will return + filter = event.type === 'search' ? filter : + event.type === 'updateComplete' ? c.$table.data( 'lastSearch' ) : ''; + if ( /(update|add)/.test( event.type ) && event.type !== 'updateComplete' ) { + // force a new search since content has changed + c.lastCombinedFilter = null; + c.lastSearch = []; + // update filterFormatters after update (& small delay) - Fixes #1237 + setTimeout(function() { + c.$table.triggerHandler( 'filterFomatterUpdate' ); + }, 100); + } + // pass true ( skipFirst ) to prevent the tablesorter.setFilters function from skipping the first + // input ensures all inputs are updated when a search is triggered on the table + // $( 'table' ).trigger( 'search', [...] ); + tsf.searching( table, filter, true ); } - // delay filtering - clearTimeout(timer); - timer = setTimeout(function(){ - checkFilters(); - }, wo.filter_searchDelay || 300); + return false; }); // reset button/link - if (wo.filter_reset && $(wo.filter_reset).length){ - $(wo.filter_reset).bind('click', function(){ - $t.find('.' + css).val(''); - checkFilters(); - return false; - }); + if ( wo.filter_reset ) { + if ( wo.filter_reset instanceof $ ) { + // reset contains a jQuery object, bind to it + wo.filter_reset.click( function() { + c.$table.triggerHandler( 'filterReset' ); + }); + } else if ( $( wo.filter_reset ).length ) { + // reset is a jQuery selector, use event delegation + $( document ) + .undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ) + .delegate( wo.filter_reset, 'click' + c.namespace + 'filter', function() { + // trigger a reset event, so other functions ( filter_formatter ) know when to reset + c.$table.triggerHandler( 'filterReset' ); + }); + } } - if (wo.filter_functions){ - // i = column # (string) - for (col in wo.filter_functions){ - if (wo.filter_functions.hasOwnProperty(col) && typeof col === 'string'){ - t = $ths.filter('[data-column="' + col + '"]:last'); - ff = ''; - if (wo.filter_functions[col] === true && !t.hasClass('filter-false')){ - buildSelect(col); - } else if (typeof col === 'string' && !t.hasClass('filter-false')){ + if ( wo.filter_functions ) { + for ( column = 0; column < c.columns; column++ ) { + fxn = ts.getColumnData( table, wo.filter_functions, column ); + if ( fxn ) { + // remove 'filter-select' from header otherwise the options added here are replaced with + // all options + $header = c.$headerIndexed[ column ].removeClass( 'filter-select' ); + // don't build select if 'filter-false' or 'parser-false' set + noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); + options = ''; + if ( fxn === true && noSelect ) { + tsf.buildSelect( table, column ); + } else if ( typeof fxn === 'object' && noSelect ) { // add custom drop down list - for (str in wo.filter_functions[col]){ - if (typeof str === 'string'){ - ff += ff === '' ? '' : ''; - ff += ''; + for ( string in fxn ) { + if ( typeof string === 'string' ) { + options += options === '' ? + '' : ''; + val = string; + txt = string; + if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + val = string.split( wo.filter_selectSourceSeparator ); + txt = val[1]; + val = val[0]; + } + options += ''; } } - $t.find('thead').find('select.' + css + '[data-column="' + col + '"]').append(ff); + c.$table + .find( 'thead' ) + .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) + .append( options ); + txt = wo.filter_selectSource; + fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column ); + if ( fxn ) { + // updating so the extra options are appended + tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) ); + } } } } } - // not really updating, but if the column has both the "filter-select" class & filter_functions set to true, - // it would append the same options twice. - buildDefault(true); + // not really updating, but if the column has both the 'filter-select' class & + // filter_functions set to true, it would append the same options twice. + tsf.buildDefault( table, true ); - $t.find('select.' + css).bind('change search', function(){ - checkFilters(); - }); + tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); + if ( wo.filter_external ) { + tsf.bindSearch( table, wo.filter_external ); + } - if (wo.filter_hideFilters === true){ - $t - .find('.tablesorter-filter-row') - .addClass('hideme') - .bind('mouseenter mouseleave', function(e){ - // save event object - http://bugs.jquery.com/ticket/12140 - var all, evt = e; - ft = $(this); - clearTimeout(st); - st = setTimeout(function(){ - if (/enter|over/.test(evt.type)){ - ft.removeClass('hideme'); - } else { - // don't hide if input has focus - // $(':focus') needs jQuery 1.6+ - if ($(document.activeElement).closest('tr')[0] !== ft[0]){ - // get all filter values - all = $t.find('.' + (wo.filter_cssFilter || 'tablesorter-filter')).map(function(){ - return $(this).val() || ''; - }).get().join(''); - // don't hide row if any filter has a value - if (all === ''){ - ft.addClass('hideme'); - } - } - } - }, 200); - }) - .find('input, select').bind('focus blur', function(e){ - ft2 = $(this).closest('tr'); - clearTimeout(st); - st = setTimeout(function(){ - // don't hide row if any filter has a value - if ($t.find('.' + (wo.filter_cssFilter || 'tablesorter-filter')).map(function(){ return $(this).val() || ''; }).get().join('') === ''){ - ft2[ e.type === 'focus' ? 'removeClass' : 'addClass']('hideme'); - } - }, 200); - }); + if ( wo.filter_hideFilters ) { + tsf.hideFilters( c ); } // show processing icon - if (c.showProcessing) { - $t.bind('filterStart filterEnd', function(e, v) { - var fc = (v) ? $t.find('.' + c.cssHeader).filter('[data-column]').filter(function(){ - return v[$(this).data('column')] !== ''; - }) : ''; - ts.isProcessing($t[0], e.type === 'filterStart', v ? fc : ''); + if ( c.showProcessing ) { + txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter-sp ' ); + c.$table + .unbind( txt.replace( ts.regex.spaces, ' ' ) ) + .bind( txt, function( event, columns ) { + // only add processing to certain columns to all columns + $header = ( columns ) ? + c.$table + .find( '.' + tscss.header ) + .filter( '[data-column]' ) + .filter( function() { + return columns[ $( this ).data( 'column' ) ] !== ''; + }) : ''; + ts.isProcessing( table, event.type === 'filterStart', columns ? $header : '' ); }); } - if (c.debug){ - ts.benchmark("Applying Filter widget", time); + // set filtered rows count ( intially unfiltered ) + c.filteredRows = c.totalRows; + + // add default values + txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table + .unbind( txt.replace( ts.regex.spaces, ' ' ) ) + .bind( txt, function() { + tsf.completeInit( this ); + }); + // if filter widget is added after pager has initialized; then set filter init flag + if ( c.pager && c.pager.initialized && !wo.filter_initialized ) { + c.$table.triggerHandler( 'filterFomatterUpdate' ); + setTimeout( function() { + tsf.filterInitComplete( c ); + }, 100 ); + } else if ( !wo.filter_initialized ) { + tsf.completeInit( table ); + } + }, + completeInit: function( table ) { + // redefine 'c' & 'wo' so they update properly inside this callback + var c = table.config, + wo = c.widgetOptions, + filters = tsf.setDefaults( table, c, wo ) || []; + if ( filters.length ) { + // prevent delayInit from triggering a cache build if filters are empty + if ( !( c.delayInit && filters.join( '' ) === '' ) ) { + ts.setFilters( table, filters, true ); + } + } + c.$table.triggerHandler( 'filterFomatterUpdate' ); + // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers + setTimeout( function() { + if ( !wo.filter_initialized ) { + tsf.filterInitComplete( c ); + } + }, 100 ); + }, + + // $cell parameter, but not the config, is passed to the filter_formatters, + // so we have to work with it instead + formatterUpdated: function( $cell, column ) { + // prevent error if $cell is undefined - see #1056 + var $table = $cell && $cell.closest( 'table' ); + var config = $table.length && $table[0].config, + wo = config && config.widgetOptions; + if ( wo && !wo.filter_initialized ) { + // add updates by column since this function + // may be called numerous times before initialization + wo.filter_formatterInit[ column ] = 1; } - // filter widget initialized - $t.trigger('filterInit'); - } - }, - remove: function(table, c, wo){ - var k, $tb, - $t = $(table), - b = c.$tbodies; - $t - .removeClass('hasFilters') - // add .tsfilter namespace to all BUT search - .unbind('addRows updateCell update appendCache search'.split(' ').join('.tsfilter')) - .find('.tablesorter-filter-row').remove(); - for (k = 0; k < b.length; k++ ){ - $tb = $.tablesorter.processTbody(table, b.eq(k), true); // remove tbody - $tb.children().removeClass('filtered').show(); - $.tablesorter.processTbody(table, $tb, false); // restore tbody - } - if (wo.filterreset) { $(wo.filter_reset).unbind('click'); } - } -}); - -// Widget: Sticky headers -// based on this awesome article: -// http://css-tricks.com/13465-persistent-headers/ -// and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech -// ************************** -$.tablesorter.addWidget({ - id: "stickyHeaders", - format: function(table){ - if ($(table).hasClass('hasStickyHeaders')) { return; } - var $table = $(table).addClass('hasStickyHeaders'), - c = table.config, - wo = c.widgetOptions, - win = $(window), - header = $(table).children('thead:first'), //.add( $(table).find('caption') ), - hdrCells = header.children('tr:not(.sticky-false)').children(), - css = wo.stickyHeaders || 'tablesorter-stickyHeader', - innr = '.tablesorter-header-inner', - firstRow = hdrCells.eq(0).parent(), - tfoot = $table.find('tfoot'), - t2 = wo.$sticky = $table.clone(), // clone table, but don't remove id... the table might be styled by css - // clone the entire thead - seems to work in IE8+ - stkyHdr = t2.children('thead:first') - .addClass(css) - .css({ - width : header.outerWidth(true), - position : 'fixed', - margin : 0, - top : 0, - visibility : 'hidden', - zIndex : 1 - }), - stkyCells = stkyHdr.children('tr:not(.sticky-false)').children(), // issue #172 - laststate = '', - spacing = 0, - resizeHdr = function(){ - var bwsr = navigator.userAgent; - spacing = 0; - // yes, I dislike browser sniffing, but it really is needed here :( - // webkit automatically compensates for border spacing - if ($table.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(bwsr)) { - // Firefox & Opera use the border-spacing - // update border-spacing here because of demos that switch themes - spacing = parseInt(hdrCells.eq(0).css('border-left-width'), 10) * 2; - } - stkyHdr.css({ - left : header.offset().left - win.scrollLeft() - spacing, - width: header.outerWidth() - }); - stkyCells - .each(function(i){ - var $h = hdrCells.eq(i); - $(this).css({ - width: $h.width() - spacing, - height: $h.height() - }); - }) - .find(innr).each(function(i){ - var hi = hdrCells.eq(i).find(innr), - w = hi.width(); // - ( parseInt(hi.css('padding-left'), 10) + parseInt(hi.css('padding-right'), 10) ); - $(this).width(w); - }); - }; - // clear out cloned table, except for sticky header - t2.find('thead:gt(0),tr.sticky-false,tbody,tfoot,caption').remove(); - t2.css({ height:0, width:0, padding:0, margin:0, border:0 }); - // remove rows you don't want to be sticky - stkyHdr.find('tr.sticky-false').remove(); - // remove resizable block - stkyCells.find('.tablesorter-resizer').remove(); - // update sticky header class names to match real header after sorting - $table - .bind('sortEnd.tsSticky', function(){ - hdrCells.each(function(i){ - var t = stkyCells.eq(i); - t.attr('class', $(this).attr('class')); - if (c.cssIcon){ - t - .find('.' + c.cssIcon) - .attr('class', $(this).find('.' + c.cssIcon).attr('class')); + }, + filterInitComplete: function( c ) { + var indx, len, + wo = c.widgetOptions, + count = 0, + completed = function() { + wo.filter_initialized = true; + // update lastSearch - it gets cleared often + c.lastSearch = c.$table.data( 'lastSearch' ); + c.$table.triggerHandler( 'filterInit', c ); + tsf.findRows( c.table, c.lastSearch || [] ); + if (ts.debug(c, 'filter')) { + console.log('Filter >> Widget initialized'); + } + }; + if ( $.isEmptyObject( wo.filter_formatter ) ) { + completed(); + } else { + len = wo.filter_formatterInit.length; + for ( indx = 0; indx < len; indx++ ) { + if ( wo.filter_formatterInit[ indx ] === 1 ) { + count++; + } + } + clearTimeout( wo.filter_initTimer ); + if ( !wo.filter_initialized && count === wo.filter_formatterCount ) { + // filter widget initialized + completed(); + } else if ( !wo.filter_initialized ) { + // fall back in case a filter_formatter doesn't call + // $.tablesorter.filter.formatterUpdated( $cell, column ), and the count is off + wo.filter_initTimer = setTimeout( function() { + completed(); + }, 500 ); + } + } + }, + // encode or decode filters for storage; see #1026 + processFilters: function( filters, encode ) { + var indx, + // fixes #1237; previously returning an encoded "filters" value + result = [], + mode = encode ? encodeURIComponent : decodeURIComponent, + len = filters.length; + for ( indx = 0; indx < len; indx++ ) { + if ( filters[ indx ] ) { + result[ indx ] = mode( filters[ indx ] ); + } + } + return result; + }, + setDefaults: function( table, c, wo ) { + var isArray, saved, indx, col, $filters, + // get current ( default ) filters + filters = ts.getFilters( table ) || []; + if ( wo.filter_saveFilters && ts.storage ) { + saved = ts.storage( table, 'tablesorter-filters' ) || []; + isArray = $.isArray( saved ); + // make sure we're not just getting an empty array + if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) { + filters = tsf.processFilters( saved ); + } + } + // if no filters saved, then check default settings + if ( filters.join( '' ) === '' ) { + // allow adding default setting to external filters + $filters = c.$headers.add( wo.filter_$externalFilters ) + .filter( '[' + wo.filter_defaultAttrib + ']' ); + for ( indx = 0; indx <= c.columns; indx++ ) { + // include data-column='all' external filters + col = indx === c.columns ? 'all' : indx; + filters[ indx ] = $filters + .filter( '[data-column="' + col + '"]' ) + .attr( wo.filter_defaultAttrib ) || filters[indx] || ''; + } + } + c.$table.data( 'lastSearch', filters ); + return filters; + }, + parseFilter: function( c, filter, data, parsed ) { + return parsed || data.parsed[ data.index ] ? + c.parsers[ data.index ].format( filter, c.table, [], data.index ) : + filter; + }, + buildRow: function( table, c, wo ) { + var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp, + // c.columns defined in computeThIndexes() + cellFilter = wo.filter_cellFilter, + columns = c.columns, + arry = $.isArray( cellFilter ), + buildFilter = ''; + for ( column = 0; column < columns; column++ ) { + if ( c.$headerIndexed[ column ].length ) { + // account for entire column set with colspan. See #1047 + tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0; + if ( tmp > 1 ) { + buildFilter += '' ).appendTo( $filter ); + } else { + ffxn = ts.getColumnData( table, wo.filter_formatter, column ); + if ( ffxn ) { + wo.filter_formatterCount++; + buildFilter = ffxn( $filter, column ); + // no element returned, so lets go find it + if ( buildFilter && buildFilter.length === 0 ) { + buildFilter = $filter.children( 'input' ); + } + // element not in DOM, so lets attach it + if ( buildFilter && ( buildFilter.parent().length === 0 || + ( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) { + $filter.append( buildFilter ); + } + } else { + buildFilter = $( '' ).appendTo( $filter ); + } + if ( buildFilter ) { + tmp = $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.search || ''; + buildFilter.attr( 'placeholder', tmp ); + } + } + if ( buildFilter ) { + // add filter class name + name = ( $.isArray( wo.filter_cssFilter ) ? + ( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) : + wo.filter_cssFilter ) || ''; + // copy data-column from table cell (it will include colspan) + buildFilter.addClass( tscss.filter + ' ' + name ); + name = wo.filter_filterLabel; + tmp = name.match(/{{([^}]+?)}}/g); + if (!tmp) { + tmp = [ '{{label}}' ]; + } + $.each(tmp, function(indx, attr) { + var regex = new RegExp(attr, 'g'), + data = $header.attr('data-' + attr.replace(/{{|}}/g, '')), + text = typeof data === 'undefined' ? $header.text() : data; + name = name.replace( regex, $.trim( text ) ); + }); + buildFilter.attr({ + 'data-column': $filter.attr( 'data-column' ), + 'aria-label': name + }); + if ( disabled ) { + buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true; + } + } + } + } + }, + bindSearch: function( table, $el, internal ) { + table = $( table )[0]; + $el = $( $el ); // allow passing a selector string + if ( !$el.length ) { return; } + var tmp, + c = table.config, + wo = c.widgetOptions, + namespace = c.namespace + 'filter', + $ext = wo.filter_$externalFilters; + if ( internal !== true ) { + // save anyMatch element + tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector; + wo.filter_$anyMatch = $el.filter( tmp ); + if ( $ext && $ext.length ) { + wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); + } else { + wo.filter_$externalFilters = $el; + } + // update values ( external filters added after table initialization ) + ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false ); + } + // unbind events + tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) ); + $el + // use data attribute instead of jQuery data since the head is cloned without including + // the data/binding + .attr( 'data-lastSearchTime', new Date().getTime() ) + .unbind( tmp.replace( ts.regex.spaces, ' ' ) ) + .bind( 'keydown' + namespace, function( event ) { + if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) { + // prevent keypress event + return false; } - }); - }) - .bind('pagerComplete.tsSticky', function(){ - resizeHdr(); - }); - // set sticky header cell width and link clicks to real header - hdrCells.find('*').andSelf().filter(c.selectorSort).each(function(i){ - var t = $(this); - stkyCells.eq(i) - // clicking on sticky will trigger sort - .bind('mouseup', function(e){ - t.trigger(e, true); // external mouseup flag (click timer is ignored) }) - // prevent sticky header text selection - .bind('mousedown', function(){ - this.onselectstart = function(){ return false; }; - return false; - }); - }); - // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. - $table.after( t2 ); - // make it sticky! - win - .bind('scroll.tsSticky', function(){ - var offset = firstRow.offset(), - sTop = win.scrollTop(), - tableHt = $table.height() - (stkyHdr.height() + (tfoot.height() || 0)), - vis = (sTop > offset.top) && (sTop < offset.top + tableHt) ? 'visible' : 'hidden'; - stkyHdr - .css({ - // adjust when scrolling horizontally - fixes issue #143 - left : header.offset().left - win.scrollLeft() - spacing, - visibility : vis + .bind( 'keyup' + namespace, function( event ) { + wo = table.config.widgetOptions; // make sure "wo" isn't cached + var column = parseInt( $( this ).attr( 'data-column' ), 10 ), + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch : + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( typeof liveSearch === 'undefined' ) { + liveSearch = wo.filter_liveSearch.fallback || false; + } + $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); + // emulate what webkit does.... escape clears the filter + if ( event.which === tskeyCodes.escape ) { + // make sure to restore the last value on escape + this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column]; + // don't return if the search value is empty ( all rows need to be revealed ) + } else if ( this.value !== '' && ( + // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace + ( typeof liveSearch === 'number' && this.value.length < liveSearch ) || + // let return & backspace continue on, but ignore arrows & non-valid characters + ( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace && + ( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) { + return; + // live search + } else if ( liveSearch === false ) { + if ( this.value !== '' && event.which !== tskeyCodes.enter ) { + return; + } + } + // change event = no delay; last true flag tells getFilters to skip newest timed input + tsf.searching( table, true, true, column ); + }) + // include change for select - fixes #473 + .bind( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) { + // don't get cached data, in case data-column changes dynamically + var column = parseInt( $( this ).attr( 'data-column' ), 10 ), + eventType = event.type, + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? + wo.filter_liveSearch : + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( table.config.widgetOptions.filter_initialized && + // immediate search if user presses enter + ( event.which === tskeyCodes.enter || + // immediate search if a "search" or "blur" is triggered on the input + ( eventType === 'search' || eventType === 'blur' ) || + // change & input events must be ignored if liveSearch !== true + ( eventType === 'change' || eventType === 'input' ) && + // prevent search if liveSearch is a number + ( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) && + // don't allow 'change' or 'input' event to process if the input value + // is the same - fixes #685 + this.value !== c.lastSearch[column] + ) + ) { + event.preventDefault(); + // init search with no delay + $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); + tsf.searching( table, eventType !== 'keypress' || event.which === tskeyCodes.enter, true, column ); + } }); - if (vis !== laststate){ - // make sure the column widths match - resizeHdr(); - laststate = vis; - } - }) - .bind('resize.tsSticky', function(){ - resizeHdr(); - }); - }, - remove: function(table, c, wo){ - var $t = $(table), - css = wo.stickyHeaders || 'tablesorter-stickyHeader'; - $t - .removeClass('hasStickyHeaders') - .unbind('sortEnd.tsSticky pagerComplete.tsSticky') - .find('.' + css).remove(); - if (wo.$sticky) { wo.$sticky.remove(); } // remove cloned thead - $(window).unbind('scroll.tsSticky resize.tsSticky'); - } -}); - -// Add Column resizing widget -// this widget saves the column widths if -// $.tablesorter.storage function is included -// ************************** -$.tablesorter.addWidget({ - id: "resizable", - format: function(table){ - if ($(table).hasClass('hasResizable')) { return; } - $(table).addClass('hasResizable'); - var $t, t, i, j, s, $c, $cols, w, tw, - $tbl = $(table), - c = table.config, - wo = c.widgetOptions, - position = 0, - $target = null, - $next = null, - fullWidth = Math.abs($tbl.parent().width() - $tbl.width()) < 20, - stopResize = function(){ - if ($.tablesorter.storage && $target){ - s[$target.index()] = $target.width(); - s[$next.index()] = $next.width(); - $target.width( s[$target.index()] ); - $next.width( s[$next.index()] ); - if (wo.resizable !== false){ - $.tablesorter.storage(table, 'tablesorter-resizable', s); - } - } - position = 0; - $target = $next = null; - $(window).trigger('resize'); // will update stickyHeaders, just in case - }; - s = ($.tablesorter.storage && wo.resizable !== false) ? $.tablesorter.storage(table, 'tablesorter-resizable') : {}; - // process only if table ID or url match - if (s){ - for (j in s){ - if (!isNaN(j) && j < c.headerList.length){ - $(c.headerList[j]).width(s[j]); // set saved resizable widths + }, + searching: function( table, filter, skipFirst, column ) { + var liveSearch, + wo = table.config.widgetOptions; + if (typeof column === 'undefined') { + // no delay + liveSearch = false; + } else { + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? + wo.filter_liveSearch : + // get column setting, or set to fallback value, or default to false + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( typeof liveSearch === 'undefined' ) { + liveSearch = wo.filter_liveSearch.fallback || false; } } - } - $t = $tbl.children('thead:first').children('tr'); - // add resizable-false class name to headers (across rows as needed) - $t.children().each(function(){ - t = $(this); - i = t.attr('data-column'); - j = $.tablesorter.getData( t, c.headers[i], 'resizable') === "false"; - $t.children().filter('[data-column="' + i + '"]').toggleClass('resizable-false', j); - }); - // add wrapper inside each cell to allow for positioning of the resizable target block - $t.each(function(){ - $c = $(this).children(':not(.resizable-false)'); - if (!$(this).find('.tablesorter-wrapper').length) { - // Firefox needs this inner div to position the resizer correctly - $c.wrapInner('
'); - } - $c = $c.slice(0,-1); // don't include the last column of the row - $cols = $cols ? $cols.add($c) : $c; - }); - $cols - .each(function(){ - $t = $(this); - j = parseInt($t.css('padding-right'), 10) + 10; // 8 is 1/2 of the 16px wide resizer grip - t = '
'; - $t - .find('.tablesorter-wrapper') - .append(t); - }) - .bind('mousemove.tsresize', function(e){ - // ignore mousemove if no mousedown - if (position === 0 || !$target) { return; } - // resize columns - w = e.pageX - position; - tw = $target.width(); - $target.width( tw + w ); - if ($target.width() !== tw && fullWidth){ - $next.width( $next.width() - w ); - } - position = e.pageX; - }) - .bind('mouseup.tsresize', function(){ - stopResize(); - }) - .find('.tablesorter-resizer,.tablesorter-resizer-grip') - .bind('mousedown', function(e){ - // save header cell and mouse position; closest() not supported by jQuery v1.2.6 - $target = $(e.target).closest('th'); - t = c.$headers.filter('[data-column="' + $target.attr('data-column') + '"]'); - if (t.length > 1) { $target = $target.add(t); } - // if table is not as wide as it's parent, then resize the table - $next = e.shiftKey ? $target.parent().find('th:not(.resizable-false)').filter(':last') : $target.nextAll(':not(.resizable-false)').eq(0); - position = e.pageX; - }); - $tbl.find('thead:first') - .bind('mouseup.tsresize mouseleave.tsresize', function(e){ - stopResize(); - }) - // right click to reset columns to default widths - .bind('contextmenu.tsresize', function(){ - $.tablesorter.resizableReset(table); - // $.isEmptyObject() needs jQuery 1.4+ - var rtn = $.isEmptyObject ? $.isEmptyObject(s) : s === {}; // allow right click if already reset - s = {}; - return rtn; - }); - }, - remove: function(table, c, wo){ - $(table) - .removeClass('hasResizable') - .find('thead') - .unbind('mouseup.tsresize mouseleave.tsresize contextmenu.tsresize') - .find('tr').children() - .unbind('mousemove.tsresize mouseup.tsresize') - // don't remove "tablesorter-wrapper" as uitheme uses it too - .find('.tablesorter-resizer,.tablesorter-resizer-grip').remove(); - $.tablesorter.resizableReset(table); - } -}); -$.tablesorter.resizableReset = function(table){ - $(table.config.headerList).filter(':not(.resizable-false)').css('width',''); - if ($.tablesorter.storage) { $.tablesorter.storage(table, 'tablesorter-resizable', {}); } -}; - -// Save table sort widget -// this widget saves the last sort only if the -// saveSort widget option is true AND the -// $.tablesorter.storage function is included -// ************************** -$.tablesorter.addWidget({ - id: 'saveSort', - init: function(table, thisWidget){ - // run widget format before all other widgets are applied to the table - thisWidget.format(table, true); - }, - format: function(table, init){ - var sl, time, c = table.config, - wo = c.widgetOptions, - ss = wo.saveSort !== false, // make saveSort active/inactive; default to true - sortList = { "sortList" : c.sortList }; - if (c.debug){ - time = new Date(); - } - if ($(table).hasClass('hasSaveSort')){ - if (ss && table.hasInitialized && $.tablesorter.storage){ - $.tablesorter.storage( table, 'tablesorter-savesort', sortList ); - if (c.debug){ - $.tablesorter.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time); + clearTimeout( wo.filter_searchTimer ); + if ( typeof filter === 'undefined' || filter === true ) { + // delay filtering + wo.filter_searchTimer = setTimeout( function() { + tsf.checkFilters( table, filter, skipFirst ); + }, liveSearch ? wo.filter_searchDelay : 10 ); + } else { + // skip delay + tsf.checkFilters( table, filter, skipFirst ); + } + }, + equalFilters: function (c, filter1, filter2) { + var indx, + f1 = [], + f2 = [], + len = c.columns + 1; // add one to include anyMatch filter + filter1 = $.isArray(filter1) ? filter1 : []; + filter2 = $.isArray(filter2) ? filter2 : []; + for (indx = 0; indx < len; indx++) { + f1[indx] = filter1[indx] || ''; + f2[indx] = filter2[indx] || ''; + } + return f1.join(',') === f2.join(','); + }, + checkFilters: function( table, filter, skipFirst ) { + var c = table.config, + wo = c.widgetOptions, + filterArray = $.isArray( filter ), + filters = ( filterArray ) ? filter : ts.getFilters( table, true ), + currentFilters = filters || []; // current filter values + // prevent errors if delay init is set + if ( $.isEmptyObject( c.cache ) ) { + // update cache if delayInit set & pager has initialized ( after user initiates a search ) + if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) { + ts.updateCache( c, function() { + tsf.checkFilters( table, false, skipFirst ); + }); } + return; } - } else { - // set table sort on initial run of the widget - $(table).addClass('hasSaveSort'); - sortList = ''; - // get data - if ($.tablesorter.storage){ - sl = $.tablesorter.storage( table, 'tablesorter-savesort' ); - sortList = (sl && sl.hasOwnProperty('sortList') && $.isArray(sl.sortList)) ? sl.sortList : ''; - if (c.debug){ - $.tablesorter.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time); - } - } - // init is true when widget init is run, this will run this widget before all other widgets have initialized - // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice. - if (init && sortList && sortList.length > 0){ - c.sortList = sortList; - } else if (table.hasInitialized && sortList && sortList.length > 0){ - // update sort change - $(table).trigger('sorton', [sortList]); + // add filter array back into inputs + if ( filterArray ) { + ts.setFilters( table, filters, false, skipFirst !== true ); + if ( !wo.filter_initialized ) { + c.lastSearch = []; + c.lastCombinedFilter = ''; + } } - } - }, - remove: function(table, c, wo){ - // clear storage - if ($.tablesorter.storage) { $.tablesorter.storage( table, 'tablesorter-savesort', '' ); } - } -}); + if ( wo.filter_hideFilters ) { + // show/hide filter row as needed + c.$table + .find( '.' + tscss.filterRow ) + .triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' ); + } + // return if the last search is the same; but filter === false when updating the search + // see example-widget-filter.html filter toggle buttons + if ( tsf.equalFilters(c, c.lastSearch, currentFilters) ) { + if ( filter !== false ) { + return; + } else { + // force filter refresh + c.lastCombinedFilter = ''; + c.lastSearch = []; + } + } + // define filter inside it is false + filters = filters || []; + // convert filters to strings - see #1070 + filters = Array.prototype.map ? + filters.map( String ) : + // for IE8 & older browsers - maybe not the best method + filters.join( '\ufffd' ).split( '\ufffd' ); + + if ( wo.filter_initialized ) { + c.$table.triggerHandler( 'filterStart', [ filters ] ); + } + if ( c.showProcessing ) { + // give it time for the processing icon to kick in + setTimeout( function() { + tsf.findRows( table, filters, currentFilters ); + return false; + }, 30 ); + } else { + tsf.findRows( table, filters, currentFilters ); + return false; + } + }, + hideFiltersCheck: function( c ) { + if (typeof c.widgetOptions.filter_hideFilters === 'function') { + var val = c.widgetOptions.filter_hideFilters( c ); + if (typeof val === 'boolean') { + return val; + } + } + return ts.getFilters( c.$table ).join( '' ) === ''; + }, + hideFilters: function( c, $table ) { + var timer; + ( $table || c.$table ) + .find( '.' + tscss.filterRow ) + .addClass( tscss.filterRowHide ) + .bind( 'mouseenter mouseleave', function( e ) { + // save event object - http://bugs.jquery.com/ticket/12140 + var event = e, + $row = $( this ); + clearTimeout( timer ); + timer = setTimeout( function() { + if ( /enter|over/.test( event.type ) ) { + $row.removeClass( tscss.filterRowHide ); + } else { + // don't hide if input has focus + // $( ':focus' ) needs jQuery 1.6+ + if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) { + // don't hide row if any filter has a value + $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) ); + } + } + }, 200 ); + }) + .find( 'input, select' ).bind( 'focus blur', function( e ) { + var event = e, + $row = $( this ).closest( 'tr' ); + clearTimeout( timer ); + timer = setTimeout( function() { + clearTimeout( timer ); + // don't hide row if any filter has a value + $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' ); + }, 200 ); + }); + }, + defaultFilter: function( filter, mask ) { + if ( filter === '' ) { return filter; } + var regex = tsfRegex.iQuery, + maskLen = mask.match( tsfRegex.igQuery ).length, + query = maskLen > 1 ? $.trim( filter ).split( /\s/ ) : [ $.trim( filter ) ], + len = query.length - 1, + indx = 0, + val = mask; + if ( len < 1 && maskLen > 1 ) { + // only one 'word' in query but mask has >1 slots + query[1] = query[0]; + } + // replace all {query} with query words... + // if query = 'Bob', then convert mask from '!{query}' to '!Bob' + // if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank' + while ( regex.test( val ) ) { + val = val.replace( regex, query[indx++] || '' ); + if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) { + val = mask.replace( regex, val ); + } + } + return val; + }, + getLatestSearch: function( $input ) { + if ( $input ) { + return $input.sort( function( a, b ) { + return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' ); + }); + } + return $input || $(); + }, + findRange: function( c, val, ignoreRanges ) { + // look for multiple columns '1-3,4-6,8' in data-column + var temp, ranges, range, start, end, singles, i, indx, len, + columns = []; + if ( /^[0-9]+$/.test( val ) ) { + // always return an array + return [ parseInt( val, 10 ) ]; + } + // process column range + if ( !ignoreRanges && /-/.test( val ) ) { + ranges = val.match( /(\d+)\s*-\s*(\d+)/g ); + len = ranges ? ranges.length : 0; + for ( indx = 0; indx < len; indx++ ) { + range = ranges[indx].split( /\s*-\s*/ ); + start = parseInt( range[0], 10 ) || 0; + end = parseInt( range[1], 10 ) || ( c.columns - 1 ); + if ( start > end ) { + temp = start; start = end; end = temp; // swap + } + if ( end >= c.columns ) { + end = c.columns - 1; + } + for ( ; start <= end; start++ ) { + columns[ columns.length ] = start; + } + // remove processed range from val + val = val.replace( ranges[ indx ], '' ); + } + } + // process single columns + if ( !ignoreRanges && /,/.test( val ) ) { + singles = val.split( /\s*,\s*/ ); + len = singles.length; + for ( i = 0; i < len; i++ ) { + if ( singles[ i ] !== '' ) { + indx = parseInt( singles[ i ], 10 ); + if ( indx < c.columns ) { + columns[ columns.length ] = indx; + } + } + } + } + // return all columns + if ( !columns.length ) { + for ( indx = 0; indx < c.columns; indx++ ) { + columns[ columns.length ] = indx; + } + } + return columns; + }, + getColumnElm: function( c, $elements, column ) { + // data-column may contain multiple columns '1-3,5-6,8' + // replaces: c.$filters.filter( '[data-column="' + column + '"]' ); + return $elements.filter( function() { + var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) ); + return $.inArray( column, cols ) > -1; + }); + }, + multipleColumns: function( c, $input ) { + // look for multiple columns '1-3,4-6,8' in data-column + var wo = c.widgetOptions, + // only target 'all' column inputs on initialization + // & don't target 'all' column inputs if they don't exist + targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length, + val = $.trim( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ); + return tsf.findRange( c, val, !targets ); + }, + processTypes: function( c, data, vars ) { + var ffxn, + filterMatched = null, + matches = null; + for ( ffxn in tsf.types ) { + if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) { + matches = tsf.types[ffxn]( c, data, vars ); + if ( matches !== null ) { + data.matchedOn = ffxn; + filterMatched = matches; + } + } + } + return filterMatched; + }, + matchType: function( c, columnIndex ) { + var isMatch, + wo = c.widgetOptions, + $el = c.$headerIndexed[ columnIndex ]; + // filter-exact > filter-match > filter_matchType for type + if ( $el.hasClass( 'filter-exact' ) ) { + isMatch = false; + } else if ( $el.hasClass( 'filter-match' ) ) { + isMatch = true; + } else { + // filter-select is not applied when filter_functions are used, so look for a select + if ( wo.filter_columnFilters ) { + $el = c.$filters + .find( '.' + tscss.filter ) + .add( wo.filter_$externalFilters ) + .filter( '[data-column="' + columnIndex + '"]' ); + } else if ( wo.filter_$externalFilters ) { + $el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' ); + } + isMatch = $el.length ? + c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' : + // default to exact, if no inputs found + false; + } + return isMatch; + }, + processRow: function( c, data, vars ) { + var result, filterMatched, + fxn, ffxn, txt, + wo = c.widgetOptions, + showRow = true, + hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length, + + // if wo.filter_$anyMatch data-column attribute is changed dynamically + // we don't want to do an "anyMatch" search on one column using data + // for the entire row - see #998 + columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ? + // look for multiple columns '1-3,4-6,8' + tsf.multipleColumns( c, wo.filter_$anyMatch ) : + []; + data.$cells = data.$row.children(); + data.matchedOn = null; + if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { + data.anyMatch = true; + data.isMatch = true; + data.rowArray = data.$cells.map( function( i ) { + if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { + if ( data.parsed[ i ] ) { + txt = data.cacheArray[ i ]; + } else { + txt = data.rawArray[ i ]; + txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt ); + if ( c.sortLocaleCompare ) { + txt = ts.replaceAccents( txt ); + } + } + return txt; + } + }).get(); + data.filter = data.anyMatchFilter; + data.iFilter = data.iAnyMatchFilter; + data.exact = data.rowArray.join( ' ' ); + data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact; + data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' ); + vars.excludeMatch = vars.noAnyMatch; + filterMatched = tsf.processTypes( c, data, vars ); + if ( filterMatched !== null ) { + showRow = filterMatched; + } else { + if ( wo.filter_startsWith ) { + showRow = false; + // data.rowArray may not contain all columns + columnIndex = Math.min( c.columns, data.rowArray.length ); + while ( !showRow && columnIndex > 0 ) { + columnIndex--; + showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0; + } + } else { + showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0; + } + } + data.anyMatch = false; + // no other filters to process + if ( data.filters.join( '' ) === data.filter ) { + return showRow; + } + } + + for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { + data.filter = data.filters[ columnIndex ]; + data.index = columnIndex; + + // filter types to exclude, per column + vars.excludeMatch = vars.excludeFilter[ columnIndex ]; + + // ignore if filter is empty or disabled + if ( data.filter ) { + data.cache = data.cacheArray[ columnIndex ]; + result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || ''; + data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405 + data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ? + data.exact.toLowerCase() : data.exact; + data.isMatch = tsf.matchType( c, columnIndex ); + + result = showRow; // if showRow is true, show that row + + // in case select filter option has a different value vs text 'a - z|A through Z' + ffxn = wo.filter_columnFilters ? + c.$filters.add( wo.filter_$externalFilters ) + .filter( '[data-column="' + columnIndex + '"]' ) + .find( 'select option:selected' ) + .attr( 'data-function-name' ) || '' : ''; + // replace accents - see #357 + if ( c.sortLocaleCompare ) { + data.filter = ts.replaceAccents( data.filter ); + } + + // replace column specific default filters - see #1088 + if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) { + data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] ); + } + + // data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ), + // data.filter = case sensitive + data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter; + fxn = vars.functions[ columnIndex ]; + filterMatched = null; + if ( fxn ) { + if ( typeof fxn === 'function' ) { + // filter callback( exact cell content, parser normalized content, + // filter input value, column index, jQuery row object ) + filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); + } else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) { + // selector option function + txt = ffxn || data.filter; + filterMatched = + fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); + } + } + if ( filterMatched === null ) { + // cycle through the different filters + // filters return a boolean or null if nothing matches + filterMatched = tsf.processTypes( c, data, vars ); + // select with exact match; ignore "and" or "or" within the text; fixes #1486 + txt = fxn === true && (data.matchedOn === 'and' || data.matchedOn === 'or'); + if ( filterMatched !== null && !txt) { + result = filterMatched; + // Look for match, and add child row data for matching + } else { + // check fxn (filter-select in header) after filter types are checked + // without this, the filter + jQuery UI selectmenu demo was breaking + if ( fxn === true ) { + // default selector uses exact match unless 'filter-match' class is found + result = data.isMatch ? + // data.iExact may be a number + ( '' + data.iExact ).search( data.iFilter ) >= 0 : + data.filter === data.exact; + } else { + txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) ); + result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) ); + } + } + } else { + result = filterMatched; + } + showRow = ( result ) ? showRow : false; + } + } + return showRow; + }, + findRows: function( table, filters, currentFilters ) { + if ( + tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) || + !table.config.widgetOptions.filter_initialized + ) { + return; + } + var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex, + isChild, childRow, lastSearch, showRow, showParent, time, val, indx, + notFiltered, searchFiltered, query, injected, res, id, txt, + storedFilters = $.extend( [], filters ), + c = table.config, + wo = c.widgetOptions, + debug = ts.debug(c, 'filter'), + // data object passed to filters; anyMatch is a flag for the filters + data = { + anyMatch: false, + filters: filters, + // regex filter type cache + filter_regexCache : [] + }, + vars = { + // anyMatch really screws up with these types of filters + noAnyMatch: [ 'range', 'operators' ], + // cache filter variables that use ts.getColumnData in the main loop + functions : [], + excludeFilter : [], + defaultColFilter : [], + defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '' + }; + // parse columns after formatter, in case the class is added at that point + data.parsed = []; + for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { + data.parsed[ columnIndex ] = wo.filter_useParsedData || + // parser has a "parsed" parameter + ( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed || + // getData may not return 'parsed' if other 'filter-' class names exist + // ( e.g. ) + ts.getData && ts.getData( c.$headerIndexed[ columnIndex ], + ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' || + c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) ); + + vars.functions[ columnIndex ] = + ts.getColumnData( table, wo.filter_functions, columnIndex ) || + c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' ); + vars.defaultColFilter[ columnIndex ] = + ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || ''; + vars.excludeFilter[ columnIndex ] = + ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ ); + } + + if ( debug ) { + console.log( 'Filter >> Starting filter widget search', filters ); + time = new Date(); + } + // filtered rows count + c.filteredRows = 0; + c.totalRows = 0; + currentFilters = ( storedFilters || [] ); + + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true ); + // skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel! + // $rows = $tbody.children( 'tr' ).not( c.selectorRemove ); + columnIndex = c.columns; + // convert stored rows into a jQuery object + norm_rows = c.cache[ tbodyIndex ].normalized; + $rows = $( $.map( norm_rows, function( el ) { + return el[ columnIndex ].$row.get(); + }) ); + + if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) { + $rows + .removeClass( wo.filter_filteredRow ) + .not( '.' + c.cssChildRow ) + .css( 'display', '' ); + } else { + // filter out child rows + $rows = $rows.not( '.' + c.cssChildRow ); + len = $rows.length; + + if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) || + typeof filters[c.columns] !== 'undefined' ) { + data.anyMatchFlag = true; + data.anyMatchFilter = '' + ( + filters[ c.columns ] || + wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() || + '' + ); + if ( wo.filter_columnAnyMatch ) { + // specific columns search + query = data.anyMatchFilter.split( tsfRegex.andSplit ); + injected = false; + for ( indx = 0; indx < query.length; indx++ ) { + res = query[ indx ].split( ':' ); + if ( res.length > 1 ) { + // make the column a one-based index ( non-developers start counting from one :P ) + if ( isNaN( res[0] ) ) { + $.each( c.headerContent, function( i, txt ) { + // multiple matches are possible + if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) { + id = i; + filters[ id ] = res[1]; + } + }); + } else { + id = parseInt( res[0], 10 ) - 1; + } + if ( id >= 0 && id < c.columns ) { // if id is an integer + filters[ id ] = res[1]; + query.splice( indx, 1 ); + indx--; + injected = true; + } + } + } + if ( injected ) { + data.anyMatchFilter = query.join( ' && ' ); + } + } + } + + // optimize searching only through already filtered rows - see #313 + searchFiltered = wo.filter_searchFiltered; + lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || []; + if ( searchFiltered ) { + // cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669 + for ( indx = 0; indx < columnIndex + 1; indx++ ) { + val = filters[indx] || ''; + // break out of loop if we've already determined not to search filtered rows + if ( !searchFiltered ) { indx = columnIndex; } + // search already filtered rows if... + searchFiltered = searchFiltered && lastSearch.length && + // there are no changes from beginning of filter + val.indexOf( lastSearch[indx] || '' ) === 0 && + // if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string + !tsfRegex.alreadyFiltered.test( val ) && + // if we are not doing exact matches, using '|' ( logical or ) or not '!' + !tsfRegex.exactTest.test( val ) && + // don't search only filtered if the value is negative + // ( '> -10' => '> -100' will ignore hidden rows ) + !( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) && + // if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593 + !( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length && + !tsf.matchType( c, indx ) ); + } + } + notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length; + // can't search when all rows are hidden - this happens when looking for exact matches + if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; } + if ( debug ) { + console.log( 'Filter >> Searching through ' + + ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' ); + } + if ( data.anyMatchFlag ) { + if ( c.sortLocaleCompare ) { + // replace accents + data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter ); + } + if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) { + data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter ); + // clear search filtered flag because default filters are not saved to the last search + searchFiltered = false; + } + // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true + // when c.ignoreCase is true, the cache contains all lower case data + data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ? + data.anyMatchFilter : + data.anyMatchFilter.toLowerCase(); + } + + // loop through the rows + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + + txt = $rows[ rowIndex ].className; + // the first row can never be a child row + isChild = rowIndex && tsfRegex.child.test( txt ); + // skip child rows & already filtered rows + if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) { + continue; + } + + data.$row = $rows.eq( rowIndex ); + data.rowIndex = rowIndex; + data.cacheArray = norm_rows[ rowIndex ]; + rowData = data.cacheArray[ c.columns ]; + data.rawArray = rowData.raw; + data.childRowText = ''; + + if ( !wo.filter_childByColumn ) { + txt = ''; + // child row cached text + childRow = rowData.child; + // so, if 'table.config.widgetOptions.filter_childRows' is true and there is + // a match anywhere in the child row, then it will make the row visible + // checked here so the option can be changed dynamically + for ( indx = 0; indx < childRow.length; indx++ ) { + txt += ' ' + childRow[indx].join( ' ' ) || ''; + } + data.childRowText = wo.filter_childRows ? + ( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) : + ''; + } + + showRow = false; + showParent = tsf.processRow( c, data, vars ); + $row = rowData.$row; + + // don't pass reference to val + val = showParent ? true : false; + childRow = rowData.$row.filter( ':gt(0)' ); + if ( wo.filter_childRows && childRow.length ) { + if ( wo.filter_childByColumn ) { + if ( !wo.filter_childWithSibs ) { + // hide all child rows + childRow.addClass( wo.filter_filteredRow ); + // if only showing resulting child row, only include parent + $row = $row.eq( 0 ); + } + // cycle through each child row + for ( indx = 0; indx < childRow.length; indx++ ) { + data.$row = childRow.eq( indx ); + data.cacheArray = rowData.child[ indx ]; + data.rawArray = data.cacheArray; + val = tsf.processRow( c, data, vars ); + // use OR comparison on child rows + showRow = showRow || val; + if ( !wo.filter_childWithSibs && val ) { + childRow.eq( indx ).removeClass( wo.filter_filteredRow ); + } + } + } + // keep parent row match even if no child matches... see #1020 + showRow = showRow || showParent; + } else { + showRow = val; + } + $row + .toggleClass( wo.filter_filteredRow, !showRow )[0] + .display = showRow ? '' : 'none'; + } + } + c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length; + c.totalRows += $rows.length; + ts.processTbody( table, $tbody, false ); + } + // lastCombinedFilter is no longer used internally + c.lastCombinedFilter = storedFilters.join(''); // save last search + // don't save 'filters' directly since it may have altered ( AnyMatch column searches ) + c.lastSearch = storedFilters; + c.$table.data( 'lastSearch', storedFilters ); + if ( wo.filter_saveFilters && ts.storage ) { + ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) ); + } + if ( debug ) { + console.log( 'Filter >> Completed search' + ts.benchmark(time) ); + } + if ( wo.filter_initialized ) { + c.$table.triggerHandler( 'filterBeforeEnd', c ); + c.$table.triggerHandler( 'filterEnd', c ); + } + setTimeout( function() { + ts.applyWidget( c.table ); // make sure zebra widget is applied + }, 0 ); + }, + getOptionSource: function( table, column, onlyAvail ) { + table = $( table )[0]; + var c = table.config, + wo = c.widgetOptions, + arry = false, + source = wo.filter_selectSource, + last = c.$table.data( 'lastSearch' ) || [], + fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column ); + + if ( onlyAvail && last[column] !== '' ) { + onlyAvail = false; + } + + // filter select source option + if ( fxn === true ) { + // OVERALL source + arry = source( table, column, onlyAvail ); + } else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '' ) >= 0 ) ) { + // selectSource is a jQuery object or string of options + return fxn; + } else if ( $.isArray( fxn ) ) { + arry = fxn; + } else if ( $.type( source ) === 'object' && fxn ) { + // custom select source function for a SPECIFIC COLUMN + arry = fxn( table, column, onlyAvail ); + // abort - updating the selects from an external method + if (arry === null) { + return null; + } + } + if ( arry === false ) { + // fall back to original method + arry = tsf.getOptions( table, column, onlyAvail ); + } + + return tsf.processOptions( table, column, arry ); + + }, + processOptions: function( table, column, arry ) { + if ( !$.isArray( arry ) ) { + return false; + } + table = $( table )[0]; + var cts, txt, indx, len, parsedTxt, str, + c = table.config, + validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns, + direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false, + parsed = []; + // get unique elements and sort the list + // if $.tablesorter.sortText exists ( not in the original tablesorter ), + // then natural sort the list otherwise use a basic sort + arry = $.grep( arry, function( value, indx ) { + if ( value.text ) { + return true; + } + return $.inArray( value, arry ) === indx; + }); + if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) { + // unsorted select options + return arry; + } else { + len = arry.length; + // parse select option values + for ( indx = 0; indx < len; indx++ ) { + txt = arry[ indx ]; + // check for object + str = txt.text ? txt.text : txt; + // sortNatural breaks if you don't pass it strings + parsedTxt = ( validColumn && c.parsers && c.parsers.length && + c.parsers[ column ].format( str, table, [], column ) || str ).toString(); + parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt; + // parse array data using set column parser; this DOES NOT pass the original + // table cell to the parser format function + if ( txt.text ) { + txt.parsed = parsedTxt; + parsed[ parsed.length ] = txt; + } else { + parsed[ parsed.length ] = { + text : txt, + // check parser length - fixes #934 + parsed : parsedTxt + }; + } + } + // sort parsed select options + cts = c.textSorter || ''; + parsed.sort( function( a, b ) { + var x = direction ? b.parsed : a.parsed, + y = direction ? a.parsed : b.parsed; + if ( validColumn && typeof cts === 'function' ) { + // custom OVERALL text sorter + return cts( x, y, true, column, table ); + } else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) { + // custom text sorter for a SPECIFIC COLUMN + return cts[column]( x, y, true, column, table ); + } else if ( ts.sortNatural ) { + // fall back to natural sort + return ts.sortNatural( x, y ); + } + // using an older version! do a basic sort + return true; + }); + // rebuild arry from sorted parsed data + arry = []; + len = parsed.length; + for ( indx = 0; indx < len; indx++ ) { + arry[ arry.length ] = parsed[indx]; + } + return arry; + } + }, + getOptions: function( table, column, onlyAvail ) { + table = $( table )[0]; + var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen, + c = table.config, + wo = c.widgetOptions, + arry = []; + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + cache = c.cache[tbodyIndex]; + len = c.cache[tbodyIndex].normalized.length; + // loop through the rows + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + // get cached row from cache.row ( old ) or row data object + // ( new; last item in normalized array ) + row = cache.row ? + cache.row[ rowIndex ] : + cache.normalized[ rowIndex ][ c.columns ].$row[0]; + // check if has class filtered + if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) { + continue; + } + // get non-normalized cell content + if ( wo.filter_useParsedData || + c.parsers[column].parsed || + c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) { + arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ]; + // child row parsed data + if ( wo.filter_childRows && wo.filter_childByColumn ) { + childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1; + for ( indx = 0; indx < childLen; indx++ ) { + arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ]; + } + } + } else { + // get raw cached data instead of content directly from the cells + arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ]; + // child row unparsed data + if ( wo.filter_childRows && wo.filter_childByColumn ) { + childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length; + for ( indx = 1; indx < childLen; indx++ ) { + child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column ); + arry[ arry.length ] = '' + ts.getElementText( c, child, column ); + } + } + } + } + } + return arry; + }, + buildSelect: function( table, column, arry, updating, onlyAvail ) { + table = $( table )[0]; + column = parseInt( column, 10 ); + if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) { + return; + } + + var indx, val, txt, t, $filters, $filter, option, + c = table.config, + wo = c.widgetOptions, + node = c.$headerIndexed[ column ], + // t.data( 'placeholder' ) won't work in jQuery older than 1.4.3 + options = '', + // Get curent filter value + currentValue = c.$table + .find( 'thead' ) + .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) + .val(); + + // nothing included in arry ( external source ), so get the options from + // filter_selectSource or column data + if ( typeof arry === 'undefined' || arry === '' ) { + arry = tsf.getOptionSource( table, column, onlyAvail ); + // abort, selects are updated by an external method + if (arry === null) { + return; + } + } + + if ( $.isArray( arry ) ) { + // build option list + for ( indx = 0; indx < arry.length; indx++ ) { + option = arry[ indx ]; + if ( option.text ) { + // OBJECT!! add data-function-name in case the value is set in filter_functions + option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value; + + // support jQuery < v1.8, otherwise the below code could be shortened to + // options += $( '