flapjack 1.6.0 → 2.0.0b1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -6
  3. data/.gitmodules +1 -1
  4. data/.rspec +1 -1
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +12 -13
  7. data/CHANGELOG.md +2 -9
  8. data/CONTRIBUTING.md +7 -2
  9. data/Gemfile +4 -13
  10. data/LICENCE +1 -0
  11. data/README.md +8 -2
  12. data/Rakefile +2 -2
  13. data/bin/flapjack +3 -12
  14. data/build.sh +4 -2
  15. data/etc/flapjack_config.toml.example +273 -0
  16. data/features/ack_after_sched_maint.feature +18 -21
  17. data/features/cli.feature +11 -71
  18. data/features/cli_flapjack-feed-events.feature +14 -15
  19. data/features/cli_flapjack-nagios-receiver.feature +12 -41
  20. data/features/cli_flapper.feature +12 -41
  21. data/features/cli_purge.feature +5 -6
  22. data/features/cli_receive-events.feature +6 -7
  23. data/features/cli_simulate-failed-check.feature +5 -6
  24. data/features/events.feature +206 -181
  25. data/features/events_check_names.feature +4 -7
  26. data/features/notification_rules.feature +144 -223
  27. data/features/notifications.feature +65 -57
  28. data/features/rollup.feature +45 -47
  29. data/features/steps/cli_steps.rb +4 -5
  30. data/features/steps/events_steps.rb +163 -373
  31. data/features/steps/notifications_steps.rb +408 -264
  32. data/features/steps/packaging-lintian_steps.rb +0 -4
  33. data/features/steps/time_travel_steps.rb +0 -26
  34. data/features/support/daemons.rb +6 -31
  35. data/features/support/env.rb +65 -74
  36. data/flapjack.gemspec +22 -24
  37. data/lib/flapjack.rb +14 -7
  38. data/lib/flapjack/cli/flapper.rb +74 -173
  39. data/lib/flapjack/cli/maintenance.rb +278 -109
  40. data/lib/flapjack/cli/migrate.rb +950 -0
  41. data/lib/flapjack/cli/purge.rb +19 -22
  42. data/lib/flapjack/cli/receiver.rb +150 -326
  43. data/lib/flapjack/cli/server.rb +8 -235
  44. data/lib/flapjack/cli/simulate.rb +42 -57
  45. data/lib/flapjack/configuration.rb +51 -37
  46. data/lib/flapjack/coordinator.rb +138 -129
  47. data/lib/flapjack/data/acknowledgement.rb +177 -0
  48. data/lib/flapjack/data/alert.rb +97 -158
  49. data/lib/flapjack/data/check.rb +611 -0
  50. data/lib/flapjack/data/condition.rb +70 -0
  51. data/lib/flapjack/data/contact.rb +226 -456
  52. data/lib/flapjack/data/event.rb +96 -184
  53. data/lib/flapjack/data/extensions/associations.rb +59 -0
  54. data/lib/flapjack/data/extensions/short_name.rb +25 -0
  55. data/lib/flapjack/data/medium.rb +428 -0
  56. data/lib/flapjack/data/metrics.rb +194 -0
  57. data/lib/flapjack/data/notification.rb +22 -281
  58. data/lib/flapjack/data/rule.rb +473 -0
  59. data/lib/flapjack/data/scheduled_maintenance.rb +244 -0
  60. data/lib/flapjack/data/state.rb +221 -0
  61. data/lib/flapjack/data/statistic.rb +112 -0
  62. data/lib/flapjack/data/tag.rb +277 -0
  63. data/lib/flapjack/data/test_notification.rb +182 -0
  64. data/lib/flapjack/data/unscheduled_maintenance.rb +159 -0
  65. data/lib/flapjack/data/validators/id_validator.rb +20 -0
  66. data/lib/flapjack/exceptions.rb +6 -0
  67. data/lib/flapjack/filters/acknowledgement.rb +23 -16
  68. data/lib/flapjack/filters/base.rb +0 -5
  69. data/lib/flapjack/filters/delays.rb +53 -43
  70. data/lib/flapjack/filters/ok.rb +23 -14
  71. data/lib/flapjack/filters/scheduled_maintenance.rb +3 -3
  72. data/lib/flapjack/filters/unscheduled_maintenance.rb +12 -3
  73. data/lib/flapjack/gateways/aws_sns.rb +65 -49
  74. data/lib/flapjack/gateways/aws_sns/alert.text.erb +2 -2
  75. data/lib/flapjack/gateways/aws_sns/alert_subject.text.erb +2 -2
  76. data/lib/flapjack/gateways/aws_sns/rollup_subject.text.erb +1 -1
  77. data/lib/flapjack/gateways/email.rb +107 -90
  78. data/lib/flapjack/gateways/email/alert.html.erb +19 -18
  79. data/lib/flapjack/gateways/email/alert.text.erb +20 -14
  80. data/lib/flapjack/gateways/email/alert_subject.text.erb +2 -1
  81. data/lib/flapjack/gateways/email/rollup.html.erb +14 -13
  82. data/lib/flapjack/gateways/email/rollup.text.erb +13 -10
  83. data/lib/flapjack/gateways/jabber.rb +679 -671
  84. data/lib/flapjack/gateways/jabber/alert.text.erb +9 -6
  85. data/lib/flapjack/gateways/jsonapi.rb +164 -350
  86. data/lib/flapjack/gateways/jsonapi/data/join_descriptor.rb +44 -0
  87. data/lib/flapjack/gateways/jsonapi/data/method_descriptor.rb +21 -0
  88. data/lib/flapjack/gateways/jsonapi/helpers/headers.rb +63 -0
  89. data/lib/flapjack/gateways/jsonapi/helpers/miscellaneous.rb +136 -0
  90. data/lib/flapjack/gateways/jsonapi/helpers/resources.rb +227 -0
  91. data/lib/flapjack/gateways/jsonapi/helpers/serialiser.rb +313 -0
  92. data/lib/flapjack/gateways/jsonapi/helpers/swagger_docs.rb +322 -0
  93. data/lib/flapjack/gateways/jsonapi/methods/association_delete.rb +115 -0
  94. data/lib/flapjack/gateways/jsonapi/methods/association_get.rb +288 -0
  95. data/lib/flapjack/gateways/jsonapi/methods/association_patch.rb +178 -0
  96. data/lib/flapjack/gateways/jsonapi/methods/association_post.rb +116 -0
  97. data/lib/flapjack/gateways/jsonapi/methods/metrics.rb +71 -0
  98. data/lib/flapjack/gateways/jsonapi/methods/resource_delete.rb +119 -0
  99. data/lib/flapjack/gateways/jsonapi/methods/resource_get.rb +186 -0
  100. data/lib/flapjack/gateways/jsonapi/methods/resource_patch.rb +239 -0
  101. data/lib/flapjack/gateways/jsonapi/methods/resource_post.rb +197 -0
  102. data/lib/flapjack/gateways/jsonapi/middleware/array_param_fixer.rb +27 -0
  103. data/lib/flapjack/gateways/jsonapi/{rack → middleware}/json_params_parser.rb +7 -6
  104. data/lib/flapjack/gateways/jsonapi/middleware/request_timestamp.rb +18 -0
  105. data/lib/flapjack/gateways/oobetet.rb +222 -170
  106. data/lib/flapjack/gateways/pager_duty.rb +388 -0
  107. data/lib/flapjack/gateways/pager_duty/alert.text.erb +13 -0
  108. data/lib/flapjack/gateways/slack.rb +56 -48
  109. data/lib/flapjack/gateways/slack/alert.text.erb +1 -1
  110. data/lib/flapjack/gateways/slack/rollup.text.erb +1 -1
  111. data/lib/flapjack/gateways/sms_aspsms.rb +155 -0
  112. data/lib/flapjack/gateways/sms_aspsms/alert.text.erb +7 -0
  113. data/lib/flapjack/gateways/sms_aspsms/rollup.text.erb +2 -0
  114. data/lib/flapjack/gateways/sms_messagenet.rb +77 -57
  115. data/lib/flapjack/gateways/sms_messagenet/alert.text.erb +3 -2
  116. data/lib/flapjack/gateways/sms_nexmo.rb +53 -51
  117. data/lib/flapjack/gateways/sms_nexmo/alert.text.erb +2 -2
  118. data/lib/flapjack/gateways/sms_nexmo/rollup.text.erb +1 -1
  119. data/lib/flapjack/gateways/sms_twilio.rb +79 -62
  120. data/lib/flapjack/gateways/sms_twilio/alert.text.erb +3 -2
  121. data/lib/flapjack/gateways/web.rb +437 -345
  122. data/lib/flapjack/gateways/web/middleware/request_timestamp.rb +18 -0
  123. data/lib/flapjack/gateways/web/public/css/bootstrap.css +3793 -4340
  124. data/lib/flapjack/gateways/web/public/css/bootstrap.css.map +1 -0
  125. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.eot +0 -0
  126. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.svg +273 -214
  127. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  128. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.woff +0 -0
  129. data/lib/flapjack/gateways/web/public/fonts/glyphicons-halflings-regular.woff2 +0 -0
  130. data/lib/flapjack/gateways/web/public/js/bootstrap.js +1637 -1607
  131. data/lib/flapjack/gateways/web/public/js/self_stats.js +1 -2
  132. data/lib/flapjack/gateways/web/views/_pagination.html.erb +19 -0
  133. data/lib/flapjack/gateways/web/views/check.html.erb +159 -121
  134. data/lib/flapjack/gateways/web/views/checks.html.erb +82 -41
  135. data/lib/flapjack/gateways/web/views/contact.html.erb +59 -71
  136. data/lib/flapjack/gateways/web/views/contacts.html.erb +32 -8
  137. data/lib/flapjack/gateways/web/views/index.html.erb +2 -2
  138. data/lib/flapjack/gateways/web/views/{layout.erb → layout.html.erb} +7 -23
  139. data/lib/flapjack/gateways/web/views/self_stats.html.erb +32 -33
  140. data/lib/flapjack/gateways/web/views/tag.html.erb +32 -0
  141. data/lib/flapjack/gateways/web/views/tags.html.erb +51 -0
  142. data/lib/flapjack/logger.rb +34 -3
  143. data/lib/flapjack/notifier.rb +180 -112
  144. data/lib/flapjack/patches.rb +8 -63
  145. data/lib/flapjack/pikelet.rb +185 -143
  146. data/lib/flapjack/processor.rb +323 -191
  147. data/lib/flapjack/record_queue.rb +33 -0
  148. data/lib/flapjack/redis_proxy.rb +66 -0
  149. data/lib/flapjack/utility.rb +21 -15
  150. data/lib/flapjack/version.rb +2 -1
  151. data/libexec/httpbroker.go +218 -14
  152. data/libexec/oneoff.go +13 -10
  153. data/spec/lib/flapjack/configuration_spec.rb +286 -0
  154. data/spec/lib/flapjack/coordinator_spec.rb +103 -157
  155. data/spec/lib/flapjack/data/check_spec.rb +175 -0
  156. data/spec/lib/flapjack/data/contact_spec.rb +26 -349
  157. data/spec/lib/flapjack/data/event_spec.rb +76 -291
  158. data/spec/lib/flapjack/data/medium_spec.rb +19 -0
  159. data/spec/lib/flapjack/data/rule_spec.rb +43 -0
  160. data/spec/lib/flapjack/data/scheduled_maintenance_spec.rb +976 -0
  161. data/spec/lib/flapjack/data/unscheduled_maintenance_spec.rb +34 -0
  162. data/spec/lib/flapjack/gateways/aws_sns_spec.rb +111 -60
  163. data/spec/lib/flapjack/gateways/email_spec.rb +194 -161
  164. data/spec/lib/flapjack/gateways/jabber_spec.rb +961 -162
  165. data/spec/lib/flapjack/gateways/jsonapi/methods/check_links_spec.rb +155 -0
  166. data/spec/lib/flapjack/gateways/jsonapi/methods/checks_spec.rb +426 -0
  167. data/spec/lib/flapjack/gateways/jsonapi/methods/contact_links_spec.rb +217 -0
  168. data/spec/lib/flapjack/gateways/jsonapi/methods/contacts_spec.rb +425 -0
  169. data/spec/lib/flapjack/gateways/jsonapi/methods/events_spec.rb +271 -0
  170. data/spec/lib/flapjack/gateways/jsonapi/methods/media_spec.rb +257 -0
  171. data/spec/lib/flapjack/gateways/jsonapi/methods/medium_links_spec.rb +163 -0
  172. data/spec/lib/flapjack/gateways/jsonapi/methods/metrics_spec.rb +8 -0
  173. data/spec/lib/flapjack/gateways/jsonapi/methods/rule_links_spec.rb +212 -0
  174. data/spec/lib/flapjack/gateways/jsonapi/methods/rules_spec.rb +289 -0
  175. data/spec/lib/flapjack/gateways/jsonapi/methods/scheduled_maintenance_links_spec.rb +49 -0
  176. data/spec/lib/flapjack/gateways/jsonapi/methods/scheduled_maintenances_spec.rb +242 -0
  177. data/spec/lib/flapjack/gateways/jsonapi/methods/tag_links_spec.rb +274 -0
  178. data/spec/lib/flapjack/gateways/jsonapi/methods/tags_spec.rb +302 -0
  179. data/spec/lib/flapjack/gateways/jsonapi/methods/unscheduled_maintenance_links_spec.rb +49 -0
  180. data/spec/lib/flapjack/gateways/jsonapi/methods/unscheduled_maintenances_spec.rb +339 -0
  181. data/spec/lib/flapjack/gateways/jsonapi_spec.rb +1 -1
  182. data/spec/lib/flapjack/gateways/oobetet_spec.rb +151 -79
  183. data/spec/lib/flapjack/gateways/pager_duty_spec.rb +353 -0
  184. data/spec/lib/flapjack/gateways/slack_spec.rb +53 -53
  185. data/spec/lib/flapjack/gateways/sms_aspsms_spec.rb +106 -0
  186. data/spec/lib/flapjack/gateways/sms_messagenet_spec.rb +111 -54
  187. data/spec/lib/flapjack/gateways/sms_nexmo_spec.rb +50 -51
  188. data/spec/lib/flapjack/gateways/sms_twilio_spec.rb +108 -48
  189. data/spec/lib/flapjack/gateways/web_spec.rb +144 -216
  190. data/spec/lib/flapjack/notifier_spec.rb +132 -1
  191. data/spec/lib/flapjack/pikelet_spec.rb +111 -50
  192. data/spec/lib/flapjack/processor_spec.rb +210 -40
  193. data/spec/lib/flapjack/redis_proxy_spec.rb +45 -0
  194. data/spec/lib/flapjack/utility_spec.rb +11 -15
  195. data/spec/service_consumers/fixture_data.rb +547 -0
  196. data/spec/service_consumers/pact_helper.rb +21 -32
  197. data/spec/service_consumers/pacts/flapjack-diner_v2.0.json +4652 -0
  198. data/spec/service_consumers/provider_states_for_flapjack-diner.rb +279 -322
  199. data/spec/service_consumers/provider_support.rb +8 -0
  200. data/spec/spec_helper.rb +34 -44
  201. data/spec/support/erb_view_helper.rb +1 -1
  202. data/spec/support/factories.rb +58 -0
  203. data/spec/support/jsonapi_helper.rb +15 -26
  204. data/spec/support/mock_logger.rb +43 -0
  205. data/spec/support/xmpp_comparable.rb +24 -0
  206. data/src/flapjack/transport_test.go +30 -1
  207. data/tasks/dump_keys.rake +82 -0
  208. data/tasks/events.rake +7 -7
  209. data/tasks/support/flapjack_config_benchmark.toml +28 -0
  210. data/tasks/support/flapjack_config_benchmark.yaml +0 -2
  211. metadata +175 -222
  212. data/Guardfile +0 -14
  213. data/etc/flapjack_config.yaml.example +0 -477
  214. data/features/cli_flapjack-populator.feature +0 -90
  215. data/features/support/silent_system.rb +0 -4
  216. data/lib/flapjack/cli/import.rb +0 -108
  217. data/lib/flapjack/data/entity.rb +0 -652
  218. data/lib/flapjack/data/entity_check.rb +0 -1044
  219. data/lib/flapjack/data/message.rb +0 -56
  220. data/lib/flapjack/data/migration.rb +0 -234
  221. data/lib/flapjack/data/notification_rule.rb +0 -425
  222. data/lib/flapjack/data/semaphore.rb +0 -44
  223. data/lib/flapjack/data/tagged.rb +0 -48
  224. data/lib/flapjack/gateways/jsonapi/check_methods.rb +0 -206
  225. data/lib/flapjack/gateways/jsonapi/check_presenter.rb +0 -221
  226. data/lib/flapjack/gateways/jsonapi/contact_methods.rb +0 -186
  227. data/lib/flapjack/gateways/jsonapi/entity_methods.rb +0 -223
  228. data/lib/flapjack/gateways/jsonapi/medium_methods.rb +0 -185
  229. data/lib/flapjack/gateways/jsonapi/metrics_methods.rb +0 -132
  230. data/lib/flapjack/gateways/jsonapi/notification_rule_methods.rb +0 -141
  231. data/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods.rb +0 -139
  232. data/lib/flapjack/gateways/jsonapi/report_methods.rb +0 -146
  233. data/lib/flapjack/gateways/pagerduty.rb +0 -318
  234. data/lib/flapjack/gateways/pagerduty/alert.text.erb +0 -10
  235. data/lib/flapjack/gateways/web/public/css/select2-bootstrap.css +0 -87
  236. data/lib/flapjack/gateways/web/public/css/select2.css +0 -615
  237. data/lib/flapjack/gateways/web/public/css/tablesort.css +0 -67
  238. data/lib/flapjack/gateways/web/public/img/select2-spinner.gif +0 -0
  239. data/lib/flapjack/gateways/web/public/img/select2.png +0 -0
  240. data/lib/flapjack/gateways/web/public/img/select2x2.png +0 -0
  241. data/lib/flapjack/gateways/web/public/js/backbone.js +0 -1581
  242. data/lib/flapjack/gateways/web/public/js/backbone.jsonapi.js +0 -322
  243. data/lib/flapjack/gateways/web/public/js/flapjack.js +0 -82
  244. data/lib/flapjack/gateways/web/public/js/jquery.tablesorter.js +0 -1640
  245. data/lib/flapjack/gateways/web/public/js/jquery.tablesorter.widgets.js +0 -1390
  246. data/lib/flapjack/gateways/web/public/js/modules/contact.js +0 -520
  247. data/lib/flapjack/gateways/web/public/js/modules/entity.js +0 -28
  248. data/lib/flapjack/gateways/web/public/js/modules/medium.js +0 -40
  249. data/lib/flapjack/gateways/web/public/js/select2.js +0 -3397
  250. data/lib/flapjack/gateways/web/public/js/tablesort.js +0 -44
  251. data/lib/flapjack/gateways/web/public/js/underscore.js +0 -1276
  252. data/lib/flapjack/gateways/web/views/edit_contacts.html.erb +0 -173
  253. data/lib/flapjack/gateways/web/views/entities.html.erb +0 -30
  254. data/lib/flapjack/gateways/web/views/entity.html.erb +0 -51
  255. data/lib/flapjack/rack_logger.rb +0 -47
  256. data/lib/flapjack/redis_pool.rb +0 -42
  257. data/spec/lib/flapjack/data/entity_check_spec.rb +0 -1418
  258. data/spec/lib/flapjack/data/entity_spec.rb +0 -872
  259. data/spec/lib/flapjack/data/message_spec.rb +0 -30
  260. data/spec/lib/flapjack/data/migration_spec.rb +0 -104
  261. data/spec/lib/flapjack/data/notification_rule_spec.rb +0 -232
  262. data/spec/lib/flapjack/data/notification_spec.rb +0 -53
  263. data/spec/lib/flapjack/data/semaphore_spec.rb +0 -24
  264. data/spec/lib/flapjack/filters/acknowledgement_spec.rb +0 -6
  265. data/spec/lib/flapjack/filters/delays_spec.rb +0 -6
  266. data/spec/lib/flapjack/filters/ok_spec.rb +0 -6
  267. data/spec/lib/flapjack/filters/scheduled_maintenance_spec.rb +0 -6
  268. data/spec/lib/flapjack/filters/unscheduled_maintenance_spec.rb +0 -6
  269. data/spec/lib/flapjack/gateways/jsonapi/check_methods_spec.rb +0 -315
  270. data/spec/lib/flapjack/gateways/jsonapi/check_presenter_spec.rb +0 -223
  271. data/spec/lib/flapjack/gateways/jsonapi/contact_methods_spec.rb +0 -131
  272. data/spec/lib/flapjack/gateways/jsonapi/entity_methods_spec.rb +0 -389
  273. data/spec/lib/flapjack/gateways/jsonapi/medium_methods_spec.rb +0 -231
  274. data/spec/lib/flapjack/gateways/jsonapi/notification_rule_methods_spec.rb +0 -169
  275. data/spec/lib/flapjack/gateways/jsonapi/pagerduty_credential_methods_spec.rb +0 -114
  276. data/spec/lib/flapjack/gateways/jsonapi/report_methods_spec.rb +0 -590
  277. data/spec/lib/flapjack/gateways/pagerduty_spec.rb +0 -249
  278. data/spec/lib/flapjack/gateways/web/views/check.html.erb_spec.rb +0 -21
  279. data/spec/lib/flapjack/gateways/web/views/contact.html.erb_spec.rb +0 -24
  280. data/spec/lib/flapjack/gateways/web/views/index.html.erb_spec.rb +0 -16
  281. data/spec/lib/flapjack/redis_pool_spec.rb +0 -29
  282. data/spec/service_consumers/pacts/flapjack-diner_v1.0.json +0 -4702
  283. data/tasks/entities.rake +0 -151
  284. data/tasks/profile.rake +0 -282
  285. data/tmp/acknowledge.rb +0 -13
  286. data/tmp/create_config_yaml.rb +0 -16
  287. data/tmp/create_event_ok.rb +0 -30
  288. data/tmp/create_event_unknown.rb +0 -30
  289. data/tmp/create_events_failure.rb +0 -34
  290. data/tmp/create_events_ok.rb +0 -32
  291. data/tmp/create_events_ok_fail_ack_ok.rb +0 -53
  292. data/tmp/create_events_ok_failure.rb +0 -41
  293. data/tmp/create_events_ok_failure_ack.rb +0 -53
  294. data/tmp/dummy_contacts.json +0 -43
  295. data/tmp/dummy_entities.json +0 -37
  296. data/tmp/generate_nagios_test_hosts.rb +0 -16
  297. data/tmp/notification_rules.rb +0 -73
  298. data/tmp/parse_config_yaml.rb +0 -7
  299. data/tmp/redis_find_spurious_unknown_states.rb +0 -52
  300. data/tmp/test_json_post.rb +0 -19
  301. data/tmp/test_notification_rules_api.rb +0 -171
@@ -1,1390 +0,0 @@
1
- /*! tableSorter 2.8+ widgets - updated 12/16/2013 (v2.14.5)
2
- *
3
- * Column Styles
4
- * Column Filters
5
- * Column Resizing
6
- * Sticky Header
7
- * UI Theme (generalized)
8
- * Save Sort
9
- * [ "columns", "filter", "resizable", "stickyHeaders", "uitheme", "saveSort" ]
10
- */
11
- /*jshint browser:true, jquery:true, unused:false, loopfunc:true */
12
- /*global jQuery: false, localStorage: false, navigator: false */
13
- ;(function($) {
14
- "use strict";
15
- var ts = $.tablesorter = $.tablesorter || {};
16
-
17
- ts.themes = {
18
- "bootstrap" : {
19
- table : 'table table-bordered table-striped',
20
- caption : 'caption',
21
- header : 'bootstrap-header', // give the header a gradient background
22
- footerRow : '',
23
- footerCells: '',
24
- icons : '', // add "icon-white" to make them white; this icon class is added to the <i> in the header
25
- sortNone : 'bootstrap-icon-unsorted',
26
- sortAsc : 'icon-chevron-up glyphicon glyphicon-chevron-up',
27
- sortDesc : 'icon-chevron-down glyphicon glyphicon-chevron-down',
28
- active : '', // applied when column is sorted
29
- hover : '', // use custom css here - bootstrap class may not override it
30
- filterRow : '', // filter row class
31
- even : '', // even row zebra striping
32
- odd : '' // odd row zebra striping
33
- },
34
- "jui" : {
35
- table : 'ui-widget ui-widget-content ui-corner-all', // table classes
36
- caption : 'ui-widget-content ui-corner-all',
37
- header : 'ui-widget-header ui-corner-all ui-state-default', // header classes
38
- footerRow : '',
39
- footerCells: '',
40
- icons : 'ui-icon', // icon class added to the <i> in the header
41
- sortNone : 'ui-icon-carat-2-n-s',
42
- sortAsc : 'ui-icon-carat-1-n',
43
- sortDesc : 'ui-icon-carat-1-s',
44
- active : 'ui-state-active', // applied when column is sorted
45
- hover : 'ui-state-hover', // hover class
46
- filterRow : '',
47
- even : 'ui-widget-content', // even row zebra striping
48
- odd : 'ui-state-default' // odd row zebra striping
49
- }
50
- };
51
-
52
- // *** Store data in local storage, with a cookie fallback ***
53
- /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json)
54
- if you need it, then include https://github.com/douglascrockford/JSON-js
55
-
56
- $.parseJSON is not available is jQuery versions older than 1.4.1, using older
57
- versions will only allow storing information for one page at a time
58
-
59
- // *** Save data (JSON format only) ***
60
- // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid
61
- var val = { "mywidget" : "data1" }; // valid JSON uses double quotes
62
- // $.tablesorter.storage(table, key, val);
63
- $.tablesorter.storage(table, 'tablesorter-mywidget', val);
64
-
65
- // *** Get data: $.tablesorter.storage(table, key); ***
66
- v = $.tablesorter.storage(table, 'tablesorter-mywidget');
67
- // val may be empty, so also check for your data
68
- val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : '';
69
- alert(val); // "data1" if saved, or "" if not
70
- */
71
- ts.storage = function(table, key, value, options) {
72
- table = $(table)[0];
73
- var cookieIndex, cookies, date,
74
- hasLocalStorage = false,
75
- values = {},
76
- c = table.config,
77
- $table = $(table),
78
- id = options && options.id || $table.attr(options && options.group ||
79
- 'data-table-group') || table.id || $('.tablesorter').index( $table ),
80
- url = options && options.url || $table.attr(options && options.page ||
81
- 'data-table-page') || c && c.fixedUrl || window.location.pathname;
82
- // https://gist.github.com/paulirish/5558557
83
- if ("localStorage" in window) {
84
- try {
85
- window.localStorage.setItem('_tmptest', 'temp');
86
- hasLocalStorage = true;
87
- window.localStorage.removeItem('_tmptest');
88
- } catch(error) {}
89
- }
90
- // *** get value ***
91
- if ($.parseJSON) {
92
- if (hasLocalStorage) {
93
- values = $.parseJSON(localStorage[key] || '{}');
94
- } else {
95
- // old browser, using cookies
96
- cookies = document.cookie.split(/[;\s|=]/);
97
- // add one to get from the key to the value
98
- cookieIndex = $.inArray(key, cookies) + 1;
99
- values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || '{}') : {};
100
- }
101
- }
102
- // allow value to be an empty string too
103
- if ((value || value === '') && window.JSON && JSON.hasOwnProperty('stringify')) {
104
- // add unique identifiers = url pathname > table ID/index on page > data
105
- if (!values[url]) {
106
- values[url] = {};
107
- }
108
- values[url][id] = value;
109
- // *** set value ***
110
- if (hasLocalStorage) {
111
- localStorage[key] = JSON.stringify(values);
112
- } else {
113
- date = new Date();
114
- date.setTime(date.getTime() + (31536e+6)); // 365 days
115
- document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g,'\"') + '; expires=' + date.toGMTString() + '; path=/';
116
- }
117
- } else {
118
- return values && values[url] ? values[url][id] : {};
119
- }
120
- };
121
-
122
- // Add a resize event to table headers
123
- // **************************
124
- ts.addHeaderResizeEvent = function(table, disable, settings) {
125
- var headers,
126
- defaults = {
127
- timer : 250
128
- },
129
- options = $.extend({}, defaults, settings),
130
- c = table.config,
131
- wo = c.widgetOptions,
132
- checkSizes = function(triggerEvent) {
133
- wo.resize_flag = true;
134
- headers = [];
135
- c.$headers.each(function() {
136
- var $header = $(this),
137
- sizes = $header.data('savedSizes') || [0,0], // fixes #394
138
- width = this.offsetWidth,
139
- height = this.offsetHeight;
140
- if (width !== sizes[0] || height !== sizes[1]) {
141
- $header.data('savedSizes', [ width, height ]);
142
- headers.push(this);
143
- }
144
- });
145
- if (headers.length && triggerEvent !== false) {
146
- c.$table.trigger('resize', [ headers ]);
147
- }
148
- wo.resize_flag = false;
149
- };
150
- checkSizes(false);
151
- clearInterval(wo.resize_timer);
152
- if (disable) {
153
- wo.resize_flag = false;
154
- return false;
155
- }
156
- wo.resize_timer = setInterval(function() {
157
- if (wo.resize_flag) { return; }
158
- checkSizes();
159
- }, options.timer);
160
- };
161
-
162
- // Widget: General UI theme
163
- // "uitheme" option in "widgetOptions"
164
- // **************************
165
- ts.addWidget({
166
- id: "uitheme",
167
- priority: 10,
168
- format: function(table, c, wo) {
169
- var time, classes, $header, $icon, $tfoot,
170
- themesAll = ts.themes,
171
- $table = c.$table,
172
- $headers = c.$headers,
173
- theme = c.theme || 'jui',
174
- themes = themesAll[theme] || themesAll.jui,
175
- remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc;
176
- if (c.debug) { time = new Date(); }
177
- // initialization code - run once
178
- if (!$table.hasClass('tablesorter-' + theme) || c.theme === theme || !table.hasInitialized) {
179
- // update zebra stripes
180
- if (themes.even !== '') { wo.zebra[0] += ' ' + themes.even; }
181
- if (themes.odd !== '') { wo.zebra[1] += ' ' + themes.odd; }
182
- // add caption style
183
- $table.find('caption').addClass(themes.caption);
184
- // add table/footer class names
185
- $tfoot = $table
186
- // remove other selected themes
187
- .removeClass( c.theme === '' ? '' : 'tablesorter-' + c.theme )
188
- .addClass('tablesorter-' + theme + ' ' + themes.table) // add theme widget class name
189
- .find('tfoot');
190
- if ($tfoot.length) {
191
- $tfoot
192
- .find('tr').addClass(themes.footerRow)
193
- .children('th, td').addClass(themes.footerCells);
194
- }
195
- // update header classes
196
- $headers
197
- .addClass(themes.header)
198
- .not('.sorter-false')
199
- .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) {
200
- // toggleClass with switch added in jQuery 1.3
201
- $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover);
202
- });
203
- if (!$headers.find('.tablesorter-wrapper').length) {
204
- // Firefox needs this inner div to position the resizer correctly
205
- $headers.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>');
206
- }
207
- if (c.cssIcon) {
208
- // if c.cssIcon is '', then no <i> is added to the header
209
- $headers.find('.' + ts.css.icon).addClass(themes.icons);
210
- }
211
- if ($table.hasClass('hasFilters')) {
212
- $headers.find('.tablesorter-filter-row').addClass(themes.filterRow);
213
- }
214
- }
215
- $.each($headers, function() {
216
- $header = $(this);
217
- $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $header;
218
- if (this.sortDisabled) {
219
- // no sort arrows for disabled columns!
220
- $header.removeClass(remove);
221
- $icon.removeClass(remove + ' tablesorter-icon ' + themes.icons);
222
- } else {
223
- classes = ($header.hasClass(ts.css.sortAsc)) ?
224
- themes.sortAsc :
225
- ($header.hasClass(ts.css.sortDesc)) ? themes.sortDesc :
226
- $header.hasClass(ts.css.header) ? themes.sortNone : '';
227
- $header[classes === themes.sortNone ? 'removeClass' : 'addClass'](themes.active);
228
- $icon.removeClass(remove).addClass(classes);
229
- }
230
- });
231
- if (c.debug) {
232
- ts.benchmark("Applying " + theme + " theme", time);
233
- }
234
- },
235
- remove: function(table, c, wo) {
236
- var $table = c.$table,
237
- theme = c.theme || 'jui',
238
- themes = ts.themes[ theme ] || ts.themes.jui,
239
- $headers = $table.children('thead').children(),
240
- remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc;
241
- $table
242
- .removeClass('tablesorter-' + theme + ' ' + themes.table)
243
- .find(ts.css.header).removeClass(themes.header);
244
- $headers
245
- .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover
246
- .removeClass(themes.hover + ' ' + remove + ' ' + themes.active)
247
- .find('.tablesorter-filter-row')
248
- .removeClass(themes.filterRow);
249
- $headers.find('.tablesorter-icon').removeClass(themes.icons);
250
- }
251
- });
252
-
253
- // Widget: Column styles
254
- // "columns", "columns_thead" (true) and
255
- // "columns_tfoot" (true) options in "widgetOptions"
256
- // **************************
257
- ts.addWidget({
258
- id: "columns",
259
- priority: 30,
260
- options : {
261
- columns : [ "primary", "secondary", "tertiary" ]
262
- },
263
- format: function(table, c, wo) {
264
- var time, $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx,
265
- $table = c.$table,
266
- $tbodies = c.$tbodies,
267
- sortList = c.sortList,
268
- len = sortList.length,
269
- // removed c.widgetColumns support
270
- css = wo && wo.columns || [ "primary", "secondary", "tertiary" ],
271
- last = css.length - 1;
272
- remove = css.join(' ');
273
- if (c.debug) {
274
- time = new Date();
275
- }
276
- // check if there is a sort (on initialization there may not be one)
277
- for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
278
- $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody
279
- $rows = $tbody.children('tr');
280
- // loop through the visible rows
281
- $rows.each(function() {
282
- $row = $(this);
283
- if (this.style.display !== 'none') {
284
- // remove all columns class names
285
- $cells = $row.children().removeClass(remove);
286
- // add appropriate column class names
287
- if (sortList && sortList[0]) {
288
- // primary sort column class
289
- $cells.eq(sortList[0][0]).addClass(css[0]);
290
- if (len > 1) {
291
- for (indx = 1; indx < len; indx++) {
292
- // secondary, tertiary, etc sort column classes
293
- $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] );
294
- }
295
- }
296
- }
297
- }
298
- });
299
- ts.processTbody(table, $tbody, false);
300
- }
301
- // add classes to thead and tfoot
302
- rows = wo.columns_thead !== false ? ['thead tr'] : [];
303
- if (wo.columns_tfoot !== false) {
304
- rows.push('tfoot tr');
305
- }
306
- if (rows.length) {
307
- $rows = $table.find( rows.join(',') ).children().removeClass(remove);
308
- if (len) {
309
- for (indx = 0; indx < len; indx++) {
310
- // add primary. secondary, tertiary, etc sort column classes
311
- $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]);
312
- }
313
- }
314
- }
315
- if (c.debug) {
316
- ts.benchmark("Applying Columns widget", time);
317
- }
318
- },
319
- remove: function(table, c, wo) {
320
- var tbodyIndex, $tbody,
321
- $tbodies = c.$tbodies,
322
- remove = (wo.columns || [ "primary", "secondary", "tertiary" ]).join(' ');
323
- c.$headers.removeClass(remove);
324
- c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove);
325
- for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
326
- $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
327
- $tbody.children('tr').each(function() {
328
- $(this).children().removeClass(remove);
329
- });
330
- ts.processTbody(table, $tbody, false); // restore tbody
331
- }
332
- }
333
- });
334
-
335
- // Widget: filter
336
- // **************************
337
- ts.addWidget({
338
- id: "filter",
339
- priority: 50,
340
- options : {
341
- filter_anyMatch : false, // if true overrides default find rows behaviours and if any column matches query it returns that row
342
- filter_childRows : false, // if true, filter includes child row content in the search
343
- filter_columnFilters : true, // if true, a filter will be added to the top of each table column
344
- filter_cssFilter : '', // css class name added to the filter row & each input in the row (tablesorter-filter is ALWAYS added)
345
- filter_filteredRow : 'filtered', // class added to filtered rows; needed by pager plugin
346
- filter_formatter : null, // add custom filter elements to the filter row
347
- filter_functions : null, // add custom filter functions using this option
348
- filter_hideFilters : false, // collapse filter row when mouse leaves the area
349
- filter_ignoreCase : true, // if true, make all searches case-insensitive
350
- filter_liveSearch : true, // if true, search column content while the user types (with a delay)
351
- filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available (visible) options within the drop down
352
- filter_reset : null, // jQuery selector string of an element used to reset the filters
353
- filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters
354
- filter_searchDelay : 300, // typing delay in milliseconds before starting a search
355
- filter_startsWith : false, // if true, filter start from the beginning of the cell contents
356
- filter_useParsedData : false, // filter all data using parsed content
357
- filter_serversideFiltering : true, // if true, server-side filtering should be performed because client-side filtering will be disabled, but the ui and events will still be used.
358
- filter_defaultAttrib : 'data-value' // data attribute in the header cell that contains the default filter value
359
- },
360
- format: function(table, c, wo) {
361
- if (!c.$table.hasClass('hasFilters')) {
362
- if (c.parsers || !c.parsers && wo.filter_serversideFiltering) {
363
- ts.filter.init(table, c, wo);
364
- }
365
- }
366
- },
367
- remove: function(table, c, wo) {
368
- var tbodyIndex, $tbody,
369
- $table = c.$table,
370
- $tbodies = c.$tbodies;
371
- $table
372
- .removeClass('hasFilters')
373
- // add .tsfilter namespace to all BUT search
374
- .unbind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '))
375
- .find('.tablesorter-filter-row').remove();
376
- for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
377
- $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody
378
- $tbody.children().removeClass(wo.filter_filteredRow).show();
379
- ts.processTbody(table, $tbody, false); // restore tbody
380
- }
381
- if (wo.filter_reset) {
382
- $(document).undelegate(wo.filter_reset, 'click.tsfilter');
383
- }
384
- }
385
- });
386
-
387
- ts.filter = {
388
-
389
- // regex used in filter "check" functions - not for general use and not documented
390
- regex: {
391
- regex : /^\/((?:\\\/|[^\/])+)\/([mig]{0,3})?$/, // regex to test for regex
392
- child : /tablesorter-childRow/, // child row class name; this gets updated in the script
393
- filtered : /filtered/, // filtered (hidden) row class name; updated in the script
394
- type : /undefined|number/, // check type
395
- exact : /(^[\"|\'|=]+)|([\"|\'|=]+$)/g, // exact match (allow '==')
396
- nondigit : /[^\w,. \-()]/g, // replace non-digits (from digit & currency parser)
397
- operators : /[<>=]/g // replace operators
398
- },
399
- // function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed )
400
- // filter = array of filter input values; iFilter = same array, except lowercase
401
- // exact = table cell text (or parsed data if column parser enabled)
402
- // iExact = same as exact, except lowercase
403
- // cached = table cell text from cache, so it has been parsed
404
- // index = column index; table = table element (DOM)
405
- // wo = widget options (table.config.widgetOptions)
406
- // parsed = array (by column) of boolean values (from filter_useParsedData or "filter-parsed" class)
407
- types: {
408
- // Look for regex
409
- regex: function( filter, iFilter, exact, iExact ) {
410
- if ( ts.filter.regex.regex.test(iFilter) ) {
411
- var matches,
412
- regex = ts.filter.regex.regex.exec(iFilter);
413
- try {
414
- matches = new RegExp(regex[1], regex[2]).test( iExact );
415
- } catch (error) {
416
- matches = false;
417
- }
418
- return matches;
419
- }
420
- return null;
421
- },
422
- // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric
423
- exact: function( filter, iFilter, exact, iExact ) {
424
- /*jshint eqeqeq:false */
425
- if (ts.filter.regex.exact.test(iFilter)) {
426
- return iFilter.replace(ts.filter.regex.exact, '') == iExact;
427
- }
428
- return null;
429
- },
430
- // Look for a not match
431
- notMatch: function( filter, iFilter, exact, iExact, cached, index, table, wo ) {
432
- if ( /^\!/.test(iFilter) ) {
433
- iFilter = iFilter.replace('!', '');
434
- var indx = iExact.search( $.trim(iFilter) );
435
- return iFilter === '' ? true : !(wo.filter_startsWith ? indx === 0 : indx >= 0);
436
- }
437
- return null;
438
- },
439
- // Look for operators >, >=, < or <=
440
- operators: function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) {
441
- if ( /^[<>]=?/.test(iFilter) ) {
442
- var cachedValue, result,
443
- c = table.config,
444
- query = ts.formatFloat( iFilter.replace(ts.filter.regex.operators, ''), table ),
445
- parser = c.parsers[index],
446
- savedSearch = query;
447
- // parse filter value in case we're comparing numbers (dates)
448
- if (parsed[index] || parser.type === 'numeric') {
449
- cachedValue = parser.format( '' + iFilter.replace(ts.filter.regex.operators, ''), table, c.$headers.eq(index), index );
450
- query = ( typeof query === "number" && cachedValue !== '' && !isNaN(cachedValue) ) ? cachedValue : query;
451
- }
452
- // iExact may be numeric - see issue #149;
453
- // check if cached is defined, because sometimes j goes out of range? (numeric columns)
454
- cachedValue = ( parsed[index] || parser.type === 'numeric' ) && !isNaN(query) && cached ? cached :
455
- isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) :
456
- ts.formatFloat( iExact, table );
457
- if ( />/.test(iFilter) ) { result = />=/.test(iFilter) ? cachedValue >= query : cachedValue > query; }
458
- if ( /</.test(iFilter) ) { result = /<=/.test(iFilter) ? cachedValue <= query : cachedValue < query; }
459
- // keep showing all rows if nothing follows the operator
460
- if ( !result && savedSearch === '' ) { result = true; }
461
- return result;
462
- }
463
- return null;
464
- },
465
- // Look for an AND or && operator (logical and)
466
- and : function( filter, iFilter, exact, iExact ) {
467
- if ( /\s+(AND|&&)\s+/g.test(filter) ) {
468
- var query = iFilter.split( /(?:\s+(?:and|&&)\s+)/g ),
469
- result = iExact.search( $.trim(query[0]) ) >= 0,
470
- indx = query.length - 1;
471
- while (result && indx) {
472
- result = result && iExact.search( $.trim(query[indx]) ) >= 0;
473
- indx--;
474
- }
475
- return result;
476
- }
477
- return null;
478
- },
479
- // Look for a range (using " to " or " - ") - see issue #166; thanks matzhu!
480
- range : function( filter, iFilter, exact, iExact, cached, index, table, wo, parsed ) {
481
- if ( /\s+(-|to)\s+/.test(iFilter) ) {
482
- var result, tmp,
483
- c = table.config,
484
- query = iFilter.split(/(?: - | to )/), // make sure the dash is for a range and not indicating a negative number
485
- range1 = ts.formatFloat(query[0].replace(ts.filter.regex.nondigit, ''), table),
486
- range2 = ts.formatFloat(query[1].replace(ts.filter.regex.nondigit, ''), table);
487
- // parse filter value in case we're comparing numbers (dates)
488
- if (parsed[index] || c.parsers[index].type === 'numeric') {
489
- result = c.parsers[index].format('' + query[0], table, c.$headers.eq(index), index);
490
- range1 = (result !== '' && !isNaN(result)) ? result : range1;
491
- result = c.parsers[index].format('' + query[1], table, c.$headers.eq(index), index);
492
- range2 = (result !== '' && !isNaN(result)) ? result : range2;
493
- }
494
- result = ( parsed[index] || c.parsers[index].type === 'numeric' ) && !isNaN(range1) && !isNaN(range2) ? cached :
495
- isNaN(iExact) ? ts.formatFloat( iExact.replace(ts.filter.regex.nondigit, ''), table) :
496
- ts.formatFloat( iExact, table );
497
- if (range1 > range2) { tmp = range1; range1 = range2; range2 = tmp; } // swap
498
- return (result >= range1 && result <= range2) || (range1 === '' || range2 === '');
499
- }
500
- return null;
501
- },
502
- // Look for wild card: ? = single, * = multiple, or | = logical OR
503
- wild : function( filter, iFilter, exact, iExact, cached, index, table ) {
504
- if ( /[\?|\*]/.test(iFilter) || /\s+OR\s+/.test(filter) ) {
505
- var c = table.config,
506
- query = iFilter.replace(/\s+OR\s+/gi,"|");
507
- // look for an exact match with the "or" unless the "filter-match" class is found
508
- if (!c.$headers.filter('[data-column="' + index + '"]:last').hasClass('filter-match') && /\|/.test(query)) {
509
- query = '^(' + query + ')$';
510
- }
511
- return new RegExp( query.replace(/\?/g, '\\S{1}').replace(/\*/g, '\\S*') ).test(iExact);
512
- }
513
- return null;
514
- },
515
- // fuzzy text search; modified from https://github.com/mattyork/fuzzy (MIT license)
516
- fuzzy: function( filter, iFilter, exact, iExact ) {
517
- if ( /^~/.test(iFilter) ) {
518
- var indx,
519
- patternIndx = 0,
520
- len = iExact.length,
521
- pattern = iFilter.slice(1);
522
- for (indx = 0; indx < len; indx++) {
523
- if (iExact[indx] === pattern[patternIndx]) {
524
- patternIndx += 1;
525
- }
526
- }
527
- if (patternIndx === pattern.length) {
528
- return true;
529
- }
530
- return false;
531
- }
532
- return null;
533
- }
534
- },
535
- init: function(table, c, wo) {
536
- var options, string, $header, column, filters, time;
537
- if (c.debug) {
538
- time = new Date();
539
- }
540
- c.$table.addClass('hasFilters');
541
-
542
- ts.filter.regex.child = new RegExp(c.cssChildRow);
543
- ts.filter.regex.filtered = new RegExp(wo.filter_filteredRow);
544
-
545
- // don't build filter row if columnFilters is false or all columns are set to "filter-false" - issue #156
546
- if (wo.filter_columnFilters !== false && c.$headers.filter('.filter-false').length !== c.$headers.length) {
547
- // build filter row
548
- ts.filter.buildRow(table, c, wo);
549
- }
550
-
551
- c.$table.bind('addRows updateCell update updateRows updateComplete appendCache filterReset filterEnd search '.split(' ').join('.tsfilter '), function(event, filter) {
552
- if ( !/(search|filterReset|filterEnd)/.test(event.type) ) {
553
- event.stopPropagation();
554
- ts.filter.buildDefault(table, true);
555
- }
556
- if (event.type === 'filterReset') {
557
- ts.filter.searching(table, []);
558
- } else if (event.type === 'filterEnd') {
559
- ts.filter.buildDefault(table, true);
560
- } else {
561
- // send false argument to force a new search; otherwise if the filter hasn't changed, it will return
562
- filter = event.type === 'search' ? filter : event.type === 'updateComplete' ? c.$table.data('lastSearch') : '';
563
- ts.filter.searching(table, filter);
564
- }
565
- return false;
566
- });
567
- ts.filter.bindSearch( table, c.$table.find('input.tablesorter-filter') );
568
-
569
- // reset button/link
570
- if (wo.filter_reset) {
571
- $(document).delegate(wo.filter_reset, 'click.tsfilter', function() {
572
- // trigger a reset event, so other functions (filterFormatter) know when to reset
573
- c.$table.trigger('filterReset');
574
- });
575
- }
576
- if (wo.filter_functions) {
577
- // column = column # (string)
578
- for (column in wo.filter_functions) {
579
- if (wo.filter_functions.hasOwnProperty(column) && typeof column === 'string') {
580
- $header = c.$headers.filter('[data-column="' + column + '"]:last');
581
- options = '';
582
- if (wo.filter_functions[column] === true && !$header.hasClass('filter-false')) {
583
- ts.filter.buildSelect(table, column);
584
- } else if (typeof column === 'string' && !$header.hasClass('filter-false')) {
585
- // add custom drop down list
586
- for (string in wo.filter_functions[column]) {
587
- if (typeof string === 'string') {
588
- options += options === '' ?
589
- '<option value="">' + ($header.data('placeholder') || $header.attr('data-placeholder') || '') + '</option>' : '';
590
- options += '<option value="' + string + '">' + string + '</option>';
591
- }
592
- }
593
- c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]').append(options);
594
- }
595
- }
596
- }
597
- }
598
- // not really updating, but if the column has both the "filter-select" class & filter_functions set to true,
599
- // it would append the same options twice.
600
- ts.filter.buildDefault(table, true);
601
-
602
- c.$table.find('select.tablesorter-filter').bind('change search', function(event, filter) {
603
- ts.filter.checkFilters(table, filter);
604
- });
605
-
606
- // show processing icon
607
- if (c.showProcessing) {
608
- c.$table.bind('filterStart.tsfilter filterEnd.tsfilter', function(event, columns) {
609
- // only add processing to certain columns to all columns
610
- $header = (columns) ? c.$table.find('.' + ts.css.header).filter('[data-column]').filter(function() {
611
- return columns[$(this).data('column')] !== '';
612
- }) : '';
613
- ts.isProcessing(table, event.type === 'filterStart', columns ? $header : '');
614
- });
615
- }
616
-
617
- if (c.debug) {
618
- ts.benchmark("Applying Filter widget", time);
619
- }
620
- // add default values
621
- c.$table.bind('tablesorter-initialized pagerInitialized', function() {
622
- filters = ts.filter.setDefaults(table, c, wo) || [];
623
- if (filters.length) {
624
- ts.setFilters(table, filters, true);
625
- }
626
- ts.filter.checkFilters(table, filters);
627
- });
628
- // filter widget initialized
629
- wo.filter_Initialized = true;
630
- c.$table.trigger('filterInit');
631
- },
632
- setDefaults: function(table, c, wo) {
633
- var indx, isArray,
634
- filters = [],
635
- columns = c.columns;
636
- if (wo.filter_saveFilters && ts.storage) {
637
- filters = ts.storage( table, 'tablesorter-filters' ) || [];
638
- isArray = $.isArray(filters);
639
- // make sure we're not just saving an empty array
640
- if (isArray && filters.join('') === '' || !isArray ) { filters = []; }
641
- }
642
- // if not filters saved, then check default settings
643
- if (!filters.length) {
644
- for (indx = 0; indx < columns; indx++) {
645
- filters[indx] = c.$headers.filter('[data-column="' + indx + '"]:last').attr(wo.filter_defaultAttrib) || filters[indx];
646
- }
647
- }
648
- $(table).data('lastSearch', filters);
649
- return filters;
650
- },
651
- buildRow: function(table, c, wo) {
652
- var column, $header, buildSelect, disabled,
653
- // c.columns defined in computeThIndexes()
654
- columns = c.columns,
655
- buildFilter = '<tr class="tablesorter-filter-row">';
656
- for (column = 0; column < columns; column++) {
657
- buildFilter += '<td></td>';
658
- }
659
- c.$filters = $(buildFilter += '</tr>').appendTo( c.$table.find('thead').eq(0) ).find('td');
660
- // build each filter input
661
- for (column = 0; column < columns; column++) {
662
- disabled = false;
663
- // assuming last cell of a column is the main column
664
- $header = c.$headers.filter('[data-column="' + column + '"]:last');
665
- buildSelect = (wo.filter_functions && wo.filter_functions[column] && typeof wo.filter_functions[column] !== 'function') ||
666
- $header.hasClass('filter-select');
667
- // get data from jQuery data, metadata, headers option or header class name
668
- if (ts.getData) {
669
- // get data from jQuery data, metadata, headers option or header class name
670
- disabled = ts.getData($header[0], c.headers[column], 'filter') === 'false';
671
- } else {
672
- // only class names and header options - keep this for compatibility with tablesorter v2.0.5
673
- disabled = (c.headers[column] && c.headers[column].hasOwnProperty('filter') && c.headers[column].filter === false) ||
674
- $header.hasClass('filter-false');
675
- }
676
- if (buildSelect) {
677
- buildFilter = $('<select>').appendTo( c.$filters.eq(column) );
678
- } else {
679
- if (wo.filter_formatter && $.isFunction(wo.filter_formatter[column])) {
680
- buildFilter = wo.filter_formatter[column]( c.$filters.eq(column), column );
681
- // no element returned, so lets go find it
682
- if (buildFilter && buildFilter.length === 0) {
683
- buildFilter = c.$filters.eq(column).children('input');
684
- }
685
- // element not in DOM, so lets attach it
686
- if ( buildFilter && (buildFilter.parent().length === 0 ||
687
- (buildFilter.parent().length && buildFilter.parent()[0] !== c.$filters[column])) ) {
688
- c.$filters.eq(column).append(buildFilter);
689
- }
690
- } else {
691
- buildFilter = $('<input type="search">').appendTo( c.$filters.eq(column) );
692
- }
693
- if (buildFilter) {
694
- buildFilter.attr('placeholder', $header.data('placeholder') || $header.attr('data-placeholder') || '');
695
- }
696
- }
697
- if (buildFilter) {
698
- buildFilter.addClass('tablesorter-filter ' + wo.filter_cssFilter).attr('data-column', column);
699
- if (disabled) {
700
- buildFilter.addClass('disabled')[0].disabled = true; // disabled!
701
- }
702
- }
703
- }
704
- },
705
- bindSearch: function(table, $el) {
706
- table = $(table)[0];
707
- var external, wo = table.config.widgetOptions;
708
- $el.unbind('keyup search filterReset')
709
- .bind('keyup search', function(event, filter) {
710
- var $this = $(this);
711
- // emulate what webkit does.... escape clears the filter
712
- if (event.which === 27) {
713
- this.value = '';
714
- // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace
715
- } else if ( (typeof wo.filter_liveSearch === 'number' && this.value.length < wo.filter_liveSearch && this.value !== '') ||
716
- ( event.type === 'keyup' && ( (event.which < 32 && event.which !== 8 && wo.filter_liveSearch === true && event.which !== 13) ||
717
- ( event.which >= 37 && event.which <= 40 ) || (event.which !== 13 && wo.filter_liveSearch === false) ) ) ) {
718
- return;
719
- }
720
- // external searches won't have a filter parameter, so grab the value
721
- if ($this.hasClass('tablesorter-filter') && !$this.hasClass('tablesorter-external-filter')) {
722
- external = filter;
723
- } else {
724
- external = [];
725
- $el.each(function(){
726
- // target the appropriate column if the external input has a data-column attribute
727
- external[ $(this).data('column') || 0 ] = $(this).val();
728
- });
729
- }
730
- ts.filter.searching(table, filter, external);
731
- })
732
- .bind('filterReset', function(){
733
- $el.val('');
734
- });
735
- },
736
- checkFilters: function(table, filter) {
737
- var c = table.config,
738
- wo = c.widgetOptions,
739
- filterArray = $.isArray(filter),
740
- filters = (filterArray) ? filter : ts.getFilters(table),
741
- combinedFilters = (filters || []).join(''); // combined filter values
742
- // add filter array back into inputs
743
- if (filterArray) {
744
- ts.setFilters( table, filters );
745
- }
746
- // return if the last search is the same; but filter === false when updating the search
747
- // see example-widget-filter.html filter toggle buttons
748
- if (c.lastCombinedFilter === combinedFilters && filter !== false) {
749
- return;
750
- } else if (filter === false) {
751
- // force filter refresh
752
- c.lastCombinedFilter = null;
753
- }
754
- c.$table.trigger('filterStart', [filters]);
755
- if (c.showProcessing) {
756
- // give it time for the processing icon to kick in
757
- setTimeout(function() {
758
- ts.filter.findRows(table, filters, combinedFilters);
759
- return false;
760
- }, 30);
761
- } else {
762
- ts.filter.findRows(table, filters, combinedFilters);
763
- return false;
764
- }
765
- },
766
- findRows: function(table, filters, combinedFilters) {
767
- if (table.config.lastCombinedFilter === combinedFilters) { return; }
768
- var cached, len, $rows, rowIndex, tbodyIndex, $tbody, $cells, columnIndex,
769
- childRow, childRowText, exact, iExact, iFilter, lastSearch, matches, result,
770
- searchFiltered, filterMatched, showRow, time,
771
- c = table.config,
772
- wo = c.widgetOptions,
773
- columns = c.columns,
774
- $tbodies = c.$tbodies,
775
- // anyMatch really screws up with these types of filters
776
- anyMatchNotAllowedTypes = [ 'range', 'notMatch', 'operators' ],
777
- // parse columns after formatter, in case the class is added at that point
778
- parsed = c.$headers.map(function(columnIndex) {
779
- return (ts.getData) ?
780
- ts.getData(c.$headers.filter('[data-column="' + columnIndex + '"]:last'), c.headers[columnIndex], 'filter') === 'parsed' :
781
- $(this).hasClass('filter-parsed');
782
- }).get();
783
- if (c.debug) { time = new Date(); }
784
- for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
785
- if ($tbodies.eq(tbodyIndex).hasClass(ts.css.info)) { continue; } // ignore info blocks, issue #264
786
- $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true);
787
- // skip child rows & widget added (removable) rows - fixes #448 thanks to @hempel!
788
- $rows = $tbody.children('tr').not('.' + c.cssChildRow).not(c.selectorRemove);
789
- len = $rows.length;
790
- if (combinedFilters === '' || wo.filter_serversideFiltering) {
791
- $tbody.children().show().removeClass(wo.filter_filteredRow);
792
- } else {
793
- // optimize searching only through already filtered rows - see #313
794
- searchFiltered = true;
795
- lastSearch = c.lastSearch || c.$table.data('lastSearch') || [];
796
- $.each(filters, function(indx, val) {
797
- // check for changes from beginning of filter; but ignore if there is a logical "or" in the string
798
- searchFiltered = (val || '').indexOf(lastSearch[indx] || '') === 0 && searchFiltered && !/(\s+or\s+|\|)/g.test(val || '');
799
- });
800
- // can't search when all rows are hidden - this happens when looking for exact matches
801
- if (searchFiltered && $rows.filter(':visible').length === 0) { searchFiltered = false; }
802
- // loop through the rows
803
- for (rowIndex = 0; rowIndex < len; rowIndex++) {
804
- childRow = $rows[rowIndex].className;
805
- // skip child rows & already filtered rows
806
- if ( ts.filter.regex.child.test(childRow) || (searchFiltered && ts.filter.regex.filtered.test(childRow)) ) { continue; }
807
- showRow = true;
808
- // *** nextAll/nextUntil not supported by Zepto! ***
809
- childRow = $rows.eq(rowIndex).nextUntil('tr:not(.' + c.cssChildRow + ')');
810
- // so, if "table.config.widgetOptions.filter_childRows" is true and there is
811
- // a match anywhere in the child row, then it will make the row visible
812
- // checked here so the option can be changed dynamically
813
- childRowText = (childRow.length && wo.filter_childRows) ? childRow.text() : '';
814
- childRowText = wo.filter_ignoreCase ? childRowText.toLocaleLowerCase() : childRowText;
815
- $cells = $rows.eq(rowIndex).children('td');
816
- for (columnIndex = 0; columnIndex < columns; columnIndex++) {
817
- // ignore if filter is empty or disabled
818
- if (filters[columnIndex] || wo.filter_anyMatch) {
819
- cached = c.cache[tbodyIndex].normalized[rowIndex][columnIndex];
820
- // check if column data should be from the cell or from parsed data
821
- if (wo.filter_useParsedData || parsed[columnIndex]) {
822
- exact = cached;
823
- } else {
824
- // using older or original tablesorter
825
- exact = $.trim($cells.eq(columnIndex).text());
826
- exact = c.sortLocaleCompare ? ts.replaceAccents(exact) : exact; // issue #405
827
- }
828
- iExact = !ts.filter.regex.type.test(typeof exact) && wo.filter_ignoreCase ? exact.toLocaleLowerCase() : exact;
829
- result = showRow; // if showRow is true, show that row
830
-
831
- if (typeof filters[columnIndex] === "undefined" || filters[columnIndex] === null) {
832
- filters[columnIndex] = wo.filter_anyMatch ? combinedFilters : filters[columnIndex];
833
- }
834
-
835
- // replace accents - see #357
836
- filters[columnIndex] = c.sortLocaleCompare ? ts.replaceAccents(filters[columnIndex]) : filters[columnIndex];
837
- // val = case insensitive, filters[columnIndex] = case sensitive
838
- iFilter = wo.filter_ignoreCase ? filters[columnIndex].toLocaleLowerCase() : filters[columnIndex];
839
- if (wo.filter_functions && wo.filter_functions[columnIndex]) {
840
- if (wo.filter_functions[columnIndex] === true) {
841
- // default selector; no "filter-select" class
842
- result = (c.$headers.filter('[data-column="' + columnIndex + '"]:last').hasClass('filter-match')) ?
843
- iExact.search(iFilter) >= 0 : filters[columnIndex] === exact;
844
- } else if (typeof wo.filter_functions[columnIndex] === 'function') {
845
- // filter callback( exact cell content, parser normalized content, filter input value, column index, jQuery row object )
846
- result = wo.filter_functions[columnIndex](exact, cached, filters[columnIndex], columnIndex, $rows.eq(rowIndex));
847
- } else if (typeof wo.filter_functions[columnIndex][filters[columnIndex]] === 'function') {
848
- // selector option function
849
- result = wo.filter_functions[columnIndex][filters[columnIndex]](exact, cached, filters[columnIndex], columnIndex, $rows.eq(rowIndex));
850
- }
851
- } else {
852
- filterMatched = null;
853
- // cycle through the different filters
854
- // filters return a boolean or null if nothing matches
855
- $.each(ts.filter.types, function(type, typeFunction) {
856
- if (!wo.filter_anyMatch || (wo.filter_anyMatch && $.inArray(type, anyMatchNotAllowedTypes) < 0)) {
857
- matches = typeFunction( filters[columnIndex], iFilter, exact, iExact, cached, columnIndex, table, wo, parsed );
858
- if (matches !== null) {
859
- filterMatched = matches;
860
- return false;
861
- }
862
- }
863
- });
864
- if (filterMatched !== null) {
865
- result = filterMatched;
866
- // Look for match, and add child row data for matching
867
- } else {
868
- exact = (iExact + childRowText).indexOf(iFilter);
869
- result = ( (!wo.filter_startsWith && exact >= 0) || (wo.filter_startsWith && exact === 0) );
870
- }
871
- }
872
- if (wo.filter_anyMatch) {
873
- showRow = result;
874
- if (showRow){
875
- break;
876
- }
877
- } else {
878
- showRow = (result) ? showRow : false;
879
- }
880
- }
881
- }
882
- $rows[rowIndex].style.display = (showRow ? '' : 'none');
883
- $rows.eq(rowIndex)[showRow ? 'removeClass' : 'addClass'](wo.filter_filteredRow);
884
- if (childRow.length) {
885
- if (c.pager && c.pager.countChildRows || wo.pager_countChildRows || wo.filter_childRows) {
886
- childRow[showRow ? 'removeClass' : 'addClass'](wo.filter_filteredRow); // see issue #396
887
- }
888
- childRow.toggle(showRow);
889
- }
890
- }
891
- }
892
- ts.processTbody(table, $tbody, false);
893
- }
894
- c.lastCombinedFilter = combinedFilters; // save last search
895
- c.lastSearch = filters;
896
- c.$table.data('lastSearch', filters);
897
- if (wo.filter_saveFilters && ts.storage) {
898
- ts.storage( table, 'tablesorter-filters', filters );
899
- }
900
- if (c.debug) {
901
- ts.benchmark("Completed filter widget search", time);
902
- }
903
- c.$table.trigger('applyWidgets'); // make sure zebra widget is applied
904
- c.$table.trigger('filterEnd');
905
- },
906
- buildSelect: function(table, column, updating, onlyavail) {
907
- column = parseInt(column, 10);
908
- var indx, rowIndex, tbodyIndex, len, currentValue, txt,
909
- c = table.config,
910
- wo = c.widgetOptions,
911
- $tbodies = c.$tbodies,
912
- arry = [],
913
- node = c.$headers.filter('[data-column="' + column + '"]:last'),
914
- // t.data('placeholder') won't work in jQuery older than 1.4.3
915
- options = '<option value="">' + ( node.data('placeholder') || node.attr('data-placeholder') || '' ) + '</option>';
916
- for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) {
917
- len = c.cache[tbodyIndex].row.length;
918
- // loop through the rows
919
- for (rowIndex = 0; rowIndex < len; rowIndex++) {
920
- // check if has class filtered
921
- if (onlyavail && c.cache[tbodyIndex].row[rowIndex][0].className.match(wo.filter_filteredRow)) { continue; }
922
- // get non-normalized cell content
923
- if (wo.filter_useParsedData) {
924
- arry.push( '' + c.cache[tbodyIndex].normalized[rowIndex][column] );
925
- } else {
926
- node = c.cache[tbodyIndex].row[rowIndex][0].cells[column];
927
- if (node) {
928
- arry.push( $.trim( node.textContent || node.innerText || $(node).text() ) );
929
- }
930
- }
931
- }
932
- }
933
- // get unique elements and sort the list
934
- // if $.tablesorter.sortText exists (not in the original tablesorter),
935
- // then natural sort the list otherwise use a basic sort
936
- arry = $.grep(arry, function(value, indx) {
937
- return $.inArray(value, arry) === indx;
938
- });
939
- arry = (ts.sortNatural) ? arry.sort(function(a, b) { return ts.sortNatural(a, b); }) : arry.sort(true);
940
-
941
- // Get curent filter value
942
- currentValue = c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]').val();
943
-
944
- // build option list
945
- for (indx = 0; indx < arry.length; indx++) {
946
- txt = arry[indx].replace(/\"/g, "&quot;");
947
- // replace quotes - fixes #242 & ignore empty strings - see http://stackoverflow.com/q/14990971/145346
948
- options += arry[indx] !== '' ? '<option value="' + txt + '"' + (currentValue === txt ? ' selected="selected"' : '') +
949
- '>' + arry[indx] + '</option>' : '';
950
- }
951
- c.$table.find('thead').find('select.tablesorter-filter[data-column="' + column + '"]')[ updating ? 'html' : 'append' ](options);
952
- },
953
- buildDefault: function(table, updating) {
954
- var columnIndex, $header,
955
- c = table.config,
956
- wo = c.widgetOptions,
957
- columns = c.columns;
958
- // build default select dropdown
959
- for (columnIndex = 0; columnIndex < columns; columnIndex++) {
960
- $header = c.$headers.filter('[data-column="' + columnIndex + '"]:last');
961
- // look for the filter-select class; build/update it if found
962
- if (($header.hasClass('filter-select') || wo.filter_functions && wo.filter_functions[columnIndex] === true) &&
963
- !$header.hasClass('filter-false')) {
964
- if (!wo.filter_functions) { wo.filter_functions = {}; }
965
- wo.filter_functions[columnIndex] = true; // make sure this select gets processed by filter_functions
966
- ts.filter.buildSelect(table, columnIndex, updating, $header.hasClass(wo.filter_onlyAvail));
967
- }
968
- }
969
- },
970
- searching: function(table, filter, external) {
971
- if (typeof filter === 'undefined' || filter === true || external) {
972
- var wo = table.config.widgetOptions;
973
- // delay filtering
974
- clearTimeout(wo.searchTimer);
975
- wo.searchTimer = setTimeout(function() {
976
- ts.filter.checkFilters(table, external || filter);
977
- }, wo.filter_liveSearch ? wo.filter_searchDelay : 10);
978
- } else {
979
- // skip delay
980
- ts.filter.checkFilters(table, filter);
981
- }
982
- }
983
- };
984
-
985
- ts.getFilters = function(table) {
986
- var c = table ? $(table)[0].config : {};
987
- if (c && c.widgetOptions && !c.widgetOptions.filter_columnFilters) {
988
- // no filter row
989
- return $(table).data('lastSearch');
990
- }
991
- return c && c.$filters ? c.$filters.map(function(indx, el) {
992
- return $(el).find('.tablesorter-filter').val() || '';
993
- }).get() || [] : false;
994
- };
995
-
996
- ts.setFilters = function(table, filter, apply) {
997
- var $table = $(table),
998
- c = $table.length ? $table[0].config : {},
999
- valid = c && c.$filters ? c.$filters.each(function(indx, el) {
1000
- $(el).find('.tablesorter-filter').val(filter[indx] || '');
1001
- }).trigger('change.tsfilter') || false : false;
1002
- if (apply) { $table.trigger('search', [filter, false]); }
1003
- return !!valid;
1004
- };
1005
-
1006
- // Widget: Sticky headers
1007
- // based on this awesome article:
1008
- // http://css-tricks.com/13465-persistent-headers/
1009
- // and https://github.com/jmosbech/StickyTableHeaders by Jonas Mosbech
1010
- // **************************
1011
- ts.addWidget({
1012
- id: "stickyHeaders",
1013
- priority: 60, // sticky widget must be initialized after the filter widget!
1014
- options: {
1015
- stickyHeaders : '', // extra class name added to the sticky header row
1016
- stickyHeaders_attachTo : null, // jQuery selector or object to attach sticky header to
1017
- stickyHeaders_offset : 0, // number or jquery selector targeting the position:fixed element
1018
- stickyHeaders_cloneId : '-sticky', // added to table ID, if it exists
1019
- stickyHeaders_addResizeEvent : true, // trigger "resize" event on headers
1020
- stickyHeaders_includeCaption : true, // if false and a caption exist, it won't be included in the sticky header
1021
- stickyHeaders_zIndex : 2 // The zIndex of the stickyHeaders, allows the user to adjust this to their needs
1022
- },
1023
- format: function(table, c, wo) {
1024
- // filter widget doesn't initialize on an empty table. Fixes #449
1025
- if ( c.$table.hasClass('hasStickyHeaders') || ($.inArray('filter', c.widgets) >= 0 && !c.$table.hasClass('hasFilters')) ) {
1026
- return;
1027
- }
1028
- var $cell,
1029
- $table = c.$table,
1030
- $attach = $(wo.stickyHeaders_attachTo),
1031
- $thead = $table.children('thead:first'),
1032
- $win = $attach.length ? $attach : $(window),
1033
- $header = $thead.children('tr').not('.sticky-false').children(),
1034
- innerHeader = '.tablesorter-header-inner',
1035
- $tfoot = $table.find('tfoot'),
1036
- filterInputs = '.tablesorter-filter',
1037
- $stickyOffset = isNaN(wo.stickyHeaders_offset) ? $(wo.stickyHeaders_offset) : '',
1038
- stickyOffset = $attach.length ? 0 : $stickyOffset.length ?
1039
- $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0,
1040
- $stickyTable = wo.$sticky = $table.clone()
1041
- .addClass('containsStickyHeaders')
1042
- .css({
1043
- position : $attach.length ? 'absolute' : 'fixed',
1044
- margin : 0,
1045
- top : stickyOffset,
1046
- left : 0,
1047
- visibility : 'hidden',
1048
- zIndex : wo.stickyHeaders_zIndex ? wo.stickyHeaders_zIndex : 2
1049
- }),
1050
- $stickyThead = $stickyTable.children('thead:first').addClass('tablesorter-stickyHeader ' + wo.stickyHeaders),
1051
- $stickyCells,
1052
- laststate = '',
1053
- spacing = 0,
1054
- nonwkie = $table.css('border-collapse') !== 'collapse' && !/(webkit|msie)/i.test(navigator.userAgent),
1055
- resizeHeader = function() {
1056
- stickyOffset = $stickyOffset.length ? $stickyOffset.height() || 0 : parseInt(wo.stickyHeaders_offset, 10) || 0;
1057
- spacing = 0;
1058
- // yes, I dislike browser sniffing, but it really is needed here :(
1059
- // webkit automatically compensates for border spacing
1060
- if (nonwkie) {
1061
- // Firefox & Opera use the border-spacing
1062
- // update border-spacing here because of demos that switch themes
1063
- spacing = parseInt($header.eq(0).css('border-left-width'), 10) * 2;
1064
- }
1065
- $stickyTable.css({
1066
- left : $attach.length ? parseInt($attach.css('padding-left'), 10) +
1067
- parseInt($attach.css('margin-left'), 10) + parseInt($table.css('border-left-width'), 10) :
1068
- $thead.offset().left - $win.scrollLeft() - spacing,
1069
- width: $table.width()
1070
- });
1071
- $stickyCells.filter(':visible').each(function(i) {
1072
- var $cell = $header.filter(':visible').eq(i),
1073
- // some wibbly-wobbly... timey-wimey... stuff, to make columns line up in Firefox
1074
- offset = nonwkie && $(this).attr('data-column') === ( '' + parseInt(c.columns/2, 10) ) ? 1 : 0;
1075
- $(this)
1076
- .css({
1077
- width: $cell.width() - spacing,
1078
- height: $cell.height()
1079
- })
1080
- .find(innerHeader).width( $cell.find(innerHeader).width() - offset );
1081
- });
1082
- };
1083
- // fix clone ID, if it exists - fixes #271
1084
- if ($stickyTable.attr('id')) { $stickyTable[0].id += wo.stickyHeaders_cloneId; }
1085
- // clear out cloned table, except for sticky header
1086
- // include caption & filter row (fixes #126 & #249)
1087
- $stickyTable.find('thead:gt(0), tr.sticky-false, tbody, tfoot').remove();
1088
- if (!wo.stickyHeaders_includeCaption) {
1089
- $stickyTable.find('caption').remove();
1090
- } else {
1091
- $stickyTable.find('caption').css( 'margin-left', '-1px' );
1092
- }
1093
- // issue #172 - find td/th in sticky header
1094
- $stickyCells = $stickyThead.children().children();
1095
- $stickyTable.css({ height:0, width:0, padding:0, margin:0, border:0 });
1096
- // remove resizable block
1097
- $stickyCells.find('.tablesorter-resizer').remove();
1098
- // update sticky header class names to match real header after sorting
1099
- $table
1100
- .addClass('hasStickyHeaders')
1101
- .bind('sortEnd.tsSticky', function() {
1102
- $header.filter(':visible').each(function(indx) {
1103
- $cell = $stickyCells.filter(':visible').eq(indx)
1104
- .attr('class', $(this).attr('class'))
1105
- // remove processing icon
1106
- .removeClass(ts.css.processing + ' ' + c.cssProcessing);
1107
- if (c.cssIcon) {
1108
- $cell
1109
- .find('.' + ts.css.icon)
1110
- .attr('class', $(this).find('.' + ts.css.icon).attr('class'));
1111
- }
1112
- });
1113
- })
1114
- .bind('pagerComplete.tsSticky', function() {
1115
- resizeHeader();
1116
- });
1117
- // http://stackoverflow.com/questions/5312849/jquery-find-self;
1118
- $header.find(c.selectorSort).add( c.$headers.filter(c.selectorSort) ).each(function(indx) {
1119
- var $header = $(this),
1120
- // clicking on sticky will trigger sort
1121
- $cell = $stickyThead.children('tr.tablesorter-headerRow').children().eq(indx).bind('mouseup', function(event) {
1122
- $header.trigger(event, true); // external mouseup flag (click timer is ignored)
1123
- });
1124
- // prevent sticky header text selection
1125
- if (c.cancelSelection) {
1126
- $cell
1127
- .attr('unselectable', 'on')
1128
- .bind('selectstart', false)
1129
- .css({
1130
- 'user-select': 'none',
1131
- 'MozUserSelect': 'none'
1132
- });
1133
- }
1134
- });
1135
- // add stickyheaders AFTER the table. If the table is selected by ID, the original one (first) will be returned.
1136
- $table.after( $stickyTable );
1137
-
1138
- // make it sticky!
1139
- $win.bind('scroll.tsSticky resize.tsSticky', function(event) {
1140
- if (!$table.is(':visible')) { return; } // fixes #278
1141
- var prefix = 'tablesorter-sticky-',
1142
- offset = $table.offset(),
1143
- captionHeight = (wo.stickyHeaders_includeCaption ? 0 : $table.find('caption').outerHeight(true)),
1144
- scrollTop = ($attach.length ? $attach.offset().top : $win.scrollTop()) + stickyOffset - captionHeight,
1145
- tableHeight = $table.height() - ($stickyTable.height() + ($tfoot.height() || 0)),
1146
- isVisible = (scrollTop > offset.top) && (scrollTop < offset.top + tableHeight) ? 'visible' : 'hidden',
1147
- cssSettings = { visibility : isVisible };
1148
- if ($attach.length) {
1149
- cssSettings.top = $attach.scrollTop();
1150
- } else {
1151
- // adjust when scrolling horizontally - fixes issue #143
1152
- cssSettings.left = $thead.offset().left - $win.scrollLeft() - spacing;
1153
- }
1154
- $stickyTable
1155
- .removeClass(prefix + 'visible ' + prefix + 'hidden')
1156
- .addClass(prefix + isVisible)
1157
- .css(cssSettings);
1158
- if (isVisible !== laststate || event.type === 'resize') {
1159
- // make sure the column widths match
1160
- resizeHeader();
1161
- laststate = isVisible;
1162
- }
1163
- });
1164
- if (wo.stickyHeaders_addResizeEvent) {
1165
- ts.addHeaderResizeEvent(table);
1166
- }
1167
-
1168
- // look for filter widget
1169
- if ($table.hasClass('hasFilters')) {
1170
- $table.bind('filterEnd', function() {
1171
- // $(':focus') needs jQuery 1.6+
1172
- if ( $(document.activeElement).closest('thead')[0] !== $stickyThead[0] ) {
1173
- // don't update the stickyheader filter row if it already has focus
1174
- $stickyThead.find('.tablesorter-filter-row').children().each(function(indx) {
1175
- $(this).find(filterInputs).val( c.$filters.find(filterInputs).eq(indx).val() );
1176
- });
1177
- }
1178
- });
1179
-
1180
- ts.filter.bindSearch( $table, $stickyCells.find('.tablesorter-filter').addClass('tablesorter-external-filter') );
1181
- }
1182
-
1183
- $table.trigger('stickyHeadersInit');
1184
-
1185
- },
1186
- remove: function(table, c, wo) {
1187
- c.$table
1188
- .removeClass('hasStickyHeaders')
1189
- .unbind('sortEnd.tsSticky pagerComplete.tsSticky')
1190
- .find('.tablesorter-stickyHeader').remove();
1191
- if (wo.$sticky && wo.$sticky.length) { wo.$sticky.remove(); } // remove cloned table
1192
- // don't unbind if any table on the page still has stickyheaders applied
1193
- if (!$('.hasStickyHeaders').length) {
1194
- $(window).unbind('scroll.tsSticky resize.tsSticky');
1195
- }
1196
- ts.addHeaderResizeEvent(table, false);
1197
- }
1198
- });
1199
-
1200
- // Add Column resizing widget
1201
- // this widget saves the column widths if
1202
- // $.tablesorter.storage function is included
1203
- // **************************
1204
- ts.addWidget({
1205
- id: "resizable",
1206
- priority: 40,
1207
- options: {
1208
- resizable : true,
1209
- resizable_addLastColumn : false
1210
- },
1211
- format: function(table, c, wo) {
1212
- if (c.$table.hasClass('hasResizable')) { return; }
1213
- c.$table.addClass('hasResizable');
1214
- var $rows, $columns, $column, column,
1215
- storedSizes = {},
1216
- $table = c.$table,
1217
- mouseXPosition = 0,
1218
- $target = null,
1219
- $next = null,
1220
- fullWidth = Math.abs($table.parent().width() - $table.width()) < 20,
1221
- stopResize = function() {
1222
- if (ts.storage && $target) {
1223
- storedSizes[$target.index()] = $target.width();
1224
- storedSizes[$next.index()] = $next.width();
1225
- $target.width( storedSizes[$target.index()] );
1226
- $next.width( storedSizes[$next.index()] );
1227
- if (wo.resizable !== false) {
1228
- ts.storage(table, 'tablesorter-resizable', storedSizes);
1229
- }
1230
- }
1231
- mouseXPosition = 0;
1232
- $target = $next = null;
1233
- $(window).trigger('resize'); // will update stickyHeaders, just in case
1234
- };
1235
- storedSizes = (ts.storage && wo.resizable !== false) ? ts.storage(table, 'tablesorter-resizable') : {};
1236
- // process only if table ID or url match
1237
- if (storedSizes) {
1238
- for (column in storedSizes) {
1239
- if (!isNaN(column) && column < c.$headers.length) {
1240
- c.$headers.eq(column).width(storedSizes[column]); // set saved resizable widths
1241
- }
1242
- }
1243
- }
1244
- $rows = $table.children('thead:first').children('tr');
1245
- // add resizable-false class name to headers (across rows as needed)
1246
- $rows.children().each(function() {
1247
- var canResize,
1248
- $column = $(this);
1249
- column = $column.attr('data-column');
1250
- canResize = ts.getData( $column, c.headers[column], 'resizable') === "false";
1251
- $rows.children().filter('[data-column="' + column + '"]')[canResize ? 'addClass' : 'removeClass']('resizable-false');
1252
- });
1253
- // add wrapper inside each cell to allow for positioning of the resizable target block
1254
- $rows.each(function() {
1255
- $column = $(this).children().not('.resizable-false');
1256
- if (!$(this).find('.tablesorter-wrapper').length) {
1257
- // Firefox needs this inner div to position the resizer correctly
1258
- $column.wrapInner('<div class="tablesorter-wrapper" style="position:relative;height:100%;width:100%"></div>');
1259
- }
1260
- // don't include the last column of the row
1261
- if (!wo.resizable_addLastColumn) { $column = $column.slice(0,-1); }
1262
- $columns = $columns ? $columns.add($column) : $column;
1263
- });
1264
- $columns
1265
- .each(function() {
1266
- var $column = $(this),
1267
- padding = parseInt($column.css('padding-right'), 10) + 10; // 10 is 1/2 of the 20px wide resizer grip
1268
- $column
1269
- .find('.tablesorter-wrapper')
1270
- .append('<div class="tablesorter-resizer" style="cursor:w-resize;position:absolute;z-index:1;right:-' +
1271
- padding + 'px;top:0;height:100%;width:20px;"></div>');
1272
- })
1273
- .bind('mousemove.tsresize', function(event) {
1274
- // ignore mousemove if no mousedown
1275
- if (mouseXPosition === 0 || !$target) { return; }
1276
- // resize columns
1277
- var leftEdge = event.pageX - mouseXPosition,
1278
- targetWidth = $target.width();
1279
- $target.width( targetWidth + leftEdge );
1280
- if ($target.width() !== targetWidth && fullWidth) {
1281
- $next.width( $next.width() - leftEdge );
1282
- }
1283
- mouseXPosition = event.pageX;
1284
- })
1285
- .bind('mouseup.tsresize', function() {
1286
- stopResize();
1287
- })
1288
- .find('.tablesorter-resizer,.tablesorter-resizer-grip')
1289
- .bind('mousedown', function(event) {
1290
- // save header cell and mouse position; closest() not supported by jQuery v1.2.6
1291
- $target = $(event.target).closest('th');
1292
- var $header = c.$headers.filter('[data-column="' + $target.attr('data-column') + '"]');
1293
- if ($header.length > 1) { $target = $target.add($header); }
1294
- // if table is not as wide as it's parent, then resize the table
1295
- $next = event.shiftKey ? $target.parent().find('th').not('.resizable-false').filter(':last') : $target.nextAll(':not(.resizable-false)').eq(0);
1296
- mouseXPosition = event.pageX;
1297
- });
1298
- $table.find('thead:first')
1299
- .bind('mouseup.tsresize mouseleave.tsresize', function() {
1300
- stopResize();
1301
- })
1302
- // right click to reset columns to default widths
1303
- .bind('contextmenu.tsresize', function() {
1304
- ts.resizableReset(table);
1305
- // $.isEmptyObject() needs jQuery 1.4+; allow right click if already reset
1306
- var allowClick = $.isEmptyObject ? $.isEmptyObject(storedSizes) : true;
1307
- storedSizes = {};
1308
- return allowClick;
1309
- });
1310
- },
1311
- remove: function(table, c) {
1312
- c.$table
1313
- .removeClass('hasResizable')
1314
- .children('thead')
1315
- .unbind('mouseup.tsresize mouseleave.tsresize contextmenu.tsresize')
1316
- .children('tr').children()
1317
- .unbind('mousemove.tsresize mouseup.tsresize')
1318
- // don't remove "tablesorter-wrapper" as uitheme uses it too
1319
- .find('.tablesorter-resizer,.tablesorter-resizer-grip').remove();
1320
- ts.resizableReset(table);
1321
- }
1322
- });
1323
- ts.resizableReset = function(table) {
1324
- table.config.$headers.not('.resizable-false').css('width','');
1325
- if (ts.storage) { ts.storage(table, 'tablesorter-resizable', {}); }
1326
- };
1327
-
1328
- // Save table sort widget
1329
- // this widget saves the last sort only if the
1330
- // saveSort widget option is true AND the
1331
- // $.tablesorter.storage function is included
1332
- // **************************
1333
- ts.addWidget({
1334
- id: 'saveSort',
1335
- priority: 20,
1336
- options: {
1337
- saveSort : true
1338
- },
1339
- init: function(table, thisWidget, c, wo) {
1340
- // run widget format before all other widgets are applied to the table
1341
- thisWidget.format(table, c, wo, true);
1342
- },
1343
- format: function(table, c, wo, init) {
1344
- var stored, time,
1345
- $table = c.$table,
1346
- saveSort = wo.saveSort !== false, // make saveSort active/inactive; default to true
1347
- sortList = { "sortList" : c.sortList };
1348
- if (c.debug) {
1349
- time = new Date();
1350
- }
1351
- if ($table.hasClass('hasSaveSort')) {
1352
- if (saveSort && table.hasInitialized && ts.storage) {
1353
- ts.storage( table, 'tablesorter-savesort', sortList );
1354
- if (c.debug) {
1355
- ts.benchmark('saveSort widget: Saving last sort: ' + c.sortList, time);
1356
- }
1357
- }
1358
- } else {
1359
- // set table sort on initial run of the widget
1360
- $table.addClass('hasSaveSort');
1361
- sortList = '';
1362
- // get data
1363
- if (ts.storage) {
1364
- stored = ts.storage( table, 'tablesorter-savesort' );
1365
- sortList = (stored && stored.hasOwnProperty('sortList') && $.isArray(stored.sortList)) ? stored.sortList : '';
1366
- if (c.debug) {
1367
- ts.benchmark('saveSort: Last sort loaded: "' + sortList + '"', time);
1368
- }
1369
- $table.bind('saveSortReset', function(event) {
1370
- event.stopPropagation();
1371
- ts.storage( table, 'tablesorter-savesort', '' );
1372
- });
1373
- }
1374
- // init is true when widget init is run, this will run this widget before all other widgets have initialized
1375
- // this method allows using this widget in the original tablesorter plugin; but then it will run all widgets twice.
1376
- if (init && sortList && sortList.length > 0) {
1377
- c.sortList = sortList;
1378
- } else if (table.hasInitialized && sortList && sortList.length > 0) {
1379
- // update sort change
1380
- $table.trigger('sorton', [sortList]);
1381
- }
1382
- }
1383
- },
1384
- remove: function(table) {
1385
- // clear storage
1386
- if (ts.storage) { ts.storage( table, 'tablesorter-savesort', '' ); }
1387
- }
1388
- });
1389
-
1390
- })(jQuery);