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/<theme name>` 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/<theme name>` 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 -[](http://stillmaintained.com/linjunpop/jquery-tablesorter-rails) [](http://badge.fury.io/rb/jquery-tablesorter) -[](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 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="2" height="13"> + <rect style="fill:#333;fill-opacity:.8;" width="1" height="1" x="1" y="2"/> + <rect style="fill:#333;fill-opacity:.8;" width="1" height="1" x="1" y="4"/> + <rect style="fill:#333;fill-opacity:.8;" width="1" height="1" x="1" y="6"/> + <rect style="fill:#333;fill-opacity:.8;" width="1" height="1" x="1" y="8"/> + <rect style="fill:#333;fill-opacity:.8;" width="1" height="1" x="1" y="10"/> +</svg> \ 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 '<input type="text" class="ts-' + extra[0] + '" style="max-width:' + len + 'em" value="' + indx + '"/>'; + } + 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 += '<option>' + i + '</option>'; + options = buildPageSelect( table, p ); + len = options.length; + for (indx = 0; indx < len; indx++) { + t += '<option value="' + options[indx] + '">' + options[indx] + '</option>'; } - $(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('<tr class="pagerSavedHeightSpacer ' + table.config.selectorRemove.replace(/(tr)?\./g,'') + '" style="height:' + d + 'px;"></tr>'); + 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('<tr class="pagerSavedHeightSpacer ' + c.selectorRemove.slice(1) + '" style="height:' + d + 'px;"></tr>'); + } } } - } - }, - - 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 = '<tr class="' + c.cssErrorRow + ' ' + tc.selectorRemove.replace(/(tr)?\./g,'') + '"><td style="text-align: center;" colspan="' + hl + '">' + - (exception ? exception.message + ' (' + exception.name + ')' : 'No rows found') + '</td></tr>', - 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('<tr class="pagerSavedHeightSpacer ' + c.selectorRemove.slice(1) + '"><td> </td></tr>'); + } + 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 += '<tr>'; - for ( j = 0; j < d[i].length; j++ ) { - // build tbody cells - tds += '<td>' + d[i][j] + '</td>'; - } - tds += '</tr>'; - } - } - // 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 += '<tr>'; + for ( j = 0; j < d[i].length; j++ ) { + // build tbody cells; watch for data containing HTML markup - see #434 + tds += /^\s*<td/.test(d[i][j]) ? $.trim(d[i][j]) : '<td>' + d[i][j] + '</td>'; + } + tds += '</tr>'; + } + // 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 : '<tr><td colspan="' + c.columns + '">' + message + '</td></tr>' ) + .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 : '<a href="#">{page}</a>', // page element; use {page} to include the page number + currentClass : 'current', // current page class name + adjacentSpacer : '<span> | </span>', // spacer for page numbers next to each other + distanceSpacer : '<span> … <span>', // 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 = $('<div/>'), + 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 = $('<div class="' + wo.reorder_helperBar + '" />') + .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('<div class="' + _this.options.sortClass.replace('.', '') + '"/>') + // add handle class + "-disabled" to drag-disabled columns + .prepend('<div class="' + handle + ( hasAccept ? '' : '-disabled' ) + '"></div>'); + } + }); + }, + 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 = '<ul class="dragtable-sortable" style="position:absolute; width:' + totalWidth + 'px;">'; + 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] = '<li style="width:' + row.outerWidth() + 'px;">' + + '<table ' + tableAttrsString + '>' + + ( captionHeight ? '<caption style="height:' + captionHeight + 'px;"></caption>' : '' ) + + '<thead>'; + // thead + headerRows.each(function(j){ + sortableColumn[i] += '<tr ' + rowAttrsArr[rowIndex++] + + ( heightArr[j] ? ' style="height:' + heightArr[j] + 'px;"' : '' ) + '>' + + row[j].outerHTML + '</tr>'; + }); + sortableColumn[i] += '</thead><tbody>'; + // 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] += '<tr ' + rowAttrsArr[rowIndex++] + + ( heightArr[j] ? ' style="height:' + heightArr[j] + 'px;"' : '' ) + '>' + + this.outerHTML + '</tr>'; + }); + sortableColumn[i] += '</tbody>'; + + // add footer to end of max Rows + if (!_this.options.excludeFooter) { + sortableColumn[i] += '<tfoot><tr ' + rowAttrsArr[rowIndex++] + '>' + + thtb.filter('tfoot').children('tr:visible').children()[i].outerHTML + '</tr></tfoot>'; + } + sortableColumn[i] += '</table></li>'; + } + } + sortableHtml += sortableColumn.join('') + '</ul>'; + 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('<div class="outer" style="height:100%;"><div class="inner" style="height:100%;"></div></div>'); + }, + 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 <style> tag to the <head> + // remove any current document selections + function disableTextSelection() { + // jQuery doesn't support the element.text attribute in MSIE 8 + // http://stackoverflow.com/questions/2692770/style-style-textcss-appendtohead-does-not-work-in-ie + var $style = $('<style id="__dragtable_disable_text_selection__" type="text/css">body { -ms-user-select:none;-moz-user-select:-moz-none;-khtml-user-select:none;-webkit-user-select:none;user-select:none; }</style>'); + $(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 <style> tag, and restore the original <body> onselectstart attribute + function restoreTextSelection() { + $('#__dragtable_disable_text_selection__').remove(); + if (body_onselectstart_save) { + $(document.body).attr('onselectstart', body_onselectstart_save); + } else { + $(document.body).removeAttr('onselectstart'); + } + if (body_unselectable_save) { + $(document.body).attr('unselectable', body_unselectable_save); + } else { + $(document.body).removeAttr('unselectable'); + } + } + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/jquery.metadata.js b/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.metadata.js similarity index 99% rename from vendor/assets/javascripts/jquery-tablesorter/jquery.metadata.js rename to vendor/assets/javascripts/jquery-tablesorter/extras/jquery.metadata.js index dc4538e..ddff5e8 100644 --- a/vendor/assets/javascripts/jquery-tablesorter/jquery.metadata.js +++ b/vendor/assets/javascripts/jquery-tablesorter/extras/jquery.metadata.js @@ -1,4 +1,4 @@ -/* +/* * Metadata - jQuery plugin for parsing metadata from elements * * Copyright (c) 2006 John Resig, Yehuda Katz, Jörn Zaefferer, Paul McLanahan @@ -92,6 +92,7 @@ $.extend({ if ( data.indexOf( '{' ) <0 ) { data = "{" + data + "}"; } + /*jshint evil:true */ data = eval("(" + data + ")"); $.data( elem, settings.single, data ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/extras/semver-mod.js b/vendor/assets/javascripts/jquery-tablesorter/extras/semver-mod.js new file mode 100644 index 0000000..c9a3826 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/extras/semver-mod.js @@ -0,0 +1,1217 @@ +/*! Modified semver.js for node.js (v4.3.3, 3/27/2015) *//* + semver-mod.js by R.Garrison (@Mottie) + semver.js by @isaacs: https://github.com/isaacs/node-semver + ( all modifications have been labeled ) + */ +// ***** MODIFIED LINE BELOW ***** +(function(){ +// ***** MODIFIED LINE BELOW ***** +var module = { exports : {} }; +// export the class if we are in a Node-like system. +// ***** MODIFIED LINE BELOW ***** +// if (typeof module === 'object' && module.exports === exports) +// ***** MODIFIED LINE BELOW ***** +var exports = module.exports = SemVer; + +// The debug function is excluded entirely from the minified version. +/* nomin */ var debug; +/* nomin */ if (typeof process === 'object' && + /* nomin */ process.env && + /* nomin */ process.env.NODE_DEBUG && + /* nomin */ /\bsemver\b/i.test(process.env.NODE_DEBUG)) + /* nomin */ debug = function() { + /* nomin */ var args = Array.prototype.slice.call(arguments, 0); + /* nomin */ args.unshift('SEMVER'); + /* nomin */ console.log.apply(console, args); + /* nomin */ }; +/* nomin */ else + /* nomin */ debug = function() {}; + +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +exports.SEMVER_SPEC_VERSION = '2.0.0'; + +var MAX_LENGTH = 256; +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + +// The actual regexps go on exports.re +var re = exports.re = []; +var src = exports.src = []; +var R = 0; + +// The following Regular Expressions can be used for tokenizing, +// validating, and parsing SemVer version strings. + +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. + +var NUMERICIDENTIFIER = R++; +src[NUMERICIDENTIFIER] = '0|[1-9]\\d*'; +var NUMERICIDENTIFIERLOOSE = R++; +src[NUMERICIDENTIFIERLOOSE] = '[0-9]+'; + + +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or +// more letters, digits, or hyphens. + +var NONNUMERICIDENTIFIER = R++; +src[NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*'; + + +// ## Main Version +// Three dot-separated numeric identifiers. + +var MAINVERSION = R++; +src[MAINVERSION] = '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')'; + +var MAINVERSIONLOOSE = R++; +src[MAINVERSIONLOOSE] = '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')'; + +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. + +var PRERELEASEIDENTIFIER = R++; +src[PRERELEASEIDENTIFIER] = '(?:' + src[NUMERICIDENTIFIER] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + +var PRERELEASEIDENTIFIERLOOSE = R++; +src[PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[NUMERICIDENTIFIERLOOSE] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + + +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. + +var PRERELEASE = R++; +src[PRERELEASE] = '(?:-(' + src[PRERELEASEIDENTIFIER] + + '(?:\\.' + src[PRERELEASEIDENTIFIER] + ')*))'; + +var PRERELEASELOOSE = R++; +src[PRERELEASELOOSE] = '(?:-?(' + src[PRERELEASEIDENTIFIERLOOSE] + + '(?:\\.' + src[PRERELEASEIDENTIFIERLOOSE] + ')*))'; + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +var BUILDIDENTIFIER = R++; +src[BUILDIDENTIFIER] = '[0-9A-Za-z-]+'; + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +var BUILD = R++; +src[BUILD] = '(?:\\+(' + src[BUILDIDENTIFIER] + + '(?:\\.' + src[BUILDIDENTIFIER] + ')*))'; + + +// ## Full Version String +// A main version, followed optionally by a pre-release version and +// build metadata. + +// Note that the only major, minor, patch, and pre-release sections of +// the version string are capturing groups. The build metadata is not a +// capturing group, because it should not ever be used in version +// comparison. + +var FULL = R++; +var FULLPLAIN = 'v?' + src[MAINVERSION] + + src[PRERELEASE] + '?' + + src[BUILD] + '?'; + +src[FULL] = '^' + FULLPLAIN + '$'; + +// like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty +// common in the npm registry. +var LOOSEPLAIN = '[v=\\s]*' + src[MAINVERSIONLOOSE] + + src[PRERELEASELOOSE] + '?' + + src[BUILD] + '?'; + +var LOOSE = R++; +src[LOOSE] = '^' + LOOSEPLAIN + '$'; + +var GTLT = R++; +src[GTLT] = '((?:<|>)?=?)'; + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +var XRANGEIDENTIFIERLOOSE = R++; +src[XRANGEIDENTIFIERLOOSE] = src[NUMERICIDENTIFIERLOOSE] + '|x|X|\\*'; +var XRANGEIDENTIFIER = R++; +src[XRANGEIDENTIFIER] = src[NUMERICIDENTIFIER] + '|x|X|\\*'; + +var XRANGEPLAIN = R++; +src[XRANGEPLAIN] = '[v=\\s]*(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:' + src[PRERELEASE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGEPLAINLOOSE = R++; +src[XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:' + src[PRERELEASELOOSE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGE = R++; +src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$'; +var XRANGELOOSE = R++; +src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'; + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +var LONETILDE = R++; +src[LONETILDE] = '(?:~>?)'; + +var TILDETRIM = R++; +src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+'; +re[TILDETRIM] = new RegExp(src[TILDETRIM], 'g'); +var tildeTrimReplace = '$1~'; + +var TILDE = R++; +src[TILDE] = '^' + src[LONETILDE] + src[XRANGEPLAIN] + '$'; +var TILDELOOSE = R++; +src[TILDELOOSE] = '^' + src[LONETILDE] + src[XRANGEPLAINLOOSE] + '$'; + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +var LONECARET = R++; +src[LONECARET] = '(?:\\^)'; + +var CARETTRIM = R++; +src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+'; +re[CARETTRIM] = new RegExp(src[CARETTRIM], 'g'); +var caretTrimReplace = '$1^'; + +var CARET = R++; +src[CARET] = '^' + src[LONECARET] + src[XRANGEPLAIN] + '$'; +var CARETLOOSE = R++; +src[CARETLOOSE] = '^' + src[LONECARET] + src[XRANGEPLAINLOOSE] + '$'; + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +var COMPARATORLOOSE = R++; +src[COMPARATORLOOSE] = '^' + src[GTLT] + '\\s*(' + LOOSEPLAIN + ')$|^$'; +var COMPARATOR = R++; +src[COMPARATOR] = '^' + src[GTLT] + '\\s*(' + FULLPLAIN + ')$|^$'; + + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +var COMPARATORTRIM = R++; +src[COMPARATORTRIM] = '(\\s*)' + src[GTLT] + + '\\s*(' + LOOSEPLAIN + '|' + src[XRANGEPLAIN] + ')'; + +// this one has to use the /g flag +re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], 'g'); +var comparatorTrimReplace = '$1$2$3'; + + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be +// checked against either the strict or loose comparator form +// later. +var HYPHENRANGE = R++; +src[HYPHENRANGE] = '^\\s*(' + src[XRANGEPLAIN] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAIN] + ')' + + '\\s*$'; + +var HYPHENRANGELOOSE = R++; +src[HYPHENRANGELOOSE] = '^\\s*(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s*$'; + +// Star ranges basically just allow anything at all. +var STAR = R++; +src[STAR] = '(<|>)?=?\\s*\\*'; + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (var i = 0; i < R; i++) { + debug(i, src[i]); + if (!re[i]) + re[i] = new RegExp(src[i]); +} + +exports.parse = parse; +function parse(version, loose) { + if (version instanceof SemVer) + return version; + + if (typeof version !== 'string') + return null; + + if (version.length > MAX_LENGTH) + return null; + + var r = loose ? re[LOOSE] : re[FULL]; + if (!r.test(version)) + return null; + + try { + return new SemVer(version, loose); + } catch (er) { + return null; + } +} + +exports.valid = valid; +function valid(version, loose) { + var v = parse(version, loose); + return v ? v.version : null; +} + + +exports.clean = clean; +function clean(version, loose) { + var s = parse(version.trim().replace(/^[=v]+/, ''), loose); + return s ? s.version : null; +} + +// ***** MODIFIED LINE BELOW ***** +window.semver = exports.SemVer = SemVer; + +function SemVer(version, loose) { + if (version instanceof SemVer) { + if (version.loose === loose) + return version; + else + version = version.version; + } else if (typeof version !== 'string') { + throw new TypeError('Invalid Version: ' + version); + } + + if (version.length > MAX_LENGTH) + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + + if (!(this instanceof SemVer)) + return new SemVer(version, loose); + + debug('SemVer', version, loose); + this.loose = loose; + var m = version.trim().match(loose ? re[LOOSE] : re[FULL]); + + if (!m) + throw new TypeError('Invalid Version: ' + version); + + this.raw = version; + + // these are actually numbers + this.major = +m[1]; + this.minor = +m[2]; + this.patch = +m[3]; + + if (this.major > MAX_SAFE_INTEGER || this.major < 0) + throw new TypeError('Invalid major version') + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) + throw new TypeError('Invalid minor version') + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) + throw new TypeError('Invalid patch version') + + // numberify any prerelease numeric ids + if (!m[4]) + this.prerelease = []; + else + this.prerelease = m[4].split('.').map(function(id) { + if (/^[0-9]+$/.test(id)) { + var num = +id + if (num >= 0 && num < MAX_SAFE_INTEGER) + return num + } + return id; + }); + + this.build = m[5] ? m[5].split('.') : []; + this.format(); +} + +SemVer.prototype.format = function() { + this.version = this.major + '.' + this.minor + '.' + this.patch; + if (this.prerelease.length) + this.version += '-' + this.prerelease.join('.'); + return this.version; +}; + +SemVer.prototype.inspect = function() { + return '<SemVer "' + this + '">'; +}; + +SemVer.prototype.toString = function() { + return this.version; +}; + +SemVer.prototype.compare = function(other) { + debug('SemVer.compare', this.version, this.loose, other); + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return this.compareMain(other) || this.comparePre(other); +}; + +SemVer.prototype.compareMain = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return compareIdentifiers(this.major, other.major) || + compareIdentifiers(this.minor, other.minor) || + compareIdentifiers(this.patch, other.patch); +}; + +SemVer.prototype.comparePre = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) + return -1; + else if (!this.prerelease.length && other.prerelease.length) + return 1; + else if (!this.prerelease.length && !other.prerelease.length) + return 0; + + var i = 0; + do { + var a = this.prerelease[i]; + var b = other.prerelease[i]; + debug('prerelease compare', i, a, b); + if (a === undefined && b === undefined) + return 0; + else if (b === undefined) + return 1; + else if (a === undefined) + return -1; + else if (a === b) + continue; + else + return compareIdentifiers(a, b); + } while (++i); +}; + +// preminor will bump the version up to the next minor release, and immediately +// down to pre-release. premajor and prepatch work the same way. +SemVer.prototype.inc = function(release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0; + this.patch = 0; + this.minor = 0; + this.major++; + this.inc('pre', identifier); + break; + case 'preminor': + this.prerelease.length = 0; + this.patch = 0; + this.minor++; + this.inc('pre', identifier); + break; + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0; + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) + this.major++; + this.minor = 0; + this.patch = 0; + this.prerelease = []; + break; + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) + this.minor++; + this.patch = 0; + this.prerelease = []; + break; + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) + this.patch++; + this.prerelease = []; + break; + // This probably shouldn't be used publicly. + // 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) + this.prerelease = [0]; + else { + var i = this.prerelease.length; + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++; + i = -2; + } + } + if (i === -1) // didn't increment anything + this.prerelease.push(0); + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) + this.prerelease = [identifier, 0]; + } else + this.prerelease = [identifier, 0]; + } + break; + + default: + throw new Error('invalid increment argument: ' + release); + } + this.format(); + return this; +}; + +exports.inc = inc; +function inc(version, release, loose, identifier) { + if (typeof(loose) === 'string') { + identifier = loose; + loose = undefined; + } + + try { + return new SemVer(version, loose).inc(release, identifier).version; + } catch (er) { + return null; + } +} + +exports.diff = diff; +function diff(version1, version2) { + if (eq(version1, version2)) { + return null; + } else { + var v1 = parse(version1); + var v2 = parse(version2); + if (v1.prerelease.length || v2.prerelease.length) { + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return 'pre'+key; + } + } + } + return 'prerelease'; + } + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return key; + } + } + } + } +} + +exports.compareIdentifiers = compareIdentifiers; + +var numeric = /^[0-9]+$/; +function compareIdentifiers(a, b) { + var anum = numeric.test(a); + var bnum = numeric.test(b); + + if (anum && bnum) { + a = +a; + b = +b; + } + + return (anum && !bnum) ? -1 : + (bnum && !anum) ? 1 : + a < b ? -1 : + a > b ? 1 : + 0; +} + +exports.rcompareIdentifiers = rcompareIdentifiers; +function rcompareIdentifiers(a, b) { + return compareIdentifiers(b, a); +} + +exports.major = major; +function major(a, loose) { + return new SemVer(a, loose).major; +} + +exports.minor = minor; +function minor(a, loose) { + return new SemVer(a, loose).minor; +} + +exports.patch = patch; +function patch(a, loose) { + return new SemVer(a, loose).patch; +} + +exports.compare = compare; +function compare(a, b, loose) { + return new SemVer(a, loose).compare(b); +} + +exports.compareLoose = compareLoose; +function compareLoose(a, b) { + return compare(a, b, true); +} + +exports.rcompare = rcompare; +function rcompare(a, b, loose) { + return compare(b, a, loose); +} + +exports.sort = sort; +function sort(list, loose) { + return list.sort(function(a, b) { + return exports.compare(a, b, loose); + }); +} + +exports.rsort = rsort; +function rsort(list, loose) { + return list.sort(function(a, b) { + return exports.rcompare(a, b, loose); + }); +} + +exports.gt = gt; +function gt(a, b, loose) { + return compare(a, b, loose) > 0; +} + +exports.lt = lt; +function lt(a, b, loose) { + return compare(a, b, loose) < 0; +} + +exports.eq = eq; +function eq(a, b, loose) { + return compare(a, b, loose) === 0; +} + +exports.neq = neq; +function neq(a, b, loose) { + return compare(a, b, loose) !== 0; +} + +exports.gte = gte; +function gte(a, b, loose) { + return compare(a, b, loose) >= 0; +} + +exports.lte = lte; +function lte(a, b, loose) { + return compare(a, b, loose) <= 0; +} + +exports.cmp = cmp; +function cmp(a, op, b, loose) { + var ret; + switch (op) { + case '===': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a === b; + break; + case '!==': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a !== b; + break; + case '': case '=': case '==': ret = eq(a, b, loose); break; + case '!=': ret = neq(a, b, loose); break; + case '>': ret = gt(a, b, loose); break; + case '>=': ret = gte(a, b, loose); break; + case '<': ret = lt(a, b, loose); break; + case '<=': ret = lte(a, b, loose); break; + default: throw new TypeError('Invalid operator: ' + op); + } + return ret; +} + +exports.Comparator = Comparator; +function Comparator(comp, loose) { + if (comp instanceof Comparator) { + if (comp.loose === loose) + return comp; + else + comp = comp.value; + } + + if (!(this instanceof Comparator)) + return new Comparator(comp, loose); + + debug('comparator', comp, loose); + this.loose = loose; + this.parse(comp); + + if (this.semver === ANY) + this.value = ''; + else + this.value = this.operator + this.semver.version; + + debug('comp', this); +} + +var ANY = {}; +Comparator.prototype.parse = function(comp) { + var r = this.loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var m = comp.match(r); + + if (!m) + throw new TypeError('Invalid comparator: ' + comp); + + this.operator = m[1]; + if (this.operator === '=') + this.operator = ''; + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) + this.semver = ANY; + else + this.semver = new SemVer(m[2], this.loose); +}; + +Comparator.prototype.inspect = function() { + return '<SemVer Comparator "' + this + '">'; +}; + +Comparator.prototype.toString = function() { + return this.value; +}; + +Comparator.prototype.test = function(version) { + debug('Comparator.test', version, this.loose); + + if (this.semver === ANY) + return true; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + return cmp(version, this.operator, this.semver, this.loose); +}; + + +exports.Range = Range; +function Range(range, loose) { + if ((range instanceof Range) && range.loose === loose) + return range; + + if (!(this instanceof Range)) + return new Range(range, loose); + + this.loose = loose; + + // First, split based on boolean or || + this.raw = range; + this.set = range.split(/\s*\|\|\s*/).map(function(range) { + return this.parseRange(range.trim()); + }, this).filter(function(c) { + // throw out any that are not relevant for whatever reason + return c.length; + }); + + if (!this.set.length) { + throw new TypeError('Invalid SemVer Range: ' + range); + } + + this.format(); +} + +Range.prototype.inspect = function() { + return '<SemVer Range "' + this.range + '">'; +}; + +Range.prototype.format = function() { + this.range = this.set.map(function(comps) { + return comps.join(' ').trim(); + }).join('||').trim(); + return this.range; +}; + +Range.prototype.toString = function() { + return this.range; +}; + +Range.prototype.parseRange = function(range) { + var loose = this.loose; + range = range.trim(); + debug('range', range, loose); + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE]; + range = range.replace(hr, hyphenReplace); + debug('hyphen replace', range); + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace); + debug('comparator trim', range, re[COMPARATORTRIM]); + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[TILDETRIM], tildeTrimReplace); + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[CARETTRIM], caretTrimReplace); + + // normalize spaces + range = range.split(/\s+/).join(' '); + + // At this point, the range is completely trimmed and + // ready to be split into comparators. + + var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var set = range.split(' ').map(function(comp) { + return parseComparator(comp, loose); + }).join(' ').split(/\s+/); + if (this.loose) { + // in loose mode, throw out any that are not valid comparators + set = set.filter(function(comp) { + return !!comp.match(compRe); + }); + } + set = set.map(function(comp) { + return new Comparator(comp, loose); + }); + + return set; +}; + +// Mostly just for testing and legacy API reasons +exports.toComparators = toComparators; +function toComparators(range, loose) { + return new Range(range, loose).set.map(function(comp) { + return comp.map(function(c) { + return c.value; + }).join(' ').trim().split(' '); + }); +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +function parseComparator(comp, loose) { + debug('comp', comp); + comp = replaceCarets(comp, loose); + debug('caret', comp); + comp = replaceTildes(comp, loose); + debug('tildes', comp); + comp = replaceXRanges(comp, loose); + debug('xrange', comp); + comp = replaceStars(comp, loose); + debug('stars', comp); + return comp; +} + +function isX(id) { + return !id || id.toLowerCase() === 'x' || id === '*'; +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceTilde(comp, loose); + }).join(' '); +} + +function replaceTilde(comp, loose) { + var r = loose ? re[TILDELOOSE] : re[TILDE]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('tilde', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) + // ~1.2 == >=1.2.0- <1.3.0- + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else if (pr) { + debug('replaceTilde pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + + debug('tilde return', ret); + return ret; + }); +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceCaret(comp, loose); + }).join(' '); +} + +function replaceCaret(comp, loose) { + debug('caret', comp, loose); + var r = loose ? re[CARETLOOSE] : re[CARET]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('caret', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) { + if (M === '0') + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else + ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0'; + } else if (pr) { + debug('replaceCaret pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + (+M + 1) + '.0.0'; + } else { + debug('no pr'); + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + (+M + 1) + '.0.0'; + } + + debug('caret return', ret); + return ret; + }); +} + +function replaceXRanges(comp, loose) { + debug('replaceXRanges', comp, loose); + return comp.split(/\s+/).map(function(comp) { + return replaceXRange(comp, loose); + }).join(' '); +} + +function replaceXRange(comp, loose) { + comp = comp.trim(); + var r = loose ? re[XRANGELOOSE] : re[XRANGE]; + return comp.replace(r, function(ret, gtlt, M, m, p, pr) { + debug('xRange', comp, ret, gtlt, M, m, p, pr); + var xM = isX(M); + var xm = xM || isX(m); + var xp = xm || isX(p); + var anyX = xp; + + if (gtlt === '=' && anyX) + gtlt = ''; + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0'; + } else { + // nothing is forbidden + ret = '*'; + } + } else if (gtlt && anyX) { + // replace X with 0 + if (xm) + m = 0; + if (xp) + p = 0; + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + gtlt = '>='; + if (xm) { + M = +M + 1; + m = 0; + p = 0; + } else if (xp) { + m = +m + 1; + p = 0; + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<' + if (xm) + M = +M + 1 + else + m = +m + 1 + } + + ret = gtlt + M + '.' + m + '.' + p; + } else if (xm) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + } else if (xp) { + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + } + + debug('xRange return', ret); + + return ret; + }); +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +function replaceStars(comp, loose) { + debug('replaceStars', comp, loose); + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[STAR], ''); +} + +// This function is passed to string.replace(re[HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) { + + if (isX(fM)) + from = ''; + else if (isX(fm)) + from = '>=' + fM + '.0.0'; + else if (isX(fp)) + from = '>=' + fM + '.' + fm + '.0'; + else + from = '>=' + from; + + if (isX(tM)) + to = ''; + else if (isX(tm)) + to = '<' + (+tM + 1) + '.0.0'; + else if (isX(tp)) + to = '<' + tM + '.' + (+tm + 1) + '.0'; + else if (tpr) + to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr; + else + to = '<=' + to; + + return (from + ' ' + to).trim(); +} + + +// if ANY of the sets match ALL of its comparators, then pass +Range.prototype.test = function(version) { + if (!version) + return false; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + for (var i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version)) + return true; + } + return false; +}; + +function testSet(set, version) { + for (var i = 0; i < set.length; i++) { + if (!set[i].test(version)) + return false; + } + + if (version.prerelease.length) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (var i = 0; i < set.length; i++) { + debug(set[i].semver); + if (set[i].semver === ANY) + return true; + + if (set[i].semver.prerelease.length > 0) { + var allowed = set[i].semver; + if (allowed.major === version.major && + allowed.minor === version.minor && + allowed.patch === version.patch) + return true; + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false; + } + + return true; +} + +exports.satisfies = satisfies; +function satisfies(version, range, loose) { + try { + range = new Range(range, loose); + } catch (er) { + return false; + } + return range.test(version); +} + +exports.maxSatisfying = maxSatisfying; +function maxSatisfying(versions, range, loose) { + return versions.filter(function(version) { + return satisfies(version, range, loose); + }).sort(function(a, b) { + return rcompare(a, b, loose); + })[0] || null; +} + +exports.validRange = validRange; +function validRange(range, loose) { + try { + // Return '*' instead of '' so that truthiness works. + // This will throw if it's invalid anyway + return new Range(range, loose).range || '*'; + } catch (er) { + return null; + } +} + +// Determine if version is less than all the versions possible in the range +exports.ltr = ltr; +function ltr(version, range, loose) { + return outside(version, range, '<', loose); +} + +// Determine if version is greater than all the versions possible in the range. +exports.gtr = gtr; +function gtr(version, range, loose) { + return outside(version, range, '>', loose); +} + +exports.outside = outside; +function outside(version, range, hilo, loose) { + version = new SemVer(version, loose); + range = new Range(range, loose); + + var gtfn, ltefn, ltfn, comp, ecomp; + switch (hilo) { + case '>': + gtfn = gt; + ltefn = lte; + ltfn = lt; + comp = '>'; + ecomp = '>='; + break; + case '<': + gtfn = lt; + ltefn = gte; + ltfn = gt; + comp = '<'; + ecomp = '<='; + break; + default: + throw new TypeError('Must provide a hilo val of "<" or ">"'); + } + + // If it satisifes the range it is not outside + if (satisfies(version, range, loose)) { + return false; + } + + // From now on, variable terms are as if we're in "gtr" mode. + // but note that everything is flipped for the "ltr" function. + + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i]; + + var high = null; + var low = null; + + comparators.forEach(function(comparator) { + high = high || comparator; + low = low || comparator; + if (gtfn(comparator.semver, high.semver, loose)) { + high = comparator; + } else if (ltfn(comparator.semver, low.semver, loose)) { + low = comparator; + } + }); + + // If the edge version comparator has a operator then our version + // isn't outside it + if (high.operator === comp || high.operator === ecomp) { + return false; + } + + // If the lowest version comparator has an operator and our version + // is less than it then it isn't higher than the range + if ((!low.operator || low.operator === comp) && + ltefn(version, low.semver)) { + return false; + } else if (low.operator === ecomp && ltfn(version, low.semver)) { + return false; + } + } + return true; +} + +// Use the define() function if we're in AMD land +if (typeof define === 'function' && define.amd) + define(exports); + +// ***** MODIFIED LINE BELOW ***** +})(); \ No newline at end of file diff --git a/vendor/assets/javascripts/jquery-tablesorter/extras/semver.js b/vendor/assets/javascripts/jquery-tablesorter/extras/semver.js new file mode 100644 index 0000000..b95992b --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/extras/semver.js @@ -0,0 +1,1202 @@ +// export the class if we are in a Node-like system. +if (typeof module === 'object' && module.exports === exports) + exports = module.exports = SemVer; + +// The debug function is excluded entirely from the minified version. +/* nomin */ var debug; +/* nomin */ if (typeof process === 'object' && + /* nomin */ process.env && + /* nomin */ process.env.NODE_DEBUG && + /* nomin */ /\bsemver\b/i.test(process.env.NODE_DEBUG)) + /* nomin */ debug = function() { + /* nomin */ var args = Array.prototype.slice.call(arguments, 0); + /* nomin */ args.unshift('SEMVER'); + /* nomin */ console.log.apply(console, args); + /* nomin */ }; +/* nomin */ else + /* nomin */ debug = function() {}; + +// Note: this is the semver.org version of the spec that it implements +// Not necessarily the package version of this code. +exports.SEMVER_SPEC_VERSION = '2.0.0'; + +var MAX_LENGTH = 256; +var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; + +// The actual regexps go on exports.re +var re = exports.re = []; +var src = exports.src = []; +var R = 0; + +// The following Regular Expressions can be used for tokenizing, +// validating, and parsing SemVer version strings. + +// ## Numeric Identifier +// A single `0`, or a non-zero digit followed by zero or more digits. + +var NUMERICIDENTIFIER = R++; +src[NUMERICIDENTIFIER] = '0|[1-9]\\d*'; +var NUMERICIDENTIFIERLOOSE = R++; +src[NUMERICIDENTIFIERLOOSE] = '[0-9]+'; + + +// ## Non-numeric Identifier +// Zero or more digits, followed by a letter or hyphen, and then zero or +// more letters, digits, or hyphens. + +var NONNUMERICIDENTIFIER = R++; +src[NONNUMERICIDENTIFIER] = '\\d*[a-zA-Z-][a-zA-Z0-9-]*'; + + +// ## Main Version +// Three dot-separated numeric identifiers. + +var MAINVERSION = R++; +src[MAINVERSION] = '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')\\.' + + '(' + src[NUMERICIDENTIFIER] + ')'; + +var MAINVERSIONLOOSE = R++; +src[MAINVERSIONLOOSE] = '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')\\.' + + '(' + src[NUMERICIDENTIFIERLOOSE] + ')'; + +// ## Pre-release Version Identifier +// A numeric identifier, or a non-numeric identifier. + +var PRERELEASEIDENTIFIER = R++; +src[PRERELEASEIDENTIFIER] = '(?:' + src[NUMERICIDENTIFIER] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + +var PRERELEASEIDENTIFIERLOOSE = R++; +src[PRERELEASEIDENTIFIERLOOSE] = '(?:' + src[NUMERICIDENTIFIERLOOSE] + + '|' + src[NONNUMERICIDENTIFIER] + ')'; + + +// ## Pre-release Version +// Hyphen, followed by one or more dot-separated pre-release version +// identifiers. + +var PRERELEASE = R++; +src[PRERELEASE] = '(?:-(' + src[PRERELEASEIDENTIFIER] + + '(?:\\.' + src[PRERELEASEIDENTIFIER] + ')*))'; + +var PRERELEASELOOSE = R++; +src[PRERELEASELOOSE] = '(?:-?(' + src[PRERELEASEIDENTIFIERLOOSE] + + '(?:\\.' + src[PRERELEASEIDENTIFIERLOOSE] + ')*))'; + +// ## Build Metadata Identifier +// Any combination of digits, letters, or hyphens. + +var BUILDIDENTIFIER = R++; +src[BUILDIDENTIFIER] = '[0-9A-Za-z-]+'; + +// ## Build Metadata +// Plus sign, followed by one or more period-separated build metadata +// identifiers. + +var BUILD = R++; +src[BUILD] = '(?:\\+(' + src[BUILDIDENTIFIER] + + '(?:\\.' + src[BUILDIDENTIFIER] + ')*))'; + + +// ## Full Version String +// A main version, followed optionally by a pre-release version and +// build metadata. + +// Note that the only major, minor, patch, and pre-release sections of +// the version string are capturing groups. The build metadata is not a +// capturing group, because it should not ever be used in version +// comparison. + +var FULL = R++; +var FULLPLAIN = 'v?' + src[MAINVERSION] + + src[PRERELEASE] + '?' + + src[BUILD] + '?'; + +src[FULL] = '^' + FULLPLAIN + '$'; + +// like full, but allows v1.2.3 and =1.2.3, which people do sometimes. +// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty +// common in the npm registry. +var LOOSEPLAIN = '[v=\\s]*' + src[MAINVERSIONLOOSE] + + src[PRERELEASELOOSE] + '?' + + src[BUILD] + '?'; + +var LOOSE = R++; +src[LOOSE] = '^' + LOOSEPLAIN + '$'; + +var GTLT = R++; +src[GTLT] = '((?:<|>)?=?)'; + +// Something like "2.*" or "1.2.x". +// Note that "x.x" is a valid xRange identifer, meaning "any version" +// Only the first item is strictly required. +var XRANGEIDENTIFIERLOOSE = R++; +src[XRANGEIDENTIFIERLOOSE] = src[NUMERICIDENTIFIERLOOSE] + '|x|X|\\*'; +var XRANGEIDENTIFIER = R++; +src[XRANGEIDENTIFIER] = src[NUMERICIDENTIFIER] + '|x|X|\\*'; + +var XRANGEPLAIN = R++; +src[XRANGEPLAIN] = '[v=\\s]*(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIER] + ')' + + '(?:' + src[PRERELEASE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGEPLAINLOOSE = R++; +src[XRANGEPLAINLOOSE] = '[v=\\s]*(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:\\.(' + src[XRANGEIDENTIFIERLOOSE] + ')' + + '(?:' + src[PRERELEASELOOSE] + ')?' + + src[BUILD] + '?' + + ')?)?'; + +var XRANGE = R++; +src[XRANGE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAIN] + '$'; +var XRANGELOOSE = R++; +src[XRANGELOOSE] = '^' + src[GTLT] + '\\s*' + src[XRANGEPLAINLOOSE] + '$'; + +// Tilde ranges. +// Meaning is "reasonably at or greater than" +var LONETILDE = R++; +src[LONETILDE] = '(?:~>?)'; + +var TILDETRIM = R++; +src[TILDETRIM] = '(\\s*)' + src[LONETILDE] + '\\s+'; +re[TILDETRIM] = new RegExp(src[TILDETRIM], 'g'); +var tildeTrimReplace = '$1~'; + +var TILDE = R++; +src[TILDE] = '^' + src[LONETILDE] + src[XRANGEPLAIN] + '$'; +var TILDELOOSE = R++; +src[TILDELOOSE] = '^' + src[LONETILDE] + src[XRANGEPLAINLOOSE] + '$'; + +// Caret ranges. +// Meaning is "at least and backwards compatible with" +var LONECARET = R++; +src[LONECARET] = '(?:\\^)'; + +var CARETTRIM = R++; +src[CARETTRIM] = '(\\s*)' + src[LONECARET] + '\\s+'; +re[CARETTRIM] = new RegExp(src[CARETTRIM], 'g'); +var caretTrimReplace = '$1^'; + +var CARET = R++; +src[CARET] = '^' + src[LONECARET] + src[XRANGEPLAIN] + '$'; +var CARETLOOSE = R++; +src[CARETLOOSE] = '^' + src[LONECARET] + src[XRANGEPLAINLOOSE] + '$'; + +// A simple gt/lt/eq thing, or just "" to indicate "any version" +var COMPARATORLOOSE = R++; +src[COMPARATORLOOSE] = '^' + src[GTLT] + '\\s*(' + LOOSEPLAIN + ')$|^$'; +var COMPARATOR = R++; +src[COMPARATOR] = '^' + src[GTLT] + '\\s*(' + FULLPLAIN + ')$|^$'; + + +// An expression to strip any whitespace between the gtlt and the thing +// it modifies, so that `> 1.2.3` ==> `>1.2.3` +var COMPARATORTRIM = R++; +src[COMPARATORTRIM] = '(\\s*)' + src[GTLT] + + '\\s*(' + LOOSEPLAIN + '|' + src[XRANGEPLAIN] + ')'; + +// this one has to use the /g flag +re[COMPARATORTRIM] = new RegExp(src[COMPARATORTRIM], 'g'); +var comparatorTrimReplace = '$1$2$3'; + + +// Something like `1.2.3 - 1.2.4` +// Note that these all use the loose form, because they'll be +// checked against either the strict or loose comparator form +// later. +var HYPHENRANGE = R++; +src[HYPHENRANGE] = '^\\s*(' + src[XRANGEPLAIN] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAIN] + ')' + + '\\s*$'; + +var HYPHENRANGELOOSE = R++; +src[HYPHENRANGELOOSE] = '^\\s*(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s+-\\s+' + + '(' + src[XRANGEPLAINLOOSE] + ')' + + '\\s*$'; + +// Star ranges basically just allow anything at all. +var STAR = R++; +src[STAR] = '(<|>)?=?\\s*\\*'; + +// Compile to actual regexp objects. +// All are flag-free, unless they were created above with a flag. +for (var i = 0; i < R; i++) { + debug(i, src[i]); + if (!re[i]) + re[i] = new RegExp(src[i]); +} + +exports.parse = parse; +function parse(version, loose) { + if (version instanceof SemVer) + return version; + + if (typeof version !== 'string') + return null; + + if (version.length > MAX_LENGTH) + return null; + + var r = loose ? re[LOOSE] : re[FULL]; + if (!r.test(version)) + return null; + + try { + return new SemVer(version, loose); + } catch (er) { + return null; + } +} + +exports.valid = valid; +function valid(version, loose) { + var v = parse(version, loose); + return v ? v.version : null; +} + + +exports.clean = clean; +function clean(version, loose) { + var s = parse(version.trim().replace(/^[=v]+/, ''), loose); + return s ? s.version : null; +} + +exports.SemVer = SemVer; + +function SemVer(version, loose) { + if (version instanceof SemVer) { + if (version.loose === loose) + return version; + else + version = version.version; + } else if (typeof version !== 'string') { + throw new TypeError('Invalid Version: ' + version); + } + + if (version.length > MAX_LENGTH) + throw new TypeError('version is longer than ' + MAX_LENGTH + ' characters') + + if (!(this instanceof SemVer)) + return new SemVer(version, loose); + + debug('SemVer', version, loose); + this.loose = loose; + var m = version.trim().match(loose ? re[LOOSE] : re[FULL]); + + if (!m) + throw new TypeError('Invalid Version: ' + version); + + this.raw = version; + + // these are actually numbers + this.major = +m[1]; + this.minor = +m[2]; + this.patch = +m[3]; + + if (this.major > MAX_SAFE_INTEGER || this.major < 0) + throw new TypeError('Invalid major version') + + if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) + throw new TypeError('Invalid minor version') + + if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) + throw new TypeError('Invalid patch version') + + // numberify any prerelease numeric ids + if (!m[4]) + this.prerelease = []; + else + this.prerelease = m[4].split('.').map(function(id) { + if (/^[0-9]+$/.test(id)) { + var num = +id + if (num >= 0 && num < MAX_SAFE_INTEGER) + return num + } + return id; + }); + + this.build = m[5] ? m[5].split('.') : []; + this.format(); +} + +SemVer.prototype.format = function() { + this.version = this.major + '.' + this.minor + '.' + this.patch; + if (this.prerelease.length) + this.version += '-' + this.prerelease.join('.'); + return this.version; +}; + +SemVer.prototype.inspect = function() { + return '<SemVer "' + this + '">'; +}; + +SemVer.prototype.toString = function() { + return this.version; +}; + +SemVer.prototype.compare = function(other) { + debug('SemVer.compare', this.version, this.loose, other); + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return this.compareMain(other) || this.comparePre(other); +}; + +SemVer.prototype.compareMain = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + return compareIdentifiers(this.major, other.major) || + compareIdentifiers(this.minor, other.minor) || + compareIdentifiers(this.patch, other.patch); +}; + +SemVer.prototype.comparePre = function(other) { + if (!(other instanceof SemVer)) + other = new SemVer(other, this.loose); + + // NOT having a prerelease is > having one + if (this.prerelease.length && !other.prerelease.length) + return -1; + else if (!this.prerelease.length && other.prerelease.length) + return 1; + else if (!this.prerelease.length && !other.prerelease.length) + return 0; + + var i = 0; + do { + var a = this.prerelease[i]; + var b = other.prerelease[i]; + debug('prerelease compare', i, a, b); + if (a === undefined && b === undefined) + return 0; + else if (b === undefined) + return 1; + else if (a === undefined) + return -1; + else if (a === b) + continue; + else + return compareIdentifiers(a, b); + } while (++i); +}; + +// preminor will bump the version up to the next minor release, and immediately +// down to pre-release. premajor and prepatch work the same way. +SemVer.prototype.inc = function(release, identifier) { + switch (release) { + case 'premajor': + this.prerelease.length = 0; + this.patch = 0; + this.minor = 0; + this.major++; + this.inc('pre', identifier); + break; + case 'preminor': + this.prerelease.length = 0; + this.patch = 0; + this.minor++; + this.inc('pre', identifier); + break; + case 'prepatch': + // If this is already a prerelease, it will bump to the next version + // drop any prereleases that might already exist, since they are not + // relevant at this point. + this.prerelease.length = 0; + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + // If the input is a non-prerelease version, this acts the same as + // prepatch. + case 'prerelease': + if (this.prerelease.length === 0) + this.inc('patch', identifier); + this.inc('pre', identifier); + break; + + case 'major': + // If this is a pre-major version, bump up to the same major version. + // Otherwise increment major. + // 1.0.0-5 bumps to 1.0.0 + // 1.1.0 bumps to 2.0.0 + if (this.minor !== 0 || this.patch !== 0 || this.prerelease.length === 0) + this.major++; + this.minor = 0; + this.patch = 0; + this.prerelease = []; + break; + case 'minor': + // If this is a pre-minor version, bump up to the same minor version. + // Otherwise increment minor. + // 1.2.0-5 bumps to 1.2.0 + // 1.2.1 bumps to 1.3.0 + if (this.patch !== 0 || this.prerelease.length === 0) + this.minor++; + this.patch = 0; + this.prerelease = []; + break; + case 'patch': + // If this is not a pre-release version, it will increment the patch. + // If it is a pre-release it will bump up to the same patch version. + // 1.2.0-5 patches to 1.2.0 + // 1.2.0 patches to 1.2.1 + if (this.prerelease.length === 0) + this.patch++; + this.prerelease = []; + break; + // This probably shouldn't be used publicly. + // 1.0.0 "pre" would become 1.0.0-0 which is the wrong direction. + case 'pre': + if (this.prerelease.length === 0) + this.prerelease = [0]; + else { + var i = this.prerelease.length; + while (--i >= 0) { + if (typeof this.prerelease[i] === 'number') { + this.prerelease[i]++; + i = -2; + } + } + if (i === -1) // didn't increment anything + this.prerelease.push(0); + } + if (identifier) { + // 1.2.0-beta.1 bumps to 1.2.0-beta.2, + // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0 + if (this.prerelease[0] === identifier) { + if (isNaN(this.prerelease[1])) + this.prerelease = [identifier, 0]; + } else + this.prerelease = [identifier, 0]; + } + break; + + default: + throw new Error('invalid increment argument: ' + release); + } + this.format(); + return this; +}; + +exports.inc = inc; +function inc(version, release, loose, identifier) { + if (typeof(loose) === 'string') { + identifier = loose; + loose = undefined; + } + + try { + return new SemVer(version, loose).inc(release, identifier).version; + } catch (er) { + return null; + } +} + +exports.diff = diff; +function diff(version1, version2) { + if (eq(version1, version2)) { + return null; + } else { + var v1 = parse(version1); + var v2 = parse(version2); + if (v1.prerelease.length || v2.prerelease.length) { + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return 'pre'+key; + } + } + } + return 'prerelease'; + } + for (var key in v1) { + if (key === 'major' || key === 'minor' || key === 'patch') { + if (v1[key] !== v2[key]) { + return key; + } + } + } + } +} + +exports.compareIdentifiers = compareIdentifiers; + +var numeric = /^[0-9]+$/; +function compareIdentifiers(a, b) { + var anum = numeric.test(a); + var bnum = numeric.test(b); + + if (anum && bnum) { + a = +a; + b = +b; + } + + return (anum && !bnum) ? -1 : + (bnum && !anum) ? 1 : + a < b ? -1 : + a > b ? 1 : + 0; +} + +exports.rcompareIdentifiers = rcompareIdentifiers; +function rcompareIdentifiers(a, b) { + return compareIdentifiers(b, a); +} + +exports.major = major; +function major(a, loose) { + return new SemVer(a, loose).major; +} + +exports.minor = minor; +function minor(a, loose) { + return new SemVer(a, loose).minor; +} + +exports.patch = patch; +function patch(a, loose) { + return new SemVer(a, loose).patch; +} + +exports.compare = compare; +function compare(a, b, loose) { + return new SemVer(a, loose).compare(b); +} + +exports.compareLoose = compareLoose; +function compareLoose(a, b) { + return compare(a, b, true); +} + +exports.rcompare = rcompare; +function rcompare(a, b, loose) { + return compare(b, a, loose); +} + +exports.sort = sort; +function sort(list, loose) { + return list.sort(function(a, b) { + return exports.compare(a, b, loose); + }); +} + +exports.rsort = rsort; +function rsort(list, loose) { + return list.sort(function(a, b) { + return exports.rcompare(a, b, loose); + }); +} + +exports.gt = gt; +function gt(a, b, loose) { + return compare(a, b, loose) > 0; +} + +exports.lt = lt; +function lt(a, b, loose) { + return compare(a, b, loose) < 0; +} + +exports.eq = eq; +function eq(a, b, loose) { + return compare(a, b, loose) === 0; +} + +exports.neq = neq; +function neq(a, b, loose) { + return compare(a, b, loose) !== 0; +} + +exports.gte = gte; +function gte(a, b, loose) { + return compare(a, b, loose) >= 0; +} + +exports.lte = lte; +function lte(a, b, loose) { + return compare(a, b, loose) <= 0; +} + +exports.cmp = cmp; +function cmp(a, op, b, loose) { + var ret; + switch (op) { + case '===': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a === b; + break; + case '!==': + if (typeof a === 'object') a = a.version; + if (typeof b === 'object') b = b.version; + ret = a !== b; + break; + case '': case '=': case '==': ret = eq(a, b, loose); break; + case '!=': ret = neq(a, b, loose); break; + case '>': ret = gt(a, b, loose); break; + case '>=': ret = gte(a, b, loose); break; + case '<': ret = lt(a, b, loose); break; + case '<=': ret = lte(a, b, loose); break; + default: throw new TypeError('Invalid operator: ' + op); + } + return ret; +} + +exports.Comparator = Comparator; +function Comparator(comp, loose) { + if (comp instanceof Comparator) { + if (comp.loose === loose) + return comp; + else + comp = comp.value; + } + + if (!(this instanceof Comparator)) + return new Comparator(comp, loose); + + debug('comparator', comp, loose); + this.loose = loose; + this.parse(comp); + + if (this.semver === ANY) + this.value = ''; + else + this.value = this.operator + this.semver.version; + + debug('comp', this); +} + +var ANY = {}; +Comparator.prototype.parse = function(comp) { + var r = this.loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var m = comp.match(r); + + if (!m) + throw new TypeError('Invalid comparator: ' + comp); + + this.operator = m[1]; + if (this.operator === '=') + this.operator = ''; + + // if it literally is just '>' or '' then allow anything. + if (!m[2]) + this.semver = ANY; + else + this.semver = new SemVer(m[2], this.loose); +}; + +Comparator.prototype.inspect = function() { + return '<SemVer Comparator "' + this + '">'; +}; + +Comparator.prototype.toString = function() { + return this.value; +}; + +Comparator.prototype.test = function(version) { + debug('Comparator.test', version, this.loose); + + if (this.semver === ANY) + return true; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + return cmp(version, this.operator, this.semver, this.loose); +}; + + +exports.Range = Range; +function Range(range, loose) { + if ((range instanceof Range) && range.loose === loose) + return range; + + if (!(this instanceof Range)) + return new Range(range, loose); + + this.loose = loose; + + // First, split based on boolean or || + this.raw = range; + this.set = range.split(/\s*\|\|\s*/).map(function(range) { + return this.parseRange(range.trim()); + }, this).filter(function(c) { + // throw out any that are not relevant for whatever reason + return c.length; + }); + + if (!this.set.length) { + throw new TypeError('Invalid SemVer Range: ' + range); + } + + this.format(); +} + +Range.prototype.inspect = function() { + return '<SemVer Range "' + this.range + '">'; +}; + +Range.prototype.format = function() { + this.range = this.set.map(function(comps) { + return comps.join(' ').trim(); + }).join('||').trim(); + return this.range; +}; + +Range.prototype.toString = function() { + return this.range; +}; + +Range.prototype.parseRange = function(range) { + var loose = this.loose; + range = range.trim(); + debug('range', range, loose); + // `1.2.3 - 1.2.4` => `>=1.2.3 <=1.2.4` + var hr = loose ? re[HYPHENRANGELOOSE] : re[HYPHENRANGE]; + range = range.replace(hr, hyphenReplace); + debug('hyphen replace', range); + // `> 1.2.3 < 1.2.5` => `>1.2.3 <1.2.5` + range = range.replace(re[COMPARATORTRIM], comparatorTrimReplace); + debug('comparator trim', range, re[COMPARATORTRIM]); + + // `~ 1.2.3` => `~1.2.3` + range = range.replace(re[TILDETRIM], tildeTrimReplace); + + // `^ 1.2.3` => `^1.2.3` + range = range.replace(re[CARETTRIM], caretTrimReplace); + + // normalize spaces + range = range.split(/\s+/).join(' '); + + // At this point, the range is completely trimmed and + // ready to be split into comparators. + + var compRe = loose ? re[COMPARATORLOOSE] : re[COMPARATOR]; + var set = range.split(' ').map(function(comp) { + return parseComparator(comp, loose); + }).join(' ').split(/\s+/); + if (this.loose) { + // in loose mode, throw out any that are not valid comparators + set = set.filter(function(comp) { + return !!comp.match(compRe); + }); + } + set = set.map(function(comp) { + return new Comparator(comp, loose); + }); + + return set; +}; + +// Mostly just for testing and legacy API reasons +exports.toComparators = toComparators; +function toComparators(range, loose) { + return new Range(range, loose).set.map(function(comp) { + return comp.map(function(c) { + return c.value; + }).join(' ').trim().split(' '); + }); +} + +// comprised of xranges, tildes, stars, and gtlt's at this point. +// already replaced the hyphen ranges +// turn into a set of JUST comparators. +function parseComparator(comp, loose) { + debug('comp', comp); + comp = replaceCarets(comp, loose); + debug('caret', comp); + comp = replaceTildes(comp, loose); + debug('tildes', comp); + comp = replaceXRanges(comp, loose); + debug('xrange', comp); + comp = replaceStars(comp, loose); + debug('stars', comp); + return comp; +} + +function isX(id) { + return !id || id.toLowerCase() === 'x' || id === '*'; +} + +// ~, ~> --> * (any, kinda silly) +// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0 <3.0.0 +// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0 <2.1.0 +// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0 <1.3.0 +// ~1.2.3, ~>1.2.3 --> >=1.2.3 <1.3.0 +// ~1.2.0, ~>1.2.0 --> >=1.2.0 <1.3.0 +function replaceTildes(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceTilde(comp, loose); + }).join(' '); +} + +function replaceTilde(comp, loose) { + var r = loose ? re[TILDELOOSE] : re[TILDE]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('tilde', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) + // ~1.2 == >=1.2.0- <1.3.0- + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else if (pr) { + debug('replaceTilde pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + // ~1.2.3 == >=1.2.3 <1.3.0 + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + + debug('tilde return', ret); + return ret; + }); +} + +// ^ --> * (any, kinda silly) +// ^2, ^2.x, ^2.x.x --> >=2.0.0 <3.0.0 +// ^2.0, ^2.0.x --> >=2.0.0 <3.0.0 +// ^1.2, ^1.2.x --> >=1.2.0 <2.0.0 +// ^1.2.3 --> >=1.2.3 <2.0.0 +// ^1.2.0 --> >=1.2.0 <2.0.0 +function replaceCarets(comp, loose) { + return comp.trim().split(/\s+/).map(function(comp) { + return replaceCaret(comp, loose); + }).join(' '); +} + +function replaceCaret(comp, loose) { + debug('caret', comp, loose); + var r = loose ? re[CARETLOOSE] : re[CARET]; + return comp.replace(r, function(_, M, m, p, pr) { + debug('caret', comp, _, M, m, p, pr); + var ret; + + if (isX(M)) + ret = ''; + else if (isX(m)) + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + else if (isX(p)) { + if (M === '0') + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + else + ret = '>=' + M + '.' + m + '.0 <' + (+M + 1) + '.0.0'; + } else if (pr) { + debug('replaceCaret pr', pr); + if (pr.charAt(0) !== '-') + pr = '-' + pr; + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + pr + + ' <' + (+M + 1) + '.0.0'; + } else { + debug('no pr'); + if (M === '0') { + if (m === '0') + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + m + '.' + (+p + 1); + else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + M + '.' + (+m + 1) + '.0'; + } else + ret = '>=' + M + '.' + m + '.' + p + + ' <' + (+M + 1) + '.0.0'; + } + + debug('caret return', ret); + return ret; + }); +} + +function replaceXRanges(comp, loose) { + debug('replaceXRanges', comp, loose); + return comp.split(/\s+/).map(function(comp) { + return replaceXRange(comp, loose); + }).join(' '); +} + +function replaceXRange(comp, loose) { + comp = comp.trim(); + var r = loose ? re[XRANGELOOSE] : re[XRANGE]; + return comp.replace(r, function(ret, gtlt, M, m, p, pr) { + debug('xRange', comp, ret, gtlt, M, m, p, pr); + var xM = isX(M); + var xm = xM || isX(m); + var xp = xm || isX(p); + var anyX = xp; + + if (gtlt === '=' && anyX) + gtlt = ''; + + if (xM) { + if (gtlt === '>' || gtlt === '<') { + // nothing is allowed + ret = '<0.0.0'; + } else { + // nothing is forbidden + ret = '*'; + } + } else if (gtlt && anyX) { + // replace X with 0 + if (xm) + m = 0; + if (xp) + p = 0; + + if (gtlt === '>') { + // >1 => >=2.0.0 + // >1.2 => >=1.3.0 + // >1.2.3 => >= 1.2.4 + gtlt = '>='; + if (xm) { + M = +M + 1; + m = 0; + p = 0; + } else if (xp) { + m = +m + 1; + p = 0; + } + } else if (gtlt === '<=') { + // <=0.7.x is actually <0.8.0, since any 0.7.x should + // pass. Similarly, <=7.x is actually <8.0.0, etc. + gtlt = '<' + if (xm) + M = +M + 1 + else + m = +m + 1 + } + + ret = gtlt + M + '.' + m + '.' + p; + } else if (xm) { + ret = '>=' + M + '.0.0 <' + (+M + 1) + '.0.0'; + } else if (xp) { + ret = '>=' + M + '.' + m + '.0 <' + M + '.' + (+m + 1) + '.0'; + } + + debug('xRange return', ret); + + return ret; + }); +} + +// Because * is AND-ed with everything else in the comparator, +// and '' means "any version", just remove the *s entirely. +function replaceStars(comp, loose) { + debug('replaceStars', comp, loose); + // Looseness is ignored here. star is always as loose as it gets! + return comp.trim().replace(re[STAR], ''); +} + +// This function is passed to string.replace(re[HYPHENRANGE]) +// M, m, patch, prerelease, build +// 1.2 - 3.4.5 => >=1.2.0 <=3.4.5 +// 1.2.3 - 3.4 => >=1.2.0 <3.5.0 Any 3.4.x will do +// 1.2 - 3.4 => >=1.2.0 <3.5.0 +function hyphenReplace($0, + from, fM, fm, fp, fpr, fb, + to, tM, tm, tp, tpr, tb) { + + if (isX(fM)) + from = ''; + else if (isX(fm)) + from = '>=' + fM + '.0.0'; + else if (isX(fp)) + from = '>=' + fM + '.' + fm + '.0'; + else + from = '>=' + from; + + if (isX(tM)) + to = ''; + else if (isX(tm)) + to = '<' + (+tM + 1) + '.0.0'; + else if (isX(tp)) + to = '<' + tM + '.' + (+tm + 1) + '.0'; + else if (tpr) + to = '<=' + tM + '.' + tm + '.' + tp + '-' + tpr; + else + to = '<=' + to; + + return (from + ' ' + to).trim(); +} + + +// if ANY of the sets match ALL of its comparators, then pass +Range.prototype.test = function(version) { + if (!version) + return false; + + if (typeof version === 'string') + version = new SemVer(version, this.loose); + + for (var i = 0; i < this.set.length; i++) { + if (testSet(this.set[i], version)) + return true; + } + return false; +}; + +function testSet(set, version) { + for (var i = 0; i < set.length; i++) { + if (!set[i].test(version)) + return false; + } + + if (version.prerelease.length) { + // Find the set of versions that are allowed to have prereleases + // For example, ^1.2.3-pr.1 desugars to >=1.2.3-pr.1 <2.0.0 + // That should allow `1.2.3-pr.2` to pass. + // However, `1.2.4-alpha.notready` should NOT be allowed, + // even though it's within the range set by the comparators. + for (var i = 0; i < set.length; i++) { + debug(set[i].semver); + if (set[i].semver === ANY) + return true; + + if (set[i].semver.prerelease.length > 0) { + var allowed = set[i].semver; + if (allowed.major === version.major && + allowed.minor === version.minor && + allowed.patch === version.patch) + return true; + } + } + + // Version has a -pre, but it's not one of the ones we like. + return false; + } + + return true; +} + +exports.satisfies = satisfies; +function satisfies(version, range, loose) { + try { + range = new Range(range, loose); + } catch (er) { + return false; + } + return range.test(version); +} + +exports.maxSatisfying = maxSatisfying; +function maxSatisfying(versions, range, loose) { + return versions.filter(function(version) { + return satisfies(version, range, loose); + }).sort(function(a, b) { + return rcompare(a, b, loose); + })[0] || null; +} + +exports.validRange = validRange; +function validRange(range, loose) { + try { + // Return '*' instead of '' so that truthiness works. + // This will throw if it's invalid anyway + return new Range(range, loose).range || '*'; + } catch (er) { + return null; + } +} + +// Determine if version is less than all the versions possible in the range +exports.ltr = ltr; +function ltr(version, range, loose) { + return outside(version, range, '<', loose); +} + +// Determine if version is greater than all the versions possible in the range. +exports.gtr = gtr; +function gtr(version, range, loose) { + return outside(version, range, '>', loose); +} + +exports.outside = outside; +function outside(version, range, hilo, loose) { + version = new SemVer(version, loose); + range = new Range(range, loose); + + var gtfn, ltefn, ltfn, comp, ecomp; + switch (hilo) { + case '>': + gtfn = gt; + ltefn = lte; + ltfn = lt; + comp = '>'; + ecomp = '>='; + break; + case '<': + gtfn = lt; + ltefn = gte; + ltfn = gt; + comp = '<'; + ecomp = '<='; + break; + default: + throw new TypeError('Must provide a hilo val of "<" or ">"'); + } + + // If it satisifes the range it is not outside + if (satisfies(version, range, loose)) { + return false; + } + + // From now on, variable terms are as if we're in "gtr" mode. + // but note that everything is flipped for the "ltr" function. + + for (var i = 0; i < range.set.length; ++i) { + var comparators = range.set[i]; + + var high = null; + var low = null; + + comparators.forEach(function(comparator) { + high = high || comparator; + low = low || comparator; + if (gtfn(comparator.semver, high.semver, loose)) { + high = comparator; + } else if (ltfn(comparator.semver, low.semver, loose)) { + low = comparator; + } + }); + + // If the edge version comparator has a operator then our version + // isn't outside it + if (high.operator === comp || high.operator === ecomp) { + return false; + } + + // If the lowest version comparator has an operator and our version + // is less than it then it isn't higher than the range + if ((!low.operator || low.operator === comp) && + ltefn(version, low.semver)) { + return false; + } else if (low.operator === ecomp && ltfn(version, low.semver)) { + return false; + } + } + return true; +} + +// Use the define() function if we're in AMD land +if (typeof define === 'function' && define.amd) + define(exports); diff --git a/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.combined.js b/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.combined.js new file mode 100644 index 0000000..6f910e1 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/jquery.tablesorter.combined.js @@ -0,0 +1,6099 @@ +/*** This file is dynamically generated *** +█████▄ ▄████▄ █████▄ ▄████▄ ██████ ███████▄ ▄████▄ █████▄ ██ ██████ ██ ██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ██▄▄██ +██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀▀▀██ +█████▀ ▀████▀ ██ ██ ▀████▀ ██ ██ ██ ██ ▀████▀ █████▀ ██ ██ █████▀ +*/ +/*! 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) { +/*! TableSorter (FORK) v2.31.3 *//* +* Client-side table sorting with ease! +* @requires jQuery v1.2.6+ +* +* Copyright (c) 2007 Christian Bach +* 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 (FORK) +* @cat Plugins/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 */ +;( 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} = <i/> // class from cssIcon + onRenderTemplate : null, // function( index, template ) { return template; }, // template is a string + onRenderHeader : null, // function( index ) {}, // nothing to return + + // *** functionality + cancelSelection : true, // prevent text selection in the header + tabIndex : true, // add tabindex to header for keyboard accessibility + dateFormat : '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'] } + + }, + + // 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' + }, + + // 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; + } + + 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() ); + } + + // 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 : '' ); + } + + // 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 ); + } + } + + // 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 { + 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 ); + } + }) + // $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' ); + } + } + 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+ + }); + } + }, + + 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 ? + '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : + ''; + // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683 + c.$headers = $( $.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; + } + } + $elem.html( '<div class="' + ts.css.headerIn + '">' + template + '</div>' ); // faster than wrapInner + } + if ( c.onRenderHeader ) { + c.onRenderHeader.apply( $elem, [ index, c, c.$table ] ); + } + 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 ); + } + 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--; + } + } + } + colIndex++; + } + } + 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 ( 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; + }, + + 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 + '"' ); + } + } + } 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 ) ); + } + } + // 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 ); + } + // 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++; + } + // 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?' ); + } + 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; + } + cache.colMax = colMax; + // total up rows, not including child rows + c.totalRows += cache.normalized.length; + + } + 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 ]; + } + } + console[ console.table ? 'table' : 'log' ]( val ); + if ( console.groupEnd ) { console.groupEnd(); } + } + if ( $.isFunction( callback ) ) { + callback( table ); + } + }, + + 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; + } + } + } + // return everything + return data; + } + }, + + /* + ██ ██ █████▄ █████▄ ▄████▄ ██████ ██████ + ██ ██ ██▄▄██ ██ ██ ██▄▄██ ██ ██▄▄ + ██ ██ ██▀▀▀ ██ ██ ██▀▀██ ██ ██▀▀ + ▀████▀ ██ █████▀ ██ ██ ██ ██████ + */ + 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 + $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 ) ); + } + }, + + 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 { + 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'); + } + } + }, + + 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 ); + } + }, + + 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 ); + } + } + }, + + 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; + } + } + }, + + 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; + } + } + } else { + icell = $cell.index(); + } + 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; + } + }, + + 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 && /<tr/.test( $row || '' ), + table = c.table; + if ( valid ) { + $row = $( $row ); + c.$tbodies.append( $row ); + } else if ( + !$row || + // row is a jQuery object? + !( $row instanceof $ ) || + // row contained in the table? + ( ts.getClosest( $row, 'table' )[ 0 ] !== c.table ) + ) { + if ( ts.debug(c, 'core') ) { + console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' + + 'been added to the table, or (2) row HTML string to be added to a table with only one tbody' ); + } + return false; + } + table.isUpdating = true; + if ( ts.isEmptyObject( c.cache ) ) { + // empty table, do an update instead - fixes #450 + ts.updateHeader( c ); + ts.commonUpdate( c, resort, callback ); + } else { + rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length; + tbodyIndex = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) ); + // fixes adding rows to an empty table - see issue #179 + if ( !( c.parsers && c.parsers.length ) ) { + ts.setupParsers( c ); + } + // add each row + for ( rowIndex = 0; rowIndex < rows; rowIndex++ ) { + cacheIndex = 0; + len = $row[ rowIndex ].cells.length; + order = c.cache[ tbodyIndex ].normalized.length; + cells = []; + rowData = { + child : [], + raw : [], + $row : $row.eq( rowIndex ), + order : order + }; + // add each cell + for ( cellIndex = 0; cellIndex < len; cellIndex++ ) { + cell = $row[ rowIndex ].cells[ cellIndex ]; + txt = ts.getElementText( c, cell, cacheIndex ); + rowData.raw[ cacheIndex ] = txt; + val = ts.getParsedText( c, cell, cacheIndex, txt ); + cells[ cacheIndex ] = val; + if ( ( c.parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { + // update column max value (ignore sign) + c.cache[ tbodyIndex ].colMax[ cacheIndex ] = + Math.max( Math.abs( val ) || 0, c.cache[ tbodyIndex ].colMax[ cacheIndex ] || 0 ); + } + span = cell.colSpan - 1; + if ( span > 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 ); + } + }, + + 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 ); + } + + 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; + } + } + } + // 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'; + } + } + } + // 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; + } + } + } + // 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; + } + } + } + } 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; + } + } + } + } + } + // 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 ); + }, + + // 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; + } + + // 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; } + } + return a[ c.columns ].order - b[ c.columns ].order; + }); + } + if ( ts.debug(c, 'core') ) { + console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) ); + } + }, + + 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 { + ts.sortReset( c, function() { + ts.resortComplete( c, callback ); + ts.applyWidget( c.table, false ); + } ); + } + } else { + ts.resortComplete( c, callback ); + ts.applyWidget( c.table, false ); + } + }, + + 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 ); + } + }, + + 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 ); + } + }, + + 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; + }, + + 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 num * n; + } + return 0; + }, + + 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; + }, + + 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; + } + } + }, + + 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 ); + } + } + } + }, + + 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' ); + } + } + } + }, + + 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 ); + } + } + if ( !init && typeof widget.format === 'function' ) { + applied = true; + if ( debug ) { + console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' ); + } + widget.format( table, c, wo, false ); + } + if ( debug ) { + if ( applied ) { + console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) ); + if ( console.groupEnd ) { console.groupEnd(); } + } + } + } + }, + + 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!' ); + } + } + // 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 ); + }, + + 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; + }, + + 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; + }, + + 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; + }, + + 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 ( !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 = $( '<colgroup class="' + ts.css.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( $( '<col>' ).css( 'width', percent ) ); + } + c.$table.prepend( $colgroup ); + } + }, + + // 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 ]; + } + } + } + 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 ); + } + }, + + // 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( '<colgroup class="tablesorter-savemyplace"/>' ); + return $.fn.detach ? $tb.detach() : $tb.remove(); + } + var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' ); + $tb.insertAfter( holdr ); + holdr.remove(); + table.isProcessing = false; + }, + + 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' ); + } + } + 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 ); + } + } + } + return str; + }, + + 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' ); + } + } + + }; + + $.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 }; + }; + } + + // add default parsers + ts.addParser({ + id : 'no-parser', + is : function() { + return false; + }, + format : function() { + return ''; + }, + type : 'text' + }); + + ts.addParser({ + id : 'text', + is : function() { + return true; + }, + format : function( str, table ) { + var c = table.config; + if ( str ) { + str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str ); + str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str; + } + return str; + }, + type : 'text' + }); + + ts.regex.nondigit = /[^\w,. \-()]/g; + ts.addParser({ + id : 'digit', + is : function( str ) { + return ts.isDigit( str ); + }, + 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' + }); + + ts.regex.currencyReplace = /[+\-,. ]/g; + ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/; + ts.addParser({ + id : 'currency', + is : function( str ) { + str = ( str || '' ).replace( ts.regex.currencyReplace, '' ); + // test for £$€¤¥¢ + return ts.regex.currencyTest.test( str ); + }, + 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' + }); + + // 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( str ) { + return ts.regex.urlProtocolTest.test( str ); + }, + format : function( str ) { + return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str; + }, + type : 'text' + }); + + ts.regex.dash = /-/g; + ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/; + ts.addParser({ + id : 'isoDate', + is : function( str ) { + return ts.regex.isoDate.test( str ); + }, + 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' + }); + + ts.regex.percent = /%/g; + ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/; + ts.addParser({ + id : 'percent', + is : function( str ) { + return ts.regex.percentTest.test( str ) && str.length < 15; + }, + format : function( str, table ) { + return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str; + }, + type : 'numeric' + }); + + // added image parser to core v2.17.9 + ts.addParser({ + id : 'image', + is : function( str, table, node, $node ) { + return $node.find( 'img' ).length > 0; + }, + format : function( str, table, cell ) { + return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str; + }, + 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 : '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 : '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( 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' + }); + + // 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 : 'time', + is : function( str ) { + return ts.regex.timeTest.test( str ); + }, + 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' + }); + + ts.addParser({ + id : 'metadata', + is : function() { + return false; + }, + format : function( str, table, cell ) { + var c = table.config, + p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName; + return $( cell ).metadata()[ p ]; + }, + type : 'numeric' + }); + + /* + ██████ ██████ █████▄ █████▄ ▄████▄ + ▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██ + ▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██ + ██████ ██████ █████▀ ██ ██ ██ ██ + */ + // add default widgets + ts.addWidget({ + id : 'zebra', + priority : 90, + format : function( table, c, wo ) { + var $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 + 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 ] ); + } + } + }, + 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 ); + +/*! 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: '' + } + }); + + // *** 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 && 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 (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') || {} : {}; + } + } + // 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 { + date = new Date(); + date.setTime(date.getTime() + (31536e+6)); // 365 days + document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; + } + } else { + return values && values[url] ? values[url][id] : ''; + } + }; + +})(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 <i> 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 <i> 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 + } + }; + + $.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('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); + } + }); + if (c.cssIcon) { + // if c.cssIcon is '', then no <i> 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 || ''); + } + } + } + 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); + } + }); + +})(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 + } + } + }); + +})(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' ); + } + } + }); + + 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; + }, + // 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; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; + } + return null; + }, + // 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; + } + 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; + } + return null; + }, + // 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 ); + + tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; + range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; + range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || c.parsers[ index ].type === 'numeric' ) { + result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); + range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; + result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); + range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; + } + if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { + result = data.cache; + } else { + tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + result = ts.formatFloat( tmp, table ); + } + if ( range1 > range2 ) { + tmp = range1; range1 = range2; range2 = tmp; // swap + } + return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); + } + return null; + }, + // Look for wild card: ? = single, * = multiple, or | = logical OR + wild : function( c, data ) { + if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { + var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); + // look for an exact match with the 'or' unless the 'filter-match' class is found + if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { + query = data.isMatch ? query : '^(' + query + ')$'; + } + // parsing the filter may not work properly when using wildcards =/ + try { + return new RegExp( + query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), + c.widgetOptions.filter_ignoreCase ? 'i' : '' + ) + .test( data.exact ); + } catch ( error ) { + return null; + } + } + return null; + }, + // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) + fuzzy: function( c, data ) { + if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { + var indx, + patternIndx = 0, + len = data.iExact.length, + txt = data.iFilter.slice( 1 ), + pattern = tsf.parseFilter( c, txt, data ) || ''; + for ( indx = 0; indx < len; indx++ ) { + if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { + patternIndx += 1; + } + } + return patternIndx === pattern.length; + } + 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 : /</, + lteTest : /<=/, + notTest : /^\!/, + wildOrTest : /[\?\*\|]/, + wildTest : /\?\*/, + fuzzyTest : /^~/, + exactTest : /[=\"\|!]/ + }); + + // don't build filter row if columnFilters is false or all columns are set to 'filter-false' + // see issue #156 + val = c.$headers.filter( '.filter-false, .parser-false' ).length; + if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) { + // build filter row + tsf.buildRow( table, c, wo ); + } + + txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + + 'filterAndSortReset filterResetSaved filterEnd search '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table.bind( txt, function( event, filter ) { + val = wo.filter_hideEmpty && + $.isEmptyObject( c.cache ) && + !( c.delayInit && event.type === 'appendCache' ); + // hide filter row using the 'filtered' class name + c.$table.find( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450 + if ( !/(search|filter)/.test( event.type ) ) { + event.stopPropagation(); + tsf.buildDefault( table, true ); + } + // 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 ); + } + return false; + }); + + // reset button/link + 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 ) { + 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 ( string in fxn ) { + if ( typeof string === 'string' ) { + options += options === '' ? + '<option value="">' + + ( $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || + '' + ) + + '</option>' : ''; + val = string; + txt = string; + if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + val = string.split( wo.filter_selectSourceSeparator ); + txt = val[1]; + val = val[0]; + } + options += '<option ' + + ( txt === val ? '' : 'data-function-name="' + string + '" ' ) + + 'value="' + val + '">' + txt + '</option>'; + } + } + 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. + tsf.buildDefault( table, true ); + + tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); + if ( wo.filter_external ) { + tsf.bindSearch( table, wo.filter_external ); + } + + if ( wo.filter_hideFilters ) { + tsf.hideFilters( c ); + } + + // show processing icon + 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 : '' ); + }); + } + + // 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; + } + }, + 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 = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">'; + 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 += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"'; + } else { + buildFilter += '<td data-column="' + column + '"'; + } + if ( arry ) { + buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' ); + } else { + buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' ); + } + buildFilter += '></td>'; + } + } + c.$filters = $( buildFilter += '</tr>' ) + .appendTo( c.$table.children( 'thead' ).eq( 0 ) ) + .children( 'td' ); + // build each filter input + for ( column = 0; column < columns; column++ ) { + disabled = false; + // assuming last cell of a column is the main column + $header = c.$headerIndexed[ column ]; + if ( $header && $header.length ) { + // $filter = c.$filters.filter( '[data-column="' + column + '"]' ); + $filter = tsf.getColumnElm( c, c.$filters, column ); + ffxn = ts.getColumnData( table, wo.filter_functions, column ); + makeSelect = ( wo.filter_functions && ffxn && typeof ffxn !== 'function' ) || + $header.hasClass( 'filter-select' ); + // get data from jQuery data, metadata, headers option or header class name + col = ts.getColumnData( table, c.headers, column ); + disabled = ts.getData( $header[0], col, 'filter' ) === 'false' || + ts.getData( $header[0], col, 'parser' ) === 'false'; + + if ( makeSelect ) { + buildFilter = $( '<select>' ).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 = $( '<input type="search">' ).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( '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 ); + } + }); + }, + 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; + } + } + 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; + } + // add filter array back into inputs + if ( filterArray ) { + ts.setFilters( table, filters, false, skipFirst !== true ); + if ( !wo.filter_initialized ) { + c.lastSearch = []; + c.lastCombinedFilter = ''; + } + } + 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. <th class="filter-select filter-parsed"> ) + 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( '</option>' ) >= 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 = '<option value="">' + + ( node.data( 'placeholder' ) || + node.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || '' + ) + '</option>', + // 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 += $( '<option>', option )[ 0 ].outerHTML; + options += '<option'; + for ( val in option ) { + if ( option.hasOwnProperty( val ) && val !== 'text' ) { + options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '"' ) + '"'; + } + } + if ( !option.value ) { + options += ' value="' + option.text.replace( tsfRegex.quote, '"' ) + '"'; + } + options += '>' + option.text.replace( tsfRegex.quote, '"' ) + '</option>'; + // above code is needed in jQuery < v1.8 + + // make sure we don't turn an object into a string (objects without a "text" property) + } else if ( '' + option !== '[object Object]' ) { + txt = option = ( '' + option ).replace( tsfRegex.quote, '"' ); + val = txt; + // allow including a symbol in the selectSource array + // 'a-z|A through Z' so that 'a-z' becomes the option value + // and 'A through Z' becomes the option text + if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + t = txt.split( wo.filter_selectSourceSeparator ); + val = t[0]; + txt = t[1]; + } + // replace quotes - fixes #242 & ignore empty strings + // see http://stackoverflow.com/q/14990971/145346 + options += option !== '' ? + '<option ' + + ( val === txt ? '' : 'data-function-name="' + option + '" ' ) + + 'value="' + val + '">' + txt + + '</option>' : ''; + } + } + // clear arry so it doesn't get appended twice + arry = []; + } + + // update all selects in the same column ( clone thead in sticky headers & + // any external selects ) - fixes 473 + $filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) ) + .find( '.' + tscss.filter ); + if ( wo.filter_$externalFilters ) { + $filters = $filters && $filters.length ? + $filters.add( wo.filter_$externalFilters ) : + wo.filter_$externalFilters; + } + $filter = $filters.filter( 'select[data-column="' + column + '"]' ); + + // make sure there is a select there! + if ( $filter.length ) { + $filter[ updating ? 'html' : 'append' ]( options ); + if ( !$.isArray( arry ) ) { + // append options if arry is provided externally as a string or jQuery object + // options ( default value ) was already added + $filter.append( arry ).val( currentValue ); + } + $filter.val( currentValue ); + } + }, + buildDefault: function( table, updating ) { + var columnIndex, $header, noSelect, + c = table.config, + wo = c.widgetOptions, + columns = c.columns; + // build default select dropdown + for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) { + $header = c.$headerIndexed[columnIndex]; + noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); + // look for the filter-select class; build/update it if found + if ( ( $header.hasClass( 'filter-select' ) || + ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) { + tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) ); + } + } + } + }; + + // filter regex variable + tsfRegex = tsf.regex; + + ts.getFilters = function( table, getRaw, setFilters, skipFirst ) { + var i, $filters, $column, cols, + filters = [], + c = table ? $( table )[0].config : '', + wo = c ? c.widgetOptions : ''; + if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) || + // setFilters called, but last search is exactly the same as the current + // fixes issue #733 & #903 where calling update causes the input values to reset + ( $.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) ) + ) { + return $( table ).data( 'lastSearch' ) || []; + } + if ( c ) { + if ( c.$filters ) { + $filters = c.$filters.find( '.' + tscss.filter ); + } + if ( wo.filter_$externalFilters ) { + $filters = $filters && $filters.length ? + $filters.add( wo.filter_$externalFilters ) : + wo.filter_$externalFilters; + } + if ( $filters && $filters.length ) { + filters = setFilters || []; + for ( i = 0; i < c.columns + 1; i++ ) { + cols = ( i === c.columns ? + // 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' ) + wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector : + '[data-column="' + i + '"]' ); + $column = $filters.filter( cols ); + if ( $column.length ) { + // move the latest search to the first slot in the array + $column = tsf.getLatestSearch( $column ); + if ( $.isArray( setFilters ) ) { + // skip first ( latest input ) to maintain cursor position while typing + if ( skipFirst && $column.length > 1 ) { + $column = $column.slice( 1 ); + } + if ( i === c.columns ) { + // prevent data-column='all' from filling data-column='0,1' ( etc ) + cols = $column.filter( wo.filter_anyColumnSelector ); + $column = cols.length ? cols : $column; + } + $column + .val( setFilters[ i ] ) + // must include a namespace here; but not c.namespace + 'filter'? + .trigger( 'change' + c.namespace ); + } else { + filters[i] = $column.val() || ''; + // don't change the first... it will move the cursor + if ( i === c.columns ) { + // don't update range columns from 'all' setting + $column + .slice( 1 ) + .filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' ) + .val( filters[ i ] ); + } else { + $column + .slice( 1 ) + .val( filters[ i ] ); + } + } + // save any match input dynamically + if ( i === c.columns && $column.length ) { + wo.filter_$anyMatch = $column; + } + } + } + } + } + return filters; + }; + + ts.setFilters = function( table, filter, apply, skipFirst ) { + var c = table ? $( table )[0].config : '', + valid = ts.getFilters( table, true, filter, skipFirst ); + // default apply to "true" + if ( typeof apply === 'undefined' ) { + apply = true; + } + if ( c && apply ) { + // ensure new set filters are applied, even if the search is the same + c.lastCombinedFilter = null; + c.lastSearch = []; + tsf.searching( c.table, filter, skipFirst ); + c.$table.triggerHandler( 'filterFomatterUpdate' ); + } + return valid.length !== 0; + }; + +})( jQuery ); + +/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.4.3+ + * by Rob Garrison + */ +;(function ($, window) { + 'use strict'; + var ts = $.tablesorter || {}; + + $.extend(ts.css, { + sticky : 'tablesorter-stickyHeader', // stickyHeader + stickyVis : 'tablesorter-sticky-visible', + stickyHide: 'tablesorter-sticky-hidden', + stickyWrap: 'tablesorter-sticky-wrapper' + }); + + // Add a resize event to table headers + ts.addHeaderResizeEvent = function(table, disable, settings) { + table = $(table)[0]; // make sure we're using a dom element + if ( !table.config ) { return; } + var defaults = { + timer : 250 + }, + options = $.extend({}, defaults, settings), + c = table.config, + wo = c.widgetOptions, + checkSizes = function( triggerEvent ) { + var index, headers, $header, sizes, width, height, + len = c.$headers.length; + wo.resize_flag = true; + headers = []; + for ( index = 0; index < len; index++ ) { + $header = c.$headers.eq( index ); + sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394 + width = $header[0].offsetWidth; + height = $header[0].offsetHeight; + if ( width !== sizes[0] || height !== sizes[1] ) { + $header.data( 'savedSizes', [ width, height ] ); + headers.push( $header[0] ); + } + } + if ( headers.length && triggerEvent !== false ) { + c.$table.triggerHandler( 'resize', [ headers ] ); + } + wo.resize_flag = false; + }; + clearInterval(wo.resize_timer); + if (disable) { + wo.resize_flag = false; + return false; + } + checkSizes( false ); + wo.resize_timer = setInterval(function() { + if (wo.resize_flag) { return; } + checkSizes(); + }, options.timer); + }; + + function getStickyOffset(c, wo) { + var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : []; + return $el.length ? + $el.height() || 0 : + parseInt(wo.stickyHeaders_offset, 10) || 0; + } + + // Sticky headers based on this awesome article: + // http://css-tricks.com/13465-persistent-headers/ + // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech + // ************************** + ts.addWidget({ + id: 'stickyHeaders', + priority: 54, // sticky widget must be initialized after the filter & before pager widget! + options: { + stickyHeaders : '', // extra class name added to the sticky header row + stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers + stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings) + stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window) + stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window) + stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element + stickyHeaders_filteredToTop: true, // scroll table top into view after filtering + stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists + stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers + stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header + stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs + }, + format: function(table, c, wo) { + // filter widget doesn't initialize on an empty table. Fixes #449 + if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { + return; + } + var index, len, $t, + $table = c.$table, + // add position: relative to attach element, hopefully it won't cause trouble. + $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo), + namespace = c.namespace + 'stickyheaders ', + // element to watch for the scroll event + $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window), + $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window), + $thead = $table.children('thead:first'), + $header = $thead.children('tr').not('.sticky-false').children(), + $tfoot = $table.children('tfoot'), + stickyOffset = getStickyOffset(c, wo), + // is this table nested? If so, find parent sticky header wrapper (div, not table) + $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ? + $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [], + nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0, + // clone table, then wrap to make sticky header + $stickyTable = wo.$sticky = $table.clone() + .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' ) + .wrap('<div class="' + ts.css.stickyWrap + '">'), + $stickyWrap = $stickyTable.parent() + .addClass(ts.css.stickyHide) + .css({ + position : $attach.length ? 'absolute' : 'fixed', + padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ), + top : stickyOffset + nestedStickyTop, + left : 0, + visibility : 'hidden', + zIndex : wo.stickyHeaders_zIndex || 2 + }), + $stickyThead = $stickyTable.children('thead:first'), + $stickyCells, + laststate = '', + setWidth = function($orig, $clone) { + var index, width, border, $cell, $this, + $cells = $orig.filter(':visible'), + len = $cells.length; + for ( index = 0; index < len; index++ ) { + $cell = $clone.filter(':visible').eq(index); + $this = $cells.eq(index); + // code from https://github.com/jmosbech/StickyTableHeaders + if ($this.css('box-sizing') === 'border-box') { + width = $this.outerWidth(); + } else { + if ($cell.css('border-collapse') === 'collapse') { + if (window.getComputedStyle) { + width = parseFloat( window.getComputedStyle($this[0], null).width ); + } else { + // ie8 only + border = parseFloat( $this.css('border-width') ); + width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border; + } + } else { + width = $this.width(); + } + } + $cell.css({ + 'width': width, + 'min-width': width, + 'max-width': width + }); + } + }, + getLeftPosition = function(yWindow) { + if (yWindow === false && $nestedSticky.length) { + return $table.position().left; + } + return $attach.length ? + parseInt($attach.css('padding-left'), 10) || 0 : + $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft(); + }, + resizeHeader = function() { + $stickyWrap.css({ + left : getLeftPosition(), + width: $table.outerWidth() + }); + setWidth( $table, $stickyTable ); + setWidth( $header, $stickyCells ); + }, + scrollSticky = function( resizing ) { + if (!$table.is(':visible')) { return; } // fixes #278 + // Detect nested tables - fixes #724 + nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0; + var tmp, + offset = $table.offset(), + stickyOffset = getStickyOffset(c, wo), + yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3 + yScroll = yWindow ? + $yScroll.scrollTop() : + // use parent sticky position if nested AND inside of a scrollable element - see #1512 + $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top, + attachTop = $attach.length ? yScroll : $yScroll.scrollTop(), + captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0, + scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight, + tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight, + isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden', + state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide, + needsUpdating = !$stickyWrap.hasClass( state ), + cssSettings = { visibility : isVisible }; + if ($attach.length) { + // attached sticky headers always need updating + needsUpdating = true; + cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop(); + } + // adjust when scrolling horizontally - fixes issue #143 + tmp = getLeftPosition(yWindow); + if (tmp !== parseInt($stickyWrap.css('left'), 10)) { + needsUpdating = true; + cssSettings.left = tmp; + } + cssSettings.top = ( cssSettings.top || 0 ) + + // If nested AND inside of a scrollable element, only add parent sticky height + (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop); + if (needsUpdating) { + $stickyWrap + .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide ) + .addClass( state ) + .css(cssSettings); + } + if (isVisible !== laststate || resizing) { + // make sure the column widths match + resizeHeader(); + laststate = isVisible; + } + }; + // only add a position relative if a position isn't already defined + if ($attach.length && !$attach.css('position')) { + $attach.css('position', 'relative'); + } + // fix clone ID, if it exists - fixes #271 + if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } + // clear out cloned table, except for sticky header + // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing + $stickyTable.find('> thead:gt(0), tr.sticky-false').hide(); + $stickyTable.find('> tbody, > tfoot').remove(); + $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption); + // issue #172 - find td/th in sticky header + $stickyCells = $stickyThead.children().children(); + $stickyTable.css({ height:0, width:0, margin: 0 }); + // remove resizable block + $stickyCells.find('.' + ts.css.resizer).remove(); + // update sticky header class names to match real header after sorting + $table + .addClass('hasStickyHeaders') + .bind('pagerComplete' + namespace, function() { + resizeHeader(); + }); + + ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header)); + + if (wo.stickyHeaders_appendTo) { + $(wo.stickyHeaders_appendTo).append( $stickyWrap ); + } else { + // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. + $table.after( $stickyWrap ); + } + + // onRenderHeader is defined, we need to do something about it (fixes #641) + if (c.onRenderHeader) { + $t = $stickyThead.children('tr').children(); + len = $t.length; + for ( index = 0; index < len; index++ ) { + // send second parameter + c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] ); + } + } + // make it sticky! + $xScroll.add($yScroll) + .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') ) + .bind('scroll resize '.split(' ').join( namespace ), function( event ) { + scrollSticky( event.type === 'resize' ); + }); + c.$table + .unbind('stickyHeadersUpdate' + namespace) + .bind('stickyHeadersUpdate' + namespace, function() { + scrollSticky( true ); + }); + + if (wo.stickyHeaders_addResizeEvent) { + ts.addHeaderResizeEvent(table); + } + + // look for filter widget + if ($table.hasClass('hasFilters') && wo.filter_columnFilters) { + // scroll table into view after filtering, if sticky header is active - #482 + $table.bind('filterEnd' + namespace, function() { + // $(':focus') needs jQuery 1.6+ + var $td = $(document.activeElement).closest('td'), + column = $td.parent().children().index($td); + // only scroll if sticky header is active + if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { + // scroll to original table (not sticky clone) + window.scrollTo(0, $table.position().top); + // give same input/select focus; check if c.$filters exists; fixes #594 + if (column >= 0 && c.$filters) { + c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); + } + } + }); + ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); + // support hideFilters + if (wo.filter_hideFilters) { + ts.filter.hideFilters(c, $stickyTable); + } + } + + // resize table (Firefox) + if (wo.stickyHeaders_addResizeEvent) { + $table.bind('resize' + c.namespace + 'stickyheaders', function() { + resizeHeader(); + }); + } + + // make sure sticky is visible if page is partially scrolled + scrollSticky( true ); + $table.triggerHandler('stickyHeadersInit'); + + }, + remove: function(table, c, wo) { + var namespace = c.namespace + 'stickyheaders '; + c.$table + .removeClass('hasStickyHeaders') + .unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) + .next('.' + ts.css.stickyWrap).remove(); + if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table + $(window) + .add(wo.stickyHeaders_xScroll) + .add(wo.stickyHeaders_yScroll) + .add(wo.stickyHeaders_attachTo) + .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); + ts.addHeaderResizeEvent(table, true); + } + }); + +})(jQuery, window); + +/*! Widget: resizable - updated 2018-03-26 (v2.30.2) */ +/*jshint browser:true, jquery:true, unused:false */ +;(function ($, window) { + 'use strict'; + var ts = $.tablesorter || {}; + + $.extend(ts.css, { + resizableContainer : 'tablesorter-resizable-container', + resizableHandle : 'tablesorter-resizable-handle', + resizableNoSelect : 'tablesorter-disableSelection', + resizableStorage : 'tablesorter-resizable' + }); + + // Add extra scroller css + $(function() { + var s = '<style>' + + 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' + + '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' + + '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' + + // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header + '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' + + 'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' + + '</style>'; + $('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('<span>').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('<span>'); + } + */ + + 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 = $( '<div class="' + ts.css.resizableContainer + '">' ) + .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 ) { + $( '<div class="' + ts.css.resizableHandle + '">' ) + .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} = <i/> (class from cssIcon) - onRenderTemplate : null, // function(index, template){ return template; }, (template is a string) - onRenderHeader : null, // function(index){}, (nothing to return) - - // *** functionality - cancelSelection : true, // prevent text selection in the header - 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 <i> 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} = <i/> // class from cssIcon + onRenderTemplate : null, // function( index, template ) { return template; }, // template is a string + onRenderHeader : null, // function( index ) {}, // nothing to return + + // *** functionality + cancelSelection : true, // prevent text selection in the header + tabIndex : true, // add tabindex to header for keyboard accessibility + dateFormat : '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 ? + '<i class="' + ( c.cssIcon === ts.css.icon ? ts.css.icon : c.cssIcon + ' ' + ts.css.icon ) + '"></i>' : + ''; + // redefine c.$headers here in case of an updateAll that replaces or adds an entire header cell - see #683 + c.$headers = $( $.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( '<div class="' + ts.css.headerIn + '">' + template + '</div>' ); // 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 ? '<i class="' + c.cssIcon + '"></i>' : ''; // 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 = '<div class="tablesorter-header-inner">' + t + '</div>'; // 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 = $('<colgroup>'), - $cgo = c.$table.find('colgroup'), - n = c.columns.length, - overallWidth = c.$table.width(); - $("tr:first td", table.tBodies[0]).each(function(i) { - $c = $('<col>'); - 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 && /<tr/.test( $row || '' ), + table = c.table; + if ( valid ) { + $row = $( $row ); + c.$tbodies.append( $row ); + } else if ( + !$row || + // row is a jQuery object? + !( $row instanceof $ ) || + // row contained in the table? + ( ts.getClosest( $row, 'table' )[ 0 ] !== c.table ) + ) { + if ( ts.debug(c, 'core') ) { + console.error( 'addRows method requires (1) a jQuery selector reference to rows that have already ' + + 'been added to the table, or (2) row HTML string to be added to a table with only one tbody' ); } + return false; } + table.isUpdating = true; + if ( ts.isEmptyObject( c.cache ) ) { + // empty table, do an update instead - fixes #450 + ts.updateHeader( c ); + ts.commonUpdate( c, resort, callback ); + } else { + rows = $row.filter( 'tr' ).attr( 'role', 'row' ).length; + tbodyIndex = c.$tbodies.index( $row.parents( 'tbody' ).filter( ':first' ) ); + // fixes adding rows to an empty table - see issue #179 + if ( !( c.parsers && c.parsers.length ) ) { + ts.setupParsers( c ); + } + // add each row + for ( rowIndex = 0; rowIndex < rows; rowIndex++ ) { + cacheIndex = 0; + len = $row[ rowIndex ].cells.length; + order = c.cache[ tbodyIndex ].normalized.length; + cells = []; + rowData = { + child : [], + raw : [], + $row : $row.eq( rowIndex ), + order : order + }; + // add each cell + for ( cellIndex = 0; cellIndex < len; cellIndex++ ) { + cell = $row[ rowIndex ].cells[ cellIndex ]; + txt = ts.getElementText( c, cell, cacheIndex ); + rowData.raw[ cacheIndex ] = txt; + val = ts.getParsedText( c, cell, cacheIndex, txt ); + cells[ cacheIndex ] = val; + if ( ( c.parsers[ cacheIndex ].type || '' ).toLowerCase() === 'numeric' ) { + // update column max value (ignore sign) + c.cache[ tbodyIndex ].colMax[ cacheIndex ] = + Math.max( Math.abs( val ) || 0, c.cache[ tbodyIndex ].colMax[ cacheIndex ] || 0 ); + } + span = cell.colSpan - 1; + if ( span > 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 = $('<span>x</span>')[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('<span class="tablesorter-savemyplace"/>'); - 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 = $( '<colgroup class="' + ts.css.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( $( '<col>' ).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( '<colgroup class="tablesorter-savemyplace"/>' ); + 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 <i> 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 <i> 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('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>'); - } - if (c.cssIcon){ - // if c.cssIcon is '', then no <i> 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 <i> 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 <i> 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('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); + } + }); + if (c.cssIcon) { + // if c.cssIcon is '', then no <i> 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 (/</.test(val)) { ff = /<=/.test(val) ? rg <= s : rg < s; } - // Look for wild card: ? = single, or * = multiple - } else if (/[\?|\*]/.test(val)){ - ff = new RegExp( val.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(xi); - // Look for match, and add child row data for matching - } else { - x = (xi + t).indexOf(val); - ff = ( (!wo.filter_startsWith && x >= 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 = '<option value="">' + ($ths.filter('[data-column="' + i + '"]:last').attr('data-placeholder') || '') + '</option>'; - 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 += '<option value="' + arry[k] + '">' + arry[k] + '</option>'; + 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 = '<tr class="tablesorter-filter-row">'; // 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 += '<td>'; - if (sel){ - t += '<select data-column="' + i + '" class="' + css; - } else { - t += '<input type="search" placeholder="' + ($th.attr('data-placeholder') || "") + '" data-column="' + i + '" class="' + css; + tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; + range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; + range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || c.parsers[ index ].type === 'numeric' ) { + result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); + range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; + result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); + range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; } - // use header option - headers: { 1: { filter: false } } OR add class="filter-false" - if (ts.getData){ - dis = ts.getData($th[0], c.headers[i], 'filter') === 'false'; - // get data from jQuery data, metadata, headers option or header class name - t += dis ? ' disabled" disabled' : '"'; + if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { + result = data.cache; } else { - dis = (c.headers[i] && c.headers[i].hasOwnProperty('filter') && c.headers[i].filter === false) || $th.hasClass('filter-false'); - // only class names and header options - keep this for compatibility with tablesorter v2.0.5 - t += (dis) ? ' disabled" disabled' : '"'; + tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + result = ts.formatFloat( tmp, table ); + } + if ( range1 > range2 ) { + tmp = range1; range1 = range2; range2 = tmp; // swap + } + return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); + } + return null; + }, + // Look for wild card: ? = single, * = multiple, or | = logical OR + wild : function( c, data ) { + if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { + var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); + // look for an exact match with the 'or' unless the 'filter-match' class is found + if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { + query = data.isMatch ? query : '^(' + query + ')$'; + } + // parsing the filter may not work properly when using wildcards =/ + try { + return new RegExp( + query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), + c.widgetOptions.filter_ignoreCase ? 'i' : '' + ) + .test( data.exact ); + } catch ( error ) { + return null; + } + } + return null; + }, + // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) + fuzzy: function( c, data ) { + if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { + var indx, + patternIndx = 0, + len = data.iExact.length, + txt = data.iFilter.slice( 1 ), + pattern = tsf.parseFilter( c, txt, data ) || ''; + for ( indx = 0; indx < len; indx++ ) { + if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { + patternIndx += 1; + } } - t += (sel ? '></select>' : '>') + '</td>'; + return patternIndx === pattern.length; } - $t.find('thead').eq(0).append(t += '</tr>'); + 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 : /</, + lteTest : /<=/, + notTest : /^\!/, + wildOrTest : /[\?\*\|]/, + wildTest : /\?\*/, + fuzzyTest : /^~/, + exactTest : /[=\"\|!]/ + }); + + // don't build filter row if columnFilters is false or all columns are set to 'filter-false' + // see issue #156 + val = c.$headers.filter( '.filter-false, .parser-false' ).length; + if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) { + // build filter row + tsf.buildRow( table, c, wo ); } - $t - // add .tsfilter namespace to all BUT search - .bind('addRows updateCell update appendCache search'.split(' ').join('.tsfilter '), function(e, filter){ - if (e.type !== 'search'){ - buildDefault(true); + + txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + + 'filterAndSortReset filterResetSaved filterEnd search '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table.bind( txt, function( event, filter ) { + val = wo.filter_hideEmpty && + $.isEmptyObject( c.cache ) && + !( c.delayInit && event.type === 'appendCache' ); + // hide filter row using the 'filtered' class name + c.$table.find( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450 + if ( !/(search|filter)/.test( event.type ) ) { + event.stopPropagation(); + tsf.buildDefault( table, true ); } - checkFilters(e.type === 'search' ? filter : ''); - return false; - }) - .find('input.' + css).bind('keyup search', function(e, filter){ - // ignore arrow and meta keys; allow backspace - if ((e.which < 32 && e.which !== 8) || (e.which >= 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 === '' ? '<option value="">' + (t.attr('data-placeholder') || '') + '</option>' : ''; - ff += '<option value="' + str + '">' + str + '</option>'; + for ( string in fxn ) { + if ( typeof string === 'string' ) { + options += options === '' ? + '<option value="">' + + ( $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || + '' + ) + + '</option>' : ''; + val = string; + txt = string; + if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + val = string.split( wo.filter_selectSourceSeparator ); + txt = val[1]; + val = val[0]; + } + options += '<option ' + + ( txt === val ? '' : 'data-function-name="' + string + '" ' ) + + 'value="' + val + '">' + txt + '</option>'; } } - $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 = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">'; + 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 += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"'; + } else { + buildFilter += '<td data-column="' + column + '"'; + } + if ( arry ) { + buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' ); + } else { + buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' ); + } + buildFilter += '></td>'; + } + } + c.$filters = $( buildFilter += '</tr>' ) + .appendTo( c.$table.children( 'thead' ).eq( 0 ) ) + .children( 'td' ); + // build each filter input + for ( column = 0; column < columns; column++ ) { + disabled = false; + // assuming last cell of a column is the main column + $header = c.$headerIndexed[ column ]; + if ( $header && $header.length ) { + // $filter = c.$filters.filter( '[data-column="' + column + '"]' ); + $filter = tsf.getColumnElm( c, c.$filters, column ); + ffxn = ts.getColumnData( table, wo.filter_functions, column ); + makeSelect = ( wo.filter_functions && ffxn && typeof ffxn !== 'function' ) || + $header.hasClass( 'filter-select' ); + // get data from jQuery data, metadata, headers option or header class name + col = ts.getColumnData( table, c.headers, column ); + disabled = ts.getData( $header[0], col, 'filter' ) === 'false' || + ts.getData( $header[0], col, 'parser' ) === 'false'; + + if ( makeSelect ) { + buildFilter = $( '<select>' ).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 = $( '<input type="search">' ).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('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>'); - } - $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 = '<div class="tablesorter-resizer" style="cursor:w-resize;position:absolute;z-index:1;right:-' + j + - 'px;top:0;height:100%;width:20px;"></div>'; - $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. <th class="filter-select filter-parsed"> ) + 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( '</option>' ) >= 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 = '<option value="">' + + ( node.data( 'placeholder' ) || + node.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || '' + ) + '</option>', + // 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 += $( '<option>', option )[ 0 ].outerHTML; + options += '<option'; + for ( val in option ) { + if ( option.hasOwnProperty( val ) && val !== 'text' ) { + options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '"' ) + '"'; + } + } + if ( !option.value ) { + options += ' value="' + option.text.replace( tsfRegex.quote, '"' ) + '"'; + } + options += '>' + option.text.replace( tsfRegex.quote, '"' ) + '</option>'; + // above code is needed in jQuery < v1.8 + + // make sure we don't turn an object into a string (objects without a "text" property) + } else if ( '' + option !== '[object Object]' ) { + txt = option = ( '' + option ).replace( tsfRegex.quote, '"' ); + val = txt; + // allow including a symbol in the selectSource array + // 'a-z|A through Z' so that 'a-z' becomes the option value + // and 'A through Z' becomes the option text + if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + t = txt.split( wo.filter_selectSourceSeparator ); + val = t[0]; + txt = t[1]; + } + // replace quotes - fixes #242 & ignore empty strings + // see http://stackoverflow.com/q/14990971/145346 + options += option !== '' ? + '<option ' + + ( val === txt ? '' : 'data-function-name="' + option + '" ' ) + + 'value="' + val + '">' + txt + + '</option>' : ''; + } + } + // clear arry so it doesn't get appended twice + arry = []; + } + + // update all selects in the same column ( clone thead in sticky headers & + // any external selects ) - fixes 473 + $filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) ) + .find( '.' + tscss.filter ); + if ( wo.filter_$externalFilters ) { + $filters = $filters && $filters.length ? + $filters.add( wo.filter_$externalFilters ) : + wo.filter_$externalFilters; + } + $filter = $filters.filter( 'select[data-column="' + column + '"]' ); + + // make sure there is a select there! + if ( $filter.length ) { + $filter[ updating ? 'html' : 'append' ]( options ); + if ( !$.isArray( arry ) ) { + // append options if arry is provided externally as a string or jQuery object + // options ( default value ) was already added + $filter.append( arry ).val( currentValue ); + } + $filter.val( currentValue ); + } + }, + buildDefault: function( table, updating ) { + var columnIndex, $header, noSelect, + c = table.config, + wo = c.widgetOptions, + columns = c.columns; + // build default select dropdown + for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) { + $header = c.$headerIndexed[columnIndex]; + noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); + // look for the filter-select class; build/update it if found + if ( ( $header.hasClass( 'filter-select' ) || + ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) { + tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) ); + } + } + } + }; + + // filter regex variable + tsfRegex = tsf.regex; + + ts.getFilters = function( table, getRaw, setFilters, skipFirst ) { + var i, $filters, $column, cols, + filters = [], + c = table ? $( table )[0].config : '', + wo = c ? c.widgetOptions : ''; + if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) || + // setFilters called, but last search is exactly the same as the current + // fixes issue #733 & #903 where calling update causes the input values to reset + ( $.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) ) + ) { + return $( table ).data( 'lastSearch' ) || []; + } + if ( c ) { + if ( c.$filters ) { + $filters = c.$filters.find( '.' + tscss.filter ); + } + if ( wo.filter_$externalFilters ) { + $filters = $filters && $filters.length ? + $filters.add( wo.filter_$externalFilters ) : + wo.filter_$externalFilters; + } + if ( $filters && $filters.length ) { + filters = setFilters || []; + for ( i = 0; i < c.columns + 1; i++ ) { + cols = ( i === c.columns ? + // 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' ) + wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector : + '[data-column="' + i + '"]' ); + $column = $filters.filter( cols ); + if ( $column.length ) { + // move the latest search to the first slot in the array + $column = tsf.getLatestSearch( $column ); + if ( $.isArray( setFilters ) ) { + // skip first ( latest input ) to maintain cursor position while typing + if ( skipFirst && $column.length > 1 ) { + $column = $column.slice( 1 ); + } + if ( i === c.columns ) { + // prevent data-column='all' from filling data-column='0,1' ( etc ) + cols = $column.filter( wo.filter_anyColumnSelector ); + $column = cols.length ? cols : $column; + } + $column + .val( setFilters[ i ] ) + // must include a namespace here; but not c.namespace + 'filter'? + .trigger( 'change' + c.namespace ); + } else { + filters[i] = $column.val() || ''; + // don't change the first... it will move the cursor + if ( i === c.columns ) { + // don't update range columns from 'all' setting + $column + .slice( 1 ) + .filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' ) + .val( filters[ i ] ); + } else { + $column + .slice( 1 ) + .val( filters[ i ] ); + } + } + // save any match input dynamically + if ( i === c.columns && $column.length ) { + wo.filter_$anyMatch = $column; + } + } + } + } + } + return filters; + }; + + ts.setFilters = function( table, filter, apply, skipFirst ) { + var c = table ? $( table )[0].config : '', + valid = ts.getFilters( table, true, filter, skipFirst ); + // default apply to "true" + if ( typeof apply === 'undefined' ) { + apply = true; + } + if ( c && apply ) { + // ensure new set filters are applied, even if the search is the same + c.lastCombinedFilter = null; + c.lastSearch = []; + tsf.searching( c.table, filter, skipFirst ); + c.$table.triggerHandler( 'filterFomatterUpdate' ); + } + return valid.length !== 0; + }; + +})( jQuery ); + +/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.4.3+ + * by Rob Garrison + */ +;(function ($, window) { + 'use strict'; + var ts = $.tablesorter || {}; + + $.extend(ts.css, { + sticky : 'tablesorter-stickyHeader', // stickyHeader + stickyVis : 'tablesorter-sticky-visible', + stickyHide: 'tablesorter-sticky-hidden', + stickyWrap: 'tablesorter-sticky-wrapper' + }); + + // Add a resize event to table headers + ts.addHeaderResizeEvent = function(table, disable, settings) { + table = $(table)[0]; // make sure we're using a dom element + if ( !table.config ) { return; } + var defaults = { + timer : 250 + }, + options = $.extend({}, defaults, settings), + c = table.config, + wo = c.widgetOptions, + checkSizes = function( triggerEvent ) { + var index, headers, $header, sizes, width, height, + len = c.$headers.length; + wo.resize_flag = true; + headers = []; + for ( index = 0; index < len; index++ ) { + $header = c.$headers.eq( index ); + sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394 + width = $header[0].offsetWidth; + height = $header[0].offsetHeight; + if ( width !== sizes[0] || height !== sizes[1] ) { + $header.data( 'savedSizes', [ width, height ] ); + headers.push( $header[0] ); + } + } + if ( headers.length && triggerEvent !== false ) { + c.$table.triggerHandler( 'resize', [ headers ] ); + } + wo.resize_flag = false; + }; + clearInterval(wo.resize_timer); + if (disable) { + wo.resize_flag = false; + return false; + } + checkSizes( false ); + wo.resize_timer = setInterval(function() { + if (wo.resize_flag) { return; } + checkSizes(); + }, options.timer); + }; + + function getStickyOffset(c, wo) { + var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : []; + return $el.length ? + $el.height() || 0 : + parseInt(wo.stickyHeaders_offset, 10) || 0; + } + + // Sticky headers based on this awesome article: + // http://css-tricks.com/13465-persistent-headers/ + // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech + // ************************** + ts.addWidget({ + id: 'stickyHeaders', + priority: 54, // sticky widget must be initialized after the filter & before pager widget! + options: { + stickyHeaders : '', // extra class name added to the sticky header row + stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers + stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings) + stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window) + stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window) + stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element + stickyHeaders_filteredToTop: true, // scroll table top into view after filtering + stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists + stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers + stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header + stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs + }, + format: function(table, c, wo) { + // filter widget doesn't initialize on an empty table. Fixes #449 + if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { + return; + } + var index, len, $t, + $table = c.$table, + // add position: relative to attach element, hopefully it won't cause trouble. + $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo), + namespace = c.namespace + 'stickyheaders ', + // element to watch for the scroll event + $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window), + $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window), + $thead = $table.children('thead:first'), + $header = $thead.children('tr').not('.sticky-false').children(), + $tfoot = $table.children('tfoot'), + stickyOffset = getStickyOffset(c, wo), + // is this table nested? If so, find parent sticky header wrapper (div, not table) + $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ? + $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [], + nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0, + // clone table, then wrap to make sticky header + $stickyTable = wo.$sticky = $table.clone() + .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' ) + .wrap('<div class="' + ts.css.stickyWrap + '">'), + $stickyWrap = $stickyTable.parent() + .addClass(ts.css.stickyHide) + .css({ + position : $attach.length ? 'absolute' : 'fixed', + padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ), + top : stickyOffset + nestedStickyTop, + left : 0, + visibility : 'hidden', + zIndex : wo.stickyHeaders_zIndex || 2 + }), + $stickyThead = $stickyTable.children('thead:first'), + $stickyCells, + laststate = '', + setWidth = function($orig, $clone) { + var index, width, border, $cell, $this, + $cells = $orig.filter(':visible'), + len = $cells.length; + for ( index = 0; index < len; index++ ) { + $cell = $clone.filter(':visible').eq(index); + $this = $cells.eq(index); + // code from https://github.com/jmosbech/StickyTableHeaders + if ($this.css('box-sizing') === 'border-box') { + width = $this.outerWidth(); + } else { + if ($cell.css('border-collapse') === 'collapse') { + if (window.getComputedStyle) { + width = parseFloat( window.getComputedStyle($this[0], null).width ); + } else { + // ie8 only + border = parseFloat( $this.css('border-width') ); + width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border; + } + } else { + width = $this.width(); + } + } + $cell.css({ + 'width': width, + 'min-width': width, + 'max-width': width + }); + } + }, + getLeftPosition = function(yWindow) { + if (yWindow === false && $nestedSticky.length) { + return $table.position().left; + } + return $attach.length ? + parseInt($attach.css('padding-left'), 10) || 0 : + $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft(); + }, + resizeHeader = function() { + $stickyWrap.css({ + left : getLeftPosition(), + width: $table.outerWidth() + }); + setWidth( $table, $stickyTable ); + setWidth( $header, $stickyCells ); + }, + scrollSticky = function( resizing ) { + if (!$table.is(':visible')) { return; } // fixes #278 + // Detect nested tables - fixes #724 + nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0; + var tmp, + offset = $table.offset(), + stickyOffset = getStickyOffset(c, wo), + yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3 + yScroll = yWindow ? + $yScroll.scrollTop() : + // use parent sticky position if nested AND inside of a scrollable element - see #1512 + $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top, + attachTop = $attach.length ? yScroll : $yScroll.scrollTop(), + captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0, + scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight, + tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight, + isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden', + state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide, + needsUpdating = !$stickyWrap.hasClass( state ), + cssSettings = { visibility : isVisible }; + if ($attach.length) { + // attached sticky headers always need updating + needsUpdating = true; + cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop(); + } + // adjust when scrolling horizontally - fixes issue #143 + tmp = getLeftPosition(yWindow); + if (tmp !== parseInt($stickyWrap.css('left'), 10)) { + needsUpdating = true; + cssSettings.left = tmp; + } + cssSettings.top = ( cssSettings.top || 0 ) + + // If nested AND inside of a scrollable element, only add parent sticky height + (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop); + if (needsUpdating) { + $stickyWrap + .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide ) + .addClass( state ) + .css(cssSettings); + } + if (isVisible !== laststate || resizing) { + // make sure the column widths match + resizeHeader(); + laststate = isVisible; + } + }; + // only add a position relative if a position isn't already defined + if ($attach.length && !$attach.css('position')) { + $attach.css('position', 'relative'); + } + // fix clone ID, if it exists - fixes #271 + if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } + // clear out cloned table, except for sticky header + // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing + $stickyTable.find('> thead:gt(0), tr.sticky-false').hide(); + $stickyTable.find('> tbody, > tfoot').remove(); + $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption); + // issue #172 - find td/th in sticky header + $stickyCells = $stickyThead.children().children(); + $stickyTable.css({ height:0, width:0, margin: 0 }); + // remove resizable block + $stickyCells.find('.' + ts.css.resizer).remove(); + // update sticky header class names to match real header after sorting + $table + .addClass('hasStickyHeaders') + .bind('pagerComplete' + namespace, function() { + resizeHeader(); + }); + + ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header)); + + if (wo.stickyHeaders_appendTo) { + $(wo.stickyHeaders_appendTo).append( $stickyWrap ); + } else { + // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. + $table.after( $stickyWrap ); + } + + // onRenderHeader is defined, we need to do something about it (fixes #641) + if (c.onRenderHeader) { + $t = $stickyThead.children('tr').children(); + len = $t.length; + for ( index = 0; index < len; index++ ) { + // send second parameter + c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] ); + } + } + // make it sticky! + $xScroll.add($yScroll) + .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') ) + .bind('scroll resize '.split(' ').join( namespace ), function( event ) { + scrollSticky( event.type === 'resize' ); + }); + c.$table + .unbind('stickyHeadersUpdate' + namespace) + .bind('stickyHeadersUpdate' + namespace, function() { + scrollSticky( true ); + }); + + if (wo.stickyHeaders_addResizeEvent) { + ts.addHeaderResizeEvent(table); + } + + // look for filter widget + if ($table.hasClass('hasFilters') && wo.filter_columnFilters) { + // scroll table into view after filtering, if sticky header is active - #482 + $table.bind('filterEnd' + namespace, function() { + // $(':focus') needs jQuery 1.6+ + var $td = $(document.activeElement).closest('td'), + column = $td.parent().children().index($td); + // only scroll if sticky header is active + if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { + // scroll to original table (not sticky clone) + window.scrollTo(0, $table.position().top); + // give same input/select focus; check if c.$filters exists; fixes #594 + if (column >= 0 && c.$filters) { + c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); + } + } + }); + ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); + // support hideFilters + if (wo.filter_hideFilters) { + ts.filter.hideFilters(c, $stickyTable); + } + } + + // resize table (Firefox) + if (wo.stickyHeaders_addResizeEvent) { + $table.bind('resize' + c.namespace + 'stickyheaders', function() { + resizeHeader(); + }); + } + + // make sure sticky is visible if page is partially scrolled + scrollSticky( true ); + $table.triggerHandler('stickyHeadersInit'); + + }, + remove: function(table, c, wo) { + var namespace = c.namespace + 'stickyheaders '; + c.$table + .removeClass('hasStickyHeaders') + .unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) + .next('.' + ts.css.stickyWrap).remove(); + if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table + $(window) + .add(wo.stickyHeaders_xScroll) + .add(wo.stickyHeaders_yScroll) + .add(wo.stickyHeaders_attachTo) + .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); + ts.addHeaderResizeEvent(table, true); + } + }); + +})(jQuery, window); + +/*! Widget: resizable - updated 2018-03-26 (v2.30.2) */ +/*jshint browser:true, jquery:true, unused:false */ +;(function ($, window) { + 'use strict'; + var ts = $.tablesorter || {}; + + $.extend(ts.css, { + resizableContainer : 'tablesorter-resizable-container', + resizableHandle : 'tablesorter-resizable-handle', + resizableNoSelect : 'tablesorter-disableSelection', + resizableStorage : 'tablesorter-resizable' + }); + + // Add extra scroller css + $(function() { + var s = '<style>' + + 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' + + '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' + + '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' + + // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header + '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' + + 'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' + + '</style>'; + $('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('<span>').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('<span>'); + } + */ + + 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 = $( '<div class="' + ts.css.resizableContainer + '">' ) + .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 ) { + $( '<div class="' + ts.css.resizableHandle + '">' ) + .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/parsers/parser-date-extract.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-extract.js new file mode 100644 index 0000000..d99300e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-extract.js @@ -0,0 +1,98 @@ +/*! Parser: Extract out date - updated 10/26/2014 (v2.18.0) */ +/*jshint jquery:true */ +;(function($) { + 'use strict'; + + var regex = { + usLong : /[A-Z]{3,10}\.?\s+\d{1,2},?\s+(?:\d{4})(?:\s+\d{1,2}:\d{2}(?::\d{2})?(?:\s+[AP]M)?)?/i, + mdy : /(\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4}(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?)/i, + + dmy : /(\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4}(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?)/i, + dmyreplace : /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/, + + ymd : /(\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2}(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?)/i, + ymdreplace : /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/ + }; + + /*! extract US Long Date *//* (ignore any other text) + * e.g. 'Sue's Birthday! Jun 26, 2004 7:22 AM (8# 2oz)' + * demo: http://jsfiddle.net/Mottie/abkNM/4165/ */ + $.tablesorter.addParser({ + id: 'extractUSLongDate', + is: function () { + // don't auto detect this parser + return false; + }, + format: function (s) { + var date, + str = s ? s.match(regex.usLong) : s; + if (str) { + date = new Date( str[0] ); + return date instanceof Date && isFinite(date) ? date.getTime() : s; + } + return s; + }, + type: 'numeric' + }); + + /*! extract MMDDYYYY *//* (ignore any other text) + * demo: http://jsfiddle.net/Mottie/abkNM/4166/ */ + $.tablesorter.addParser({ + id: 'extractMMDDYYYY', + is: function () { + // don't auto detect this parser + return false; + }, + format: function (s) { + var date, + str = s ? s.replace(/\s+/g, ' ').replace(/[\-.,]/g, '/').match(regex.mdy) : s; + if (str) { + date = new Date( str[0] ); + return date instanceof Date && isFinite(date) ? date.getTime() : s; + } + return s; + }, + type: 'numeric' + }); + + /*! extract DDMMYYYY *//* (ignore any other text) + * demo: http://jsfiddle.net/Mottie/abkNM/4167/ */ + $.tablesorter.addParser({ + id: 'extractDDMMYYYY', + is: function () { + // don't auto detect this parser + return false; + }, + format: function (s) { + var date, + str = s ? s.replace(/\s+/g, ' ').replace(/[\-.,]/g, '/').match(regex.dmy) : s; + if (str) { + date = new Date( str[0].replace(regex.dmyreplace, '$2/$1/$3') ); + return date instanceof Date && isFinite(date) ? date.getTime() : s; + } + return s; + }, + type: 'numeric' + }); + + /*! extract YYYYMMDD *//* (ignore any other text) + * demo: http://jsfiddle.net/Mottie/abkNM/4168/ */ + $.tablesorter.addParser({ + id: 'extractYYYYMMDD', + is: function () { + // don't auto detect this parser + return false; + }, + format: function (s) { + var date, + str = s ? s.replace(/\s+/g, ' ').replace(/[\-.,]/g, '/').match(regex.ymd) : s; + if (str) { + date = new Date( str[0].replace(regex.ymdreplace, '$2/$3/$1') ); + return date instanceof Date && isFinite(date) ? date.getTime() : s; + } + return s; + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-iso8601.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-iso8601.js new file mode 100644 index 0000000..e7c230c --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-iso8601.js @@ -0,0 +1,35 @@ +/*! Parser: ISO-8601 date - updated 10/26/2014 (v2.18.0) */ +/* This parser works with dates in ISO8601 format + * 2013-02-18T18:18:44+00:00 + * Written by Sean Ellingham :https://github.com/seanellingham + * See https://github.com/Mottie/tablesorter/issues/247 + */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var iso8601date = /^([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/; + + $.tablesorter.addParser({ + id : 'iso8601date', + is : function(s) { + return s ? s.match(iso8601date) : false; + }, + format : function(s) { + var result = s ? s.match(iso8601date) : s; + if (result) { + var date = new Date(result[1], 0, 1); + if (result[3]) { date.setMonth(result[3] - 1); } + if (result[5]) { date.setDate(result[5]); } + if (result[7]) { date.setHours(result[7]); } + if (result[8]) { date.setMinutes(result[8]); } + if (result[10]) { date.setSeconds(result[10]); } + if (result[12]) { date.setMilliseconds(Number('0.' + result[12]) * 1000); } + return date.getTime(); + } + return s; + }, + type : 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-month.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-month.js new file mode 100644 index 0000000..19eda84 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-month.js @@ -0,0 +1,62 @@ +/*! Parser: Month - updated 11/22/2015 (v2.24.6) */ +/* Demo: http://jsfiddle.net/Mottie/abkNM/4169/ */ +/*jshint jquery:true */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter; + + if ( !ts.dates ) { ts.dates = {}; } + if ( !ts.dates.months ) { ts.dates.months = {}; } + ts.dates.months.en = { + // See http://mottie.github.io/tablesorter/docs/example-widget-grouping.html + // for details on how to use CLDR data for a locale to add data for this parser + // CLDR returns an object { 1: "Jan", 2: "Feb", 3: "Mar", ..., 12: "Dec" } + 1 : 'Jan', + 2 : 'Feb', + 3 : 'Mar', + 4 : 'Apr', + 5 : 'May', + 6 : 'Jun', + 7 : 'Jul', + 8 : 'Aug', + 9 : 'Sep', + 10: 'Oct', + 11: 'Nov', + 12: 'Dec' + }; + + ts.addParser({ + id: 'month', + is: function() { + return false; + }, + format: function( str, table, cell, cellIndex ) { + if ( str ) { + var m, month, + c = table.config, + // add options to 'config.globalize' for all columns --> globalize : { lang: 'en' } + // or per column by using the column index --> globalize : { 0 : { lang: 'fr' } } + options = c.globalize && ( c.globalize[ cellIndex ] || c.globalize ) || {}, + months = ts.dates.months[ options.lang || 'en' ]; + if ( c.ignoreCase ) { + str = str.toLowerCase(); + } + for ( month in months ) { + if ( typeof month === 'string' ) { + m = months[ month ]; + if ( c.ignoreCase ) { + m = m.toLowerCase(); + } + if ( str.match( m ) ) { + return parseInt( month, 10 ); + } + } + } + } + return str; + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-range.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-range.js new file mode 100644 index 0000000..f33ab0e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-range.js @@ -0,0 +1,174 @@ +/*! Parser: date ranges -updated 11/22/2015 (v2.24.6) */ +/* Include the 'widget-filter-type-insideRange.js' to filter ranges */ +/*jshint jquery:true */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + getMonthVal, + + regex = { + mdy : /(\d{1,2}[-\s]\d{1,2}[-\s]\d{4}(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?)/gi, + + dmy : /(\d{1,2}[-\s]\d{1,2}[-\s]\d{4}(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?)/gi, + dmyreplace : /(\d{1,2})[-\s](\d{1,2})[-\s](\d{4})/, + + ymd : /(\d{4}[-\s]\d{1,2}[-\s]\d{1,2}(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?)/gi, + ymdreplace : /(\d{4})[-\s](\d{1,2})[-\s](\d{1,2})/, + + // extract out date format (dd MMM yyyy hms) e.g. 13 March 2016 12:55 PM + overall_dMMMyyyy : /(\d{1,2}\s+\w+\s+\d{4}(\s+\d{1,2}:\d{2}(:\d{2})?(\s\w+)?)?)/g, + matches_dMMMyyyy : /(\d{1,2})\s+(\w+)\s+(\d{4})/ + + }; + + /*! date-range MMDDYYYY *//* (2/15/2000 - 5/18/2000) */ + $.tablesorter.addParser({ + id: 'date-range-mdy', + is: function () { + return false; + }, + format: function (text) { + var date, str, i, len, + parsed = []; + str = text.replace( /\s+/g, ' ' ).replace( /[\/\-.,]/g, '-' ).match( regex.mdy ); + len = str && str.length; + // work on dates, even if there is no range + if ( len ) { + for (i = 0; i < len; i++) { + date = new Date( str[i] ); + parsed.push( date instanceof Date && isFinite(date) ? date.getTime() : str[i] ); + } + // sort from min to max + return parsed.sort().join( ' - ' ); + } + return text; + }, + type: 'text' + }); + + /*! date-range DDMMYYYY *//* (15/2/2000 - 18/5/2000) */ + $.tablesorter.addParser({ + id: 'date-range-dmy', + is: function () { + return false; + }, + format: function (text) { + var date, str, i, len, + parsed = []; + str = text.replace( /\s+/g, ' ' ).replace( /[\/\-.,]/g, '-' ).match( regex.dmy ); + len = str && str.length; + if ( len ) { + for (i = 0; i < len; i++) { + date = new Date( ( '' + str[i] ).replace( regex.dmyreplace, '$2/$1/$3' ) ); + parsed.push( date instanceof Date && isFinite(date) ? date.getTime() : str[i] ); + } + // sort from min to max + return parsed.sort().join( ' - ' ); + } + return text; + }, + type: 'text' + }); + + /*! date-range DDMMYYYY *//* (2000/2/15 - 2000/5/18) */ + $.tablesorter.addParser({ + id: 'date-range-ymd', + is: function () { + return false; + }, + format: function (text) { + var date, str, i, len, + parsed = []; + str = text.replace( /\s+/g, ' ' ).replace( /[\/\-.,]/g, '-' ).match( regex.ymd ); + len = str && str.length; + if ( len ) { + for (i = 0; i < len; i++) { + date = new Date( ( '' + str[i] ).replace( regex.ymdreplace, '$2/$3/$1' ) ); + parsed.push( date instanceof Date && isFinite(date) ? date.getTime() : str[i] ); + } + // sort from min to max + return parsed.sort().join( ' - ' ); + } + return text; + }, + type: 'text' + }); + + if ( !ts.dates ) { ts.dates = {}; } + if ( !ts.dates.months ) { ts.dates.months = {}; } + ts.dates.months.en = { + // See http://mottie.github.io/tablesorter/docs/example-widget-grouping.html + // for details on how to use CLDR data for a locale to add data for this parser + // CLDR returns an object { 1: "Jan", 2: "Feb", 3: "Mar", ..., 12: "Dec" } + 1 : 'Jan', + 2 : 'Feb', + 3 : 'Mar', + 4 : 'Apr', + 5 : 'May', + 6 : 'Jun', + 7 : 'Jul', + 8 : 'Aug', + 9 : 'Sep', + 10: 'Oct', + 11: 'Nov', + 12: 'Dec' + }; + + getMonthVal = function( str, c, cellIndex ) { + var m, month, + // add options to 'config.globalize' for all columns --> globalize : { lang: 'en' } + // or per column by using the column index --> globalize : { 0 : { lang: 'fr' } } + options = c.globalize && ( c.globalize[ cellIndex ] || c.globalize ) || {}, + months = ts.dates.months[ options.lang || 'en' ]; + if ( c.ignoreCase ) { + str = str.toLowerCase(); + } + for ( month in months ) { + if ( typeof month === 'string' ) { + m = months[ month ]; + if ( c.ignoreCase ) { + m = m.toLowerCase(); + } + if ( str.match( m ) ) { + return parseInt( month, 10 ); + } + } + } + return str; + }; + + /*! date-range "dd MMM yyyy - dd MMM yyyy" *//* ( hms optional )*/ + ts.addParser({ + id: 'date-range-dMMMyyyy', + is: function () { + return false; + }, + format: function( text, table, cell, cellIndex ) { + var date, month, matches, i, + parsed = [], + str = text.replace( /\s+/g, ' ' ).match( regex.overall_dMMMyyyy ), + len = str && str.length; + if ( len ) { + for ( i = 0; i < len; i++ ) { + date = ''; + matches = str[ i ].match( regex.matches_dMMMyyyy ); + if ( matches && matches.length >= 4 ) { + matches.shift(); + month = getMonthVal( matches[1], table.config, cellIndex ); + if ( !isNaN( month ) ) { + str[i] = str[i].replace( matches[1], month ); + } + date = new Date( ( '' + str[ i ] ).replace( ts.regex.shortDateXXY, '$3/$2/$1' ) ); + } + parsed.push( date instanceof Date && isFinite(date) ? date.getTime() : str[i] ); + } + // sort from min to max + return parsed.sort().join( ' - ' ); + } + return text; + }, + type: 'text' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-two-digit-year.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-two-digit-year.js new file mode 100644 index 0000000..e301170 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-two-digit-year.js @@ -0,0 +1,82 @@ +/*! Parser: two digit year - updated 11/26/2016 (v2.28.0) */ +/* Demo: http://mottie.github.io/tablesorter/docs/example-parsers-dates.html */ +/*jshint jquery:true */ +;(function($) { + 'use strict'; + + // Make the date be within +/- range of the 2 digit year + // so if the current year is 2020, and the 2 digit year is 80 (2080 - 2020 > 50), it becomes 1980 + // if the 2 digit year is 50 (2050 - 2020 < 50), then it becomes 2050. + var range = 50, + + // no need to change any of the code below + ts = $.tablesorter, + now = new Date().getFullYear(); + + // add dateRange to defaults for validator; value must be falsy + ts.defaults.dataRange = ''; + + if ( !ts.dates ) { ts.dates = {}; } + ts.dates.regxxxxyy = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{2})/; + ts.dates.regyyxxxx = /(\d{2})[\/\s](\d{1,2})[\/\s](\d{1,2})/; + + ts.formatDate = function(s, regex, format, table) { + if (s) { + var y, rng, + n = s + // replace separators + .replace(/\s+/g, ' ').replace(/[-.,]/g, '/') + // reformat xx/xx/xx to mm/dd/19yy; + .replace(regex, format), + d = new Date(n); + if ( d instanceof Date && isFinite(d) ) { + y = d.getFullYear(); + rng = table && table.config.dateRange || range; + // if date > 50 years old (set range), add 100 years + // this will work when people start using '50' and mean '2050' + while (now - y > rng) { + y += 100; + } + return d.setFullYear(y); + } + } + return s; + }; + + $.tablesorter.addParser({ + id: 'ddmmyy', + is: function() { + return false; + }, + format: function(s, table) { + // reformat dd/mm/yy to mm/dd/19yy; + return ts.formatDate(s, ts.dates.regxxxxyy, '$2/$1/19$3', table); + }, + type: 'numeric' + }); + + $.tablesorter.addParser({ + id: 'mmddyy', + is: function() { + return false; + }, + format: function(s, table) { + // reformat mm/dd/yy to mm/dd/19yy + return ts.formatDate(s, ts.dates.regxxxxyy, '$1/$2/19$3', table); + }, + type: 'numeric' + }); + + $.tablesorter.addParser({ + id: 'yymmdd', + is: function() { + return false; + }, + format: function(s, table) { + // reformat yy/mm/dd to mm/dd/19yy + return ts.formatDate(s, ts.dates.regyyxxxx, '$2/$3/19$1', table); + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-weekday.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-weekday.js new file mode 100644 index 0000000..af02891 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date-weekday.js @@ -0,0 +1,94 @@ +/*! Parser: weekday - updated 11/22/2015 (v2.24.6) */ +/* Demo: http://jsfiddle.net/Mottie/abkNM/4169/ */ +/*jshint jquery:true */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter; + + if ( !ts.dates ) { ts.dates = {}; } + if ( !ts.dates.weekdays ) { ts.dates.weekdays = {}; } + // See http://mottie.github.io/tablesorter/docs/example-widget-grouping.html + // for details on how to use CLDR data for a locale to add data for this parser + // CLDR returns { sun: "Sun", mon: "Mon", tue: "Tue", wed: "Wed", thu: "Thu", ... } + ts.dates.weekdays.en = { + 'sun' : 'Sun', + 'mon' : 'Mon', + 'tue' : 'Tue', + 'wed' : 'Wed', + 'thu' : 'Thu', + 'fri' : 'Fri', + 'sat' : 'Sat' + }; + // set table.config.weekStarts to change weekday start date for your locale + // cross-reference of a date on which the week starts on a... + // https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/weekData.json + // locale agnostic + ts.dates.weekStartList = { + 'sun' : '1995', // Sun 1/1/1995 + 'mon' : '1996', // Mon 1/1/1996 + 'fri' : '1999', // Friday 1/1/1999 + 'sat' : '2000' // Sat 1/1/2000 + }; + // do not modify this array; it is used for cross referencing weekdays + // locale agnostic + ts.dates.weekdaysXref = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; + + ts.addParser({ + id: 'weekday', + is: function() { + return false; + }, + format: function( str, table, cell, cellIndex ) { + if ( str ) { + var d, day, num, + c = table.config, + // add options to 'config.globalize' for all columns --> globalize : { lang: 'en' } + // or per column by using the column index --> globalize : { 0 : { lang: 'fr' } } + options = c.globalize && ( c.globalize[ cellIndex ] || c.globalize ) || {}, + days = ts.dates.weekdays[ options.lang || 'en' ], + xref = ts.dates.weekdaysXref; + if ( c.ignoreCase ) { + str = str.toLowerCase(); + } + for ( day in days ) { + if ( typeof day === 'string' ) { + d = days[ day ]; + if ( c.ignoreCase ) { + d = d.toLowerCase(); + } + if ( str.match( d ) ) { + num = $.inArray( day, xref ); + return num > -1 ? num : str; + } + } + } + } + return str; + }, + type: 'numeric' + }); + + // useful when a group widget date column is set to "group-date-week" + // and you want to only sort on the day of the week ignore the actual date, month and year + ts.addParser({ + id: 'weekday-index', + is: function() { + return false; + }, + format: function( str, table ) { + if ( str ) { + var c = table.config, + date = new Date( str ); + if ( date instanceof Date && isFinite( date ) ) { + // use a specific date that started with that weekday so sorting is only going to be + // based on the day of the week and not the date, month or year + return new Date( '1/' + ( date.getDay() + 1 ) + '/' + ts.dates.weekStartList[ c.weekStarts || 'sun' ] ); + } + } + return str; + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date.js new file mode 100644 index 0000000..313288a --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-date.js @@ -0,0 +1,38 @@ +/*! Parser: dates - updated 5/24/2017 (v2.28.11) */ +/* Extract dates using popular natural language date parsers */ +/*jshint jquery:true */ +/*global Sugar*/ +;(function($) { + 'use strict'; + + /*! Sugar (https://sugarjs.com/docs/#/DateParsing) */ + /* demo: http://jsfiddle.net/Mottie/7z0ss5xn/ */ + $.tablesorter.addParser({ + id: 'sugar', + is: function() { + return false; + }, + format: function(s) { + // Add support for sugar v2.0+ + var create = Date.create || Sugar.Date.create, + date = create ? create(s) : s ? new Date(s) : s; + return date instanceof Date && isFinite(date) ? date.getTime() : s; + }, + type: 'numeric' + }); + + /*! Datejs (http://www.datejs.com/) */ + /* demo: http://jsfiddle.net/Mottie/zge0L2u6/ */ + $.tablesorter.addParser({ + id: 'datejs', + is: function() { + return false; + }, + format: function(s) { + var date = Date.parse ? Date.parse(s) : s ? new Date(s) : s; + return date instanceof Date && isFinite(date) ? date.getTime() : s; + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-duration.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-duration.js new file mode 100644 index 0000000..0848cf7 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-duration.js @@ -0,0 +1,67 @@ +/*! Parser: duration & countdown - updated 2/7/2015 (v2.19.0) */ +/*jshint jquery:true, unused:false */ +;(function($) { + 'use strict'; + + // If any number > 9999, then set table.config.durationLength = 5 + // The below regex matches this duration example: 1y 23d 12h 44m 9s + $.tablesorter.addParser({ + id: 'duration', + is: function() { + return false; + }, + format: function(s, table) { + var i, time, + c = table.config, + t = '', + duration = '', + len = c.durationLength || 4, + str = new Array(len + 1).join('0'), + labels = (c.durationLabels || '(?:years|year|y),(?:days|day|d),(?:hours|hour|h),(?:minutes|minute|min|m),(?:seconds|second|sec|s)').split(/\s*,\s*/), + llen = labels.length; + // build regex + if (!c.durationRegex) { + for (i = 0; i < llen; i++) { + t += '(?:(\\d+)\\s*' + labels[i] + '\\s*)?'; + } + c.durationRegex = new RegExp(t, 'i'); + } + // remove commas from value + time = ( c.usNumberFormat ? s.replace(/,/g, '') : s.replace( /(\d)(?:\.|\s*)(\d)/g, '$1$2') ).match(c.durationRegex); + for (i = 1; i < llen + 1; i++) { + duration += ( str + ( time[i] || 0 ) ).slice(-len); + } + return duration; + }, + type: 'text' + }); + + /*! Countdown parser ( hh:mm:ss ) */ + /* Added 2/7/2015 (v2.19.0) - see http://stackoverflow.com/a/27023733/145346 */ + $.tablesorter.addParser({ + id: 'countdown', + is: function () { + return false; + }, + format: function ( text, table ) { + // change maxDigits to 4, if values go > 999 + // or to 5 for values > 9999, etc. + var maxDigits = table.config.durationLength || 4, + // prefix contains leading zeros that are tacked + prefix = new Array( maxDigits + 1 ).join( '0' ), + // split time into blocks + blocks = text.split( /\s*:\s*/ ), + len = blocks.length, + result = []; + // add values in reverse, so if there is only one block + // ( e.g. '10' ), then it would be the time in seconds + while ( len ) { + result.push( ( prefix + ( blocks[ --len ] || 0 ) ).slice( -maxDigits ) ); + } + // reverse the results and join them + return result.length ? result.reverse().join( '' ) : text; + }, + type: 'text' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-feet-inch-fraction.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-feet-inch-fraction.js new file mode 100644 index 0000000..6abdb3c --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-feet-inch-fraction.js @@ -0,0 +1,63 @@ +/*! Parser: distance *//* +* This parser will parser numbers like 5'10" (5 foot 10 inches) +* and 31½ into sortable values. +* Demo: http://jsfiddle.net/Mottie/abkNM/154/ +*/ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter; + ts.symbolRegex = /[\u215b\u215c\u215d\u215e\u00bc\u00bd\u00be]/g; + ts.processFractions = function(n, table) { + if (n) { + var t, p = 0; + n = $.trim(n.replace(/\"/, '')); + // look for a space in the first part of the number: '10 3/4' and save the '10' + if (/\s/.test(n)) { + p = ts.formatFloat(n.split(' ')[0], table); + // remove stuff to the left of the space + n = $.trim(n.substring(n.indexOf(' '), n.length)); + } + // look for a '/' to calculate fractions + if (/\//g.test(n)) { + t = n.split('/'); + // turn 3/4 into .75; make sure we don't divide by zero + n = p + parseInt(t[0], 10) / parseInt(t[1] || 1, 10); + // look for fraction symbols + } else if (ts.symbolRegex.test(n)) { + n = p + n.replace(ts.symbolRegex, function(m) { + return { + '\u215b' : '.125', // 1/8 + '\u215c' : '.375', // 3/8 + '\u215d' : '.625', // 5/8 + '\u215e' : '.875', // 7/8 + '\u00bc' : '.25', // 1/4 + '\u00bd' : '.5', // 1/2 + '\u00be' : '.75' // 3/4 + }[m]; + }); + } + } + return n || 0; + }; + + $.tablesorter.addParser({ + id: 'distance', + is: function() { + // return false so this parser is not auto detected + return false; + }, + format: function(s, table) { + if (s === '') { return ''; } + // look for feet symbol = ' + // very generic test to catch 1.1', 1 1/2' and 1½' + var d = (/^\s*\S*(\s+\S+)?\s*\'/.test(s)) ? s.split(/\'/) : [ 0, s ], + f = ts.processFractions(d[0], table), // feet + i = ts.processFractions(d[1], table); // inches + return (/[\'\"]/).test(s) ? parseFloat(f) + (parseFloat(i) / 12 || 0) : parseFloat(f) + parseFloat(i); + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-file-type.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-file-type.js new file mode 100644 index 0000000..9e211b5 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-file-type.js @@ -0,0 +1,95 @@ +/*! Parser: filetype - updated 11/10/2015 (v2.24.4) *//* + * When a file type extension is found, the equivalent name is + * prefixed into the parsed data, so sorting occurs in groups + */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + // basic list from http://en.wikipedia.org/wiki/List_of_file_formats + // To add a custom equivalent, define: + // $.tablesorter.fileTypes.equivalents['xx'] = 'A|B|C'; + $.tablesorter.fileTypes = { + // divides filetype extensions in the equivalent list below + separator : '|', + equivalents : { + '3D Image' : '3dm|3ds|dwg|max|obj', + 'Audio' : 'aif|aac|ape|flac|la|m4a|mid|midi|mp2|mp3|ogg|ra|raw|rm|wav|wma', + 'Compressed' : '7z|bin|cab|cbr|gz|gzip|iso|lha|lz|rar|tar|tgz|zip|zipx|zoo', + 'Database' : 'csv|dat|db|dbf|json|ldb|mdb|myd|pdb|sql|tsv|wdb|wmdb|xlr|xls|xlsx|xml', + 'Development' : 'asm|c|class|cls|cpp|cc|cs|cxx|cbp|cs|dba|fla|h|java|lua|pl|py|pyc|pyo|sh|sln|r|rb|vb', + 'Document' : 'doc|docx|odt|ott|pages|pdf|rtf|tex|wpd|wps|wrd|wri', + 'Executable' : 'apk|app|com|exe|gadget|lnk|msi', + 'Fonts' : 'eot|fnt|fon|otf|ttf|woff', + 'Icons' : 'ani|cur|icns|ico', + 'Images' : 'bmp|gif|jpg|jpeg|jpe|jp2|pic|png|psd|tga|tif|tiff|wmf|webp', + 'Presentation' : 'pps|ppt', + 'Published' : 'chp|epub|lit|pub|ppp|fm|mobi', + 'Script' : 'as|bat|cgi|cmd|jar|js|lua|scpt|scptd|sh|vbs|vb|wsf', + 'Styles' : 'css|less|sass', + 'Text' : 'info|log|md|markdown|nfo|tex|text|txt', + 'Vectors' : 'awg|ai|eps|cdr|ps|svg', + 'Video' : 'asf|avi|flv|m4v|mkv|mov|mp4|mpe|mpeg|mpg|ogg|rm|rv|swf|vob|wmv', + 'Web' : 'asp|aspx|cer|cfm|htm|html|php|url|xhtml' + } + }; + + $.tablesorter.addParser({ + id: 'filetype', + is: function() { + return false; + }, + format: function(s, table) { + var t, + c = table.config, + wo = c.widgetOptions, + groupSeparator = wo.group_separator || '-', + i = s.lastIndexOf('.'), + sep = $.tablesorter.fileTypes.separator, + m = $.tablesorter.fileTypes.matching, + types = $.tablesorter.fileTypes.equivalents; + if (!m) { + // make a string to 'quick' match the existing equivalents + t = []; + $.each(types, function(i, v) { + t.push(v); + }); + m = $.tablesorter.fileTypes.matching = sep + t.join(sep) + sep; + } + if (i >= 0) { + t = sep + s.substring(i + 1, s.length) + sep; + if (m.indexOf(t) >= 0) { + for (i in types) { + if ((sep + types[i] + sep).indexOf(t) >= 0) { + // groupSeparator may use a regular expression! + return i + ( groupSeparator.toString().charAt(0) !== '/' ? wo.group_separator : '-' ) + s; + } + } + } + } + return s; + }, + type: 'text' + }); + + // sort by file extension + // converts "this.is.an.image.jpg" into "jpg.this.is.an.image" + $.tablesorter.addParser({ + id: 'file-extension', + is: function() { + return false; + }, + format: function( str ) { + var ext, + parts = str.split( '.' ); + if ( parts.length ) { + ext = parts.pop(); + parts.unshift( ext ); + return parts.join( '.' ); + } + return str; + }, + type: 'text' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-globalize.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-globalize.js new file mode 100644 index 0000000..fddf043 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-globalize.js @@ -0,0 +1,69 @@ +/*! Parser: jQuery Globalize - updated 11/2/2015 (v2.24.1) */ +/* Extract localized data using jQuery's Globalize parsers; set + Globalize.locale( 'xx' ) in the globalize settings */ +/*jshint jquery:true, newcap: false */ +/*global Globalize:false */ +;( function( $ ) { + 'use strict'; + + /*! jQuery Globalize date parser (https://github.com/jquery/globalize#date-module) */ + $.tablesorter.addParser({ + id: 'globalize-date', + is: function () { + return false; + }, + format: function ( str, table, cell, cellIndex ) { + var globalize, date, + c = table.config, + // add options to 'config.globalize' for all columns --> globalize : { skeleton: 'GyMMMd' } + // or per column by using the column index --> globalize : { 0 : { datetime: 'medium' } } + options = c.globalize && ( c.globalize[ cellIndex ] || c.globalize ) || {}; + if ( Globalize ) { + globalize = typeof options.Globalize === 'object' ? + // initialized Globalize object + options.Globalize : + // Globalize initialized from "lang" option + Globalize( options.lang || 'en' ); + if ( !options.Globalize ) { + // cache the object + options.Globalize = globalize; + } + } + date = globalize && globalize.dateParser ? globalize.dateParser( options )( str ) : + str ? new Date( str ) : str; + return date instanceof Date && isFinite( date ) ? date.getTime() : str; + }, + type: 'numeric' + }); + + /*! jQuery Globalize number parser (https://github.com/jquery/globalize#number-module) */ + $.tablesorter.addParser({ + id: 'globalize-number', + is: function () { + return false; + }, + format: function ( str, table, cell, cellIndex ) { + var globalize, num, + c = table.config, + // add options to 'config.globalize' for all columns --> globalize : { skeleton: 'GyMMMd' } + // or per column by using the column index --> globalize : { 0 : { datetime: 'medium' } } + options = c.globalize && ( c.globalize[ cellIndex ] || c.globalize ) || {}; + if ( Globalize ) { + globalize = typeof options.Globalize === 'object' ? + // initialized Globalize object + options.Globalize : + // Globalize initialized from "lang" option + Globalize( options.lang || 'en' ); + if ( !options.Globalize ) { + // cache the object + options.Globalize = globalize; + } + } + num = globalize && globalize.numberParser ? globalize.numberParser( options )( str ) : + str ? $.tablesorter.formatFloat( ( str || '' ).replace( /[^\w,. \-()]/g, '' ), table ) : str; + return str && typeof num === 'number' ? num : str; + }, + type: 'numeric' + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-huge-numbers.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-huge-numbers.js new file mode 100644 index 0000000..7cabf24 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-huge-numbers.js @@ -0,0 +1,21 @@ +/*! Parser: hugeNumbers - updated 3/1/2016 (v2.25.5) *//* + * See https://github.com/Mottie/tablesorter/issues/1161 + */ +/*jshint jquery:true */ +;( function( $ ) { + 'use strict'; + + $.tablesorter.addParser({ + id: 'hugeNumbers', + is : function() { + return false; + }, + format : function( str ) { + // add commas every 12 digits; Number.MAX_SAFE_INTEGER is 16 digits long + // regex modified from: http://stackoverflow.com/a/2901298/145346 + return str.replace(/\B(?=(\d{12})+(?!\d))/g, ','); + }, + type : 'text' + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-ignore-articles.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-ignore-articles.js new file mode 100644 index 0000000..1de34ed --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-ignore-articles.js @@ -0,0 +1,61 @@ +/*! Parser: ignoreArticles - updated 9/15/2014 (v2.17.8) *//* + * This parser will remove 'The', 'A' and 'An' from the beginning of a book + * or movie title, so it sorts by the second word or number + * Demo: http://jsfiddle.net/Mottie/abkNM/5/ + */ +/*jshint browser: true, jquery:true, unused:false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter; + + // basic list from http://en.wikipedia.org/wiki/Article_%28grammar%29 + ts.ignoreArticles = { + 'en' : 'the, a, an', + 'de' : 'der, die, das, des, dem, den, ein, eine, einer, eines, einem, einen', + 'nl' : 'de, het, de, een', + 'es' : 'el, la, lo, los, las, un, una, unos, unas', + 'pt' : 'o, a, os, as, um, uma, uns, umas', + 'fr' : 'le, la, l\'_, les, un, une, des', + 'it' : 'il, lo, la, l\'_, i, gli, le, un\', uno, una, un', + 'hu' : 'a, az, egy' + }; + + // To add a custom parser, define: + // $.tablesorter.ignoreArticles['xx'] = 'A, B, C'; + // and then set the language id 'xx' in the headers option + // ignoreArticles : 'xx' + + ts.addParser({ + id: 'ignoreArticles', + is: function() { + return false; + }, + format: function(s, table, cell, cellIndex) { + var art, ignore, lang, + c = table.config, + str = s || ''; + if ( !(c.headers && c.headers[cellIndex] && c.headers[cellIndex].ignoreArticlesRegex) ) { + // initialize - save regex in c.headers[cellIndex].ignoreArticlesRegex + if (!c.headers) { c.headers = {}; } + if (!c.headers[cellIndex]) { c.headers[cellIndex] = {}; } + lang = ts.getData( c.$headers.eq(cellIndex), ts.getColumnData( table, c.headers, cellIndex ), 'ignoreArticles' ); + art = (ts.ignoreArticles[lang] || 'the, a, an' ) + ''; + c.headers[cellIndex].ignoreArticlesRegex = new RegExp('^(' + $.trim( art.split(/\s*\,\s*/).join('\\s|') + '\\s' ).replace('_\\s', '') + ')', 'i'); + // exception regex stored in c.headers[cellIndex].ignoreArticlesRegex2 + ignore = ts.getData( c.$headers.eq(cellIndex), ts.getColumnData( table, c.headers, cellIndex ), 'ignoreArticlesExcept' ); + c.headers[cellIndex].ignoreArticlesRegex2 = ignore !== '' ? new RegExp('^(' + ignore.replace(/\s/g, '\\s') + ')', 'i') : ''; + } + art = c.headers[cellIndex].ignoreArticlesRegex; + if (art.test(str)) { + ignore = c.headers[cellIndex].ignoreArticlesRegex2; + if ( !(ignore && ignore.test(str)) ) { + return str.replace(art, ''); + } + } + return str; + }, + type: 'text' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-image.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-image.js new file mode 100644 index 0000000..59eb023 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-image.js @@ -0,0 +1,20 @@ +/*! Parser: image - new 7/17/2014 (v2.17.5) */ +/* alt attribute parser for jQuery 1.7+ & tablesorter 2.7.11+ */ +/* NOTE! Moved to jquery.tablesorter.js (core) in v2.18.0 */ +/*jshint jquery:true, unused:false */ +;(function($) { + 'use strict'; + + $.tablesorter.addParser({ + id: 'image', + is: function() { + return false; + }, + format: function(s, table, cell) { + return $(cell).find('img').attr(table.config.imgAttr || 'alt') || s; + }, + parsed : true, // filter widget flag + type: 'text' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-input-select.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-input-select.js new file mode 100644 index 0000000..129952e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-input-select.js @@ -0,0 +1,359 @@ +/*! Parser: input & select - updated 2018-07-10 (v2.30.7) *//* + * for jQuery 1.7+ & tablesorter 2.7.11+ + * Demo: http://mottie.github.com/tablesorter/docs/example-widget-grouping.html + */ +/*jshint browser: true, jquery:true, unused:false */ +;( function( $ ) { + 'use strict'; + + var updateServer = function( /* event, $table, $input */ ) { + // do something here to update your server, if needed + // event = change event object + // $table = jQuery object of the table that was just updated + // $input = jQuery object(s) of the input or select that was modified; in v2.24.6, + // if the thead has a checkbox, $input may include multiple elements + }; + + // Custom parser for parsing input values + // updated dynamically using the 'change' function below + $.tablesorter.addParser({ + id : 'inputs', + is : function() { + return false; + }, + format : function( txt, table, cell ) { + var $input = $( cell ).find( 'input' ); + return $input.length ? $input.val() : txt; + }, + parsed : true, // filter widget flag + type : 'text' + }); + + $.tablesorter.addParser({ + id : 'inputs-numeric', + is : function() { + return false; + }, + format : function( txt, table, cell ) { + var $input = $( cell ).find( 'input' ); + var val = $input.length ? $input.val() : txt, + num = $.tablesorter.formatFloat( ( val || '' ).replace( /[^\w,. \-()]/g, '' ), table ); + return txt && typeof num === 'number' ? num : + txt ? $.trim( txt && table.config.ignoreCase ? txt.toLocaleLowerCase() : txt ) : txt; + }, + parsed : true, // filter widget flag + type : 'numeric' + }); + + // Custom parser for including checkbox status if using the grouping widget + // updated dynamically using the 'change' function below + $.tablesorter.addParser({ + id : 'checkbox', + is : function() { + return false; + }, + format : function( txt, table, cell ) { + var $cell = $( cell ), + wo = table.config.widgetOptions, + // returning plain language here because this is what is shown in the + // group headers - change it as desired + status = wo.group_checkbox ? wo.group_checkbox : [ 'checked', 'unchecked' ], + $input = $cell.find( 'input[type="checkbox"]' ), + isChecked = $input.length ? $input[ 0 ].checked : ''; + return $input.length ? status[ isChecked ? 0 : 1 ] : txt; + }, + parsed : true, // filter widget flag + type : 'text' + }); + + $.tablesorter.addParser({ + id: 'radio', + is: function() { + return false; + }, + format: function(txt, table, cell) { + var $input = $(cell).find('input:checked'); + return $input.length ? $input.val() : txt; + }, + parsed: true, + type: 'text' + }); + + // Custom parser which returns the currently selected options + // updated dynamically using the 'change' function below + $.tablesorter.addParser({ + id : 'select', + is : function() { + return false; + }, + format : function( txt, table, cell ) { + var $select = $( cell ).find( 'select' ); + return $select.length ? $select.val() : txt; + }, + parsed : true, // filter widget flag + type : 'text' + }); + + // Select parser to get the selected text + $.tablesorter.addParser({ + id : 'select-text', + is : function() { + return false; + }, + format : function( txt, table, cell ) { + var $select = $( cell ).find( 'select' ); + return $select.length ? $select.find( 'option:selected' ).text() || '' : txt; + }, + parsed : true, // filter widget flag + type : 'text' + }); + + // Custom parser for parsing textarea values + // updated dynamically using the 'change' function below + $.tablesorter.addParser({ + id : 'textarea', + is : function() { + return false; + }, + format : function( txt, table, cell ) { + var $textarea = $( cell ).find( 'textarea' ); + return $textarea.length ? $textarea.val() : txt; + }, + parsed : true, // filter widget flag + type : 'text' + }); + + // update defaults for validator; values must be falsy + $.tablesorter.defaults.checkboxClass = ''; + $.tablesorter.defaults.checkboxVisible = ''; + + // update select and all input types in the tablesorter cache when the change event fires. + // This method only works with jQuery 1.7+ + // you can change it to use delegate (v1.4.3+) or live (v1.3+) as desired + // if this code interferes somehow, target the specific table $('#mytable'), instead of $('table') + $( function() { + if ( !$.fn.on ) { return; } + var toggleRowClass = function( $row, checkboxClass, indx, isChecked ) { + // adding class to row, indicating that a checkbox is checked; includes + // a column index in case more than one checkbox happens to be in a row + $row.toggleClass( checkboxClass + '-' + indx, isChecked ); + // don't remove checked class if other columns have a check + if ( ( $row[0].className || '' ).match( checkboxClass + '-' ) ) { + $row.addClass( checkboxClass ); + } else { + $row.removeClass( checkboxClass ); + } + }, + updateCheckbox = function($el, state) { + if ($el.length && $el[0].nodeName !== 'INPUT') { + $el = $el.find( 'input[type="checkbox"]' ); + } + if ($el.length) { + var ua = window.navigator.userAgent; + if (state === 'indeterminate') { + // needed for IE + $el.prop('checked', !(ua.indexOf('Trident/') > -1 || ua.indexOf('Edge/') > -1)); + $el.prop('indeterminate', true); + } else { + $el.prop('checked', state); + $el.prop('indeterminate', false); + } + } + }, + updateHeaderCheckbox = function( $table, checkboxClass ) { + var $sticky, + $rows = $table.children( 'tbody' ).children( ':visible' ), // (include child rows?) + len = $rows.length, + c = $table[0].config, + wo = c && c.widgetOptions, + $headers = c && c.$headers.add( $( c.namespace + '_extra_headers' ) ) || $table.children( 'thead' ), + hasSticky = wo && wo.$sticky; + // set indeterminate state on header checkbox + $headers.find( 'input[type="checkbox"]' ).each( function() { + if (hasSticky) { + $sticky = hasSticky.find( '[data-column="' + column + '"]' ); + } + var column = $( this ).closest( 'td, th' ).attr( 'data-column' ), + vis = $rows.filter( '.' + checkboxClass + '-' + column ).length, + allChecked = vis === len && len > 0; + if ( vis === 0 || allChecked ) { + updateCheckbox($(this), allChecked); + if ($sticky) { + updateCheckbox($sticky, allChecked); + } + } else { + updateCheckbox($(this), 'indeterminate'); + if ($sticky) { + updateCheckbox($sticky, 'indeterminate'); + } + } + }); + + }; + + $( 'table' ).on( 'tablesorter-initialized updateComplete', function() { + this.tablesorterBusy = false; + var namespace = '.parser-forms'; + $( this ) + // add namespace to table in case of version mismatch (v2.28.10) + .addClass( this.config.namespace.slice(1) ) + .children( 'tbody' ) + .off( namespace ) + .on( 'mouseleave' + namespace, function( event ) { + // make sure we restore original values (trigger blur) + // isTbody is needed to prevent the select from closing in IE + // see https://connect.microsoft.com/IE/feedbackdetail/view/962618/ + if ( event.target.nodeName === 'TBODY' ) { + $( ':focus' ).blur(); + } + }) + .on( 'focus' + namespace, 'select, input:not([type=checkbox]), textarea', function( event ) { + var $row = $( event.target ).closest( 'tr' ), + c = $row.closest( 'table' )[0].config; + if ( !c || c && c.ignoreChildRow && $row.hasClass( c.cssChildRow ) ) { + return; + } + $( this ).data( 'ts-original-value', this.value ); + }) + .on( 'blur' + namespace, 'input:not([type=checkbox]), textarea', function( event ) { + var $row = $( event.target ).closest( 'tr' ), + c = $row.closest( 'table' )[0].config; + if ( !c || c && c.ignoreChildRow && $row.hasClass( c.cssChildRow ) ) { + return; + } + // restore input value; + // 'change' is triggered before 'blur' so this doesn't replace the new update with the original + this.value = $( this ).data( 'ts-original-value' ); + }) + .on( 'change keyup '.split( ' ' ).join( namespace + ' ' ), 'select, input, textarea', function( event ) { + var $row = $( this ).closest( 'tr' ), + c = $row.closest( 'table' )[0].config; + if ( !c || c && c.ignoreChildRow && $row.hasClass( c.cssChildRow ) ) { + return; + } + if ( event.which === 27 && !( this.nodeName === 'INPUT' && this.type === 'checkbox' ) ) { + // escape: restore original value + this.value = $( this ).data( 'ts-original-value' ); + return; + } + // Update cell cache using... select: change, input: enter or textarea: alt + enter + if ( event.type === 'change' || + ( event.type === 'keyup' && event.which === 13 && + ( event.target.nodeName === 'INPUT' || event.target.nodeName === 'TEXTAREA' && event.altKey ) ) ) { + var undef, checkboxClass, + $target = $( event.target ), + isCheckbox = event.target.type === 'checkbox', + $cell = $target.closest( 'td' ), + indx = $cell[ 0 ].cellIndex, + busy = c.table.tablesorterBusy, + $hdr = c.$headerIndexed && c.$headerIndexed[ indx ] || [], + val = isCheckbox ? event.target.checked : $target.val(); + // abort if not a tablesorter table, or busy + if ( $.isEmptyObject( c ) || busy !== false ) { + return; + } + if ( isCheckbox ) { + checkboxClass = c.checkboxClass || 'checked'; + toggleRowClass( $cell.closest( 'tr' ), checkboxClass, indx, val ); + updateHeaderCheckbox( c.$table, checkboxClass ); + } + // don't use updateCell if column is set to 'sorter-false' and 'filter-false', + // or column is set to 'parser-false' + if ( $hdr.length && ( $hdr.hasClass( 'parser-false' ) || + ( $hdr.hasClass( 'sorter-false' ) && $hdr.hasClass( 'filter-false' ) ) ) || + // table already updating; get out of here, we might be in an endless loop (in IE)! See #971 + ( event.type === 'change' && c.table.isUpdating ) ) { + return; + } + // ignore change event if nothing changed + if ( c && val !== $target.data( 'ts-original-value' ) || isCheckbox ) { + $target.data( 'ts-original-value', val ); + c.table.tablesorterBusy = true; + // pass undefined resort value so it falls back to config.resort setting + $.tablesorter.updateCell( c, $cell, undef, function() { + updateServer( event, c.$table, $target ); + c.table.tablesorterBusy = false; + }); + } + } + }); + + // add code for a checkbox in the header to set/unset all checkboxes in a column + if ( $( this ).children( 'thead' ).find( 'input[type="checkbox"]' ) ) { + $( this ) + .off( namespace ) + .on( 'tablesorter-ready' + namespace, function() { + var checkboxClass, + $table = $( this ), + c = $table.length && $table[ 0 ].config; + if ( !$.isEmptyObject( c ) ) { + this.tablesorterBusy = true; + checkboxClass = c && c.checkboxClass || 'checked'; + // set indeterminate state on header checkbox + updateHeaderCheckbox( $table, checkboxClass ); + this.tablesorterBusy = false; + } + }) + .children( 'thead' ) + .add( this.config.widgetOptions.$sticky ) + .off( namespace ) + // modified from http://jsfiddle.net/abkNM/6163/ + // click needed for IE; a change isn't fired when going from an indeterminate checkbox to + // either checked or unchecked + .on( 'click' + namespace + ' change' + namespace, 'input[type="checkbox"]', function( event ) { + var c, undef, onlyVisible, column, $target, isParsed, checkboxClass, + $checkbox = $( this ), + isChecked = this.checked, + $table = $checkbox.closest( 'table' ), + isSticky = $table.length && $table[0].className.match(/(tablesorter\w+)_extra_table/); + if (isSticky) { + isSticky = isSticky[1]; + $table = $('.' + isSticky + ':not(.' + isSticky + '_extra_table)'); + } + c = $table.length && $table[ 0 ].config; + if ( $table.length && c && !$table[ 0 ].tablesorterBusy ) { + column = parseInt( $checkbox.closest( 'td, th' ).attr( 'data-column' ), 10 ); + isParsed = c.parsers[ column ].id === 'checkbox'; + onlyVisible = c.checkboxVisible; + $table[ 0 ].tablesorterBusy = true; // prevent "change" event from calling updateCell numerous times (see #971) + updateCheckbox( + $target = $table + .children( 'tbody' ) + .children( 'tr' + ( typeof onlyVisible === 'undefined' || onlyVisible === true ? ':visible' : '' ) ) + .children( ':nth-child(' + ( column + 1 ) + ')' ), + isChecked + ); + // add checkbox class names to row + checkboxClass = c.checkboxClass || 'checked'; + $target.each( function() { + toggleRowClass( $( this ).closest( 'tr' ), checkboxClass, column, isChecked ); + }); + if (isSticky) { + // make main table checkbox match sticky header checkbox + updateCheckbox($table.children( 'thead' ).find( '[data-column="' + column + '"]' ), isChecked); + } else if (c.widgetOptions.$sticky) { + updateCheckbox(c.widgetOptions.$sticky.find( 'thead' ).find( '[data-column="' + column + '"]' ), isChecked); + } + updateHeaderCheckbox( $table, checkboxClass ); + if ( isParsed ) { + // only update cache if checkboxes are being sorted + $.tablesorter.update( c, undef, function() { + updateServer( event, $table, $target ); + $table[ 0 ].tablesorterBusy = false; + }); + } else { + updateServer( event, $table, $target ); + $table[ 0 ].tablesorterBusy = false; + } + // needed for IE8 + return true; + } + // update already going on, don't do anything! + return false; + }); + } + + }); + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-leading-zeros.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-leading-zeros.js new file mode 100644 index 0000000..99ba16b --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-leading-zeros.js @@ -0,0 +1,34 @@ +/*! Parser: leading zeros - updated 4/2/2017 (v2.28.6) */ +/* jshint jquery:true, unused:false */ +;( function( $ ) { + 'use strict'; + + var ts = $.tablesorter, + // modify this value to increase precision as needed + precision = 1e-10; + + ts.addParser({ + id: 'leadingZeros', + is: function() { + return false; + }, + format: function( s, table ) { + var val = ( s || '' ).replace( ts.regex.nondigit, '' ), + number = ts.formatFloat( val, table ), + str = number.toString(); + if ( + !isNaN( number ) && + // eslint-disable-next-line eqeqeq + number == val && // jshint ignore:line + val.length !== str.length + ) { + // subtract a decimal equivalent of the string length + // so "0001" sorts before "01" + number -= precision * ( s.length - str.length ); + } + return number; + }, + type: 'number' + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-metric.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-metric.js new file mode 100644 index 0000000..2fff9d9 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-metric.js @@ -0,0 +1,92 @@ +/*! Parser: metric *//* + * Demo: http://jsfiddle.net/Mottie/abkNM/382/ + * Set the metric name in the header (defaults to 'm|meter'), e.g. + * <th data-metric-name-abbr="b|B" data-metric-name-full="byte|Byte|BYTE">HDD Size</th> + * <th data-metric-name="m|meter">Distance</th> <!-- data-metric-name is deprecated in v2.22.2 --> + */ +/*jshint jquery:true */ +;( function( $ ) { + 'use strict'; + + var prefixes = { + // 'prefix' : [ base 10, base 2 ] + // skipping IEEE 1541 defined prefixes: kibibyte, mebibyte, etc, for now. + 'Y|Yotta|yotta' : [ 1e24, Math.pow(1024, 8) ], // 1024^8 + 'Z|Zetta|zetta' : [ 1e21, Math.pow(1024, 7) ], // 1024^7 + 'E|Exa|exa' : [ 1e18, Math.pow(1024, 6) ], // 1024^6 + 'P|Peta|peta' : [ 1e15, Math.pow(1024, 5) ], // 1024^5 + 'T|Tera|tera' : [ 1e12, Math.pow(1024, 4) ], // 1024^4 + 'G|Giga|giga' : [ 1e9, Math.pow(1024, 3) ], // 1024^3 + 'M|Mega|mega' : [ 1e6, Math.pow(1024, 2) ], // 1024^2 + 'k|Kilo|kilo' : [ 1e3, 1024 ], // 1024 + // prefixes below here are rarely, if ever, used in binary + 'h|hecto' : [ 1e2, 1e2 ], + 'da|deka' : [ 1e1, 1e1 ], + 'd|deci' : [ 1e-1, 1e-1 ], + 'c|centi' : [ 1e-2, 1e-2 ], + 'm|milli' : [ 1e-3, 1e-3 ], + 'µ|micro' : [ 1e-6, 1e-6 ], + 'n|nano' : [ 1e-9, 1e-9 ], + 'p|pico' : [ 1e-12, 1e-12 ], + 'f|femto' : [ 1e-15, 1e-15 ], + 'a|atto' : [ 1e-18, 1e-18 ], + 'z|zepto' : [ 1e-21, 1e-21 ], + 'y|yocto' : [ 1e-24, 1e-24 ] + }, + // the \\d+ will not catch digits with spaces, commas or decimals; so use the value from n instead + RegLong = '(\\d+)(\\s+)?([Zz]etta|[Ee]xa|[Pp]eta|[Tt]era|[Gg]iga|[Mm]ega|kilo|hecto|deka|deci|centi|milli|micro|nano|pico|femto|atto|zepto|yocto)(', + RegAbbr = '(\\d+)(\\s+)?(Z|E|P|T|G|M|k|h|da|d|c|m|µ|n|p|f|a|z|y)(', + // make these case-insensitive because we all forget the case for these binary values + byteTest = /^[b|bit|byte|o|octet]/i; + + $.tablesorter.addParser({ + id: 'metric', + is: function() { + return false; + }, + format: function(txt, table, cell, cellIndex) { + var unit, isBinary, nameLong, nameAbbr, + // default base unit name + base = 'm|meter', + // process number here to get a numerical format (us or eu) + num = $.tablesorter.formatFloat( txt.replace(/[^\w,. \-()]/g, ''), table ), + $t = table.config.$headerIndexed[ cellIndex ], + regex = $t.data( 'metric' ); + if ( !regex ) { + // stored values + unit = ( $t.attr('data-metric-name') || base ).split( '|' ); + nameLong = $t.attr( 'data-metric-name-full' ) || ''; + nameAbbr = $t.attr( 'data-metric-name-abbr' ) || ''; + regex = [ nameLong || unit[1] || unit[0].substring(1), nameAbbr || unit[0] ]; + isBinary = byteTest.test( regex.join( '' ) ); + // adding 'data-metric-name-full' which would contain 'byte|BYTE|Byte' etc + regex[2] = new RegExp( RegLong + ( + ( nameLong === '' ? '' : nameLong + '|' + nameAbbr ) || + // with data-metric-name='b|byte', we end up with 'b|B|byte|BYTE' - maybe not the best solution for case-insensitivity + ( ( isBinary ? regex[0].toLowerCase() + '|' + regex[0].toUpperCase() : regex[0] ) + '|' + + ( isBinary ? regex[1].toLowerCase() + '|' + regex[1].toUpperCase() : regex[1] ) ) ) + + ')' ); + // adding 'data-metric-name-abbr' which would contain 'b|B' etc + regex[3] = new RegExp( RegAbbr + ( nameAbbr || + ( ( isBinary ? regex[1].toLowerCase() + '|' + regex[1].toUpperCase() : regex[1] ) ) ) + + ')' ); + $t.data( 'metric', regex ); + } + // find match to full name or abbreviation + unit = txt.match( regex[2] ) || txt.match( regex[3] ); + if ( unit ) { + for ( base in prefixes ) { + if ( unit[3].match( base ) ) { + // exception when using binary prefix + // change base for binary use + isBinary = byteTest.test( unit[4] ) ? 1 : 0; + return num * prefixes[ base ][ isBinary ]; + } + } + } + return num; + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-named-numbers.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-named-numbers.js new file mode 100644 index 0000000..a39930f --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-named-numbers.js @@ -0,0 +1,122 @@ +/*! Parser: namedNumbers - updated 10/26/2014 (v2.18.0) *//* + * code modified from http://stackoverflow.com/a/12014376/145346 + */ +/*jshint jquery:true */ +;(function($) { + 'use strict'; + + // Change language of the named numbers as needed + var named = { + negative: [ 'negative', 'minus' ], + numbers : { + 'zero' : 0, + 'one' : 1, + 'two' : 2, + 'three' : 3, + 'four' : 4, + 'five' : 5, + 'six' : 6, + 'seven' : 7, + 'eight' : 8, + 'nine' : 9, + 'ten' : 10, + 'eleven' : 11, + 'twelve' : 12, + 'thirteen' : 13, + 'fourteen' : 14, + 'fifteen' : 15, + 'sixteen' : 16, + 'seventeen' : 17, + 'eighteen' : 18, + 'nineteen' : 19, + 'twenty' : 20, + 'thirty' : 30, + 'forty' : 40, + 'fourty' : 40, // common misspelling + 'fifty' : 50, + 'sixty' : 60, + 'seventy' : 70, + 'eighty' : 80, + 'ninety' : 90 + }, + // special case + hundred : 'hundred', + // multiples + powers : { + 'thousand' : 1e3, + 'million' : 1e6, + 'billion' : 1e9, + 'trillion' : 1e12, + 'quadrillion' : 1e15, + 'quintillion' : 1e18, + 'sextillion' : 1e21, + 'septillion' : 1e24, + 'octillion' : 1e27, + 'nonillion' : 1e30, + 'decillion' : 1e33, + 'undecillion' : 1e36, + 'duodecillion' : 1e39, + 'tredecillion' : 1e42, + 'quattuordecillion' : 1e45, + 'quindecillion' : 1e48, + 'sexdecillion' : 1e51, + 'septendecillion' : 1e54, + 'octodecillion' : 1e57, + 'novemdecillion' : 1e60, + 'vigintillion' : 1e63, + 'unvigintillion' : 1e66, + 'duovigintillion' : 1e69, + 'trevigintillion' : 1e72, + 'quattuorvigintillion' : 1e75, + 'quinvigintillion' : 1e78, + 'sexvigintillion' : 1e81, + 'septenvigintillion' : 1e84, + 'octovigintillion' : 1e87, + 'novemvigintillion' : 1e90, + 'trigintillion' : 1e93, + 'untrigintillion' : 1e96, + 'duotrigintillion' : 1e99, + 'googl' : 1e100 + } + }, + result, group, + negativeRegex = new RegExp('(' + named.negative.join('|') + ')'), + calc = function ( rawWord, table ) { + // remove extra characters that might be next to the word + var word = rawWord.replace( /[,."']/g, '' ), + // formatFloat will deal with the commas & decimals in the number format + num = $.tablesorter.formatFloat( rawWord || '', table ), + power = named.powers.hasOwnProperty( word ) ? named.powers[ word ] : null; + num = typeof num === 'number' ? num : named.numbers.hasOwnProperty( word ) ? named.numbers[ word ] : null; + if ( num !== null ) { + group += num; + } else if ( word === named.hundred ) { + group *= 100; + } else if ( power !== null ) { + result += group * power; + group = 0; + } + }; + + $.tablesorter.addParser({ + id: 'namedNumbers', + is: function () { + return false; + }, + format: function ( str, table ) { + result = 0; + group = 0; + var indx, + arry = ( str || '' ).split( /[\s-]+/ ), + len = arry.length; + for ( indx = 0; indx < len; indx++ ) { + calc( arry[ indx ].toLowerCase(), table ); + } + result = ( result + group ) * ( str.match( negativeRegex ) ? -1 : 1 ); + // make sure to let zero get parsed, so check hasOwnProperty + return result || named.numbers.hasOwnProperty( str ) ? result : $.tablesorter.formatFloat( str || '', table ); + }, + type: 'numeric' + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-network.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-network.js new file mode 100644 index 0000000..148f53a --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-network.js @@ -0,0 +1,143 @@ +/*! Parser: network - updated 2018-01-10 (v2.29.3) */ +/* IPv4, IPv6 and MAC Addresses */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + ipv4Format, + ipv4Is; + + /*! IPv6 Address parser (WIP) *//* + * IPv6 Address (ffff:0000:0000:0000:0000:0000:0000:0000) + * needs to support short versions like '::8' or '1:2::7:8' + * and '::00:192.168.10.184' (embedded IPv4 address) + * see http://www.intermapper.com/support/tools/IPV6-Validator.aspx + */ + $.extend( ts.regex, {}, { + ipv4Validate : /((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})/, + ipv4Extract : /([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/, + + // simplified regex from http://www.intermapper.com/support/tools/IPV6-Validator.aspx + // (specifically from http://download.dartware.com/thirdparty/ipv6validator.js) + ipv6Validate : /^\s*((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/i + }); + + // used for internal testing; it's not useful to set this to true because the natural sort algorithm + // is set up to only sort solitary hex values ("ffff") vs separated hex values ("ffff.ffff") + ts.defaults.ipv6HexFormat = false; + + ts.addParser({ + id: 'ipv6Address', + is: function(s) { + return ts.regex.ipv6Validate.test(s); + }, + format: function(address, table) { + // code modified from http://zurb.com/forrst/posts/JS_Expand_Abbreviated_IPv6_Addresses-1OR + // Saved to https://gist.github.com/Mottie/7018157 + var i, t, sides, groups, groupsPresent, + hex = table ? (typeof table === 'boolean' ? table : table && table.config.ipv6HexFormat || false) : false, + fullAddress = '', + expandedAddress = '', + validGroupCount = 8; + // validGroupSize = 4; <- removed while loop + // remove any extra spaces + address = address.replace(/\s*/g, ''); + // look for embedded ipv4 + if (ts.regex.ipv4Validate.test(address)) { + groups = address.match(ts.regex.ipv4Extract); + t = ''; + for (i = 1; i < groups.length; i++) { + t += ('00' + (parseInt(groups[i], 10).toString(16)) ).slice(-2) + ( i === 2 ? ':' : '' ); + } + address = address.replace( ts.regex.ipv4Extract, t ); + } + + if (address.indexOf('::') === -1) { + // All eight groups are present + fullAddress = address; + } else { + // Consecutive groups of zeroes have been collapsed with '::'. + sides = address.split('::'); + groupsPresent = 0; + for (i = 0; i < sides.length; i++) { + groupsPresent += sides[i].split(':').length; + } + fullAddress += sides[0] + ':'; + for (i = 0; i < validGroupCount - groupsPresent; i++) { + fullAddress += '0000:'; + } + fullAddress += sides[1]; + } + groups = fullAddress.split(':'); + for (i = 0; i < validGroupCount; i++) { + // it's fastest & easiest for tablesorter to sort decimal values (vs hex) + groups[i] = hex ? ('0000' + groups[i]).slice(-4) : + ('00000' + (parseInt(groups[i], 16) || 0)).slice(-5); + expandedAddress += ( i !== validGroupCount - 1) ? groups[i] + ':' : groups[i]; + } + return expandedAddress; + }, + // uses natural sort hex compare + type: 'text' + }); + + // ipv4 address + // moved here from jquery.tablesorter.js (core file) + ipv4Is = function(s) { + return (/^\d{1,3}[\.]\d{1,3}[\.]\d{1,3}[\.]\d{1,3}$/).test(s); + }; + ipv4Format = function(s) { + var i, + a = s ? s.split('.') : '', + r = [], + l = a.length; + for (i = 0; i < l; i++) { + r.push(('000' + a[i]).slice(-3)); + } + return s ? r.join('.') : s; + }; + + /*! Parser: ipv4Address (a.k.a. ipAddress) */ + // duplicate 'ipAddress' as 'ipv4Address' (to maintain backwards compatility) + ts.addParser({ + id: 'ipAddress', + is: ipv4Is, + format: ipv4Format, + type: 'text' + }); + ts.addParser({ + id: 'ipv4Address', + is: ipv4Is, + format: ipv4Format, + type: 'text' + }); + + /*! Parser: MAC address */ + /* MAC examples: 12:34:56:78:9A:BC, 1234.5678.9ABC, 12-34-56-78-9A-BC, and 123456789ABC + */ + ts.addParser({ + id : 'MAC', + is : function() { + return false; + }, + format : function( str ) { + var indx, len, + mac = [], + val = ( str || '' ).replace( /[:.-]/g, '' ).match( /\w{2}/g ); + if ( val ) { + // not assuming all mac addresses in the column will end up with six + // groups of two to process, so it's not actually validating the address + len = val.length; + for ( indx = 0; indx < len; indx++ ) { + mac.push(( '000' + parseInt( val[ indx ], 16 ) ).slice( -3 )); + } + return mac.join('.'); + } + return str; + }, + // uses natural sort hex compare + type : 'text' + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-roman.js b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-roman.js new file mode 100644 index 0000000..62f7c8e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/parsers/parser-roman.js @@ -0,0 +1,117 @@ +/*! Parser: roman - updated 6/28/MMXIV (v2.17.3) *//* + * code modified from both: + * Steven Levithan @ http://blog.stevenlevithan.com/archives/javascript-roman-numeral-converter + * Jonathan Snook comment @ http://blog.stevenlevithan.com/archives/javascript-roman-numeral-converter#comment-16140 + */ +/*jshint jquery:true, unused:false */ +;(function($) { + 'use strict'; + + // allow lower case roman numerals, since lists use i, ii, iii, etc. + var validator = /^M*(?:D?C{0,3}|C[MD])(?:L?X{0,3}|X[CL])(?:V?I{0,3}|I[XV])$/i, + matcher = /\b([MCDLXVI]+\b)/gi, + lookup = { I:1, V:5, X:10, L:50, C:100, D:500, M:1000 }; + + $.tablesorter.addParser({ + id: 'roman', + is: function() { + return false; + }, + format: function(s) { + var val, + roman = s.toUpperCase().split(''), + num = 0; + + // roman numerals not found! + if ( !(s && validator.test(s)) ) { + return s; + } + + while (roman.length) { + val = lookup[roman.shift()]; + num += val * (val < lookup[roman[0]] ? -1 : 1); + } + + return num; + }, + type: 'numeric' + }); + + $.tablesorter.addParser({ + id: 'roman-ignore', + is: function() { + return false; + }, + format: function(s, table, cell, column) { + var val, orig, + c = table.config, + ignore = $.isArray(c.roman_ignore) ? c.roman_ignore[column] : 0, + // find roman numerals + roman = ( isNaN(ignore) ? + // ignore can be a regex or string + $.trim( s.replace(ignore, '') ) : + // or a number to ignore the last x letters... + $.trim( s.substring(0, s.length - ignore) ) + ).match(matcher), + v = validator.test(roman), + num = 0; + + // roman numerals not found! + if ( !(v) ) { + return s; + } + + // save roman numeral for replacement + orig = roman[0]; + roman = orig.toUpperCase().split(''); + + while (roman.length) { + val = lookup[roman.shift()]; + // ignore non-roman numerals + if (val) { + num += val * (val < lookup[roman[0]] ? -1 : 1); + } + } + + return num ? s.replace(orig, num) : s; + }, + type: 'text' + }); + + $.tablesorter.addParser({ + id: 'roman-extract', + is: function() { + return false; + }, + format: function(s) { + var val, + // find roman numerals + roman = $.grep(s.split(/\b/), function(v) { + return validator.test(v) ? v : ''; + }).join('').match(matcher), + + v = roman ? validator.test(roman) : 0, + num = 0; + + // roman numerals not found! + if ( !(v) ) { + return s; + } + + // save roman numeral for replacement + roman = roman[0].toUpperCase().split(''); + + while (roman.length) { + val = lookup[roman.shift()]; + // ignore non-roman numerals + if (val) { + num += val * (val < lookup[roman[0]] ? -1 : 1); + } + } + + return num ? num : s; + }, + type: 'numeric' + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-alignChar.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-alignChar.js new file mode 100644 index 0000000..bae8ba7 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-alignChar.js @@ -0,0 +1,147 @@ +/*! Widget: alignChar - updated 2/7/2015 (v2.19.0) *//* + * Align Characters, Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + var ts = $.tablesorter; + + ts.alignChar = { + + init : function(table, c, wo) { + c.$headers.filter('[' + wo.alignChar_charAttrib + ']').each(function() { + var $this = $(this), + vars = { + column : this.column, + align : $this.attr(wo.alignChar_charAttrib), + alignIndex : parseInt( $this.attr(wo.alignChar_indexAttrib) || 0, 10), + adjust : parseFloat($this.attr(wo.alignChar_adjustAttrib)) || 0 + }; + vars.regex = new RegExp('\\' + vars.align, 'g'); + if (typeof vars.align !== 'undefined') { + wo.alignChar_savedVars[this.column] = vars; + ts.alignChar.setup(table, c, wo, vars); + } + }); + }, + + setup: function(table, c, wo, v) { + // do nothing for empty tables + if ($.isEmptyObject(c.cache)) { return; } + var tbodyIndex, rowIndex, start, end, last, index, rows, val, count, + len, wLeft, wRight, alignChar, $row, + left = [], + right = []; + for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++) { + rows = c.cache[tbodyIndex]; + len = rows.normalized.length; + for (rowIndex = 0; rowIndex < len; rowIndex++) { + // set up to work with modified cache v2.16.0+ + $row = rows.row ? rows.row[rowIndex] : rows.normalized[rowIndex][c.columns].$row; + val = $row.find('td').eq(v.column).text().replace(/[ ]/g, '\u00a0'); + // count how many 'align' characters are in the string + count = (val.match( v.regex ) || []).length; + // set alignment @ alignIndex (one-based index) + if (count > 0 && v.alignIndex > 0) { + end = Math.min(v.alignIndex, count); + start = 0; + index = 0; + last = 0; + // find index of nth align character based on alignIndex (data-align-index) + while (start++ < end) { + last = val.indexOf(v.align, last + 1); + index = last < 0 ? index : last; + } + } else { + index = val.indexOf(v.align); + } + if ( index >= 0 ) { + left.push( val.substring(0, index) || '' ); + right.push( val.substring(index, val.length) || '' ); + } else { + // no align character found! + // put val in right or left based on the align index + left.push( (count >= 1 && v.alignIndex >= count) ? '' : val || '' ); + right.push( (count >= 1 && v.alignIndex >= count) ? val || '' : '' ); + } + } + } + + // find widest segments + wLeft = ($.extend([], left)).sort(function(a, b) { return b.length - a.length; })[0]; + wRight = ($.extend([], right)).sort(function(a, b) { return b.length - a.length; })[0]; + // calculate percentage widths + v.width = v.width || ( Math.floor(wLeft.length / (wLeft.length + wRight.length) * 100) + v.adjust ); + wLeft = 'min-width:' + v.width + '%'; + wRight = 'min-width:' + (100 - v.width) + '%'; + + for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++) { + rows = c.cache[tbodyIndex]; + len = rows.normalized.length; + for (rowIndex = 0; rowIndex < len; rowIndex++) { + alignChar = $(wo.alignChar_wrap).length ? $(wo.alignChar_wrap).html(v.align)[0].outerHTML : v.align; + $row = rows.row ? rows.row[rowIndex] : rows.normalized[rowIndex][c.columns].$row; + last = right[rowIndex].slice(v.align.length); + $row.find('td').eq(v.column).html( + '<span class="ts-align-wrap"><span class="ts-align-left" style="' + wLeft + '">' + left[rowIndex] + '</span>' + + '<span class="ts-align-right" style="' + wRight + '">' + ( last.length ? alignChar + last : '' ) + '</span></span>' + ); + } + } + wo.alignChar_initialized = true; + + }, + + remove: function(table, c, column) { + if ($.isEmptyObject(c.cache)) { return; } + var tbodyIndex, rowIndex, len, rows, $row, $cell; + for (tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++) { + rows = c.cache[tbodyIndex]; + len = rows.normalized.length; + for (rowIndex = 0; rowIndex < len; rowIndex++) { + $row = rows.row ? rows.row[rowIndex] : rows.normalized[rowIndex][c.columns].$row; + $cell = $row.find('td').eq(column); + $cell.html( $cell.text().replace(/\s/g, ' ') ); + } + } + } + }; + + ts.addWidget({ + id: 'alignChar', + priority: 100, + options: { + alignChar_wrap : '', + alignChar_charAttrib : 'data-align-char', + alignChar_indexAttrib : 'data-align-index', + alignChar_adjustAttrib : 'data-align-adjust' // percentage width adjustments + }, + init: function(table, thisWidget, c, wo) { + wo.alignChar_initialized = false; + wo.alignChar_savedVars = []; + ts.alignChar.init(table, c, wo); + c.$table.on('pagerEnd refreshAlign', function() { + c.$headers.filter('[' + wo.alignChar_charAttrib + ']').each(function() { + ts.alignChar.remove(table, c, this.column); + }); + ts.alignChar.init(table, c, wo); + }); + }, + format : function(table, c, wo) { + // reinitialize in case table is empty when first initialized + if (!wo.alignChar_initialized) { + c.$table.triggerHandler('refreshAlign'); + } + }, + remove : function(table, c, wo, refreshing) { + if (refreshing) { return; } + c.$headers.filter('[' + wo.alignChar_charAttrib + ']').each(function() { + ts.alignChar.remove(table, c, this.column); + }); + wo.alignChar_initialized = false; + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-build-table.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-build-table.js new file mode 100644 index 0000000..10aab49 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-build-table.js @@ -0,0 +1,469 @@ +/*! Widget: Build Table - updated 2018-03-26 (v2.30.2) *//* + * for tableSorter v2.16.0+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + var ts = $.tablesorter = $.tablesorter || {}, + + // build a table from data (requires existing <table> tag) + // data.header contains an array of header titles + // data.rows contains an array of rows which contains an array of cells + bt = ts.buildTable = function(tar, c) { + // add build options to defaults to prevent warnings + $.extend(true, ts.defaults.widgetOptions, bt.defaults); + // add table if one doesn't exist + var $tbl = tar.nodeName === 'TABLE' ? $(tar) : $('<table>').appendTo(tar), + table = $tbl[0], + wo = c.widgetOptions = $.extend( true, {}, bt.defaults, c.widgetOptions ), + p = wo.build_processing, + typ = wo.build_type, + d = wo.build_source || c.data, + debug = ts.debug(c, 'build'), + + // determine type: html, json, array, csv, object + runType = function(d) { + var t = $.type(d), + jq = d instanceof $; + // run any processing if set + if ( typeof p === 'function' ) { d = p(d, wo); } + // store processed data in table.config.data + c.data = d; + // String (html or unprocessed json) or jQuery object + if ( jq || t === 'string' ) { + // look for </tr> closing tag, then we have an HTML string + if ( jq || /<\s*\/tr\s*>/.test(d) ) { + return bt.html( table, d, wo ); + } + try { + d = $.parseJSON(d || 'null'); + if (d) { + // valid JSON! + return bt.object( table, d, wo ); + } + } catch (ignore) {} + // fall through in case it's a csv string + } + // Array + if (t === 'array' || t === 'string' || typ === 'array' || typ === 'csv') { + // build table using an array (csv & array combined script) + return bt.csv( table, d, wo ); + } + // if we got here, it's an object, or nothing + return bt.object( table, d, wo ); + }; + + // store config + table.config = c; + + // even if wo.build_type is undefined, we can try to figure out the type + if ( !ts.buildTable.hasOwnProperty(typ) && typ !== '' ) { + if (debug) { + console.error('Build >> ERROR: Aborting build table widget, incorrect build type'); + } + return false; + } + + if ( d instanceof $ ) { + // get data from within a jQuery object (csv) + runType( $.trim( d.html() ) ); + } else if ( d && ( d.hasOwnProperty('url') || typ === 'json' ) ) { + // load data via ajax + $.ajax( wo.build_source ) + .done(function(data) { + runType(data); + }) + .fail(function( jqXHR, textStatus) { + if (debug) { + console.error('Build >> ERROR: Aborting build table widget, failed ajax load'); + } + $tbl.html('<tr><td class="error">' + jqXHR.status + ' ' + textStatus + '</td></tr>'); + }); + } else { + runType(d); + } + }; + + // add data to defaults for validator; value must be falsy! + ts.defaults.data = ''; + + bt.defaults = { + // *** build widget core *** + build_type : '', // array, csv, object, json, html + build_source : '', // array, object, jQuery Object or ajaxObject { url: '', dataType: 'json' }, + build_processing : null, // function that returns a useable build_type (e.g. string to array) + build_complete : 'tablesorter-build-complete', // triggered event when build completes + + // *** CSV & Array *** + build_headers : { + rows : 1, // Number of header rows from the csv + classes : [], // Header classes to apply to cells + text : [], // Header cell text + widths : [] // set header cell widths (set in colgroup) + }, + build_footers : { + rows : 1, // Number of header rows from the csv + classes : [], // Footer classes to apply to cells + text : [] // Footer cell text + }, + build_numbers : { + addColumn : false, // include row numbering column? + sortable : false // make column sortable? + }, + + // *** CSV only options *** + build_csvStartLine : 0, // line within the csv to start adding to table + build_csvSeparator : ',', // csv separator + + // *** build object options *** + build_objectRowKey : 'rows', // object key containing table rows + build_objectCellKey : 'cells', // object key containing table cells (within the rows object) + build_objectHeaderKey : 'headers', // object key containing table headers + build_objectFooterKey : 'footers' // object key containing table footers + }; + + bt.build = { + colgroup : function(widths) { + var t = ''; + // add colgroup if widths set + if (widths && widths.length) { + t += '<colgroup>'; + $.each(widths, function(i, w) { + t += '<col' + ( w ? ' style="width:' + w + '"' : '' ) + '>'; + }); + t += '</colgroup>'; + } + return t; + }, + // d = cell data; typ = 'th' or 'td'; first = save widths from first header row only + cell : function(d, wo, typ, col, first) { + var j, $td, + $col = first ? $('<col>') : '', + cls = wo.build_headers.classes, + cw = wo.build_headers.widths; + // d is just an array + if (/string|number/.test(typeof d)) { + // add classes from options, but not text + $td = $('<' + typ + (cls && cls[col] ? ' class="' + cls[col] + '"' : '') + '>' + d + '</' + typ + '>'); + // get widths from options (only from first row) + if (first && cw && cw[col]) { + $col.width(cw[col] || ''); + } + } else { + // assume we have an object + $td = $('<' + typ + '>'); + for (j in d) { + if (d.hasOwnProperty(j)) { + if (j === 'text' || j === 'html') { + $td[j]( d[j] ); + } else if (first && j === 'width') { + // set column width, but only from first row + $col.width(d[j] || ''); + } else { + $td.attr(j, d[j]); + } + } + } + } + return [ $td, $col ]; + }, + // h1 = header text from data + header : function(h1, wo) { + var h2 = wo.build_headers.text, + cls = wo.build_headers.classes, + t = '<tr>' + (wo.build_numbers.addColumn ? '<th' + (wo.build_numbers.sortable ? '' : + ' class="sorter-false"') + '>' + wo.build_numbers.addColumn + '</th>' : ''); + $.each(h1, function(i, h) { + if (/<\s*\/t(d|h)\s*>/.test(h)) { + t += h; + } else { + t += '<th' + (cls && cls[i] ? ' class="' + cls[i] + '"' : '') + '>' + + (h2 && h2[i] ? h2[i] : h) + '</th>'; + } + }); + return t + '</tr>'; + }, + rows : function(items, txt, c, wo, num, ftr) { + var h = (ftr ? 'th' : 'td'), + t = '<tr>' + (wo.build_numbers.addColumn ? '<' + h + '>' + (ftr ? '' : num) + '</' + h + '>' : ''); + $.each(items, function(i, item) { + // test if HTML is already included; look for closing </td> + if (/<\s*\/t(d|h)\s*>/.test(item)) { + t += item; + } else { + t += '<' + (ftr ? h + (c && c[i] ? ' class="' + c[i] + '"' : '') : h) + '>' + + (ftr && txt && txt.length && txt[i] ? txt[i] : item) + '</' + h + '>'; + } + }); + return t + '</tr>'; + } + }; + + bt.buildComplete = function(table, wo) { + $(table).triggerHandler(wo.build_complete); + if (table.config && ts.debug(table.config, 'build')) { + console.log('Build >> Table build complete'); + } + ts.setup(table, table.config); + }; + + /* ==== Array example ==== + [ + [ "header1", "header2", ... "headerN" ], + [ "row1cell1", "row1cell2", ... "row1cellN" ], + [ "row2cell1", "row2cell2", ... "row2cellN" ], + ... + [ "rowNcell1", "rowNcell2", ... "rowNcellN" ] + ] + */ + bt.array = function(table, data, wo) { + return bt.csv(table, data, wo); + }; + + /* ==== CSV example ==== + ID, Name, Age, Date + A42b, Parker, 28, "Jul 6, 2006 8:14 AM" + A255, Hood, 33, "Dec 10, 2002 5:14 AM" + A33, Kent, 18, "Jan 12, 2003 11:14 AM" + A1, Franklin, 45, "Jan 18, 2001 9:12 AM" + A102, Evans, 22, "Jan 18, 2007 9:12 AM" + A42a, Everet, 22, "Jan 18, 2007 9:12 AM" + ID, Name, Age, Date + */ + // Adapted & modified from csvToTable.js by Steve Sobel + // MIT license: https://code.google.com/p/jquerycsvtotable/ + bt.csv = function(table, data, wo) { + var c, h, + csv = wo.build_type === 'csv' || typeof data === 'string', + $t = $(table), + lines = csv ? data.replace('\r', '').split('\n') : data, + len = lines.length, + printedLines = 0, + infooter = false, + r = wo.build_headers.rows + (csv ? wo.build_csvStartLine : 0), + f = wo.build_footers.rows, + headerCount = 0, + error = '', + items, + tableHTML = bt.build.colgroup( wo.build_headers.widths ) + '<thead>'; + + $.each(lines, function(n, line) { + if ( n >= len - f ) { infooter = true; } + // build header + if ( (csv ? n >= wo.build_csvStartLine : true) && ( n < r ) ) { + h = csv ? bt.splitCSV( line, wo.build_csvSeparator ) : line; + headerCount = h.length; + tableHTML += bt.build.header(h, wo); + } else if ( n >= r ) { + // build tbody & tfoot rows + if (n === r) { + tableHTML += '</thead><tbody>'; + } + items = csv ? bt.splitCSV( line, wo.build_csvSeparator ) : line; + if (infooter && f > 0) { + tableHTML += (n === len - f ? '</tbody><tfoot>' : '') + + (n === len ? '</tfoot>' : ''); + } + if (items.length > 1) { + printedLines++; + if ( items.length !== headerCount ) { + error += 'error on line ' + n + ': Item count (' + items.length + + ') does not match header count (' + headerCount + ') \n'; + } + c = infooter ? wo.build_footers.classes : ''; + tableHTML += bt.build.rows(items, wo.build_footers.text, c, wo, printedLines, infooter); + } + } + }); + tableHTML += (f > 0 ? '' : '</tbody>'); + if (error) { + $t.html(error); + } else { + $t.html(tableHTML); + bt.buildComplete(table, wo); + } + }; + + // CSV Parser by Brian Huisman (http://www.greywyvern.com/?post=258) + bt.splitCSV = function(str, sep) { + var x, tl, + thisCSV = $.trim(str).split(sep = sep || ','); + for ( x = thisCSV.length - 1; x >= 0; x-- ) { + if ( thisCSV[x].replace(/\"\s+$/, '"').charAt(thisCSV[x].length - 1) === '"' ) { + if ( (tl = thisCSV[x].replace(/^\s+\"/, '"')).length > 1 && tl.charAt(0) === '"' ) { + thisCSV[x] = thisCSV[x].replace(/^\s*"|"\s*$/g, '').replace(/""/g, '"'); + } else if (x) { + thisCSV.splice(x - 1, 2, [ thisCSV[x - 1], thisCSV[x] ].join(sep)); + } else { + thisCSV = thisCSV.shift().split(sep).concat(thisCSV); + } + } else { + thisCSV[x].replace(/""/g, '"'); + } + } + return thisCSV; + }; + + // data may be a jQuery object after processing + bt.html = function(table, data, wo) { + var $t = $(table); + if ( data instanceof $ ) { + $t.empty().append(data); + } else { + $t.html(data); + } + bt.buildComplete(table, wo); + }; + + /* ==== Object example ==== + data : { + headers : [ + [ + { text: 'First Name', class: 'fname', width: '20%' }, // row 1 cell 1 + 'Last Name', + { text: 'Age', class: 'age', 'data-sorter' : false }, + 'Total', + { text: 'Discount', class : 'sorter-false' }, + { text: 'Date', class : 'date' } // row 1 cell 6 + ] + ], + footers : 'clone', // clone headers or assign array like headers + rows : [ + // TBODY 1 + [ 'Peter', 'Parker', 28, '$9.99', '20%', 'Jul 6, 2006 8:14 AM' ], // row 1 + [ 'John', 'Hood', 33, '$19.99', '25%', 'Dec 10, 2002 5:14 AM' ], // row 2 + [ 'Clark', 'Kent', 18, '$15.89', '44%', 'Jan 12, 2003 11:14 AM' ], // row 3 + + // TBODY 2 + { newTbody: true, class: 'tablesorter-infoOnly' }, + { cells : [ { text: 'Info Row', colSpan: 6 } ] }, // row 4 + + // TBODY 3 + { newTbody: true }, + [ 'Bruce', 'Evans', 22, '$13.19', '11%', 'Jan 18, 2007 9:12 AM' ], // row 5 + [ 'Brice', 'Almighty', 45, '$153.19', '44%', 'Jan 18, 2001 9:12 AM' ], // row 6 + + { class: 'specialRow', // row 7 + cells: [ + { text: 'Fred', class: 'fname' }, + { text: 'Smith', class: 'lname' }, + { text: 18, class: 'age', 'data-info': 'fake ID!, he is really 16' }, + { text: '$22.44', class: 'total' }, + { text: '8%', class: 'discount' }, + { text: 'Aug 20, 2012 10:15 AM', class: 'date' } + ], + 'data-info' : 'This row likes turtles' + } + ] + } + */ + bt.object = function(table, data, wo) { + // 'rows' + var j, l, t, $c, $t, $tb, $tr, + c = table.config, + kh = wo.build_objectHeaderKey, + kr = wo.build_objectRowKey, + h = data.hasOwnProperty(kh) && !$.isEmptyObject(data.kh) ? data.kh : data.hasOwnProperty('headers') ? data.headers : false, + r = data.hasOwnProperty(kr) && !$.isEmptyObject(data.kr) ? data.kr : data.hasOwnProperty('rows') ? data.rows : false; + + if (!h || !r || h.length === 0 || r.length === 0) { + if (ts.debug(c, 'build')) { + console.error('Build >> ERROR: Aborting build table widget, missing data for object build'); + } + return false; + } + + $c = $('<colgroup>'); + $t = $('<table><thead/></table>'); + + // Build thead + // h = [ ['headerRow1Cell1', 'headerRow1Cell2', ... 'headerRow1CellN' ], ['headerRow2Cell1', ... ] ] + // or h = [ [ { text: 'firstCell', class: 'fc', width: '20%' }, ..., { text: 'last Cell' } ], [ /* second row */ ] ] + $.each(h, function(i, d) { + $tr = $('<tr>').appendTo( $t.find('thead') ); + l = d.length; // header row + for ( j = 0; j < l; j++ ) { + // cell(cellData, widgetOptions, 'th', first row) + t = bt.build.cell(d[j], wo, 'th', j, i === 0); + if (t[0] && t[0].length) { t[0].appendTo( $tr ); } // add cell + if (i === 0 && t[1]) { t[1].appendTo( $c ); } // add col to colgroup + } + }); + if ($c.find('col[style]').length) { + // add colgroup if it contains col elements + $t.prepend( $c ); + } + + $tb = $('<tbody>'); + // Build tbody + $.each(r, function(i, d) { + var j; + t = $.type(d) === 'object'; + // add new tbody + if (t && d.newTbody) { + $tb = $('<tbody>').appendTo( $t ); + for (j in d) { + if (d.hasOwnProperty(j) && j !== 'newTbody') { + $tb.attr(j, d[j]); + } + } + } else { + if (i === 0) { + // add tbody, if the first item in the object isn't a call for a new tbody + $tb.appendTo( $t ); + } + + $tr = $('<tr>').appendTo( $tb ); + if (t) { + // row defined by object + for (j in d) { + if (d.hasOwnProperty(j) && j !== wo.build_objectCellKey) { + $tr.attr(j, d[j]); + } + } + if (d.hasOwnProperty(wo.build_objectCellKey)) { + // cells contains each cell info + d = d.cells; + } + } + + l = d.length; + for ( j = 0; j < l; j++ ) { + // cell(cellData, widgetOptions, 'td') + $c = bt.build.cell(d[j], wo, 'td', j); + if ($c[0] && $c[0].length) { $c[0].appendTo( $tr ); } // add cell + } + } + }); + + // add footer + if (data.hasOwnProperty(wo.build_objectFooterKey)) { + t = data[wo.build_objectFooterKey]; + if (t === 'clone') { + $c = $t.find('thead').html(); + $t.append('<tfoot>' + $c + '</tfoot>'); + } else { + $c = $('<tfoot>').appendTo( $t ); + $.each(t, function(i, d) { + $tr = $('<tr>').appendTo( $c ); + l = d.length; // footer cells + for ( j = 0; j < l; j++ ) { + // cell(cellData, widgetOptions, 'th') + $tb = bt.build.cell(d[j], wo, 'th', j); + if ($tb[0] && $tb[0].length) { $tb[0].appendTo( $tr ); } // add cell + } + }); + } + } + + $(table).html( $t.html() ); + bt.buildComplete(table, wo); + }; + + bt.ajax = bt.json = function(table, data, wo) { + return bt.object(table, data, wo); + }; + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-chart.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-chart.js new file mode 100644 index 0000000..08838aa --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-chart.js @@ -0,0 +1,275 @@ +/* Widget: chart (beta) - updated 10/31/2015 (v2.24.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + + // temp variables + chart_cols = [], + chart_headers = [], + // google charts + chart_rows = [], + chart_data = [], + // highcharts + chart_categories = [], + chart_series = [], + // fusioncharts + chart_category = [], + chart_dataset = [], + + chart = ts.chart = { + + // regex used to strip out non-digit values before sending + // the string to the $.tablesorter.formatFloat function + nonDigit : /[^\d,.\-()]/g, + + init: function(c, wo) { + c.$table + .off(wo.chart_event) + .on(wo.chart_event, function() { + if (this.hasInitialized) { + // refresh 'c' variable in case options are updated dynamically + var c = this.config; + chart.getCols(c, c.widgetOptions); + chart.getData(c, c.widgetOptions); + } + }); + }, + + getCols: function(c, wo) { + var i; + chart_cols = []; + chart_series = []; + chart_dataset = []; + + for ( i = 0; i < c.columns; i++ ) { + if ( wo.chart_useSelector && ts.hasWidget( c.table, 'columnSelector' ) && !c.selector.auto ) { + if ( ( c.selector.states[i] && $.inArray(i, wo.chart_ignoreColumns) < 0 ) || + i === wo.chart_labelCol || i === wo.chart_sort[0][0] ) { + chart_cols.push(i); + } + } else { + if ( $.inArray(i, wo.chart_ignoreColumns) < 0 || i === wo.chart_labelCol || i === wo.chart_sort[0][0] ) { + chart_cols.push(i); + } + } + } + }, + + getData: function(c, wo) { + chart.getHeaders(c, wo); + chart.getRows(c, wo); + + /* == Google data == + array of arrays (Google charts) + [ + [ "Year", "Sales", "Expenses" ], + [ "2004", 1000, 400 ], + [ "2005", 1170, 460 ], + [ "2006", 660, 1120 ], + [ "2007", 1030, 540 ] + ] + + == Highcharts == + categories -> [ '2004', '2005', '2006', '2007' ] + series -> [{ + name: 'Sales', + data: [ 1000, 1170, 660, 1030 ] + }, { + name: 'Expenses', + data: [ 400, 460, 1120, 540 ] + }] + + == Fusioncharts + "categories": [{ + "category": [ + {"label": "2004"}, + {"label": "2005"}, + {"label": "2006"}, + {"label": "2007"} + ] + }], + "dataset": [ + { + "seriesname": "Sales", + "data": [ + {"value": "1000"}, + {"value": "1170"}, + {"value": "660"}, + {"value": "1030"} + ] + },{ + "seriesname": "Expenses", + "data": [ + {"value": "400"}, + {"value": "600"}, + {"value": "1120"}, + {"value": "540"} + ] + } + ] + */ + + chart_data = [ chart_headers ]; + $.each(chart_rows, function(k, row) { + chart_data.push(row); + }); + + c.chart = { + // google + data: chart_data, + // highcharts + categories: chart_categories, + series: chart_series, + // FusionCharts + category: chart_category, + dataset: chart_dataset + }; + }, + + getHeaders: function(c, wo) { + var text; + chart_headers = []; + chart_series = []; + chart_dataset = []; + chart_headers.push( c.headerContent[wo.chart_labelCol] ); + $.each(chart_cols, function(k, col) { + if (col === wo.chart_labelCol) { + return true; + } + text = c.headerContent[col]; + chart_headers.push( text ); + chart_series.push( { name: text, data: [] } ); + chart_dataset.push( { seriesname: text, data: [] } ); + }); + }, + + getRows: function(c, wo) { + var norm_rows = c.cache[0].normalized, + rows = []; + chart_rows = []; + chart_categories = []; + chart_category = []; + + $.each(norm_rows, function(indx, rowVal) { + var i, txt, + $tr = rowVal[c.columns].$row, + $cells = $tr.children('th,td'), + row = []; + if ( + (/v/i.test(wo.chart_incRows) && $tr.is(':visible')) || + (/f/i.test(wo.chart_incRows) && !$tr.hasClass(wo.filter_filteredRow || 'filtered')) || + (!/(v|f)/i.test(wo.chart_incRows)) + ) { + // Add all cols (don't mess up indx for sorting) + for (i = 0; i < c.columns; i++) { + if ( $.inArray(indx, wo.chart_parsed) >= 0 ) { + row.push( rowVal[i] ); + } else { + txt = $cells[i].getAttribute( c.textAttribute ) || $cells[i].textContent || $cells.eq(i).text(); + row.push( $.trim( txt ) ); + } + } + rows.push(row); + } + }); + + // sort based on chart_sort + rows.sort(function(a, b) { + if ( wo.chart_sort[0][1] === 1 ) { + return ts.sortNatural( b[wo.chart_sort[0][0]], a[wo.chart_sort[0][0]] ); + } + return ts.sortNatural( a[wo.chart_sort[0][0]], b[wo.chart_sort[0][0]] ); + }); + + $.each(rows, function(i, rowVal) { + var value, + objIndex = 0, + row = [], + label = rowVal[wo.chart_labelCol]; + + row.push( '' + label ); + + $.each(rowVal, function(indx, cellValue) { + var tempVal; + if (indx === wo.chart_labelCol) { + chart_categories.push( cellValue ); + chart_category.push({ 'label': cellValue }); + return true; + } + value = false; + if ( wo.chart_useSelector && ts.hasWidget( c.table, 'columnSelector' ) && !c.selector.auto ) { + if ( c.selector.states[indx] && $.inArray(indx, wo.chart_ignoreColumns) < 0 ) { + value = '' + cellValue; + } + } else { + if ($.inArray(indx, wo.chart_ignoreColumns) < 0) { + value = '' + cellValue; + } + } + + if (value !== false) { + if ( /s/i.test( '' + wo.chart_layout[indx] ) ) { + row.push( value ); + chart_series[objIndex].data.push( value ); + chart_dataset[objIndex].data.push( value ); + } else { + // using format float, after stripping out all non-digit values + tempVal = ts.formatFloat( value.replace( chart.nonDigit, '' ), c.table ); + tempVal = isNaN(tempVal) ? value : tempVal; + // if tempVal ends up being an empty string, fall back to value + row.push( tempVal ); + chart_series[objIndex].data.push( tempVal ); + chart_dataset[objIndex].data.push( { value : tempVal } ); + } + objIndex++; + } + }); + chart_rows.push(row); + }); + }, + + remove: function(c, wo) { + c.$table.off(wo.chart_event); + } + + }; + + ts.addWidget({ + id: 'chart', + options: { + // (a)ll, (v)isible or (f)iltered - only the first letter is needed + chart_incRows: 'filtered', + // prefer columnSelector for ignoreColumns + chart_useSelector: false, + // columns to ignore [0, 1,... ] (zero-based index) + chart_ignoreColumns: [], + // Use parsed data instead of cell.text() + chart_parsed: [], + // data output layout, float is default + chart_layout: { + // first element is a string, all others will be float + 0: 'string' + }, + // Set the label column + chart_labelCol: 0, + // data sort, should always be first row, might want [[0,1]] + chart_sort: [ [ 0, 0 ] ], + // event to trigger get updated data + chart_event: 'chartData' + }, + + init: function(table, thisWidget, c, wo) { + chart.init(c, wo); + }, + + remove: function(table, c, wo) { + chart.remove(c, wo); + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columnSelector.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columnSelector.js new file mode 100644 index 0000000..90f7af2 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columnSelector.js @@ -0,0 +1,592 @@ +/* Widget: columnSelector (responsive table widget) - updated 2018-08-03 (v2.31.2) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Justin Hallett & Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + namespace = '.tscolsel', + tsColSel = ts.columnSelector = { + + queryAll : '@media only all { [columns] { display: none; } } ', + queryBreak : '@media all and (min-width: [size]) { [columns] { display: table-cell; } } ', + + init: function(table, c, wo) { + var $t, colSel, + debug = ts.debug(c, 'columnSelector'); + + // abort if no input is contained within the layout + $t = $(wo.columnSelector_layout); + if (!$t.find('input').add( $t.filter('input') ).length) { + if (debug) { + console.error('ColumnSelector >> ERROR: Column Selector aborting, no input found in the layout! ***'); + } + return; + } + + // unique table class name + c.$table.addClass( c.namespace.slice(1) + 'columnselector' ); + + // build column selector/state array + colSel = c.selector = { $container : $(wo.columnSelector_container || '<div>') }; + colSel.$style = $('<style></style>').prop('disabled', true).appendTo('head'); + colSel.$breakpoints = $('<style></style>').prop('disabled', true).appendTo('head'); + + colSel.isInitializing = true; + tsColSel.setUpColspan(c, wo); + tsColSel.setupSelector(c, wo); + + if (wo.columnSelector_mediaquery) { + tsColSel.setupBreakpoints(c, wo); + } + + colSel.isInitializing = false; + if (colSel.$container.length) { + tsColSel.updateCols(c, wo); + } else if (debug) { + console.warn('ColumnSelector >> container not found'); + } + + c.$table + .off('refreshColumnSelector' + namespace) + /* $('table').trigger('refreshColumnSelector', arguments ); showing arguments below + undefined = refresh current settings (update css hiding columns) + 'selectors' = update container contents (replace inputs/labels) + [ [2,3,4] ] = set visible columns; turn off "auto" mode. + [ 'columns', [2,3,4] ] = set visible columns; turn off "auto" mode. + [ 'auto', [2,3,4] ] = set visible columns; turn on "auto" mode. + true = turn on "auto" mode. + */ + .on('refreshColumnSelector' + namespace, function( e, optName, optState ) { + // make sure we're using current config settings + tsColSel.refreshColumns( this.config, optName, optState ); + }); + + if (debug) { + console.log('ColumnSelector >> Widget initialized'); + } + }, + + refreshColumns: function( c, optName, optState ) { + var i, arry, $el, val, + colSel = c.selector, + isArry = $.isArray(optState || optName), + wo = c.widgetOptions; + // see #798 + if (typeof optName !== 'undefined' && optName !== null && colSel.$container.length) { + // pass "selectors" to update the all of the container contents + if ( optName === 'selectors' ) { + colSel.$container.empty(); + tsColSel.setupSelector(c, wo); + tsColSel.setupBreakpoints(c, wo); + // if optState is undefined, maintain the current "auto" state + if ( typeof optState === 'undefined' && optState !== null ) { + optState = colSel.auto; + } + } + // pass an array of column zero-based indexes to turn off auto mode & toggle selected columns + if (isArry) { + arry = optState || optName; + // make sure array contains numbers + $.each(arry, function(i, v) { + arry[i] = parseInt(v, 10); + }); + for (i = 0; i < c.columns; i++) { + val = $.inArray( i, arry ) >= 0; + $el = colSel.$container.find( 'input[data-column=' + i + ']' ); + if ( $el.length ) { + $el.prop( 'checked', val ); + colSel.states[i] = val; + } + } + } + // if passing an array, set auto to false to allow manual column selection & update columns + // refreshColumns( c, 'auto', true ) === refreshColumns( c, true ); + val = optState === true || optName === true || optName === 'auto' && optState !== false; + $el = colSel.$container.find( 'input[data-column="auto"]' ).prop( 'checked', val ); + tsColSel.updateAuto( c, wo, $el ); + } else { + tsColSel.updateBreakpoints(c, wo); + tsColSel.updateCols(c, wo); + } + tsColSel.saveValues( c, wo ); + tsColSel.adjustColspans( c, wo ); + }, + + setupSelector: function(c, wo) { + var index, name, $header, priority, col, colId, $el, + colSel = c.selector, + $container = colSel.$container, + useStorage = wo.columnSelector_saveColumns && ts.storage, + // get stored column states + saved = useStorage ? ts.storage( c.table, 'tablesorter-columnSelector' ) : [], + state = useStorage ? ts.storage( c.table, 'tablesorter-columnSelector-auto') : {}; + + // initial states + colSel.auto = $.isEmptyObject(state) || $.type(state.auto) !== 'boolean' ? wo.columnSelector_mediaqueryState : state.auto; + colSel.states = []; + colSel.$column = []; + colSel.$wrapper = []; + colSel.$checkbox = []; + // populate the selector container + for ( index = 0; index < c.columns; index++ ) { + $header = c.$headerIndexed[ index ]; + // if no data-priority is assigned, default to 1, but don't remove it from the selector list + priority = $header.attr(wo.columnSelector_priority) || 1; + colId = $header.attr('data-column'); + col = ts.getColumnData( c.table, c.headers, colId ); + state = ts.getData( $header, col, 'columnSelector'); + + // if this column not hidable at all + // include getData check (includes 'columnSelector-false' class, data attribute, etc) + if ( isNaN(priority) && priority.length > 0 || state === 'disable' || + ( wo.columnSelector_columns[colId] && wo.columnSelector_columns[colId] === 'disable') ) { + colSel.states[colId] = null; + continue; // goto next + } + + // set default state; storage takes priority + colSel.states[colId] = saved && (typeof saved[colId] !== 'undefined' && saved[colId] !== null) ? + saved[colId] : (typeof wo.columnSelector_columns[colId] !== 'undefined' && wo.columnSelector_columns[colId] !== null) ? + wo.columnSelector_columns[colId] : (state === 'true' || state !== 'false'); + colSel.$column[colId] = $(this); + if ($container.length) { + // set default col title + name = $header.attr(wo.columnSelector_name) || $header.text().trim(); + if (typeof wo.columnSelector_layoutCustomizer === 'function') { + $el = $header.find('.' + ts.css.headerIn); + name = wo.columnSelector_layoutCustomizer( $el.length ? $el : $header, name, parseInt(colId, 10) ); + } + colSel.$wrapper[colId] = $(wo.columnSelector_layout.replace(/\{name\}/g, name)).appendTo($container); + colSel.$checkbox[colId] = colSel.$wrapper[colId] + // input may not be wrapped within the layout template + .find('input').add( colSel.$wrapper[colId].filter('input') ) + .attr('data-column', colId) + .toggleClass( wo.columnSelector_cssChecked, colSel.states[colId] ) + .prop('checked', colSel.states[colId]) + .on('change', function() { + if (!colSel.isInitializing) { + // ensure states is accurate + var colId = $(this).attr('data-column'); + if (tsColSel.checkChange(c, this.checked)) { + // if (wo.columnSelector_maxVisible) + c.selector.states[colId] = this.checked; + tsColSel.updateCols(c, wo); + } else { + this.checked = !this.checked; + return false; + } + } + }).change(); + } + } + + }, + + checkChange: function(c, checked) { + var wo = c.widgetOptions, + max = wo.columnSelector_maxVisible, + min = wo.columnSelector_minVisible, + states = c.selector.states, + indx = states.length, + count = 0; + while (indx-- >= 0) { + if (states[indx]) { + count++; + } + } + if ((checked & max !== null && count >= max) || + (!checked && min !== null && count <= min)) { + return false; + } + return true; + }, + + setupBreakpoints: function(c, wo) { + var colSel = c.selector; + + // add responsive breakpoints + if (wo.columnSelector_mediaquery) { + // used by window resize function + colSel.lastIndex = -1; + tsColSel.updateBreakpoints(c, wo); + c.$table + .off('updateAll' + namespace) + .on('updateAll' + namespace, function() { + tsColSel.setupSelector(c, wo); + tsColSel.setupBreakpoints(c, wo); + tsColSel.updateBreakpoints(c, wo); + tsColSel.updateCols(c, wo); + }); + } + + if (colSel.$container.length) { + // Add media queries toggle + if (wo.columnSelector_mediaquery) { + colSel.$auto = $( wo.columnSelector_layout.replace(/\{name\}/g, wo.columnSelector_mediaqueryName) ).prependTo(colSel.$container); + colSel.$auto + // needed in case the input in the layout is not wrapped + .find('input').add( colSel.$auto.filter('input') ) + .attr('data-column', 'auto') + .prop('checked', colSel.auto) + .toggleClass( wo.columnSelector_cssChecked, colSel.auto ) + .on('change', function() { + tsColSel.updateAuto(c, wo, $(this)); + }).change(); + } + // Add a bind on update to re-run col setup + c.$table.off('update' + namespace).on('update' + namespace, function() { + tsColSel.updateCols(c, wo); + }); + } + }, + + updateAuto: function(c, wo, $el) { + var colSel = c.selector; + colSel.auto = $el.prop('checked') || false; + $.each( colSel.$checkbox, function(i, $cb) { + if ($cb) { + $cb[0].disabled = colSel.auto; + colSel.$wrapper[i].toggleClass('disabled', colSel.auto); + } + }); + if (wo.columnSelector_mediaquery) { + tsColSel.updateBreakpoints(c, wo); + } + tsColSel.updateCols(c, wo); + // copy the column selector to a popup/tooltip + if (c.selector.$popup) { + c.selector.$popup.find('.tablesorter-column-selector') + .html( colSel.$container.html() ) + .find('input').each(function() { + var indx = $(this).attr('data-column'); + $(this).prop( 'checked', indx === 'auto' ? colSel.auto : colSel.states[indx] ); + }); + } + tsColSel.saveValues( c, wo ); + tsColSel.adjustColspans( c, wo ); + // trigger columnUpdate if auto is true (it gets skipped in updateCols() + if (colSel.auto) { + c.$table.triggerHandler(wo.columnSelector_updated); + } + }, + addSelectors: function( wo, prefix, column ) { + var array = [], + temp = ' col:nth-child(' + column + ')'; + array.push(prefix + temp + ',' + prefix + '_extra_table' + temp); + temp = ' tr:not(.' + wo.columnSelector_classHasSpan + ') th[data-column="' + ( column - 1 ) + '"]'; + array.push(prefix + temp + ',' + prefix + '_extra_table' + temp); + temp = ' tr:not(.' + wo.columnSelector_classHasSpan + ') td:nth-child(' + column + ')'; + array.push(prefix + temp + ',' + prefix + '_extra_table' + temp); + // for other cells in colspan columns + temp = ' tr td:not(' + prefix + wo.columnSelector_classHasSpan + ')[data-column="' + (column - 1) + '"]'; + array.push(prefix + temp + ',' + prefix + '_extra_table' + temp); + return array; + }, + updateBreakpoints: function(c, wo) { + var priority, col, column, breaks, + isHidden = [], + colSel = c.selector, + prefix = c.namespace + 'columnselector', + mediaAll = [], + breakpts = ''; + if (wo.columnSelector_mediaquery && !colSel.auto) { + colSel.$breakpoints.prop('disabled', true); + colSel.$style.prop('disabled', false); + return; + } + if (wo.columnSelector_mediaqueryHidden) { + // add columns to be hidden; even when "auto" is set - see #964 + for ( column = 0; column < c.columns; column++ ) { + col = ts.getColumnData( c.table, c.headers, column ); + isHidden[ column + 1 ] = ts.getData( c.$headerIndexed[ column ], col, 'columnSelector' ) === 'false'; + if ( isHidden[ column + 1 ] ) { + // hide columnSelector false column (in auto mode) + mediaAll = mediaAll.concat( tsColSel.addSelectors( wo, prefix, column + 1 ) ); + } + } + } + // only 6 breakpoints (same as jQuery Mobile) + for (priority = 0; priority < wo.columnSelector_maxPriorities; priority++) { + /*jshint loopfunc:true */ + breaks = []; + c.$headers.filter('[' + wo.columnSelector_priority + '=' + (priority + 1) + ']').each(function() { + column = parseInt($(this).attr('data-column'), 10) + 1; + // don't reveal columnSelector false columns + if ( !isHidden[ column ] ) { + breaks = breaks.concat( tsColSel.addSelectors( wo, prefix, column ) ); + } + }); + if (breaks.length) { + mediaAll = mediaAll.concat( breaks ); + breakpts += tsColSel.queryBreak + .replace(/\[size\]/g, wo.columnSelector_breakpoints[priority]) + .replace(/\[columns\]/g, breaks.join(',')); + } + } + if (colSel.$style) { + colSel.$style.prop('disabled', true); + } + if (mediaAll.length) { + colSel.$breakpoints + .prop('disabled', false) + .text( tsColSel.queryAll.replace(/\[columns\]/g, mediaAll.join(',')) + breakpts ); + } + }, + updateCols: function(c, wo) { + if (wo.columnSelector_mediaquery && c.selector.auto || c.selector.isInitializing) { + return; + } + var column, + colSel = c.selector, + styles = [], + prefix = c.namespace + 'columnselector'; + colSel.$container.find('input[data-column]').filter('[data-column!="auto"]').each(function() { + if (!this.checked) { + column = parseInt( $(this).attr('data-column'), 10 ) + 1; + styles = styles.concat( tsColSel.addSelectors( wo, prefix, column ) ); + } + $(this).toggleClass( wo.columnSelector_cssChecked, this.checked ); + }); + if (wo.columnSelector_mediaquery) { + colSel.$breakpoints.prop('disabled', true); + } + if (colSel.$style) { + colSel.$style.prop('disabled', false).text( styles.length ? styles.join(',') + ' { display: none; }' : '' ); + } + tsColSel.saveValues( c, wo ); + tsColSel.adjustColspans( c, wo ); + c.$table.triggerHandler(wo.columnSelector_updated); + }, + + setUpColspan: function(c, wo) { + var index, span, nspace, + $window = $( window ), + hasSpans = false, + $cells = c.$table + .add( $(c.namespace + '_extra_table') ) + .children() + .children('tr') + .children('th, td'), + len = $cells.length; + for ( index = 0; index < len; index++ ) { + span = $cells[ index ].colSpan; + if ( span > 1 ) { + hasSpans = true; + $cells.eq( index ) + .addClass( c.namespace.slice( 1 ) + 'columnselector' + wo.columnSelector_classHasSpan ) + .attr( 'data-col-span', span ); + // add data-column values + ts.computeColumnIndex( $cells.eq( index ).parent().addClass( wo.columnSelector_classHasSpan ) ); + } + } + // only add resize end if using media queries + if ( hasSpans && wo.columnSelector_mediaquery ) { + nspace = c.namespace + 'columnselector'; + // Setup window.resizeEnd event + $window + .off( nspace ) + .on( 'resize' + nspace, ts.window_resize ) + .on( 'resizeEnd' + nspace, function() { + // IE calls resize when you modify content, so we have to unbind the resize event + // so we don't end up with an infinite loop. we can rebind after we're done. + $window.off( 'resize' + nspace, ts.window_resize ); + tsColSel.adjustColspans( c, wo ); + $window.on( 'resize' + nspace, ts.window_resize ); + }); + } + }, + + // Extracted from buildHeaders in core; needed for scroller widget compatibility + findHeaders : function(c) { + var indx, $temp, + sel = '.' + ts.css.scrollerHeader + ' thead > tr > ', + $headers = $(sel + 'th,' + sel + 'td'), + result = []; + for ( indx = 0; indx < c.columns; indx++ ) { + // Use $headers.parent() in case selectorHeaders doesn't point to the th/td + $temp = $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 + result[ indx ] = $temp.length ? + $temp.not( '.sorter-false' ).length ? + $temp.not( '.sorter-false' ).filter( ':last' ) : + $temp.filter( ':last' ) : + $(); + } + return result; + }, + + adjustColspans: function(c, wo) { + var index, cols, col, span, end, $cell, + colSel = c.selector, + filtered = wo.filter_filteredRow || 'filtered', + autoModeOn = wo.columnSelector_mediaquery && colSel.auto, + // find all header/footer cells in case a regular column follows a colspan; see #1238 + $headers = c.$table.children( 'thead, tfoot' ).children().children() + .add( $(c.namespace + '_extra_table').children( 'thead, tfoot' ).children().children() ) + // include grouping widget headers (they have colspans!) + .add( c.$table.find( '.group-header' ).children() ), + len = $headers.length, + $headerIndexed = ts.hasWidget(c.table, 'scroller') + ? tsColSel.findHeaders(c) + : c.$headerIndexed; + for ( index = 0; index < len; index++ ) { + $cell = $headers.eq(index); + col = parseInt( $cell.attr('data-column'), 10 ) || $cell[0].cellIndex; + span = parseInt( $cell.attr('data-col-span'), 10 ) || 1; + end = col + span; + if ( span > 1 ) { + for ( cols = col; cols < end; cols++ ) { + if ( !autoModeOn && colSel.states[ cols ] === false || + autoModeOn && $headerIndexed[ cols ] && !$headerIndexed[ cols ].is(':visible') ) { + span--; + } + } + if ( span ) { + $cell.removeClass( filtered )[0].colSpan = span; + } else { + $cell.addClass( filtered ); + } + } else if ( typeof colSel.states[ col ] !== 'undefined' && colSel.states[ col ] !== null ) { + $cell.toggleClass( filtered, !autoModeOn && !colSel.states[ col ] ); + } + } + }, + + saveValues : function( c, wo ) { + if ( wo.columnSelector_saveColumns && ts.storage ) { + var colSel = c.selector; + ts.storage( c.$table[0], 'tablesorter-columnSelector-auto', { auto : colSel.auto } ); + ts.storage( c.$table[0], 'tablesorter-columnSelector', colSel.states ); + } + }, + + attachTo : function(table, elm) { + table = $(table)[0]; + var colSel, wo, indx, + c = table.config, + $popup = $(elm); + if ($popup.length && c) { + if (!$popup.find('.tablesorter-column-selector').length) { + // add a wrapper to add the selector into, in case the popup has other content + $popup.append('<span class="tablesorter-column-selector"></span>'); + } + colSel = c.selector; + wo = c.widgetOptions; + $popup.find('.tablesorter-column-selector') + .html( colSel.$container.html() ) + .find('input').each(function() { + var indx = $(this).attr('data-column'), + isChecked = indx === 'auto' ? colSel.auto : colSel.states[indx]; + $(this) + .toggleClass( wo.columnSelector_cssChecked, isChecked ) + .prop( 'checked', isChecked ); + }); + colSel.$popup = $popup.on('change', 'input', function() { + if (!colSel.isInitializing) { + if (tsColSel.checkChange(c, this.checked)) { + // data input + indx = $(this).toggleClass( wo.columnSelector_cssChecked, this.checked ).attr('data-column'); + // update original popup + colSel.$container.find('input[data-column="' + indx + '"]') + .prop('checked', this.checked) + .trigger('change'); + } else { + this.checked = !this.checked; + return false; + } + } + }); + } + } + + }; + + /* Add window resizeEnd event (also used by scroller widget) */ + ts.window_resize = function() { + if ( ts.timer_resize ) { + clearTimeout( ts.timer_resize ); + } + ts.timer_resize = setTimeout( function() { + $( window ).trigger( 'resizeEnd' ); + }, 250 ); + }; + + ts.addWidget({ + id: 'columnSelector', + priority: 10, + options: { + // target the column selector markup + columnSelector_container : null, + // column status, true = display, false = hide + // disable = do not display on list + columnSelector_columns : {}, + // remember selected columns + columnSelector_saveColumns: true, + + // container layout + columnSelector_layout : '<label><input type="checkbox">{name}</label>', + // layout customizer callback called for each column + // function($cell, name, column) { return name || $cell.html(); } + columnSelector_layoutCustomizer : null, + // data attribute containing column name to use in the selector container + columnSelector_name : 'data-selector-name', + + /* Responsive Media Query settings */ + // enable/disable mediaquery breakpoints + columnSelector_mediaquery : true, + // toggle checkbox name + columnSelector_mediaqueryName : 'Auto: ', + // breakpoints checkbox initial setting + columnSelector_mediaqueryState : true, + // hide columnSelector false columns while in auto mode + columnSelector_mediaqueryHidden : false, + // set the maximum and/or minimum number of visible columns + columnSelector_maxVisible : null, + columnSelector_minVisible : null, + + // responsive table hides columns with priority 1-6 at these breakpoints + // see http://view.jquerymobile.com/1.3.2/dist/demos/widgets/table-column-toggle/#Applyingapresetbreakpoint + // *** set to false to disable *** + columnSelector_breakpoints : [ '20em', '30em', '40em', '50em', '60em', '70em' ], + // maximum number of priority settings; if this value is changed (especially increased), + // then make sure to modify the columnSelector_breakpoints - see #1204 + columnSelector_maxPriorities : 6, + // data attribute containing column priority + // duplicates how jQuery mobile uses priorities: + // http://view.jquerymobile.com/1.3.2/dist/demos/widgets/table-column-toggle/ + columnSelector_priority : 'data-priority', + // class name added to checked checkboxes - this fixes an issue with Chrome not updating FontAwesome + // applied icons; use this class name (input.checked) instead of input:checked + columnSelector_cssChecked : 'checked', + // class name added to rows that have a span (e.g. grouping widget & other rows inside the tbody) + columnSelector_classHasSpan : 'hasSpan', + // event triggered when columnSelector completes + columnSelector_updated : 'columnUpdate' + }, + init: function(table, thisWidget, c, wo) { + tsColSel.init(table, c, wo); + }, + remove: function(table, c, wo, refreshing) { + var csel = c.selector; + if ( refreshing || !csel ) { return; } + if ( csel ) { csel.$container.empty(); } + if ( csel.$popup ) { csel.$popup.empty(); } + csel.$style.remove(); + csel.$breakpoints.remove(); + $( c.namespace + 'columnselector' + wo.columnSelector_classHasSpan ) + .removeClass( wo.filter_filteredRow || 'filtered' ); + c.$table.find('[data-col-span]').each(function(indx, el) { + var $el = $(el); + $el.attr('colspan', $el.attr('data-col-span')); + }); + c.$table.off('updateAll' + namespace + ' update' + namespace); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columns.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columns.js new file mode 100644 index 0000000..20a2ac0 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-columns.js @@ -0,0 +1,78 @@ +/*! 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 + } + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-cssStickyHeaders.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-cssStickyHeaders.js new file mode 100644 index 0000000..4a12268 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-cssStickyHeaders.js @@ -0,0 +1,168 @@ +/*! Widget: cssStickyHeaders - updated 6/16/2018 (v2.30.6) *//* +* Requires a modern browser, tablesorter v2.8+ +*/ +/*jshint jquery:true, unused:false */ +;(function($, window) { + 'use strict'; + + var ts = $.tablesorter; + + function cssStickyHeadersInit(c, wo) { + var offst, adjustY, + $table = c.$table, + $attach = $(wo.cssStickyHeaders_attachTo), + // target all versions of IE + isIE = 'ActiveXObject' in window || window.navigator.userAgent.indexOf('Edge') > -1, + namespace = c.namespace + 'cssstickyheader ', + $thead = $table.children('thead'), + $caption = $table.children('caption'), + $win = $attach.length ? $attach : $(window), + $parent = $table.parent().closest('table.' + ts.css.table), + $parentThead = $parent.length && ts.hasWidget($parent[0], 'cssStickyHeaders') ? $parent.children('thead') : [], + borderTopWidth = ( parseInt( $table.css('border-top-width'), 10 ) || 0 ), + // Fixes for Safari + tableH = $table.height(), + lastCaptionSetting = wo.cssStickyHeaders_addCaption, + // table offset top changes while scrolling in FF + adjustOffsetTop = false, + addCaptionHeight = false, + setTransform = function( $elms, y ) { + var translate = y === 0 ? '' : 'translate(0px,' + y + 'px)'; + $elms.css({ + 'transform' : translate, + '-ms-transform' : translate, + '-webkit-transform' : translate + }); + }; + + // Firefox fixes + if ($caption.length) { + // Firefox does not include the caption height when getting the table height + // see https://bugzilla.mozilla.org/show_bug.cgi?id=820891, so lets detect it instead of browser sniff + $caption.hide(); + addCaptionHeight = $table.height() === tableH; + $caption.show(); + + // Firefox changes the offset().top when translating the table caption + offst = $table.offset().top; + setTransform( $caption, 20 ); + adjustOffsetTop = $table.offset().top !== offst; + setTransform( $caption, 0 ); + } + + $win + .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) + .bind('scroll resize '.split(' ').join(namespace), function() { + // make sure "wo" is current otherwise changes to widgetOptions + // are not dynamic (like the add caption button in the demo) + wo = c.widgetOptions; + + if ( adjustOffsetTop ) { + // remove transform from caption to get the correct offset().top value + setTransform( $caption, 0 ); + adjustY = $table.offset().top; + } + + // Fix for safari, when caption present, table + // height changes while scrolling + if ($win.scrollTop() < $caption.outerHeight(true)) { + tableH = $table.height(); + } + + var top = $attach.length ? $attach.offset().top : $win.scrollTop(), + // add caption height; include table padding top & border-spacing or text may be above the fold (jQuery UI themes) + // border-spacing needed in Firefox, but not webkit... not sure if I should account for that + captionHeight = ( $caption.outerHeight(true) || 0 ) + + ( parseInt( $table.css('padding-top'), 10 ) || 0 ) + + ( parseInt( $table.css('border-spacing'), 10 ) || 0 ), + + bottom = tableH + ( addCaptionHeight && wo.cssStickyHeaders_addCaption ? captionHeight : 0 ) - + $thead.height() - ( $table.children('tfoot').height() || 0 ) - + ( wo.cssStickyHeaders_addCaption ? captionHeight : ( addCaptionHeight ? 0 : captionHeight ) ), + + parentTheadHeight = $parentThead.length ? $parentThead.height() : 0, + + // get bottom of nested sticky headers + nestedStickyBottom = $parentThead.length ? ( + isIE ? $parent.data('cssStickyHeaderBottom') + parentTheadHeight : + $parentThead.offset().top + parentTheadHeight - $win.scrollTop() + ) : 0, + + // in this case FF's offsetTop changes while scrolling, so we get a saved offsetTop before scrolling occurs + // but when the table is inside a wrapper ($attach) we need to continually update the offset top + tableOffsetTop = adjustOffsetTop ? adjustY : $table.offset().top, + + offsetTop = addCaptionHeight ? tableOffsetTop - ( wo.cssStickyHeaders_addCaption ? captionHeight : 0 ) : tableOffsetTop, + + // Detect nested tables - fixes #724 + deltaY = top - offsetTop + nestedStickyBottom + borderTopWidth + ( wo.cssStickyHeaders_offset || 0 ) - + ( wo.cssStickyHeaders_addCaption ? ( addCaptionHeight ? captionHeight : 0 ) : captionHeight ), + + finalY = deltaY > 0 && deltaY <= bottom ? deltaY : 0, + + // All IE (even IE11) can only transform header cells - fixes #447 thanks to @gakreol! + $cells = isIE ? $thead.children().children() : $thead; + + // more crazy IE stuff... + if (isIE) { + // I didn't bother testing 3 nested tables deep in IE, because I hate it + c.$table.data( 'cssStickyHeaderBottom', ( $parentThead.length ? parentTheadHeight : 0 ) - + ( wo.cssStickyHeaders_addCaption ? captionHeight : 0 ) ); + } + + if (wo.cssStickyHeaders_addCaption) { + $cells = $cells.add($caption); + } + if (lastCaptionSetting !== wo.cssStickyHeaders_addCaption) { + lastCaptionSetting = wo.cssStickyHeaders_addCaption; + // reset caption position if addCaption option is dynamically changed to false + if (!lastCaptionSetting) { + setTransform( $caption, 0 ); + } + } + + setTransform( $cells, finalY ); + + }); + $table + .unbind( ('filterEnd updateComplete '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) + .bind('filterEnd' + namespace, function() { + if (wo.cssStickyHeaders_filteredToTop) { + // scroll top of table into view + window.scrollTo(0, $table.position().top); + } + }) + .bind('updateComplete' + namespace, function() { + cssStickyHeadersInit(c, c.widgetOptions); + }); + } + + ts.addWidget({ + id: 'cssStickyHeaders', + priority: 10, + options: { + cssStickyHeaders_offset : 0, + cssStickyHeaders_addCaption : false, + // jQuery selector or object to attach sticky header to + cssStickyHeaders_attachTo : null, + cssStickyHeaders_filteredToTop : true + }, + init : function(table, thisWidget, c, wo) { + cssStickyHeadersInit(c, wo); + }, + remove: function(table, c, wo, refreshing) { + if (refreshing) { return; } + var namespace = c.namespace + 'cssstickyheader '; + $(window).unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); + c.$table + .unbind( ('filterEnd scroll resize updateComplete '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) + .add( c.$table.children('thead').children().children() ) + .children('thead, caption').css({ + 'transform' : '', + '-ms-transform' : '', + '-webkit-transform' : '' + }); + } + }); + +})(jQuery, window); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-currentSort.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-currentSort.js new file mode 100644 index 0000000..f4e02f3 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-currentSort.js @@ -0,0 +1,60 @@ +/*! Widget: currentSort - 7/31/2016 (v2.27.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter; + + ts.currentSortLanguage = { + 0: 'asc', + 1: 'desc', + 2: 'unsorted' + }; + + ts.currentSort = { + init : function( c ) { + c.$table.on( 'sortEnd.tscurrentSort', function() { + ts.currentSort.update( this.config ); + }); + }, + update: function( c ) { + if ( c ) { + var indx, + wo = c.widgetOptions, + lang = ts.currentSortLanguage, + unsort = lang[ 2 ], + // see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill + // order = new Array(c.columns).fill(unsort), + // the above ES6 will not work in all browsers, so + // we're stuck with this messy code to fill the array: + order = Array + .apply( null, Array( c.columns ) ) + .map( String.prototype.valueOf, unsort ), + sortList = c.sortList, + len = sortList.length; + for ( indx = 0; indx < len; indx++ ) { + order[ sortList[ indx ][ 0 ] ] = lang[ sortList[ indx ][ 1 ] ]; + } + c.currentSort = order; + if ( typeof wo.currentSort_callback === 'function' ) { + wo.currentSort_callback(c, order); + } + } + } + }; + + ts.addWidget({ + id: 'currentSort', + options: { + currentSort_callback : null + }, + init : function( table, thisWidget, c, wo ) { + ts.currentSort.init( c, wo ); + }, + remove : function( table, c ) { + c.$table.off( 'sortEnd.tscurrentSort' ); + } + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-editable.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-editable.js new file mode 100644 index 0000000..8e3f16d --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-editable.js @@ -0,0 +1,323 @@ +/*! Widget: editable - updated 2018-08-27 (v2.31.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;( function( $ ) { + 'use strict'; + + var tse = $.tablesorter.editable = { + namespace : '.tseditable', + // last edited class name + lastEdited: 'tseditable-last-edited-cell', + + editComplete: function( c, wo, $cell, refocus ) { + c.$table + .find( '.' + tse.lastEdited ) + .removeClass( tse.lastEdited ) + .trigger( wo.editable_editComplete, [ c ] ); + // restore focus last cell after updating + if ( refocus ) { + setTimeout( function() { + $cell.focus(); + }, 50 ); + } + }, + + selectAll: function( cell ) { + setTimeout( function() { + if ( document.queryCommandSupported( 'SelectAll' ) ) { + document.execCommand( 'selectAll', false, null ); + } else { + // select all text in contenteditable + // see http://stackoverflow.com/a/6150060/145346 + var range, selection; + if ( document.body.createTextRange ) { + range = document.body.createTextRange(); + range.moveToElementText( cell ); + range.select(); + } else if ( window.getSelection ) { + selection = window.getSelection(); + range = document.createRange(); + range.selectNodeContents( cell ); + selection.removeAllRanges(); + selection.addRange( range ); + } + } + // need delay of at least 100ms or last contenteditable will get refocused + }, 100 ); + }, + + getColumns : function( c, wo ) { + var list, indx, range, len, tmp, + editableColumns = wo.editable_columns, + cols = []; + if ( typeof editableColumns === 'string' ) { + // editable_columns can contain a range string, or comma separated values (e.g. '1,2-4,7') + list = editableColumns.replace( /\s+/, '' ).split( /,/ ); + len = list.length - 1; + while ( len >= 0 ) { + if ( list[ len ].indexOf( '-' ) >= 0 ) { + range = list[ len ].split( '-' ); + indx = parseInt( range[ 0 ], 10 ) || 0; + range = parseInt( range[ 1 ], 10) || c.columns - 1; + if ( indx > range ) { + // in case someone does '5-3' + tmp = indx; indx = range; range = tmp; + } + for ( ; indx <= range; indx++ ) { + cols.push( 'td:nth-child(' + ( indx + 1 ) + ')' ); + } + } else { + cols.push( 'td:nth-child(' + ( ( parseInt( list[ len ], 10 ) || 0 ) + 1 ) + ')' ); + } + len--; + } + } else if ( $.isArray( editableColumns ) ) { + len = editableColumns.length; + for ( indx = 0; indx < len; indx++ ) { + if ( editableColumns[ indx ] < c.columns ) { + cols.push( 'td:nth-child(' + ( editableColumns[ indx ] + 1 ) + ')' ); + } + } + } + return cols; + }, + + trimContent: function( wo, $cell ) { + if ( wo.editable_trimContent ) { + var html = $cell.html(); + if (html.trim() !== html) { + // Add to bypass Firefox issue - see #1570 + $cell.html( html === '' ? ' ' : html ); + } + } + }, + + update: function( c, wo ) { + var $t, $cells, cellIndex, cellLen, $editable, editableIndex, editableLen, + tmp = $( '<div>' ).wrapInner( wo.editable_wrapContent ).children().length || $.isFunction( wo.editable_wrapContent ), + cols = tse.getColumns( c, wo ).join( ',' ); + + // turn off contenteditable to allow dynamically setting the wo.editable_noEdit + // class on table cells - see issue #900 + c.$tbodies.find( cols ).find( '[contenteditable]' ).prop( 'contenteditable', false ); + + // IE does not allow making TR/TH/TD cells directly editable ( issue #404 ) + // so add a div or span inside ( it's faster than using wrapInner() ) + $cells = c.$tbodies.find( cols ).not( '.' + wo.editable_noEdit ); + cellLen = $cells.length; + for ( cellIndex = 0; cellIndex < cellLen; cellIndex++ ) { + /*jshint loopfunc:true */ + // test for children, if they exist, then make the children editable + $t = $cells.eq( cellIndex ); + if ( tmp && $t.children( 'div, span' ).length === 0 ) { + $t.wrapInner( wo.editable_wrapContent ); + if ($t.children().text().trim() === '') { + $t.children().html(' '); + } + } + $editable = $t.children( 'div, span' ).not( '.' + wo.editable_noEdit ); + editableLen = $editable.length; + if ( editableLen ) { + // make div/span children content editable + for ( editableIndex = 0; editableIndex < editableLen; editableIndex++ ) { + var $this = $editable.eq( editableIndex ); + tse.trimContent( wo, $this ); + $this.prop( 'contenteditable', true ); + } + } else { + tse.trimContent( wo, $t ); + $t.prop( 'contenteditable', true ); + } + } + }, + + bindEvents: function( c, wo ) { + var namespace = tse.namespace; + c.$table + .off( ( 'updateComplete pagerComplete '.split( ' ' ).join( namespace + ' ' ) ).replace( /\s+/g, ' ' ) ) + .on( 'updateComplete pagerComplete '.split( ' ' ).join( namespace + ' ' ), function() { + tse.update( c, c.widgetOptions ); + }) + // prevent sort initialized by user click on the header from changing the row indexing before + // updateCell can finish processing the change + .children( 'thead' ) + .add( $( c.namespace + '_extra_table' ).children( 'thead' ) ) + .off( 'mouseenter' + namespace ) + .on( 'mouseenter' + namespace, function() { + if ( c.$table.data( 'contentFocused' ) ) { + // change to 'true' instead of element to allow focusout to process + c.$table.data( 'contentFocused', true ); + $( ':focus' ).trigger( 'focusout' ); + } + }); + + c.$tbodies + .off( ( 'focus focusout keydown '.split( ' ' ).join( namespace + ' ' ) ).replace( /\s+/g, ' ' ) ) + .on( 'focus' + namespace, '[contenteditable]', function( e ) { + clearTimeout( $( this ).data( 'timer' ) ); + c.$table.data( 'contentFocused', e.target ); + c.table.isUpdating = true; // prevent sorting while editing + var $this = $( this ), + selAll = wo.editable_selectAll, + column = $this.closest( 'td' ).index(), + txt = $this.html(); + if ( wo.editable_trimContent ) { + txt = $.trim( txt === '' ? ' ' : txt ); + } + // prevent enter from adding into the content + $this + .off( 'keydown' + namespace ) + .on( 'keydown' + namespace, function( e ) { + if ( wo.editable_enterToAccept && e.which === 13 && !e.shiftKey ) { + e.preventDefault(); + } + }); + $this.data({ before : txt, original: txt }); + + if ( typeof wo.editable_focused === 'function' ) { + wo.editable_focused( txt, column, $this ); + } + + if ( selAll ) { + if ( typeof selAll === 'function' ) { + if ( selAll( txt, column, $this ) ) { + tse.selectAll( $this[0] ); + } + } else { + tse.selectAll( $this[0] ); + } + } + }) + .on( 'focusout keydown '.split( ' ' ).join( namespace + ' ' ), '[contenteditable]', function( e ) { + if ( !c.$table.data( 'contentFocused' ) ) { return; } + var t, validate, + valid = false, + $this = $( e.target ), + txt = $this.html(), + column = $this.closest( 'td' ).index(); + if ( wo.editable_trimContent ) { + txt = $.trim( txt === '' ? ' ' : txt ); + } + if ( e.which === 27 ) { + // user cancelled + $this + .html( $this.data( 'original' ) ) + .trigger( 'blur' + namespace ); + c.$table.data( 'contentFocused', false ); + c.table.isUpdating = false; + return false; + } + // accept on enter ( if set ), alt-enter ( always ) or if autoAccept is set and element is blurred or unfocused + t = e.which === 13 && !e.shiftKey && ( wo.editable_enterToAccept || e.altKey ) || wo.editable_autoAccept && e.type !== 'keydown'; + // change if new or user hits enter ( if option set ) + if ( t && $this.data( 'before' ) !== txt ) { + + validate = wo.editable_validate; + valid = txt; + + if ( typeof validate === 'function' ) { + valid = validate( txt, $this.data( 'original' ), column, $this ); + } else if ( typeof ( validate = $.tablesorter.getColumnData( c.table, validate, column ) ) === 'function' ) { + valid = validate( txt, $this.data( 'original' ), column, $this ); + } + + if ( t && valid !== false ) { + c.$table.find( '.' + tse.lastEdited ).removeClass( tse.lastEdited ); + $this + .addClass( tse.lastEdited ) + .html( valid ) + .data( 'before', valid ) + .data( 'original', valid ) + .trigger( 'change' ); + // prevent error if table was destroyed - see #1099 + if ( c.table.hasInitialized ) { + $.tablesorter.updateCell( c, $this.closest( 'td' ), false, function() { + if ( wo.editable_autoResort ) { + setTimeout( function() { + $.tablesorter.sortOn( c, c.sortList, function() { + tse.editComplete( c, wo, c.$table.data( 'contentFocused' ), true ); + }, true ); + }, 10 ); + } else { + tse.editComplete( c, wo, c.$table.data( 'contentFocused' ) ); + } + }); + } + return false; + } + } else if ( !valid && e.type !== 'keydown' ) { + clearTimeout( $this.data( 'timer' ) ); + $this.data( 'timer', setTimeout( function() { + c.table.isUpdating = false; // clear flag or sorting will be disabled + + if ( $.isFunction( wo.editable_blur ) ) { + txt = $this.html(); + wo.editable_blur( wo.editable_trimContent ? $.trim( txt ) : txt, column, $this ); + } + }, 100 ) ); + // restore original content on blur + $this.html( $this.data( 'original' ) ); + } + }) + // paste plain text from Excel - fixes #994 + .on('paste' + namespace, '[contenteditable]', function() { + var content, + $this = $(this); + // setTimeout needed to get pasted-in content + setTimeout(function() { + if ($this.is(':focus')) { + content = '<div>' + $this.html() + '</div>'; + $this.html( $(content).text().trim() ); + } + }, 0); + }); + }, + destroy : function( c, wo ) { + var namespace = tse.namespace, + cols = tse.getColumns( c, wo ), + + tmp = ( 'updateComplete pagerComplete '.split( ' ' ).join( namespace + ' ' ) ).replace( /\s+/g, ' ' ); + c.$table.off( tmp ); + + tmp = ( 'focus focusout keydown paste '.split( ' ' ).join( namespace + ' ' ) ).replace( /\s+/g, ' ' ); + c.$tbodies + .off( tmp ) + .find( cols.join( ',' ) ) + .find( '[contenteditable]' ) + .prop( 'contenteditable', false ); + } + + }; + + $.tablesorter.addWidget({ + id: 'editable', + options : { + editable_columns : [], + editable_enterToAccept : true, + editable_autoAccept : true, + editable_autoResort : false, + editable_wrapContent : '<div>', // wrap the cell content... makes this widget work in IE, and with autocomplete + editable_trimContent : true, // trim content inside of contenteditable ( remove tabs & carriage returns ) + editable_validate : null, // function( text, originalText ) { return text; } + editable_focused : null, // function( text, columnIndex, $element ) {} + editable_blur : null, // function( text, columnIndex, $element ) { } + editable_selectAll : false, // true/false or function( text, columnIndex, $element ) { return true; } + editable_noEdit : 'no-edit', + editable_editComplete : 'editComplete' + }, + init: function( table, thisWidget, c, wo ) { + if ( !wo.editable_columns.length ) { return; } + tse.update( c, wo ); + tse.bindEvents( c, wo ); + }, + remove : function( table, c, wo, refreshing ) { + if ( !refreshing ) { + tse.destroy( c, wo ) ; + } + } + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-html5.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-html5.js new file mode 100644 index 0000000..771fc9c --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-html5.js @@ -0,0 +1,431 @@ +/*! Widget: filter, html5 formatter functions - updated 7/17/2014 (v2.17.5) *//* + * requires: tableSorter (FORK) 2.15+ and jQuery 1.4.3+ + * + * html5Number (spinner) + * html5Range (slider) + * html5Color (color) + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter || {}, + + // compare option selector class name (jQuery selector) + compareSelect = '.compare-select', + + tsff = ts.filterFormatter = $.extend( {}, ts.filterFormatter, { + + addCompare: function($cell, indx, options) { + if (options.compare && $.isArray(options.compare) && options.compare.length > 1) { + var opt = '', + compareSelectClass = [ compareSelect.slice(1), ' ' + compareSelect.slice(1), '' ], + txt = options.cellText ? '<label class="' + compareSelectClass.join('-label') + indx + '">' + options.cellText + '</label>' : ''; + $.each(options.compare, function(i, c) { + opt += '<option ' + (options.selected === i ? 'selected' : '') + '>' + c + '</option>'; + }); + $cell + .wrapInner('<div class="' + compareSelectClass.join('-wrapper') + indx + '" />') + .prepend( txt + '<select class="' + compareSelectClass.join('') + indx + '" />' ) + .find('select') + .append(opt); + } + }, + + updateCompare : function($cell, $input, o) { + var val = $input.val() || '', + num = val.replace(/\s*?[><=]\s*?/g, ''), + compare = val.match(/[><=]/g) || ''; + if (o.compare) { + if ($.isArray(o.compare)) { + compare = (compare || []).join('') || o.compare[o.selected || 0]; + } + $cell.find(compareSelect).val( compare ); + } + return [ val, num ]; + }, + + /**********************\ + HTML5 Number (spinner) + \**********************/ + html5Number : function($cell, indx, def5Num) { + var t, o = $.extend({ + value : 0, + min : 0, + max : 100, + step : 1, + delayed : true, + disabled : false, + addToggle : false, + exactMatch : false, + cellText : '', + compare : '', + skipTest: false + }, def5Num), + + $input, + // test browser for HTML5 range support + $number = $('<input type="number" style="visibility:hidden;" value="test">').appendTo($cell), + // test if HTML5 number is supported - from Modernizr + numberSupported = o.skipTest || $number.attr('type') === 'number' && $number.val() !== 'test', + $shcell = [], + c = $cell.closest('table')[0].config, + + updateNumber = function(delayed, notrigger) { + var chkd = o.addToggle ? $cell.find('.toggle').is(':checked') : true, + v = $cell.find('.number').val(), + compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '', + searchType = c.$table[0].hasInitialized ? (delayed ? delayed : o.delayed) || '' : true; + $input + // add equal to the beginning, so we filter exact numbers + .val( !o.addToggle || chkd ? (compare ? compare : o.exactMatch ? '=' : '') + v : '' ) + .trigger( notrigger ? '' : 'search', searchType ).end() + .find('.number').val(v); + if ($cell.find('.number').length) { + $cell.find('.number')[0].disabled = (o.disabled || !chkd); + } + // update sticky header cell + if ($shcell.length) { + $shcell.find('.number').val(v)[0].disabled = (o.disabled || !chkd); + $shcell.find(compareSelect).val(compare); + if (o.addToggle) { + $shcell.find('.toggle')[0].checked = chkd; + } + } + }; + $number.remove(); + + if (numberSupported) { + t = o.addToggle ? '<div class="button"><input id="html5button' + indx + '" type="checkbox" class="toggle" />' + + '<label for="html5button' + indx + '"></label></div>' : ''; + t += '<input class="number" type="number" min="' + o.min + '" max="' + o.max + '" value="' + + o.value + '" step="' + o.step + '" />'; + // add HTML5 number (spinner) + $cell + .append(t + '<input type="hidden" />') + .find('.toggle, .number').bind('change', function() { + updateNumber(); + }) + .closest('thead').find('th[data-column=' + indx + ']') + .addClass('filter-parsed') // get exact numbers from column + // on reset + .closest('table').bind('filterReset', function() { + if ($.isArray(o.compare)) { + $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] ); + } + // turn off the toggle checkbox + if (o.addToggle) { + $cell.find('.toggle')[0].checked = false; + if ($shcell.length) { + $shcell.find('.toggle')[0].checked = false; + } + } + $cell.find('.number').val( o.value ); + setTimeout(function() { + updateNumber(); + }, 0); + }); + $input = $cell.find('input[type=hidden]').bind('change', function() { + $cell.find('.number').val( this.value ); + updateNumber(); + }); + + // update slider from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate', function() { + var val = tsff.updateCompare($cell, $input, o)[0] || o.value; + $cell.find('.number').val( ((val || '') + '').replace(/[><=]/g, '') ); + updateNumber(false, true); + ts.filter.formatterUpdated($cell, indx); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($cell, indx, o); + $cell.find(compareSelect).bind('change', function() { + updateNumber(); + }); + } + + // has sticky headers? + c.$table.bind('stickyHeadersInit', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + $shcell + .append(t) + .find('.toggle, .number').bind('change', function() { + $cell.find('.number').val( $(this).val() ); + updateNumber(); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($shcell, indx, o); + $shcell.find(compareSelect).bind('change', function() { + $cell.find(compareSelect).val( $(this).val() ); + updateNumber(); + }); + } + + updateNumber(); + }); + + updateNumber(); + + } + + return numberSupported ? $cell.find('input[type="hidden"]') : $('<input type="search">'); + }, + + /**********************\ + HTML5 range slider + \**********************/ + html5Range : function($cell, indx, def5Range) { + var o = $.extend({ + value : 0, + min : 0, + max : 100, + step : 1, + delayed : true, + valueToHeader : true, + exactMatch : true, + cellText : '', + compare : '', + allText : 'all', + skipTest : false + }, def5Range), + + $input, + // test browser for HTML5 range support + $range = $('<input type="range" style="visibility:hidden;" value="test">').appendTo($cell), + // test if HTML5 range is supported - from Modernizr (but I left out the method to detect in Safari 2-4) + // see https://github.com/Modernizr/Modernizr/blob/master/feature-detects/inputtypes.js + rangeSupported = o.skipTest || $range.attr('type') === 'range' && $range.val() !== 'test', + $shcell = [], + c = $cell.closest('table')[0].config, + + updateRange = function(v, delayed, notrigger) { + /*jshint eqeqeq:false */ + // hidden input changes may include compare symbols + v = ( typeof v === 'undefined' ? $input.val() : v ).toString().replace(/[<>=]/g, '') || o.value; + var compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '', + // eslint-disable-next-line eqeqeq + t = ' (' + (compare ? compare + v : v == o.min ? o.allText : v) + ')', + searchType = c.$table[0].hasInitialized ? (delayed ? delayed : o.delayed) || '' : true; + $cell.find('input[type=hidden]') + // add equal to the beginning, so we filter exact numbers + // eslint-disable-next-line eqeqeq + .val( ( compare ? compare + v : ( v == o.min ? '' : ( o.exactMatch ? '=' : '' ) + v ) ) ) + // ( val == o.min ? '' : val + (o.exactMatch ? '=' : '')) + .trigger( notrigger ? '' : 'search', searchType ).end() + .find('.range').val(v); + // or add current value to the header cell, if desired + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.curvalue').html(t); + // update sticky header cell + if ($shcell.length) { + $shcell + .find('.range').val(v).end() + .find(compareSelect).val( compare ); + $shcell.closest('thead').find('th[data-column=' + indx + ']').find('.curvalue').html(t); + } + }; + $range.remove(); + + if (rangeSupported) { + // add HTML5 range + $cell + .html('<input type="hidden"><input class="range" type="range" min="' + o.min + '" max="' + o.max + '" value="' + o.value + '" />') + .closest('thead').find('th[data-column=' + indx + ']') + .addClass('filter-parsed') // get exact numbers from column + // add span to header for the current slider value + .find('.tablesorter-header-inner').append('<span class="curvalue" />'); + // hidden filter update namespace trigger by filter widget + $input = $cell.find('input[type=hidden]').bind('change' + c.namespace + 'filter', function() { + /*jshint eqeqeq:false */ + var v = this.value, + compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || ''; + if (v !== this.lastValue) { + // eslint-disable-next-line eqeqeq + this.lastValue = ( compare ? compare + v : ( v == o.min ? '' : ( o.exactMatch ? '=' : '' ) + v ) ); + this.value = this.lastValue; + updateRange( v ); + } + }); + + $cell.find('.range').bind('change', function() { + updateRange( this.value ); + }); + + // update spinner from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate', function() { + var val = tsff.updateCompare($cell, $input, o)[0]; + $cell.find('.range').val( val ); + updateRange(val, false, true); + ts.filter.formatterUpdated($cell, indx); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($cell, indx, o); + $cell.find(compareSelect).bind('change', function() { + updateRange(); + }); + } + + // has sticky headers? + c.$table.bind('stickyHeadersInit', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + $shcell + .html('<input class="range" type="range" min="' + o.min + '" max="' + o.max + '" value="' + o.value + '" />') + .find('.range').bind('change', function() { + updateRange( $shcell.find('.range').val() ); + }); + updateRange(); + + if (o.compare) { + // add compare select + tsff.addCompare($shcell, indx, o); + $shcell.find(compareSelect).bind('change', function() { + $cell.find(compareSelect).val( $(this).val() ); + updateRange(); + }); + } + + }); + + // on reset + $cell.closest('table').bind('filterReset', function() { + if ($.isArray(o.compare)) { + $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] ); + } + setTimeout(function() { + updateRange(o.value, false, true); + }, 0); + }); + updateRange(); + + } + + return rangeSupported ? $cell.find('input[type="hidden"]') : $('<input type="search">'); + }, + + /**********************\ + HTML5 Color picker + \**********************/ + html5Color: function($cell, indx, defColor) { + var t, o = $.extend({ + value : '#000000', + disabled : false, + addToggle : true, + exactMatch : true, + valueToHeader : false, + skipTest : false + }, defColor), + $input, + // Add a hidden input to hold the range values + $color = $('<input type="color" style="visibility:hidden;" value="test">').appendTo($cell), + // test if HTML5 color is supported - from Modernizr + colorSupported = o.skipTest || $color.attr('type') === 'color' && $color.val() !== 'test', + $shcell = [], + c = $cell.closest('table')[0].config, + + updateColor = function(v, notrigger) { + v = ( typeof v === 'undefined' ? $input.val() : v ).toString().replace('=', '') || o.value; + var chkd = true, + t = ' (' + v + ')'; + if (o.addToggle) { + chkd = $cell.find('.toggle').is(':checked'); + } + if ($cell.find('.colorpicker').length) { + $cell.find('.colorpicker').val(v)[0].disabled = (o.disabled || !chkd); + } + + $input + .val( chkd ? v + (o.exactMatch ? '=' : '') : '' ) + .trigger( !c.$table[0].hasInitialized || notrigger ? '' : 'search' ); + if (o.valueToHeader) { + // add current color to the header cell + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.curcolor').html(t); + } else { + // current color to span in cell + $cell.find('.currentColor').html(t); + } + + // update sticky header cell + if ($shcell.length) { + $shcell.find('.colorpicker').val(v)[0].disabled = (o.disabled || !chkd); + if (o.addToggle) { + $shcell.find('.toggle')[0].checked = chkd; + } + if (o.valueToHeader) { + // add current color to the header cell + $shcell.closest('thead').find('th[data-column=' + indx + ']').find('.curcolor').html(t); + } else { + // current color to span in cell + $shcell.find('.currentColor').html(t); + } + } + }; + $color.remove(); + + if (colorSupported) { + t = '' + indx + Math.round(Math.random() * 100); + // add HTML5 color picker + t = '<div class="color-controls-wrapper">' + + (o.addToggle ? '<div class="button"><input id="colorbutton' + t + '" type="checkbox" class="toggle" /><label for="colorbutton' + + t + '"></label></div>' : '') + + '<input type="hidden"><input class="colorpicker" type="color" />' + + (o.valueToHeader ? '' : '<span class="currentColor">(#000000)</span>') + '</div>'; + $cell.html(t); + // add span to header for the current color value - only works if the line in the updateColor() function is also un-commented out + if (o.valueToHeader) { + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.tablesorter-header-inner').append('<span class="curcolor" />'); + } + + $cell.find('.toggle, .colorpicker').bind('change', function() { + updateColor( $cell.find('.colorpicker').val() ); + }); + + // hidden filter update namespace trigger by filter widget + $input = $cell.find('input[type=hidden]').bind('change' + c.namespace + 'filter', function() { + updateColor( this.value ); + }); + + // update slider from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate', function() { + updateColor( $input.val(), true ); + ts.filter.formatterUpdated($cell, indx); + }); + + // on reset + $cell.closest('table').bind('filterReset', function() { + // just turn off the colorpicker + if (o.addToggle) { + $cell.find('.toggle')[0].checked = false; + } + // delay needed because default color needs to be set in the filter + // there is no compare option here, so if addToggle = false, + // default color is #000000 (even with no value set) + setTimeout(function() { + updateColor(); + }, 0); + }); + + // has sticky headers? + c.$table.bind('stickyHeadersInit', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx); + $shcell + .html(t) + .find('.toggle, .colorpicker').bind('change', function() { + updateColor( $shcell.find('.colorpicker').val() ); + }); + updateColor( $shcell.find('.colorpicker').val() ); + }); + + updateColor( o.value ); + } + return colorSupported ? $cell.find('input[type="hidden"]') : $('<input type="search">'); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-jui.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-jui.js new file mode 100644 index 0000000..6e39d63 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-jui.js @@ -0,0 +1,770 @@ +/*! Widget: filter jQuery UI formatter functions - updated 7/17/2014 (v2.17.5) *//* + * requires: tableSorter (FORK) 2.15+ and jQuery 1.4.3+ + * + * uiSpinner (jQuery UI spinner) + * uiSlider (jQuery UI slider) + * uiRange (jQuery UI range slider) + * uiDateCompare (jQuery UI datepicker; 1 input) + * uiDatepicker (jQuery UI datepicker; 2 inputs, filter range) + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter || {}, + + // compare option selector class name (jQuery selector) + compareSelect = '.compare-select', + + tsff = ts.filterFormatter = $.extend( {}, ts.filterFormatter, { + + addCompare: function($cell, indx, options) { + if (options.compare && $.isArray(options.compare) && options.compare.length > 1) { + var opt = '', + compareSelectClass = [ compareSelect.slice(1), ' ' + compareSelect.slice(1), '' ], + txt = options.cellText ? '<label class="' + compareSelectClass.join('-label') + indx + '">' + options.cellText + '</label>' : ''; + $.each(options.compare, function(i, c) { + opt += '<option ' + (options.selected === i ? 'selected' : '') + '>' + c + '</option>'; + }); + $cell + .wrapInner('<div class="' + compareSelectClass.join('-wrapper') + indx + '" />') + .prepend( txt + '<select class="' + compareSelectClass.join('') + indx + '" />' ) + .find('select') + .append(opt); + } + }, + + updateCompare : function($cell, $input, o) { + var val = $input.val() || '', + num = val.replace(/\s*?[><=]\s*?/g, ''), + compare = val.match(/[><=]/g) || ''; + if (o.compare) { + if ($.isArray(o.compare)) { + compare = (compare || []).join('') || o.compare[o.selected || 0]; + } + $cell.find(compareSelect).val( compare ); + } + return [ val, num ]; + }, + + /**********************\ + jQuery UI Spinner + \**********************/ + uiSpinner: function($cell, indx, spinnerDef) { + var o = $.extend({ + // filter formatter options + delayed : true, + addToggle : true, + exactMatch : true, + value : 1, + cellText : '', + compare : '', + // include ANY jQuery UI spinner options below + min : 0, + max : 100, + step : 1, + disabled : false + + }, spinnerDef ), + c = $cell.closest('table')[0].config, + // Add a hidden input to hold the range values + $input = $('<input class="filter" type="hidden">') + .appendTo($cell) + // hidden filter update namespace trigger by filter widget + .bind('change' + c.namespace + 'filter', function() { + updateSpinner({ value: this.value, delayed: false }); + }), + $shcell = [], + + // this function updates the hidden input and adds the current values to the header cell text + updateSpinner = function(ui, notrigger) { + var chkd = true, state, + // ui is not undefined on create + v = ui && ui.value && ts.formatFloat((ui.value + '').replace(/[><=]/g, '')) || + $cell.find('.spinner').val() || o.value, + compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '', + searchType = ui && typeof ui.delayed === 'boolean' ? ui.delayed : c.$table[0].hasInitialized ? o.delayed || '' : true; + if (o.addToggle) { + chkd = $cell.find('.toggle').is(':checked'); + } + state = o.disabled || !chkd ? 'disable' : 'enable'; + if (!ts.isEmptyObject($cell.find('.spinner').data())) { + $cell.find('.filter') + // add equal to the beginning, so we filter exact numbers + .val( chkd ? (compare ? compare : o.exactMatch ? '=' : '') + v : '' ) + .trigger( notrigger ? '' : 'search', searchType ).end() + .find('.spinner').spinner(state).val(v); + // update sticky header cell + if ($shcell.length) { + $shcell + .find('.spinner').spinner(state).val(v).end() + .find(compareSelect).val( compare ); + if (o.addToggle) { + $shcell.find('.toggle')[0].checked = chkd; + } + } + } + }; + + // add callbacks; preserve added callbacks + o.oldcreate = o.create; + o.oldspin = o.spin; + o.create = function(event, ui) { + updateSpinner(); // ui is an empty object on create + if (typeof o.oldcreate === 'function') { o.oldcreate(event, ui); } + }; + o.spin = function(event, ui) { + updateSpinner(ui); + if (typeof o.oldspin === 'function') { o.oldspin(event, ui); } + }; + if (o.addToggle) { + $('<div class="button"><input id="uispinnerbutton' + indx + '" type="checkbox" class="toggle" />' + + '<label for="uispinnerbutton' + indx + '"></label></div>') + .appendTo($cell) + .find('.toggle') + .bind('change', function() { + updateSpinner(); + }); + } + // make sure we use parsed data + $cell.closest('thead').find('th[data-column=' + indx + ']').addClass('filter-parsed'); + // add a jQuery UI spinner! + $('<input class="spinner spinner' + indx + '" />') + .val(o.value) + .appendTo($cell) + .spinner(o) + .bind('change keyup', function() { + updateSpinner(); + }); + + // update spinner from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate' + c.namespace + 'filter', function() { + var val = tsff.updateCompare($cell, $input, o)[0]; + $cell.find('.spinner').val( val ); + updateSpinner({ value: val }, true); + ts.filter.formatterUpdated($cell, indx); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($cell, indx, o); + $cell.find(compareSelect).bind('change', function() { + updateSpinner(); + }); + } + + // has sticky headers? + c.$table.bind('stickyHeadersInit' + c.namespace + 'filter', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + if (o.addToggle) { + $('<div class="button"><input id="stickyuispinnerbutton' + indx + '" type="checkbox" class="toggle" />' + + '<label for="stickyuispinnerbutton' + indx + '"></label></div>') + .appendTo($shcell) + .find('.toggle') + .bind('change', function() { + $cell.find('.toggle')[0].checked = this.checked; + updateSpinner(); + }); + } + // add a jQuery UI spinner! + $('<input class="spinner spinner' + indx + '" />') + .val(o.value) + .appendTo($shcell) + .spinner(o) + .bind('change keyup', function() { + $cell.find('.spinner').val( this.value ); + updateSpinner(); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($shcell, indx, o); + $shcell.find(compareSelect).bind('change', function() { + $cell.find(compareSelect).val( $(this).val() ); + updateSpinner(); + }); + } + + }); + + // on reset + c.$table.bind('filterReset' + c.namespace + 'filter', function() { + if ($.isArray(o.compare)) { + $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] ); + } + // turn off the toggle checkbox + if (o.addToggle) { + $cell.find('.toggle')[0].checked = false; + } + $cell.find('.spinner').spinner('value', o.value); + setTimeout(function() { + updateSpinner(); + }, 0); + }); + + updateSpinner(); + return $input; + }, + + /**********************\ + jQuery UI Slider + \**********************/ + uiSlider: function($cell, indx, sliderDef) { + var o = $.extend({ + // filter formatter options + delayed : true, + valueToHeader : false, + exactMatch : true, + cellText : '', + compare : '', + allText : 'all', + // include ANY jQuery UI spinner options below + // except values, since this is a non-range setup + value : 0, + min : 0, + max : 100, + step : 1, + range : 'min' + }, sliderDef ), + c = $cell.closest('table')[0].config, + // Add a hidden input to hold the range values + $input = $('<input class="filter" type="hidden">') + .appendTo($cell) + // hidden filter update namespace trigger by filter widget + .bind('change' + c.namespace + 'filter', function() { + updateSlider({ value: this.value }); + }), + $shcell = [], + + // this function updates the hidden input and adds the current values to the header cell text + updateSlider = function(ui, notrigger) { + // ui is not undefined on create + var v = typeof ui !== 'undefined' ? ts.formatFloat((ui.value + '').replace(/[><=]/g, '')) || o.value : o.value, + val = o.compare ? v : v === o.min ? o.allText : v, + compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '', + result = compare + val, + searchType = ui && typeof ui.delayed === 'boolean' ? ui.delayed : c.$table[0].hasInitialized ? o.delayed || '' : true; + if (o.valueToHeader) { + // add range indication to the header cell above! + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.curvalue').html(' (' + result + ')'); + } else { + // add values to the handle data-value attribute so the css tooltip will work properly + $cell.find('.ui-slider-handle').addClass('value-popup').attr('data-value', result); + } + // prevent JS error if "resetToLoadState" or filter widget was removed for another reason + if (!ts.isEmptyObject($cell.find('.slider').data())) { + // update the hidden input; + $cell.find('.filter') + // ****** ADD AN EQUAL SIGN TO THE BEGINNING! <- this makes the slide exactly match the number ****** + // when the value is at the minimum, clear the hidden input so all rows will be seen + .val( ( compare ? compare + v : v === o.min ? '' : (o.exactMatch ? '=' : '') + v ) ) + .trigger( notrigger ? '' : 'search', searchType ).end() + .find('.slider').slider('value', v); + + // update sticky header cell + if ($shcell.length) { + $shcell + .find(compareSelect).val( compare ).end() + .find('.slider').slider('value', v); + if (o.valueToHeader) { + $shcell.closest('thead').find('th[data-column=' + indx + ']').find('.curvalue').html(' (' + result + ')'); + } else { + $shcell.find('.ui-slider-handle').addClass('value-popup').attr('data-value', result); + } + } + } + + }; + $cell.closest('thead').find('th[data-column=' + indx + ']').addClass('filter-parsed'); + + // add span to header for value - only works if the line in the updateSlider() function is also un-commented out + if (o.valueToHeader) { + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.tablesorter-header-inner').append('<span class="curvalue" />'); + } + + // add callbacks; preserve added callbacks + o.oldcreate = o.create; + o.oldslide = o.slide; + o.create = function(event, ui) { + updateSlider(); // ui is an empty object on create + if (typeof o.oldcreate === 'function') { o.oldcreate(event, ui); } + }; + o.slide = function(event, ui) { + updateSlider(ui); + if (typeof o.oldslide === 'function') { o.oldslide(event, ui); } + }; + // add a jQuery UI slider! + $('<div class="slider slider' + indx + '"/>') + .appendTo($cell) + .slider(o); + + // update slider from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate' + c.namespace + 'filter', function() { + var val = tsff.updateCompare($cell, $input, o)[0]; + $cell.find('.slider').slider('value', val ); + updateSlider({ value: val }, false); + ts.filter.formatterUpdated($cell, indx); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($cell, indx, o); + $cell.find(compareSelect).bind('change', function() { + updateSlider({ value: $cell.find('.slider').slider('value') }); + }); + } + + // on reset + c.$table.bind('filterReset' + c.namespace + 'filter', function() { + if ($.isArray(o.compare)) { + $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] ); + } + setTimeout(function() { + updateSlider({ value: o.value }); + }, 0); + }); + + // has sticky headers? + c.$table.bind('stickyHeadersInit' + c.namespace + 'filter', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + + // add a jQuery UI slider! + $('<div class="slider slider' + indx + '"/>') + .val(o.value) + .appendTo($shcell) + .slider(o) + .bind('change keyup', function() { + $cell.find('.slider').slider('value', this.value ); + updateSlider(); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($shcell, indx, o); + $shcell.find(compareSelect).bind('change', function() { + $cell.find(compareSelect).val( $(this).val() ); + updateSlider(); + }); + } + + }); + + return $input; + }, + + /*************************\ + jQuery UI Range Slider (2 handles) + \*************************/ + uiRange: function($cell, indx, rangeDef) { + var o = $.extend({ + // filter formatter options + delayed : true, + valueToHeader : false, + // include ANY jQuery UI spinner options below + // except value, since this one is range specific) + values : [ 0, 100 ], + min : 0, + max : 100, + range : true + }, rangeDef ), + c = $cell.closest('table')[0].config, + // Add a hidden input to hold the range values + $input = $('<input class="filter" type="hidden">') + .appendTo($cell) + // hidden filter update namespace trigger by filter widget + .bind('change' + c.namespace + 'filter', function() { + getRange(); + }), + $shcell = [], + + getRange = function() { + var val = $input.val(), + v = val.split(' - '); + if (val === '') { v = [ o.min, o.max ]; } + if (v && v[1]) { + updateUiRange({ values: v, delay: false }, true); + } + }, + + // this function updates the hidden input and adds the current values to the header cell text + updateUiRange = function(ui, notrigger) { + // ui.values are undefined for some reason on create + var val = ui && ui.values || o.values, + result = val[0] + ' - ' + val[1], + // make range an empty string if entire range is covered so the filter row will hide (if set) + range = val[0] === o.min && val[1] === o.max ? '' : result, + searchType = ui && typeof ui.delayed === 'boolean' ? ui.delayed : c.$table[0].hasInitialized ? o.delayed || '' : true; + if (o.valueToHeader) { + // add range indication to the header cell above (if not using the css method)! + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.currange').html(' (' + result + ')'); + } else { + // add values to the handle data-value attribute so the css tooltip will work properly + $cell.find('.ui-slider-handle') + .addClass('value-popup') + .eq(0).attr('data-value', val[0]).end() // adding value to data attribute + .eq(1).attr('data-value', val[1]); // value popup shown via css + } + if (!ts.isEmptyObject($cell.find('.range').data())) { + // update the hidden input + $cell.find('.filter').val(range) + .trigger(notrigger ? '' : 'search', searchType).end() + .find('.range').slider('values', val); + // update sticky header cell + if ($shcell.length) { + $shcell.find('.range').slider('values', val); + if (o.valueToHeader) { + $shcell.closest('thead').find('th[data-column=' + indx + ']').find('.currange').html(' (' + result + ')'); + } else { + $shcell.find('.ui-slider-handle') + .addClass('value-popup') + .eq(0).attr('data-value', val[0]).end() // adding value to data attribute + .eq(1).attr('data-value', val[1]); // value popup shown via css + } + } + } + + }; + $cell.closest('thead').find('th[data-column=' + indx + ']').addClass('filter-parsed'); + + // add span to header for value - only works if the line in the updateUiRange() function is also un-commented out + if (o.valueToHeader) { + $cell.closest('thead').find('th[data-column=' + indx + ']').find('.tablesorter-header-inner').append('<span class="currange"/>'); + } + + // add callbacks; preserve added callbacks + o.oldcreate = o.create; + o.oldslide = o.slide; + // add a jQuery UI range slider! + o.create = function(event, ui) { + updateUiRange(); // ui is an empty object on create + if (typeof o.oldcreate === 'function') { o.oldcreate(event, ui); } + }; + o.slide = function(event, ui) { + updateUiRange(ui); + if (typeof o.oldslide === 'function') { o.oldslide(event, ui); } + }; + $('<div class="range range' + indx + '"/>') + .appendTo($cell) + .slider(o); + + // update slider from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate' + c.namespace + 'filter', function() { + getRange(); + ts.filter.formatterUpdated($cell, indx); + }); + + // on reset + c.$table.bind('filterReset' + c.namespace + 'filter', function() { + $cell.find('.range').slider('values', o.values); + setTimeout(function() { + updateUiRange(); + }, 0); + }); + + // has sticky headers? + c.$table.bind('stickyHeadersInit' + c.namespace + 'filter', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + + // add a jQuery UI slider! + $('<div class="range range' + indx + '"/>') + .val(o.value) + .appendTo($shcell) + .slider(o) + .bind('change keyup', function() { + $cell.find('.range').val( this.value ); + updateUiRange(); + }); + + }); + + // return the hidden input so the filter widget has a reference to it + return $input; + }, + + /*************************\ + jQuery UI Datepicker compare (1 input) + \*************************/ + uiDateCompare: function($cell, indx, defDate) { + var o = $.extend({ + // filter formatter options + cellText : '', + compare : '', + endOfDay : true, + // include ANY jQuery UI spinner options below + + defaultDate : '', + + changeMonth : true, + changeYear : true, + numberOfMonths : 1 + }, defDate), + + $date, + c = $cell.closest('table')[0].config, + // make sure we're using parsed dates in the search + $hdr = $cell.closest('thead').find('th[data-column=' + indx + ']').addClass('filter-parsed'), + // Add a hidden input to hold the range values + $input = $('<input class="dateCompare" type="hidden">') + .appendTo($cell) + // hidden filter update namespace trigger by filter widget + .bind('change' + c.namespace + 'filter', function() { + var v = this.value; + if (v) { + o.onClose(v); + } + }), + t, $shcell = [], + + // this function updates the hidden input + date1Compare = function(notrigger) { + var date, query, + getdate = $date.datepicker('getDate') || '', + compare = ($.isArray(o.compare) ? $cell.find(compareSelect).val() || o.compare[ o.selected || 0] : o.compare) || '', + searchType = c.$table[0].hasInitialized ? o.delayed || '' : true; + $date.datepicker('setDate', (getdate === '' ? '' : getdate) || null); + if (getdate === '') { notrigger = false; } + date = $date.datepicker('getDate'); + query = date ? ( o.endOfDay && /<=/.test(compare) ? date.setHours(23, 59, 59, 999) : date.getTime() ) || '' : ''; + if (date && o.endOfDay && compare === '=') { + compare = ''; + query += ' - ' + date.setHours(23, 59, 59, 999); + notrigger = false; + } + $cell.find('.dateCompare') + // add equal to the beginning, so we filter exact numbers + .val(compare + query) + .trigger( notrigger ? '' : 'search', searchType ).end(); + // update sticky header cell + if ($shcell.length) { + $shcell + .find('.dateCompare').val(compare + query).end() + .find(compareSelect).val(compare); + } + }; + + // Add date range picker + t = '<input type="text" class="date date' + indx + '" placeholder="' + + ($hdr.data('placeholder') || $hdr.attr('data-placeholder') || c.widgetOptions.filter_placeholder.search || '') + '" />'; + $date = $(t).appendTo($cell); + + // add callbacks; preserve added callbacks + o.oldonClose = o.onClose; + + o.onClose = function( selectedDate, ui ) { + date1Compare(); + if (typeof o.oldonClose === 'function') { o.oldonClose(selectedDate, ui); } + }; + $date.datepicker(o); + + // on reset + c.$table.bind('filterReset' + c.namespace + 'filter', function() { + if ($.isArray(o.compare)) { + $cell.add($shcell).find(compareSelect).val( o.compare[ o.selected || 0 ] ); + } + $cell.add($shcell).find('.date').val(o.defaultDate).datepicker('setDate', o.defaultDate || null); + setTimeout(function() { + date1Compare(); + }, 0); + }); + + // update date compare from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate' + c.namespace + 'filter', function() { + var num, v = $input.val(); + if (/\s+-\s+/.test(v)) { + // date range found; assume an exact match on one day + $cell.find(compareSelect).val('='); + num = v.split(/\s+-\s+/)[0]; + $date.datepicker( 'setDate', num || null ); + } else { + num = (tsff.updateCompare($cell, $input, o)[1]).toString() || ''; + // differeniate 1388556000000 from 1/1/2014 using \d{5} regex + num = num !== '' ? /\d{5}/g.test(num) ? new Date(Number(num)) : num || '' : ''; + } + $cell.add($shcell).find('.date').datepicker( 'setDate', num || null ); + setTimeout(function() { + date1Compare(true); + ts.filter.formatterUpdated($cell, indx); + }, 0); + }); + + if (o.compare) { + // add compare select + tsff.addCompare($cell, indx, o); + $cell.find(compareSelect).bind('change', function() { + date1Compare(); + }); + } + + // has sticky headers? + c.$table.bind('stickyHeadersInit' + c.namespace + 'filter', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + + // add a jQuery datepicker! + $shcell + .append(t) + .find('.date') + .datepicker(o); + + if (o.compare) { + // add compare select + tsff.addCompare($shcell, indx, o); + $shcell.find(compareSelect).bind('change', function() { + $cell.find(compareSelect).val( $(this).val() ); + date1Compare(); + }); + } + + }); + + // return the hidden input so the filter widget has a reference to it + return $input.val( o.defaultDate ? o.defaultDate : '' ); + }, + + /*************************\ + jQuery UI Datepicker (2 inputs) + \*************************/ + uiDatepicker: function($cell, indx, defDate) { + var o = $.extend({ + // filter formatter options + endOfDay : true, + textFrom : 'from', + textTo : 'to', + from : '', // defaultDate 'from' input + to : '', // defaultDate 'to' input + // include ANY jQuery UI spinner options below + changeMonth : true, + changeYear : true, + numberOfMonths : 1 + }, defDate), + t, closeDate, $shcell = [], + c = $cell.closest('table')[0].config, + validDate = function(d) { + return d instanceof Date && isFinite(d); + }, + // Add a hidden input to hold the range values + $input = $('<input class="dateRange" type="hidden">') + .appendTo($cell) + // hidden filter update namespace trigger by filter widget + .bind('change' + c.namespace + 'filter', function() { + var v = this.value; + if (v.match(' - ')) { + v = v.split(' - '); + $cell.find('.dateTo').val(v[1]); + closeDate(v[0]); + } else if (v.match('>=')) { + closeDate( v.replace('>=', '') ); + } else if (v.match('<=')) { + closeDate( v.replace('<=', '') ); + } + }), + + // make sure we're using parsed dates in the search + $hdr = $cell.closest('thead').find('th[data-column=' + indx + ']').addClass('filter-parsed'); + // Add date range picker + t = '<label>' + o.textFrom + '</label><input type="text" class="dateFrom" placeholder="' + + ($hdr.data('placeholderFrom') || $hdr.attr('data-placeholder-from') || c.widgetOptions.filter_placeholder.from || '') + '" />' + + '<label>' + o.textTo + '</label><input type="text" class="dateTo" placeholder="' + + ($hdr.data('placeholderTo') || $hdr.attr('data-placeholder-to') || c.widgetOptions.filter_placeholder.to || '') + '" />'; + $(t).appendTo($cell); + + // add callbacks; preserve added callbacks + o.oldonClose = o.onClose; + + closeDate = o.onClose = function( selectedDate, ui ) { + var range, + from = $cell.find('.dateFrom').datepicker('getDate'), + to = $cell.find('.dateTo').datepicker('getDate'); + from = validDate(from) ? from.getTime() : ''; + to = validDate(to) ? ( o.endOfDay ? to.setHours(23, 59, 59, 999) : to.getTime() ) || '' : ''; + range = from ? ( to ? from + ' - ' + to : '>=' + from ) : (to ? '<=' + to : ''); + $cell.add( $shcell ) + .find('.dateRange').val(range) + .trigger('search'); + // date picker needs date objects + from = from ? new Date(from) : ''; + to = to ? new Date(to) : ''; + + if (/<=/.test(range)) { + $cell.add( $shcell ) + .find('.dateFrom').datepicker('option', 'maxDate', to || null ).end() + .find('.dateTo').datepicker('option', 'minDate', null).datepicker('setDate', to || null); + } else if (/>=/.test(range)) { + $cell.add( $shcell ) + .find('.dateFrom').datepicker('option', 'maxDate', null).datepicker('setDate', from || null).end() + .find('.dateTo').datepicker('option', 'minDate', from || null ); + } else { + $cell.add( $shcell ) + .find('.dateFrom').datepicker('option', 'maxDate', null).datepicker('setDate', from || null ).end() + .find('.dateTo').datepicker('option', 'minDate', null).datepicker('setDate', to || null); + } + + if (typeof o.oldonClose === 'function') { o.oldonClose(selectedDate, ui); } + }; + + o.defaultDate = o.from || ''; + $cell.find('.dateFrom').datepicker(o); + o.defaultDate = o.to || '+7d'; // set to date +7 days from today (if not defined) + $cell.find('.dateTo').datepicker(o); + + // update date compare from hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate' + c.namespace + 'filter', function() { + var val = $input.val() || '', + from = '', + to = ''; + // date range + if (/\s+-\s+/.test(val)) { + val = val.split(/\s+-\s+/) || []; + from = val[0] || ''; + to = val[1] || ''; + } else if (/>=/.test(val)) { + // greater than date (to date empty) + from = val.replace(/>=/, '') || ''; + } else if (/<=/.test(val)) { + // less than date (from date empty) + to = val.replace(/<=/, '') || ''; + } + + // differeniate 1388556000000 from 1/1/2014 using \d{5} regex + from = from !== '' ? /\d{5}/g.test(from) ? new Date(Number(from)) : from || '' : ''; + to = to !== '' ? /\d{5}/g.test(to) ? new Date(Number(to)) : to || '' : ''; + + $cell.add($shcell).find('.dateFrom').datepicker('setDate', from || null); + $cell.add($shcell).find('.dateTo').datepicker('setDate', to || null); + // give datepicker time to process + setTimeout(function() { + closeDate(); + ts.filter.formatterUpdated($cell, indx); + }, 0); + }); + + // has sticky headers? + c.$table.bind('stickyHeadersInit' + c.namespace + 'filter', function() { + $shcell = c.widgetOptions.$sticky.find('.tablesorter-filter-row').children().eq(indx).empty(); + $shcell.append(t); + + // add a jQuery datepicker! + o.defaultDate = o.from || ''; + $shcell.find('.dateFrom').datepicker(o); + + o.defaultDate = o.to || '+7d'; + $shcell.find('.dateTo').datepicker(o); + + }); + + // on reset + $cell.closest('table').bind('filterReset' + c.namespace + 'filter', function() { + $cell.add($shcell).find('.dateFrom').val('').datepicker('setDate', o.from || null ); + $cell.add($shcell).find('.dateTo').val('').datepicker('setDate', o.to || null ); + setTimeout(function() { + closeDate(); + }, 0); + }); + + // return the hidden input so the filter widget has a reference to it + return $input.val( o.from ? ( o.to ? o.from + ' - ' + o.to : '>=' + o.from ) : (o.to ? '<=' + o.to : '') ); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-select2.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-select2.js new file mode 100644 index 0000000..46bee72 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-formatter-select2.js @@ -0,0 +1,172 @@ +/*! Widget: filter, select2 formatter function - updated 12/1/2019 (v2.31.2) *//* + * requires: jQuery 1.7.2+, tableSorter (FORK) 2.16+, filter widget 2.16+ + and select2 v3.4.6+ plugin (this code is NOT compatible with select2 v4+) + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter || {}; + ts.filterFormatter = ts.filterFormatter || {}; + + /************************\ + Select2 Filter Formatter + \************************/ + ts.filterFormatter.select2 = function($cell, indx, select2Def) { + var o = $.extend({ + // select2 filter formatter options + cellText : '', // Text (wrapped in a label element) + match : true, // adds 'filter-match' to header + value : '', + // include ANY select2 options below + multiple : true, + width : '100%' + + }, select2Def ), + arry, data, + // add class to $cell since it may point to a removed DOM node + // after a "refreshWidgets"; see #1237 + c = $cell.addClass('select2col' + indx).closest('table')[0].config, + wo = c.widgetOptions, + // Add a hidden input to hold the range values + $input = $('<input class="filter" type="hidden">') + .appendTo($cell) + // hidden filter update namespace trigger by filter widget + .bind('change' + c.namespace + 'filter', function() { + var val = convertRegex(this.value); + c.$table.find('.select2col' + indx + ' .select2').select2('val', val); + updateSelect2(); + }), + $header = c.$headerIndexed[indx], + onlyAvail = $header.hasClass(wo.filter_onlyAvail), + matchPrefix = o.match ? '' : '^', + matchSuffix = o.match ? '' : '$', + flags = wo.filter_ignoreCase ? 'i' : '', + + convertRegex = function(val) { + // value = '/(^x$|^y$)/' => ['x','y'] + return val + .replace(/^\/\(\^?/, '') + .replace(/\$\|\^/g, '|') + .replace(/\$?\)\/i?$/g, '') + // unescape special regex characters + .replace(/\\/g, '') + .split('|'); + }, + + // this function updates the hidden input and adds the current values to the header cell text + updateSelect2 = function() { + var arry = false, + v = c.$table.find('.select2col' + indx + ' .select2').select2('val') || o.value || ''; + // convert array to string + if ($.isArray(v)) { + arry = true; + v = v.join('\u0000'); + } + // escape special regex characters (http://stackoverflow.com/a/9310752/145346) + var v_escape = v.replace(/[-[\]{}()*+?.,/\\^$|#]/g, '\\$&'); + // convert string back into an array + if (arry) { + v = v.split('\u0000'); + v_escape = v_escape.split('\u0000'); + } + if (!ts.isEmptyObject($cell.find('.select2').data())) { + $input + // add regex, so we filter exact numbers + .val( + $.isArray(v_escape) && v_escape.length && v_escape.join('') !== '' ? + '/(' + matchPrefix + (v_escape || []).join(matchSuffix + '|' + matchPrefix) + matchSuffix + ')/' + flags : + '' + ) + .trigger('search'); + $cell.find('.select2').select2('val', v); + // update sticky header cell + if (c.widgetOptions.$sticky) { + c.widgetOptions.$sticky.find('.select2col' + indx + ' .select2').select2('val', v); + } + } + }, + + // get options from table cell content or filter_selectSource (v2.16) + updateOptions = function() { + data = []; + arry = ts.filter.getOptionSource(c.$table[0], indx, onlyAvail) || []; + // build select2 data option + $.each(arry, function(i, v) { + // getOptionSource returns { parsed: "value", text: "value" } in v2.24.4 + data.push({ id: '' + v.parsed, text: v.text }); + }); + o.data = data; + }; + + // get filter-match class from option + $header.toggleClass('filter-match', o.match); + if (o.cellText) { + $cell.prepend('<label>' + o.cellText + '</label>'); + } + + // don't add default in table options if either ajax or + // data options are already defined + if (!(o.ajax && !$.isEmptyObject(o.ajax)) && !o.data) { + updateOptions(); + c.$table.bind('filterEnd', function() { + updateOptions(); + c.$table + .find('.select2col' + indx) + .add(c.widgetOptions.$sticky && c.widgetOptions.$sticky.find('.select2col' + indx)) + .find('.select2').select2(o); + }); + } + + // add a select2 hidden input! + $('<input class="select2 select2-' + indx + '" type="hidden" />') + .val(o.value) + .appendTo($cell) + .select2(o) + .bind('change', function() { + updateSelect2(); + }); + + // update select2 from filter hidden input, in case of saved filters + c.$table.bind('filterFomatterUpdate', function() { + // value = '/(^x$|^y$)/' => 'x,y' + var val = convertRegex(c.$table.data('lastSearch')[indx] || ''); + $cell = c.$table.find('.select2col' + indx); + $cell.find('.select2').select2('val', val); + updateSelect2(); + ts.filter.formatterUpdated($cell, indx); + }); + + // has sticky headers? + c.$table.bind('stickyHeadersInit', function() { + var $shcell = c.widgetOptions.$sticky.find('.select2col' + indx).empty(); + // add a select2! + $('<input class="select2 select2-' + indx + '" type="hidden">') + .val(o.value) + .appendTo($shcell) + .select2(o) + .bind('change', function() { + c.$table.find('.select2col' + indx) + .find('.select2') + .select2('val', c.widgetOptions.$sticky.find('.select2col' + indx + ' .select2').select2('val') ); + updateSelect2(); + }); + if (o.cellText) { + $shcell.prepend('<label>' + o.cellText + '</label>'); + } + }); + + // on reset + c.$table.bind('filterReset', function() { + c.$table.find('.select2col' + indx).find('.select2').select2('val', o.value || ''); + setTimeout(function() { + updateSelect2(); + }, 0); + }); + + updateSelect2(); + return $input; + }; + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-type-insideRange.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-type-insideRange.js new file mode 100644 index 0000000..f6152a5 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter-type-insideRange.js @@ -0,0 +1,42 @@ +/*! Widget: filter, insideRange filter type - updated 12/10/2015 (v2.25.0) */ +;(function($) { + 'use strict'; + + // Add insideRange filter type + // ============================ + // This allows you to enter a number (e.g. 8) and show the + // resulting rows that will have that query within it's range + // demo at http://mottie.github.io/tablesorter/docs/example-widget-filter-custom-search2.html + var ts = $.tablesorter, + isDigit = /\d+/, + range = /\s+-\s+/, + parseNumber = function(num) { + return isNaN(num) ? num : parseFloat(num); + }; + + ts.filter.types.insideRange = function( c, data ) { + // don't look for an inside range if "any" match is enabled... multiple "-" really screw things up + if ( !data.anyMatch && isDigit.test( data.iFilter ) && range.test( data.iExact ) ) { + var t, val, low, high, + index = data.index, + cell = data.$cells[ index ], + parts = data.iExact.split( range ), + format = c.parsers[data.index] && c.parsers[data.index].format; + // the cell does not contain a range or the parser isn't defined + if ( parts && parts.length < 2 || typeof format !== 'function' ) { + return null; + } + // format each side part of the range using the assigned parser + low = parseNumber( format( parts[0], c.table, cell, index ) ); + high = parseNumber( format( parts[1], c.table, cell, index ) ); + val = parseNumber( format( data.iFilter, c.table, cell, index ) ); + if ( high < low ) { + // swap high & low + t = high; high = low; low = t; + } + return low <= val && val <= high; + } + return null; + }; + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter.js new file mode 100644 index 0000000..c6953ac --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-filter.js @@ -0,0 +1,1958 @@ +/*! 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' ); + } + } + }); + + 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; + }, + // 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; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; + } + return null; + }, + // 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; + } + 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; + } + return null; + }, + // 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 ); + + tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; + range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; + range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || c.parsers[ index ].type === 'numeric' ) { + result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); + range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; + result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); + range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; + } + if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { + result = data.cache; + } else { + tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + result = ts.formatFloat( tmp, table ); + } + if ( range1 > range2 ) { + tmp = range1; range1 = range2; range2 = tmp; // swap + } + return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); + } + return null; + }, + // Look for wild card: ? = single, * = multiple, or | = logical OR + wild : function( c, data ) { + if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { + var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); + // look for an exact match with the 'or' unless the 'filter-match' class is found + if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { + query = data.isMatch ? query : '^(' + query + ')$'; + } + // parsing the filter may not work properly when using wildcards =/ + try { + return new RegExp( + query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), + c.widgetOptions.filter_ignoreCase ? 'i' : '' + ) + .test( data.exact ); + } catch ( error ) { + return null; + } + } + return null; + }, + // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) + fuzzy: function( c, data ) { + if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { + var indx, + patternIndx = 0, + len = data.iExact.length, + txt = data.iFilter.slice( 1 ), + pattern = tsf.parseFilter( c, txt, data ) || ''; + for ( indx = 0; indx < len; indx++ ) { + if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { + patternIndx += 1; + } + } + return patternIndx === pattern.length; + } + 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 : /</, + lteTest : /<=/, + notTest : /^\!/, + wildOrTest : /[\?\*\|]/, + wildTest : /\?\*/, + fuzzyTest : /^~/, + exactTest : /[=\"\|!]/ + }); + + // don't build filter row if columnFilters is false or all columns are set to 'filter-false' + // see issue #156 + val = c.$headers.filter( '.filter-false, .parser-false' ).length; + if ( wo.filter_columnFilters !== false && val !== c.$headers.length ) { + // build filter row + tsf.buildRow( table, c, wo ); + } + + txt = 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + + 'filterAndSortReset filterResetSaved filterEnd search '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table.bind( txt, function( event, filter ) { + val = wo.filter_hideEmpty && + $.isEmptyObject( c.cache ) && + !( c.delayInit && event.type === 'appendCache' ); + // hide filter row using the 'filtered' class name + c.$table.find( '.' + tscss.filterRow ).toggleClass( wo.filter_filteredRow, val ); // fixes #450 + if ( !/(search|filter)/.test( event.type ) ) { + event.stopPropagation(); + tsf.buildDefault( table, true ); + } + // 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 ); + } + return false; + }); + + // reset button/link + 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 ) { + 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 ( string in fxn ) { + if ( typeof string === 'string' ) { + options += options === '' ? + '<option value="">' + + ( $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || + '' + ) + + '</option>' : ''; + val = string; + txt = string; + if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + val = string.split( wo.filter_selectSourceSeparator ); + txt = val[1]; + val = val[0]; + } + options += '<option ' + + ( txt === val ? '' : 'data-function-name="' + string + '" ' ) + + 'value="' + val + '">' + txt + '</option>'; + } + } + 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. + tsf.buildDefault( table, true ); + + tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); + if ( wo.filter_external ) { + tsf.bindSearch( table, wo.filter_external ); + } + + if ( wo.filter_hideFilters ) { + tsf.hideFilters( c ); + } + + // show processing icon + 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 : '' ); + }); + } + + // 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; + } + }, + 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 = '<tr role="search" class="' + tscss.filterRow + ' ' + c.cssIgnoreRow + '">'; + 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 += '<td data-column="' + column + '-' + ( column + tmp - 1 ) + '" colspan="' + tmp + '"'; + } else { + buildFilter += '<td data-column="' + column + '"'; + } + if ( arry ) { + buildFilter += ( cellFilter[ column ] ? ' class="' + cellFilter[ column ] + '"' : '' ); + } else { + buildFilter += ( cellFilter !== '' ? ' class="' + cellFilter + '"' : '' ); + } + buildFilter += '></td>'; + } + } + c.$filters = $( buildFilter += '</tr>' ) + .appendTo( c.$table.children( 'thead' ).eq( 0 ) ) + .children( 'td' ); + // build each filter input + for ( column = 0; column < columns; column++ ) { + disabled = false; + // assuming last cell of a column is the main column + $header = c.$headerIndexed[ column ]; + if ( $header && $header.length ) { + // $filter = c.$filters.filter( '[data-column="' + column + '"]' ); + $filter = tsf.getColumnElm( c, c.$filters, column ); + ffxn = ts.getColumnData( table, wo.filter_functions, column ); + makeSelect = ( wo.filter_functions && ffxn && typeof ffxn !== 'function' ) || + $header.hasClass( 'filter-select' ); + // get data from jQuery data, metadata, headers option or header class name + col = ts.getColumnData( table, c.headers, column ); + disabled = ts.getData( $header[0], col, 'filter' ) === 'false' || + ts.getData( $header[0], col, 'parser' ) === 'false'; + + if ( makeSelect ) { + buildFilter = $( '<select>' ).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 = $( '<input type="search">' ).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( '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 ); + } + }); + }, + 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; + } + } + 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; + } + // add filter array back into inputs + if ( filterArray ) { + ts.setFilters( table, filters, false, skipFirst !== true ); + if ( !wo.filter_initialized ) { + c.lastSearch = []; + c.lastCombinedFilter = ''; + } + } + 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. <th class="filter-select filter-parsed"> ) + 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( '</option>' ) >= 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 = '<option value="">' + + ( node.data( 'placeholder' ) || + node.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || '' + ) + '</option>', + // 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 += $( '<option>', option )[ 0 ].outerHTML; + options += '<option'; + for ( val in option ) { + if ( option.hasOwnProperty( val ) && val !== 'text' ) { + options += ' ' + val + '="' + option[ val ].replace( tsfRegex.quote, '"' ) + '"'; + } + } + if ( !option.value ) { + options += ' value="' + option.text.replace( tsfRegex.quote, '"' ) + '"'; + } + options += '>' + option.text.replace( tsfRegex.quote, '"' ) + '</option>'; + // above code is needed in jQuery < v1.8 + + // make sure we don't turn an object into a string (objects without a "text" property) + } else if ( '' + option !== '[object Object]' ) { + txt = option = ( '' + option ).replace( tsfRegex.quote, '"' ); + val = txt; + // allow including a symbol in the selectSource array + // 'a-z|A through Z' so that 'a-z' becomes the option value + // and 'A through Z' becomes the option text + if ( txt.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + t = txt.split( wo.filter_selectSourceSeparator ); + val = t[0]; + txt = t[1]; + } + // replace quotes - fixes #242 & ignore empty strings + // see http://stackoverflow.com/q/14990971/145346 + options += option !== '' ? + '<option ' + + ( val === txt ? '' : 'data-function-name="' + option + '" ' ) + + 'value="' + val + '">' + txt + + '</option>' : ''; + } + } + // clear arry so it doesn't get appended twice + arry = []; + } + + // update all selects in the same column ( clone thead in sticky headers & + // any external selects ) - fixes 473 + $filters = ( c.$filters ? c.$filters : c.$table.children( 'thead' ) ) + .find( '.' + tscss.filter ); + if ( wo.filter_$externalFilters ) { + $filters = $filters && $filters.length ? + $filters.add( wo.filter_$externalFilters ) : + wo.filter_$externalFilters; + } + $filter = $filters.filter( 'select[data-column="' + column + '"]' ); + + // make sure there is a select there! + if ( $filter.length ) { + $filter[ updating ? 'html' : 'append' ]( options ); + if ( !$.isArray( arry ) ) { + // append options if arry is provided externally as a string or jQuery object + // options ( default value ) was already added + $filter.append( arry ).val( currentValue ); + } + $filter.val( currentValue ); + } + }, + buildDefault: function( table, updating ) { + var columnIndex, $header, noSelect, + c = table.config, + wo = c.widgetOptions, + columns = c.columns; + // build default select dropdown + for ( columnIndex = 0; columnIndex < columns; columnIndex++ ) { + $header = c.$headerIndexed[columnIndex]; + noSelect = !( $header.hasClass( 'filter-false' ) || $header.hasClass( 'parser-false' ) ); + // look for the filter-select class; build/update it if found + if ( ( $header.hasClass( 'filter-select' ) || + ts.getColumnData( table, wo.filter_functions, columnIndex ) === true ) && noSelect ) { + tsf.buildSelect( table, columnIndex, '', updating, $header.hasClass( wo.filter_onlyAvail ) ); + } + } + } + }; + + // filter regex variable + tsfRegex = tsf.regex; + + ts.getFilters = function( table, getRaw, setFilters, skipFirst ) { + var i, $filters, $column, cols, + filters = [], + c = table ? $( table )[0].config : '', + wo = c ? c.widgetOptions : ''; + if ( ( getRaw !== true && wo && !wo.filter_columnFilters ) || + // setFilters called, but last search is exactly the same as the current + // fixes issue #733 & #903 where calling update causes the input values to reset + ( $.isArray(setFilters) && tsf.equalFilters(c, setFilters, c.lastSearch) ) + ) { + return $( table ).data( 'lastSearch' ) || []; + } + if ( c ) { + if ( c.$filters ) { + $filters = c.$filters.find( '.' + tscss.filter ); + } + if ( wo.filter_$externalFilters ) { + $filters = $filters && $filters.length ? + $filters.add( wo.filter_$externalFilters ) : + wo.filter_$externalFilters; + } + if ( $filters && $filters.length ) { + filters = setFilters || []; + for ( i = 0; i < c.columns + 1; i++ ) { + cols = ( i === c.columns ? + // 'all' columns can now include a range or set of columms ( data-column='0-2,4,6-7' ) + wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector : + '[data-column="' + i + '"]' ); + $column = $filters.filter( cols ); + if ( $column.length ) { + // move the latest search to the first slot in the array + $column = tsf.getLatestSearch( $column ); + if ( $.isArray( setFilters ) ) { + // skip first ( latest input ) to maintain cursor position while typing + if ( skipFirst && $column.length > 1 ) { + $column = $column.slice( 1 ); + } + if ( i === c.columns ) { + // prevent data-column='all' from filling data-column='0,1' ( etc ) + cols = $column.filter( wo.filter_anyColumnSelector ); + $column = cols.length ? cols : $column; + } + $column + .val( setFilters[ i ] ) + // must include a namespace here; but not c.namespace + 'filter'? + .trigger( 'change' + c.namespace ); + } else { + filters[i] = $column.val() || ''; + // don't change the first... it will move the cursor + if ( i === c.columns ) { + // don't update range columns from 'all' setting + $column + .slice( 1 ) + .filter( '[data-column*="' + $column.attr( 'data-column' ) + '"]' ) + .val( filters[ i ] ); + } else { + $column + .slice( 1 ) + .val( filters[ i ] ); + } + } + // save any match input dynamically + if ( i === c.columns && $column.length ) { + wo.filter_$anyMatch = $column; + } + } + } + } + } + return filters; + }; + + ts.setFilters = function( table, filter, apply, skipFirst ) { + var c = table ? $( table )[0].config : '', + valid = ts.getFilters( table, true, filter, skipFirst ); + // default apply to "true" + if ( typeof apply === 'undefined' ) { + apply = true; + } + if ( c && apply ) { + // ensure new set filters are applied, even if the search is the same + c.lastCombinedFilter = null; + c.lastSearch = []; + tsf.searching( c.table, filter, skipFirst ); + c.$table.triggerHandler( 'filterFomatterUpdate' ); + } + return valid.length !== 0; + }; + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-formatter.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-formatter.js new file mode 100644 index 0000000..d7b224a --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-formatter.js @@ -0,0 +1,72 @@ +/*! Widget: formatter - 2/9/2015 (v2.19.1) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + var ts = $.tablesorter; + + ts.formatter = { + init : function( c ) { + var events = c.widgetOptions.formatter_event + + ' pagerComplete updateComplete '.split(' ').join('.tsformatter '); + c.$table + .off( events.replace(/\s+/g, ' ') ) + .on( events, function() { + ts.formatter.setup( c ); + }); + ts.formatter.setup( c ); + }, + setup : function( c ) { + // do nothing for empty tables + if ( $.isEmptyObject( c.cache ) ) { return; } + var $tbody, tbodyIndex, rowIndex, rows, cell, len, column, + wo = c.widgetOptions, + data = { config: c, wo: wo }, + formatter = [], + $headers = []; + // set up variables + for ( column = 0; column < c.columns; column++ ) { + $headers[ column ] = c.$headerIndexed[ column ]; + formatter[ column ] = ts.getColumnData( c.table, wo.formatter_column, column ) || false; + } + // main loop + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( c.table, c.$tbodies.eq( tbodyIndex ), true ); // detach tbody + rows = c.cache[ tbodyIndex ]; + len = rows.normalized.length; + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + data.$row = rows.normalized[ rowIndex ][ c.columns ].$row; + data.$cells = data.$row.children( 'th, td' ); + for ( column = 0; column < c.columns; column++ ) { + if ( formatter[ column ] ) { + data.columnIndex = column; + data.$header = $headers[ column ]; + data.$cell = data.$cells.eq( column ); + cell = data.$cell[0]; + // get text from attribute first, just in case we're updating + data.text = cell.getAttribute( c.textAttribute ) || cell.textContent || data.$cell.text(); + cell.innerHTML = formatter[ column ]( data.text, data ); + } + } + } + ts.processTbody( c.table, $tbody, false); // restore tbody + } + } + }; + + ts.addWidget({ + id: 'formatter', + priority: 100, + options: { + formatter_column : {}, + formatter_event : 'applyFormatter' + }, + init: function( table ) { + ts.formatter.init( table.config ); + } + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-grouping.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-grouping.js new file mode 100644 index 0000000..1791419 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-grouping.js @@ -0,0 +1,361 @@ +/*! Widget: grouping - updated 9/27/2017 (v2.29.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + var ts = $.tablesorter, + + tsg = ts.grouping = { + + types : { + number : function(c, $column, txt, num) { + var result, + ascSort = $column.hasClass( ts.css.sortAsc ); + if ( num > 1 && txt !== '' ) { + if ( ascSort ) { + result = Math.floor( parseFloat( txt ) / num ) * num; + } else { + result = Math.ceil( parseFloat( txt ) / num ) * num; + } + // show range + result += ' - ' + ( result + ( num - 1 ) * ( ascSort ? 1 : -1 ) ); + } else { + result = parseFloat( txt ) || txt; + } + return result; + }, + separator : function(c, $column, txt, num) { + var word = (txt + '').split(c.widgetOptions.group_separator); + // return $.trim(word && num > 0 && word.length >= num ? word[(num || 1) - 1] : ''); + return $.trim( word[ num - 1 ] || '' ); + }, + text : function( c, $column, txt ) { + return txt; + }, + word : function(c, $column, txt, num) { + var word = (txt + ' ').match(/\w+/g) || []; + // return word && word.length >= num ? word[num - 1] : txt || ''; + return word[ num - 1 ] || ''; + }, + letter : function(c, $column, txt, num) { + return txt ? (txt + ' ').substring(0, num) : ''; + }, + date : function(c, $column, txt, part) { + var year, month, + wo = c.widgetOptions, + time = new Date(txt || ''); + // check for valid date + if ( time instanceof Date && isFinite( time ) ) { + year = time.getFullYear(); + month = tsg.findMonth( wo, time.getMonth() ); + return part === 'year' ? year : + part === 'month' ? month : + part === 'monthyear' ? month + ' ' + year : + part === 'day' ? month + ' ' + time.getDate() : + part === 'week' ? tsg.findWeek( wo, time.getDay() ) : + part === 'time' ? tsg.findTime( wo, time ) : + part === 'hour' ? tsg.findTime( wo, time, 'hour' ) : + wo.group_dateString( time, c, $column ); + } else { + return wo.group_dateInvalid; + } + } + }, + + // group date type functions to allow using this widget with Globalize + findMonth : function( wo, month ) { + // CLDR returns an object { 1: "Jan", 2: "Feb", 3: "Mar", ..., 12: "Dec" } + return wo.group_months[ month + ( ( wo.group_months[0] || '' ) === '' ? 1 : 0 ) ]; + }, + findWeek : function( wo, day ) { + if ( $.isArray( wo.group_week ) ) { + return wo.group_week[ day ]; + } else if ( !$.isEmptyObject( wo.group_week ) ) { + // CLDR returns { sun: "Sun", mon: "Mon", tue: "Tue", wed: "Wed", thu: "Thu", ... } + var cldrWeek = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; + return wo.group_week[ cldrWeek[ day ] ]; + } + }, + findTime : function( wo, time, part ) { + var suffix, + // CLDR returns { am: "AM", pm: "PM", ... } + isObj = wo.group_time.am && wo.group_time.pm, + h = time.getHours(), + period = h >= 12 ? 1 : 0, + p24 = wo.group_time24Hour && h > 12 ? h - 12 : + wo.group_time24Hour && h === 0 ? h + 12 : h, + hours = ( '00' + p24 ).slice(-2), + min = ( '00' + time.getMinutes() ).slice(-2); + suffix = wo.group_time[ isObj ? [ 'am', 'pm' ][ period ] : period ]; + if ( part === 'hour' ) { + return hours; + } + return hours + ':' + min + ( wo.group_time24Hour ? '' : ' ' + ( suffix || '' ) ); + }, + + update : function(table) { + if ($.isEmptyObject(table.config.cache)) { return; } + var c = table.config, + wo = c.widgetOptions, + hasSort = typeof c.sortList[0] !== 'undefined', + data = {}, + column = $.isArray( wo.group_forceColumn ) && typeof wo.group_forceColumn[0] !== 'undefined' ? + ( wo.group_enforceSort && !hasSort ? -1 : wo.group_forceColumn[0] ) : + ( hasSort ? c.sortList[0][0] : -1 ); + c.$table + .find('tr.group-hidden').removeClass('group-hidden').end() + .find('tr.group-header').remove(); + if (wo.group_collapsible) { + // clear pager saved spacer height (in case the rows are collapsed) + c.$table.data('pagerSavedHeight', 0); + } + if (column >= 0 && column < c.columns && !c.$headerIndexed[column].hasClass('group-false')) { + wo.group_collapsedGroup = ''; // save current groups + wo.group_collapsedGroups = {}; + + data.column = column; + // group class finds 'group-{word/separator/letter/number/date/false}-{optional:#/year/month/day/week/time}' + data.groupClass = (c.$headerIndexed[column].attr('class') || '').match(/(group-\w+(-\w+)?)/g); + // grouping = [ 'group', '{word/separator/letter/number/date/false}', '{#/year/month/day/week/time}' ] + data.grouping = data.groupClass ? data.groupClass[0].split('-') : [ 'group', 'letter', 1 ]; // default to letter 1 + + // save current grouping + data.savedGroup = tsg.saveCurrentGrouping( c, wo, data ); + + // find column groups + tsg.findColumnGroups( c, wo, data ); + tsg.processHeaders( c, wo, data ); + + c.$table.triggerHandler(wo.group_complete); + } + }, + + processHeaders : function( c, wo, data ) { + var index, isHidden, $label, name, $rows, $row, + $headers = c.$table.find( 'tr.group-header' ), + len = $headers.length; + + $headers.bind( 'selectstart', false ); + for ( index = 0; index < len; index++ ) { + $row = $headers.eq( index ); + $rows = $row.nextUntil( 'tr.group-header' ).filter( ':visible' ); + + // add group count (only visible rows!) + if ( wo.group_count || $.isFunction( wo.group_callback ) ) { + $label = $row.find( '.group-count' ); + if ( $label.length ) { + if ( wo.group_count ) { + $label.html( wo.group_count.toString().replace( /\{num\}/g, $rows.length ) ); + } + if ( $.isFunction( wo.group_callback ) ) { + wo.group_callback( $row.find( 'td' ), $rows, data.column, c.table ); + } + } + } + // save collapsed groups + if ( wo.group_saveGroups && + !$.isEmptyObject( wo.group_collapsedGroups ) && + wo.group_collapsedGroups[ wo.group_collapsedGroup ].length ) { + name = $row.find( '.group-name' ).text().toLowerCase() + $row.attr( 'data-group-index' ); + isHidden = $.inArray( name, wo.group_collapsedGroups[ wo.group_collapsedGroup ] ) > -1; + $row.toggleClass( 'collapsed', isHidden ); + $rows.toggleClass( 'group-hidden', isHidden ); + } else if ( wo.group_collapsed && wo.group_collapsible ) { + $row.addClass( 'collapsed' ); + $rows.addClass( 'group-hidden' ); + } + } + }, + + groupHeaderHTML : function( c, wo, data ) { + var name = ( data.currentGroup || '' ).toString().replace(/</g, '<').replace(/>/g, '>'); + return '<tr class="group-header ' + c.selectorRemove.slice(1) + ' ' + + // prevent grouping row from being hidden by the columnSelector; + // classHasSpan option added 2.29.0 + ( wo.columnSelector_classHasSpan || 'hasSpan' ) + + '" unselectable="on" ' + ( c.tabIndex ? 'tabindex="0" ' : '' ) + 'data-group-index="' + + data.groupIndex + '">' + + '<td colspan="' + c.columns + '">' + + ( wo.group_collapsible ? '<i/>' : '' ) + + '<span class="group-name">' + name + '</span>' + + '<span class="group-count"></span>' + + '</td></tr>'; + }, + saveCurrentGrouping : function( c, wo, data ) { + // save current grouping + var saveName, direction, + savedGroup = false; + if (wo.group_collapsible && wo.group_saveGroups) { + wo.group_collapsedGroups = ts.storage && ts.storage( c.table, 'tablesorter-groups' ) || {}; + // include direction when saving groups (reversed numbers shows different range values) + direction = 'dir' + c.sortList[0][1]; + // combine column, sort direction & grouping as save key + saveName = wo.group_collapsedGroup = '' + c.sortList[0][0] + direction + data.grouping.join(''); + if (!wo.group_collapsedGroups[saveName]) { + wo.group_collapsedGroups[saveName] = []; + } else { + savedGroup = true; + } + } + return savedGroup; + }, + findColumnGroups : function( c, wo, data ) { + var tbodyIndex, norm_rows, rowIndex, end, undef, + hasPager = ts.hasWidget( c.table, 'pager' ), + p = c.pager || {}; + data.groupIndex = 0; + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + norm_rows = c.cache[ tbodyIndex ].normalized; + data.group = undef; // clear grouping across tbodies + rowIndex = hasPager && !p.ajax ? p.startRow - 1 : 0; + end = hasPager ? p.endRow - ( p.ajax ? p.startRow : 0 ) : norm_rows.length; + for ( ; rowIndex < end; rowIndex++ ) { + data.rowData = norm_rows[ rowIndex ]; + // fixes #1232 - ajax issue; if endRow > norm_rows.length (after filtering), then data.rowData is undefined + if (data.rowData) { + data.$row = data.rowData[ c.columns ].$row; + // fixes #438 + if ( data.$row.is( ':visible' ) && tsg.types[ data.grouping[ 1 ] ] ) { + tsg.insertGroupHeader( c, wo, data ); + } + } + } + } + if ( ts.hasWidget( c.table, 'columnSelector' ) ) { + // make sure to handle the colspan adjustments of the grouping rows + ts.columnSelector.setUpColspan( c, wo ); + } + }, + insertGroupHeader: function( c, wo, data ) { + var $header = c.$headerIndexed[ data.column ], + txt = data.rowData[ data.column ], + num = /date/.test( data.groupClass ) ? data.grouping[ 2 ] : parseInt( data.grouping[ 2 ] || 1, 10 ) || 1; + data.currentGroup = data.rowData ? + tsg.types[ data.grouping[ 1 ] ]( c, $header, txt, num, data.group ) : + data.currentGroup; + if ( data.group !== data.currentGroup ) { + data.group = data.currentGroup; + if ( $.isFunction( wo.group_formatter ) ) { + data.currentGroup = wo.group_formatter( ( data.group || '' ).toString(), data.column, c.table, c, wo, data ) || data.group; + } + // add first() for grouping with childRows + data.$row.first().before( tsg.groupHeaderHTML( c, wo, data ) ); + if ( wo.group_saveGroups && !data.savedGroup && wo.group_collapsed && wo.group_collapsible ) { + // all groups start collapsed; data.groupIndex is 1 more than the expected index. + wo.group_collapsedGroups[ wo.group_collapsedGroup ].push( data.currentGroup + data.groupIndex ); + } + data.groupIndex++; + } + }, + + bindEvents : function(table, c, wo) { + if (wo.group_collapsible) { + wo.group_collapsedGroups = []; + // .on() requires jQuery 1.7+ + c.$table.on('click toggleGroup keyup', 'tr.group-header', function(event) { + event.stopPropagation(); + // pressing enter will toggle the group + if (event.type === 'keyup' && event.which !== 13) { return; } + var isCollapsed, indx, + $this = $(this), + name = $this.find('.group-name').text().toLowerCase() + $this.attr('data-group-index'); + // use shift-click to toggle ALL groups + if (event.shiftKey && (event.type === 'click' || event.type === 'keyup')) { + $this.siblings('.group-header').trigger('toggleGroup'); + } + $this.toggleClass('collapsed'); + // nextUntil requires jQuery 1.4+ + $this.nextUntil('tr.group-header').toggleClass('group-hidden', $this.hasClass('collapsed') ); + isCollapsed = $this.hasClass('collapsed'); + // reapply zebra widget after opening collapsed group - see #1156 + if (!isCollapsed && ts.hasWidget(c.$table, 'zebra')) { + ts.applyWidgetId(c.$table, 'zebra'); + } + // save collapsed groups + if (wo.group_saveGroups && ts.storage) { + if (!wo.group_collapsedGroups[wo.group_collapsedGroup]) { + wo.group_collapsedGroups[wo.group_collapsedGroup] = []; + } + if (isCollapsed && wo.group_collapsedGroup) { + wo.group_collapsedGroups[wo.group_collapsedGroup].push( name ); + } else if (wo.group_collapsedGroup) { + indx = $.inArray( name, wo.group_collapsedGroups[wo.group_collapsedGroup] ); + if (indx > -1) { + wo.group_collapsedGroups[wo.group_collapsedGroup].splice( indx, 1 ); + } + } + ts.storage( table, 'tablesorter-groups', wo.group_collapsedGroups ); + } + }); + } + $(wo.group_saveReset).on('click', function() { + tsg.clearSavedGroups(table); + }); + c.$table.on('pagerChange.tsgrouping', function() { + tsg.update(table); + }); + }, + + clearSavedGroups: function(table) { + if (table && ts.storage) { + ts.storage(table, 'tablesorter-groups', ''); + tsg.update(table); + } + } + + }; + + ts.addWidget({ + id: 'group', + priority: 100, + options: { + group_collapsible : true, // make the group header clickable and collapse the rows below it. + group_collapsed : false, // start with all groups collapsed + group_saveGroups : true, // remember collapsed groups + group_saveReset : null, // element to clear saved collapsed groups + group_count : ' ({num})', // if not false, the '{num}' string is replaced with the number of rows in the group + group_separator : '-', // group name separator; used when group-separator-# class is used. + group_formatter : null, // function(txt, column, table, c, wo) { return txt; } + group_callback : null, // function($cell, $rows, column, table) {}, callback allowing modification of the group header labels + group_complete : 'groupingComplete', // event triggered on the table when the grouping widget has finished work + + // apply the grouping widget only to selected column + group_forceColumn : [], // only the first value is used; set as an array for future expansion + group_enforceSort : true, // only apply group_forceColumn when a sort is applied to the table + + // checkbox parser text used for checked/unchecked values + group_checkbox : [ 'checked', 'unchecked' ], + // change these default date names based on your language preferences + group_months : [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ], + group_week : [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ], + group_time : [ 'AM', 'PM' ], + + // use 12 vs 24 hour time + group_time24Hour : false, + // group header text added for invalid dates + group_dateInvalid : 'Invalid Date', + + // this function is used when 'group-date' is set to create the date string + // you can just return date, date.toLocaleString(), date.toLocaleDateString() or d.toLocaleTimeString() + // reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#Conversion_getter + group_dateString : function(date) { return date.toLocaleString(); } + }, + init: function(table, thisWidget, c, wo) { + tsg.bindEvents(table, c, wo); + }, + format: function(table) { + tsg.update(table); + }, + remove : function(table, c) { + c.$table + .off('click', 'tr.group-header') + .off('pagerChange.tsgrouping') + .find('.group-hidden').removeClass('group-hidden').end() + .find('tr.group-header').remove(); + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-headerTitles.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-headerTitles.js new file mode 100644 index 0000000..fabe370 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-headerTitles.js @@ -0,0 +1,91 @@ +/*! Widget: headerTitles - updated 11/10/2015 (v2.24.4) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + var ts = $.tablesorter; + + ts.addWidget({ + id: 'headerTitles', + options: { + // use aria-label text + // e.g. 'First Name: Ascending sort applied, activate to apply a descending sort' + headerTitle_useAria : false, + // add tooltip class + headerTitle_tooltip : '', + // custom titles [ ascending, descending, unsorted ] + headerTitle_cur_text : [ ' sort: A - Z', ' sort: Z - A', 'ly unsorted' ], + headerTitle_cur_numeric : [ ' sort: 0 - 9', ' sort: 9 - 0', 'ly unsorted' ], + headerTitle_nxt_text : [ ' sort: A - Z', ' sort: Z - A', 'remove sort' ], + headerTitle_nxt_numeric : [ ' sort: 0 - 9', ' sort: 9 - 0', 'remove sort' ], + + // title display; {prefix} adds above prefix + // {type} adds the current sort order from above (text or numeric) + // {next} adds the next sort direction using the sort order above + headerTitle_output_sorted : 'current{current}; activate to {next}', + headerTitle_output_unsorted : 'current{current}; activate to {next} ', + headerTitle_output_nosort : 'No sort available', + // use this type to override the parser detection result + // e.g. use for numerically parsed columns (e.g. dates), but you + // want the user to see a text sort, e.g. [ 'text', 'numeric' ] + headerTitle_type : [], + // manipulate the title as desired + headerTitle_callback : null // function($cell, txt) { return txt; } + }, + init: function(table, thisWidget, c, wo) { + // force refresh + c.$table.on('refreshHeaderTitle', function() { + thisWidget.format(table, c, wo); + }); + // add tooltip class + if ($.isArray(wo.headerTitle_tooltip)) { + c.$headers.each(function() { + $(this).addClass( wo.headerTitle_tooltip[this.column] || '' ); + }); + } else if (wo.headerTitle_tooltip !== '') { + c.$headers.addClass( wo.headerTitle_tooltip ); + } + }, + format: function (table, c, wo) { + var txt; + c.$headers.each(function() { + var $this = $(this), + col = parseInt( $this.attr( 'data-column' ), 10 ), + sortType = wo.headerTitle_type[ col ] || c.parsers[ col ].type || 'text', + sortDirection = $this.hasClass(ts.css.sortAsc) ? 0 : $this.hasClass(ts.css.sortDesc) ? 1 : 2, + sortNext = c.sortVars[ col ].order[ ( c.sortVars[ col ].count + 1 ) % ( c.sortReset ? 3 : 2 ) ]; + if (wo.headerTitle_useAria) { + txt = $this.attr('aria-label') || wo.headerTitle_output_nosort || ''; + } else { + txt = (wo.headerTitle_prefix || '') + // now deprecated + ($this.hasClass('sorter-false') ? wo.headerTitle_output_nosort : + ts.isValueInArray( col, c.sortList ) >= 0 ? wo.headerTitle_output_sorted : wo.headerTitle_output_unsorted); + txt = txt.replace(/\{(current|next|name)\}/gi, function(m) { + return { + '{name}' : $this.text(), + '{current}' : wo[ 'headerTitle_cur_' + sortType ][ sortDirection ] || '', + '{next}' : wo[ 'headerTitle_nxt_' + sortType ][ sortNext ] || '' + }[m.toLowerCase()]; + }); + } + $this.attr('title', $.isFunction(wo.headerTitle_callback) ? wo.headerTitle_callback($this, txt) : txt); + }); + }, + remove: function (table, c, wo) { + c.$headers.attr('title', ''); + c.$table.off('refreshHeaderTitle'); + // remove tooltip class + if ($.isArray(wo.headerTitle_tooltip)) { + c.$headers.each(function() { + $(this).removeClass( wo.headerTitle_tooltip[this.column] || '' ); + }); + } else if (wo.headerTitle_tooltip !== '') { + c.$headers.removeClass( wo.headerTitle_tooltip ); + } + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-lazyload.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-lazyload.js new file mode 100644 index 0000000..ba83d24 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-lazyload.js @@ -0,0 +1,381 @@ +/*! Widget: lazyload (BETA) - 4/1/2016 (v2.25.7) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +;( function( $, window ) { + 'use strict'; + var ts = $.tablesorter; + + ts.lazyload = { + init : function( c, wo ) { + if ( wo.lazyload_event === 'scrollstop' && !ts.addScrollStopDone ) { + ts.addScrollStop(); + ts.addScrollStopDone = true; + $.event.special.scrollstop.latency = wo.lazyload_latency || 250; + } + ts.lazyload.update( c, wo ); + var namespace = c.namespace + 'lazyload ', + events = [ + wo.lazyload_update, + 'pagerUpdate', + wo.columnSelector_updated || 'columnUpdate', + '' + ].join( namespace ); + c.$table + .on( events, function() { + ts.lazyload.update( c, c.widgetOptions ); + }) + .on( 'filterEnd' + namespace, function() { + // give lazyload a nudge after filtering the table. Fixes #1169 + $(window).scroll(); + }); + }, + update : function( c, wo ) { + // add '.' if not already included + var sel = ( /(\.|#)/.test( wo.lazyload_imageClass ) ? '' : '.' ) + wo.lazyload_imageClass; + c.$table.find( sel ).lazyload({ + threshold : wo.lazyload_threshold, + failure_limit : wo.lazyload_failure_limit, + event : wo.lazyload_event, + effect : wo.lazyload_effect, + container : wo.lazyload_container, + data_attribute : wo.lazyload_data_attribute, + skip_invisible : wo.lazyload_skip_invisible, + appear : wo.lazyload_appear, + load : wo.lazyload_load, + placeholder : wo.lazyload_placeholder + }); + // give lazyload a nudge after updating the table. Fixes #1169 + setTimeout(function() { + $(window).scroll(); + }, 1); + }, + remove : function( c ) { + c.$table.off( c.namespace + 'lazyload' ); + } + }; + + ts.addWidget({ + id: 'lazyload', + options: { + // widget options + lazyload_imageClass : 'lazy', + lazyload_update : 'lazyloadUpdate', + // scrollStop option (https://github.com/ssorallen/jquery-scrollstop) + lazyload_latency : 250, + // lazyload options (see http://www.appelsiini.net/projects/lazyload) + lazyload_threshold : 0, + lazyload_failure_limit : 0, + lazyload_event : 'scrollstop', + lazyload_effect : 'show', + lazyload_container : window, + lazyload_data_attribute : 'original', + lazyload_skip_invisible : true, + lazyload_appear : null, + lazyload_load : null, + lazyload_placeholder : 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + }, + init: function( table, thisWidget, c, wo ) { + ts.lazyload.init( c, wo ); + }, + remove : function( table, c, wo ) { + ts.lazyload.remove( c, wo ); + } + }); + + // jscs:disable + ts.addScrollStop = function() { + // jQuery Scrollstop Plugin v1.2.0 + // https://github.com/ssorallen/jquery-scrollstop + /* + (function (factory) { + // UMD[2] wrapper for jQuery plugins to work in AMD or in CommonJS. + // + // [2] https://github.com/umdjs/umd + + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof exports === 'object') { + // Node/CommonJS + module.exports = factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } + }(function ($) { */ + // $.event.dispatch was undocumented and was deprecated in jQuery 1.7[1]. It + // was replaced by $.event.handle in jQuery 1.9. + // + // Use the first of the available functions to support jQuery <1.8. + // + // [1] https://github.com/jquery/jquery-migrate/blob/master/src/event.js#L25 + var dispatch = $.event.dispatch || $.event.handle; + + var special = $.event.special, + uid1 = 'D' + (+new Date()), + uid2 = 'D' + (+new Date() + 1); + + special.scrollstart = { + setup: function(data) { + var _data = $.extend({ + latency: special.scrollstop.latency + }, data); + + var timer, + handler = function(evt) { + var _self = this, + _args = arguments; + if (timer) { + clearTimeout(timer); + } else { + evt.type = 'scrollstart'; + dispatch.apply(_self, _args); + } + timer = setTimeout(function() { + timer = null; + }, _data.latency); + }; + $(this).bind('scroll', handler).data(uid1, handler); + }, + teardown: function() { + $(this).unbind('scroll', $(this).data(uid1)); + } + }; + special.scrollstop = { + latency: 250, + setup: function(data) { + var _data = $.extend({ + latency: special.scrollstop.latency + }, data); + var timer, + handler = function(evt) { + var _self = this, + _args = arguments; + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(function() { + timer = null; + evt.type = 'scrollstop'; + dispatch.apply(_self, _args); + }, _data.latency); + }; + $(this).bind('scroll', handler).data(uid2, handler); + }, + teardown: function() { + $(this).unbind('scroll', $(this).data(uid2)); + } + }; + /* + })); + */ + + }; + // jscs:enable + +})( jQuery, window ); + +// jscs:disable +/*! +* Lazy Load - jQuery plugin for lazy loading images +* +* Copyright (c) 2007-2015 Mika Tuupola +* +* Licensed under the MIT license: +* http://www.opensource.org/licenses/mit-license.php +* +* Project home: +* http://www.appelsiini.net/projects/lazyload +* +* Version: 1.9.7 +* +*/ +(function($, window, document, undefined) { + var $window = $(window); + $.fn.lazyload = function(options) { + var elements = this; + var $container; + var settings = { + threshold : 0, + failure_limit : 0, + event : 'scroll', + effect : 'show', + container : window, + data_attribute : 'original', + skip_invisible : false, + appear : null, + load : null, + placeholder : 'data:image/gif;base64,R0lGODlhAQABAIABAP///wAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==' + }; + function update() { + var counter = 0; + elements.each(function() { + var $this = $(this); + if (settings.skip_invisible && !$this.is(':visible')) { + return; + } + if ($.abovethetop(this, settings) || $.leftofbegin(this, settings)) { + /* Nothing. */ + } else if (!$.belowthefold(this, settings) && !$.rightoffold(this, settings)) { + $this.trigger('appear'); + /* if we found an image we'll load, reset the counter */ + counter = 0; + } else { + if (++counter > settings.failure_limit) { + return false; + } + } + }); + } + if (options) { + /* Maintain BC for a couple of versions. */ + if (undefined !== options.failurelimit) { + options.failure_limit = options.failurelimit; + delete options.failurelimit; + } + if (undefined !== options.effectspeed) { + options.effect_speed = options.effectspeed; + delete options.effectspeed; + } + $.extend(settings, options); + } + /* Cache container as jQuery as object. */ + $container = (settings.container === undefined || + settings.container === window) ? $window : $(settings.container); + /* Fire one scroll event per scroll. Not one scroll event per image. */ + if (0 === settings.event.indexOf('scroll')) { + $container.bind(settings.event, function() { + return update(); + }); + } + this.each(function() { + var self = this; + var $self = $(self); + self.loaded = false; + /* If no src attribute given use data:uri. */ + if ($self.attr('src') === undefined || $self.attr('src') === false) { + if ($self.is('img')) { + $self.attr('src', settings.placeholder); + } + } + /* When appear is triggered load original image. */ + $self.one('appear', function() { + if (!this.loaded) { + if (settings.appear) { + var elements_left = elements.length; + settings.appear.call(self, elements_left, settings); + } + $('<img />') + .bind('load', function() { + var original = $self.attr('data-' + settings.data_attribute); + $self.hide(); + if ($self.is('img')) { + $self.attr('src', original); + } else { + $self.css('background-image', 'url("' + original + '")'); + } + $self[settings.effect](settings.effect_speed); + self.loaded = true; + /* Remove image from array so it is not looped next time. */ + var temp = $.grep(elements, function(element) { + return !element.loaded; + }); + elements = $(temp); + if (settings.load) { + var elements_left = elements.length; + settings.load.call(self, elements_left, settings); + } + }) + .attr('src', $self.attr('data-' + settings.data_attribute)); + } + }); + /* When wanted event is triggered load original image */ + /* by triggering appear. */ + if (0 !== settings.event.indexOf('scroll')) { + $self.bind(settings.event, function() { + if (!self.loaded) { + $self.trigger('appear'); + } + }); + } + }); + /* Check if something appears when window is resized. */ + $window.bind('resize', function() { + update(); + }); + /* With IOS5 force loading images when navigating with back button. */ + /* Non optimal workaround. */ + if ((/(?:iphone|ipod|ipad).*os 5/gi).test(navigator.appVersion)) { + $window.bind('pageshow', function(event) { + if (event.originalEvent && event.originalEvent.persisted) { + elements.each(function() { + $(this).trigger('appear'); + }); + } + }); + } + /* Force initial check if images should appear. */ + $(document).ready(function() { + update(); + }); + return this; + }; + /* Convenience methods in jQuery namespace. */ + /* Use as $.belowthefold(element, {threshold : 100, container : window}) */ + $.belowthefold = function(element, settings) { + var fold; + if (settings.container === undefined || settings.container === window) { + fold = (window.innerHeight ? window.innerHeight : $window.height()) + $window.scrollTop(); + } else { + fold = $(settings.container).offset().top + $(settings.container).height(); + } + return fold <= $(element).offset().top - settings.threshold; + }; + $.rightoffold = function(element, settings) { + var fold; + if (settings.container === undefined || settings.container === window) { + fold = $window.width() + $window.scrollLeft(); + } else { + fold = $(settings.container).offset().left + $(settings.container).width(); + } + return fold <= $(element).offset().left - settings.threshold; + }; + $.abovethetop = function(element, settings) { + var fold; + if (settings.container === undefined || settings.container === window) { + fold = $window.scrollTop(); + } else { + fold = $(settings.container).offset().top; + } + return fold >= $(element).offset().top + settings.threshold + $(element).height(); + }; + $.leftofbegin = function(element, settings) { + var fold; + if (settings.container === undefined || settings.container === window) { + fold = $window.scrollLeft(); + } else { + fold = $(settings.container).offset().left; + } + return fold >= $(element).offset().left + settings.threshold + $(element).width(); + }; + $.inviewport = function(element, settings) { + return !$.rightoffold(element, settings) && !$.leftofbegin(element, settings) && + !$.belowthefold(element, settings) && !$.abovethetop(element, settings); + }; + /* Custom selectors for your convenience. */ + /* Use as $('img:below-the-fold').something() or */ + /* $('img').filter(':below-the-fold').something() which is faster */ + $.extend($.expr[':'], { + 'below-the-fold' : function(a) { return $.belowthefold(a, {threshold : 0}); }, + 'above-the-top' : function(a) { return !$.belowthefold(a, {threshold : 0}); }, + 'right-of-screen': function(a) { return $.rightoffold(a, {threshold : 0}); }, + 'left-of-screen' : function(a) { return !$.rightoffold(a, {threshold : 0}); }, + 'in-viewport' : function(a) { return $.inviewport(a, {threshold : 0}); }, + /* Maintain BC for couple of versions. */ + 'above-the-fold' : function(a) { return !$.belowthefold(a, {threshold : 0}); }, + 'right-of-fold' : function(a) { return $.rightoffold(a, {threshold : 0}); }, + 'left-of-fold' : function(a) { return !$.rightoffold(a, {threshold : 0}); } + }); +})(jQuery, window, document); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-mark.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-mark.js new file mode 100644 index 0000000..53d5a50 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-mark.js @@ -0,0 +1,173 @@ +/*! Widget: mark.js - updated 9/23/2016 (v2.27.7) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter; + + ts.mark = { + init : function( c ) { + if ( typeof $.fn.mark === 'function' ) { + var tmp, + update = c.widgetOptions.mark_tsUpdate; + c.$table.on( 'filterEnd.tsmark pagerComplete.tsmark' + + ( update ? ' ' + update : '' ), function( e, filters ) { + // filterEnd passes "config" as the param + ts.mark.update( c, e.type === update ? filters : '' ); + }); + // Regex to split up a query + tmp = '(?:<|=|>|\\||\"|' + "\\'|" + + '\\s+(?:&&|-|' + + ( ts.language.and || 'and' ) + '|' + + ( ts.language.or || 'or' ) + '|' + + ( ts.language.to || 'to' ) + ')\\s+)'; + ts.mark.regex.filter = new RegExp(tmp, 'gim'); + } else { + console.warn('Widget-mark not initialized: missing "jquery.mark.js"'); + } + }, + regex : { + mark : /^mark_(.+)$/, + // test for regex (e.g. "/(lorem|ipsum)/gi") + pure : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/ + }, + checkRegex : function( regex ) { + if ( regex instanceof RegExp ) { + // prevent lock up of mark.js + // (see https://github.com/julmot/mark.js/issues/55) + var result = '\u0001\u0002\u0003\u0004\u0005'.match( regex ); + return result === null || result.length < 5; + } + return false; + }, + cleanMatches : function( matches ) { + var results = [], + indx = matches && matches.length || 0; + while ( indx-- ) { + if ( matches[indx] !== '' ) { + results[ results.length ] = matches[ indx ]; + } + } + return results; + }, + // used when "any" match is performed + ignoreColumns : function( c ) { + var wo = c.widgetOptions, + len = c.columns, + cols = []; + while (len--) { + if (wo.mark_tsIgnore[len] || + $( c.$headerIndexed[len] ).hasClass( 'mark-ignore' ) ) { + cols[cols.length] = ':nth-child(' + (len + 1) + ')'; + } + } + if (cols.length) { + return ':not(' + cols.join(',') + ')'; + } + return ''; + }, + update : function( c, filters ) { + var options = {}, + wo = c.widgetOptions, + regex = ts.mark.regex, + $rows = c.$table + .find( 'tbody tr' ) + .unmark() + .not( '.' + ( c.widgetOptions.filter_filteredRow || 'filtered' ) ); + filters = filters || $.tablesorter.getFilters( c.$table ); + // extract & save mark options from widgetOptions (prefixed with "mark_") + // update dynamically + $.each( c.widgetOptions, function( key, val ) { + var matches = key.match( regex.mark ); + if ( matches && typeof matches[1] !== 'undefined' ) { + options[ matches[1] ] = val; + } + }); + $.each( filters, function( indx, filter ) { + if ( filter && + !( $(c.$headerIndexed[indx]).hasClass('mark-ignore') || + wo.mark_tsIgnore[indx] + ) ) { + var testRegex = null, + matches = filter, + useRegex = false, + col = indx === c.columns ? + ts.mark.ignoreColumns( c ) : + ':nth-child(' + ( indx + 1 ) + ')'; + // regular expression entered + if ( regex.pure.test( filter ) ) { + matches = regex.pure.exec( filter ); + // ignore "all" matches (i.e. /.*/) + if (matches[1] === '.*') { + matches[1] = ''; + } + try { + // make sure to include global flag when testing regex + testRegex = new RegExp( matches[ 1 ], 'gim' ); + matches = new RegExp( matches[ 1 ], matches[ 2 ] ); + } catch (err) { + matches = null; + } + if ( ts.mark.checkRegex( testRegex ) ) { + $rows.children( col ).markRegExp( matches, options ); + } + // matches is either null, invalid, or done my markRegExp + return; + } + // all special querys (or, and, wild cards & fuzzy) + // regex seems to not be consistent here; so use string indexOf + // fuzzy or wild card matches + if ( filter.indexOf( '~' ) === 0 ) { + useRegex = true; + // fuzzy search separate each letter + matches = filter.replace( /~/g, '' ).split( '' ); + } else { + // wild card matching + if ( filter.indexOf( '?' ) > -1 ) { + useRegex = true; + filter = filter.replace( /\?/g, '\\S{1}' ); + } + if ( filter.indexOf( '*' ) > -1 ) { + useRegex = true; + filter = filter.replace( /\*/g, '\\S*' ); + } + matches = filter.split( regex.filter ); + } + if ( useRegex && matches && matches.length ) { + matches = new RegExp( + ts.mark.cleanMatches( matches ).join( '.*' ), + 'gm' + ); + if ( ts.mark.checkRegex( matches ) ) { + $rows.children( col ).markRegExp( matches, options ); + } + } else { + // pass an array of matches + $rows.children( col ).mark( + ts.mark.cleanMatches( matches ), + options + ); + } + } + }); + } + }; + + ts.addWidget({ + id: 'mark', + options: { + mark_tsUpdate : 'markUpdate', + mark_tsIgnore : {} + }, + init : function( table, thisWidget, c, wo ) { + ts.mark.init( c, wo ); + }, + remove : function( table, c ) { + var update = c.widgetOptions.mark_tsUpdate; + c.$table.off( 'filterEnd.tsmark pagerComplete.tsmark' + + ( update ? ' ' + update : '' ) ); + } + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-math.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-math.js new file mode 100644 index 0000000..3199a2e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-math.js @@ -0,0 +1,668 @@ +/*! Widget: math - updated 12/1/2019 (v2.31.2) *//* +* Requires tablesorter v2.16+ and jQuery 1.7+ +* by Rob Garrison +*/ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;( function( $ ) { + 'use strict'; + + var ts = $.tablesorter, + + math = { + + error: { + 0 : 'Infinity result: Divide by zero', + 1 : 'Need more than one element to make this calculation', + 'undef' : 'No elements found' + }, + + // value returned when calculation is not possible, e.g. no values, dividing by zero, etc. + invalid : function( c, name, errorIndex ) { + // name = function returning invalid results + // errorIndex = math.error index with an explanation of the error + console.warn( name, math.error[ errorIndex ] ); + return c && c.widgetOptions.math_none || ''; // text for cell + }, + + events : ( 'tablesorter-initialized update updateAll updateRows addRows updateCell filterReset ' ) + .split(' ').join('.tsmath '), + + processText : function( c, $cell ) { + var tmp, + wo = c.widgetOptions, + txt = ts.getElementText( c, $cell, math.getCellIndex( $cell ) ), + prefix = c.widgetOptions.math_prefix; + if (wo.math_textAttr) { + txt = $cell.attr(wo.math_textAttr) || txt; + } + if ( /</.test( prefix ) ) { + // prefix contains HTML; remove it & any text before using formatFloat + tmp = $( '<div>' + prefix + '</div>' ).text() + .replace(/\{content\}/g, '').trim(); + txt = txt.replace( tmp, '' ); + } + txt = ts.formatFloat( txt.replace( /[^\w,. \-()]/g, '' ), c.table ) || 0; + // isNaN('') => false + return isNaN( txt ) ? 0 : txt; + }, + + // get all of the row numerical values in an arry + getRow : function( c, $el, hasFilter ) { + var $cells, + wo = c.widgetOptions, + arry = [], + $row = $el.closest( 'tr' ), + isFiltered = $row.hasClass( wo.filter_filteredRow || 'filtered' ); + if ( hasFilter ) { + $row = $row.filter( hasFilter ); + } + if ( hasFilter || !isFiltered ) { + $cells = $row.children().not( '[' + wo.math_dataAttrib + '=ignore]' ); + if ( wo.math_ignore.length ) { + $cells = $cells.filter( function() { + // using $.inArray is not optimal (needed for IE8) + return $.inArray( math.getCellIndex( $( this ) ), wo.math_ignore ) === -1; + }); + } + arry = $cells.not( $el ).map( function() { + return math.processText( c, $( this ) ); + }).get(); + } + return arry; + }, + + // get all of the column numerical values in an arry + getColumn : function( c, $el, type, hasFilter ) { + var index, $t, $tr, len, $mathRows, mathAbove, + wo = c.widgetOptions, + arry = [], + $row = $el.closest( 'tr' ), + mathAttr = wo.math_dataAttrib, + mathIgnore = '[' + mathAttr + '=ignore]', + filtered = wo.filter_filteredRow || 'filtered', + cIndex = math.getCellIndex( $el ), + // get all rows to keep row indexing + $rows = c.$table.children( 'tbody' ).children(), + mathAttrs = [ + '[' + mathAttr + '^=above]', + '[' + mathAttr + '^=below]', + '[' + mathAttr + '^=col]', + '[' + mathAttr + '^=all]' + ]; + if ( type === 'above' ) { + len = $rows.index( $row ); + index = len; + while ( index >= 0 ) { + $tr = $rows.eq( index ); + mathAbove = $tr.children().filter( mathAttrs[0] ).length; + if ( hasFilter ) { + $tr = $tr.filter( hasFilter ); + } + $t = $tr.children().filter( function() { + return math.getCellIndex( $( this ) ) === cIndex; + }); + // ignore filtered rows & rows with data-math="ignore" (and starting row) + if ( ( ( hasFilter || !$tr.hasClass( filtered ) ) && + $tr.not( mathIgnore ).length && + index !== len ) || + mathAbove && index !== len ) { + // stop calculating 'above', when encountering another 'above' + if ( mathAbove ) { + index = 0; + } else if ( $t.length && $t.not( mathIgnore ).length ) { + arry[ arry.length ] = math.processText( c, $t ); + } + } + index--; + } + } else if ( type === 'below' ) { + len = $rows.length; + // index + 1 to ignore starting node + for ( index = $rows.index( $row ) + 1; index < len; index++ ) { + $tr = $rows.eq( index ); + if ( $tr.children().filter( mathAttrs[1] ).length ) { + break; + } + if ( hasFilter ) { + $tr = $tr.filter( hasFilter ); + } + $t = $tr.children().filter( function() { + return math.getCellIndex( $( this ) ) === cIndex; + }); + if ( ( hasFilter || !$tr.hasClass( filtered ) ) && + $tr.not( mathIgnore ).length && + $t.length && $t.not( mathIgnore ) ) { + arry[ arry.length ] = math.processText( c, $t ); + } + } + } else { + $mathRows = $rows.not( mathIgnore ); + len = $mathRows.length; + for ( index = 0; index < len; index++ ) { + $tr = $mathRows.eq( index ); + if ( hasFilter ) { + $tr = $tr.filter( hasFilter ); + } + $t = $tr.children().filter( function() { + return math.getCellIndex( $( this ) ) === cIndex; + }); + if ( ( hasFilter || !$tr.hasClass( filtered ) ) && + $t.not( mathAttrs.join( ',' ) ).length && + !$t.is( $el ) && $t.not( mathIgnore ).length + ) { + arry[ arry.length ] = math.processText( c, $t ); + } + } + } + return arry; + }, + + // get all of the column numerical values in an arry + getAll : function( c, hasFilter ) { + var $t, col, $row, rowIndex, rowLen, $cells, cellIndex, cellLen, + arry = [], + wo = c.widgetOptions, + mathAttr = wo.math_dataAttrib, + mathIgnore = '[' + mathAttr + '=ignore]', + filtered = wo.filter_filteredRow || 'filtered', + $rows = c.$table.children( 'tbody' ).children().not( mathIgnore ); + rowLen = $rows.length; + for ( rowIndex = 0; rowIndex < rowLen; rowIndex++ ) { + $row = $rows.eq( rowIndex ); + if ( hasFilter ) { + $row = $row.filter( hasFilter ); + } + if ( hasFilter || !$row.hasClass( filtered ) ) { + $cells = $row.children().not( mathIgnore ); + cellLen = $cells.length; + for ( cellIndex = 0; cellIndex < cellLen; cellIndex++ ) { + $t = $cells.eq( cellIndex ); + col = math.getCellIndex( $t ); + if ( !$t.filter( '[' + mathAttr + ']' ).length && $.inArray( col, wo.math_ignore ) < 0 ) { + arry[ arry.length ] = math.processText( c, $t ); + } + } + } + } + return arry; + }, + + setColumnIndexes : function( c ) { + var $table = c.$table, + last = 1, + // only target rows with a colspan or rows included in a rowspan + $rows = $table.children( 'tbody' ).children().filter( function() { + var cells, indx, + $this = $( this ), + include = $this.children( '[colspan]' ).length > 0; + if ( last > 1 ) { + last--; + include = true; + } else if ( last < 1 ) { + last = 1; + } + if ( $this.children( '[rowspan]' ).length > 0 ) { + cells = this.cells; + // find max rowspan (in case more than one cell has a rowspan) + for ( indx = 0; indx < cells.length; indx++ ) { + last = Math.max( cells[ indx ].rowSpan, last ); + } + } + return include; + }); + // pass `c` (table.config) to computeColumnIndex so it won't add a data-column + // to every tbody cell, just the ones where the .cellIndex property doesn't match + // the calculated cell index - hopefully fixes the lag issue in #1048 + ts.computeColumnIndex( $rows, c ); + }, + + getCellIndex : function( $cell ) { + var indx = $cell.attr( 'data-column' ); + return typeof indx === 'undefined' ? $cell[0].cellIndex : parseInt( indx, 10 ); + }, + + recalculate : function(c, wo, init) { + if ( c && ( !wo.math_isUpdating || init ) ) { + + var time, mathAttr, $mathCells, indx, len, + changed = false, + filters = {}; + if ( c.debug || wo.math_debug ) { + time = new Date(); + } + + // add data-column attributes to all table cells + if ( init ) { + math.setColumnIndexes( c ) ; + } + + // data-attribute name (defaults to data-math) + wo.math_dataAttrib = 'data-' + (wo.math_data || 'math'); + + // all non-info tbody cells + mathAttr = wo.math_dataAttrib; + $mathCells = c.$tbodies.children( 'tr' ).children( '[' + mathAttr + ']' ); + changed = math.mathType( c, $mathCells, wo.math_priority ) || changed; + // only info tbody cells + $mathCells = c.$table + .children( '.' + c.cssInfoBlock + ', tfoot' ) + .children( 'tr' ) + .children( '[' + mathAttr + ']' ); + math.mathType( c, $mathCells, wo.math_priority ); + + // find the 'all' total + $mathCells = c.$table.children().children( 'tr' ).children( '[' + mathAttr + '^=all]' ); + len = $mathCells.length; + // get math filter, if any + // hasFilter = $row.attr( mathAttr + '-filter' ) || wo.math_rowFilter; + for (indx = 0; indx < len; indx++) { + var $cell = $mathCells.eq( indx ), + filter = $cell.attr( mathAttr + '-filter' ) || wo.math_rowFilter; + filters[ filter ] = filters[ filter ] ? filters[ filter ].add( $cell ) : $cell; + } + $.each( filters, function( hasFilter, $cells ) { + changed = math.mathType( c, $cells, [ 'all' ], hasFilter ) || changed; + }); + + // trigger an update only if cells inside the tbody changed + if ( changed ) { + wo.math_isUpdating = true; + if ( c.debug || wo.math_debug ) { + console[ console.group ? 'group' : 'log' ]( 'Math widget updating the cache after recalculation' ); + } + + // update internal cache, but ignore "remove-me" rows and do not resort + ts.updateCache( c, function() { + math.updateComplete( c ); + if ( !init && typeof wo.math_completed === 'function' ) { + wo.math_completed( c ); + } + if ( c.debug || wo.math_debug ) { + console.log( 'Math widget update completed' + ts.benchmark( time ) ); + } + }); + } else { + if ( !init && typeof wo.math_completed === 'function' ) { + wo.math_completed( c ); + } + if ( c.debug || wo.math_debug ) { + console.log( 'Math widget found no changes in data' + ts.benchmark( time ) ); + } + } + } + }, + + updateComplete : function( c ) { + var wo = c.widgetOptions; + if ( wo.math_isUpdating && (c.debug || wo.math_debug ) && console.groupEnd ) { + console.groupEnd(); + } + wo.math_isUpdating = false; + }, + + mathType : function( c, $cells, priority, hasFilter ) { + if ( $cells.length ) { + var getAll, + changed = false, + wo = c.widgetOptions, + mathAttr = wo.math_dataAttrib, + equations = ts.equations; + if ( priority[0] === 'all' ) { + // mathType is called multiple times if more than one "hasFilter" is used + getAll = math.getAll( c, hasFilter ); + } + if (c.debug || wo.math_debug) { + console[ console.group ? 'group' : 'log' ]( 'Tablesorter Math widget recalculation' ); + } + // $.each is okay here... only 4 priorities + $.each( priority, function( i, type ) { + var index, arry, formula, result, $el, + $targetCells = $cells.filter( '[' + mathAttr + '^=' + type + ']' ), + len = $targetCells.length; + if ( len ) { + if (c.debug || wo.math_debug) { + console[ console.group ? 'group' : 'log' ]( type ); + } + for ( index = 0; index < len; index++ ) { + $el = $targetCells.eq( index ); + // Row is filtered, no need to do further checking + if ( $el.parent().hasClass( wo.filter_filteredRow || 'filtered' ) ) { + continue; + } + hasFilter = $el.attr( mathAttr + '-filter' ) || wo.math_rowFilter; + formula = ( $el.attr( mathAttr ) || '' ).replace( type + '-', '' ); + arry = ( type === 'row' ) ? math.getRow( c, $el, hasFilter ) : + ( type === 'all' ) ? getAll : math.getColumn( c, $el, type, hasFilter ); + if ( equations[ formula ] ) { + if ( arry.length ) { + result = equations[ formula ]( arry, c ); + if ( c.debug || wo.math_debug ) { + console.log( $el.attr( mathAttr ), hasFilter ? '("' + hasFilter + '")' : '', arry, '=', result ); + } + } else { + // mean will return a divide by zero error, everything else shows an undefined error + result = math.invalid( c, formula, formula === 'mean' ? 0 : 'undef' ); + } + changed = math.output( $el, c, result, arry ) || changed; + } + } + if ( ( c.debug || wo.math_debug ) && console.groupEnd ) { console.groupEnd(); } + } + }); + if ( ( c.debug || wo.math_debug ) && console.groupEnd ) { console.groupEnd(); } + return changed; + } + return false; + }, + + output : function( $cell, c, value, arry ) { + // get mask from cell data-attribute: data-math-mask="#,##0.00" + var $el, + wo = c.widgetOptions, + changed = false, + prev = $cell.html(), + mask = $cell.attr( 'data-' + wo.math_data + '-mask' ) || wo.math_mask, + target = $cell.attr( 'data-' + wo.math_data + '-target' ) || '', + result = ts.formatMask( mask, value, wo.math_prefix, wo.math_suffix ); + if (target) { + $el = $cell.find(target); + if ($el.length) { + $cell = $el; + } + } + if ( typeof wo.math_complete === 'function' ) { + result = wo.math_complete( $cell, wo, result, value, arry ); + } + if ( result !== false ) { + changed = prev !== result; + $cell.html( result ); + } + // check if in a regular tbody, otherwise don't pass a changed flag + // to prevent unnecessary updating of the table cache + if ( changed ) { + $el = $cell.closest( 'tbody' ); + // content was changed in a tfoot, info-only tbody or the resulting tbody is in a nested table + // then don't signal a change + if ( !$el.length || $el.hasClass( c.cssInfoBlock ) || $el.parent()[0] !== c.table ) { + return false; + } + } + return changed; + } + + }; + + // Modified from https://code.google.com/p/javascript-number-formatter/ + /** + * @preserve IntegraXor Web SCADA - JavaScript Number Formatter + * http:// www.integraxor.com/ + * author: KPL, KHL + * (c)2011 ecava + * Dual licensed under the MIT or GPL Version 2 licenses. + */ + ts.formatMask = function( mask, val, tmpPrefix, tmpSuffix ) { + if ( !mask || isNaN( +val ) ) { + return val; // return as it is. + } + + var isNegative, result, decimal, group, posLeadZero, posTrailZero, posSeparator, part, szSep, + integer, str, offset, index, end, inv, + suffix = '', + + // find prefix/suffix + len = mask.length, + start = mask.search( /[0-9\-\+#]/ ), + tmp = start > 0 ? mask.substring( 0, start ) : '', + prefix = tmp; + if ( tmpPrefix ) { + if ( /\{content\}/.test( tmpPrefix || '' ) ) { + prefix = ( tmpPrefix || '' ).replace( /\{content\}/g, tmp || '' ); + } else { + prefix = ( tmpPrefix || '' ) + tmp; + } + } + // reverse string: not an ideal method if there are surrogate pairs + inv = mask.split( '' ).reverse().join( '' ); + end = inv.search( /[0-9\-\+#]/ ); + index = len - end; + index += ( mask.substring( index, index + 1 ) === '.' ) ? 1 : 0; + tmp = end > 0 ? mask.substring( index, len ) : ''; + suffix = tmp; + if ( tmpSuffix ) { + if ( /\{content\}/.test( tmpSuffix || '' ) ) { + suffix = ( tmpSuffix || '' ).replace( /\{content\}/g, tmp || '' ); + } else { + suffix = tmp + ( tmpSuffix || '' ); + } + } + mask = mask.substring( start, index ); + + // convert any string to number according to formation sign. + val = mask.charAt( 0 ) === '-' ? -val : +val; + isNegative = val < 0 ? val = -val : 0; // process only abs(), and turn on flag. + + // search for separator for grp & decimal, anything not digit, not +/- sign, not #. + result = mask.match( /[^\d\-\+#]/g ); + decimal = ( result && result[ result.length - 1 ] ) || '.'; // treat the right most symbol as decimal + group = ( result && result[ 1 ] && result[ 0 ] ) || ','; // treat the left most symbol as group separator + + // split the decimal for the format string if any. + mask = mask.split( decimal ); + // Fix the decimal first, toFixed will auto fill trailing zero. + val = val.toFixed( mask[ 1 ] && mask[ 1 ].length ); + val = +( val ) + ''; // convert number to string to trim off *all* trailing decimal zero(es) + + // fill back any trailing zero according to format + posTrailZero = mask[ 1 ] && mask[ 1 ].lastIndexOf( '0' ); // look for last zero in format + part = val.split( '.' ); + // integer will get !part[1] + if ( !part[ 1 ] || ( part[ 1 ] && part[ 1 ].length <= posTrailZero ) ) { + val = ( +val ).toFixed( posTrailZero + 1 ); + } + szSep = mask[ 0 ].split( group ); // look for separator + mask[ 0 ] = szSep.join( '' ); // join back without separator for counting the pos of any leading 0. + + posLeadZero = mask[ 0 ] && mask[ 0 ].indexOf( '0' ); + if ( posLeadZero > -1 ) { + while ( part[ 0 ].length < ( mask[ 0 ].length - posLeadZero ) ) { + part[ 0 ] = '0' + part[ 0 ]; + } + } else if ( +part[ 0 ] === 0 ) { + part[ 0 ] = ''; + } + + val = val.split( '.' ); + val[ 0 ] = part[ 0 ]; + + // process the first group separator from decimal (.) only, the rest ignore. + // get the length of the last slice of split result. + posSeparator = ( szSep[ 1 ] && szSep[ szSep.length - 1 ].length ); + if ( posSeparator ) { + integer = val[ 0 ]; + str = ''; + offset = integer.length % posSeparator; + len = integer.length; + for ( index = 0; index < len; index++ ) { + str += integer.charAt( index ); // ie6 only support charAt for sz. + // -posSeparator so that won't trail separator on full length + /*jshint -W018 */ + if ( !( ( index - offset + 1 ) % posSeparator ) && index < len - posSeparator ) { + str += group; + } + } + val[ 0 ] = str; + } + + val[ 1 ] = ( mask[ 1 ] && val[ 1 ] ) ? decimal + val[ 1 ] : ''; + // put back any negation, combine integer and fraction, and add back prefix & suffix + return prefix + ( ( isNegative ? '-' : '' ) + val[ 0 ] + val[ 1 ] ) + suffix; + }; + + ts.equations = { + count : function( arry ) { + return arry.length; + }, + sum : function( arry ) { + var index, + len = arry.length, + total = 0; + for ( index = 0; index < len; index++ ) { + total += arry[ index ]; + } + return total; + }, + mean : function( arry ) { + var total = ts.equations.sum( arry ); + return total / arry.length; + }, + median : function( arry, c ) { + var half, + len = arry.length; + if ( len > 1 ) { + // https://gist.github.com/caseyjustus/1166258 + arry.sort( function( a, b ) { return a - b; } ); + half = Math.floor( len / 2 ); + return ( len % 2 ) ? arry[ half ] : ( arry[ half - 1 ] + arry[ half ] ) / 2; + } + return math.invalid( c, 'median', 1 ); + }, + mode : function( arry ) { + // http://stackoverflow.com/a/3451640/145346 + var index, el, m, + modeMap = {}, + maxCount = 1, + modes = [ arry[ 0 ] ]; + for ( index = 0; index < arry.length; index++ ) { + el = arry[ index ]; + modeMap[ el ] = modeMap[ el ] ? modeMap[ el ] + 1 : 1; + m = modeMap[ el ]; + if ( m > maxCount ) { + modes = [ el ]; + maxCount = m; + } else if ( m === maxCount ) { + modes[ modes.length ] = el; + maxCount = m; + } + } + // returns arry of modes if there is a tie + return modes.sort( function( a, b ) { return a - b; } ); + }, + max : function(arry) { + return Math.max.apply( Math, arry ); + }, + min : function(arry) { + return Math.min.apply( Math, arry ); + }, + range: function(arry) { + var v = arry.sort( function( a, b ) { return a - b; } ); + return v[ arry.length - 1 ] - v[ 0 ]; + }, + // common variance equation + // (not accessible via data-attribute setting) + variance: function( arry, population, c ) { + var divisor, + avg = ts.equations.mean( arry ), + v = 0, + i = arry.length; + while ( i-- ) { + v += Math.pow( ( arry[ i ] - avg ), 2 ); + } + divisor = ( arry.length - ( population ? 0 : 1 ) ); + if ( divisor === 0 ) { + return math.invalid( c, 'variance', 0 ); + } + v /= divisor; + return v; + }, + // variance (population) + varp : function( arry, c ) { + return ts.equations.variance( arry, true, c ); + }, + // variance (sample) + vars : function( arry, c ) { + return ts.equations.variance( arry, false, c ); + }, + // standard deviation (sample) + stdevs : function( arry, c ) { + var vars = ts.equations.variance( arry, false, c ); + return Math.sqrt( vars ); + }, + // standard deviation (population) + stdevp : function( arry, c ) { + var varp = ts.equations.variance( arry, true, c ); + return Math.sqrt( varp ); + } + }; + + // add new widget called repeatHeaders + // ************************************ + ts.addWidget({ + id: 'math', + priority: 100, + options: { + math_data : 'math', + math_debug : false, + // column index to ignore + math_ignore : [], + // mask info: https://code.google.com/p/javascript-number-formatter/ + math_mask : '#,##0.00', + // complete executed after each fucntion + math_complete : null, // function($cell, wo, result, value, arry) { return result; }, + // math_completed called after all math calculations have completed + math_completed: function( /* config */ ) {}, + // order of calculation; 'all' is last + math_priority : [ 'row', 'above', 'below', 'col' ], + // template for or just prepend the mask prefix & suffix with this HTML + // e.g. '<span class="red">{content}</span>' + math_prefix : '', + math_suffix : '', + // cell attribute containing the math value to use + math_textAttr : '', + // no matching math elements found (text added to cell) + math_none : 'N/A', + math_event : 'recalculate', + // use this filter to target specific rows (e.g. ':visible', or ':not(.empty-row)') + math_rowFilter: '' + }, + init : function( table, thisWidget, c, wo ) { + // filterEnd fires after updateComplete + var update = ( ts.hasWidget( table, 'filter' ) ? 'filterEnd' : 'updateComplete' ) + '.tsmath'; + // filterEnd is when the pager hides rows... so bind to pagerComplete + math.events += ( ts.hasWidget( table, 'pager' ) ? 'pagerComplete' : 'filterEnd' ) + '.tsmath '; + c.$table + .off( ( math.events + 'updateComplete.tsmath ' + wo.math_event ).replace( /\s+/g, ' ' ) ) + .on( math.events + wo.math_event, function( e ) { + if ( !this.hasInitialized ) { return; } + var init = e.type === 'tablesorter-initialized'; + if ( !wo.math_isUpdating || init ) { + // don't setColumnIndexes on init here, or it gets done twice + if ( !/filter/.test( e.type ) && !init ) { + // redo data-column indexes on update + math.setColumnIndexes( c ); + } + math.recalculate( c, wo, init ); + } + }) + .on( update, function() { + setTimeout( function() { + math.updateComplete( c ); + }, 40 ); + }); + wo.math_isUpdating = false; + // math widget initialized after table - see #946 + if ( table.hasInitialized ) { + math.recalculate( c, wo, true ); + } + }, + // this remove function is called when using the refreshWidgets method or when destroying the tablesorter plugin + // this function only applies to tablesorter v2.4+ + remove: function( table, c, wo, refreshing ) { + if ( refreshing ) { return; } + c.$table + .off( ( math.events + ' updateComplete.tsmath ' + wo.math_event ).replace( /\s+/g, ' ' ) ) + .children().children( 'tr' ).children( '[data-' + wo.math_data + ']' ).empty(); + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-output.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-output.js new file mode 100644 index 0000000..9d3187d --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-output.js @@ -0,0 +1,440 @@ +/*! Widget: output - updated 9/27/2017 (v2.29.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * Modified from: + * HTML Table to CSV: http://www.kunalbabre.com/projects/table2CSV.php (License unknown?) + * Download-File-JS: https://github.com/PixelsCommander/Download-File-JS (http://www.apache.org/licenses/LICENSE-2.0) + * FileSaver.js: https://github.com/eligrey/FileSaver.js (MIT) + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery:false, alert:false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + + output = ts.output = { + + event : 'outputTable', + // Double click time is about 500ms; this value ignores double clicks + // and prevents multiple windows from opening - issue in Firefox + noDblClick : 600, // ms + lastEvent : 0, + // prevent overlapping multiple opens in case rendering of content in + // popup or download is longer than noDblClick time. + busy : false, + + // wrap line breaks & tabs in quotes + regexQuote : /([\n\t\x09\x0d\x0a]|<[^<]+>)/, // test if cell needs wrapping quotes + regexBR : /(<br([\s\/])?>|\n)/g, // replace + regexIMG : /<img[^>]+alt\s*=\s*['"]([^'"]+)['"][^>]*>/i, // match + regexHTML : /<[^<]+>/g, // replace + + replaceCR : '\x0d\x0a', + replaceTab : '\x09', + + popupTitle : 'Output', + popupStyle : 'width:100%;height:100%;margin:0;resize:none;', // for textarea + message : 'Your device does not support downloading. Please try again in desktop browser.', + + init : function(c) { + c.$table + .off(output.event) + .on(output.event, function( e ) { + e.stopPropagation(); + // prevent multiple windows opening + if ( + !output.busy && + (e.timeStamp - output.lastEvent > output.noDblClick) + ) { + output.lastEvent = e.timeStamp; + output.busy = true; + // explicitly use table.config.widgetOptions because we want + // the most up-to-date values; not the 'wo' from initialization + output.process(c, c.widgetOptions); + } + }); + }, + + processRow: function(c, $rows, isHeader, isJSON) { + var $cell, $cells, cellsLen, rowIndex, row, col, indx, rowspanLen, colspanLen, txt, + wo = c.widgetOptions, + tmpRow = [], + dupe = wo.output_duplicateSpans, + addSpanIndex = isHeader && isJSON && wo.output_headerRows && $.isFunction(wo.output_callbackJSON), + cellIndex = 0, + rowsLength = $rows.length; + + for ( rowIndex = 0; rowIndex < rowsLength; rowIndex++ ) { + if (!tmpRow[rowIndex]) { tmpRow[rowIndex] = []; } + cellIndex = 0; + $cells = $rows.eq( rowIndex ).children(); + cellsLen = $cells.length; + for ( indx = 0; indx < cellsLen; indx++ ) { + $cell = $cells.eq( indx ); + // process rowspans + if ($cell.filter('[rowspan]').length) { + rowspanLen = parseInt( $cell.attr('rowspan'), 10) - 1; + txt = output.formatData( c, wo, $cell, isHeader, indx ); + for (row = 1; row <= rowspanLen; row++) { + if (!tmpRow[rowIndex + row]) { tmpRow[rowIndex + row] = []; } + tmpRow[rowIndex + row][cellIndex] = isHeader ? txt : dupe ? txt : ''; + } + } + // process colspans + if ($cell.filter('[colspan]').length) { + colspanLen = parseInt( $cell.attr('colspan'), 10) - 1; + // allow data-attribute to be an empty string + txt = output.formatData( c, wo, $cell, isHeader, indx ); + for (col = 0; col < colspanLen; col++) { + // if we're processing the header & making JSON, the header names need to be unique + if ($cell.filter('[rowspan]').length) { + rowspanLen = parseInt( $cell.attr('rowspan'), 10); + for (row = 0; row < rowspanLen; row++) { + if (!tmpRow[rowIndex + row]) { tmpRow[rowIndex + row] = []; } + tmpRow[rowIndex + row][cellIndex + col] = addSpanIndex ? + wo.output_callbackJSON($cell, txt, cellIndex + col) || + txt + '(' + (cellIndex + col) + ')' : isHeader ? txt : dupe ? txt : ''; + } + } else { + tmpRow[rowIndex][cellIndex + col] = addSpanIndex ? + wo.output_callbackJSON($cell, txt, cellIndex + col) || + txt + '(' + (cellIndex + col) + ')' : isHeader ? txt : dupe ? txt : ''; + } + } + } + + // skip column if already defined + while (typeof tmpRow[rowIndex][cellIndex] !== 'undefined') { cellIndex++; } + + tmpRow[rowIndex][cellIndex] = tmpRow[rowIndex][cellIndex] || + output.formatData( c, wo, $cell, isHeader, cellIndex ); + cellIndex++; + } + } + return ts.output.removeColumns( c, wo, tmpRow ); + }, + + // remove hidden/ignored columns + removeColumns : function( c, wo, arry ) { + var rowIndex, row, colIndex, + data = [], + len = arry.length; + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + row = arry[ rowIndex ]; + data[ rowIndex ] = []; + for ( colIndex = 0; colIndex < c.columns; colIndex++ ) { + if ( !wo.output_hiddenColumnArray[ colIndex ] ) { + data[ rowIndex ].push( row[ colIndex ] ); + } + } + } + return data; + }, + + // optional vars $rows and dump added by TheSin to make + // process callable via callback for ajaxPager + process : function(c, wo, $rows, dump) { + var mydata, $this, headers, csvData, len, rowsLen, tmp, + hasStringify = window.JSON && JSON.hasOwnProperty('stringify'), + indx = 0, + tmpData = (wo.output_separator || ',').toLowerCase(), + outputJSON = tmpData === 'json', + outputArray = tmpData === 'array', + separator = outputJSON || outputArray ? ',' : wo.output_separator, + saveRows = wo.output_saveRows, + $el = c.$table; + // regex to look for the set separator or HTML + wo.output_regex = new RegExp('(' + (/\\/.test(separator) ? '\\' : '' ) + separator + ')' ); + + // make a list of hidden columns + wo.output_hiddenColumnArray = []; + for ( indx = 0; indx < c.columns; indx++ ) { + wo.output_hiddenColumnArray[ indx ] = $.inArray( indx, wo.output_ignoreColumns ) > -1 || + ( !wo.output_hiddenColumns && c.$headerIndexed[ indx ].css( 'display' ) === 'none' && + !c.$headerIndexed[ indx ].hasClass( 'tablesorter-scroller-hidden-column' ) ); + } + + // get header cells + $this = $el + .children('thead').children('tr') + .not('.' + (ts.css.filterRow || 'tablesorter-filter-row') ) + .filter( function() { + return wo.output_hiddenColumns || $(this).css('display') !== 'none'; + }); + headers = output.processRow(c, $this, true, outputJSON); + + // all tbody rows - do not include widget added rows (e.g. grouping widget headers) + if ( !$rows ) { + $rows = $el.children('tbody').children('tr').not(c.selectorRemove); + } + + // check for a filter callback function first! because + // /^f/.test(function() { console.log('test'); }) is TRUE! (function is converted to a string) + $rows = typeof saveRows === 'function' ? $rows.filter(saveRows) : + // get (f)iltered, (v)isible, all rows (look for the first letter only), or jQuery filter selector + /^f/.test(saveRows) ? $rows.not('.' + (wo.filter_filteredRow || 'filtered') ) : + /^v/.test(saveRows) ? $rows.filter(':visible') : + // look for '.' (class selector), '#' (id selector), + // ':' (basic filters, e.g. ':not()') or '[' (attribute selector start) + /^[.#:\[]/.test(saveRows) ? $rows.filter(saveRows) : + // default to all rows + $rows; + + // process to array of arrays + csvData = output.processRow(c, $rows); + if (wo.output_includeFooter) { + // clone, to force the tfoot rows to the end of this selection of rows + // otherwise they appear after the thead (the order in the HTML) + csvData = csvData.concat( output.processRow( c, $el.children('tfoot').children('tr:visible') ) ); + } + + len = headers.length; + + if (outputJSON) { + tmpData = []; + rowsLen = csvData.length; + for ( indx = 0; indx < rowsLen; indx++ ) { + // multiple header rows & output_headerRows = true, pick the last row... + tmp = headers[ ( len > 1 && wo.output_headerRows ) ? indx % len : len - 1 ]; + tmpData.push( output.row2Hash( tmp, csvData[ indx ] ) ); + } + + // requires JSON stringify; if it doesn't exist, the output will show [object Object],... in the output window + mydata = hasStringify ? JSON.stringify(tmpData) : tmpData; + } else { + if (wo.output_includeHeader) { + tmp = [ headers[ ( len > 1 && wo.output_headerRows ) ? indx % len : len - 1 ] ]; + tmpData = output.row2CSV(wo, wo.output_headerRows ? headers : tmp, outputArray) + .concat( output.row2CSV(wo, csvData, outputArray) ); + } else { + tmpData = output.row2CSV(wo, csvData, outputArray); + } + + // stringify the array; if stringify doesn't exist the array will be flattened + mydata = outputArray && hasStringify ? JSON.stringify(tmpData) : tmpData.join('\n'); + } + + if (dump) { + return mydata; + } + + // callback; if true returned, continue processing + if ($.isFunction(wo.output_callback)) { + tmp = wo.output_callback(c, mydata, c.pager && c.pager.ajaxObject.url || null); + if ( tmp === false ) { + output.busy = false; + return; + } else if ( typeof tmp === 'string' ) { + mydata = tmp; + } + } + + if ( /p/i.test( wo.output_delivery || '' ) ) { + output.popup(mydata, wo.output_popupStyle, outputJSON || outputArray); + } else { + output.download(c, wo, mydata); + } + output.busy = false; + + }, // end process + + row2CSV : function(wo, tmpRow, outputArray) { + var tmp, rowIndex, + csvData = [], + rowLen = tmpRow.length; + for (rowIndex = 0; rowIndex < rowLen; rowIndex++) { + // remove any blank rows + tmp = ( tmpRow[rowIndex] || [] ).join('').replace(/\"/g, ''); + if ( ( tmpRow[rowIndex] || [] ).length > 0 && tmp !== '' ) { + csvData[csvData.length] = outputArray ? tmpRow[rowIndex] : tmpRow[rowIndex].join(wo.output_separator); + } + } + return csvData; + }, + + row2Hash : function( keys, values ) { + var indx, + json = {}, + len = values.length; + for ( indx = 0; indx < len; indx++ ) { + if ( indx < keys.length ) { + json[ keys[ indx ] ] = values[ indx ]; + } + } + return json; + }, + + formatData : function(c, wo, $el, isHeader, colIndex) { + var attr = $el.attr(wo.output_dataAttrib), + txt = typeof attr !== 'undefined' ? attr : $el.html(), + quotes = (wo.output_separator || ',').toLowerCase(), + separator = quotes === 'json' || quotes === 'array', + // replace " with “ if undefined + result = txt.replace(/\"/g, wo.output_replaceQuote || '\u201c'); + // replace line breaks with \\n & tabs with \\t + if (!wo.output_trimSpaces) { + result = result.replace(output.regexBR, output.replaceCR).replace(/\t/g, output.replaceTab); + } else { + result = result.replace(output.regexBR, ''); + } + // extract img alt text + txt = result.match(output.regexIMG); + if (!wo.output_includeHTML && txt !== null) { + result = txt[1]; + } + // replace/remove html + result = wo.output_includeHTML && !isHeader ? result : result.replace(output.regexHTML, ''); + result = wo.output_trimSpaces || isHeader ? $.trim(result) : result; + // JSON & array outputs don't need quotes + quotes = separator ? false : wo.output_wrapQuotes || wo.output_regex.test(result) || output.regexQuote.test(result); + result = quotes ? '"' + result + '"' : result; + // formatting callback - added v2.22.4 + if ( typeof wo.output_formatContent === 'function' ) { + return wo.output_formatContent( c, wo, { + isHeader : isHeader || false, + $cell : $el, + content : result, + columnIndex: colIndex, + parsed: c.parsers[colIndex].format(result, c.table, $el[0], colIndex) + }); + } + + return result; + }, + + popup : function(data, style, wrap) { + var generator = window.open('', output.popupTitle, style); + try { + generator.document.write( + '<html><head><title>' + output.popupTitle + '</title></head><body>' + + '<textarea wrap="' + (wrap ? 'on' : 'off') + '" style="' + + output.popupStyle + '">' + data + '\n</textarea>' + + '</body></html>' + ); + generator.document.close(); + generator.focus(); + } catch (e) { + // popup already open + generator.close(); + return output.popup(data, style, wrap); + } + // select all text and focus within the textarea in the popup + // $(generator.document).find('textarea').select().focus(); + return true; + }, + + // modified from https://github.com/PixelsCommander/Download-File-JS + // & http://html5-demos.appspot.com/static/a.download.html + download : function (c, wo, data) { + + if (typeof wo.output_savePlugin === 'function') { + return wo.output_savePlugin(c, wo, data); + } + + var e, blob, gotBlob, bom, + nav = window.navigator, + link = document.createElement('a'); + + // iOS devices do not support downloading. We have to inform user about + // this limitation. + if (/(iP)/g.test(nav.userAgent)) { + alert(output.message); + return false; + } + + // test for blob support + try { + gotBlob = !!new Blob(); + } catch (err) { + gotBlob = false; + } + + // Use HTML5 Blob if browser supports it + if ( gotBlob ) { + + window.URL = window.URL || window.webkitURL; + // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + // see https://github.com/eligrey/FileSaver.js/blob/master/FileSaver.js#L68 + bom = (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(wo.output_encoding)) ? + [ '\ufeff', data ] : [ data ]; + blob = new Blob( bom, { type: wo.output_encoding } ); + + if (nav.msSaveBlob) { + // IE 10+ + nav.msSaveBlob(blob, wo.output_saveFileName); + } else { + // all other browsers + link.href = window.URL.createObjectURL(blob); + link.download = wo.output_saveFileName; + // Dispatching click event; using $(link).trigger() won't work + if (document.createEvent) { + e = document.createEvent('MouseEvents'); + // event.initMouseEvent(type, canBubble, cancelable, view, detail, screenX, screenY, clientX, clientY, + // ctrlKey, altKey, shiftKey, metaKey, button, relatedTarget); + e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + link.dispatchEvent(e); + } + } + return false; + } + + // fallback to force file download (whether supported by server). + // not sure if this actually works in IE9 and older... + window.open( wo.output_encoding + encodeURIComponent(data) + '?download', '_self'); + return true; + + }, + + remove : function(c) { + c.$table.off(output.event); + } + + }; + + ts.addWidget({ + id: 'output', + options: { + output_separator : ',', // set to 'json', 'array' or any separator + output_ignoreColumns : [], // columns to ignore [0, 1,... ] (zero-based index) + output_hiddenColumns : false, // include hidden columns in the output + output_includeFooter : false, // include footer rows in the output + output_includeHeader : true, // include header rows in the output + output_headerRows : false, // if true, include multiple header rows + output_dataAttrib : 'data-name', // header attrib containing modified header name + output_delivery : 'popup', // popup, download + output_saveRows : 'filtered', // (a)ll, (v)isible, (f)iltered or jQuery filter selector + output_duplicateSpans : true, // duplicate output data in tbody colspan/rowspan + output_replaceQuote : '\u201c;', // left double quote + output_includeHTML : false, + output_trimSpaces : true, + output_wrapQuotes : false, + output_popupStyle : 'width=500,height=300', + output_saveFileName : 'mytable.csv', + // format $cell content callback + output_formatContent : null, // function(config, widgetOptions, data) { return data.content; } + // callback executed when processing completes + // return true to continue download/output + // return false to stop delivery & do something else with the data + output_callback : function(/* config, data */) { return true; }, + // JSON callback executed when a colspan is encountered in the header + output_callbackJSON : function($cell, txt, cellIndex) { return txt + '(' + (cellIndex) + ')'; }, + // the need to modify this for Excel no longer exists + output_encoding : 'data:application/octet-stream;charset=utf8,', + // override internal save file code and use an external plugin such as + // https://github.com/eligrey/FileSaver.js + output_savePlugin : null /* function(c, wo, data) { + var blob = new Blob([data], {type: wo.output_encoding}); + saveAs(blob, wo.output_saveFileName); + } */ + }, + init: function(table, thisWidget, c) { + output.init(c); + }, + remove: function(table, c) { + output.remove(c); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-pager.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-pager.js new file mode 100644 index 0000000..0ce0388 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-pager.js @@ -0,0 +1,1375 @@ +/*! Widget: Pager - updated 2020-03-03 (v2.31.3) */ +/* Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +/*jshint browser:true, jquery:true, unused:false */ +;(function($) { + 'use strict'; + var tsp, + ts = $.tablesorter; + + ts.addWidget({ + id: 'pager', + priority: 55, // load pager after filter widget + options: { + // output default: '{page}/{totalPages}' + // possible variables: {size}, {page}, {totalPages}, {filteredPages}, {startRow}, + // {endRow}, {filteredRows} and {totalRows} + pager_output: '{startRow} to {endRow} of {totalRows} rows', // '{page}/{totalPages}' + + // apply disabled classname to the pager arrows when the rows at either extreme is visible + pager_updateArrows: true, + + // starting page of the pager (zero based index) + pager_startPage: 0, + + // reset pager after filtering; set to desired page # + // set to false to not change page at filter start + pager_pageReset: 0, + + // Number of visible rows + pager_size: 10, + + // Number of options to include in the pager number selector + pager_maxOptionSize: 20, + + // Save pager page & size if the storage script is loaded (requires $.tablesorter.storage + // in jquery.tablesorter.widgets.js) + pager_savePages: true, + + // defines custom storage key + pager_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 + pager_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. + pager_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. + pager_removeRows: false, // removing rows in larger tables speeds up the sort + + // 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 + pager_ajaxUrl: null, + + // modify the url after all processing has been applied + pager_customAjaxUrl: function( table, url ) { return url; }, + + // ajax error callback from $.tablesorter.showError function + // pager_ajaxError: function( config, xhr, settings, exception ) { return exception; }; + // returning false will abort the error message + pager_ajaxError: null, + + // modify the $.ajax object to allow complete control over your ajax requests + pager_ajaxObject: { + dataType: 'json' + }, + + // set this to false if you want to block ajax loading on init + pager_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 + // ] + pager_ajaxProcessing: function( data ) { return data; }, + + // css class names of pager arrows + pager_css: { + container : 'tablesorter-pager', + // error information row (don't include period at beginning) + errorRow : 'tablesorter-errorRow', + // class added to arrows @ extremes (i.e. prev/first arrows 'disabled' on first page) + disabled : 'disabled' + }, + + // jQuery selectors + pager_selectors: { + container : '.pager', // target the pager markup + first : '.first', // go to first page arrow + prev : '.prev', // previous page arrow + next : '.next', // next page arrow + last : '.last', // go to last page arrow + // goto is a reserved word #657 + gotoPage : '.gotoPage', // go to page selector - select dropdown that sets the current page + pageDisplay : '.pagedisplay', // location of where the 'output' is displayed + pageSize : '.pagesize' // page size selector - select dropdown that sets the 'size' option + } + }, + init: function( table ) { + tsp.init( table ); + }, + // only update to complete sorter initialization + format: function( table, c ) { + if ( !( c.pager && c.pager.initialized ) ) { + return tsp.initComplete( c ); + } + tsp.moveToPage( c, c.pager, false ); + }, + remove: function( table, c, wo, refreshing ) { + tsp.destroyPager( c, refreshing ); + } + }); + + /* pager widget functions */ + tsp = ts.pager = { + + init: function( table ) { + // check if tablesorter has initialized + if ( table.hasInitialized && table.config.pager && table.config.pager.initialized ) { return; } + var t, + c = table.config, + wo = c.widgetOptions, + s = wo.pager_selectors, + + // save pager variables + p = c.pager = $.extend({ + totalPages: 0, + filteredRows: 0, + filteredPages: 0, + currentFilters: [], + page: wo.pager_startPage, + startRow: 0, + endRow: 0, + ajaxCounter: 0, + $size: null, + last: {}, + // save original pager size + setSize: wo.pager_size, + setPage: wo.pager_startPage + }, c.pager ); + + // Used by core appendCache; !undefined is always true + p.removeRows = wo.pager_removeRows; + + // pager initializes multiple times before table has completed initialization + if ( p.isInitializing ) { return; } + + p.isInitializing = true; + if ( ts.debug(c, 'pager') ) { + console.log( 'Pager >> Initializing' ); + } + + p.size = $.data( table, 'pagerLastSize' ) || wo.pager_size; + // added in case the pager is reinitialized after being destroyed. + p.$container = $( s.container ).addClass( wo.pager_css.container ).show(); + p.totalRows = c.$tbodies.eq( 0 ) + .children( 'tr' ) + .not( wo.pager_countChildRows ? '' : '.' + c.cssChildRow ) + .length; + p.oldAjaxSuccess = p.oldAjaxSuccess || wo.pager_ajaxObject.success; + c.appender = tsp.appender; + p.initializing = true; + if ( wo.pager_savePages && ts.storage ) { + t = ts.storage( table, wo.pager_storageKey ) || {}; // fixes #387 + p.page = ( isNaN( t.page ) ? p.page : t.page ) || p.setPage || 0; + p.size = t.size === 'all' ? t.size : ( isNaN( t.size ) ? p.size : t.size ) || p.setSize || 10; + tsp.setPageSize( c, p.size ); + } + + // skipped rows + p.regexRows = new RegExp( '(' + ( wo.filter_filteredRow || 'filtered' ) + '|' + + c.selectorRemove.slice( 1 ) + '|' + c.cssChildRow + ')' ); + p.regexFiltered = new RegExp( wo.filter_filteredRow || 'filtered' ); + + // clear initialized flag + p.initialized = false; + // before initialization event + c.$table.triggerHandler( 'pagerBeforeInitialized', c ); + + tsp.enablePager( c, false ); + + // p must have ajaxObject + p.ajaxObject = wo.pager_ajaxObject; + p.ajaxObject.url = wo.pager_ajaxUrl; + + if ( typeof wo.pager_ajaxUrl === 'string' ) { + // ajax pager; interact with database + p.ajax = true; + // When filtering with ajax, allow only custom filtering function, disable default filtering + // since it will be done server side. + wo.filter_serversideFiltering = true; + c.serverSideSorting = true; + tsp.moveToPage( c, p ); + } else { + p.ajax = false; + // Regular pager; all rows stored in memory + ts.appendCache( c, true ); // true = don't apply widgets + } + + }, + + initComplete: function( c ) { + var p = c.pager; + tsp.bindEvents( c ); + if ( !p.ajax ) { + tsp.hideRowsSetup( c ); + } + + // pager initialized + p.initialized = true; + p.initializing = false; + p.isInitializing = false; + tsp.setPageSize( c, p.size ); // page size 0 is ignored + if ( ts.debug(c, 'pager') ) { + console.log( 'Pager >> Triggering pagerInitialized' ); + } + c.$table.triggerHandler( 'pagerInitialized', c ); + // filter widget not initialized; it will update the output display & fire off the pagerComplete event + if ( !( c.widgetOptions.filter_initialized && ts.hasWidget( c.table, 'filter' ) ) ) { + // if ajax, then don't fire off pagerComplete + tsp.updatePageDisplay( c, !p.ajax ); + } + }, + + bindEvents: function( c ) { + var ctrls, fxn, tmp, + p = c.pager, + wo = c.widgetOptions, + namespace = c.namespace + 'pager', + s = wo.pager_selectors, + debug = ts.debug(c, 'pager'); + c.$table + .off( namespace ) + .on( '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 tsp.moveToPage( c, 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' && wo.pager_pageReset !== false && !filtersEqual ) { + p.page = wo.pager_pageReset; // fixes #456 & #565 + } + }) + // update pager after filter widget completes + .on( '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 + tsp.updateCache( c ); + } + tsp.updatePageDisplay( c, false ); + ts.applyWidget( c.table ); + } + }) + .on( 'disablePager' + namespace, function( e ) { + e.stopPropagation(); + tsp.showAllRows( c ); + }) + .on( 'enablePager' + namespace, function( e ) { + e.stopPropagation(); + tsp.enablePager( c, true ); + }) + .on( 'destroyPager' + namespace, function( e ) { + e.stopPropagation(); + // call removeWidget to make sure internal flags are modified. + ts.removeWidget( c.table, 'pager', false ); + }) + .on( '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 - + ( wo.pager_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 + tsp.updateCache( c ); + } + if ( p.page >= p.totalPages ) { + tsp.moveToLastPage( c, p ); + } + tsp.hideRows( c ); + tsp.changeHeight( c ); + // update without triggering pagerComplete + tsp.updatePageDisplay( c, false ); + // make sure widgets are applied - fixes #450 + ts.applyWidget( table ); + tsp.updatePageDisplay( c ); + }) + .on( 'pageSize refreshComplete '.split( ' ' ).join( namespace + ' ' ), function( e, size ) { + e.stopPropagation(); + tsp.setPageSize( c, tsp.parsePageSize( c, size, 'get' ) ); + tsp.moveToPage( c, p, true ); + tsp.hideRows( c ); + tsp.updatePageDisplay( c, false ); + }) + .on( '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; + tsp.moveToPage( c, p, true ); + tsp.updatePageDisplay( c, false ); + }) + .on( 'pageAndSize' + namespace, function( e, page, size ) { + e.stopPropagation(); + p.page = ( parseInt(page, 10) || 1 ) - 1; + tsp.setPageSize( c, tsp.parsePageSize( c, size, 'get' ) ); + tsp.moveToPage( c, p, true ); + tsp.hideRows( c ); + tsp.updatePageDisplay( c, false ); + }); + + // clicked controls + ctrls = [ s.first, s.prev, s.next, s.last ]; + fxn = [ 'moveToFirstPage', 'moveToPrevPage', 'moveToNextPage', 'moveToLastPage' ]; + if ( debug && !p.$container.length ) { + console.warn( 'Pager >> "container" not found' ); + } + p.$container.find( ctrls.join( ',' ) ) + .attr( 'tabindex', 0 ) + .off( 'click' + namespace ) + .on( 'click' + namespace, function( e ) { + e.stopPropagation(); + var i, + $c = $( this ), + l = ctrls.length; + if ( !$c.hasClass( wo.pager_css.disabled ) ) { + for ( i = 0; i < l; i++ ) { + if ( $c.is( ctrls[ i ] ) ) { + tsp[ fxn[ i ] ]( c, p ); + break; + } + } + } + }); + + tmp = p.$container.find( wo.pager_selectors.gotoPage ); + if ( tmp.length ) { + tmp + .off( 'change' + namespace ) + .on( 'change' + namespace, function() { + p.page = $( this ).val() - 1; + tsp.moveToPage( c, p, true ); + tsp.updatePageDisplay( c, false ); + }); + } else if ( debug ) { + console.warn( 'Pager >> "goto" selector not found' ); + } + + tmp = p.$container.find( wo.pager_selectors.pageSize ); + if ( tmp.length ) { + // setting an option as selected appears to cause issues with initial page size + tmp.find( 'option' ).removeAttr( 'selected' ); + tmp + .off( 'change' + namespace ) + .on( 'change' + namespace, function() { + if ( !$( this ).hasClass( wo.pager_css.disabled ) ) { + var size = $( this ).val(); + // in case there are more than one pager + p.$container.find( wo.pager_selectors.pageSize ).val( size ); + tsp.setPageSize( c, size ); + tsp.moveToPage( c, p, true ); + tsp.changeHeight( c ); + } + return false; + }); + } else if ( debug ) { + console.warn('Pager >> "size" selector not found'); + } + + }, + + // hide arrows at extremes + pagerArrows: function( c, disable ) { + var p = c.pager, + dis = !!disable, + first = dis || p.page === 0, + tp = tsp.getTotalPages( c, p ), + last = dis || p.page === tp - 1 || tp === 0, + wo = c.widgetOptions, + s = wo.pager_selectors; + if ( wo.pager_updateArrows ) { + p.$container + .find( s.first + ',' + s.prev ) + .toggleClass( wo.pager_css.disabled, first ) + .prop( 'aria-disabled', first ); + p.$container + .find( s.next + ',' + s.last ) + .toggleClass( wo.pager_css.disabled, last ) + .prop( 'aria-disabled', last ); + } + }, + + calcFilters: function( c ) { + var normalized, indx, len, + wo = c.widgetOptions, + p = c.pager, + hasFilters = c.$table.hasClass( 'hasFilters' ); + if ( hasFilters && !p.ajax ) { + if ( $.isEmptyObject( c.cache ) ) { + // delayInit: true so nothing is in the cache + p.filteredRows = p.totalRows = c.$tbodies.eq( 0 ) + .children( 'tr' ) + .not( wo.pager_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( c, completed ) { + if ( c.pager && c.pager.initializing ) { return; } + var s, t, $out, options, indx, len, output, + table = c.table, + wo = c.widgetOptions, + p = c.pager, + namespace = c.namespace + 'pager', + sz = tsp.parsePageSize( c, p.size, 'get' ); // don't allow dividing by zero + if ( sz === 'all' ) { sz = p.totalRows; } + if ( wo.pager_countChildRows ) { t[ t.length ] = c.cssChildRow; } + p.$container.find( wo.pager_selectors.pageSize + ',' + wo.pager_selectors.gotoPage ) + .removeClass( wo.pager_css.disabled ) + .removeAttr( 'disabled' ) + .prop( 'aria-disabled', 'false' ); + p.totalPages = Math.ceil( p.totalRows / sz ); // needed for 'pageSize' method + c.totalRows = p.totalRows; + tsp.parsePageNumber( c, p ); + tsp.calcFilters( c ); + c.filteredRows = p.filteredRows; + p.filteredPages = Math.ceil( p.filteredRows / sz ) || 0; + if ( tsp.getTotalPages( c, p ) >= 0 ) { + t = ( sz * p.page > p.filteredRows ) && completed; + p.page = t ? wo.pager_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( wo.pager_selectors.pageDisplay ); + + // Output param can be callback for custom rendering or string + if ( typeof wo.pager_output === 'function' ) { + s = wo.pager_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' : '')) || + wo.pager_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 '<input type="text" class="ts-' + extra[ 0 ] + + '" style="max-width:' + len + 'em" value="' + indx + '"/>'; + } + return extra.length > 1 && data && data[ extra[ 0 ] ] ? + data[ extra[ 0 ] ][ extra[ 1 ] ] : + p[ str ] || ( data ? data[ str ] : deflt ) || deflt; + }); + } + if ( p.$container.find( wo.pager_selectors.gotoPage ).length ) { + t = ''; + options = tsp.buildPageSelect( c, p ); + len = options.length; + for ( indx = 0; indx < len; indx++ ) { + t += '<option value="' + options[ indx ] + '">' + options[ indx ] + '</option>'; + } + // innerHTML doesn't work in IE9 - http://support2.microsoft.com/kb/276228 + p.$container.find( wo.pager_selectors.gotoPage ).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' ) + .off( 'change' + namespace ) + .on( 'change' + namespace, function() { + var v = $( this ).val(), + pg = $( this ).hasClass( 'ts-startRow' ) ? Math.floor( v / sz ) + 1 : v; + c.$table.triggerHandler( 'pageSet' + namespace, [ pg ] ); + }); + } + } + tsp.pagerArrows( c ); + tsp.fixHeight( c ); + if ( p.initialized && completed !== false ) { + if ( ts.debug(c, 'pager') ) { + console.log( 'Pager >> Triggering pagerComplete' ); + } + c.$table.triggerHandler( 'pagerComplete', c ); + // save pager info to storage + if ( wo.pager_savePages && ts.storage ) { + ts.storage( table, wo.pager_storageKey, { + page : p.page, + size : sz === p.totalRows ? 'all' : sz + }); + } + } + }, + + buildPageSelect: function( c, p ) { + // Filter the options page number link array if it's larger than 'pager_maxOptionSize' + // as large page set links will slow the browser on large dom inserts + var i, centralFocusSize, focusOptionPages, insertIndex, optionLength, focusLength, + wo = c.widgetOptions, + pg = tsp.getTotalPages( c, p ) || 1, + // make skip set size multiples of 5 + skipSetSize = Math.ceil( ( pg / wo.pager_maxOptionSize ) / 5 ) * 5, + largeCollection = pg > wo.pager_maxOptionSize, + currentPage = p.page + 1, + startPage = skipSetSize, + endPage = pg - skipSetSize, + optionPages = [ 1 ], + // construct default options pages array + optionPagesStartPage = largeCollection ? skipSetSize : 1; + + for ( i = optionPagesStartPage; i <= pg; ) { + optionPages[ optionPages.length ] = i; + i = i + ( largeCollection ? skipSetSize : 1 ); + } + optionPages[ optionPages.length ] = pg; + + if ( largeCollection ) { + focusOptionPages = []; + // don't allow central focus size to be > 5 on either side of current page + centralFocusSize = Math.max( Math.floor( wo.pager_maxOptionSize / skipSetSize ) - 1, 5 ); + + startPage = currentPage - centralFocusSize; + if ( startPage < 1 ) { startPage = 1; } + endPage = currentPage + centralFocusSize; + if ( endPage > pg ) { endPage = pg; } + // construct an array to get a focus set around the current page + for ( i = startPage; i <= endPage ; i++ ) { + focusOptionPages[ focusOptionPages.length ] = i; + } + + // keep unique values + optionPages = $.grep( optionPages, function( value, indx ) { + return $.inArray( value, optionPages ) === indx; + }); + + optionLength = optionPages.length; + focusLength = focusOptionPages.length; + + // make sure at all optionPages aren't replaced + if ( optionLength - focusLength > skipSetSize / 2 && optionLength + focusLength > wo.pager_maxOptionSize ) { + insertIndex = Math.floor( optionLength / 2 ) - Math.floor( focusLength / 2 ); + Array.prototype.splice.apply( optionPages, [ insertIndex, focusLength ] ); + } + optionPages = optionPages.concat( focusOptionPages ); + + } + + // keep unique values again + optionPages = $.grep( optionPages, function( value, indx ) { + return $.inArray( value, optionPages ) === indx; + }) + .sort( function( a, b ) { + return a - b; + }); + + return optionPages; + }, + + fixHeight: function( c ) { + var d, h, bs, + table = c.table, + p = c.pager, + wo = c.widgetOptions, + $b = c.$tbodies.eq( 0 ); + $b.find( 'tr.pagerSavedHeightSpacer' ).remove(); + if ( wo.pager_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( '<tr class="pagerSavedHeightSpacer ' + c.selectorRemove.slice( 1 ) + + '" style="height:' + d + 'px;"></tr>' ); + } + } + } + }, + + changeHeight: function( c ) { + var h, + table = c.table, + p = c.pager, + sz = p.size === 'all' ? p.totalRows : p.size, + $b = c.$tbodies.eq( 0 ); + $b.find( 'tr.pagerSavedHeightSpacer' ).remove(); + if ( !$b.children( 'tr:visible' ).length ) { + $b.append( '<tr class="pagerSavedHeightSpacer ' + c.selectorRemove.slice( 1 ) + '"><td> </td></tr>' ); + } + h = $b.children( 'tr' ).eq( 0 ).height() * sz; + $.data( table, 'pagerSavedHeight', h ); + tsp.fixHeight( c ); + $.data( table, 'pagerLastSize', p.size ); + }, + + hideRows: function( c ) { + if ( !c.widgetOptions.pager_ajaxUrl ) { + var tbodyIndex, rowIndex, $rows, len, lastIndex, + p = c.pager, + wo = c.widgetOptions, + tbodyLen = c.$tbodies.length, + sz = p.size === 'all' ? p.totalRows : p.size, + start = ( p.page * sz ), + end = start + sz, + last = -1, // for cache indexing + size = 0; // size counter + p.cacheIndex = []; + for ( tbodyIndex = 0; tbodyIndex < tbodyLen; tbodyIndex++ ) { + $rows = c.$tbodies.eq( tbodyIndex ).children( 'tr' ); + len = $rows.length; + lastIndex = 0; + last = -1; // for cache indexing + size = 0; // size counter + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + if ( !p.regexFiltered.test( $rows[ rowIndex ].className ) ) { + if ( size === start && $rows[ rowIndex ].className.match( c.cssChildRow ) ) { + // hide child rows @ start of pager (if already visible) + $rows[ rowIndex ].style.display = 'none'; + } else { + $rows[ rowIndex ].style.display = ( size >= start && size < end ) ? '' : 'none'; + if ( last !== size && size >= start && size < end ) { + p.cacheIndex[ p.cacheIndex.length ] = rowIndex; + last = size; + } + // don't count child rows + size += $rows[ rowIndex ].className + .match( c.cssChildRow + '|' + c.selectorRemove.slice( 1 ) ) && !wo.pager_countChildRows ? 0 : 1; + if ( size === end && $rows[ rowIndex ].style.display !== 'none' && + $rows[ rowIndex ].className.match( ts.css.cssHasChild ) ) { + lastIndex = rowIndex; + } + } + } + } + // 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 < len && $rows[ lastIndex ].className.match( c.cssChildRow ) ) { + $rows[ lastIndex ].style.display = ''; + } + } + } + } + }, + + hideRowsSetup: function( c ) { + var p = c.pager, + namespace = c.namespace + 'pager', + $el = p.$container.find( c.widgetOptions.pager_selectors.pageSize ), + size = $el.val(); + p.size = tsp.parsePageSize( c, size, 'get' ); + tsp.setPageSize( c, p.size ); + tsp.pagerArrows( c ); + if ( !c.widgetOptions.pager_removeRows ) { + tsp.hideRows( c ); + c.$table.on( 'sortEnd filterEnd '.split( ' ' ).join( namespace + ' ' ), function() { + tsp.hideRows( c ); + }); + } + }, + + renderAjax: function( data, c, xhr, settings, exception ) { + var table = c.table, + p = c.pager, + wo = c.widgetOptions, + debug = ts.debug(c, 'pager'); + // process data + if ( $.isFunction( wo.pager_ajaxProcessing ) ) { + + // 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 + c.$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, + $table = c.$table, + tds = '', + result = wo.pager_ajaxProcessing( data, table, xhr ) || [ 0, [] ]; + + // Clean up any previous error. + ts.showError( table ); + + if ( exception ) { + if ( debug ) { + 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 { + // 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 ( wo.pager_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 += '<tr>'; + for ( j = 0; j < d[i].length; j++ ) { + // build tbody cells; watch for data containing HTML markup - see #434 + tds += /^\s*<td/.test( d[ i ][ j ] ) ? $.trim( d[ i ][ j ] ) : '<td>' + d[ i ][ j ] + '</td>'; + } + tds += '</tr>'; + } + // add rows to first tbody + if ( wo.pager_processAjaxOnInit ) { + c.$tbodies.eq( 0 ).html( tds ); + } + } + wo.pager_processAjaxOnInit = true; + // update new header text + if ( th ) { + hsh = $table.hasClass( 'hasStickyHeaders' ); + $sh = hsh ? + wo.$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( wo.$sticky ); + $sh.eq( j ).find( '.' + ts.css.headerIn ).html( th[ j ] ); + } + } + $f.eq( j ).html( th[ j ] ); + } + if ( hsh ) { + tsp.bindEvents( c ); + } + } + } + if ( c.showProcessing ) { + ts.isProcessing( table ); // remove loading icon + } + sz = tsp.parsePageSize( c, 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( ',' ); + p.initializing = false; + // update display without triggering pager complete... before updating cache + tsp.updatePageDisplay( c, 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 ( debug ) { + console.log( 'Pager >> Triggering pagerChange' ); + } + $table.triggerHandler( 'pagerChange', p ); + ts.applyWidget( table ); + tsp.updatePageDisplay( c ); + }, 0 ); + } + }); + } + if ( !p.initialized ) { + ts.applyWidget( table ); + } + }, + + getAjax: function( c ) { + var counter, + url = tsp.getAjaxUrl( c ), + $doc = $( document ), + namespace = c.namespace + 'pager', + p = c.pager; + if ( url !== '' ) { + if ( c.showProcessing ) { + ts.isProcessing( c.table, true ); // show loading icon + } + $doc.on( 'ajaxError' + namespace, function( e, xhr, settings, exception ) { + tsp.renderAjax( null, c, xhr, settings, exception ); + $doc.off( '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; + } + tsp.renderAjax( data, c, jqxhr ); + $doc.off( '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( c ) { + var indx, len, + p = c.pager, + wo = c.widgetOptions, + url = wo.pager_ajaxUrl ? wo.pager_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 || c.$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 ( $.isFunction( wo.pager_customAjaxUrl ) ) { + url = wo.pager_customAjaxUrl( c.table, url ); + } + if ( ts.debug(c, 'pager') ) { + console.log( 'Pager >> Ajax url = ' + url ); + } + return url; + }, + + renderTable: function( c, rows ) { + var $tb, index, count, added, + table = c.table, + p = c.pager, + wo = c.widgetOptions, + 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 + return tsp.moveToLastPage( c, 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' ); + } + c.$table.triggerHandler( 'pagerChange', c ); + } + if ( !wo.pager_removeRows ) { + tsp.hideRows( c ); + } else { + 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 ); + } + tsp.updatePageDisplay( c ); + + wo.pager_startPage = p.page; + wo.pager_size = p.size; + if ( table.isUpdating ) { + if ( debug ) { + console.log( 'Pager >> Triggering updateComplete' ); + } + c.$table.triggerHandler( 'updateComplete', [ table, true ] ); + } + + }, + + showAllRows: function( c ) { + var index, $controls, len, + table = c.table, + p = c.pager, + wo = c.widgetOptions; + if ( p.ajax ) { + tsp.pagerArrows( c, true ); + } else { + $.data( table, 'pagerLastPage', p.page ); + $.data( table, 'pagerLastSize', p.size ); + p.page = 0; + p.size = p.totalRows; + p.totalPages = 1; + c.$table + .addClass( 'pagerDisabled' ) + .removeAttr( 'aria-describedby' ) + .find( 'tr.pagerSavedHeightSpacer' ) + .remove(); + tsp.renderTable( c, c.rowsCopy ); + p.isDisabled = true; + ts.applyWidget( table ); + if ( ts.debug(c, 'pager') ) { + console.log( 'Pager >> Disabled' ); + } + } + // disable size selector + $controls = p.$container.find( + wo.pager_selectors.pageSize + ',' + + wo.pager_selectors.gotoPage + ',' + + '.ts-startRow, .ts-page' + ); + len = $controls.length; + for ( index = 0; index < len; index++ ) { + $controls.eq( index ) + .prop( 'aria-disabled', 'true' ) + .addClass( wo.pager_css.disabled )[ 0 ].disabled = true; + } + }, + + // updateCache if delayInit: true + // this is normally done by 'appendToTable' function in the tablesorter core AFTER a sort + updateCache: function( c ) { + var p = c.pager; + // tablesorter core updateCache (not pager) + ts.updateCache( c, function() { + if ( !$.isEmptyObject( c.cache ) ) { + var index, + rows = [], + normalized = c.cache[ 0 ].normalized; + p.totalRows = normalized.length; + for ( index = 0; index < p.totalRows; index++ ) { + rows[ rows.length ] = normalized[ index ][ c.columns ].$row; + } + c.rowsCopy = rows; + tsp.moveToPage( c, p, true ); + // clear out last search to force an update + p.last.currentFilters = [ ' ' ]; + } + }); + }, + + moveToPage: function( c, p, pageMoved ) { + if ( p.isDisabled ) { return; } + if ( pageMoved !== false && p.initialized && $.isEmptyObject( c.cache ) ) { + return tsp.updateCache( c ); + } + var tmp, + table = c.table, + wo = c.widgetOptions, + l = p.last, + debug = ts.debug(c, 'pager'); + + // abort page move if the table has filters and has not been initialized + if ( p.ajax && !wo.filter_initialized && ts.hasWidget( table, 'filter' ) ) { return; } + + tsp.parsePageNumber( c, p ); + tsp.calcFilters( c ); + // fixes issue where one current filter is [] and the other is [ '', '', '' ], + // making the next if comparison think the filters as different. 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 || '' ) === ( wo.pager_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: wo.pager_ajaxUrl + }; + if ( p.ajax ) { + if ( !wo.pager_processAjaxOnInit && !$.isEmptyObject(wo.pager_initialRows) ) { + wo.pager_processAjaxOnInit = true; + tmp = wo.pager_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 ); + tsp.updatePageDisplay( c, false ); + } else { + tsp.getAjax( c ); + } + } else if ( !p.ajax ) { + tsp.renderTable( c, c.rowsCopy ); + } + $.data( table, 'pagerLastPage', p.page ); + if ( p.initialized && pageMoved !== false ) { + if ( debug ) { + console.log( 'Pager >> Triggering pageMoved' ); + } + c.$table.triggerHandler( 'pageMoved', c ); + ts.applyWidget( table ); + if ( !p.ajax && table.isUpdating ) { + if ( debug ) { + console.log( 'Pager >> Triggering updateComplete' ); + } + c.$table.triggerHandler( 'updateComplete', [ table, true ] ); + } + } + }, + + getTotalPages: function( c, p ) { + return ts.hasWidget( c.table, 'filter' ) ? + Math.min( p.totalPages, p.filteredPages ) : + p.totalPages; + }, + + parsePageNumber: function( c, p ) { + var min = tsp.getTotalPages( c, 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( c, size, mode ) { + var p = c.pager, + wo = c.widgetOptions, + s = parseInt( size, 10 ) || p.size || wo.pager_size || 10; + if (p.initialized && (/all/i.test( s + ' ' + size ) || s === p.totalRows)) { + // Fixing #1364 & #1366 + return p.$container.find( wo.pager_selectors.pageSize + ' option[value="all"]').length ? + 'all' : p.totalRows; + } + // "get" to set `p.size` or "set" to set `pageSize.val()` + return mode === 'get' ? s : p.size; + }, + + setPageSize: function( c, size ) { + var p = c.pager, + table = c.table; + // "all" size is only returned if an "all" option exists - fixes #1366 + p.size = tsp.parsePageSize( c, size, 'get' ); + p.$container + .find( c.widgetOptions.pager_selectors.pageSize ) + .val( p.size ); + $.data( table, 'pagerLastPage', tsp.parsePageNumber( c, 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( c, p ) { + p.page = 0; + tsp.moveToPage( c, p, true ); + }, + + moveToLastPage: function( c, p ) { + p.page = tsp.getTotalPages( c, p ) - 1; + tsp.moveToPage( c, p, true ); + }, + + moveToNextPage: function( c, p ) { + p.page++; + var last = tsp.getTotalPages( c, p ) - 1; + if ( p.page >= last ) { + p.page = last; + } + tsp.moveToPage( c, p, true ); + }, + + moveToPrevPage: function( c, p ) { + p.page--; + if ( p.page <= 0 ) { + p.page = 0; + } + tsp.moveToPage( c, p, true ); + }, + + destroyPager: function( c, refreshing ) { + var table = c.table, + p = c.pager, + s = c.widgetOptions.pager_selectors || {}, + ctrls = [ s.first, s.prev, s.next, s.last, s.gotoPage, s.pageSize ].join( ',' ), + namespace = c.namespace + 'pager'; + // check pager object in case two successive pager destroys are triggered + // e.g. "destroyPager" then "removeWidget" - see #1155 + if ( p ) { + p.initialized = false; + c.$table.off( namespace ); + p.$container + // hide pager + .hide() + // unbind pager controls + .find( ctrls ) + .off( namespace ); + if ( refreshing ) { return; } + c.appender = null; // remove pager appender function + tsp.showAllRows( c ); + if ( ts.storage ) { + ts.storage( table, c.widgetOptions.pager_storageKey, '' ); + } + p.$container = null; + c.pager = null; + c.rowsCopy = null; + } + }, + + enablePager: function( c, triggered ) { + var info, size, + table = c.table, + p = c.pager, + wo = c.widgetOptions, + $el = p.$container.find( wo.pager_selectors.pageSize ); + p.isDisabled = false; + p.page = $.data( table, 'pagerLastPage' ) || p.page || 0; + size = $el.find('option[selected]' ).val(); + p.size = $.data( table, 'pagerLastSize' ) || tsp.parsePageSize( c, size, 'get' ); + tsp.setPageSize( c, p.size ); // set page size + p.totalPages = p.size === 'all' ? 1 : Math.ceil( tsp.getTotalPages( c, p ) / p.size ); + c.$table.removeClass( 'pagerDisabled' ); + // if table id exists, include page display with aria info + if ( table.id && !c.$table.attr( 'aria-describedby' ) ) { + $el = p.$container.find( wo.pager_selectors.pageDisplay ); + 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 ); + } + tsp.changeHeight( c ); + if ( triggered ) { + // tablesorter core update table + ts.update( c ); + tsp.setPageSize( c, p.size ); + tsp.moveToPage( c, p, true ); + tsp.hideRowsSetup( c ); + if ( ts.debug(c, 'pager') ) { + console.log( 'Pager >> Enabled' ); + } + } + }, + + appender: function( table, rows ) { + var c = table.config, + wo = c.widgetOptions, + p = c.pager; + if ( !p.ajax ) { + c.rowsCopy = rows; + p.totalRows = wo.pager_countChildRows ? c.$tbodies.eq( 0 ).children( 'tr' ).length : rows.length; + p.size = $.data( table, 'pagerLastSize' ) || p.size || wo.pager_size || p.setSize || 10; + p.totalPages = p.size === 'all' ? 1 : Math.ceil( p.totalRows / p.size ); + tsp.moveToPage( c, p ); + // update display here in case all rows are removed + tsp.updatePageDisplay( c, false ); + } else { + tsp.moveToPage( c, p, true ); + } + } + + }; + + // 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 : + '<tr><td colspan="' + c.columns + '">' + message + '</td></tr>' + ) + .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' + }); + + }; + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-print.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-print.js new file mode 100644 index 0000000..1bdf3ad --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-print.js @@ -0,0 +1,154 @@ +/* Widget: print - updated 12/8/2016 (v2.28.1) *//* + * Requires tablesorter v2.8+ and jQuery 1.2.6+ + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + + printTable = ts.printTable = { + + event : 'printTable', + basicStyle : 'table, tr, td, th { border : solid 1px black; border-collapse : collapse; } td, th { padding: 2px; }', + popupStyle : 'width=500,height=300,scrollbars=1,resizable=1', + + init : function(c) { + c.$table + .unbind(printTable.event) + .bind(printTable.event, function() { + // explicitly use table.config.widgetOptions because we want + // the most up-to-date values; not the 'wo' from initialization + printTable.process(c, c.widgetOptions); + return false; + }); + }, + + process : function(c, wo) { + var $this, data, + $table = $('<div/>').append(c.$table.clone()), + printStyle = printTable.basicStyle + 'table { width: 100%; }' + + // hide filter row + '.' + ( ts.css.filterRow || 'tablesorter-filter-row' ) + + // hide filtered rows + ', .' + ( wo.filter_filteredRow || 'filtered' ) + ' { display: none; }' + + // hide sort arrows + '.' + ( ts.css.header || 'tablesorter-header' ) + ' { background-image: none !important; }' + + + '@media print { .print_widget_hidden { display: none; } }'; + + // replace content with data-attribute content + $table.find('[' + wo.print_dataAttrib + ']').each(function() { + $this = $(this); + $this.text( $this.attr(wo.print_dataAttrib) ); + }); + + // Make sure all lazy loaded images are visible - see #1169 + data = 'data-' + (wo.lazyload_data_attribute || 'original'); + $table.find('img[' + data + ']').each(function() { + $this = $(this); + $this.attr('src', $this.attr(data)); + }); + + // === rows === + // Assume 'visible' means rows hidden by the pager (rows set to 'display:none') + // or hidden by a class name which is added to the wo.print_extraCSS definition + // look for jQuery filter selector in wo.print_rows & use if found + if ( /^f/i.test( wo.print_rows ) ) { + printStyle += 'tbody tr:not(.' + ( wo.filter_filteredRow || 'filtered' ) + ') { display: table-row !important; }'; + } else if ( /^a/i.test( wo.print_rows ) ) { + // default force show of all rows + printStyle += 'tbody tr { display: table-row !important; }'; + } else if ( /^[.#:\[]/.test( wo.print_rows ) ) { + // look for '.' (class selector), '#' (id selector), + // ':' (basic filters, e.g. ':not()') or '[' (attribute selector start) + printStyle += 'tbody tr' + wo.print_rows + ' { display: table-row !important; }'; + } + + // === columns === + // columnSelector -> c.selector.$style + // Assume 'visible' means hidden columns have a 'display:none' style, or a class name + // add the definition to the wo.print_extraCSS option + if (/s/i.test(wo.print_columns) && c.selector && ts.hasWidget( c.table, 'columnSelector' )) { + // show selected (visible) columns; make a copy of the columnSelector widget css (not media queries) + printStyle += wo.columnSelector_mediaquery && c.selector.auto ? '' : c.selector.$style.text(); + } else if (/a/i.test(wo.print_columns)) { + // force show all cells + printStyle += 'td, th { display: table-cell !important; }'; + } + + printStyle += wo.print_extraCSS; + + // callback function + if ( $.isFunction(wo.print_callback) ) { + wo.print_callback( c, $table, printStyle ); + } else { + printTable.printOutput(c, $table.html(), printStyle); + } + + }, // end process + + printOutput : function(c, data, style) { + var wo = c.widgetOptions, + lang = ts.language, + generator = window.open( '', wo.print_title, printTable.popupStyle ), + t = wo.print_title || c.$table.find('caption').text() || c.$table[0].id || document.title || 'table', + button = wo.print_now ? '' : '<div class="print_widget_hidden"><a href="javascript:window.print();">' + + '<button type="button">' + lang.button_print + '</button></a> <a href="javascript:window.close();">' + + '<button type="button">' + lang.button_close + '</button></a><hr></div>'; + generator.document.write( + '<html><head><title>' + t + '</title>' + + ( wo.print_styleSheet ? '<link rel="stylesheet" href="' + wo.print_styleSheet + '">' : '' ) + + '<style>' + style + '</style>' + + '</head><body>' + button + data + '</body></html>' + ); + generator.document.close(); + // use timeout to allow browser to build DOM before printing + // Print preview in Chrome doesn't work without this code + if ( wo.print_now ) { + setTimeout( function() { + generator.print(); + generator.close(); + }, 10 ); + } + return true; + }, + + remove : function(c) { + c.$table.off(printTable.event); + } + + }; + + ts.language.button_close = 'Close'; + ts.language.button_print = 'Print'; + + ts.addWidget({ + id: 'print', + options: { + print_title : '', // this option > caption > table id > 'table' + print_dataAttrib : 'data-name', // header attrib containing modified header name + print_rows : 'filtered', // (a)ll, (v)isible, (f)iltered or custom css selector + print_columns : 'selected', // (a)ll, (v)isbible or (s)elected (if columnSelector widget is added) + print_extraCSS : '', // add any extra css definitions for the popup window here + print_styleSheet : '', // add the url of your print stylesheet + print_now : true, // Open the print dialog immediately if true + // callback executed when processing completes + // to continue printing, use the following function: + // function( config, $table, printStyle ) { + // // do something to the table or printStyle string + // $.tablesorter.printTable.printOutput( config, $table.html(), printStyle ); + // } + print_callback : null + }, + init: function(table, thisWidget, c) { + printTable.init(c); + }, + remove: function(table, c) { + printTable.remove(c); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-reflow.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-reflow.js new file mode 100644 index 0000000..4a32f34 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-reflow.js @@ -0,0 +1,179 @@ +/* Widget: reflow - updated 2/7/2015 (v2.19.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * Also, this widget requires the following default css (modify as desired) + + / * REQUIRED CSS: change your reflow breakpoint here (35em below) * / + @media ( max-width: 35em ) { + .ui-table-reflow td, + .ui-table-reflow th { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + float: right; + / * if not using the stickyHeaders widget (not the css3 version) + * the "!important" flag, and "height: auto" can be removed * / + width: 100% !important; + height: auto !important; + } + / * reflow widget * / + .ui-table-reflow tbody td[data-title]:before { + color: #469; + font-size: .9em; + content: attr(data-title); + float: left; + width: 50%; + white-space: pre-wrap; + text-align: bottom; + display: inline-block; + } + / * reflow2 widget * / + table.ui-table-reflow .ui-table-cell-label.ui-table-cell-label-top { + display: block; + padding: .4em 0; + margin: .4em 0; + text-transform: uppercase; + font-size: .9em; + font-weight: 400; + } + table.ui-table-reflow .ui-table-cell-label { + padding: .4em; + min-width: 30%; + display: inline-block; + margin: -.4em 1em -.4em -.4em; + } + } + .ui-table-reflow .ui-table-cell-label { + display: none; + } + + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + + tablereflow = { + // simple reflow + // add data-attribute to each cell which shows when media query is active + // this widget DOES NOT WORK on a table with multiple thead rows + init : function(table, c, wo) { + var $this, + title = wo.reflow_dataAttrib, + header = wo.reflow_headerAttrib, + headers = []; + c.$table + .addClass(wo.reflow_className) + .off('refresh.tsreflow updateComplete.tsreflow2') + // emulate jQuery Mobile refresh + // https://api.jquerymobile.com/table-reflow/#method-refresh + .on('refresh.tsreflow updateComplete.tsreflow2', function() { + tablereflow.init(table, c, wo); + }); + c.$headers.each(function() { + $this = $(this); + headers.push( $.trim( $this.attr(header) || $this.text() ) ); + }); + c.$tbodies.children().each(function() { + $(this).children().each(function(i) { + $(this).attr(title, headers[i]); + }); + }); + }, + init2: function(table, c, wo) { + var $this, $tbody, i, $hdr, txt, len, + cols = c.columns, + header = wo.reflow2_headerAttrib, + headers = []; + c.$table + .addClass(wo.reflow2_className) + .off('refresh.tsreflow2 updateComplete.tsreflow2') + // emulate jQuery Mobile refresh + // https://api.jquerymobile.com/table-reflow/#method-refresh + .on('refresh.tsreflow2 updateComplete.tsreflow2', function() { + tablereflow.init2(table, c, wo); + }); + + // add <b> to every table cell with thead cell contents + for (i = 0; i < cols; i++) { + $hdr = c.$headers.filter('[data-column="' + i + '"]'); + if ($hdr.length > 1) { + txt = []; + /*jshint loopfunc:true */ + $hdr.each(function() { + $this = $(this); + if (!$this.hasClass(wo.reflow2_classIgnore)) { + txt.push( $this.attr(header) || $this.text() ); + } + }); + } else { + txt = [ $hdr.attr(header) || $hdr.text() ]; + } + headers.push( txt ); + } + // include 'remove-me' class so these additional elements are removed before updating + txt = '<b class="' + c.selectorRemove.slice(1) + ' ' + wo.reflow2_labelClass; + c.$tbodies.children().each(function() { + $tbody = ts.processTbody(table, $(this), true); + $tbody.children().each(function(j) { + $this = $(this); + len = headers[j].length; + i = len - 1; + while (i >= 0) { + $this.prepend(txt + (i === 0 && len > 1 ? ' ' + wo.reflow2_labelTop : '') + '">' + headers[j][i] + '</b>'); + i--; + } + }); + ts.processTbody(table, $tbody, false); + }); + }, + remove : function(table, c, wo) { + c.$table.removeClass(wo.reflow_className); + }, + remove2 : function(table, c, wo) { + c.$table.removeClass(wo.reflow2_className); + } + }; + + ts.addWidget({ + id: 'reflow', + options: { + // class name added to make it responsive (class name within media query) + reflow_className : 'ui-table-reflow', + // header attribute containing modified header name + reflow_headerAttrib : 'data-name', + // data attribute added to each tbody cell + reflow_dataAttrib : 'data-title' + }, + init: function(table, thisWidget, c, wo) { + tablereflow.init(table, c, wo); + }, + remove: function(table, c, wo) { + tablereflow.remove(table, c, wo); + } + }); + + ts.addWidget({ + id: 'reflow2', + options: { + // class name added to make it responsive (class name within media query) + reflow2_className : 'ui-table-reflow', + // ignore header cell content with this class name + reflow2_classIgnore : 'ui-table-reflow-ignore', + // header attribute containing modified header name + reflow2_headerAttrib : 'data-name', + // class name applied to thead labels + reflow2_labelClass : 'ui-table-cell-label', + // class name applied to first row thead label + reflow2_labelTop : 'ui-table-cell-label-top' + }, + init: function(table, thisWidget, c, wo) { + tablereflow.init2(table, c, wo); + }, + remove: function(table, c, wo) { + tablereflow.remove2(table, c, wo); + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-repeatheaders.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-repeatheaders.js new file mode 100644 index 0000000..81e7844 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-repeatheaders.js @@ -0,0 +1,52 @@ +/*! Widget: repeatHeaders - updated 9/23/2016 (v2.27.7) *//* +* Requires tablesorter v2.8+ and jQuery 1.7+ +* Original by Christian Bach from the example-widgets.html demo +*/ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + $.tablesorter.addWidget({ + id: 'repeatHeaders', + priority: 10, + options: { + rowsToSkip : 4 + }, + // format is called on init and when a sorting has finished + format: function(table, c, wo) { + var h = '', i, $tr, l, skip; + // cache and collect all TH headers + if (!wo.repeatHeaders) { + h = '<tr class="repeated-header ' + c.selectorRemove.slice(1) + '">'; + for (i = 0; i < c.columns; i++) { + // repeat the content of the current header (including child elements) + h += '<th>' + $.trim( c.$headers.eq(i).html() ) + '</th>'; + } + // 'remove-me' class was added in case the table needs to be updated, the 'remove-me' rows will be + // removed prior to the update to prevent including the rows in the update - see 'selectorRemove' option + wo.repeatHeaders = h + '</tr>'; + } + + // number of rows to skip + skip = wo && wo.rowsToSkip || 4; + + // remove appended headers by classname + c.$table.find('tr.repeated-header').remove(); + $tr = c.$tbodies.find('tr'); + l = $tr.length; + // loop all tr elements and insert a copy of the 'headers' + for (i = skip; i < l; i += skip) { + // insert a copy of the table head every X rows + $tr.eq(i).before(wo.repeatHeaders); + } + }, + // this remove function is called when using the refreshWidgets method or when destroying the tablesorter plugin + // this function only applies to tablesorter v2.4+ + remove: function(table, c, wo) { + wo.repeatHeaders = ''; + c.$table.find('tr.repeated-header').remove(); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-resizable.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-resizable.js new file mode 100644 index 0000000..14ca65e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-resizable.js @@ -0,0 +1,415 @@ +/*! Widget: resizable - updated 2018-03-26 (v2.30.2) */ +/*jshint browser:true, jquery:true, unused:false */ +;(function ($, window) { + 'use strict'; + var ts = $.tablesorter || {}; + + $.extend(ts.css, { + resizableContainer : 'tablesorter-resizable-container', + resizableHandle : 'tablesorter-resizable-handle', + resizableNoSelect : 'tablesorter-disableSelection', + resizableStorage : 'tablesorter-resizable' + }); + + // Add extra scroller css + $(function() { + var s = '<style>' + + 'body.' + ts.css.resizableNoSelect + ' { -ms-user-select: none; -moz-user-select: -moz-none;' + + '-khtml-user-select: none; -webkit-user-select: none; user-select: none; }' + + '.' + ts.css.resizableContainer + ' { position: relative; height: 1px; }' + + // make handle z-index > than stickyHeader z-index, so the handle stays above sticky header + '.' + ts.css.resizableHandle + ' { position: absolute; display: inline-block; width: 8px;' + + 'top: 1px; cursor: ew-resize; z-index: 3; user-select: none; -moz-user-select: none; }' + + '</style>'; + $('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('<span>').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('<span>'); + } + */ + + 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 = $( '<div class="' + ts.css.resizableContainer + '">' ) + .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 ) { + $( '<div class="' + ts.css.resizableHandle + '">' ) + .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 ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-saveSort.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-saveSort.js new file mode 100644 index 0000000..5a71627 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-saveSort.js @@ -0,0 +1,82 @@ +/*! 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); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-scroller.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-scroller.js new file mode 100644 index 0000000..082b7a2 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-scroller.js @@ -0,0 +1,987 @@ +/*! Widget: scroller - updated 2018-05-07 (v2.30.4) *//* + Copyright (C) 2011 T. Connell & Associates, Inc. + + Dual-licensed under the MIT and GPL licenses + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Resizable scroller widget for the jQuery tablesorter plugin + + Version 2.0 - modified by Rob Garrison 4/12/2013; + updated 3/5/2015 (v2.22.2) with lots of help from TheSin- + Requires jQuery v1.7+ + Requires the tablesorter plugin, v2.8+, available at http://mottie.github.com/tablesorter/docs/ + + Usage: + $(function() { + $('table.tablesorter').tablesorter({ + widgets: ['zebra', 'scroller'], + widgetOptions : { + scroller_height : 300, // height of scroll window + scroller_jumpToHeader : true, // header snap to browser top when scrolling the tbody + scroller_upAfterSort : true, // scroll tbody to top after sorting + scroller_fixedColumns : 0 // set number of fixed columns + } + }); + }); + + Website: www.tconnell.com +*/ +/*jshint browser:true, jquery:true, unused:false */ +;( function( $, window ) { + 'use strict'; + + var ts = $.tablesorter, + tscss = ts.css; + + $.extend( ts.css, { + scrollerWrap : 'tablesorter-scroller', + scrollerHeader : 'tablesorter-scroller-header', + scrollerTable : 'tablesorter-scroller-table', + scrollerFooter : 'tablesorter-scroller-footer', + scrollerFixed : 'tablesorter-scroller-fixed', + scrollerFixedPanel : 'tablesorter-scroller-fixed-panel', + scrollerHasFix : 'tablesorter-scroller-has-fixed-columns', + scrollerHideColumn : 'tablesorter-scroller-hidden-column', + scrollerHideElement : 'tablesorter-scroller-hidden', + scrollerSpacerRow : 'tablesorter-scroller-spacer', + scrollerBarSpacer : 'tablesorter-scroller-bar-spacer', + scrollerAddedHeight : 'tablesorter-scroller-added-height', + scrollerHack : 'tablesorter-scroller-scrollbar-hack', + // class name on table cannot start with 'tablesorter-' or the + // suffix 'scroller-rtl' will match as a theme name + scrollerRtl : 'ts-scroller-rtl' + }); + + ts.addWidget({ + id : 'scroller', + priority : 60, // run after the filter widget + options : { + scroller_height : 300, + // pop table header into view while scrolling up the page + scroller_jumpToHeader : true, + // scroll tbody to top after sorting + scroller_upAfterSort : true, + // set number of fixed columns + scroller_fixedColumns : 0, + // add hover highlighting to the fixed column (disable if it causes slowing) + scroller_rowHighlight : 'hover', + // add a fixed column overlay for styling + scroller_addFixedOverlay : false, + // In tablesorter v2.19.0 the scroll bar width is auto-detected + // add a value here to override the auto-detected setting + scroller_barWidth : null + }, + format : function( table, c, wo ) { + if ( c.isScrolling ) { + ts.scroller.resize( c, wo ); + } else { + // initialize here instead of in widget init to give the + // filter widget time to finish building the filter row + ts.scroller.setup( c, wo ); + } + }, + remove : function( table, c, wo ) { + ts.scroller.remove( c, wo ); + } + }); + + /* Add window resizeEnd event (also used by columnSelector widget) */ + ts.window_resize = function() { + if ( ts.timer_resize ) { + clearTimeout( ts.timer_resize ); + } + ts.timer_resize = setTimeout( function() { + $( window ).trigger( 'resizeEnd' ); + }, 250 ); + }; + + // Add extra scroller css + $( function() { + var style = '<style>' + + /* overall wrapper & table section wrappers */ + '.' + tscss.scrollerWrap + ' { position: relative; overflow: hidden; }' + + /* add border-box sizing to all scroller widget tables; see #135 */ + '.' + tscss.scrollerWrap + ' * { box-sizing: border-box; }' + + '.' + tscss.scrollerHeader + ', .' + tscss.scrollerFooter + ' { position: relative; overflow: hidden; }' + + '.' + tscss.scrollerHeader + ' table.' + tscss.table + ' { margin-bottom: 0; }' + + /* always leave the scroll bar visible for tbody, or table overflows into the scrollbar + when height < max height (filtering) */ + '.' + tscss.scrollerTable + ' { position: relative; overflow: auto; }' + + '.' + tscss.scrollerTable + ' table.' + tscss.table + + ' { border-top: 0; margin-top: 0; margin-bottom: 0; overflow: hidden; max-width: initial; }' + + /* hide footer in original table */ + '.' + tscss.scrollerTable + ' tfoot, .' + tscss.scrollerHideElement + ', .' + tscss.scrollerHideColumn + + ' { display: none; }' + + + /*** fixed column ***/ + /* disable pointer-events on fixed column wrapper or the user can't interact with the horizontal scrollbar */ + '.' + tscss.scrollerFixed + ', .' + tscss.scrollerFixed + ' .' + tscss.scrollerFixedPanel + + ' { pointer-events: none; }' + + /* enable pointer-events for fixed column children; see #135 & #878 */ + '.' + tscss.scrollerFixed + ' > div { pointer-events: all; }' + + '.' + tscss.scrollerWrap + ' .' + tscss.scrollerFixed + ' { position: absolute; top: 0; z-index: 1; left: 0 } ' + + '.' + tscss.scrollerWrap + ' .' + tscss.scrollerFixed + '.' + tscss.scrollerRtl + ' { left: auto; right: 0 } ' + + /* add horizontal scroll bar; set to 'auto', see #135 */ + '.' + tscss.scrollerWrap + '.' + tscss.scrollerHasFix + ' > .' + tscss.scrollerTable + ' { overflow: auto; }' + + /* need to position the tbody & tfoot absolutely to hide the scrollbar & move the footer + below the horizontal scrollbar */ + '.' + tscss.scrollerFixed + ' .' + tscss.scrollerFooter + ' { position: absolute; bottom: 0; }' + + /* hide fixed tbody scrollbar - see http://goo.gl/VsLe6n - set overflow to auto here for mousewheel scroll */ + '.' + tscss.scrollerFixed + ' .' + tscss.scrollerTable + + ' { position: relative; left: 0; overflow: auto; -ms-overflow-style: none; }' + + '.' + tscss.scrollerFixed + ' .' + tscss.scrollerTable + '::-webkit-scrollbar { display: none; }' + + /*** fixed column panel ***/ + '.' + tscss.scrollerWrap + ' .' + tscss.scrollerFixedPanel + + ' { position: absolute; top: 0; bottom: 0; z-index: 2; left: 0; right: 0; } ' + + '</style>'; + $( 'head' ).append( style ); + }); + + ts.scroller = { + + // Ugh.. Firefox misbehaves, so it needs to be detected + isFirefox : navigator.userAgent.toLowerCase().indexOf( 'firefox' ) > -1, + // old IE needs a wrap to hide the fixed column scrollbar; http://stackoverflow.com/a/24408672/145346 + isOldIE : document.all && !window.atob, + isIE : ( document.all && !window.atob ) || navigator.appVersion.indexOf( 'Trident/' ) > 0, + // http://stackoverflow.com/questions/7944460/detect-safari-browser - needed to position scrolling body + // when the table is set up in RTL direction + isSafari : navigator.userAgent.toLowerCase().indexOf( 'safari' ) > -1 && + navigator.userAgent.toLowerCase().indexOf( 'chrome' ) === -1, + + hasScrollBar : function( $target, checkWidth ) { + if ( checkWidth ) { + return $target.get(0).scrollWidth > $target.width(); + } else { + return $target.get(0).scrollHeight > $target.height(); + } + }, + + setWidth : function( $el, width ) { + $el.css({ + 'width' : width, + 'min-width' : width, + 'max-width' : width + }); + }, + + // modified from http://davidwalsh.name/detect-scrollbar-width + getBarWidth : function() { + var $div = $( '<div>' ).css({ + 'position' : 'absolute', + 'top' : '-9999px', + 'left' : 0, + 'width' : '100px', + 'height' : '100px', + 'overflow' : 'scroll', + 'visibility' : 'hidden' + }).appendTo( 'body' ), + div = $div[0], + barWidth = div.offsetWidth - div.clientWidth; + $div.remove(); + return barWidth; + }, + + setup : function( c, wo ) { + var tbHt, $hdr, $t, $hCells, $tableWrap, events, tmp, detectedWidth, + $win = $( window ), + tsScroller = ts.scroller, + namespace = c.namespace + 'tsscroller', + $foot = $(), + // c.namespace contains a unique tablesorter ID, per table + id = c.namespace.slice( 1 ) + 'tsscroller', + $table = c.$table; + + // force config.widthFixed option - this helps maintain proper alignment across cloned tables + c.widthFixed = true; + + wo.scroller_calcWidths = []; + wo.scroller_saved = [ 0, 0 ]; + wo.scroller_isBusy = true; + wo.scroller_scrollTimer = null; + + // set scrollbar width to one of the following (1) explicitly set scroller_barWidth option, + // (2) detected scrollbar width or (3) fallback of 15px + if ( wo.scroller_barWidth !== null ) { + wo.scroller_barSetWidth = wo.scroller_barWidth; + } else { + detectedWidth = tsScroller.getBarWidth(); + wo.scroller_barSetWidth = detectedWidth !== null ? detectedWidth : 15; + } + + tmp = $table.children( 'caption' ); + + $hdr = $( '<table class="' + $table.attr( 'class' ) + '" cellpadding=0 cellspacing=0>' + + ( tmp.length ? tmp[ 0 ].outerHTML : '' ) + + $table.children( 'thead' )[ 0 ].outerHTML + '</table>' ); + wo.scroller_$header = $hdr.addClass( c.namespace.slice( 1 ) + '_extra_table' ); + + $t = $table.children( 'tfoot' ); + if ( $t.length ) { + $foot = $( '<table class="' + $table.attr( 'class' ) + + '" cellpadding=0 cellspacing=0 style="margin-top:0"></table>' ) + .addClass( c.namespace.slice( 1 ) + '_extra_table' ) + // maintain any bindings on the tfoot cells + .append( $t.clone( true ) ) + .wrap( '<div class="' + tscss.scrollerFooter + '"/>' ); + } + wo.scroller_$footer = $foot; + + $table + .wrap( '<div id="' + id + '" class="' + tscss.scrollerWrap + '" />' ) + .before( $hdr ) + // shrink filter row but don't completely hide it because the inputs/selectors may distort the columns + .find( '.' + tscss.filterRow ) + .addClass( tscss.filterRowHide ); + + wo.scroller_$container = $table.parent(); + + if ( $foot.length ) { + // $foot.parent() to include <div> wrapper + $table.after( $foot.parent() ); + } + + $hCells = $hdr + .wrap( '<div class="' + tscss.scrollerHeader + '" />' ) + .find( '.' + tscss.header ); + + // if max-height is greater than 0 use max-height, so the height resizes dynamically while filtering + // else let the table not have a vertical scroll + $table.wrap( '<div class="' + tscss.scrollerTable + + ( wo.scroller_height > 0 ? '" style="max-height:' + wo.scroller_height + 'px;">' : '">' ) ); + $tableWrap = $table.parent(); + + // make scroller header sortable + ts.bindEvents( c.table, $hCells ); + + // look for filter widget + if ( $table.hasClass( 'hasFilters' ) ) { + ts.filter.bindSearch( $table, $hdr.find( '.' + tscss.filter ) ); + } + + $table + .children( 'thead, caption' ) + .addClass( tscss.scrollerHideElement ); + tbHt = $tableWrap.parent().height(); + + // The header will always jump into view if scrolling the table body + $tableWrap + .off( 'scroll' + namespace ) + .on( 'scroll' + namespace, function() { + // Save position + clearTimeout(wo.scroller_scrollTimer); + wo.scroller_scrollTimer = setTimeout(function() { + wo.scroller_saved[0] = $tableWrap.scrollLeft(); + wo.scroller_saved[1] = $tableWrap.scrollTop(); + }, 300); + if ( wo.scroller_jumpToHeader ) { + var pos = $win.scrollTop() - $hdr.offset().top; + if ( $( this ).scrollTop() !== 0 && pos < tbHt && pos > 0 ) { + $win.scrollTop( $hdr.offset().top ); + } + } + $hdr + .parent() + .add( $foot.parent() ) + .scrollLeft( $( this ).scrollLeft() ); + }); + + // resize/update events - filterEnd fires after "tablesorter-initialized" and "updateComplete" + tmp = ts.hasWidget( c.table, 'filter' ) ? + 'filterEnd filterInit' : + 'tablesorter-initialized updateComplete'; + events = ( tmp + ' sortEnd pagerComplete columnUpdate ' ).split( ' ' ).join( namespace + ' ' ); + + $table + .off( namespace ) + .on( 'sortStart' + namespace, function() { + clearTimeout(wo.scroller_scrollTimer); + wo.scroller_isBusy = true; + }) + .on( 'sortEnd filterEnd'.split( ' ' ).join( namespace + ' ' ), function( event ) { + // Sorting, so scroll to top + if ( event.type === 'sortEnd' && wo.scroller_upAfterSort ) { + $tableWrap + .scrollLeft( wo.scroller_saved[0] ) + .animate({ scrollTop : 0 }, 'fast', function() { + wo.scroller_isBusy = false; + }); + } else if ( wo.scroller_fixedColumns ) { + setTimeout( function() { + // restore previous scroll position + $tableWrap + .scrollTop( wo.scroller_saved[1] ) + .scrollLeft( wo.scroller_saved[0] ); + tsScroller.updateFixed( c, wo ); + }, 0 ); + } + }) + .on( 'setFixedColumnSize' + namespace, function( event, size ) { + var $wrap = wo.scroller_$container; + if ( typeof size !== 'undefined' && !isNaN( size ) ) { + wo.scroller_fixedColumns = parseInt( size, 10 ); + } + // remove fixed columns + tsScroller.removeFixed( c, wo ); + size = wo.scroller_fixedColumns; + if ( size > 0 && size < c.columns - 1 ) { + tsScroller.updateFixed( c, wo ); + } else if ( $wrap.hasClass( tscss.scrollerHasFix ) ) { + $wrap.removeClass( tscss.scrollerHasFix ); + // resize needed to make tables full width + tsScroller.resize( c, wo ); + } + }) + .on( events, function( event ) { + // Stop from running twice with pager + if ( ts.hasWidget( 'pager' ) && event.type === 'updateComplete' ) { + return; + } + if ( wo.scroller_fixedColumns > 0 ) { + tsScroller.updateFixed( c, wo ); + } + // adjust column sizes after an update + tsScroller.resize( c, wo ); + }); + + // Setup window.resizeEnd event + $win + .off( 'resize resizeEnd '.split( ' ' ).join( namespace + ' ' ) ) + .on( 'resize' + namespace, ts.window_resize ) + .on( 'resizeEnd' + namespace, function() { + // IE calls resize when you modify content, so we have to unbind the resize event + // so we don't end up with an infinite loop. we can rebind after we're done. + $win.off( 'resize' + namespace, ts.window_resize ); + tsScroller.resize( c, wo ); + $win.on( 'resize' + namespace, ts.window_resize ); + $tableWrap.trigger( 'scroll' + namespace ); + }); + + // initialization flag + c.isScrolling = true; + + tsScroller.updateFixed( c, wo ); + + // updateAll called - need to give the browser time to adjust the layout + // before calculating fix column widths + if ( c.table.hasInitialized && c.isScrolling ) { + setTimeout(function() { + ts.scroller.resize( c, wo ); + }, 50); + } + + }, + + resize : function( c, wo ) { + if ( wo.scroller_isBusy ) { return; } + var index, borderWidth, setWidth, $headers, $this, + tsScroller = ts.scroller, + $container = wo.scroller_$container, + $table = c.$table, + $tableWrap = $table.parent(), + $hdr = wo.scroller_$header, + $foot = wo.scroller_$footer, + $win = $(window), + position = [ $win.scrollLeft(), $win.scrollTop() ], + id = c.namespace.slice( 1 ) + 'tsscroller', + // Hide other scrollers so we can resize + $div = $( 'div.' + tscss.scrollerWrap + '[id!="' + id + '"]' ) + .addClass( tscss.scrollerHideElement ), + temp = 'padding:0;margin:0;border:0;height:0;max-height:0;min-height:0;', + row = '<tr class="' + tscss.scrollerSpacerRow + ' ' + c.selectorRemove.slice(1) + + '" style="' + temp + '">'; + + wo.scroller_calcWidths = []; + + // Remove fixed so we get proper widths and heights + tsScroller.removeFixed( c, wo ); + $container.find( '.' + tscss.scrollerSpacerRow ).remove(); + // remove ts added colgroups + $container.find( '.' + ts.css.colgroup ).remove(); + + // show original table elements to get proper alignment + $table + .find( '.' + tscss.scrollerHideElement ) + .removeClass( tscss.scrollerHideElement ); + + // include left & right border widths + borderWidth = parseInt( $table.css( 'border-left-width' ), 10 ); + + $headers = c.$headerIndexed; + + for ( index = 0; index < c.columns; index++ ) { + $this = $headers[ index ]; + // code from https://github.com/jmosbech/StickyTableHeaders + if ( $this.css( 'box-sizing' ) === 'border-box' ) { + setWidth = $this.outerWidth(); + } else { + if ( $this.css( 'border-collapse' ) === 'collapse' ) { + if ( $this.length && window.getComputedStyle ) { + setWidth = parseFloat( window.getComputedStyle( $this[ 0 ], null ).width ); + } else { + // ie8 only + setWidth = $this.outerWidth() - parseFloat( $this.css( 'padding-left' ) ) - + parseFloat( $this.css( 'padding-right' ) ) - + ( parseFloat( $this.css( 'border-width' ) ) || 0 ); + } + } else { + setWidth = $this.width(); + } + } + row += '<td data-column="' + index + '" style="' + temp + 'width:' + setWidth + + 'px;min-width:' + setWidth + 'px;max-width:' + setWidth + 'px"></td>'; + + // save current widths + wo.scroller_calcWidths[ index ] = setWidth; + } + row += '</tr>'; + c.$tbodies.eq(0).append( row ); // tbody + $hdr.children( 'thead' ).append( row ); + $foot.children( 'tfoot' ).append( row ); + + // include colgroup or alignment is off + ts.fixColumnWidth( c.table ); + row = c.$table.children( 'colgroup' )[0].outerHTML; + $hdr.append( row ); + $foot.append( row ); + + temp = $tableWrap.parent().innerWidth() - + ( tsScroller.hasScrollBar( $tableWrap ) ? wo.scroller_barSetWidth : 0 ); + $tableWrap.width( temp ); + + temp = ( tsScroller.hasScrollBar( $tableWrap ) ? wo.scroller_barSetWidth : 0 ) + borderWidth; + setWidth = $tableWrap.innerWidth() - temp; + + $hdr + .parent() + .add( $foot.parent() ) + .width( setWidth ); + + $tableWrap + .width( setWidth + temp ); + + // hide original table thead + $table.children( 'thead, caption' ).addClass( tscss.scrollerHideElement ); + + // update fixed column sizes + tsScroller.updateFixed( c, wo ); + + $div.removeClass( tscss.scrollerHideElement ); + + // restore scrollTop - fixes #926 + $tableWrap.scrollTop( wo.scroller_saved[1] ); + wo.scroller_$container + .find( '.' + tscss.scrollerFixed ) + .find( '.' + tscss.scrollerTable ) + .scrollTop( wo.scroller_saved[1] ); + $win.scrollLeft( position[0] ); + $win.scrollTop( position[1] ); + + // update resizable widget handles + setTimeout( function() { + c.$table.triggerHandler( 'resizableUpdate' ); + c.$table.triggerHandler( 'scrollerComplete' ); + }, 100 ); + + }, + + // Add fixed (frozen) columns (Do not call directly, use updateFixed) + setupFixed : function( c, wo ) { + var index, index2, $el, len, temp, $fixedColumn, $fixedTbody, + $table = c.$table, + $wrapper = wo.scroller_$container, + fixedColumns = wo.scroller_fixedColumns; + + $fixedColumn = $wrapper + .addClass( tscss.scrollerHasFix ) + .clone() + .addClass( tscss.scrollerFixed ) + .removeClass( tscss.scrollerWrap ) + .attr( 'id', '' ); + + $fixedColumn.find('caption').html(' '); + + if ( wo.scroller_addFixedOverlay ) { + $fixedColumn.append( '<div class="' + tscss.scrollerFixedPanel + '">' ); + } + + $fixedTbody = $fixedColumn.find( '.' + tscss.scrollerTable ); + $fixedTbody + .children( 'table' ) + .addClass( c.namespace.slice( 1 ) + '_extra_table' ) + .attr( 'id', '' ) + .children( 'thead, tfoot' ) + .remove(); + + wo.scroller_$fixedColumns = $fixedColumn; + + // RTL support (fixes column on right) + if ( $table.hasClass( tscss.scrollerRtl ) ) { + $fixedColumn.addClass( tscss.scrollerRtl ); + } + + $el = $fixedColumn.find( 'tr' ); + len = $el.length; + for ( index = 0; index < len; index++ ) { + $el.eq( index ).children( ':gt(' + ( fixedColumns - 1 ) + ')' ).remove(); + } + $fixedColumn + .addClass( tscss.scrollerHideElement ) + .prependTo( $wrapper ); + + // look for filter widget + if ( c.$table.hasClass( 'hasFilters' ) ) { + // make sure fixed column filters aren't disabled + $el = $fixedColumn + .find( '.' + tscss.filter ) + .not( '.' + tscss.filterDisabled ) + .prop( 'disabled', false ); + ts.filter.bindSearch( $table, $fixedColumn.find( '.' + tscss.filter ) ); + // disable/enable filters behind fixed column + $el = $wrapper + .children( '.' + tscss.scrollerHeader ) + .find( '.' + tscss.filter ); + len = $el.length; + for ( index = 0; index < len; index++ ) { + // previously disabled filter; don't mess with it! filterDisabled class added by filter widget + if ( !$el.eq( index ).hasClass( tscss.filterDisabled || 'disabled' ) ) { + // disable filters behind fixed column; don't disable visible filters + $el.eq( index ).prop( 'disabled', index < fixedColumns ); + } + } + } + + // disable/enable tab indexes behind fixed column + c.$table + .add( '.' + tscss.scrollerFooter + ' table' ) + .children( 'thead' ) + .children( 'tr.' + tscss.headerRow ) + .children() + .attr( 'tabindex', -1 ); + + $el = wo.scroller_$header + .add( $fixedColumn.find( '.' + tscss.scrollerTable + ' table' ) ) + .children( 'thead' ) + .children( 'tr.' + tscss.headerRow ); + len = $el.length; + for ( index = 0; index < len; index++ ) { + temp = $el.eq( index ).children(); + for ( index2 = 0; index2 < temp.length; index2++ ) { + temp.eq( index2 ).attr( 'tabindex', index2 < fixedColumns ? -1 : 0 ); + } + } + + ts.bindEvents( c.table, $fixedColumn.find( '.' + tscss.header ) ); + ts.scroller.bindFixedColumnEvents( c, wo ); + + /*** Scrollbar hack! Since we can't hide the scrollbar with css ***/ + if ( ts.scroller.isFirefox || ts.scroller.isOldIE ) { + $fixedTbody.wrap( '<div class="' + tscss.scrollerHack + '" style="overflow:hidden;">' ); + } + + }, + + // https://remysharp.com/2010/07/21/throttling-function-calls + throttle : function(fn, threshhold, scope) { + threshhold = threshhold || 50; + var last, deferTimer; + return function() { + var context = scope || this, + now = +(new Date()), + args = arguments; + if (last && now < last + threshhold) { + // hold on to it + clearTimeout(deferTimer); + deferTimer = setTimeout(function() { + last = now; + fn.apply(context, args); + }, threshhold); + } else { + last = now; + fn.apply(context, args); + } + }; + }, + + bindFixedColumnEvents : function( c, wo ) { + // update thead & tbody in fixed column + var tsScroller = ts.scroller, + namespace = c.namespace + 'tsscrollerFixed', + events = 'scroll' + namespace, + $fixedTbody = wo.scroller_$fixedColumns.find( '.' + tscss.scrollerTable ), + fixedScroll = true, + tableScroll = true; + + c.$table + .parent() + // *** SCROLL *** scroll fixed column along with main + .off( events ) + .on( events, tsScroller.throttle(function() { + // using flags to prevent firing the scroll event excessively leading to slow scrolling in Firefox + if ( !wo.scroller_isBusy && fixedScroll ) { + tableScroll = false; + var $this = $( this ); + $fixedTbody[0].scrollTop = wo.scroller_saved[1] = $this.scrollTop(); + wo.scroller_saved[0] = $this.scrollLeft(); + setTimeout( function() { + tableScroll = true; + }, 20 ); + } + })); + // scroll main along with fixed column + $fixedTbody + .off( events ) + .on( events, tsScroller.throttle(function() { + // using flags to prevent firing the scroll event excessively leading to slow scrolling in Firefox + if ( !wo.scroller_isBusy && tableScroll ) { + fixedScroll = false; + c.$table.parent()[0].scrollTop = wo.scroller_saved[1] = $( this ).scrollTop(); + setTimeout( function() { + fixedScroll = true; + }, 20 ); + } + })) + .scroll(); + + // *** ROW HIGHLIGHT *** + if ( wo.scroller_rowHighlight !== '' ) { + events = 'mouseover mouseleave '.split( ' ' ).join( namespace + ' ' ); + // can't use c.$tbodies because it doesn't include info-only tbodies + c.$table + .off( events, 'tbody > tr' ) + .on( events, 'tbody > tr', function( event ) { + var indx = c.$table.children( 'tbody' ).children( 'tr' ).index( this ); + $fixedTbody + .children( 'table' ) + .children( 'tbody' ) + .children( 'tr' ) + .eq( indx ) + .add( this ) + .toggleClass( wo.scroller_rowHighlight, event.type === 'mouseover' ); + }); + $fixedTbody + .find( 'table' ) + .off( events, 'tbody > tr' ) + .on( events, 'tbody > tr', function( event ) { + var $fixed = $fixedTbody.children( 'table' ).children( 'tbody' ).children( 'tr' ), + indx = $fixed.index( this ); + c.$table + .children( 'tbody' ) + .children( 'tr' ) + .eq( indx ) + .add( this ) + .toggleClass( wo.scroller_rowHighlight, event.type === 'mouseover' ); + }); + } + }, + + adjustWidth : function( c, wo, totalWidth, adj, dir ) { + var $wrapper = wo.scroller_$container; + + // RTL support (fixes column on right) + $wrapper + .children( '.' + tscss.scrollerTable ) + .css( dir ? 'right' : 'left', totalWidth ); + $wrapper + .children( '.' + tscss.scrollerHeader + ', .' + tscss.scrollerFooter ) + // Safari needs a scrollbar width of extra adjusment to align the fixed & scrolling columns + .css( dir ? 'right' : 'left', totalWidth + ( dir && ts.scroller.isSafari ? adj : 0 ) ); + }, + + updateFixed : function( c, wo ) { + var temp, adj, + $wrapper = wo.scroller_$container, + $hdr = wo.scroller_$header, + $foot = wo.scroller_$footer, + $table = c.$table, + $tableWrap = $table.parent(), + scrollBarWidth = wo.scroller_barSetWidth, + dir = $table.hasClass( tscss.scrollerRtl ); + + if ( wo.scroller_fixedColumns === 0 ) { + wo.scroller_isBusy = false; + ts.scroller.removeFixed( c, wo ); + temp = $wrapper.width(); + $tableWrap.width( temp ); + adj = ts.scroller.hasScrollBar( $tableWrap ) ? scrollBarWidth : 0; + $hdr + .parent() + .add( $foot.parent() ) + .width( temp - adj ); + return; + } + + if ( !c.isScrolling ) { + return; + } + + wo.scroller_isBusy = true; + + // Make sure the wo.scroller_$fixedColumns container exists, if not build it + if ( !$wrapper.find( '.' + tscss.scrollerFixed ).length ) { + ts.scroller.setupFixed( c, wo ); + } + + // scroller_fixedColumns + var index, tbodyIndex, rowIndex, $tbody, $adjCol, $fb, totalRows, + + // source cells for measurement + $mainTbodies = wo.scroller_$container + .children( '.' + tscss.scrollerTable ) + .children( 'table' ) + .children( 'tbody' ), + // variable gets redefined + $rows = wo.scroller_$header + .children( 'thead' ) + .children( '.' + tscss.headerRow ), + + // hide fixed column during resize, or we get a FOUC + $fixedColumn = wo.scroller_$fixedColumns + .addClass( tscss.scrollerHideElement ), + + // target cells + $fixedTbodiesTable = $fixedColumn + .find( '.' + tscss.scrollerTable ) + .children( 'table' ), + $fixedTbodies = $fixedTbodiesTable + .children( 'tbody' ), + // variables + tsScroller = ts.scroller, + fixedColumns = wo.scroller_fixedColumns, + // get dimensions + getDim = function ($el, name, deflt) { + return parseInt( $el.css(name) || '', 10 ) || deflt || 0; + }, + $temp = $table.find( 'tbody td' ), + borderRightWidth = getDim( $temp, 'border-right-width', 1 ), + borderSpacing = getDim( $temp, 'border-spacing', 0 ), + totalWidth = getDim( $table, 'padding-left' ) + + getDim( $table, 'padding-right' ) + + // include table left & row left border + getDim( $table, 'border-left-width', 1 ) * 2 + + getDim( $table, 'border-right-width', 1 ) - + borderRightWidth + borderSpacing / 2, + widths = wo.scroller_calcWidths; + + ts.scroller.removeFixed( c, wo, false ); + + // calculate fixed column width + for ( index = 0; index < fixedColumns; index++ ) { + totalWidth += widths[ index ] + borderSpacing; + } + + // set fixed column width + totalWidth = totalWidth + borderRightWidth * 2; + tsScroller.setWidth( $fixedColumn.add( $fixedColumn.children() ), totalWidth ); + tsScroller.setWidth( $fixedColumn.children().children( 'table' ), totalWidth ); + + // update fixed column tbody content, set cell widths on hidden row + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + $tbody = $mainTbodies.eq( tbodyIndex ); + if ( $tbody.length ) { + // get tbody + $rows = $tbody.children(); + totalRows = $rows.length; + $fb = ts.processTbody( $fixedTbodiesTable, $fixedTbodies.eq( tbodyIndex ), true ); + $fb.empty(); + // update tbody cells after sort/filtering + for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) { + $adjCol = $( $rows[ rowIndex ].outerHTML ); + $adjCol + .children( 'td, th' ) + .slice( fixedColumns ) + .remove(); + $fb.append( $adjCol ); + } + + // restore tbody + ts.processTbody( $fixedTbodiesTable, $fb, false ); + } + } + + adj = ts.scroller.hasScrollBar( $tableWrap ) ? scrollBarWidth : 0; + + /*** scrollbar HACK! Since we can't hide the scrollbar with css ***/ + if ( tsScroller.isFirefox || tsScroller.isOldIE ) { + $fixedTbodiesTable + .css( 'width', totalWidth ) + .parent() + .css( 'width', totalWidth + adj ); + } + + for ( index = 0; index < fixedColumns; index++ ) { + temp = ':nth-child(' + ( index + 1 ) + ')'; + $wrapper + .children( 'div' ) + .children( 'table' ) + .find( 'th' + temp + ', td' + temp + ', col' + temp ) + .addClass( tscss.scrollerHideColumn ); + } + $fixedColumn + .removeClass( tscss.scrollerHideElement ) + .find('colgroup') + .each(function() { + $(this) + .find('col:gt(' + (fixedColumns - 1) + ')') + .addClass( tscss.scrollerHideElement ); + }); + + totalWidth = totalWidth - borderRightWidth; + temp = $tableWrap.parent().innerWidth() - totalWidth; + $tableWrap.width( temp ); + // RTL support (fixes column on right) + $wrapper + .children( '.' + tscss.scrollerTable ) + .css( dir ? 'right' : 'left', totalWidth ); + $wrapper + .children( '.' + tscss.scrollerHeader + ', .' + tscss.scrollerFooter ) + // Safari needs a scrollbar width of extra adjusment to align the fixed & scrolling columns + .css( dir ? 'right' : 'left', totalWidth + ( dir && ts.scroller.isSafari ? adj : 0 ) ); + + $hdr + .parent() + .add( $foot.parent() ) + .width( temp - adj ); + + // fix gap under the tbody for the horizontal scrollbar + temp = ts.scroller.hasScrollBar( $tableWrap, true ); + adj = temp ? scrollBarWidth : 0; + if ( !$fixedColumn.find( '.' + tscss.scrollerBarSpacer ).length && temp ) { + $temp = $( '<div class="' + tscss.scrollerBarSpacer + '">' ) + .css( 'height', adj + 'px' ); + $fixedColumn.find( '.' + tscss.scrollerTable ).append( $temp ); + } else if ( !temp ) { + $fixedColumn.find( '.' + tscss.scrollerBarSpacer ).remove(); + } + + ts.scroller.updateRowHeight( c, wo ); + // set fixed column height (changes with filtering) + $fixedColumn.height( $wrapper.height() ); + + $fixedColumn.removeClass( tscss.scrollerHideElement ); + + // adjust caption height, see #1202 + $fixedColumn.find('caption').height( wo.scroller_$header.find( 'caption' ).height() ); + + $tableWrap.scroll(); + setTimeout(function() { + wo.scroller_isBusy = false; + }, 0); + + }, + + fixHeight : function( $rows, $fixedRows ) { + var index, heightRow, heightFixed, $r, $f, + addedHt = tscss.scrollerAddedHeight, + len = $rows.length; + for ( index = 0; index < len; index++ ) { + $r = $rows.eq( index ); + $f = $fixedRows.eq( index ); + heightRow = $r.height(); + heightFixed = $f.height(); + if ( heightRow > heightFixed ) { + $f.addClass( addedHt ).height( heightRow ); + } else if ( heightRow < heightFixed ) { + $r.addClass( addedHt ).height( heightFixed ); + } + } + }, + + updateRowHeight : function( c, wo ) { + var $rows, $fixed, + $fixedColumns = wo.scroller_$fixedColumns; + + wo.scroller_$container + .find( '.' + tscss.scrollerAddedHeight ) + .removeClass( tscss.scrollerAddedHeight ) + .height( '' ); + + $rows = wo.scroller_$header + .children( 'thead' ) + .children( 'tr' ); + $fixed = $fixedColumns + .children( '.' + tscss.scrollerHeader ) + .children( 'table' ) + .children( 'thead' ) + .children( 'tr' ); + ts.scroller.fixHeight( $rows, $fixed ); + + $rows = wo.scroller_$footer + .children( 'tfoot' ) + .children( 'tr' ); + $fixed = $fixedColumns + .children( '.' + tscss.scrollerFooter ) + .children( 'table' ) + .children( 'tfoot' ) + .children( 'tr' ); + ts.scroller.fixHeight( $rows, $fixed ); + + if ( ts.scroller.isFirefox || ts.scroller.isOldIE ) { + // Firefox/Old IE scrollbar hack (wraps table to hide the scrollbar) + $fixedColumns = $fixedColumns.find( '.' + tscss.scrollerHack ); + } + $rows = c.$table + .children( 'tbody' ) + .children( 'tr' ); + $fixed = $fixedColumns + .children( '.' + tscss.scrollerTable ) + .children( 'table' ) + .children( 'tbody' ) + .children( 'tr' ); + ts.scroller.fixHeight( $rows, $fixed ); + + }, + + removeFixed : function( c, wo, removeIt ) { + var $table = c.$table, + $wrapper = wo.scroller_$container, + dir = $table.hasClass( tscss.scrollerRtl ); + + // remove fixed columns + if ( removeIt || typeof removeIt === 'undefined' ) { + $wrapper.find( '.' + tscss.scrollerFixed ).remove(); + } + + $wrapper + .find( '.' + tscss.scrollerHideColumn ) + .removeClass( tscss.scrollerHideColumn ); + + // RTL support ( fixes column on right ) + $wrapper + .children( ':not(.' + tscss.scrollerFixed + ')' ) + .css( dir ? 'right' : 'left', 0 ); + }, + + remove : function( c, wo ) { + var $wrap = wo.scroller_$container, + namespace = c.namespace + 'tsscroller'; + c.$table.off( namespace ); + $( window ).off( namespace ); + if ( $wrap ) { + c.$table + .insertBefore( $wrap ) + .find( 'thead' ) + .removeClass( tscss.scrollerHideElement ) + .children( 'tr.' + tscss.headerRow ) + .children() + .attr( 'tabindex', 0 ) + .end() + .find( '.' + tscss.filterRow ) + .removeClass( tscss.scrollerHideElement + ' ' + tscss.filterRowHide ); + c.$table + .find( '.' + tscss.filter ) + .not( '.' + tscss.filterDisabled ) + .prop( 'disabled', false ); + $wrap.remove(); + c.isScrolling = false; + } + } + + }; + +})( jQuery, window ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sort2Hash.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sort2Hash.js new file mode 100644 index 0000000..303b8cb --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sort2Hash.js @@ -0,0 +1,278 @@ +/*! Widget: sort2Hash (BETA) - updated 9/27/2017 (v2.29.0) */ +/* Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter || {}, + s2h = ts.sort2Hash = { + init : function( c, wo ) { + var filter, temp, page, size, + table = c.table, + pager = c.pager, + hasSaveSort = ts.hasWidget( table, 'saveSort' ), + sort = s2h.decodeHash( c, wo, 'sort' ); + if ( ( sort && !hasSaveSort ) || ( sort && hasSaveSort && wo.sort2Hash_overrideSaveSort ) ) { + s2h.convertString2Sort( c, wo, sort ); + } + if ( ts.hasWidget( c.table, 'pager' ) ) { + temp = parseInt( s2h.decodeHash( c, wo, 'page' ), 10 ); + page = pager.page = ( temp < 0 ? 0 : ( temp > pager.totalPages ? pager.totalPages - 1 : temp ) ); + size = pager.size = parseInt( s2h.decodeHash( c, wo, 'size' ), 10 ); + } + if ( ts.hasWidget( table, 'filter' ) ) { + filter = s2h.decodeHash( c, wo, 'filter' ); + if ( filter ) { + filter = filter.split( wo.sort2Hash_separator ); + c.$table.one( 'tablesorter-ready', function() { + setTimeout(function() { + c.$table.one( 'filterEnd', function() { + $(this).triggerHandler( 'pageAndSize', [ page, size ] ); + }); + // use the newest filter comparison code + if ( ts.filter.equalFilters ) { + temp = ts.filter.equalFilters( c, c.lastSearch, filter ); + } else { + // quick n' dirty comparison... it will miss filter changes of + // the same value in a different column, see #1363 + temp = ( c.lastSearch || [] ).join( '' ) !== ( filter || [] ).join( '' ); + } + // don't set filters if they haven't changed + if ( !temp ) { + $.tablesorter.setFilters( table, filter, true ); + } + }, 100 ); + }); + } + } + if ( !filter ) { + c.$table.one( 'tablesorter-ready', function() { + c.$table.triggerHandler( 'pageAndSize', [ page, size ] ); + }); + } + + c.$table.on( 'sortEnd.sort2hash filterEnd.sort2hash pagerComplete.sort2Hash', function() { + if ( this.hasInitialized ) { + s2h.setHash( this.config, this.config.widgetOptions ); + } + }); + }, + + getTableId : function( c, wo ) { + // option > table id > table index on page + return wo.sort2Hash_tableId || + c.table.id || + 'table' + $( 'table' ).index( c.$table ); + }, + regexEscape : function( v ) { + return v.replace( /([\.\^\$\*\+\-\?\(\)\[\]\{\}\\\|])/g, '\\$1'); + }, + // convert 'first%20name,asc,last%20name,desc' into [[0,0], [1,1]] + convertString2Sort : function( c, wo, sortHash ) { + var regex, column, direction, temp, index, $cell, + arry = sortHash.split( wo.sort2Hash_separator ), + indx = 0, + len = arry.length, + sort = []; + while ( indx < len ) { + // column index or text + column = arry[ indx++ ]; + temp = parseInt( column, 10 ); + // ignore wo.sort2Hash_useHeaderText setting & + // just see if column contains a number + if ( isNaN( temp ) || temp > c.columns ) { + regex = new RegExp( '(' + s2h.regexEscape( column ) + ')', 'i' ); + for ( index = 0; index < c.columns; index++ ) { + $cell = c.$headerIndexed[ index ]; + if ( regex.test( $cell.attr( wo.sort2Hash_headerTextAttr ) ) ) { + column = index; + index = c.columns; + } + } + } + direction = arry[ indx++ ]; + // ignore unpaired values + if ( typeof column !== 'undefined' && typeof direction !== 'undefined' ) { + // convert text to 0, 1 + if ( isNaN( direction ) ) { + // default to ascending sort + direction = direction.indexOf( wo.sort2Hash_directionText[ 1 ] ) > -1 ? 1 : 0; + } + sort.push( [ column, direction ] ); + } + } + if ( sort.length ) { + c.sortList = sort; + } + }, + + // convert [[0,0],[1,1]] to 'first%20name,asc,last%20name,desc' + convertSort2String : function( c, wo ) { + var index, txt, column, direction, + sort = [], + arry = c.sortList || [], + len = arry.length; + for ( index = 0; index < len; index++ ) { + column = arry[ index ][ 0 ]; + txt = $.trim( c.$headerIndexed[ column ].attr( wo.sort2Hash_headerTextAttr ) ); + sort.push( txt !== '' ? encodeURIComponent( txt ) : column ); + direction = wo.sort2Hash_directionText[ arry[ index ][ 1 ] ]; + sort.push( direction ); + } + // join with separator + return sort.join( wo.sort2Hash_separator ); + }, + + convertFilter2String : function( c, wo ) { + var index, txt, column, direction, + sort = [], + arry = c.sortList || [], + len = arry.length; + for ( index = 0; index < len; index++ ) { + column = arry[ index ][ 0 ]; + txt = $.trim( c.$headerIndexed[ column ].attr( wo.sort2Hash_headerTextAttr ) ); + column = typeof txt !== 'undefined' ? encodeURIComponent( txt ) : column; + sort.push( column ); + direction = wo.sort2Hash_directionText[ arry[ index ][ 1 ] ]; + sort.push( direction ); + } + // join with separator + return sort.join( wo.sort2Hash_separator ); + }, + + // Get URL Parameters (getParam) + // modified from http://www.netlobo.com/url_query_string_javascript.html + getParam : function ( name, hash, returnRegex ) { + if ( !hash ) { hash = window.location.hash; } + var regex = new RegExp( '[\\?&]' + s2h.regexEscape( name ) + '=([^&#]*)' ), + match = regex.exec( hash ); + if ( returnRegex ) { return regex; } + return match === null ? '' : decodeURIComponent( match[ 1 ] ); + }, + + // remove parameter from hash + removeParam : function( name, hash ) { + if ( !hash ) { hash = window.location.hash; } + var index, + regex = s2h.getParam( name, hash, true ), + result = [], + parts = hash.split( '&' ), + len = parts.length; + for ( index = 0; index < len; index++ ) { + // regex expects a leading '&'... + if ( !regex.test( '&' + parts[ index ] ) ) { + result.push( parts[ index ] ); + } + } + return result.length ? result.join( '&' ) : ''; + }, + + encodeHash : function( c, wo, component, value, rawValue ) { + var result = false, + tableId = s2h.getTableId( c, wo ); + if ( typeof wo.sort2Hash_encodeHash === 'function' ) { + result = wo.sort2Hash_encodeHash( c, tableId, component, value, rawValue || value ); + } + if ( result === false ) { + result = '&' + component + '[' + tableId + ']=' + value; + } + return result; + }, + + decodeHash : function( c, wo, component ) { + var result = false, + tableId = s2h.getTableId( c, wo ); + if ( typeof wo.sort2Hash_decodeHash === 'function' ) { + // return a string + result = wo.sort2Hash_decodeHash( c, tableId, component ); + } + if ( result === false ) { + result = s2h.getParam( component + '[' + tableId + ']' ); + } + return result || ''; + }, + + cleanHash : function( c, wo, component, hash ) { + var result = false, + tableId = s2h.getTableId( c, wo ); + if ( typeof wo.sort2Hash_cleanHash === 'function' ) { + // can return an array or string + result = wo.sort2Hash_cleanHash( c, tableId, component, hash ); + } + if ( result === false ) { + // parameter example: 'sort[table0]=0,0' + result = s2h.removeParam( component + '[' + tableId + ']', hash ); + } + return result || ''; + }, + + setHash : function( c, wo ) { + var str = '', + hash = window.location.hash, + hasPager = ts.hasWidget( c.table, 'pager' ), + hasFilter = ts.hasWidget( c.table, 'filter' ), + sortList = s2h.convertSort2String( c, wo ), + filters = ( hasFilter && c.lastSearch.join('') !== '' ? c.lastSearch : [] ), + filtersStr = encodeURIComponent( filters.join( c.widgetOptions.sort2Hash_separator ) ), + components = { + 'sort' : sortList ? s2h.encodeHash( c, wo, 'sort', sortList, c.sortList ) : '', + 'page' : hasPager ? s2h.encodeHash( c, wo, 'page', c.pager.page + 1 ) : '', + 'size' : hasPager ? s2h.encodeHash( c, wo, 'size', c.pager.size ) : '', + 'filter' : filtersStr ? s2h.encodeHash( c, wo, 'filter', filtersStr, filters ) : '' + }; + // remove old hash + $.each( components, function( component, value ) { + hash = s2h.cleanHash( c, wo, component, hash ); + str += value; + }); + + var hashChar = wo.sort2Hash_hash; + // Combine new hash with any existing hashes + var newHash = ( + ( window.location.hash || '' ).replace( hashChar, '' ).length ? + hash : hashChar + ) + str; + + if (wo.sort2Hash_replaceHistory) { + var baseUrl = window.location.href.split(hashChar)[0]; + // Ensure that there is a leading hash character + var firstChar = newHash[0]; + if (firstChar !== hashChar) { + newHash = hashChar + newHash; + } + // Update URL in browser + window.location.replace(baseUrl + newHash); + } else { + // Add updated hash + window.location.hash = newHash; + } + } + }; + + ts.addWidget({ + id: 'sort2Hash', + priority: 60, // after saveSort & pager + options: { + sort2Hash_hash : '#', // hash prefix + sort2Hash_separator : '-', // don't '#' or '=' here + sort2Hash_headerTextAttr : 'data-header', // data attribute containing alternate header text + sort2Hash_directionText : [ 0, 1 ], // [ 'asc', 'desc' ], + sort2Hash_overrideSaveSort : false, // if true, override saveSort widget if saved sort available + sort2Hash_replaceHistory : false, // if true, hash changes are not saved to browser history + + // this option > table ID > table index on page + sort2Hash_tableId : null, + // custom hash processing functions + sort2Hash_encodeHash : null, + sort2Hash_decodeHash : null, + sort2Hash_cleanHash : null + }, + init: function(table, thisWidget, c, wo) { + s2h.init( c, wo ); + }, + remove: function(table, c) { + c.$table.off( '.sort2hash' ); + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sortTbodies.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sortTbodies.js new file mode 100644 index 0000000..eba047e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-sortTbodies.js @@ -0,0 +1,240 @@ +/*! tablesorter tbody sorting widget (BETA) - 11/26/2016 (v2.28.0) + * Requires tablesorter v2.22.2+ and jQuery 1.4+ + * by Rob Garrison + * Contributors: Chris Rogers + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter; + + ts.sortTbodies = { + init: function( c, wo ) { + + var index, rows, txt, max, $rows, + namespace = c.namespace + 'sortTbody', + $tbodies = c.$table.children( 'tbody' ), + len = $tbodies.length; + + // save serverSideSorting value; use to toggle internal row sorting + wo.sortTbody_original_serverSideSorting = c.serverSideSorting; + + // include info-only tbodies - we need parsed data from *all* tbodies + wo.sortTbody_original_cssInfoBlock = c.cssInfoBlock; + c.cssInfoBlock = wo.sortTbody_noSort; + ts.sortTbodies.setTbodies( c, wo ); + + // add original order index for stable sort + for ( index = 0; index < len; index++ ) { + $tbodies.eq( index ).attr( 'data-ts-original-order', index ); + } + + c.$table + .unbind( 'sortBegin updateComplete '.split( ' ' ).join( namespace + ' ' ) ) + .bind( 'sortBegin' + namespace, function() { + ts.sortTbodies.sorter( c ); + }) + .bind( 'updateComplete' + namespace, function() { + // find parsers for each column + ts.sortTbodies.setTbodies( c, wo ); + ts.updateCache( c, null, c.$tbodies ); + }) + .bind('sortEnd', function() { + // Moves the head row back to the top of the tbody + var primaryRow = wo.sortTbody_primaryRow; + if ( wo.sortTbody_lockHead && primaryRow ) { + c.$table.find( primaryRow ).each( function() { + $( this ).parents( 'tbody' ).prepend( this ); + }); + } + }); + + // detect parsers - in case the table contains only info-only tbodies + if ( $.isEmptyObject( c.parsers ) || c.$tbodies.length !== $tbodies.length ) { + ts.sortTbodies.setTbodies( c, wo ); + ts.updateCache( c, null, c.$tbodies ); + } + + // find colMax; this only matter for numeric columns + $rows = $tbodies.children( 'tr' ); + len = $rows.length; + for ( index = 0; index < c.columns; index++ ) { + max = 0; + if ( c.parsers[ index ].type === 'numeric' ) { + for ( rows = 0; rows < len; rows++ ) { + // update column max value (ignore sign) + txt = ts.getParsedText( c, $rows.eq( rows ).children()[ index ], index ); + max = Math.max( Math.abs( txt ) || 0, max ); + } + } + c.$headerIndexed[ index ].attr( 'data-ts-col-max-value', max ); + } + + }, + + // make sure c.$tbodies is up-to-date (init & after updates) + setTbodies: function( c, wo ) { + c.$tbodies = c.$table.children( 'tbody' ).not( '.' + wo.sortTbody_noSort ); + }, + + sorter: function( c ) { + var $table = c.$table, + wo = c.widgetOptions; + + // prevent multiple calls while processing + if ( wo.sortTbody_busy !== true ) { + wo.sortTbody_busy = true; + var $tbodies = $table.children( 'tbody' ).not( '.' + wo.sortTbody_noSort ), + primary = wo.sortTbody_primaryRow || 'tr:eq(0)', + sortList = c.sortList || [], + len = sortList.length; + + if ( len ) { + + // toggle internal row sorting + c.serverSideSorting = !wo.sortTbody_sortRows; + + $tbodies.sort( function( a, b ) { + var sortListIndex, txt, dir, num, colMax, sort, col, order, colA, colB, x, y, + table = c.table, + parsers = c.parsers, + cts = c.textSorter || '', + $tbodyA = $( a ), + $tbodyB = $( b ), + $a = $tbodyA.find( primary ).children( 'td, th' ), + $b = $tbodyB.find( primary ).children( 'td, th' ); + for ( sortListIndex = 0; sortListIndex < len; sortListIndex++ ) { + col = sortList[ sortListIndex ][0]; + order = sortList[ sortListIndex ][1]; + // sort direction, true = asc, false = desc + dir = order === 0; + // column txt - tbody A + txt = ts.getElementText( c, $a.eq( col ), col ); + colA = parsers[ col ].format( txt, table, $a[ col ], col ); + // column txt - tbody B + txt = ts.getElementText( c, $b.eq( col ), col ); + colB = parsers[ col ].format( txt, table, $b[ col ], col ); + + if (c.sortStable && colA === colB && len === 1) { + return $tbodyA.attr( 'data-ts-original-order' ) - $tbodyB.attr( 'data-ts-original-order' ); + } + + // fallback to natural sort since it is more robust + num = /n/i.test( parsers && parsers[ col ] ? parsers[ col ].type || '' : '' ); + if ( num && c.strings[ col ] ) { + colMax = c.$headerIndexed[ col ].attr( 'data-ts-col-max-value' ) || + 1.79E+308; // close to Number.MAX_VALUE + // 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, b, dir, colMax, table); + sort = c.numberSorter ? c.numberSorter( colA, colB, dir, colMax, table ) : + ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( colA, colB, num, colMax, col, c ); + } else { + // set a & b depending on sort direction + x = dir ? colA : colB; + y = dir ? colB : colA; + // text sort function + if ( typeof ( cts ) === 'function' ) { + // custom OVERALL text sorter + sort = cts( x, y, dir, col, table ); + } else if ( typeof ( cts ) === 'object' && cts.hasOwnProperty( col ) ) { + // custom text sorter for a SPECIFIC COLUMN + sort = cts[ col ]( x, y, dir, col, table ); + } else { + // fall back to natural sort + sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( colA, colB, col, c ); + } + } + if ( sort ) { return sort; } + } + return $tbodyA.attr( 'data-ts-original-order' ) - $tbodyB.attr( 'data-ts-original-order' ); + }); + + ts.sortTbodies.restoreTbodies( c, wo, $tbodies ); + wo.sortTbody_busy = false; + } + } + }, + + restoreTbodies : function ( c, wo, $sortedTbodies ) { + var $nosort, $tbodies, $thisTbody, tbLen, nsLen, index, targetIndex, + $table = c.$table, + hasShuffled = true, + indx = 0; + + // hide entire table to improve sort performance + $table.hide(); + $sortedTbodies.appendTo( $table ); + + // reposition no-sort tbodies + $tbodies = $table.children( 'tbody' ); + tbLen = $tbodies.length; + $nosort = $tbodies.filter( '.' + wo.sortTbody_noSort ).appendTo( $table ); + nsLen = $nosort.length; + + if ( nsLen ) { + // don't allow the while loop to cycle more times than the set number of no-sort tbodies + while ( hasShuffled && indx < nsLen ) { + hasShuffled = false; + for ( index = 0; index < nsLen; index++ ) { + targetIndex = parseInt( $nosort.eq( index ).attr( 'data-ts-original-order' ), 10 ); + // if target index > number of tbodies, make it last + targetIndex = targetIndex >= tbLen ? tbLen : targetIndex < 0 ? 0 : targetIndex; + + if ( targetIndex !== $nosort.eq( index ).index() ) { + hasShuffled = true; + $thisTbody = $nosort.eq( index ).detach(); + + if ( targetIndex >= tbLen ) { + // Are we trying to be the last tbody? + $thisTbody.appendTo( $table ); + } else if ( targetIndex === 0 ) { + // Are we trying to be the first tbody? + $thisTbody.prependTo( $table ); + } else { + // No, we want to be somewhere in the middle! + $thisTbody.insertBefore( $table.children( 'tbody:eq(' + targetIndex + ')' ) ); + } + + } + } + indx++; + } + } + + $table.show(); + } + + }; + + ts.addWidget({ + id: 'sortTbody', + // priority < 50 (filter widget), so c.$tbodies has the correct elements + priority: 40, + options: { + // lock primary row as a header when sorting + sortTbody_lockHead : false, + // point to a row within the tbody that matches the number of header columns + sortTbody_primaryRow : null, + // sort tbody internal rows + sortTbody_sortRows : false, + // static tbodies (like static rows) + sortTbody_noSort : 'tablesorter-no-sort-tbody' + }, + init : function( table, thisWidget, c, wo ) { + ts.sortTbodies.init( c, wo ); + }, + remove : function( table, c, wo ) { + c.$table.unbind( 'sortBegin updateComplete '.split( ' ' ).join( c.namespace + 'sortTbody ' ) ); + c.serverSideSorting = wo.sortTbody_original_serverSideSorting; + c.cssInfoBlock = wo.sortTbody_original_cssInfoBlock; + } + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-staticRow.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-staticRow.js new file mode 100644 index 0000000..ff8411e --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-staticRow.js @@ -0,0 +1,121 @@ +/*! widget: staticRow - updated 10/31/2015 (v2.24.0) *//* + * Version 1.2 mod by Rob Garrison (requires tablesorter v2.16+) + * Requires: + * jQuery v1.4+ + * tablesorter plugin, v2.8+, available at http://mottie.github.com/tablesorter/docs/ + * + * Copyright (c) 2011 Nils Luxton + * Licensed under the MIT license: + * http://www.opensource.org/licenses/mit-license.php + * + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + var ts = $.tablesorter, + + // add/refresh row indexes + addIndexes = function(table) { + var $tr, wo, v, indx, rows, + c = table.config; + // 'Index' the static rows, saving their current (starting) position in the + // table inside a data() param on the <tr> element itself for later use. + if (c) { + wo = c.widgetOptions; + c.$tbodies.each(function() { + $tr = $(this).children(); + rows = $tr.length; + $tr.filter(wo.staticRow_class).each(function() { + $tr = $(this); + indx = $tr.data(wo.staticRow_index); + if (typeof indx !== 'undefined') { + v = parseFloat(indx); + // percentage of total rows + indx = (/%/.test(indx)) ? Math.round(v / 100 * rows) : v; + } else { + indx = $tr.index(); + } + // row indexing starts over within each tbody + $tr.data( wo.staticRow_data, indx ); + }); + }); + } + }; + + ts.addWidget({ + // Give the new Widget an ID to be used in the tablesorter() call, as follows: + // $('#myElement').tablesorter({ widgets: ['zebra', 'staticRow'] }); + id: 'staticRow', + + options: { + staticRow_class : '.static', + staticRow_data : 'static-index', + staticRow_index : 'row-index', + staticRow_event : 'staticRowsRefresh' + }, + + init: function(table, thisWidget, c, wo) { + addIndexes(table); + // refresh static rows after updates + c.$table + .unbind( ('updateComplete.tsstaticrows ' + wo.staticRow_event).replace(/\s+/g, ' ') ) + .bind('updateComplete.tsstaticrows ' + wo.staticRow_event, function() { + addIndexes(table); + ts.applyWidget( table ); + }); + }, + + format: function(table, c, wo) { + // Loop thru static rows, moving them to their original 'indexed' position, + // & repeat until no more re-shuffling is needed + var targetIndex, $thisRow, indx, numRows, $tbody, hasShuffled, $rows, max; + + c.$tbodies.each(function() { + $tbody = $.tablesorter.processTbody(table, $(this), true); // remove tbody + hasShuffled = true; + indx = 0; + $rows = $tbody.children(wo.staticRow_class); + numRows = $tbody.children('tr').length - 1; + max = $rows.length; + + // don't allow the while loop to cycle more times than the set number of static rows + while (hasShuffled && indx < max) { + hasShuffled = false; + /*jshint loopfunc:true */ + $rows.each(function() { + targetIndex = $(this).data(wo.staticRow_data); + // allow setting target index >> num rows to always make a row last + targetIndex = targetIndex >= numRows ? numRows : targetIndex < 0 ? 0 : targetIndex; + if (targetIndex !== $(this).index()) { + hasShuffled = true; + $thisRow = $(this).detach(); + + if (targetIndex >= numRows) { + // Are we trying to be the last row? + $thisRow.appendTo( $tbody ); + } else if (targetIndex === 0) { + // Are we trying to be the first row? + $thisRow.prependTo( $tbody ); + } else { + // No, we want to be somewhere in the middle! + $thisRow.insertBefore( $tbody.find('tr:eq(' + targetIndex + ')') ); + } + } + }); + indx++; + } + + $.tablesorter.processTbody(table, $tbody, false); // restore tbody + }); + + c.$table.triggerHandler('staticRowsComplete', table); + }, + + remove : function(table, c, wo) { + c.$table.unbind( ('updateComplete.tsstaticrows ' + wo.staticRow_event).replace(/\s+/g, ' ') ); + } + + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-stickyHeaders.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-stickyHeaders.js new file mode 100644 index 0000000..32018ef --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-stickyHeaders.js @@ -0,0 +1,324 @@ +/*! Widget: stickyHeaders - updated 9/27/2017 (v2.29.0) *//* + * Requires tablesorter v2.8+ and jQuery 1.4.3+ + * by Rob Garrison + */ +;(function ($, window) { + 'use strict'; + var ts = $.tablesorter || {}; + + $.extend(ts.css, { + sticky : 'tablesorter-stickyHeader', // stickyHeader + stickyVis : 'tablesorter-sticky-visible', + stickyHide: 'tablesorter-sticky-hidden', + stickyWrap: 'tablesorter-sticky-wrapper' + }); + + // Add a resize event to table headers + ts.addHeaderResizeEvent = function(table, disable, settings) { + table = $(table)[0]; // make sure we're using a dom element + if ( !table.config ) { return; } + var defaults = { + timer : 250 + }, + options = $.extend({}, defaults, settings), + c = table.config, + wo = c.widgetOptions, + checkSizes = function( triggerEvent ) { + var index, headers, $header, sizes, width, height, + len = c.$headers.length; + wo.resize_flag = true; + headers = []; + for ( index = 0; index < len; index++ ) { + $header = c.$headers.eq( index ); + sizes = $header.data( 'savedSizes' ) || [ 0, 0 ]; // fixes #394 + width = $header[0].offsetWidth; + height = $header[0].offsetHeight; + if ( width !== sizes[0] || height !== sizes[1] ) { + $header.data( 'savedSizes', [ width, height ] ); + headers.push( $header[0] ); + } + } + if ( headers.length && triggerEvent !== false ) { + c.$table.triggerHandler( 'resize', [ headers ] ); + } + wo.resize_flag = false; + }; + clearInterval(wo.resize_timer); + if (disable) { + wo.resize_flag = false; + return false; + } + checkSizes( false ); + wo.resize_timer = setInterval(function() { + if (wo.resize_flag) { return; } + checkSizes(); + }, options.timer); + }; + + function getStickyOffset(c, wo) { + var $el = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : []; + return $el.length ? + $el.height() || 0 : + parseInt(wo.stickyHeaders_offset, 10) || 0; + } + + // Sticky headers based on this awesome article: + // http://css-tricks.com/13465-persistent-headers/ + // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech + // ************************** + ts.addWidget({ + id: 'stickyHeaders', + priority: 54, // sticky widget must be initialized after the filter & before pager widget! + options: { + stickyHeaders : '', // extra class name added to the sticky header row + stickyHeaders_appendTo : null, // jQuery selector or object to phycially attach the sticky headers + stickyHeaders_attachTo : null, // jQuery selector or object to attach scroll listener to (overridden by xScroll & yScroll settings) + stickyHeaders_xScroll : null, // jQuery selector or object to monitor horizontal scroll position (defaults: xScroll > attachTo > window) + stickyHeaders_yScroll : null, // jQuery selector or object to monitor vertical scroll position (defaults: yScroll > attachTo > window) + stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element + stickyHeaders_filteredToTop: true, // scroll table top into view after filtering + stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists + stickyHeaders_addResizeEvent : true, // trigger 'resize' event on headers + stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header + stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs + }, + format: function(table, c, wo) { + // filter widget doesn't initialize on an empty table. Fixes #449 + if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) { + return; + } + var index, len, $t, + $table = c.$table, + // add position: relative to attach element, hopefully it won't cause trouble. + $attach = $(wo.stickyHeaders_attachTo || wo.stickyHeaders_appendTo), + namespace = c.namespace + 'stickyheaders ', + // element to watch for the scroll event + $yScroll = $(wo.stickyHeaders_yScroll || wo.stickyHeaders_attachTo || window), + $xScroll = $(wo.stickyHeaders_xScroll || wo.stickyHeaders_attachTo || window), + $thead = $table.children('thead:first'), + $header = $thead.children('tr').not('.sticky-false').children(), + $tfoot = $table.children('tfoot'), + stickyOffset = getStickyOffset(c, wo), + // is this table nested? If so, find parent sticky header wrapper (div, not table) + $nestedSticky = $table.parent().closest('.' + ts.css.table).hasClass('hasStickyHeaders') ? + $table.parent().closest('table.tablesorter')[0].config.widgetOptions.$sticky.parent() : [], + nestedStickyTop = $nestedSticky.length ? $nestedSticky.height() : 0, + // clone table, then wrap to make sticky header + $stickyTable = wo.$sticky = $table.clone() + .addClass('containsStickyHeaders ' + ts.css.sticky + ' ' + wo.stickyHeaders + ' ' + c.namespace.slice(1) + '_extra_table' ) + .wrap('<div class="' + ts.css.stickyWrap + '">'), + $stickyWrap = $stickyTable.parent() + .addClass(ts.css.stickyHide) + .css({ + position : $attach.length ? 'absolute' : 'fixed', + padding : parseInt( $stickyTable.parent().parent().css('padding-left'), 10 ), + top : stickyOffset + nestedStickyTop, + left : 0, + visibility : 'hidden', + zIndex : wo.stickyHeaders_zIndex || 2 + }), + $stickyThead = $stickyTable.children('thead:first'), + $stickyCells, + laststate = '', + setWidth = function($orig, $clone) { + var index, width, border, $cell, $this, + $cells = $orig.filter(':visible'), + len = $cells.length; + for ( index = 0; index < len; index++ ) { + $cell = $clone.filter(':visible').eq(index); + $this = $cells.eq(index); + // code from https://github.com/jmosbech/StickyTableHeaders + if ($this.css('box-sizing') === 'border-box') { + width = $this.outerWidth(); + } else { + if ($cell.css('border-collapse') === 'collapse') { + if (window.getComputedStyle) { + width = parseFloat( window.getComputedStyle($this[0], null).width ); + } else { + // ie8 only + border = parseFloat( $this.css('border-width') ); + width = $this.outerWidth() - parseFloat( $this.css('padding-left') ) - parseFloat( $this.css('padding-right') ) - border; + } + } else { + width = $this.width(); + } + } + $cell.css({ + 'width': width, + 'min-width': width, + 'max-width': width + }); + } + }, + getLeftPosition = function(yWindow) { + if (yWindow === false && $nestedSticky.length) { + return $table.position().left; + } + return $attach.length ? + parseInt($attach.css('padding-left'), 10) || 0 : + $table.offset().left - parseInt($table.css('margin-left'), 10) - $(window).scrollLeft(); + }, + resizeHeader = function() { + $stickyWrap.css({ + left : getLeftPosition(), + width: $table.outerWidth() + }); + setWidth( $table, $stickyTable ); + setWidth( $header, $stickyCells ); + }, + scrollSticky = function( resizing ) { + if (!$table.is(':visible')) { return; } // fixes #278 + // Detect nested tables - fixes #724 + nestedStickyTop = $nestedSticky.length ? $nestedSticky.offset().top - $yScroll.scrollTop() + $nestedSticky.height() : 0; + var tmp, + offset = $table.offset(), + stickyOffset = getStickyOffset(c, wo), + yWindow = $.isWindow( $yScroll[0] ), // $.isWindow needs jQuery 1.4.3 + yScroll = yWindow ? + $yScroll.scrollTop() : + // use parent sticky position if nested AND inside of a scrollable element - see #1512 + $nestedSticky.length ? parseInt($nestedSticky[0].style.top, 10) : $yScroll.offset().top, + attachTop = $attach.length ? yScroll : $yScroll.scrollTop(), + captionHeight = wo.stickyHeaders_includeCaption ? 0 : $table.children( 'caption' ).height() || 0, + scrollTop = attachTop + stickyOffset + nestedStickyTop - captionHeight, + tableHeight = $table.height() - ($stickyWrap.height() + ($tfoot.height() || 0)) - captionHeight, + isVisible = ( scrollTop > offset.top ) && ( scrollTop < offset.top + tableHeight ) ? 'visible' : 'hidden', + state = isVisible === 'visible' ? ts.css.stickyVis : ts.css.stickyHide, + needsUpdating = !$stickyWrap.hasClass( state ), + cssSettings = { visibility : isVisible }; + if ($attach.length) { + // attached sticky headers always need updating + needsUpdating = true; + cssSettings.top = yWindow ? scrollTop - $attach.offset().top : $attach.scrollTop(); + } + // adjust when scrolling horizontally - fixes issue #143 + tmp = getLeftPosition(yWindow); + if (tmp !== parseInt($stickyWrap.css('left'), 10)) { + needsUpdating = true; + cssSettings.left = tmp; + } + cssSettings.top = ( cssSettings.top || 0 ) + + // If nested AND inside of a scrollable element, only add parent sticky height + (!yWindow && $nestedSticky.length ? $nestedSticky.height() : stickyOffset + nestedStickyTop); + if (needsUpdating) { + $stickyWrap + .removeClass( ts.css.stickyVis + ' ' + ts.css.stickyHide ) + .addClass( state ) + .css(cssSettings); + } + if (isVisible !== laststate || resizing) { + // make sure the column widths match + resizeHeader(); + laststate = isVisible; + } + }; + // only add a position relative if a position isn't already defined + if ($attach.length && !$attach.css('position')) { + $attach.css('position', 'relative'); + } + // fix clone ID, if it exists - fixes #271 + if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; } + // clear out cloned table, except for sticky header + // include caption & filter row (fixes #126 & #249) - don't remove cells to get correct cell indexing + $stickyTable.find('> thead:gt(0), tr.sticky-false').hide(); + $stickyTable.find('> tbody, > tfoot').remove(); + $stickyTable.find('caption').toggle(wo.stickyHeaders_includeCaption); + // issue #172 - find td/th in sticky header + $stickyCells = $stickyThead.children().children(); + $stickyTable.css({ height:0, width:0, margin: 0 }); + // remove resizable block + $stickyCells.find('.' + ts.css.resizer).remove(); + // update sticky header class names to match real header after sorting + $table + .addClass('hasStickyHeaders') + .bind('pagerComplete' + namespace, function() { + resizeHeader(); + }); + + ts.bindEvents(table, $stickyThead.children().children('.' + ts.css.header)); + + if (wo.stickyHeaders_appendTo) { + $(wo.stickyHeaders_appendTo).append( $stickyWrap ); + } else { + // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned. + $table.after( $stickyWrap ); + } + + // onRenderHeader is defined, we need to do something about it (fixes #641) + if (c.onRenderHeader) { + $t = $stickyThead.children('tr').children(); + len = $t.length; + for ( index = 0; index < len; index++ ) { + // send second parameter + c.onRenderHeader.apply( $t.eq( index ), [ index, c, $stickyTable ] ); + } + } + // make it sticky! + $xScroll.add($yScroll) + .unbind( ('scroll resize '.split(' ').join( namespace )).replace(/\s+/g, ' ') ) + .bind('scroll resize '.split(' ').join( namespace ), function( event ) { + scrollSticky( event.type === 'resize' ); + }); + c.$table + .unbind('stickyHeadersUpdate' + namespace) + .bind('stickyHeadersUpdate' + namespace, function() { + scrollSticky( true ); + }); + + if (wo.stickyHeaders_addResizeEvent) { + ts.addHeaderResizeEvent(table); + } + + // look for filter widget + if ($table.hasClass('hasFilters') && wo.filter_columnFilters) { + // scroll table into view after filtering, if sticky header is active - #482 + $table.bind('filterEnd' + namespace, function() { + // $(':focus') needs jQuery 1.6+ + var $td = $(document.activeElement).closest('td'), + column = $td.parent().children().index($td); + // only scroll if sticky header is active + if ($stickyWrap.hasClass(ts.css.stickyVis) && wo.stickyHeaders_filteredToTop) { + // scroll to original table (not sticky clone) + window.scrollTo(0, $table.position().top); + // give same input/select focus; check if c.$filters exists; fixes #594 + if (column >= 0 && c.$filters) { + c.$filters.eq(column).find('a, select, input').filter(':visible').focus(); + } + } + }); + ts.filter.bindSearch( $table, $stickyCells.find('.' + ts.css.filter) ); + // support hideFilters + if (wo.filter_hideFilters) { + ts.filter.hideFilters(c, $stickyTable); + } + } + + // resize table (Firefox) + if (wo.stickyHeaders_addResizeEvent) { + $table.bind('resize' + c.namespace + 'stickyheaders', function() { + resizeHeader(); + }); + } + + // make sure sticky is visible if page is partially scrolled + scrollSticky( true ); + $table.triggerHandler('stickyHeadersInit'); + + }, + remove: function(table, c, wo) { + var namespace = c.namespace + 'stickyheaders '; + c.$table + .removeClass('hasStickyHeaders') + .unbind( ('pagerComplete resize filterEnd stickyHeadersUpdate '.split(' ').join(namespace)).replace(/\s+/g, ' ') ) + .next('.' + ts.css.stickyWrap).remove(); + if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table + $(window) + .add(wo.stickyHeaders_xScroll) + .add(wo.stickyHeaders_yScroll) + .add(wo.stickyHeaders_attachTo) + .unbind( ('scroll resize '.split(' ').join(namespace)).replace(/\s+/g, ' ') ); + ts.addHeaderResizeEvent(table, true); + } + }); + +})(jQuery, window); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-storage.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-storage.js new file mode 100644 index 0000000..9819ff8 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-storage.js @@ -0,0 +1,115 @@ +/*! 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: '' + } + }); + + // *** 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 && 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 (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') || {} : {}; + } + } + // 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 { + date = new Date(); + date.setTime(date.getTime() + (31536e+6)); // 365 days + document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; + } + } else { + return values && values[url] ? values[url][id] : ''; + } + }; + +})(jQuery, window, document); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-toggle.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-toggle.js new file mode 100644 index 0000000..c7f4214 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-toggle.js @@ -0,0 +1,81 @@ +/*! tablesorter enable/disable sort & filter (BETA) - 11/10/2015 (v2.24.4) + * Requires tablesorter v2.24.4+ & jQuery 1.7+ + * by Rob Garrison + */ +;( function( $ ) { + 'use strict'; + var ts = $.tablesorter, + + tst = ts.toggleTS = { + + init : function( c, wo ) { + wo.toggleTS_isEnabled = true; // enabled + wo.toggleTS_areDisabled = { + headers : [], + filters : [] + }; + c.$table.on('enable.toggleTS disable.toggleTS', function( event ) { + tst.toggle( this.config, this.config.widgetOptions, event.type === 'enable' ); + }); + }, + toggle : function( c, wo, isEnabled ) { + if ( wo.toggleTS_isEnabled !== isEnabled ) { + wo.toggleTS_isEnabled = isEnabled; + var indx, $el, + len = c.$headers.length; + + // table headers + for ( indx = 0; indx < len; indx++ ) { + $el = c.$headers.eq( indx ); + // function added in v2.24.4 + ts.setColumnSort( c, $el, !isEnabled ); + // function added in v2.24.4; passing "isEnabled" allows removal of "next sort" labels + ts.setColumnAriaLabel( c, $el, isEnabled ); + } + if ( wo.toggleTS_hideFilterRow ) { + c.$table.find( '.' + ts.css.filterRow ).toggle( isEnabled ); + } else if ( ts.hasWidget( c.$table, 'filter' ) ) { + // c.$filters points to filter CELL + len = c.$filters.length; + for ( indx = 0; indx < len; indx++ ) { + if ( isEnabled && !wo.toggleTS_areDisabled.filters[ indx ] ) { + c.$filters.eq( indx ).find( 'input, select' ) + .removeClass( ts.css.filterDisabled ) + .prop( 'disabled', false ); + } else if ( !isEnabled ) { + $el = c.$filters.eq( indx ).find( 'input, select' ); + if ( $el.hasClass( ts.css.filterDisabled ) ) { + wo.toggleTS_areDisabled.filters[ indx ] = true; + } + $el + .addClass( ts.css.filterDisabled ) + .prop( 'disabled', true ); + } + } + } + // include external filters + wo.filter_$externalFilters + .toggleClass( ts.css.filterDisabled, isEnabled ) + .prop( 'disabled', !isEnabled ); + } + if ( typeof wo.toggleTS_callback === 'function' ) { + wo.toggleTS_callback( c, isEnabled ); + } + } + }; + + ts.addWidget({ + id: 'toggle-ts', + options: { + toggleTS_hideFilterRow : false, + toggleTS_callback : null + }, + init : function( table, thisWidget, c, wo ) { + tst.init( c, wo ); + }, + remove : function( table, c ) { + c.$table.off( 'enable.toggleTS disable.toggleTS' ); + } + }); + +})( jQuery ); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-uitheme.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-uitheme.js new file mode 100644 index 0000000..817ed17 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-uitheme.js @@ -0,0 +1,196 @@ +/*! 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 <i> 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 <i> 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 + } + }; + + $.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('<div class="' + ts.css.wrapper + '" style="position:relative;height:100%;width:100%"></div>'); + } + }); + if (c.cssIcon) { + // if c.cssIcon is '', then no <i> 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 || ''); + } + } + } + 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); + } + }); + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-vertical-group.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-vertical-group.js new file mode 100644 index 0000000..9cb39e1 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-vertical-group.js @@ -0,0 +1,135 @@ +/*! Widget: vertical-group (BETA) - updated 12/13/2017 (v2.29.1) */ +/* Requires tablesorter and jQuery + * Originally by @aavmurphy (Andrew Murphy) + * Adapted for tablesorter by Rob Garrison - see #1469 & #1470 + * + * This widget is licensed under the same terms at mottie/tablesorter itself, i.e. free to use + */ +/* jshint browser:true, jquery:true, unused:false */ +/* global jQuery:false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + tscss = ts.css; + + $.extend( ts.css, { + verticalGroupHeader: 'tablesorter-vertical-group', + verticalGroupHide: 'tablesorter-vertical-group-hide', + verticalGroupShow: 'tablesorter-vertical-group-show' + }); + + ts.addWidget({ + id: 'vertical-group', + priority: 99, + init: verticalGroup, + format: verticalGroup + }); + + function cleanUp( el ) { + el.removeClass(tscss.verticalGroupHide + ' ' + tscss.verticalGroupShow); + } + + function setZebra( wo, $cell, indx ) { + var $row = $cell.parent(); + $row + .removeClass( wo.zebra[ (indx + 1) % 2 ] ) + .addClass( wo.zebra[ indx % 2] ); + } + + function verticalGroup( table, c, wo ) { + // ------------------------------------------------------------------------- + // loop thru the header row, + // - look for .vertical-group + // + // loop thru the rows + // + // set ALWAYS_SHOW = FALSE + // loop thru the 1st 4 columns + // if this cell does not exist, skip to next row + // if ALWAYS_SHOW, then this cell is SHOW + // else if this column does not have '.vertical-group', then this cell is SHOW + // else if this cell is NOT the same as the cell-above, then this cell is SHOW + // else this cell is HIDE + // if this cell is SHOW, set ALWAYS_SHOW + // if this cell is SHOW, + // then + // set the cell class to .vertical_group_show + // else + // set the cell class to vertical_group_show + // + // TO DO add/remove classes so as not to clobber other existing classes + // TO DO add classes + // + // .vertical-group-show { background-color: white !important; } + // .vertical-group-hide { visibility: hidden; border-top: white !important;background-color: white !important; } + // + // this is all because of stripped tables + // - background-colour show be the table's background colour (or the first row's) + // - the border-color needs to be the same + // + // ------------------------------------------------------------------------------------------------ + var zebra_index = -1, // increments at start of loop + rows = table.tBodies[0].rows, + has_zebra = ts.hasWidget( table, 'zebra'), + is_vertical_group_col = [], + last_row = []; + + if ( wo.vertical_group_lock ) { + return; + } + wo.vertical_group_lock = true; + + is_vertical_group_col = $.map( c.$headerIndexed, function( el ) { + return el.hasClass( tscss.verticalGroupHeader ) ? 1 : ''; + }); + + if ( is_vertical_group_col.join('') === '' ) { + cleanUp( $(rows).find( '.' + tscss.verticalGroupHide + ',.' + tscss.verticalGroupShow ) ); + wo.vertical_group_lock = false; + return; + } + + for (var i = 0; i < rows.length; i++) { + var always_show_cell = false; + + for (var j = 0; j < c.columns; j++ ) { + if ( !is_vertical_group_col[ j ] || !rows[ i ].cells[ j ] ) { + zebra_index++; + continue; + } + + var $cell = $( rows[ i ].cells[ j ] ), + // only group if column is sorted + isSorted = ts.isValueInArray( j, c.sortList ), // returns equivalent of an indexOf value + cell_data = $cell.html(); + + if ( isSorted < 0 ) { + cleanUp( $cell ); + } else if ( !always_show_cell && cell_data === last_row[j] ) { + if ( !$cell.hasClass(tscss.verticalGroupHide) ) { + $cell.addClass( tscss.verticalGroupHide ); + } + if ( has_zebra ) { + setZebra( wo, $cell, zebra_index ); + } + $cell.removeClass( tscss.verticalGroupShow ); + } else if (isSorted === 0) { + // only show cells from the first sorted column + always_show_cell = true; // show + if ( !$cell.hasClass( tscss.verticalGroupShow ) ) { + $cell.addClass( tscss.verticalGroupShow ); + } + $cell.removeClass( tscss.verticalGroupHide ); + if ( has_zebra ) { + // only adjust striping based on the first sorted column + setZebra( wo, $cell, isSorted ? zebra_index : ++zebra_index ); + } + } + last_row[j] = cell_data; + } + } + wo.vertical_group_lock = false; + } + +})(jQuery); diff --git a/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-view.js b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-view.js new file mode 100644 index 0000000..8c76db9 --- /dev/null +++ b/vendor/assets/javascripts/jquery-tablesorter/widgets/widget-view.js @@ -0,0 +1,195 @@ +/* Widget: view (beta) - updated 7/11/2016 (v2.26.6) */ +/* By Justin F. Hallett (https://github.com/TheSin-) + * Requires tablesorter v2.8+ and jQuery 1.7+ + */ +/*jshint browser:true, jquery:true, unused:false */ +/*global jQuery: false */ +;(function($) { + 'use strict'; + + var ts = $.tablesorter, + is_hidden = false, + tpos, ttop, tleft, + + view = ts.view = { + copyCaption: function(c, wo) { + view.removeCaption(c, wo); + + if (c.$table.find('caption').length > 0) { + $(wo.view_caption).text(c.$table.find('caption').text()); + } + }, + + removeCaption: function(c, wo) { + $(wo.view_caption).empty(); + }, + + buildToolBar: function(c, wo) { + view.removeToolBar(c, wo); + view.copyCaption(c, wo); + var $toolbar = $(wo.view_toolbar); + + $.each(wo.view_layouts, function(k, v) { + var classes = wo.view_switcher_class; + if (k === wo.view_layout) { + classes += ' active'; + } + + var $switcher = $('<a>', { + 'href': '#', + 'class': classes, + 'data-view-type': k, + 'title': v.title + }); + + $switcher.append($('<i>', { + 'class': v.icon + })); + + $toolbar.append($switcher); + }); + + $toolbar.find('.' + wo.view_switcher_class).on('click', function(e) { + e.preventDefault(); + if ($(this).hasClass('active')) { + // if currently clicked button has the active class + // then we do nothing! + return false; + } else { + // otherwise we are clicking on the inactive button + // and in the process of switching views! + $toolbar.find('.' + wo.view_switcher_class).removeClass('active'); + $(this).addClass('active'); + wo.view_layout = $(this).attr('data-view-type'); + + if (wo.view_layouts[wo.view_layout].raw === true) { + view.remove(c, wo); + view.buildToolBar(c, wo); + } else { + if (is_hidden === false) { + view.hideTable(c, wo); + } + view.buildView(c, wo); + } + } + }); + }, + + removeToolBar: function(c, wo) { + $(wo.view_toolbar).empty(); + view.removeCaption(c, wo); + }, + + buildView: function(c, wo) { + view.removeView(c, wo); + + var myview = wo.view_layouts[wo.view_layout]; + var $container = $(myview.container, { + 'class': wo.view_layout + }); + + ts.getColumnText(c.$table, 0, function(data) { + var tmpl = myview.tmpl; + + $.each($(data.$row).find('td'), function(k, v) { + var attrs = {}; + var reg = '{col' + k + '}'; + $.each(v.attributes, function(idx, attr) { + attrs[attr.nodeName] = attr.nodeValue; + }); + var content = $(v).html(); + // Add 2 spans, one is dropped when using .html() + var span = $('<span />').append($('<span/>', attrs).append(content)); + tmpl = tmpl.replace(new RegExp(reg, 'g'), span.html()); + + reg = '{col' + k + ':raw}'; + tmpl = tmpl.replace(new RegExp(reg, 'g'), $(v).text()); + }); + + var $tmpl = $(tmpl); + $.each(data.$row[0].attributes, function(idx, attr) { + if (attr.nodeName === 'class') { + $tmpl.attr(attr.nodeName, $tmpl.attr(attr.nodeName) + ' ' + attr.nodeValue); + } else { + $tmpl.attr(attr.nodeName, attr.nodeValue); + } + }); + $container.append($tmpl); + }); + + $(wo.view_container).append($container); + c.$table.triggerHandler('viewComplete'); + }, + + removeView: function(c, wo) { + $(wo.view_container).empty(); + }, + + hideTable: function(c) { + tpos = c.$table.css('position'); + ttop = c.$table.css('bottom'); + tleft = c.$table.css('left'); + + c.$table.css({ + 'position': 'absolute', + 'top': '-10000px', + 'left': '-10000px' + }); + + is_hidden = true; + }, + + init: function(c, wo) { + if (wo.view_layout === false) { + return; + } + + if (typeof wo.view_layouts[wo.view_layout] === 'undefined') { + return; + } + + if (is_hidden === false) { + view.hideTable(c, wo); + } + + c.$table.on('tablesorter-ready', function() { + view.buildToolBar(c, wo); + view.buildView(c, wo); + }); + }, + + remove: function(c, wo) { + view.removeToolBar(c, wo); + view.removeView(c, wo); + + c.$table.css({ + 'position': tpos, + 'top': ttop, + 'left': tleft + }); + + is_hidden = false; + } + }; + + ts.addWidget({ + id: 'view', + options: { + view_toolbar: '#ts-view-toolbar', + view_container: '#ts-view', + view_caption: '#ts-view-caption', + view_switcher_class: 'ts-view-switcher', + view_layout: false, + view_layouts: {} + }, + + init: function(table, thisWidget, c, wo) { + view.init(c, wo); + }, + + remove: function(table, c, wo) { + view.remove(c, wo); + } + }); + +})(jQuery); diff --git a/vendor/assets/stylesheets/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.css b/vendor/assets/stylesheets/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.css index e77cd88..da4f2cc 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/addons/pager/jquery.tablesorter.pager.css @@ -39,4 +39,4 @@ td.tablesorter-pager { opacity: 0.5; filter: alpha(opacity=50); cursor: default; -} \ No newline at end of file +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/dragtable.mod.css b/vendor/assets/stylesheets/jquery-tablesorter/dragtable.mod.css new file mode 100644 index 0000000..e8b59d3 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/dragtable.mod.css @@ -0,0 +1,64 @@ +/* + * dragtable + * @Version 2.0.14 MOD + * default css + */ +.dragtable-sortable { + list-style-type: none; + margin: 0; + padding: 0; + -moz-user-select: none; + z-index: 10; +} +.dragtable-sortable li { + margin: 0; + padding: 0; + float: left; + font-size: 1em; +} +.dragtable-sortable table { + margin-top: 0; +} +.dragtable-sortable th, .dragtable-sortable td { + border-left: 0px; +} +.dragtable-sortable li:first-child th, .dragtable-sortable li:first-child td { + border-left: 1px solid #CCC; +} +.dragtable-handle-selected { + /* table-handle class while actively dragging a column */ +} +.ui-sortable-helper { + opacity: 0.7; + filter: alpha(opacity=70); +} +.ui-sortable-placeholder { + -moz-box-shadow: 4px 5px 4px rgba(0,0,0,0.2) inset; + -webkit-box-shadow: 4px 5px 4px rgba(0,0,0,0.2) inset; + box-shadow: 4px 5px 4px rgba(0,0,0,0.2) inset; + border-bottom: 1px solid rgba(0,0,0,0.2); + border-top: 1px solid rgba(0,0,0,0.2); + visibility: visible !important; + /* change the background color here to match the tablesorter theme */ + background: #EFEFEF; +} +.ui-sortable-placeholder * { + opacity: 0.0; + visibility: hidden; +} +.table-handle, .table-handle-disabled { + /* background-image: url(images/dragtable-handle.png); */ + /* background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAANAQMAAAC5Li2yAAAABlBMVEUAAAAzMzPI8eYgAAAAAnRSTlMAzORBQ6MAAAAOSURBVAjXYwABByyYAQAQWgFBLN2RnwAAAABJRU5ErkJggg=='); */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjEzIj48cmVjdCBzdHlsZT0iZmlsbDojMzMzO2ZpbGwtb3BhY2l0eTouODsiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIHg9IjEiIHk9IjIiLz4JPHJlY3Qgc3R5bGU9ImZpbGw6IzMzMztmaWxsLW9wYWNpdHk6Ljg7IiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiB4PSIxIiB5PSI0Ii8+CTxyZWN0IHN0eWxlPSJmaWxsOiMzMzM7ZmlsbC1vcGFjaXR5Oi44OyIgd2lkdGg9IjEiIGhlaWdodD0iMSIgeD0iMSIgeT0iNiIvPjxyZWN0IHN0eWxlPSJmaWxsOiMzMzM7ZmlsbC1vcGFjaXR5Oi44OyIgd2lkdGg9IjEiIGhlaWdodD0iMSIgeD0iMSIgeT0iOCIvPjxyZWN0IHN0eWxlPSJmaWxsOiMzMzM7ZmlsbC1vcGFjaXR5Oi44OyIgd2lkdGg9IjEiIGhlaWdodD0iMSIgeD0iMSIgeT0iMTAiLz48L3N2Zz4=); + background-repeat: repeat-x; + height: 13px; + margin: 0 1px; + cursor: move; +} +.table-handle-disabled { + opacity: 0; + cursor: not-allowed; +} +.dragtable-sortable table { + margin-bottom: 0; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/filter.formatter.css b/vendor/assets/stylesheets/jquery-tablesorter/filter.formatter.css new file mode 100644 index 0000000..cb5dde1 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/filter.formatter.css @@ -0,0 +1,183 @@ +/**** Filter Formatter Elements ****/ +.tablesorter .tablesorter-filter-row td { + text-align: center; + font-size: 0.9em; + font-weight: normal; +} + +/**** Sliders ****/ +/* shrink the sliders to look nicer inside of a table cell */ +.tablesorter .ui-slider, .tablesorter input.range { + width: 90%; + margin: 2px auto 2px auto; /* add enough top margin so the tooltips will fit */ + font-size: 0.8em; +} +.tablesorter .ui-slider { + top: 12px; +} +.tablesorter .ui-slider .ui-slider-handle { + width: 0.9em; + height: 0.9em; +} +.tablesorter .ui-datepicker { + font-size: 0.8em; +} +.tablesorter .ui-slider-horizontal { + height: 0.5em; +} +/* Add tooltips to slider handles */ +.tablesorter .value-popup:after { + content : attr(data-value); + position: absolute; + bottom: 14px; + left: -7px; + min-width: 18px; + height: 12px; + background-color: #444; + background-image: -webkit-gradient(linear, left top, left bottom, from(#444444), to(#999999)); + background-image: -webkit-linear-gradient(top, #444, #999); + background-image: -moz-linear-gradient(top, #444, #999); + background-image: -o-linear-gradient(top, #444, #999); + background-image: linear-gradient(to bottom, #444, #999); + -webkit-border-radius: 3px; + border-radius: 3px; + -webkit-background-clip: padding-box; background-clip: padding-box; + -webkit-box-shadow: 0px 0px 4px 0px #777; + box-shadow: 0px 0px 4px 0px #777; + border: #444 1px solid; + color: #fff; + font: 1em/1.1em Arial, Sans-Serif; + padding: 1px; + text-align: center; +} +.tablesorter .value-popup:before { + content: ""; + position: absolute; + width: 0; + height: 0; + border-top: 8px solid #777; + border-left: 8px solid transparent; + border-right: 8px solid transparent; + top: -8px; + left: 50%; + margin-left: -8px; + margin-top: -1px; +} + +/**** Date Picker ****/ +.tablesorter .dateFrom, .tablesorter .dateTo { + width: 80px; + margin: 2px 5px; +} + +/**** Color Picker/HTML5Number Toggle button ****/ +.tablesorter .button { + width: 14px; + height: 14px; + background: #fcfff4; + background: -webkit-linear-gradient(top, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); + background: -moz-linear-gradient(top, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); + background: -o-linear-gradient(top, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); + background: -ms-linear-gradient(top, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); + background: linear-gradient(top, #fcfff4 0%, #dfe5d7 40%, #b3bead 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#fcfff4', endColorstr='#b3bead', GradientType=0 ); + margin: 1px 5px 1px 1px; + -webkit-border-radius: 25px; + -moz-border-radius: 25px; + border-radius: 25px; + -webkit-box-shadow: inset 0px 1px 1px white, 0px 1px 3px rgba(0,0,0,0.5); + -moz-box-shadow: inset 0px 1px 1px white, 0px 1px 3px rgba(0,0,0,0.5); + box-shadow: inset 0px 1px 1px white, 0px 1px 3px rgba(0,0,0,0.5); + position: relative; + top: 3px; + display: inline-block; +} + +.tablesorter .button label { + cursor: pointer; + position: absolute; + width: 10px; + height: 10px; + -webkit-border-radius: 25px; + -moz-border-radius: 25px; + border-radius: 25px; + left: 2px; + top: 2px; + -webkit-box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255,255,255,1); + -moz-box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255,255,255,1); + box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255,255,255,1); + background: #45484d; + background: -webkit-linear-gradient(top, #222 0%, #45484d 100%); + background: -moz-linear-gradient(top, #222 0%, #45484d 100%); + background: -o-linear-gradient(top, #222 0%, #45484d 100%); + background: -ms-linear-gradient(top, #222 0%, #45484d 100%); + background: linear-gradient(top, #222 0%, #45484d 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#222', endColorstr='#45484d', GradientType=0 ); +} + +.tablesorter .button label:after { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + opacity: 0; + content: ''; + position: absolute; + width: 8px; + height: 8px; + background: #55f; + background: -webkit-linear-gradient(top, #aaf 0%, #55f 100%); + background: -moz-linear-gradient(top, #aaf 0%, #55f 100%); + background: -o-linear-gradient(top, #aaf 0%, #55f 100%); + background: -ms-linear-gradient(top, #aaf 0%, #55f 100%); + background: linear-gradient(top, #aaf 0%, #55f 100%); + -webkit-border-radius: 25px; + -moz-border-radius: 25px; + border-radius: 25px; + top: 1px; + left: 1px; + -webkit-box-shadow: inset 0px 1px 1px #fff, 0px 1px 3px rgba(0,0,0,0.5); + -moz-box-shadow: inset 0px 1px 1px #fff, 0px 1px 3px rgba(0,0,0,0.5); + box-shadow: inset 0px 1px 1px #fff, 0px 1px 3px rgba(0,0,0,0.5); +} + +.tablesorter .button label:hover::after { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; + filter: alpha(opacity=30); + opacity: 0.3; +} + +.tablesorter .button input[type=checkbox] { + visibility: hidden; +} + +.tablesorter .button input[type=checkbox]:checked + label:after { + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; + filter: alpha(opacity=100); + opacity: 1; +} + +.tablesorter .colorpicker { + width: 30px; + height: 18px; +} +.tablesorter .ui-spinner-input { + width: 100px; + height: 18px; +} +.tablesorter .currentColor, .tablesorter .ui-spinner { + position: relative; +} +.tablesorter input.number { + position: relative; +} + +/* hide filter row */ +.tablesorter .tablesorter-filter-row.hideme td * { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/highlights.css b/vendor/assets/stylesheets/jquery-tablesorter/highlights.css new file mode 100644 index 0000000..e0b1bd4 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/highlights.css @@ -0,0 +1,104 @@ +/* CSS Column & Row Hightlights - matches the blue theme + * See https://mottie.github.io/tablesorter/docs/example-css-highlighting.html + */ +table.hover-highlight td:before, +table.focus-highlight td:before { + background: #fff; +} + +/* ODD ZEBRA STRIPE color (needs zebra widget) */ +.hover-highlight .odd td:before, .hover-highlight .odd th:before, +.focus-highlight .odd td:before, .focus-highlight .odd th:before { + background: #ebf2fa; +} +/* EVEN ZEBRA STRIPE color (needs zebra widget) */ +.hover-highlight .even td:before, .hover-highlight .even th:before, +.focus-highlight .even td:before, .focus-highlight .even th:before { + background-color: #fff; +} + +/* FOCUS ROW highlight color (touch devices) */ +.focus-highlight td:focus::before, .focus-highlight th:focus::before { + background-color: lightblue; +} +/* FOCUS COLUMN highlight color (touch devices) */ +.focus-highlight td:focus::after, .focus-highlight th:focus::after { + background-color: lightblue; +} +/* FOCUS CELL highlight color */ +.focus-highlight th:focus, .focus-highlight td:focus, +.focus-highlight .even th:focus, .focus-highlight .even td:focus, +.focus-highlight .odd th:focus, .focus-highlight .odd td:focus { + background-color: #d9d9d9; + color: #333; +} + +/* HOVER ROW highlight colors */ +table.hover-highlight tbody > tr:hover > td, /* override tablesorter theme row hover */ +table.hover-highlight tbody > tr.odd:hover > td, +table.hover-highlight tbody > tr.even:hover > td { + background-color: #ffa; +} +/* HOVER COLUMN highlight colors */ +.hover-highlight tbody tr td:hover::after, +.hover-highlight tbody tr th:hover::after { + background-color: #ffa; +} + +/* ************************************************* */ +/* **** No need to modify the definitions below **** */ +/* ************************************************* */ +.focus-highlight td:focus::after, .focus-highlight th:focus::after, +.hover-highlight td:hover::after, .hover-highlight th:hover::after { + content: ''; + position: absolute; + width: 100%; + height: 999em; + left: 0; + top: -555em; + z-index: -1; +} +.focus-highlight td:focus::before, .focus-highlight th:focus::before { + content: ''; + position: absolute; + width: 999em; + height: 100%; + left: -555em; + top: 0; + z-index: -2; +} +/* required styles */ +.hover-highlight, +.focus-highlight { + overflow: hidden; +} +.hover-highlight td, .hover-highlight th, +.focus-highlight td, .focus-highlight th { + position: relative; + outline: 0; +} +/* override the tablesorter theme styling */ +table.hover-highlight, table.hover-highlight tbody > tr > td, +table.focus-highlight, table.focus-highlight tbody > tr > td, +/* override zebra styling */ +table.hover-highlight tbody tr.even > th, +table.hover-highlight tbody tr.even > td, +table.hover-highlight tbody tr.odd > th, +table.hover-highlight tbody tr.odd > td, +table.focus-highlight tbody tr.even > th, +table.focus-highlight tbody tr.even > td, +table.focus-highlight tbody tr.odd > th, +table.focus-highlight tbody tr.odd > td { + background: transparent; +} +/* table background positioned under the highlight */ +table.hover-highlight td:before, +table.focus-highlight td:before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + left: 0; + top: 0; + z-index: -3; +} \ No newline at end of file diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.black-ice.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.black-ice.css index d1b8809..51cde9f 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.black-ice.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.black-ice.css @@ -17,7 +17,8 @@ .tablesorter-blackice th, .tablesorter-blackice thead td { padding: 4px; - font: bold 13px/20px Arial, Sans-serif; + font: 13px/20px Arial, Sans-serif; + font-weight: bold; color: #e5e5e5; text-align: left; text-shadow: 0 1px 0 rgba(0, 0, 0, 0.7); @@ -46,6 +47,7 @@ } .tablesorter-blackice thead .sorter-false { background-image: none; + cursor: default; padding: 4px; } @@ -68,25 +70,26 @@ } /* hovered row colors */ +.tablesorter-blackice tbody > tr.hover > td, .tablesorter-blackice tbody > tr:hover > td, .tablesorter-blackice tbody > tr.even:hover > td, .tablesorter-blackice tbody > tr.odd:hover > td { - background: #000; + background-color: #000; } /* table processing indicator */ .tablesorter-blackice .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-blackice tr.odd td { +.tablesorter-blackice tr.odd > td { background-color: #333; } -.tablesorter-blackice tr.even td { +.tablesorter-blackice tr.even > td { background-color: #393939; } @@ -113,9 +116,17 @@ background-color: #5a646b; } +/* caption */ +.tablesorter-blackice > caption { + background-color: #fff; +} + /* filter widget */ +.tablesorter-blackice .tablesorter-filter-row { + background-color: #222; +} .tablesorter-blackice .tablesorter-filter-row td { - background: #222; + background-color: #222; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -140,7 +151,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-blackice .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-blackice .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -151,9 +162,10 @@ filter: alpha(opacity=0); } /* filters */ -.tablesorter-blackice .tablesorter-filter { +.tablesorter-blackice input.tablesorter-filter, +.tablesorter-blackice select.tablesorter-filter { width: 98%; - height: inherit; + height: auto; margin: 0; padding: 4px; background-color: #fff; @@ -167,3 +179,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.blue.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.blue.css index d90360a..da90a81 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.blue.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.blue.css @@ -20,7 +20,8 @@ /* header */ .tablesorter-blue th, .tablesorter-blue thead td { - font: bold 12px/18px Arial, Sans-serif; + font: 12px/18px Arial, Sans-serif; + font-weight: bold; color: #000; background-color: #99bfe6; border-collapse: collapse; @@ -40,7 +41,7 @@ /* white (unsorted) double arrow */ /* background-image: url(data:image/gif;base64,R0lGODlhFQAJAIAAAP///////yH5BAEAAAEALAAAAAAVAAkAAAIXjI+AywnaYnhUMoqt3gZXPmVg94yJVQAAOw==); */ /* image */ - /* background-image: url(/assets/jquery-tablesorter/black-unsorted.gif); */ + /* background-image: url(images/black-unsorted.gif); */ background-repeat: no-repeat; background-position: center right; padding: 4px 18px 4px 4px; @@ -56,7 +57,7 @@ /* white asc arrow */ /* background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); */ /* image */ - /* background-image: url(/assets/jquery-tablesorter/black-asc.gif); */ + /* background-image: url(images/black-asc.gif); */ } .tablesorter-blue .headerSortDown, .tablesorter-blue .tablesorter-headerSortDown, @@ -67,10 +68,11 @@ /* white desc arrow */ /* background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); */ /* image */ - /* background-image: url(/assets/jquery-tablesorter/black-desc.gif); */ + /* background-image: url(images/black-desc.gif); */ } .tablesorter-blue thead .sorter-false { background-image: none; + cursor: default; padding: 4px; } @@ -95,33 +97,36 @@ you'll need to add additional lines for rows with more than 2 child rows */ +.tablesorter-blue tbody > tr.hover > td, .tablesorter-blue tbody > tr:hover > td, .tablesorter-blue tbody > tr:hover + tr.tablesorter-childRow > td, .tablesorter-blue tbody > tr:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td, +.tablesorter-blue tbody > tr.even.hover > td, .tablesorter-blue tbody > tr.even:hover > td, .tablesorter-blue tbody > tr.even:hover + tr.tablesorter-childRow > td, .tablesorter-blue tbody > tr.even:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td { - background: #d9d9d9; + background-color: #d9d9d9; } +.tablesorter-blue tbody > tr.odd.hover > td, .tablesorter-blue tbody > tr.odd:hover > td, .tablesorter-blue tbody > tr.odd:hover + tr.tablesorter-childRow > td, .tablesorter-blue tbody > tr.odd:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td { - background: #bfbfbf; + background-color: #bfbfbf; } /* table processing indicator */ .tablesorter-blue .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-blue tbody tr.odd td { +.tablesorter-blue tbody tr.odd > td { background-color: #ebf2fa; } -.tablesorter-blue tbody tr.even td { +.tablesorter-blue tbody tr.even > td { background-color: #fff; } @@ -148,9 +153,17 @@ background-color: #ebf0fa; } +/* caption */ +.tablesorter-blue > caption { + background-color: #fff; +} + /* filter widget */ +.tablesorter-blue .tablesorter-filter-row { + background-color: #eee; +} .tablesorter-blue .tablesorter-filter-row td { - background: #eee; + background-color: #eee; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -175,7 +188,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-blue .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-blue .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -186,9 +199,10 @@ filter: alpha(opacity=0); } /* filters */ -.tablesorter-blue .tablesorter-filter { +.tablesorter-blue input.tablesorter-filter, +.tablesorter-blue select.tablesorter-filter { width: 98%; - height: inherit; + height: auto; margin: 0; padding: 4px; background-color: #fff; @@ -202,3 +216,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap.css index eaf9b30..60cc60c 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap.css @@ -1,43 +1,45 @@ -/************* - Bootstrap theme - *************/ -/* jQuery Bootstrap Theme */ +/** + * Bootstrap theme v3.x + * + * WARNING!... once a stable Bootstrap v4.x is released, + * this file will be removed; use theme.bootstrap_3.css + */ .tablesorter-bootstrap { width: 100%; } -.tablesorter-bootstrap .tablesorter-header, +.tablesorter-bootstrap thead th, +.tablesorter-bootstrap thead td, .tablesorter-bootstrap tfoot th, .tablesorter-bootstrap tfoot td { - font: bold 14px/20px Arial, Sans-serif; - position: relative; - padding: 8px; + font: 14px/20px Arial, Sans-serif; + font-weight: bold; + padding: 4px; margin: 0 0 18px; - list-style: none; - background-color: #FBFBFB; - background-image: -moz-linear-gradient(top, white, #efefef); - background-image: -ms-linear-gradient(top, white, #efefef); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(white), to(#efefef)); - background-image: -webkit-linear-gradient(top, white, #efefef); - background-image: -o-linear-gradient(top, white, #efefef); - background-image: linear-gradient(to bottom, white, #efefef); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#efefef', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 1px 0 white; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 white; + background-color: #eee; } .tablesorter-bootstrap .tablesorter-header { cursor: pointer; } +.tablesorter-bootstrap .sorter-false { + cursor: default; +} + +.tablesorter-bootstrap .tablesorter-header.sorter-false i.tablesorter-icon { + display: none; +} .tablesorter-bootstrap .tablesorter-header-inner { position: relative; padding: 4px 18px 4px 4px; } +.tablesorter-bootstrap .sorter-false .tablesorter-header-inner { + padding: 4px; +} /* bootstrap uses <i> for icons */ -.tablesorter-bootstrap .tablesorter-header i { +.tablesorter-bootstrap .tablesorter-header i.tablesorter-icon { + font-size: 11px; position: absolute; right: 2px; top: 50%; @@ -48,36 +50,75 @@ line-height: 14px; display: inline-block; } + +/* black unsorted icon */ .tablesorter-bootstrap .bootstrap-icon-unsorted { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWVJREFUeNqUUL9Lw2AUTGP8mqGlpBQkNeCSRcckEBcHq1jImMElToKuDvpHFMGhU0BQcHBwLji6CE1B4uB/INQsDi4d2jQ/fPeZxo764OV6915f7lLJ81xot9tCURXqdVEUr7IsO6ffH9Q5BlEUCaLwWxWqTcbYnaIoh0Dw4gAvcWlxq1qt9hqNxg6hUGAP+uIPUrGs0qXLer2+v/pTX6QpxLtkc2U2m53ACb8sSdIDXerSEms2m6+DweAICA4d89KGbduf9MpEVdXQ9/2LVqv1CASHjjn3iq/x1xKFfxQPqGnada1W86bT6SiO42OS3qk3KPStLMvbk8nkfjwen/LLuq6blFymMB0KdUPSGhAcOualjX6/f0bCiC7NaWGPQr0BwaFjzn0gYJqmLAiCA8/zni3LmhuGkQPBoWPOPwQeaPIqD4fDruu6L6Zp5kBw6IudchmdJAkLw3DXcZwnIPjy/FuAAQCiqqWWCAFKcwAAAABJRU5ErkJggg==); + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAA20lEQVR4AWJABpKSkoxALCstLb0aUAsZaCAMhVEY6B0amx8YZWDDEDSBa2AGe7XeIiAAClYwVGBvsAcIllsf/mvcC9DgOOd8h90fxWvngVEUbZIkuWRZZlE8eQjcisgZMM9zi+LJ6ZfwegmWZflZDugdHMfxTcGqql7TNBlUB/QObtv2VBSFrev6OY7jngzFk9OT/fn73fWYpqnlXNyXDMWT0zuYx/Bvel9ej+LJ6R08DMOu67q7DkTkrSA5vYPneV71fX/QASdTkJwezhs0TfMARn0wMDDGXEPgF4oijqwM5YjNAAAAAElFTkSuQmCC); +} + +/* white unsorted icon; updated to use bootstrap-icon-white - see #1432 */ +.tablesorter-bootstrap .bootstrap-icon-white.bootstrap-icon-unsorted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAAe0lEQVR4AbXQoRWDMBiF0Sh2QLAAQ8SxJGugWSA6A2STW1PxTsnB9cnkfuYvv8OGC1t5G3Y0QMP+Bm857keAdQIzWBP3+Bw4MADQE18B6/etRnCV/w9nnGuLezfAmXhABGtAGIkruvk6auIFRwQJDywllsEAjCecB20GP59BQQ+gtlRLAAAAAElFTkSuQmCC); } /* since bootstrap (table-striped) uses nth-child(), we just use this to add a zebra stripe color */ -.tablesorter-bootstrap tr.odd td { +.tablesorter-bootstrap > tbody > tr.odd > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.odd:hover ~ tr.tablesorter-hasChildRow.odd ~ .tablesorter-childRow.odd > td { background-color: #f9f9f9; } -.tablesorter-bootstrap tbody > .odd:hover > td, -.tablesorter-bootstrap tbody > .even:hover > td { +.tablesorter-bootstrap > tbody > tr.hover > td, +.tablesorter-bootstrap > tbody > tr.odd:hover > td, +.tablesorter-bootstrap > tbody > tr.even:hover > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.odd:hover ~ .tablesorter-childRow.odd > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.even:hover ~ .tablesorter-childRow.even > td { background-color: #f5f5f5; } -.tablesorter-bootstrap tr.even td { +.tablesorter-bootstrap > tbody > tr.even > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.even:hover ~ tr.tablesorter-hasChildRow.even ~ .tablesorter-childRow.even > td { background-color: #fff; } /* processing icon */ .tablesorter-bootstrap .tablesorter-processing { background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs='); - position: absolute; - z-index: 1000; + background-position: center center !important; + background-repeat: no-repeat !important; +} + +/* Column Widget - column sort colors */ +.tablesorter-bootstrap > tbody > tr.odd td.primary { + background-color: #bfbfbf; +} +.tablesorter-bootstrap > tbody > tr td.primary, +.tablesorter-bootstrap > tbody > tr.even td.primary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap > tbody > tr.odd td.secondary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap > tbody > tr td.secondary, +.tablesorter-bootstrap > tbody > tr.even td.secondary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap > tbody > tr.odd td.tertiary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap > tbody > tr td.tertiary, +.tablesorter-bootstrap > tbody > tr.even td.tertiary { + background-color: #f2f2f2; +} + +/* caption */ +.tablesorter-bootstrap > .caption { + background-color: #fff; } /* filter widget */ -.tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter { +.tablesorter-bootstrap .tablesorter-filter-row input.tablesorter-filter, +.tablesorter-bootstrap .tablesorter-filter-row select.tablesorter-filter { width: 98%; - height: inherit; - margin: 0 auto; + margin: 0; padding: 4px 6px; - background-color: #fff; color: #333; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; @@ -87,8 +128,21 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +.tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter.disabled { + background-color: #eee; + color: #555; + cursor: not-allowed; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset; + box-sizing: border-box; + transition: height 0.1s ease; +} +.tablesorter-bootstrap .tablesorter-filter-row { + background-color: #efefef; +} .tablesorter-bootstrap .tablesorter-filter-row td { - background: #eee; + background-color: #efefef; line-height: normal; text-align: center; padding: 4px 6px; @@ -104,7 +158,7 @@ margin: 0; line-height: 0; } -.tablesorter-bootstrap .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-bootstrap .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -114,6 +168,10 @@ opacity: 0; filter: alpha(opacity=0); } +/* rows hidden by filtering */ +.tablesorter .filtered { + display: none; +} /* pager plugin */ .tablesorter-bootstrap .tablesorter-pager select { @@ -121,4 +179,15 @@ } .tablesorter-bootstrap .tablesorter-pager .pagedisplay { border: 0; -} \ No newline at end of file +} +/* tfoot i for pager controls */ +.tablesorter-bootstrap tfoot i { + font-size: 11px; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_2.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_2.css new file mode 100644 index 0000000..2d49ba4 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_2.css @@ -0,0 +1,188 @@ +/** + * Bootstrap theme v2.x + */ +.tablesorter-bootstrap { + width: 100%; +} +.tablesorter-bootstrap .tablesorter-header, +.tablesorter-bootstrap tfoot th, +.tablesorter-bootstrap tfoot td { + font: 14px/20px Arial, Sans-serif; + font-weight: bold; + position: relative; + padding: 8px; + margin: 0 0 18px; + list-style: none; + background-color: #FBFBFB; + background-image: -moz-linear-gradient(top, white, #efefef); + background-image: -ms-linear-gradient(top, white, #efefef); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(white), to(#efefef)); + background-image: -webkit-linear-gradient(top, white, #efefef); + background-image: -o-linear-gradient(top, white, #efefef); + background-image: linear-gradient(to bottom, white, #efefef); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#efefef', GradientType=0); + background-repeat: repeat-x; + -webkit-box-shadow: inset 0 1px 0 white; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 white; +} + +.tablesorter-bootstrap .tablesorter-header { + cursor: pointer; +} +.tablesorter-bootstrap .sorter-false { + cursor: default; +} +.tablesorter-bootstrap .tablesorter-header.sorter-false i.tablesorter-icon { + display: none; +} + +.tablesorter-bootstrap .tablesorter-header-inner { + position: relative; + padding: 4px 18px 4px 4px; +} +.tablesorter-bootstrap .sorter-false .tablesorter-header-inner { + padding: 4px; +} + +/* bootstrap uses <i> for icons */ +.tablesorter-bootstrap .tablesorter-header i.tablesorter-icon { + position: absolute; + right: 2px; + top: 50%; + margin-top: -7px; /* half the icon height; older IE doesn't like this */ + width: 14px; + height: 14px; + background-repeat: no-repeat; + line-height: 14px; + display: inline-block; +} + +/* black unsorted icon */ +.tablesorter-bootstrap .bootstrap-icon-unsorted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAABGUlEQVR4AZWQAUfEYBzGC07fILfrbnfgqG1jmxliDitmYIaxb9Y3CEIEVHDaUGMfYF8gIyBqbd7eH7wWofB6/J7nN+x/IIRQbz6fH8p3slgsrkl4uv8QNU071XX9wTAMQcLTD6bi2WazubUsq3ddV5AwPftU1tbr9Z0UPhGDIHgjYXp2JS+Xy71t2wNCFEV113UxCdOzKznLshvf9z+SJHlp23ZHR8L07Er+6/u/LO96td1u3zmX/BmdjoTp2ZUchmHted4o/16sVqt7KR6TMD27kpumOc/z/EkOvWmaQp7rlYTp2ZU8juOsqqqLoij2UvhyHEeQMD27knl93x+VZXmZpukz9yVh+l+vMQzDrK7rXRzHjyQ83b8BlglBGLw1Kb4AAAAASUVORK5CYII=); +} + +/* white unsorted icon */ +.tablesorter-bootstrap .icon-white.bootstrap-icon-unsorted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAAe0lEQVR4AbXQoRWDMBiF0Sh2QLAAQ8SxJGugWSA6A2STW1PxTsnB9cnkfuYvv8OGC1t5G3Y0QMP+Bm857keAdQIzWBP3+Bw4MADQE18B6/etRnCV/w9nnGuLezfAmXhABGtAGIkruvk6auIFRwQJDywllsEAjCecB20GP59BQQ+gtlRLAAAAAElFTkSuQmCC); +} + +/* since bootstrap (table-striped) uses nth-child(), we just use this to add a zebra stripe color */ +.tablesorter-bootstrap tr.odd > td { + background-color: #f9f9f9; +} +.tablesorter-bootstrap tbody > tr.hover > td, +.tablesorter-bootstrap tbody > .odd:hover > td, +.tablesorter-bootstrap tbody > .even:hover > td { + background-color: #f5f5f5; +} +.tablesorter-bootstrap tbody > tr.even > td { + background-color: #fff; +} + +/* processing icon */ +.tablesorter-bootstrap .tablesorter-processing { + background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs='); + position: absolute; + z-index: 1000; +} + +/* Column Widget - column sort colors */ +.tablesorter-bootstrap > tbody > tr.odd td.primary { + background-color: #bfbfbf; +} +.tablesorter-bootstrap > tbody > tr td.primary, +.tablesorter-bootstrap > tbody > tr.even td.primary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap > tbody > tr.odd td.secondary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap > tbody > tr td.secondary, +.tablesorter-bootstrap > tbody > tr.even td.secondary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap > tbody > tr.odd td.tertiary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap > tbody > tr td.tertiary, +.tablesorter-bootstrap > tbody > tr.even td.tertiary { + background-color: #f2f2f2; +} + +/* caption */ +.tablesorter-bootstrap > caption { + background-color: #fff; +} + +/* filter widget */ +.tablesorter-bootstrap .tablesorter-filter-row input.tablesorter-filter, +.tablesorter-bootstrap .tablesorter-filter-row select.tablesorter-filter { + height: 28px; + width: 98%; + margin: 0; + padding: 4px 6px; + background-color: #fff; + color: #333; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: height 0.1s ease; + -moz-transition: height 0.1s ease; + -o-transition: height 0.1s ease; + transition: height 0.1s ease; +} +.tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter.disabled { + background-color: #eee; + cursor: not-allowed; +} +.tablesorter-bootstrap .tablesorter-filter-row { + background-color: #ddd; +} +.tablesorter-bootstrap .tablesorter-filter-row td { + background-color: #eee; + line-height: normal; + text-align: center; + padding: 4px 6px; + vertical-align: middle; + -webkit-transition: line-height 0.1s ease; + -moz-transition: line-height 0.1s ease; + -o-transition: line-height 0.1s ease; + transition: line-height 0.1s ease; +} +/* hidden filter row */ +.tablesorter-bootstrap tr.tablesorter-filter-row.hideme td { + padding: 2px; /* change this to modify the thickness of the closed border row */ + margin: 0; + line-height: 0; +} +.tablesorter-bootstrap tr.tablesorter-filter-row.hideme * { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} +/* rows hidden by filtering */ +.tablesorter .filtered { + display: none; +} + +/* pager plugin */ +.tablesorter-bootstrap .tablesorter-pager select { + padding: 4px 6px; +} +.tablesorter-bootstrap .tablesorter-pager .pagedisplay { + border: 0; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_3.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_3.css new file mode 100644 index 0000000..ec5ef05 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_3.css @@ -0,0 +1,190 @@ +/** + * Bootstrap theme v3.x + */ +.tablesorter-bootstrap { + width: 100%; +} +.tablesorter-bootstrap thead th, +.tablesorter-bootstrap thead td, +.tablesorter-bootstrap tfoot th, +.tablesorter-bootstrap tfoot td { + font: 14px/20px Arial, Sans-serif; + font-weight: bold; + padding: 4px; + margin: 0 0 18px; + background-color: #eee; +} + +.tablesorter-bootstrap .tablesorter-header { + cursor: pointer; +} +.tablesorter-bootstrap .sorter-false { + cursor: default; +} + +.tablesorter-bootstrap .tablesorter-header.sorter-false i.tablesorter-icon { + display: none; +} + +.tablesorter-bootstrap .tablesorter-header-inner { + position: relative; + padding: 4px 18px 4px 4px; +} +.tablesorter-bootstrap .sorter-false .tablesorter-header-inner { + padding: 4px; +} + +/* bootstrap uses <i> for icons */ +.tablesorter-bootstrap .tablesorter-header i.tablesorter-icon { + font-size: 11px; + position: absolute; + right: 2px; + top: 50%; + margin-top: -7px; /* half the icon height; older IE doesn't like this */ + width: 14px; + height: 14px; + background-repeat: no-repeat; + line-height: 14px; + display: inline-block; +} + +/* black unsorted icon */ +.tablesorter-bootstrap .bootstrap-icon-unsorted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAA20lEQVR4AWJABpKSkoxALCstLb0aUAsZaCAMhVEY6B0amx8YZWDDEDSBa2AGe7XeIiAAClYwVGBvsAcIllsf/mvcC9DgOOd8h90fxWvngVEUbZIkuWRZZlE8eQjcisgZMM9zi+LJ6ZfwegmWZflZDugdHMfxTcGqql7TNBlUB/QObtv2VBSFrev6OY7jngzFk9OT/fn73fWYpqnlXNyXDMWT0zuYx/Bvel9ej+LJ6R08DMOu67q7DkTkrSA5vYPneV71fX/QASdTkJwezhs0TfMARn0wMDDGXEPgF4oijqwM5YjNAAAAAElFTkSuQmCC); +} + +/* white unsorted icon; updated to use bootstrap-icon-white - see #1432 */ +.tablesorter-bootstrap .bootstrap-icon-white.bootstrap-icon-unsorted { + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAOCAYAAAD5YeaVAAAAe0lEQVR4AbXQoRWDMBiF0Sh2QLAAQ8SxJGugWSA6A2STW1PxTsnB9cnkfuYvv8OGC1t5G3Y0QMP+Bm857keAdQIzWBP3+Bw4MADQE18B6/etRnCV/w9nnGuLezfAmXhABGtAGIkruvk6auIFRwQJDywllsEAjCecB20GP59BQQ+gtlRLAAAAAElFTkSuQmCC); +} + +/* since bootstrap (table-striped) uses nth-child(), we just use this to add a zebra stripe color */ +.tablesorter-bootstrap > tbody > tr.odd > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.odd:hover ~ tr.tablesorter-hasChildRow.odd ~ .tablesorter-childRow.odd > td { + background-color: #f9f9f9; +} +.tablesorter-bootstrap > tbody > tr.hover > td, +.tablesorter-bootstrap > tbody > tr.odd:hover > td, +.tablesorter-bootstrap > tbody > tr.even:hover > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.odd:hover ~ .tablesorter-childRow.odd > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.even:hover ~ .tablesorter-childRow.even > td { + background-color: #f5f5f5; +} +.tablesorter-bootstrap > tbody > tr.even > td, +.tablesorter-bootstrap > tbody > tr.tablesorter-hasChildRow.even:hover ~ tr.tablesorter-hasChildRow.even ~ .tablesorter-childRow.even > td { + background-color: #fff; +} + +/* processing icon */ +.tablesorter-bootstrap .tablesorter-processing { + background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs='); + background-position: center center !important; + background-repeat: no-repeat !important; +} + +/* Column Widget - column sort colors */ +.tablesorter-bootstrap > tbody > tr.odd td.primary { + background-color: #bfbfbf; +} +.tablesorter-bootstrap > tbody > tr td.primary, +.tablesorter-bootstrap > tbody > tr.even td.primary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap > tbody > tr.odd td.secondary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap > tbody > tr td.secondary, +.tablesorter-bootstrap > tbody > tr.even td.secondary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap > tbody > tr.odd td.tertiary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap > tbody > tr td.tertiary, +.tablesorter-bootstrap > tbody > tr.even td.tertiary { + background-color: #f2f2f2; +} + +/* caption */ +.tablesorter-bootstrap > .caption { + background-color: #fff; +} + +/* filter widget */ +.tablesorter-bootstrap .tablesorter-filter-row input.tablesorter-filter, +.tablesorter-bootstrap .tablesorter-filter-row select.tablesorter-filter { + width: 98%; + margin: 0; + padding: 4px 6px; + color: #333; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: height 0.1s ease; + -moz-transition: height 0.1s ease; + -o-transition: height 0.1s ease; + transition: height 0.1s ease; +} +.tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter.disabled { + background-color: #eee; + color: #555; + cursor: not-allowed; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset; + box-sizing: border-box; + transition: height 0.1s ease; +} +.tablesorter-bootstrap .tablesorter-filter-row { + background-color: #efefef; +} +.tablesorter-bootstrap .tablesorter-filter-row td { + background-color: #efefef; + line-height: normal; + text-align: center; + padding: 4px 6px; + vertical-align: middle; + -webkit-transition: line-height 0.1s ease; + -moz-transition: line-height 0.1s ease; + -o-transition: line-height 0.1s ease; + transition: line-height 0.1s ease; +} +/* hidden filter row */ +.tablesorter-bootstrap .tablesorter-filter-row.hideme td { + padding: 2px; /* change this to modify the thickness of the closed border row */ + margin: 0; + line-height: 0; +} +.tablesorter-bootstrap .tablesorter-filter-row.hideme * { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} +/* rows hidden by filtering */ +.tablesorter .filtered { + display: none; +} + +/* pager plugin */ +.tablesorter-bootstrap .tablesorter-pager select { + padding: 4px 6px; +} +.tablesorter-bootstrap .tablesorter-pager .pagedisplay { + border: 0; +} +/* tfoot i for pager controls */ +.tablesorter-bootstrap tfoot i { + font-size: 11px; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_4.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_4.css new file mode 100644 index 0000000..3f1fa73 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.bootstrap_4.css @@ -0,0 +1,188 @@ +/** + * Bootstrap theme v4.x + */ +.tablesorter-bootstrap { + width: 100%; +} +.tablesorter-bootstrap thead th, +.tablesorter-bootstrap thead td, +.tablesorter-bootstrap tfoot th, +.tablesorter-bootstrap tfoot td { + font: 14px/20px Arial, Sans-serif; + font-weight: bold; + padding: 4px; + margin: 0 0 18px; +} + +.tablesorter-bootstrap thead .tablesorter-header { + background-position: right 5px center; + background-repeat: no-repeat; + cursor: pointer; + white-space: normal; +} +.tablesorter-bootstrap:not(.table-dark) thead:not(.thead-dark) .tablesorter-header, +.tablesorter-bootstrap:not(.table-dark) tfoot th, +.tablesorter-bootstrap:not(.table-dark) tfoot td { + background-color: #eee; +} + +.tablesorter-bootstrap thead .sorter-false { + cursor: default; + background-image: none; +} + +.tablesorter-bootstrap .tablesorter-header-inner { + position: relative; + padding: 4px 18px 4px 4px; +} +.tablesorter-bootstrap .sorter-false .tablesorter-header-inner { + padding: 4px; +} + +/* black icons */ +.tablesorter-bootstrap thead .tablesorter-headerUnSorted:not(.sorter-false) { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE0IDIwIj48cGF0aCBkPSJNMTQgMTNsLTIuNS0yLjVMNyAxNWwtNC41LTQuNUwwIDEzbDcgN3pNMTQgNy41TDExLjUgMTAgNyA1LjUgMi41IDEwIDAgNy41bDctN3oiLz48L3N2Zz4=); +} +.tablesorter-bootstrap thead .tablesorter-headerAsc { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDE0IDE0Ij48cGF0aCBkPSJNMTQgOS41TDExLjUgMTIgNyA3LjUgMi41IDEyIDAgOS41bDctN3oiLz48L3N2Zz4=); +} +.tablesorter-bootstrap thead .tablesorter-headerDesc { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDE0IDE0Ij48cGF0aCBkPSJNMTQgNWwtMi41LTIuNS00LjUgNC41LTQuNS00LjVMMCA1IDcgMTJ6Ii8+PC9zdmc+); +} + +/* white icons */ +.tablesorter-bootstrap thead.thead-dark .tablesorter-headerUnSorted:not(.sorter-false), +.tablesorter-bootstrap.table-dark thead .tablesorter-headerUnSorted:not(.sorter-false) { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE0IDIwIj48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQgMTNsLTIuNS0yLjVMNyAxNWwtNC41LTQuNUwwIDEzbDcgN3pNMTQgNy41TDExLjUgMTAgNyA1LjUgMi41IDEwIDAgNy41bDctN3oiLz48L3N2Zz4=); +} +.tablesorter-bootstrap thead.thead-dark .tablesorter-headerAsc, +.tablesorter-bootstrap.table-dark thead .tablesorter-headerAsc { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDE0IDE0Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQgOS41TDExLjUgMTIgNyA3LjUgMi41IDEyIDAgOS41bDctN3oiLz48L3N2Zz4=); +} +.tablesorter-bootstrap thead.thead-dark .tablesorter-headerDesc, +.tablesorter-bootstrap.table-dark thead .tablesorter-headerDesc { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMiIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDE0IDE0Ij48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTQgNWwtMi41LTIuNS00LjUgNC41LTQuNS00LjVMMCA1IDcgMTJ6Ii8+PC9zdmc+); +} + +/* since bootstrap (table-striped) uses nth-child(), we just use this to add a zebra stripe color */ +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.odd > td, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.tablesorter-hasChildRow.odd:hover ~ tr.tablesorter-hasChildRow.odd ~ .tablesorter-childRow.odd > td { + background-color: #f9f9f9; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.hover > td, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.odd:hover > td, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.even:hover > td, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.tablesorter-hasChildRow.odd:hover ~ .tablesorter-childRow.odd > td, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.tablesorter-hasChildRow.even:hover ~ .tablesorter-childRow.even > td { + background-color: #f5f5f5; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.even > td, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.tablesorter-hasChildRow.even:hover ~ tr.tablesorter-hasChildRow.even ~ .tablesorter-childRow.even > td { + background-color: #fff; +} + +/* processing icon */ +.tablesorter-bootstrap .tablesorter-processing { + background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs='); + background-position: center center !important; + background-repeat: no-repeat !important; +} + +/* Column Widget - column sort colors */ +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.odd td.primary { + background-color: #bfbfbf; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr td.primary, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.even td.primary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.odd td.secondary { + background-color: #d9d9d9; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr td.secondary, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.even td.secondary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.odd td.tertiary { + background-color: #e6e6e6; +} +.tablesorter-bootstrap:not(.table-dark) > tbody > tr td.tertiary, +.tablesorter-bootstrap:not(.table-dark) > tbody > tr.even td.tertiary { + background-color: #f2f2f2; +} + +/* caption */ +.tablesorter-bootstrap:not(.table-dark) > .caption { + background-color: #fff; +} + +/* filter widget */ +.tablesorter-bootstrap .tablesorter-filter-row input.tablesorter-filter, +.tablesorter-bootstrap .tablesorter-filter-row select.tablesorter-filter { + width: 98%; + margin: 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: height 0.1s ease; + -moz-transition: height 0.1s ease; + -o-transition: height 0.1s ease; + transition: height 0.1s ease; +} +.tablesorter-bootstrap:not(.table-dark) .tablesorter-filter-row { + background-color: #efefef; +} +.tablesorter-bootstrap:not(.table-dark) .tablesorter-filter-row input.tablesorter-filter, +.tablesorter-bootstrap:not(.table-dark) .tablesorter-filter-row select.tablesorter-filter { + color: #333; +} + +.tablesorter-bootstrap .tablesorter-filter-row .tablesorter-filter.disabled { + cursor: not-allowed; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset; + box-sizing: border-box; + transition: height 0.1s ease; +} + +.tablesorter-bootstrap:not(.table-dark) .tablesorter-filter-row td { + line-height: normal; + text-align: center; + padding: 4px 6px; + vertical-align: middle; + -webkit-transition: line-height 0.1s ease; + -moz-transition: line-height 0.1s ease; + -o-transition: line-height 0.1s ease; + transition: line-height 0.1s ease; +} +/* hidden filter row */ +.tablesorter-bootstrap .tablesorter-filter-row.hideme td { + padding: 2px; /* change this to modify the thickness of the closed border row */ + margin: 0; + line-height: 0; +} +.tablesorter-bootstrap .tablesorter-filter-row.hideme * { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} +/* rows hidden by filtering */ +.tablesorter .filtered { + display: none; +} + +/* pager plugin */ +.tablesorter-bootstrap .tablesorter-pager .pagedisplay { + border: 0; +} + +/* ajax error row */ +.tablesorter:not(.table-dark) .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.dark.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.dark.css index 93292bc..91c0660 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.dark.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.dark.css @@ -15,7 +15,8 @@ .tablesorter-dark th, .tablesorter-dark thead td { padding: 4px; - font: bold 12px/20px Arial, Sans-serif; + font: 12px/20px Arial, Sans-serif; + font-weight: bold; color: #fff; background-color: #000; border-collapse: collapse; @@ -45,6 +46,7 @@ } .tablesorter-dark thead .sorter-false { background-image: none; + cursor: default; padding: 4px; } @@ -67,25 +69,26 @@ } /* hovered row colors */ +.tablesorter-dark tbody > tr.hover > td, .tablesorter-dark tbody > tr:hover > td, .tablesorter-dark tbody > tr.even:hover > td, .tablesorter-dark tbody > tr.odd:hover > td { - background: #000; + background-color: #000; } /* table processing indicator */ .tablesorter-dark .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-dark tr.odd td { +.tablesorter-dark tr.odd > td { background-color: #202020; } -.tablesorter-dark tr.even td { +.tablesorter-dark tr.even > td { background-color: #101010; } @@ -112,9 +115,17 @@ background-color: #0f0f0f; } +/* caption */ +.tablesorter-dark > caption { + background-color: #202020; +} + /* filter widget */ +.tablesorter-dark .tablesorter-filter-row { + background-color: #202020; +} .tablesorter-dark .tablesorter-filter-row td { - background: #202020; + background-color: #202020; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -140,7 +151,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-dark .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-dark .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -152,10 +163,11 @@ } /* filters */ -.tablesorter-dark .tablesorter-filter { +.tablesorter-dark input.tablesorter-filter, +.tablesorter-dark select.tablesorter-filter { width: 98%; - height: inherit; - margin: 4px; + height: auto; + margin: 0; padding: 4px; background-color: #111; border: 1px solid #222; @@ -168,3 +180,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.default.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.default.css index 9efd03e..f3d271d 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.default.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.default.css @@ -15,14 +15,15 @@ Default Theme /* header */ .tablesorter-default th, .tablesorter-default thead td { - font: bold 12px/18px Arial, Sans-serif; + font-weight: bold; color: #000; background-color: #fff; border-collapse: collapse; border-bottom: #ccc 2px solid; padding: 0; } -.tablesorter-default tfoot th { +.tablesorter-default tfoot th, +.tablesorter-default tfoot td { border: 0; } .tablesorter-default .header, @@ -48,6 +49,7 @@ Default Theme } .tablesorter-default thead .sorter-false { background-image: none; + cursor: default; padding: 4px; } @@ -68,10 +70,11 @@ Default Theme } /* hovered row colors */ +.tablesorter-default tbody > tr.hover > td, .tablesorter-default tbody > tr:hover > td, .tablesorter-default tbody > tr.even:hover > td, .tablesorter-default tbody > tr.odd:hover > td { - background: #fff; + background-color: #fff; color: #000; } @@ -79,15 +82,15 @@ Default Theme .tablesorter-default .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-default tr.odd td { +.tablesorter-default tr.odd > td { background-color: #dfdfdf; } -.tablesorter-default tr.even td { +.tablesorter-default tr.even > td { background-color: #efefef; } @@ -114,9 +117,17 @@ Default Theme background-color: #f2f2f2; } +/* caption */ +.tablesorter-default > caption { + background-color: #fff; +} + /* filter widget */ +.tablesorter-default .tablesorter-filter-row { + background-color: #eee; +} .tablesorter-default .tablesorter-filter-row td { - background: #eee; + background-color: #eee; border-bottom: #ccc 1px solid; line-height: normal; text-align: center; /* center the input */ @@ -142,7 +153,7 @@ Default Theme line-height: 0; cursor: pointer; } -.tablesorter-default .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-default .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -153,10 +164,11 @@ Default Theme filter: alpha(opacity=0); } /* filters */ -.tablesorter-default .tablesorter-filter { +.tablesorter-default input.tablesorter-filter, +.tablesorter-default select.tablesorter-filter { width: 95%; - height: inherit; - margin: 4px; + height: auto; + margin: 4px auto; padding: 4px; background-color: #fff; border: 1px solid #bbb; @@ -169,3 +181,14 @@ Default Theme -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.dropbox.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.dropbox.css index 72ccf99..300db96 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.dropbox.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.dropbox.css @@ -22,7 +22,6 @@ border-color: #82cffa #e7f2fb #96c4ea; border-style: solid; border-width: 1px; - height: 29px; padding: 3px 6px; font-size: 13px; font-weight: normal; @@ -36,7 +35,6 @@ background-color: #f0f9ff; border-bottom: 1px solid #96c4ea; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.12), 0 0 0 #000000 inset; - cursor: pointer; white-space: normal; } .tablesorter-dropbox .tablesorter-headerSortUp, @@ -45,39 +43,44 @@ .tablesorter-dropbox .tablesorter-headerDesc { font-weight: 600; } -.tablesorter-dropbox .tablesorter-header i { +.tablesorter-dropbox .tablesorter-header { + cursor: pointer; +} +.tablesorter-dropbox .tablesorter-header i.tablesorter-icon { width: 9px; height: 9px; - padding: 4px 20px 4px 4px; - cursor: pointer; + padding: 0 10px 0 4px; + display: inline-block; background-position: center right; background-repeat: no-repeat; content: ""; } -.tablesorter-dropbox .tablesorter-headerSortUp i, -.tablesorter-dropbox .tablesorter-headerAsc i { +.tablesorter-dropbox .tablesorter-headerSortUp i.tablesorter-icon, +.tablesorter-dropbox .tablesorter-headerAsc i.tablesorter-icon { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALhJREFUeNpi/P//PwMhwILMiexYx8bIxNTy/9+/muUVQb9g4kzIitg4edI4+YRLQTSyOCPMupjerUI8whK3OXgEhH58+fDuy9sXqkuKvd+hmMTOxdvCxS8sxMUvxACiQXwU6+Im7DDg5BNKY+fiY2BmYWMA0SA+SByuiJ2bbzIHrwAzMxsb0AGMDCAaxAeJg+SZ7wtaqfAISfQAdTIwMUM8ywhUyMTEzPD/71+5FXvPLWUkJpwAAgwAZqYvvHStbD4AAAAASUVORK5CYII='); - /* background-image: url(/assets/jquery-tablesorter/dropbox-asc.png); */ + /* background-image: url(images/dropbox-asc.png); */ } -.tablesorter-dropbox .tablesorter-headerSortUp:hover i, -.tablesorter-dropbox .tablesorter-headerAsc:hover i { +.tablesorter-dropbox .tablesorter-headerSortUp:hover i.tablesorter-icon, +.tablesorter-dropbox .tablesorter-headerAsc:hover i.tablesorter-icon { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALVJREFUeNpi/P//PwMhwILMCc+qZGNkYmr5/+9fzcpp7b9g4kzIitjYOdM4uXlLQTSyOCPMuqi8OiEefsHbHFzcQj++fX335eN71WWTmt6hmMTOwdXCycMnBDSJAUSD+CjWxRQ0GHBw86Sxc3AyMDOzMIBoEB8kDlfEzsk1mYOLByjPCnQAIwOIBvFB4iB55rsfmVS4+QV7QNYwMTNDHApUyMTExPDv/z+5Feu3L2UkJpwAAgwA244u+I9CleAAAAAASUVORK5CYII='); - /* background-image: url(/assets/jquery-tablesorter/dropbox-asc-hovered.png); */ + /* background-image: url(images/dropbox-asc-hovered.png); */ } -.tablesorter-dropbox .tablesorter-headerSortDown i, -.tablesorter-dropbox .tablesorter-headerDesc i { +.tablesorter-dropbox .tablesorter-headerSortDown i.tablesorter-icon, +.tablesorter-dropbox .tablesorter-headerDesc i.tablesorter-icon { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALdJREFUeNpi/P//PwMhwBLdtVGFhZ3zNhMzC4bkv79/GP78/K7KCDIpZ9mVw+xcfDaMTExwBf///WP4+e3TkSlROrZg7UxMLLns3HxnmFnZmGGK/v7+9ff3j2+5YHkQMSlC48Kv719m/f//D2IKkAbxQeJwRSDw4/OHmr+/fr0DqmAA0SA+TA6uaEq0zjugG+r//vkFcks9iA/3HbJvvn18O+vf379yP758mMXAoAAXZyQmnAACDADX316BiTFbMQAAAABJRU5ErkJggg=='); - /* background-image: url(/assets/jquery-tablesorter/dropbox-desc.png); */ + /* background-image: url(images/dropbox-desc.png); */ } -.tablesorter-dropbox .tablesorter-headerSortDown:hover i, -.tablesorter-dropbox .tablesorter-headerDesc:hover i { +.tablesorter-dropbox .tablesorter-headerSortDown:hover i.tablesorter-icon, +.tablesorter-dropbox .tablesorter-headerDesc:hover i.tablesorter-icon { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAALNJREFUeNpi/P//PwMhwBJf3uP879e3PUzMzBiS//7+ZWBi43JhBJmU2z7nIzMzEx8jIyNcAUj8799/nyZXpvCzgARYuXjTWBkZVjCzIEz7++cvw+//DGkgNiPMTWVT1l5hZvynDTINbMp/pqtdOcE6IDkmmM5fv3/5//v37z9QBQOIBvFhcnBFEwoj7/5jZFnz9+8fBhAN4sN9h+ybH9++JrGxscr/+vE1CVmckZhwAggwANvlUyq5Dd1wAAAAAElFTkSuQmCC'); - /* background-image: url(/assets/jquery-tablesorter/dropbox-desc-hovered.png); */ + /* background-image: url(images/dropbox-desc-hovered.png); */ } -.tablesorter-dropbox thead .sorter-false i, -.tablesorter-dropbox thead .sorter-false:hover i { - background-image: none; - padding: 4px; +.tablesorter-dropbox thead .sorter-false { + cursor: default; +} + +.tablesorter-dropbox thead .sorter-false i.tablesorter-icon { + display: none; } /* tbody */ @@ -91,6 +94,7 @@ } /* hovered row colors */ +.tablesorter-dropbox tbody > tr.hover > td, .tablesorter-dropbox tbody > tr:hover > td, .tablesorter-dropbox tbody > tr.even:hover > td, .tablesorter-dropbox tbody > tr.odd:hover > td { @@ -106,14 +110,14 @@ .tablesorter-dropbox .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-dropbox tr.odd td { +.tablesorter-dropbox tr.odd > td { } -.tablesorter-dropbox tr.even td { +.tablesorter-dropbox tr.even > td { } /* Column Widget - column sort colors */ @@ -133,9 +137,17 @@ .tablesorter-dropbox tr.even td.tertiary { } +/* caption */ +.tablesorter-dropbox > caption { + background-color: #fff; +} + /* Filter Widget */ -.tablesorter-dropbox.tablesorter-filter-row td { - background: #eee; +.tablesorter-dropbox .tablesorter-filter-row { + background-color: #fff; +} +.tablesorter-dropbox .tablesorter-filter-row td { + background-color: #fff; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -144,14 +156,14 @@ transition: line-height 0.1s ease; } /* optional disabled input styling */ -.tablesorter-dropbox.tablesorter-filter-row .disabled { +.tablesorter-dropbox .tablesorter-filter-row .disabled { opacity: 0.5; filter: alpha(opacity=50); cursor: not-allowed; } /* hidden filter row */ -.tablesorter-dropbox.tablesorter-filter-row.hideme td { +.tablesorter-dropbox .tablesorter-filter-row.hideme td { /*** *********************************************** ***/ /*** change this padding to modify the thickness ***/ /*** of the closed filter row (height = padding x 2) ***/ @@ -161,7 +173,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-dropbox.tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-dropbox .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -173,11 +185,11 @@ } /* filters */ -.tablesorter-dropbox.tablesorter-filter { +.tablesorter-dropbox input.tablesorter-filter, +.tablesorter-dropbox select.tablesorter-filter { width: 98%; - height: inherit; - margin: 4px; - padding: 4px; + height: auto; + margin: 0; background-color: #fff; border: 1px solid #bbb; color: #333; @@ -189,3 +201,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.green.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.green.css index 57f6bdd..f6f1f4c 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.green.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.green.css @@ -6,23 +6,23 @@ width: 100%; text-align: left; border-spacing: 0; + border: #cdcdcd 1px solid; + border-width: 1px 0 0 1px; } -.tablesorter-green, .tablesorter-green th, .tablesorter-green td { font: 12px/18px Arial, Sans-serif; border: #cdcdcd 1px solid; - border-spacing: 0; - padding: 0; - text-align: left; + border-width: 0 1px 1px 0; } /* header */ -.tablesorter-green thead tr, +.tablesorter-green thead tr .tablesorter-header, .tablesorter-green tfoot tr { - background: center center repeat-x; + background-position: center center; + background-repeat: repeat-x; background-image: url(data:image/gif;base64,R0lGODlhAQBkAOYAAN/e39XU1fX19tTU1eXm5uTl5ePk5OLj4+Hi4vX29fT19PP08/Lz8vHy8fDx8O/w7+7v7uzt7Orr6ufo5/T08/Pz8ufn5uLi4eDg39/f3t3d3Nzc29HR0NDQz8/Pzuvq6urp6eno6Ojn5+fm5tfW1tbV1dTT09PS0tLR0dHQ0NDPz/f39/b29vX19fT09PPz8/Ly8vHx8e/v7+7u7u3t7ezs7Ovr6+rq6unp6ejo6Ofn5+bm5uXl5eTk5OPj4+Li4uHh4eDg4N/f397e3t3d3dzc3Nvb29ra2tnZ2djY2NfX19XV1dPT09LS0tHR0dDQ0M/Pz8rKysXFxf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAFMALAAAAAABAGQAAAdegCsrLC0tLi+ILi6FCSwsCS0KkhQVDA0OMjM0NTYfICEiIzw9P0AYGUQaG0ZHSEoDTU9Qs08pTk1MSyRJR0VDQT8+PTw7Ojg3NTMyMTAvi4WOhC0vMTI1OT9GTlFSgQA7); - /* background-image: url(/assets/jquery-tablesorter/green-header.gif); */ + /* background-image: url(images/green-header.gif); */ } .tablesorter-green th, .tablesorter-green thead td { @@ -32,33 +32,35 @@ padding: 6px; } .tablesorter-green .header, -.tablesorter-green .tablesorter-header { - background: no-repeat 5px center; +.tablesorter-green .tablesorter-header-inner { + background-position: 5px center; + background-repeat: no-repeat; background-image: url(data:image/gif;base64,R0lGODlhEAAQAOYAAA5NDBBYDlWWUzRUM5DVjp7inJ/fnQ1ECiCsGhyYFxqKFRFdDhBXDQxCCiO8HSK2HCCqGh2aGByUFxuPFhqNFhmHFRZ2EhVvERRpEBBVDSS8HiGyHB+mGh6fGRuTFxiAFBd5Eww/Cgs5CRp7Fiu+JRx8GCy/JjHAKyynKCuhJzXCMDbCMDnDMyNuHz3EODy9N0LFPSl7JkvIRjycOFDKS1LKTVPLT1XLUFTCT17OWTBkLmbQYnDTbHXVcXnWdoXago/djGmUZ112XCJEIEdjRf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAEUALAAAAAAQABAAAAdlgEWCg4SFhoIvh4cVLECKhCMeJjwFj0UlEwgaMD4Gii0WFAkRHQ47BIY6IQAZDAwBCyAPOJa1toRBGBAwNTY3OT0/AoZCDQoOKi4yNDOKRCIfGycrKZYDBxIkKLZDFxy3RTHgloEAOw==); - /* background-image: url(/assets/jquery-tablesorter/green-unsorted.gif); */ + /* background-image: url(images/green-unsorted.gif); */ border-collapse: collapse; white-space: normal; cursor: pointer; } -.tablesorter-green thead .headerSortUp, -.tablesorter-green thead .tablesorter-headerSortUp, -.tablesorter-green thead .tablesorter-headerAsc { +.tablesorter-green thead .headerSortUp .tablesorter-header-inner, +.tablesorter-green thead .tablesorter-headerSortUp .tablesorter-header-inner, +.tablesorter-green thead .tablesorter-headerAsc .tablesorter-header-inner { background-image: url(data:image/gif;base64,R0lGODlhEAAQANUAAA5NDBBYDpDVjp7inJ/fnSCsGhyYFxFdDhBXDSO8HSK2HB2aGBuPFhqNFhmHFRZ2EhBVDSS8Hh6fGRuTFxd5Eww/Chp7Fhx8GCy/JjnDMyNuHzy9N0LFPVTCTzBkLmbQYnDTbHnWdo/djP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACMALAAAAAAQABAAAAY4wJFwSCwaj8ikcslMbpojR0bEtEwwoIHywihEOCECUvNoGBaSxEdg9FQAEAQicKAoOtC8fs8fBgEAOw==) - /* background-image: url(/assets/jquery-tablesorter/green-asc.gif); */ + /* background-image: url(images/green-asc.gif); */ } -.tablesorter-green thead .headerSortDown, -.tablesorter-green thead .tablesorter-headerSortDown, -.tablesorter-green thead .tablesorter-headerDesc { +.tablesorter-green thead .headerSortDown .tablesorter-header-inner, +.tablesorter-green thead .tablesorter-headerSortDown .tablesorter-header-inner, +.tablesorter-green thead .tablesorter-headerDesc .tablesorter-header-inner { background-image: url(data:image/gif;base64,R0lGODlhEAAQANUAAFWWUzRUMw1EChqKFQxCCiO8HSCqGhyUFxVvERRpECGyHB+mGhiAFAs5CSu+JTHAKyynKCuhJzXCMDbCMD3EOELFPSl7JkvIRjycOFDKS1LKTVPLT1XLUF7OWXXVcYXagmmUZ112XCJEIEdjRf///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACQALAAAAAAQABAAAAY4QJJwSCwaj8ikcskkghKGimbD6Xg+AGOIMChIKJcMBjlqMBSPSUQZEBwcEKYIsWiSLPa8fs9HBgEAOw==) - /* background-image: url(/assets/jquery-tablesorter/green-desc.gif); */ + /* background-image: url(images/green-desc.gif); */ } .tablesorter-green th.tablesorter-header .tablesorter-header-inner, .tablesorter-green td.tablesorter-header .tablesorter-header-inner { padding-left: 23px; } -.tablesorter-green thead .tablesorter-header.sorter-false { +.tablesorter-green thead .tablesorter-header.sorter-false .tablesorter-header-inner { background-image: none; - padding: 4px; + cursor: default; + padding-left: 6px; } /* tfoot */ @@ -78,33 +80,36 @@ you'll need to add additional lines for rows with more than 2 child rows */ +.tablesorter-green tbody > tr.hover > td, .tablesorter-green tbody > tr:hover > td, .tablesorter-green tbody > tr:hover + tr.tablesorter-childRow > td, .tablesorter-green tbody > tr:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td, +.tablesorter-green tbody > tr.even.hover > td, .tablesorter-green tbody > tr.even:hover > td, .tablesorter-green tbody > tr.even:hover + tr.tablesorter-childRow > td, .tablesorter-green tbody > tr.even:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td { - background: #d9d9d9; + background-color: #d9d9d9; } +.tablesorter-green tbody > tr.odd.hover > td, .tablesorter-green tbody > tr.odd:hover > td, .tablesorter-green tbody > tr.odd:hover + tr.tablesorter-childRow > td, .tablesorter-green tbody > tr.odd:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td { - background: #bfbfbf; + background-color: #bfbfbf; } /* table processing indicator */ .tablesorter-green .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-green tr.odd td { +.tablesorter-green tr.odd > td { background-color: #ebfaeb; } -.tablesorter-green tr.even td { +.tablesorter-green tr.even > td { background-color: #fff; } @@ -131,9 +136,17 @@ background-color: #ebfaed; } +/* caption */ +.tablesorter-green > caption { + background-color: #fff; +} + /* filter widget */ +.tablesorter-green .tablesorter-filter-row { + background-color: #eee; +} .tablesorter-green .tablesorter-filter-row td { - background: #eee; + background-color: #eee; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -158,7 +171,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-green .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-green .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -169,10 +182,11 @@ filter: alpha(opacity=0); } /* filters */ -.tablesorter-green .tablesorter-filter { +.tablesorter-green input.tablesorter-filter, +.tablesorter-green select.tablesorter-filter { width: 98%; - height: inherit; - margin: 4px; + height: auto; + margin: 0; padding: 4px; background-color: #fff; border: 1px solid #bbb; @@ -185,3 +199,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.grey.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.grey.css index 3041c2c..a0baaee 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.grey.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.grey.css @@ -21,7 +21,7 @@ background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#555), to(#3c3c3c)); background-image: -webkit-linear-gradient(top, #555, #3c3c3c); background-image: -o-linear-gradient(top, #555, #3c3c3c); - background-image: linear-gradient(to bottom, #555,#3c3c3c); + background-image: linear-gradient(to bottom, #555,#3c3c3c); filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#555555', endColorstr='#3c3c3c',GradientType=0 ); background-repeat: repeat-x; border-right: #555 1px solid; @@ -36,8 +36,12 @@ position: relative; padding: 4px 15px 4px 4px; } +.tablesorter-grey .header, +.tablesorter-grey .tablesorter-header { + cursor: pointer; +} .tablesorter-grey .header i, -.tablesorter-grey .tablesorter-header i { +.tablesorter-grey .tablesorter-header i.tablesorter-icon { width: 18px; height: 10px; position: absolute; @@ -50,7 +54,6 @@ background-position: center right; padding: 4px; white-space: normal; - cursor: pointer; } .tablesorter-grey th.headerSortUp, .tablesorter-grey th.tablesorter-headerSortUp, @@ -66,21 +69,23 @@ background-image: linear-gradient(to bottom, #195c93, #0e4776); filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#195c93', endColorstr='#0e4776',GradientType=0 ); } -.tablesorter-grey .headerSortUp i, -.tablesorter-grey .tablesorter-headerSortUp i, -.tablesorter-grey .tablesorter-headerAsc i { +.tablesorter-grey .headerSortUp i.tablesorter-icon, +.tablesorter-grey .tablesorter-headerSortUp i.tablesorter-icon, +.tablesorter-grey .tablesorter-headerAsc i.tablesorter-icon { /* white asc arrow */ background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjI8Bya2wnINUMopZAQA7); } -.tablesorter-grey .headerSortDown i, -.tablesorter-grey .tablesorter-headerSortDown i, -.tablesorter-grey .tablesorter-headerDesc i { +.tablesorter-grey .headerSortDown i.tablesorter-icon, +.tablesorter-grey .tablesorter-headerSortDown i.tablesorter-icon, +.tablesorter-grey .tablesorter-headerDesc i.tablesorter-icon { /* white desc arrow */ background-image: url(data:image/gif;base64,R0lGODlhFQAEAIAAAP///////yH5BAEAAAEALAAAAAAVAAQAAAINjB+gC+jP2ptn0WskLQA7); } -.tablesorter-grey thead .sorter-false i { - background-image: none; - padding: 4px; +.tablesorter-grey thead .sorter-false { + cursor: default; +} +.tablesorter-grey thead .sorter-false i.tablesorter-icon { + display: none; } /* tfoot */ @@ -108,33 +113,36 @@ you'll need to add additional lines for rows with more than 2 child rows */ +.tablesorter-grey tbody > tr.hover > td, .tablesorter-grey tbody > tr:hover > td, .tablesorter-grey tbody > tr:hover + tr.tablesorter-childRow > td, .tablesorter-grey tbody > tr:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td, +.tablesorter-grey tbody > tr.even.hover > td, .tablesorter-grey tbody > tr.even:hover > td, .tablesorter-grey tbody > tr.even:hover + tr.tablesorter-childRow > td, .tablesorter-grey tbody > tr.even:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td { - background: #134b78; + background-color: #134b78; } +.tablesorter-grey tbody > tr.odd.hover > td, .tablesorter-grey tbody > tr.odd:hover > td, .tablesorter-grey tbody > tr.odd:hover + tr.tablesorter-childRow > td, .tablesorter-grey tbody > tr.odd:hover + tr.tablesorter-childRow + tr.tablesorter-childRow > td { - background: #134b78; + background-color: #134b78; } /* table processing indicator */ .tablesorter-grey .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-grey tbody tr.odd td { +.tablesorter-grey tbody tr.odd > td { background-color: #5e5e5e; } -.tablesorter-grey tbody tr.even td { +.tablesorter-grey tbody tr.even > td { background-color: #6d6d6d; } @@ -167,9 +175,17 @@ background-color: #2073B7; } +/* caption */ +.tablesorter-grey > caption { + background-color: #fff; +} + /* filter widget */ +.tablesorter-grey .tablesorter-filter-row { + background-color: #3c3c3c; +} .tablesorter-grey .tablesorter-filter-row td { - background: #3c3c3c; + background-color: #3c3c3c; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -194,7 +210,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-grey .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-grey .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -205,9 +221,10 @@ filter: alpha(opacity=0); } /* filters */ -.tablesorter-grey .tablesorter-filter { +.tablesorter-grey input.tablesorter-filter, +.tablesorter-grey select.tablesorter-filter { width: 98%; - height: inherit; + height: auto; margin: 0; padding: 4px; background-color: #6d6d6d; @@ -221,3 +238,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.ice.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.ice.css index 72dd19b..bffa0ac 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.ice.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.ice.css @@ -35,9 +35,11 @@ } .tablesorter-ice .header, .tablesorter-ice .tablesorter-header { - background: #f6f8f9 no-repeat center right; + background-color: #f6f8f9; + background-position: center right; + background-repeat: no-repeat; background-image: url(data:image/gif;base64,R0lGODlhDAAMAMQAAAJEjAJCiwJBigJAiANFjgNGjgNEjQRIkQRHkANIkAVMlAVQmAZWnQZUnAdYoAhdpAhZoAlhqQlepQliqQppsApmrQxutgtutQtutAxwtwxwtg1yug1zugxtsw1yuP8A/yH5BAEAAB8ALAAAAAAMAAwAAAUx4Cd+3GiOW4ado2d9VMVm1xg9ptadTsP+QNZEcjoQTBDGCAFgLRSfQgCYMAiCn8EvBAA7); - /* background-image: url(/assets/jquery-tablesorter/ice-unsorted.gif) */ + /* background-image: url(images/ice-unsorted.gif) */ padding: 4px 20px 4px 4px; white-space: normal; cursor: pointer; @@ -46,20 +48,25 @@ .tablesorter-ice .tablesorter-headerSortUp, .tablesorter-ice .tablesorter-headerAsc { color: #333; - background: #ebedee no-repeat center right; + background-color: #ebedee; + background-position: center right; + background-repeat: no-repeat; background-image: url(data:image/gif;base64,R0lGODlhDAAMANUAAAJCiwNHkANFjgNEjQRIkQNJkQRMlARKkwRKkgVPlwZSmgdaogdYnwhfpghcowlhqgliqglgqAlgpwljqwporwpmrQplrAtsswtqsgtrsgtqsQxttAtvtQtttAxyuQxwtwxxtwxvtg10uw1zuQ1xuP8A/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACUALAAAAAAMAAwAAAY6wJKwJBoahyNQ6Dj0fDoZCpPEuWgqk4jxs8FQLI+Gg8Esm5kQydFQMC7IwkOAqUiUCAIzIjA4lwBlQQA7); - /* background-image: url(/assets/jquery-tablesorter/ice-desc.gif) */ + /* background-image: url(images/ice-desc.gif) */ } .tablesorter-ice .headerSortDown, .tablesorter-ice .tablesorter-headerSortDown, .tablesorter-ice .tablesorter-headerDesc { color: #333; - background: #ebedee no-repeat center right; + background-color: #ebedee; + background-position: center right; + background-repeat: no-repeat; background-image: url(data:image/gif;base64,R0lGODlhDAAMANUAAAE/iAJBigNFjgNEjQNFjQNDiwRHkQRHjwNHjwROlgRMlQRMlARJkgRKkgZQmAVPlgZWnQZSmgZRmAdXoAdXnwdUnAdbogdZoQhbowlhqAlepglkrAliqQtstAtqsQxyugxyuQxwuAxxuAxxtwxwtgxvtQ10vA12vA10u/8A/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAACkALAAAAAAMAAwAAAY6wJQwdRoah6bP6DhEiVIdDxNEGm4yxlDpiJkwv2AmR2OhVCSJBsJ4gUQeCwOB6VAwBAXwYRAIpwBfQQA7); - /* background-image: url(/assets/jquery-tablesorter/ice-asc.gif); */ + /* background-image: url(images/ice-asc.gif); */ } .tablesorter-ice thead .sorter-false { background-image: none; + cursor: default; padding: 4px; } @@ -68,7 +75,7 @@ .tablesorter-ice tfoot .tablesorter-headerSortDown, .tablesorter-ice tfoot .tablesorter-headerAsc, .tablesorter-ice tfoot .tablesorter-headerDesc { - background: #ebedee; + background-color: #ebedee; } /* tbody */ @@ -77,25 +84,26 @@ } /* hovered row colors */ +.tablesorter-ice tbody > tr.hover > td, .tablesorter-ice tbody > tr:hover > td, .tablesorter-ice tbody > tr.even:hover > td, .tablesorter-ice tbody > tr.odd:hover > td { - background: #ebf2fa; + background-color: #ebf2fa; } /* table processing indicator */ .tablesorter-ice .tablesorter-processing { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } /* Zebra Widget - row alternating colors */ -.tablesorter-ice tr.odd td { +.tablesorter-ice tr.odd > td { background-color: #dfdfdf; } -.tablesorter-ice tr.even td { +.tablesorter-ice tr.even > td { background-color: #efefef; } @@ -122,9 +130,23 @@ background-color: #ebfafa; } +/* sticky headers */ +.tablesorter-ice.containsStickyHeaders thead tr:nth-child(1) th, +.tablesorter-ice.containsStickyHeaders thead tr:nth-child(1) td { + border-top: #ccc 1px solid; +} + +/* caption */ +.tablesorter-ice > caption { + background-color: #fff; +} + /* filter widget */ +.tablesorter-ice .tablesorter-filter-row { + background-color: #eee; +} .tablesorter-ice .tablesorter-filter-row td { - background: #eee; + background-color: #eee; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -149,7 +171,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-ice .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-ice .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -160,10 +182,11 @@ filter: alpha(opacity=0); } /* filters */ -.tablesorter-ice .tablesorter-filter { +.tablesorter-ice input.tablesorter-filter, +.tablesorter-ice select.tablesorter-filter { width: 98%; - height: inherit; - margin: 4px; + height: auto; + margin: 0; padding: 4px; background-color: #fff; border: 1px solid #bbb; @@ -176,3 +199,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.jui.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.jui.css index 740f1f5..6dd9bac 100644 --- a/vendor/assets/stylesheets/jquery-tablesorter/theme.jui.css +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.jui.css @@ -5,15 +5,17 @@ .tablesorter-jui { width: 100%; border-collapse: separate; - border-spacing: 2px; /* adjust spacing between table cells */ + border-spacing: 2px; /* adjust spacing between table cells */ margin: 10px 0 15px; padding: 5px; font-size: 0.8em; } /* header */ -.tablesorter-jui th, -.tablesorter-jui thead td { +.tablesorter-jui thead th, +.tablesorter-jui thead td, +.tablesorter-jui tfoot th, +.tablesorter-jui tfoot td { position: relative; background-repeat: no-repeat; background-position: right center; @@ -21,12 +23,12 @@ font-weight: bold !important; border-width: 1px !important; text-align: left; + padding: 8px; /* wider than the icon */ } .tablesorter-jui .header, .tablesorter-jui .tablesorter-header { cursor: pointer; white-space: normal; - padding: 8px; /* wider than the icon */ } .tablesorter-jui .tablesorter-header-inner { padding-right: 20px; @@ -38,6 +40,13 @@ margin-top: -8px; /* half the icon height; older IE doesn't like this */ } +.tablesorter-jui thead .sorter-false { + cursor: default; +} +.tablesorter-jui thead tr .sorter-false .ui-icon { + display: none; +} + /* tfoot */ .tablesorter-jui tfoot th, .tablesorter-jui tfoot td { @@ -53,7 +62,8 @@ } /* hovered row colors */ -.tablesorter-jui tbody > tr:hover td { +.tablesorter-jui tbody > tr.hover > td, +.tablesorter-jui tbody > tr:hover > td { opacity: 0.7; filter: alpha(opacity=70); } @@ -62,7 +72,7 @@ .tablesorter-jui .tablesorter-processing .tablesorter-header-inner { background-position: center center !important; background-repeat: no-repeat !important; - /* background-image: url(../addons/pager/icons/loading.gif) !important; */ + /* background-image: url(images/loading.gif) !important; */ background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=') !important; } @@ -78,9 +88,17 @@ background-color: rgba(255,255,255,0.8); } +/* caption */ +.tablesorter-jui > caption { + border: 0; +} + /* filter widget */ +.tablesorter-jui .tablesorter-filter-row { + background-color: transparent; +} .tablesorter-jui .tablesorter-filter-row td { - background: transparent; + background-color: transparent; line-height: normal; text-align: center; /* center the input */ -webkit-transition: line-height 0.1s ease; @@ -105,7 +123,7 @@ line-height: 0; cursor: pointer; } -.tablesorter-jui .tablesorter-filter-row.hideme .tablesorter-filter { +.tablesorter-jui .tablesorter-filter-row.hideme * { height: 1px; min-height: 0; border: 0; @@ -116,9 +134,10 @@ filter: alpha(opacity=0); } /* filters */ -.tablesorter-jui .tablesorter-filter { +.tablesorter-jui input.tablesorter-filter, +.tablesorter-jui select.tablesorter-filter { width: 98%; - height: inherit; + height: auto; margin: 0; padding: 4px; background-color: #fff; @@ -132,3 +151,14 @@ -o-transition: height 0.1s ease; transition: height 0.1s ease; } +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.materialize.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.materialize.css new file mode 100644 index 0000000..08a7fc4 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.materialize.css @@ -0,0 +1,176 @@ +/************* + Materialize theme (http://materializecss.com/) + *************/ +/* jQuery materialize Theme */ +.tablesorter-materialize { + width: 100%; +} +.tablesorter-materialize thead th, +.tablesorter-materialize thead td, +.tablesorter-materialize tfoot th, +.tablesorter-materialize tfoot td { + font: 14px/20px Arial, Sans-serif; + font-weight: bold; + padding: 4px; + margin: 0 0 18px; + background-color: #eee; +} + +.tablesorter-materialize .tablesorter-header { + cursor: pointer; +} +.tablesorter-materialize .sorter-false { + cursor: default; +} + +.tablesorter-materialize .tablesorter-header-inner { + position: relative; + padding: 4px 18px 4px 4px; +} + +/* sort icons */ +.tablesorter-materialize thead .tablesorter-header { + background-repeat: no-repeat; + background-position: center right; + padding: 4px 18px 4px 4px; + white-space: normal; + cursor: pointer; +} + +/* black unsorted icon */ +.tablesorter-materialize thead .tablesorter-headerUnSorted { + /* <svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 24 16"><path d="M15 8 1 8 8 0zM15 9 1 9 8 16z" fill="#222"/></svg> */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDE2Ij48cGF0aCBkPSJNMTUgOCAxIDggOCAwek0xNSA5IDEgOSA4IDE2eiIgZmlsbD0iIzIyMiIvPjwvc3ZnPg==); +} +/* black asc icon */ +.tablesorter-materialize thead .tablesorter-headerAsc { + /* <svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 24 16"><path d="M15 11 1 11 8 3z" fill="#222"/></svg> */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDE2Ij48cGF0aCBkPSJNMTUgMTEgMSAxMSA4IDN6IiBmaWxsPSIjMjIyIi8+PC9zdmc+); +} +/* black desc icon */ +.tablesorter-materialize thead .tablesorter-headerDesc { + /* <svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 24 16"><path d="M15 6 1 6 8 13z" fill="#222"/></svg> */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDE2Ij48cGF0aCBkPSJNMTUgNiAxIDYgOCAxM3oiIGZpbGw9IiMyMjIiLz48L3N2Zz4=); +} + +/* white unsorted icon */ +.tablesorter-materialize-dark thead .tablesorter-headerUnSorted { + /* <svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 24 16"><path d="M15 8 1 8 8 0zM15 9 1 9 8 16z" fill="#fff"/></svg> */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDE2Ij48cGF0aCBkPSJNMTUgOCAxIDggOCAwek0xNSA5IDEgOSA4IDE2eiIgZmlsbD0iI2ZmZiIvPjwvc3ZnPg==); +} +/* white asc icon */ +.tablesorter-materialize-dark thead .tablesorter-headerAsc { + /* <svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 24 16"><path d="M15 11 1 11 8 3z" fill="#fff"/></svg> */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDE2Ij48cGF0aCBkPSJNMTUgMTEgMSAxMSA4IDN6IiBmaWxsPSIjZmZmIi8+PC9zdmc+); +} +/* white desc icon */ +.tablesorter-materialize-dark thead .tablesorter-headerDesc { + /* <svg xmlns="http://www.w3.org/2000/svg" width="18" height="12" viewBox="0 0 24 16"><path d="M15 6 1 6 8 13z" fill="#fff"/></svg> */ + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxOCIgaGVpZ2h0PSIxMiIgdmlld0JveD0iMCAwIDI0IDE2Ij48cGF0aCBkPSJNMTUgNiAxIDYgOCAxM3oiIGZpbGw9IiNmZmYiLz48L3N2Zz4=); +} + +/* since materialize (table-striped) uses nth-child(), we just use this to add a zebra stripe color */ +.tablesorter-materialize > tbody > tr.odd > td, +.tablesorter-materialize > tbody > tr.tablesorter-hasChildRow.odd:hover ~ tr.tablesorter-hasChildRow.odd ~ .tablesorter-childRow.odd > td { + background-color: #f9f9f9; +} +.tablesorter-materialize > tbody > tr.hover > td, +.tablesorter-materialize > tbody > tr.odd:hover > td, +.tablesorter-materialize > tbody > tr.even:hover > td, +.tablesorter-materialize > tbody > tr.tablesorter-hasChildRow.odd:hover ~ .tablesorter-childRow.odd > td, +.tablesorter-materialize > tbody > tr.tablesorter-hasChildRow.even:hover ~ .tablesorter-childRow.even > td { + background-color: #f5f5f5; +} +.tablesorter-materialize > tbody > tr.even > td, +.tablesorter-materialize > tbody > tr.tablesorter-hasChildRow.even:hover ~ tr.tablesorter-hasChildRow.even ~ .tablesorter-childRow.even > td { + background-color: #fff; +} + +/* processing icon */ +.tablesorter-materialize .tablesorter-processing { + background-image: url('data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs='); + background-position: center center !important; + background-repeat: no-repeat !important; +} + +/* caption */ +.tablesorter-materialize > .caption { + background-color: #fff; +} + +/* filter widget */ +.tablesorter-materialize .tablesorter-filter-row input.tablesorter-filter, +.tablesorter-materialize .tablesorter-filter-row select.tablesorter-filter { + width: 98%; + margin: 0; + padding: 4px 6px; + color: #333; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: height 0.1s ease; + -moz-transition: height 0.1s ease; + -o-transition: height 0.1s ease; + transition: height 0.1s ease; +} +.tablesorter-materialize .tablesorter-filter-row .tablesorter-filter.disabled { + background-color: #eee; + color: #555; + cursor: not-allowed; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.075) inset; + box-sizing: border-box; + transition: height 0.1s ease; +} +.tablesorter-materialize .tablesorter-filter-row { + background-color: #efefef; +} +.tablesorter-materialize .tablesorter-filter-row td { + background-color: #efefef; + line-height: normal; + text-align: center; + padding: 4px 6px; + vertical-align: middle; + -webkit-transition: line-height 0.1s ease; + -moz-transition: line-height 0.1s ease; + -o-transition: line-height 0.1s ease; + transition: line-height 0.1s ease; +} +/* hidden filter row */ +.tablesorter-materialize .tablesorter-filter-row.hideme td { + padding: 2px; /* change this to modify the thickness of the closed border row */ + margin: 0; + line-height: 0; +} +.tablesorter-materialize .tablesorter-filter-row.hideme * { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* pager plugin */ +.tablesorter-materialize .tablesorter-pager select { + padding: 4px 6px; + display: inline-block; + width: auto; +} +.tablesorter-materialize .tablesorter-pager .pagedisplay { + border: 0; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/theme.metro-dark.css b/vendor/assets/stylesheets/jquery-tablesorter/theme.metro-dark.css new file mode 100644 index 0000000..461c2db --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/theme.metro-dark.css @@ -0,0 +1,197 @@ +/************* +Metro Dark Theme +*************/ +/* overall */ +.tablesorter-metro-dark { + width: 100%; + font: 12px/18px 'Segoe UI Semilight', 'Open Sans', Verdana, Arial, Helvetica, sans-serif; + color: #000; + background-color: #333; + border-spacing: 0; + margin: 10px 0 15px; + text-align: left; +} + +.tablesorter-metro-dark tr.dark-row th, .tablesorter-metro-dark tr.dark-row td, .tablesorter-metro-dark caption.dark-row { + background-color: #222; + color: #fff; + padding: 2px; + text-align: left; + font-size: 14px; +} + +/* header/footer */ +.tablesorter-metro-dark caption, +.tablesorter-metro-dark th, +.tablesorter-metro-dark thead td, +.tablesorter-metro-dark tfoot th, +.tablesorter-metro-dark tfoot td { + font-weight: 300; + font-size: 15px; + color: #ddd; + background-color: #333; + padding: 4px; +} + +.tablesorter-metro-dark .header, +.tablesorter-metro-dark .tablesorter-header { + background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQBAMAAADQT4M0AAAAGFBMVEUAAADu7u7u7u7u7u7u7u7u7u7u7u7u7u5jNePWAAAACHRSTlMAMxIHKwEgMWD59H4AAABSSURBVAjXY2BgYFJgAAHzYhDJ6igSAKTYBAUTgJSioKAQAwNzoaCguAFDiCAQuDIkgigxBgiA8cJAVCpQt6AgSL+JoKAzA0gjUBsQqBcBCYhFAAE/CV4zeSzxAAAAAElFTkSuQmCC); + background-position: right 5px center; + background-repeat: no-repeat; + cursor: pointer; + white-space: normal; +} +.tablesorter-metro-dark .tablesorter-header-inner { + padding: 0 18px 0 4px; +} +.tablesorter-metro-dark thead .headerSortUp, +.tablesorter-metro-dark thead .tablesorter-headerSortUp, +.tablesorter-metro-dark thead .tablesorter-headerAsc { + background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQBAMAAADQT4M0AAAAIVBMVEUAAADu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u5meJAOAAAACnRSTlMAMwsqXt+gIBUGxGoDMAAAAFlJREFUCNctzC0SQAAUReEzGNQ3AlHRiSRZFCVZYgeswRL8hLdK7834wj3tAlGP6y7fYHpKS6w6WwbVG0I1NZVnZPG8/DYxOYlnhUYkA06R1s9ESsxR4NIdPhkPFDFYuEnMAAAAAElFTkSuQmCC); +} +.tablesorter-metro-dark thead .headerSortDown, +.tablesorter-metro-dark thead .tablesorter-headerSortDown, +.tablesorter-metro-dark thead .tablesorter-headerDesc { + background-image: url(data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAQBAMAAADQT4M0AAAALVBMVEUAAADu7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7u7i0NViAAAADnRSTlMAMiweCQITTvDctZZqaTlM310AAABcSURBVAjXY2BgYEtgAAFHERDJqigUAKSYBQUNgFSioKAYAwOLIBA4MASBKFUGQxAlzAAF+94BwWuGKBC1lIFl3rt3Lx0YGCzevWsGSjK9e6cAUlT3HKyW9wADAwDRrBiDy6bKzwAAAABJRU5ErkJggg==); +} +.tablesorter-metro-dark thead .sorter-false { + background-image: none; + cursor: default; + padding: 4px; +} + +/* tbody */ +.tablesorter-metro-dark td { + background-color: #fff; + padding: 4px; + vertical-align: top; +} + +/* hovered row colors */ +.tablesorter-metro-dark tbody > tr.hover > td, +.tablesorter-metro-dark tbody > tr:hover > td, +.tablesorter-metro-dark tbody > tr.even:hover > td, +.tablesorter-metro-dark tbody > tr.odd:hover > td { + background-color: #bbb; + color: #000; +} + +/* table processing indicator */ +.tablesorter-metro-dark .tablesorter-processing { + background-position: center center !important; + background-repeat: no-repeat !important; + /* background-image: url(images/loading.gif) !important; */ + background-image: url(data:image/gif;base64,R0lGODlhFAAUAKEAAO7u7lpaWgAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQBCgACACwAAAAAFAAUAAACQZRvoIDtu1wLQUAlqKTVxqwhXIiBnDg6Y4eyx4lKW5XK7wrLeK3vbq8J2W4T4e1nMhpWrZCTt3xKZ8kgsggdJmUFACH5BAEKAAIALAcAAAALAAcAAAIUVB6ii7jajgCAuUmtovxtXnmdUAAAIfkEAQoAAgAsDQACAAcACwAAAhRUIpmHy/3gUVQAQO9NetuugCFWAAAh+QQBCgACACwNAAcABwALAAACE5QVcZjKbVo6ck2AF95m5/6BSwEAIfkEAQoAAgAsBwANAAsABwAAAhOUH3kr6QaAcSrGWe1VQl+mMUIBACH5BAEKAAIALAIADQALAAcAAAIUlICmh7ncTAgqijkruDiv7n2YUAAAIfkEAQoAAgAsAAAHAAcACwAAAhQUIGmHyedehIoqFXLKfPOAaZdWAAAh+QQFCgACACwAAAIABwALAAACFJQFcJiXb15zLYRl7cla8OtlGGgUADs=) !important; +} + +/* pager */ +.tablesorter-metro-dark .tablesorter-pager button { + background-color: #444; + color: #eee; + border: #555 1px solid; + cursor: pointer; +} +.tablesorter-metro-dark .tablesorter-pager button:hover { + background-color: #555; +} + +/* Zebra Widget - row alternating colors */ +.tablesorter-metro-dark tr.odd > td { + background-color: #eee; +} +.tablesorter-metro-dark tr.even > td { + background-color: #fff; +} + +/* Column Widget - column sort colors */ +.tablesorter-metro-dark tr.odd td.primary { + background-color: #bfbfbf; +} +.tablesorter-metro-dark td.primary, +.tablesorter-metro-dark tr.even td.primary { + background-color: #d9d9d9; +} +.tablesorter-metro-dark tr.odd td.secondary { + background-color: #d9d9d9; +} +.tablesorter-metro-dark td.secondary, +.tablesorter-metro-dark tr.even td.secondary { + background-color: #e6e6e6; +} +.tablesorter-metro-dark tr.odd td.tertiary { + background-color: #e6e6e6; +} +.tablesorter-metro-dark td.tertiary, +.tablesorter-metro-dark tr.even td.tertiary { + background-color: #f2f2f2; +} + +/* filter widget */ +.tablesorter-metro-dark .tablesorter-filter-row { + background-color: #eee; +} +.tablesorter-metro-dark .tablesorter-filter-row td { + background-color: #eee; + line-height: normal; + text-align: center; /* center the input */ + -webkit-transition: line-height 0.1s ease; + -moz-transition: line-height 0.1s ease; + -o-transition: line-height 0.1s ease; + transition: line-height 0.1s ease; +} +/* optional disabled input styling */ +.tablesorter-metro-dark .tablesorter-filter-row .disabled { + opacity: 0.5; + filter: alpha(opacity=50); + cursor: not-allowed; +} +/* hidden filter row */ +.tablesorter-metro-dark .tablesorter-filter-row.hideme td { + /*** *********************************************** ***/ + /*** change this padding to modify the thickness ***/ + /*** of the closed filter row (height = padding x 2) ***/ + padding: 2px; + /*** *********************************************** ***/ + margin: 0; + line-height: 0; + cursor: pointer; +} +.tablesorter-metro-dark .tablesorter-filter-row.hideme * { + height: 1px; + min-height: 0; + border: 0; + padding: 0; + margin: 0; + /* don't use visibility: hidden because it disables tabbing */ + opacity: 0; + filter: alpha(opacity=0); +} +/* filters */ +.tablesorter-metro-dark input.tablesorter-filter, +.tablesorter-metro-dark select.tablesorter-filter { + width: 95%; + height: auto; + margin: 0; + padding: 4px; + background-color: #fff; + border: 1px solid #bbb; + color: #333; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: height 0.1s ease; + -moz-transition: height 0.1s ease; + -o-transition: height 0.1s ease; + transition: height 0.1s ease; +} +/* rows hidden by filtering (needed for child rows) */ +.tablesorter .filtered { + display: none; +} + +/* ajax error row */ +.tablesorter .tablesorter-errorRow td { + text-align: center; + cursor: pointer; + background-color: #e6bf99; +} diff --git a/vendor/assets/stylesheets/jquery-tablesorter/widget.grouping.css b/vendor/assets/stylesheets/jquery-tablesorter/widget.grouping.css new file mode 100644 index 0000000..28a04d9 --- /dev/null +++ b/vendor/assets/stylesheets/jquery-tablesorter/widget.grouping.css @@ -0,0 +1,38 @@ +/* Grouping widget css */ +tr.group-header td { + background: #eee; +} +.group-name { + text-transform: uppercase; + font-weight: bold; +} +.group-count { + color: #999; +} +.group-hidden { + display: none !important; +} +.group-header, .group-header td { + user-select: none; + -moz-user-select: none; +} +/* collapsed arrow */ +tr.group-header td i { + display: inline-block; + width: 0; + height: 0; + border-top: 4px solid transparent; + border-bottom: 4px solid #888; + border-right: 4px solid #888; + border-left: 4px solid transparent; + margin-right: 7px; + user-select: none; + -moz-user-select: none; +} +tr.group-header.collapsed td i { + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #888; + border-right: 0; + margin-right: 10px; +}