pwpush 0.1.0

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 (139) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/Capfile +8 -0
  4. data/Gemfile +51 -0
  5. data/Gemfile.lock +224 -0
  6. data/LICENSE.txt +674 -0
  7. data/Procfile +3 -0
  8. data/README.md +91 -0
  9. data/Rakefile +8 -0
  10. data/TODO +21 -0
  11. data/app.json +21 -0
  12. data/app/assets/flash/clippy.swf +0 -0
  13. data/app/assets/flash/github-clippy.swf +0 -0
  14. data/app/assets/images/apple-touch-icon-ipad.png +0 -0
  15. data/app/assets/images/apple-touch-icon-ipad3.png +0 -0
  16. data/app/assets/images/apple-touch-icon-iphone.png +0 -0
  17. data/app/assets/images/apple-touch-icon-iphone4.png +0 -0
  18. data/app/assets/images/black_wood.jpg +0 -0
  19. data/app/assets/images/broken_noise.png +0 -0
  20. data/app/assets/images/button_down.png +0 -0
  21. data/app/assets/images/button_over.png +0 -0
  22. data/app/assets/images/button_up.png +0 -0
  23. data/app/assets/images/concrete_wall_3.png +0 -0
  24. data/app/assets/images/favicon.ico +0 -0
  25. data/app/assets/images/forkme.png +0 -0
  26. data/app/assets/images/outlets.png +0 -0
  27. data/app/assets/images/pwpush_favicon.jpg +0 -0
  28. data/app/assets/images/pwpush_logo.png +0 -0
  29. data/app/assets/images/rails.png +0 -0
  30. data/app/assets/javascripts/api.js.coffee +4 -0
  31. data/app/assets/javascripts/application.js +52 -0
  32. data/app/assets/javascripts/errors.js.coffee +3 -0
  33. data/app/assets/javascripts/fd-slider.js +1299 -0
  34. data/app/assets/javascripts/jquery-cookie.js +117 -0
  35. data/app/assets/javascripts/jquery.noty.js +520 -0
  36. data/app/assets/javascripts/layouts/top.js +34 -0
  37. data/app/assets/javascripts/passwords.js +62 -0
  38. data/app/assets/javascripts/spoiler.js +101 -0
  39. data/app/assets/javascripts/themes/default.js +156 -0
  40. data/app/assets/stylesheets/api.css.scss +3 -0
  41. data/app/assets/stylesheets/application.css +7 -0
  42. data/app/assets/stylesheets/errors.css.scss +3 -0
  43. data/app/assets/stylesheets/fd-slider.css +650 -0
  44. data/app/assets/stylesheets/global.css.scss +52 -0
  45. data/app/assets/stylesheets/passwords.css.scss +114 -0
  46. data/app/assets/stylesheets/users.css.scss +11 -0
  47. data/app/controllers/api_controller.rb +30 -0
  48. data/app/controllers/application_controller.rb +23 -0
  49. data/app/controllers/errors_controller.rb +7 -0
  50. data/app/controllers/passwords_controller.rb +153 -0
  51. data/app/controllers/users/omniauth_callbacks_controller.rb +71 -0
  52. data/app/controllers/views_controller.rb +11 -0
  53. data/app/helpers/api_helper.rb +2 -0
  54. data/app/helpers/application_helper.rb +31 -0
  55. data/app/helpers/errors_helper.rb +2 -0
  56. data/app/helpers/passwords_helper.rb +2 -0
  57. data/app/helpers/views_helper.rb +2 -0
  58. data/app/mailers/.gitkeep +0 -0
  59. data/app/models/.gitkeep +0 -0
  60. data/app/models/password.rb +51 -0
  61. data/app/models/user.rb +20 -0
  62. data/app/models/view.rb +4 -0
  63. data/app/views/api/config.html.haml +2 -0
  64. data/app/views/api/create.html.haml +2 -0
  65. data/app/views/api/generate.html.haml +2 -0
  66. data/app/views/api/list.html.haml +2 -0
  67. data/app/views/devise/confirmations/new.html.erb +12 -0
  68. data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
  69. data/app/views/devise/mailer/reset_password_instructions.html.erb +8 -0
  70. data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
  71. data/app/views/devise/passwords/edit.html.erb +16 -0
  72. data/app/views/devise/passwords/new.html.erb +12 -0
  73. data/app/views/devise/registrations/edit.html.erb +25 -0
  74. data/app/views/devise/registrations/new.html.haml +50 -0
  75. data/app/views/devise/sessions/new.html.haml +51 -0
  76. data/app/views/devise/shared/_links.erb +25 -0
  77. data/app/views/devise/unlocks/new.html.erb +12 -0
  78. data/app/views/errors/error_404.html.haml +21 -0
  79. data/app/views/errors/error_500.html.haml +21 -0
  80. data/app/views/layouts/_ga.html.erb +14 -0
  81. data/app/views/layouts/application.html.haml +41 -0
  82. data/app/views/pages/about.html.haml +159 -0
  83. data/app/views/passwords/edit.html.haml +7 -0
  84. data/app/views/passwords/index.html.haml +17 -0
  85. data/app/views/passwords/new.html.haml +68 -0
  86. data/app/views/passwords/show.html.haml +58 -0
  87. data/app/views/shared/_auth_providers.html.haml +9 -0
  88. data/app/views/shared/_messages.html.haml +4 -0
  89. data/app/views/views/_form.html.erb +16 -0
  90. data/app/views/views/edit.html.erb +8 -0
  91. data/app/views/views/index.html.erb +21 -0
  92. data/app/views/views/new.html.erb +5 -0
  93. data/app/views/views/show.html.erb +20 -0
  94. data/bin/bundle +13 -0
  95. data/config.ru +4 -0
  96. data/config/application.rb +51 -0
  97. data/config/boot.rb +6 -0
  98. data/config/capistrano_database_yml.rb +158 -0
  99. data/config/database.yml +19 -0
  100. data/config/deploy.rb +140 -0
  101. data/config/deploy/database.yml.erb +52 -0
  102. data/config/deploy/local_cap_config.rb.example +54 -0
  103. data/config/environment.rb +42 -0
  104. data/config/environments/development.rb +30 -0
  105. data/config/environments/engineyard.rb +60 -0
  106. data/config/environments/private.rb +60 -0
  107. data/config/environments/production.rb +60 -0
  108. data/config/environments/test.rb +39 -0
  109. data/config/initializers/backtrace_silencers.rb +7 -0
  110. data/config/initializers/devise.rb +211 -0
  111. data/config/initializers/inflections.rb +10 -0
  112. data/config/initializers/mime_types.rb +5 -0
  113. data/config/initializers/secret_token.rb +7 -0
  114. data/config/initializers/session_store.rb +8 -0
  115. data/config/initializers/wrap_parameters.rb +14 -0
  116. data/config/locales/devise.en.yml +58 -0
  117. data/config/locales/en.yml +5 -0
  118. data/config/routes.rb +16 -0
  119. data/config/unicorn.rb +22 -0
  120. data/db/migrate/20111128183630_create_passwords.rb +12 -0
  121. data/db/migrate/20111228183300_create_views.rb +16 -0
  122. data/db/migrate/20120102210558_devise_create_users.rb +54 -0
  123. data/db/migrate/20120102210559_create_rails_admin_histories_table.rb +18 -0
  124. data/db/migrate/20120102220933_add_admin_to_user.rb +9 -0
  125. data/db/migrate/20120129211750_add_lockable_to_users.rb +10 -0
  126. data/db/migrate/20120220172426_add_user_to_password.rb +11 -0
  127. data/db/migrate/20121105144421_add_deleted_to_password.rb +5 -0
  128. data/db/migrate/20150323145847_add_first_view_flag.rb +9 -0
  129. data/db/migrate/20160214205926_add_deletable_to_password.rb +5 -0
  130. data/db/schema.rb +78 -0
  131. data/db/seeds.rb +7 -0
  132. data/log/.gitkeep +0 -0
  133. data/public/404.html +26 -0
  134. data/public/422.html +26 -0
  135. data/public/500.html +26 -0
  136. data/public/favicon.ico +0 -0
  137. data/public/robots.txt +3 -0
  138. data/script/rails +6 -0
  139. metadata +226 -0
@@ -0,0 +1,3 @@
1
+ web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb
2
+ internalweb: RAILS_ENV=private bundle exec unicorn -p 5000 -c ./config/unicorn.rb
3
+ console: bundle exec rails console
@@ -0,0 +1,91 @@
1
+ ![Password Pusher Front Page](https://s3-eu-west-1.amazonaws.com/pwpush/pwpush_logo_2014.png)
2
+
3
+ PasswordPusher is an opensource Ruby on Rails application to communicate passwords over the web. Links to passwords expire after a certain number of views and/or time has passed. Hosted at [pwpush.com](https://pwpush.com) but you can also easily run your own instance internally or on Heroku with just a few steps.
4
+
5
+ I previously posted this project on [Reddit](http://www.reddit.com/r/sysadmin/comments/pfda0/do_you_email_out_passwords_i_wrote_this_utility/) which provided some great feedback - most of which has been implemented.
6
+
7
+ ## Deploy to Heroku
8
+
9
+ [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy)
10
+
11
+ ## Deploy Internally
12
+
13
+ Make sure you have git and Ruby installed and then:
14
+
15
+ ```sh
16
+ git clone git@github.com:pglombardo/PasswordPusher.git
17
+ cd PasswordPusher
18
+ gem install bundler
19
+ bundle install --without development production test --deployment
20
+ bundle exec rake assets:precompile
21
+ RAILS_ENV=private bundle exec rake db:setup
22
+ foreman start internalweb
23
+ ```
24
+
25
+ Then view the site @ [http://localhost:5000/](http://localhost:5000/).
26
+
27
+ _Note: You can change the listening port by modifying the
28
+ [Procfile](https://github.com/pglombardo/PasswordPusher/blob/master/Procfile#L2)_
29
+
30
+ ### Troubleshooting
31
+
32
+ #### Command not found: bundle
33
+
34
+ If you get something like `Command not found: bundle`, then you need to run
35
+
36
+ gem install bundler
37
+
38
+ _If you get something like 'Command not found: gem', then you need to install Ruby. :)_
39
+
40
+ #### SQLite3
41
+
42
+ If the 'bundle install' fails with 'checking for sqlite3.h... no', you have to install the sqlite3 packages for your operating system. For Ubuntu, the command is:
43
+
44
+ ```sh
45
+ sudo apt-get install sqlite3 ruby-sqlite3 libsqlite3-ruby libsqlite3-dev
46
+ ```
47
+
48
+ ## Other Information
49
+
50
+ * How to use the [Password API](https://github.com/pglombardo/PasswordPusher/wiki/Password-API)
51
+ * How to [Change the Front Page Default Values](https://github.com/pglombardo/PasswordPusher/wiki/Changing-the-Front-Page-Default-Values)
52
+ * How to [Switch to Production Environment](https://github.com/pglombardo/PasswordPusher/wiki/Switch-to-Production-Environment)
53
+ * How to [Switch to Another Backend Database](https://github.com/pglombardo/PasswordPusher/wiki/Switch-to-Another-Backend-Database)
54
+
55
+ ### Tip
56
+
57
+ With the internal deploy process described above, SQLite3 is provided by default for a quick and easy setup of the application.
58
+
59
+ If you plan to use PasswordPusher internally at your organization and expect to have multiple users concurrently creating passwords, it's advised to move away from SQLite3 as it doesn't support write concurrency and errors will occur.
60
+
61
+ For example, on [https://pwpush.com](https://pwpush.com), I run the application with a Postgres database.
62
+
63
+ *Initiated from [this discussion](http://www.reddit.com/r/sysadmin/comments/yxps8/passwordpusher_best_way_to_deliver_passwords_to/c5zwts9) on reddit.*
64
+
65
+ See How to [Switch to Another Backend Database](https://github.com/pglombardo/PasswordPusher/wiki/Switch-to-Another-Backend-Database) for details.
66
+
67
+ ## Note for Existing Users
68
+
69
+ If you're already hosting your own private instance of PasswordPusher, make sure to do a periodic `git pull` from time to time to always get the latest updates.
70
+
71
+ You can always checkout out the [latest commits](https://github.com/pglombardo/PasswordPusher/commits/master) to see what's been updated recently.
72
+
73
+ ## Credits
74
+
75
+ Thanks to:
76
+
77
+ * [@iandunn](https://github.com/iandunn) for better password form security.
78
+
79
+ * [Kasper 'kapöw' Grubbe](https://github.com/kaspergrubbe) for the [JSON POST fix](https://github.com/pglombardo/PasswordPusher/pull/3).
80
+
81
+ * [JarvisAndPi](http://www.reddit.com/user/JarvisAndPi) for the favicon design
82
+
83
+ ## See Also
84
+
85
+ [quasarj](https://github.com/quasarj) created a [django application](https://github.com/quasarj/projectgiraffe) based off of PasswordPusher
86
+
87
+ [phanaster](https://github.com/phanaster) created a [Coupon Pushing application](https://github.com/phanaster/cpsh.me) ([cpsh.me](http://cpsh.me/)) based off of PasswordPusher
88
+
89
+ [bemosior](https://github.com/bemosior) put together a PHP port of PasswordPusher: [PHPasswordPusher](https://github.com/bemosior/PHPasswordPusher)
90
+
91
+
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env rake
2
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
3
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
4
+
5
+ require File.expand_path('../config/application', __FILE__)
6
+
7
+
8
+ PasswordPusher::Application.load_tasks
data/TODO ADDED
@@ -0,0 +1,21 @@
1
+ This is the official TODO list for PasswordPusher.
2
+
3
+ This list is a collection of suggestions and ideas by myself
4
+ and those contributed by the community. The list is in no particular
5
+ order.
6
+
7
+ Items are identifed with one of the following:
8
+
9
+ [F] = Feature
10
+ [E] = Enhancement/Improvement
11
+ [I] = Idea, possibility, thought
12
+
13
+ Issues should be filed in the Github repo:
14
+ https://github.com/pglombardo/PasswordPusher/issues
15
+
16
+ * [F] Add a password generator to the front page - John Connelly
17
+ * [E] Change from using Rails to something much lighter such as Sinatra
18
+ * [E] Internationalize strings
19
+ * [F] Add logins and password tracking (view # of views, user agent, expire passwords)
20
+ * [F] Add command line utility to push passwords from the command line
21
+
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "PasswordPusher",
3
+ "description": "An application to communicate passwords over the web. Passwords expire after a certain number of views and/or time has passed.",
4
+ "repository": "https://github.com/pglombardo/PasswordPusher",
5
+ "logo": "https://s3-eu-west-1.amazonaws.com/pwpush/pwpush_logo.png",
6
+ "keywords": ["password", "security", "expire"],
7
+ "addons": [
8
+ "heroku-postgresql"
9
+ ],
10
+ "scripts": {
11
+ "postdeploy": "bundle exec rake db:setup"
12
+ },
13
+ "env": {
14
+ "BUNDLE_WITHOUT": "development:test:private",
15
+ "WEB_CONCURRENCY": {
16
+ "description": "The number of processes to run.",
17
+ "value": "3"
18
+ }
19
+ }
20
+ }
21
+
@@ -0,0 +1,4 @@
1
+ # Place all the behaviors and hooks related to the matching controller here.
2
+ # All this logic will automatically be available in application.js.
3
+ # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
4
+
@@ -0,0 +1,52 @@
1
+ // This is a manifest file that'll be compiled into including all the files listed below.
2
+ // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
3
+ // be included in the compiled file accessible from http://example.com/assets/application.js
4
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
5
+ // the compiled file.
6
+ //
7
+ //= require jquery
8
+ //= require jquery_ujs
9
+ //= require_tree .
10
+ //= require modernizr
11
+
12
+ function showDaysValue(newValue)
13
+ {
14
+ if (newValue > 1) {
15
+ document.getElementById("daysrange").innerHTML=newValue + ' Days';
16
+ } else {
17
+ document.getElementById("daysrange").innerHTML=newValue + ' Day';
18
+ }
19
+ }
20
+
21
+ function showViewsValue(newValue)
22
+ {
23
+ if (newValue > 1) {
24
+ document.getElementById("viewsrange").innerHTML=newValue + ' Views';
25
+ } else {
26
+ document.getElementById("viewsrange").innerHTML=newValue + ' View';
27
+ }
28
+ }
29
+
30
+
31
+ msg = "Enter the Password to be Shared"
32
+ function prepareTextField(e) {
33
+ if (e) {
34
+ if (e.value == msg) {
35
+ e.value = '';
36
+ }
37
+ }
38
+ }
39
+
40
+ function revertTextField(e) {
41
+ if (e)
42
+ if (e.value == '') {
43
+ e.value = msg;
44
+ }
45
+ }
46
+
47
+ function setCopied() {
48
+ $('#clip_tip').text('copied!');
49
+ }
50
+
51
+ $('spoiler, .spoiler').spoilerAlert({max: 10, partial: 7})
52
+
@@ -0,0 +1,3 @@
1
+ # Place all the behaviors and hooks related to the matching controller here.
2
+ # All this logic will automatically be available in application.js.
3
+ # You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
@@ -0,0 +1,1299 @@
1
+ /*! Unobtrusive Slider Control / HTML5 Input Range polyfill - MIT/GPL2 @freqdec */
2
+ var fdSlider = (function() {
3
+ var sliders = {},
4
+ uniqueid = 0,
5
+ mouseWheelEnabled = true,
6
+ fullARIA = true,
7
+ describedBy = "fd-slider-describedby",
8
+ varSetRules = {
9
+ onfocus:true,
10
+ onvalue:true
11
+ },
12
+ noRangeBar = false,
13
+ html5Animation = "jump",
14
+ isOpera = Object.prototype.toString.call(window.opera) === "[object Opera]",
15
+ fpRegExp = /^([-]{0,1}[0-9]+(\.[0-9]+){0,1})$/,
16
+ stepRegExp = /^([0-9]+(\.[0-9]+){0,1})$/;
17
+
18
+ var parseJSON = function(str) {
19
+ // Check we have a String
20
+ if(typeof str !== 'string' || str == "") {
21
+ return {};
22
+ };
23
+ try {
24
+ // Does a JSON (native or not) Object exist
25
+ if(typeof JSON === "object" && JSON.parse) {
26
+ return window.JSON.parse(str);
27
+ // Genious code taken from: http://kentbrewster.com/badges/
28
+ } else if(/mousewheelenabled|fullaria|describedby|norangebar|html5animation|varsetrules/.test(str.toLowerCase())) {
29
+ var f = Function(['var document,top,self,window,parent,Number,Date,Object,Function,',
30
+ 'Array,String,Math,RegExp,Image,ActiveXObject;',
31
+ 'return (' , str.replace(/<\!--.+-->/gim,'').replace(/\bfunction\b/g,'function-') , ');'].join(''));
32
+ return f();
33
+ };
34
+ } catch (e) { };
35
+
36
+ return {"err":"Could not parse the JSON object"};
37
+ };
38
+
39
+ var affectJSON = function(json) {
40
+ if(typeof json !== "object") { return; };
41
+ for(key in json) {
42
+ value = json[key];
43
+ switch(key.toLowerCase()) {
44
+ case "mousewheelenabled":
45
+ mouseWheelEnabled = !!value;
46
+ break;
47
+ case "fullaria":
48
+ fullARIA = !!value;
49
+ break;
50
+ case "describedby":
51
+ describedBy = String(value);
52
+ break;
53
+ case "norangebar":
54
+ noRangeBar = !!value;
55
+ break;
56
+ case "html5animation":
57
+ html5Animation = String(value).search(/^(jump|tween|timed)$/i) != -1 ? String(value).toLowerCase() : "jump";
58
+ break;
59
+ case "varsetrules":
60
+ if("onfocus" in value) {
61
+ varSetRules.onfocus = !!value.onfocus;
62
+ };
63
+ if("onvalue" in value) {
64
+ varSetRules.onvalue = !!value.onvalue;
65
+ };
66
+ break;
67
+ };
68
+ };
69
+ };
70
+
71
+ // Classic event functions
72
+ var addEvent = function(obj, type, fn) {
73
+ if( obj.attachEvent ) {
74
+ obj.attachEvent( "on"+type, fn );
75
+ } else { obj.addEventListener( type, fn, true ); }
76
+ };
77
+ var removeEvent = function(obj, type, fn) {
78
+ try {
79
+ if( obj.detachEvent ) {
80
+ obj.detachEvent( "on"+type, fn );
81
+ } else { obj.removeEventListener( type, fn, true ); }
82
+ } catch(err) {};
83
+ };
84
+ var stopEvent = function(e) {
85
+ e = e || window.event;
86
+ if(e.stopPropagation) {
87
+ e.stopPropagation();
88
+ e.preventDefault();
89
+ };
90
+
91
+ /*@cc_on@*/
92
+ /*@if(@_win32)
93
+ e.cancelBubble = true;
94
+ e.returnValue = false;
95
+ /*@end@*/
96
+
97
+ return false;
98
+ };
99
+ var preventDefault = function(e) {
100
+ e = e || window.event;
101
+ if(e.preventDefault) {
102
+ e.preventDefault();
103
+ return;
104
+ };
105
+ e.returnValue = false;
106
+ };
107
+ // Add/Remove classname utility functions
108
+ var addClass = function(e,c) {
109
+ if(new RegExp("(^|\\s)" + c + "(\\s|$)").test(e.className)) { return; };
110
+ e.className += ( e.className ? " " : "" ) + c;
111
+ };
112
+
113
+ var removeClass = function(e,c) {
114
+ e.className = !c ? "" : e.className.replace(new RegExp("(^|\\s)" + c + "(\\s|$)"), " ").replace(/^\s\s*/, '').replace(/\s\s*$/, '');
115
+ };
116
+
117
+ // Returns an Object of key value pairs indicating which sliders have values
118
+ // that have been "set" by the user
119
+ var getValueSet = function() {
120
+ var obj = {};
121
+ for(id in sliders) {
122
+ obj[id] = sliders[id].getValueSet();
123
+ };
124
+ return obj;
125
+ };
126
+
127
+ // Sets the valueSet variable for a specific slider
128
+ var setValueSet = function(sliderId, tf) {
129
+ sliders[sliderId].setValueSet(!!tf);
130
+ };
131
+
132
+ // Does the slider exist in memory
133
+ var sliderExists = function(slider) {
134
+ return !!(slider in sliders && sliders.hasOwnProperty(slider));
135
+ };
136
+
137
+ // Javascript instantiation of a slider (input type="text" or select list)
138
+ var createSlider = function(options) {
139
+ if(!options || !options.inp || !options.inp.tagName || options.inp.tagName.search(/^input|select/i) == -1) { return false; };
140
+
141
+ options.html5Shim = false;
142
+
143
+ if(options.inp.tagName.toLowerCase() == "select") {
144
+ if(options.inp.options.length < 2) {
145
+ return false;
146
+ };
147
+ options.min = 0;
148
+ options.max = options.inp.options.length - 1;
149
+ options.step = 1;
150
+ options.precision = 0;
151
+ options.scale = false;
152
+ options.forceValue = true;
153
+ } else {
154
+ if(String(options.inp.type).search(/^text$/i) == -1) {
155
+ return false;
156
+ };
157
+ options.min = options.min && String(options.min).search(fpRegExp) != -1 ? +options.min : 0;
158
+ options.max = options.max && String(options.max).search(fpRegExp) != -1 ? +options.max : 100;
159
+ options.step = options.step && String(options.step).search(stepRegExp) != -1 ? options.step : 1;
160
+ options.precision = options.precision && String(options.precision).search(/^[0-9]+$/) != -1 ? options.precision : (String(options.step).search(/\.([0-9]+)$/) != -1 ? String(options.step).match(/\.([0-9]+)$/)[1].length : 0);
161
+ options.scale = options.scale || false;
162
+ options.forceValue = ("forceValue" in options) ? !!options.forceValue : false;
163
+ };
164
+
165
+ options.maxStep = options.maxStep && String(options.maxStep).search(stepRegExp) != -1 ? +options.maxStep : +options.step * 2;
166
+ options.classNames = options.classNames || "";
167
+ options.callbacks = options.callbacks || false;
168
+
169
+ destroySingleSlider(options.inp.id);
170
+ sliders[options.inp.id] = new fdRange(options);
171
+ return true;
172
+ };
173
+
174
+ var getAttribute = function(elem, att) {
175
+ return elem.getAttribute(att) || "";
176
+ };
177
+
178
+ // HTML5 input type="range" shim - called onload or onDomReady
179
+ var init = function() {
180
+ var inputs = document.getElementsByTagName("input"),
181
+ options;
182
+
183
+ for(var i = 0, inp; inp = inputs[i]; i++) {
184
+
185
+ if(inp.tagName.toLowerCase() == "input"
186
+ &&
187
+ inp.type.toLowerCase() == "text"
188
+ &&
189
+ (getAttribute(inp, "min") && getAttribute(inp, "min").search(fpRegExp) != -1
190
+ ||
191
+ getAttribute(inp, "max") && getAttribute(inp, "max").search(fpRegExp) != -1
192
+ ||
193
+ getAttribute(inp, "step") && getAttribute(inp, "step").search(/^(any|([0-9]+(\.[0-9]+){0,1}))$/i) != -1
194
+ )) {
195
+
196
+ // Skip elements that have already been created are are resident in the DOM
197
+ if(inp.id && document.getElementById("fd-slider-"+inp.id)) {
198
+ continue;
199
+ // Destroy elements that have already been created but not resident in the DOM
200
+ } else if(inp.id && !document.getElementById("fd-slider-"+inp.id)) {
201
+ destroySingleSlider(inp.id);
202
+ };
203
+
204
+ // Create an id for the form element if necessary
205
+ if(!inp.id) {
206
+ inp.id = "fd-slider-form-elem-" + uniqueid++;
207
+ };
208
+
209
+ // Basic option Object
210
+ options = {
211
+ inp: inp,
212
+ callbacks: [],
213
+ animation: html5Animation,
214
+ vertical: getAttribute(inp, "data-fd-slider-vertical") ? true : !!(inp.offsetHeight > inp.offsetWidth),
215
+ classNames: getAttribute(inp, "data-fd-slider-vertical"),
216
+ html5Shim: true
217
+ };
218
+
219
+ if(options.vertical && !getAttribute(inp, "data-fd-slider-vertical")) {
220
+ options.inpHeight = inp.offsetHeight;
221
+ };
222
+
223
+ options.min = getAttribute(inp, "min") || 0;
224
+ options.max = getAttribute(inp, "max") || 100;
225
+ options.step = getAttribute(inp, "step").search(/^any$/i) != -1 ? options.max - options.min : getAttribute(inp, "step").search(stepRegExp) != -1 ? inp.getAttribute("step") : 1;
226
+ options.precision = String(options.step).search(/\.([0-9]+)$/) != -1 ? String(options.step).match(/\.([0-9]+)$/)[1].length : 0;
227
+ options.maxStep = options.step * 2;
228
+
229
+ destroySingleSlider(options.inp.id);
230
+ sliders[options.inp.id] = new fdRange(options);
231
+ };
232
+ };
233
+
234
+ return true;
235
+ };
236
+ var destroySingleSlider = function(id) {
237
+ if(id in sliders && sliders.hasOwnProperty(id)) {
238
+ sliders[id].destroy();
239
+ delete sliders[id];
240
+ return true;
241
+ };
242
+ return false;
243
+ };
244
+ var destroyAllsliders = function(e) {
245
+ for(slider in sliders) {
246
+ if(sliders.hasOwnProperty(slider)) {
247
+ sliders[slider].destroy();
248
+ };
249
+ };
250
+ sliders = [];
251
+ };
252
+ var unload = function(e) {
253
+ destroyAllsliders();
254
+ sliders = null;
255
+ };
256
+ var resize = function(e) {
257
+ for(slider in sliders) {
258
+ if(sliders.hasOwnProperty(slider)) {
259
+ sliders[slider].onResize();
260
+ };
261
+ };
262
+ };
263
+ var onDomReady = function() {
264
+ removeEvent(window, "load", init);
265
+ init();
266
+ };
267
+ var removeOnLoadEvent = function() {
268
+ removeEvent(window, "load", init);
269
+ };
270
+
271
+ function fdRange(options) {
272
+ var inp = options.inp,
273
+ disabled = false,
274
+ tagName = inp.tagName.toLowerCase(),
275
+ min = +options.min,
276
+ max = +options.max,
277
+ rMin = +options.min,
278
+ rMax = +options.max,
279
+ range = Math.abs(max - min),
280
+ step = tagName == "select" ? 1 : +options.step,
281
+ maxStep = options.maxStep ? +options.maxStep : step * 2,
282
+ precision = options.precision || 0,
283
+ steps = Math.ceil(range / step),
284
+ scale = options.scale || false,
285
+ hideInput = !!options.hideInput,
286
+ animation = options.animation || "",
287
+ vertical = !!options.vertical,
288
+ callbacks = options.callbacks || {},
289
+ classNames = options.classNames || "",
290
+ html5Shim = !!options.html5Shim,
291
+ defaultVal = max < min ? min : min + ((max - min) / 2),
292
+ resetDef = tagName == "select" ? inp.selectedIndex : inp.defaultValue || defaultVal,
293
+ forceValue = html5Shim || !!options.forceValue,
294
+ inpHeight = html5Shim && vertical && ("inpHeight" in options) ? options.inpHeight : false,
295
+ timer = null,
296
+ kbEnabled = true,
297
+ initialVal = tagName == "select" ? inp.selectedIndex : inp.value,
298
+ sliderH = 0,
299
+ sliderW = 0,
300
+ tweenX = 0,
301
+ tweenB = 0,
302
+ tweenC = 0,
303
+ tweenD = 0,
304
+ frame = 0,
305
+ x = 0,
306
+ y = 0,
307
+ rMaxPx = 0,
308
+ rMinPx = 0,
309
+ handlePos = 0,
310
+ destPos = 0,
311
+ mousePos = 0,
312
+ stepPx = 0,
313
+ userSet = false,
314
+ touchEvents = false,
315
+ outerWrapper,
316
+ wrapper,
317
+ handle,
318
+ rangeBar,
319
+ bar;
320
+
321
+ // For the reset event to work we have set a defaultValue
322
+ if(tagName == "input" && forceValue && !inp.defaultValue) {
323
+ inp.defaultValue = getWorkingValueFromInput();
324
+ };
325
+
326
+ // Make sure we have a negative step if the max < min
327
+ if(max < min) {
328
+ step = -Math.abs(step);
329
+ maxStep = -Math.abs(maxStep);
330
+ };
331
+
332
+ // Add the 100% scale mark if needs be
333
+ if(scale) {
334
+ scale[100] = max;
335
+ };
336
+
337
+ // Set the "userSet" variable programmatically for this slider
338
+ function valueSet(tf) {
339
+ tf = !!tf;
340
+ if(tf != userSet) {
341
+ userSet = tf;
342
+ valueToPixels(getWorkingValueFromInput());
343
+ };
344
+ };
345
+
346
+ function disableSlider(noCallback) {
347
+ if(disabled && !noCallback) {
348
+ return;
349
+ };
350
+
351
+ try {
352
+ setTabIndex(handle, -1);
353
+ removeEvent(handle, "focus", onFocus);
354
+ removeEvent(handle, "blur", onBlur);
355
+
356
+ if(!isOpera) {
357
+ removeEvent(handle, "keydown", onKeyDown);
358
+ removeEvent(handle, "keypress", onKeyPress);
359
+ } else {
360
+ removeEvent(handle, "keypress", onKeyDown);
361
+ };
362
+
363
+ removeEvent(outerWrapper, "mouseover", onMouseOver);
364
+ removeEvent(outerWrapper, "mouseout", onMouseOut);
365
+ removeEvent(outerWrapper, "mousedown", onMouseDown);
366
+ removeEvent(outerWrapper, "touchstart", onMouseDown);
367
+
368
+ if(mouseWheelEnabled) {
369
+ if (window.addEventListener && !window.devicePixelRatio) window.removeEventListener('DOMMouseScroll', trackMouseWheel, false);
370
+ else {
371
+ removeEvent(document, "mousewheel", trackMouseWheel);
372
+ removeEvent(window, "mousewheel", trackMouseWheel);
373
+ };
374
+ };
375
+ } catch(err) {};
376
+
377
+ removeClass(outerWrapper, "fd-slider-focused");
378
+ removeClass(outerWrapper, "fd-slider-active");
379
+
380
+ addClass(outerWrapper, "fd-slider-disabled");
381
+ outerWrapper.setAttribute("aria-disabled", true);
382
+ inp.disabled = disabled = true;
383
+ clearTimeout(timer);
384
+
385
+ if(!noCallback) {
386
+ callback("disable");
387
+ };
388
+ };
389
+
390
+ function enableSlider(noCallback) {
391
+ if(!disabled && !noCallback) {
392
+ return;
393
+ };
394
+
395
+ setTabIndex(handle, 0);
396
+ addEvent(handle, "focus", onFocus);
397
+ addEvent(handle, "blur", onBlur);
398
+
399
+ if(!isOpera) {
400
+ addEvent(handle, "keydown", onKeyDown);
401
+ addEvent(handle, "keypress", onKeyPress);
402
+ } else {
403
+ addEvent(handle, "keypress", onKeyDown);
404
+ };
405
+
406
+ addEvent(outerWrapper, "touchstart", onMouseDown);
407
+ addEvent(outerWrapper, "mousedown", onMouseDown);
408
+ addEvent(outerWrapper, "mouseover", onMouseOver);
409
+ addEvent(outerWrapper, "mouseout", onMouseOut);
410
+
411
+ removeClass(outerWrapper, "fd-slider-disabled");
412
+ outerWrapper.setAttribute("aria-disabled", false);
413
+ inp.disabled = disabled = touchEvents = false;
414
+
415
+ if(!noCallback) {
416
+ callback("enable");
417
+ };
418
+ };
419
+
420
+ // Destroys a slider
421
+ function destroySlider() {
422
+ // Clear any timeouts
423
+ clearTimeout(timer);
424
+
425
+ // Remove pointers to DOM nodes
426
+ wrapper = bar = handle = outerWrapper = timer = null;
427
+
428
+ // Call the "destroy" callback
429
+ callback("destroy");
430
+
431
+ // Delete the callback functions
432
+ callbacks = null;
433
+ };
434
+
435
+ // Calculates the pixel increment etc
436
+ function redraw() {
437
+ locate();
438
+ // Internet Explorer requires the try catch as hidden
439
+ // elements throw errors
440
+ try {
441
+ var sW = outerWrapper.offsetWidth,
442
+ sH = outerWrapper.offsetHeight,
443
+ hW = handle.offsetWidth,
444
+ hH = handle.offsetHeight,
445
+ bH = bar.offsetHeight,
446
+ bW = bar.offsetWidth,
447
+ mPx = vertical ? sH - hH : sW - hW;
448
+
449
+ stepPx = mPx / steps;
450
+ rMinPx = Math.max(scale ? percentToPixels(valueToPercent(rMin)) : Math.abs((rMin - min) / step) * stepPx, 0);
451
+ rMaxPx = Math.min(scale ? percentToPixels(valueToPercent(rMax)) : Math.abs((rMax - min) / step) * stepPx, Math.floor(vertical ? sH - hH : sW - hW));
452
+
453
+ sliderW = sW;
454
+ sliderH = sH;
455
+
456
+ // Use the input value
457
+ valueToPixels(forceValue ? getWorkingValueFromInput() : (tagName == "select" ? inp.selectedIndex : parseFloat(inp.value)));
458
+
459
+ } catch(err) {};
460
+ callback("redraw");
461
+ };
462
+
463
+ // Calls a callback function
464
+ function callback(type) {
465
+ if(!html5Shim) {
466
+ if(callbacks.hasOwnProperty(type)) {
467
+ var cbObj = {"disabled":disabled, "elem":inp, "value":tagName == "select" ? inp.options[inp.selectedIndex].value : inp.value};
468
+
469
+ // Call all functions in sequence
470
+ for(var i = 0, func; func = callbacks[type][i]; i++) {
471
+ func.call(inp, cbObj);
472
+ };
473
+ };
474
+ } else if(type.match(/^(blur|focus|change)$/i)) {
475
+ if(typeof(document.createEventObject) != 'undefined') {
476
+ try {
477
+ var e = document.createEventObject();
478
+ inp.fireEvent('on' + type.toLowerCase(), e);
479
+ } catch(err){ };
480
+ } else if(typeof(document.createEvent) != 'undefined') {
481
+ var e = document.createEvent('HTMLEvents');
482
+ e.initEvent(type, true, true);
483
+ inp.dispatchEvent(e);
484
+ };
485
+ };
486
+ };
487
+
488
+ // FOCUS & BLUR events
489
+ function onFocus(e) {
490
+ addClass(outerWrapper, 'fd-slider-focused');
491
+
492
+ // Is the value said to have been set by the user onfocus
493
+ if(varSetRules.onfocus) {
494
+ userSet = true;
495
+ valueToPixels(getWorkingValueFromInput());
496
+ };
497
+
498
+ // If mousewheel events required then add them
499
+ if(mouseWheelEnabled) {
500
+ addEvent(window, 'DOMMouseScroll', trackMouseWheel);
501
+ addEvent(document, 'mousewheel', trackMouseWheel);
502
+ if(!isOpera) {
503
+ addEvent(window, 'mousewheel', trackMouseWheel);
504
+ };
505
+ };
506
+
507
+ // Callback...
508
+ callback("focus");
509
+ return true;
510
+ };
511
+
512
+ function onBlur(e) {
513
+ removeClass(outerWrapper, 'fd-slider-focused');
514
+
515
+ // Remove mousewheel events if necessary
516
+ if(mouseWheelEnabled) {
517
+ removeEvent(document, 'mousewheel', trackMouseWheel);
518
+ removeEvent(window, 'DOMMouseScroll', trackMouseWheel);
519
+ if(!isOpera) {
520
+ removeEvent(window, 'mousewheel', trackMouseWheel);
521
+ };
522
+ };
523
+
524
+ kbEnabled = true;
525
+
526
+ // Callback...
527
+ callback("blur");
528
+ };
529
+
530
+ // MOUSEWHEEL events
531
+ function trackMouseWheel(e) {
532
+ if(!kbEnabled) {
533
+ return;
534
+ };
535
+ e = e || window.event;
536
+ var delta = 0,
537
+ value;
538
+
539
+ if (e.wheelDelta) {
540
+ delta = e.wheelDelta/120;
541
+ // Older versions of Opera require a small hack to inverse the delta
542
+ if (isOpera && window.opera.version() < 9.2) {
543
+ delta = -delta;
544
+ };
545
+ } else if(e.detail) {
546
+ delta = -e.detail/3;
547
+ };
548
+
549
+ if(vertical) {
550
+ delta = -delta;
551
+ };
552
+
553
+ if(delta) {
554
+ value = getWorkingValueFromInput();
555
+ value += (delta < 0) ? -step : step;
556
+ userSet = true;
557
+ valueToPixels(getValidValue(value));
558
+ };
559
+
560
+ return stopEvent(e);
561
+ };
562
+
563
+ // KEYBOARD events
564
+ function onKeyPress(e) {
565
+ e = e || window.event;
566
+ // Let all non-hijacked keyboard events pass
567
+ if((e.keyCode >= 33 && e.keyCode <= 40) || !kbEnabled || e.keyCode == 45 || e.keyCode == 46) {
568
+ return stopEvent(e);
569
+ };
570
+ return true;
571
+ };
572
+
573
+ function onKeyDown(e) {
574
+ if(!kbEnabled) {
575
+ return true;
576
+ };
577
+
578
+ e = e || window.event;
579
+ var kc = e.keyCode != null ? e.keyCode : e.charCode,
580
+ value;
581
+
582
+ if(kc < 33 || (kc > 40 && (kc != 45 && kc != 46))) {
583
+ return true;
584
+ };
585
+
586
+ value = getWorkingValueFromInput();
587
+
588
+ if( kc == 37 || kc == 40 || kc == 46 || kc == 34) {
589
+ // left, down, ins, page down
590
+ value -= (e.ctrlKey || kc == 34 ? +maxStep : +step);
591
+ } else if( kc == 39 || kc == 38 || kc == 45 || kc == 33) {
592
+ // right, up, del, page up
593
+ value += (e.ctrlKey || kc == 33 ? +maxStep : +step);
594
+ } else if( kc == 35 ) {
595
+ // max
596
+ value = rMax;
597
+ } else if( kc == 36 ) {
598
+ // min
599
+ value = rMin;
600
+ };
601
+
602
+ userSet = true;
603
+ valueToPixels(getValidValue(value));
604
+
605
+ callback("update");
606
+
607
+ // Opera doesn't let us cancel key events so the up/down arrows and home/end buttons will scroll the screen - which sucks
608
+ preventDefault(e);
609
+ };
610
+
611
+ // MOUSE & TOUCH events
612
+
613
+ // Mouseover the slider
614
+ function onMouseOver(e) {
615
+ addClass(outerWrapper, 'fd-slider-hover');
616
+ };
617
+
618
+ // Mouseout of the slider
619
+ function onMouseOut(e) {
620
+ // Should really check we are not still in the slider
621
+ removeClass(outerWrapper, 'fd-slider-hover');
622
+ };
623
+
624
+ // Mousedown on the slider
625
+ function onMouseDown(e) {
626
+ e = e || window.event;
627
+
628
+ // Stop page scrolling
629
+ preventDefault(e);
630
+
631
+ // Grab the event target
632
+ var targ;
633
+ if (e.target) {
634
+ targ = e.target;
635
+ } else if (e.srcElement) {
636
+ targ = e.srcElement;
637
+ };
638
+ if(targ && targ.nodeType == 3) {
639
+ targ = targ.parentNode;
640
+ };
641
+
642
+ // Are we using touchEvents
643
+ if(e.touches) {
644
+ // Skip gestures
645
+ if(e.targetTouches && e.targetTouches.length != 1) {
646
+ return false;
647
+ };
648
+
649
+ e = e.touches[0];
650
+ touchEvents = true;
651
+ };
652
+
653
+ // Stop any animation timers
654
+ clearTimeout(timer);
655
+ timer = null;
656
+
657
+ // Not keyboard enabled
658
+ kbEnabled = false;
659
+
660
+ // User has set a value
661
+ userSet = true;
662
+
663
+ // Handle mousedown - initiate drag
664
+ if(targ.className.search("fd-slider-handle") != -1) {
665
+ mousePos = vertical ? e.clientY : e.clientX;
666
+ handlePos = parseInt(vertical ? handle.offsetTop : handle.offsetLeft)||0;
667
+
668
+ // Set a value on first click even if no movement
669
+ trackMouse(e);
670
+
671
+ if(!touchEvents) {
672
+ addEvent(document, 'mousemove', trackMouse);
673
+ addEvent(document, 'mouseup', stopDrag);
674
+ } else {
675
+ addEvent(document, 'touchmove', trackMouse);
676
+ addEvent(document, 'touchend', stopDrag);
677
+ // Remove mouseEvents to stop them firing after the touch event
678
+ removeEvent(outerWrapper, "mousedown", onMouseDown);
679
+ };
680
+
681
+ addClass(outerWrapper, 'fd-slider-active');
682
+ addClass(document.body, "fd-slider-drag-" + (vertical ? "vertical" : "horizontal"));
683
+
684
+ callback("dragstart");
685
+
686
+ // Wrapper mousedown - initiate animation to click point
687
+ } else {
688
+ locate();
689
+
690
+ var posx = 0,
691
+ sLft = 0,
692
+ sTop = 0;
693
+
694
+ // Internet Explorer doctype woes
695
+ if(document.documentElement && document.documentElement.scrollTop) {
696
+ sTop = document.documentElement.scrollTop;
697
+ sLft = document.documentElement.scrollLeft;
698
+ } else if (document.body) {
699
+ sTop = document.body.scrollTop;
700
+ sLft = document.body.scrollLeft;
701
+ };
702
+
703
+ if(e.pageX) {
704
+ posx = vertical ? e.pageY : e.pageX;
705
+ } else if (e.clientX) {
706
+ posx = vertical ? e.clientY + sTop : e.clientX + sLft;
707
+ };
708
+
709
+ posx -= vertical ? y + Math.round(handle.offsetHeight / 2) : x + Math.round(handle.offsetWidth / 2);
710
+ posx = snapToPxValue(posx);
711
+
712
+ // Tween animation to click point
713
+ if(animation == "tween") {
714
+ addClass(outerWrapper, 'fd-slider-active');
715
+ tweenTo(posx);
716
+ // Progressive increment to click point
717
+ } else if(animation == "timed") {
718
+ addClass(outerWrapper, 'fd-slider-active');
719
+ addEvent(document, touchEvents ? 'touchend' : 'mouseup', onDocMouseUp);
720
+ destPos = posx;
721
+ onTimer();
722
+ // Immediate jump to click point
723
+ } else {
724
+ pixelsToValue(posx);
725
+ //addEvent(document, touchEvents ? 'touchend' : 'mouseup', onMouseUp);
726
+ };
727
+ };
728
+
729
+ return stopEvent(e);
730
+ };
731
+
732
+ // Progressive increment to click point - clear the animation timer and remove the mouseup/touchend event
733
+ function onDocMouseUp( e ) {
734
+ e = e || window.event;
735
+
736
+ preventDefault(e);
737
+ removeEvent(document, touchEvents ? 'touchend' : 'mouseup', onDocMouseUp);
738
+ removeClass(outerWrapper, "fd-slider-active");
739
+
740
+ clearTimeout(timer);
741
+ timer = null;
742
+ kbEnabled = true;
743
+
744
+ return stopEvent(e);
745
+ };
746
+
747
+ // Mouseup or touchend event on the document to stop drag
748
+ function stopDrag(e) {
749
+ e = e || window.event;
750
+
751
+ preventDefault(e);
752
+
753
+ if(touchEvents) {
754
+ removeEvent(document, 'touchmove', trackMouse);
755
+ removeEvent(document, 'touchend', stopDrag);
756
+ } else {
757
+ removeEvent(document, 'mousemove', trackMouse);
758
+ removeEvent(document, 'mouseup', stopDrag);
759
+ };
760
+
761
+ kbEnabled = true;
762
+ removeClass(document.body, "fd-slider-drag-" + (vertical ? "vertical" : "horizontal"));
763
+ removeClass(outerWrapper, "fd-slider-active");
764
+
765
+ callback("dragend");
766
+
767
+ return stopEvent(e);
768
+ };
769
+
770
+ // Mousemove or touchmove event on the drag handle
771
+ function trackMouse(e) {
772
+ e = e || window.event;
773
+
774
+ preventDefault(e);
775
+
776
+ if(e.touches) {
777
+ // Skip gestures
778
+ if(e.targetTouches && e.targetTouches.length != 1) {
779
+ return false;
780
+ };
781
+ e = e.touches[0];
782
+ };
783
+
784
+ pixelsToValue(snapToPxValue(handlePos + (vertical ? e.clientY - mousePos : e.clientX - mousePos)));
785
+
786
+ return false;
787
+ };
788
+
789
+ // Increments the slider by "inc" steps
790
+ function increment(inc) {
791
+ var value = getWorkingValueFromInput();
792
+ userSet = true;
793
+ value += inc * step;
794
+ valueToPixels(getValidValue(value));
795
+ };
796
+
797
+ // Attempts to locate the on-screen position of the slider
798
+ function locate(){
799
+ var curleft = 0,
800
+ curtop = 0,
801
+ obj = outerWrapper;
802
+
803
+ // Try catch for IE's benefit
804
+ try {
805
+ while (obj.offsetParent) {
806
+ curleft += obj.offsetLeft;
807
+ curtop += obj.offsetTop;
808
+ obj = obj.offsetParent;
809
+ };
810
+ } catch(err) {};
811
+ x = curleft;
812
+ y = curtop;
813
+ };
814
+
815
+ // Used during the progressive animation to click point
816
+ function onTimer() {
817
+ var xtmp = parseInt(vertical ? handle.offsetTop : handle.offsetLeft, 10);
818
+ xtmp = Math.round((destPos < xtmp) ? Math.max(destPos, Math.floor(xtmp - stepPx)) : Math.min(destPos, Math.ceil(xtmp + stepPx)));
819
+
820
+ pixelsToValue(snapToPxValue(xtmp));
821
+ if(xtmp != destPos) {
822
+ timer = setTimeout(onTimer, steps > 20 ? 50 : 100);
823
+ } else {
824
+ kbEnabled = true;
825
+ removeClass(outerWrapper, "fd-slider-active");
826
+ callback("finalise");
827
+ };
828
+ };
829
+
830
+ var tween = function(){
831
+ frame++;
832
+ var c = tweenC,
833
+ d = 20,
834
+ t = frame,
835
+ b = tweenB,
836
+ x = Math.ceil((t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b);
837
+
838
+ pixelsToValue(t == d ? tweenX : x);
839
+
840
+ if(t!=d) {
841
+ // Call the "move" callback on each animation increment
842
+ callback("move");
843
+ timer = setTimeout(tween, 20);
844
+ } else {
845
+ clearTimeout(timer);
846
+ timer = null;
847
+ kbEnabled = true;
848
+
849
+ removeClass(outerWrapper, "fd-slider-focused");
850
+ removeClass(outerWrapper, "fd-slider-active");
851
+
852
+ // Call the "finalise" callback whenever the animation is complete
853
+ callback("finalise");
854
+ };
855
+ };
856
+
857
+ function tweenTo(tx){
858
+ kbEnabled = false;
859
+ tweenX = parseInt(tx, 10);
860
+ tweenB = parseInt(vertical ? handle.offsetTop : handle.offsetLeft, 10);
861
+ tweenC = tweenX - tweenB;
862
+ tweenD = 20;
863
+ frame = 0;
864
+
865
+ if(!timer) {
866
+ timer = setTimeout(tween, 20);
867
+ };
868
+ };
869
+
870
+ // Returns a value within the range & sets the userSet var
871
+ // i.e. has the user entered a valid value
872
+ function checkValue(value) {
873
+ if(isNaN(value) || value === "" || typeof value == "undefined") {
874
+ userSet = false;
875
+ return defaultVal;
876
+ } else if(value < Math.min(rMin,rMax)) {
877
+ userSet = false;
878
+ return Math.min(rMin,rMax);
879
+ } else if(value > Math.max(rMin,rMax)) {
880
+ userSet = false;
881
+ return Math.max(rMin,rMax);
882
+ };
883
+ userSet = true;
884
+ return value;
885
+ };
886
+
887
+ // Returns a value within a range - uses the form element value as base
888
+ function getWorkingValueFromInput() {
889
+ return getValidValue(tagName == "input" ? parseFloat(inp.value) : inp.selectedIndex);
890
+ };
891
+
892
+ // Returns a value within the range
893
+ function getValidValue(value) {
894
+ return (isNaN(value) || value === "" || typeof value == "undefined") ? defaultVal : Math.min(Math.max(value, Math.min(rMin,rMax)), Math.max(rMin,rMax));
895
+ };
896
+
897
+ // Calculates value according to pixel position of slider handle
898
+ function pixelsToValue(px) {
899
+ var val = getValidValue(scale ? percentToValue(pixelsToPercent(px)) : vertical ? max - (Math.round(px / stepPx) * step) : min + (Math.round(px / stepPx) * step));
900
+
901
+ handle.style[vertical ? "top" : "left"] = (px || 0) + "px";
902
+ redrawRange();
903
+ setInputValue((tagName == "select" || step == 1) ? Math.round(val) : val);
904
+ };
905
+
906
+ // Calculates pixel position according to form element value
907
+ function valueToPixels(val) {
908
+ var clearVal = false,
909
+ value;
910
+
911
+ // Allow empty values for non-polyfill sliders
912
+ if((typeof val == "undefined" || isNaN(val) || val === "") && tagName == "input" && !forceValue) {
913
+ value = defaultVal;
914
+ clearVal = true;
915
+ userSet = false;
916
+ } else {
917
+ value = checkValue(val);
918
+ };
919
+
920
+ handle.style[vertical ? "top" : "left"] = (scale ? percentToPixels(valueToPercent(value)) : vertical ? Math.round(((max - value) / step) * stepPx) : Math.round(((value - min) / step) * stepPx)) + "px";
921
+ redrawRange();
922
+ setInputValue(clearVal ? "" : value);
923
+ };
924
+
925
+ // Rounds a pixel value to the nearest "snap" point on the slider scale
926
+ function snapToPxValue(px) {
927
+ if(scale) {
928
+ return Math.max(Math.min(rMaxPx, px), rMinPx);
929
+ } else {
930
+ var rem = px % stepPx;
931
+ if(rem && rem >= (stepPx / 2)) {
932
+ px += (stepPx - rem);
933
+ } else {
934
+ px -= rem;
935
+ };
936
+
937
+ if(px < Math.min(Math.abs(rMinPx), Math.abs(rMaxPx))) {
938
+ px = Math.min(Math.abs(rMinPx), Math.abs(rMaxPx));
939
+ } else if(px > Math.max(Math.abs(rMinPx), Math.abs(rMaxPx))) {
940
+ px = Math.max(Math.abs(rMinPx), Math.abs(rMaxPx));
941
+ };
942
+
943
+ return Math.min(Math.max(px, 0), rMaxPx);
944
+ };
945
+ };
946
+
947
+ // Calculates a value according to percentage of distance handle has travelled
948
+ function percentToValue(pct) {
949
+ var st = 0,
950
+ fr = min,
951
+ value;
952
+
953
+ for(var s in scale) {
954
+ if(!scale.hasOwnProperty(s)) {
955
+ continue;
956
+ };
957
+
958
+ if(pct >= st && pct <= +s ) {
959
+ value = fr + ((pct - st) * (+scale[s] - fr) ) / (+s - st);
960
+ };
961
+
962
+ st = +s;
963
+ fr = +scale[s];
964
+ };
965
+
966
+ return value;
967
+ };
968
+
969
+ // Calculates the percentage handle position according to form element value
970
+ function valueToPercent(value) {
971
+ var st = 0,
972
+ fr = min,
973
+ pct = 0;
974
+
975
+ for(var s in scale) {
976
+ if(!scale.hasOwnProperty(s)) {
977
+ continue;
978
+ };
979
+
980
+ if(value >= fr && value <= +scale[s]){
981
+ pct = st + (value - fr) * (+s - st) / (+scale[s] - fr);
982
+ };
983
+
984
+ st = +s;
985
+ fr = +scale[s];
986
+ };
987
+
988
+ return pct;
989
+ };
990
+
991
+ function percentToPixels(percent) {
992
+ return ((outerWrapper[vertical ? "offsetHeight" : "offsetWidth"] - handle[vertical ? "offsetHeight" : "offsetWidth"]) / 100) * percent;
993
+ };
994
+
995
+ function pixelsToPercent(pixels) {
996
+ return pixels / ((outerWrapper[vertical ? "offsetHeight" : "offsetWidth"] - outerWrapper[handle ? "offsetHeight" : "offsetWidth"]) / 100);
997
+ };
998
+
999
+ // Sets the form element with a valid value
1000
+ function setInputValue(val) {
1001
+ // The update callback doesn't mean the input value has changed
1002
+ callback("update");
1003
+
1004
+ // If the user has not set this value or has entered an incorrect value then set a class
1005
+ // to enable styling of the slider
1006
+ if(!userSet) {
1007
+ addClass(outerWrapper, "fd-slider-no-value");
1008
+ } else {
1009
+ removeClass(outerWrapper, "fd-slider-no-value");
1010
+ };
1011
+
1012
+ if(tagName == "select") {
1013
+ try {
1014
+ val = parseInt(val, 10);
1015
+ if(inp.selectedIndex === val) {
1016
+ updateAriaValues();
1017
+ return;
1018
+ };
1019
+ inp.options[val].selected = true;
1020
+ } catch (err) {};
1021
+ } else {
1022
+ if(val != "") {
1023
+ val = (min + (Math.round((val - min) / step) * step)).toFixed(precision);
1024
+ };
1025
+ if(inp.value === val) {
1026
+ updateAriaValues();
1027
+ return;
1028
+ };
1029
+ inp.value = val;
1030
+ };
1031
+
1032
+ updateAriaValues();
1033
+ callback("change");
1034
+ };
1035
+
1036
+ function checkInputValue(value) {
1037
+ return !(isNaN(value) || value === "" || value < Math.min(rMin,rMax) || value > Math.max(rMin,rMax));
1038
+ };
1039
+
1040
+ function setSliderRange(newMin, newMax) {
1041
+ if(rMin > rMax) {
1042
+ newMin = Math.min(min, Math.max(newMin, newMax));
1043
+ newMax = Math.max(max, Math.min(newMin, newMax));
1044
+ rMin = Math.max(newMin, newMax);
1045
+ rMax = Math.min(newMin, newMax);
1046
+ } else {
1047
+ newMin = Math.max(min, Math.min(newMin, newMax));
1048
+ newMax = Math.min(max, Math.max(newMin, newMax));
1049
+ rMin = Math.min(newMin, newMax);
1050
+ rMax = Math.max(newMin, newMax);
1051
+ };
1052
+
1053
+ if(defaultVal < Math.min(rMin, rMax)) {
1054
+ defaultVal = Math.min(rMin, rMax);
1055
+ } else if(defaultVal > Math.max(rMin, rMax)) {
1056
+ defaultVal = Math.max(rMin, rMax);
1057
+ };
1058
+
1059
+ handle.setAttribute("aria-valuemin", rMin);
1060
+ handle.setAttribute("aria-valuemax", rMax);
1061
+
1062
+ checkValue(tagName == "input" ? parseFloat(inp.value) : inp.selectedIndex);
1063
+ redraw();
1064
+ };
1065
+
1066
+ function redrawRange() {
1067
+ if(noRangeBar) {
1068
+ return;
1069
+ };
1070
+ if(vertical) {
1071
+ rangeBar.style["height"] = (bar.offsetHeight - handle.offsetTop) + "px";
1072
+ } else {
1073
+ rangeBar.style["width"] = handle.offsetLeft + "px";
1074
+ };
1075
+ };
1076
+
1077
+ function findLabel() {
1078
+ var label = false,
1079
+ labelList = document.getElementsByTagName('label');
1080
+ // loop through label array attempting to match each 'for' attribute to the id of the current element
1081
+ for(var i = 0, lbl; lbl = labelList[i]; i++) {
1082
+ // Internet Explorer requires the htmlFor test
1083
+ if((lbl['htmlFor'] && lbl['htmlFor'] == inp.id) || (lbl.getAttribute('for') == inp.id)) {
1084
+ label = lbl;
1085
+ break;
1086
+ };
1087
+ };
1088
+
1089
+ if(label && !label.id) {
1090
+ label.id = inp.id + "_label";
1091
+ };
1092
+
1093
+ return label;
1094
+ };
1095
+
1096
+ function updateAriaValues() {
1097
+ handle.setAttribute("aria-valuenow", tagName == "select" ? inp.options[inp.selectedIndex].value : inp.value);
1098
+ handle.setAttribute("aria-valuetext", tagName == "select" ? (inp.options[inp.selectedIndex].text ? inp.options[inp.selectedIndex].text : inp.options[inp.selectedIndex].value) : inp.value);
1099
+ };
1100
+
1101
+ function onInputChange(e) {
1102
+ userSet = true;
1103
+ valueToPixels(tagName == "input" ? parseFloat(inp.value) : inp.selectedIndex);
1104
+ updateAriaValues();
1105
+ };
1106
+
1107
+ function onReset(e) {
1108
+ if(tagName == "input") {
1109
+ inp.value = inp.defaultValue;
1110
+ } else {
1111
+ inp.selectedIndex = resetDef;
1112
+ };
1113
+ checkValue(tagName == "select" ? inp.options[inp.selectedIndex].value : inp.value);
1114
+ redraw();
1115
+ updateAriaValues();
1116
+ };
1117
+
1118
+ function valueSet(tf) {
1119
+ userSet = !!tf;
1120
+ };
1121
+
1122
+ // Sets a tabindex attribute on an element, bends over for IE.
1123
+ function setTabIndex(e, i) {
1124
+ e.setAttribute(!/*@cc_on!@*/false ? "tabIndex" : "tabindex", i);
1125
+ e.tabIndex = i;
1126
+ };
1127
+
1128
+ (function() {
1129
+ if(html5Shim || hideInput) {
1130
+ addClass(inp, "fd-form-element-hidden");
1131
+ } else {
1132
+ addEvent(inp, 'change', onInputChange);
1133
+ };
1134
+
1135
+ // Add stepUp & stepDown methods to input element if using the html5Shim
1136
+ if(html5Shim) {
1137
+ inp.stepUp = function(n) { increment(n||1); };
1138
+ inp.stepDown = function(n) { increment(n||-1); };
1139
+ };
1140
+
1141
+ outerWrapper = document.createElement('span');
1142
+ outerWrapper.className = "fd-slider" + (vertical ? "-vertical " : " ") + (!html5Shim ? " fd-slider-no-value " : "") + classNames;
1143
+ outerWrapper.id = "fd-slider-" + inp.id;
1144
+
1145
+ if(vertical && inpHeight) {
1146
+ outerWrapper.style.height = inpHeight + "px";
1147
+ };
1148
+
1149
+ wrapper = document.createElement('span');
1150
+ wrapper.className = "fd-slider-inner";
1151
+
1152
+ bar = document.createElement('span');
1153
+ bar.className = "fd-slider-bar";
1154
+
1155
+ if(!noRangeBar) {
1156
+ rangeBar = document.createElement('span');
1157
+ rangeBar.className = "fd-slider-range";
1158
+ };
1159
+
1160
+ if(fullARIA) {
1161
+ handle = document.createElement('span');
1162
+ } else {
1163
+ handle = document.createElement('a');
1164
+ handle.setAttribute("href", "#");
1165
+ addEvent(handle, "click", stopEvent);
1166
+ };
1167
+
1168
+ setTabIndex(handle, 0);
1169
+
1170
+ handle.className = "fd-slider-handle";
1171
+ handle.appendChild(document.createTextNode(String.fromCharCode(160)));
1172
+
1173
+ outerWrapper.appendChild(wrapper);
1174
+ if(!noRangeBar) {
1175
+ outerWrapper.appendChild(rangeBar);
1176
+ };
1177
+ outerWrapper.appendChild(bar);
1178
+ outerWrapper.appendChild(handle);
1179
+
1180
+ inp.parentNode.insertBefore(outerWrapper, inp);
1181
+
1182
+ /*@cc_on@*/
1183
+ /*@if(@_win32)
1184
+ handle.unselectable = "on";
1185
+ if(!noRangeBar) rangeBar.unselectable = "on";
1186
+ bar.unselectable = "on";
1187
+ wrapper.unselectable = "on";
1188
+ outerWrapper.unselectable = "on";
1189
+ /*@end@*/
1190
+
1191
+ // Add ARIA accessibility info programmatically
1192
+ outerWrapper.setAttribute("role", "application");
1193
+
1194
+ handle.setAttribute("role", "slider");
1195
+ handle.setAttribute("aria-valuemin", tagName == "select" ? inp.options[0].value : min);
1196
+ handle.setAttribute("aria-valuemax", tagName == "select" ? inp.options[inp.options.length - 1].value : max);
1197
+
1198
+ var lbl = findLabel();
1199
+ if(lbl) {
1200
+ handle.setAttribute("aria-labelledby", lbl.id);
1201
+ handle.id = "fd-slider-handle-" + inp.id;
1202
+ /*@cc_on
1203
+ /*@if(@_win32)
1204
+ lbl.setAttribute("htmlFor", handle.id);
1205
+ @else @*/
1206
+ lbl.setAttribute("for", handle.id);
1207
+ /*@end
1208
+ @*/
1209
+ };
1210
+
1211
+ // Are there page instructions
1212
+ if(document.getElementById(describedBy)) {
1213
+ handle.setAttribute("aria-describedby", describedBy);
1214
+ };
1215
+
1216
+ // Is the form element initially disabled
1217
+ if(inp.getAttribute("disabled") == true) {
1218
+ disableSlider(true);
1219
+ } else {
1220
+ enableSlider(true);
1221
+ };
1222
+
1223
+ // Does an initial form element value mean the user has set a valid value?
1224
+ // Also called onload in case browsers have automatically set the input value
1225
+ if(varSetRules.onvalue) {
1226
+ userSet = true;
1227
+ checkValue(tagName == "input" ? parseFloat(inp.value) : inp.selectedIndex);
1228
+ };
1229
+
1230
+ if(inp.form) {
1231
+ addEvent(inp.form, "reset", onReset);
1232
+ };
1233
+
1234
+ updateAriaValues();
1235
+ callback("create");
1236
+ redraw();
1237
+ })();
1238
+
1239
+ return {
1240
+ onResize: function(e) { if(outerWrapper.offsetHeight != sliderH || outerWrapper.offsetWidth != sliderW) { redraw(); }; },
1241
+ destroy: function() { destroySlider(); },
1242
+ reset: function() { valueToPixels(tagName == "input" ? parseFloat(inp.value) : inp.selectedIndex); },
1243
+ stepUp: function(n) { increment(Math.abs(n)||1); },
1244
+ stepDown: function(n) { increment(-Math.abs(n)||-1); },
1245
+ increment: function(n) { increment(n); },
1246
+ disable: function() { disableSlider(); },
1247
+ enable: function() { enableSlider(); },
1248
+ setRange: function(mi, mx) { setSliderRange(mi, mx); },
1249
+ getValueSet: function() { return !!userSet; },
1250
+ setValueSet: function(tf) { valueSet(tf); },
1251
+ checkValue: function() { if(varSetRules.onvalue) { userSet = true; checkValue(tagName == "input" ? parseFloat(inp.value) : inp.selectedIndex); }; updateAriaValues(); redraw(); }
1252
+ };
1253
+ };
1254
+
1255
+ addEvent(window, "load", init);
1256
+ addEvent(window, "load", function() { setTimeout(function() { var slider; for(slider in sliders) { sliders[slider].checkValue(); } }, 0); });
1257
+ addEvent(window, "resize", resize);
1258
+ addEvent(window, "unload", unload);
1259
+
1260
+ // Have we been passed JSON within the including script tag
1261
+ (function() {
1262
+ var scriptFiles = document.getElementsByTagName('script'),
1263
+ scriptInner = String(scriptFiles[scriptFiles.length - 1].innerHTML).replace(/[\n\r\s\t]+/g, " ").replace(/^\s+/, "").replace(/\s+$/, ""),
1264
+ json = parseJSON(scriptInner);
1265
+
1266
+ if(typeof json === "object" && !("err" in json)) {
1267
+ affectJSON(json);
1268
+ };
1269
+ })();
1270
+
1271
+ // Add oldie class if needed for IE < 9
1272
+ /*@cc_on
1273
+ @if (@_jscript_version < 9)
1274
+ addClass(document.documentElement, "oldie");
1275
+ @end
1276
+ @*/
1277
+
1278
+ return {
1279
+ createSlider: function(opts) { return createSlider(opts); },
1280
+ onDomReady: function() { onDomReady(); },
1281
+ destroyAll: function() { destroyAllsliders(); },
1282
+ destroySlider: function(id) { return destroySingleSlider(id); },
1283
+ redrawAll: function() { resize(); },
1284
+ addEvent: addEvent,
1285
+ removeEvent: removeEvent,
1286
+ stopEvent: stopEvent,
1287
+ increment: function(id, numSteps) { if(!sliderExists(id)) { return false; }; sliders[id].increment(numSteps); },
1288
+ stepUp: function(id, n) { if(!sliderExists(id)) { return false; }; sliders[id].stepUp(Math.abs(n)||1); },
1289
+ stepDown: function(id, n) { if(!sliderExists(id)) { return false; }; sliders[id].stepDown(-Math.abs(n)||-1); },
1290
+ setRange: function(id, newMin, newMax) { if(!sliderExists(id)) { return false; }; sliders[id].setRange(newMin, newMax); },
1291
+ updateSlider: function(id) { if(!sliderExists(id)) { return false; }; sliders[id].reset(); },
1292
+ disable: function(id) { if(!sliderExists(id)) { return false; }; sliders[id].disable(); },
1293
+ enable: function(id) { if(!sliderExists(id)) { return false; }; sliders[id].enable(); },
1294
+ getValueSet: function() { return getValueSet(); },
1295
+ setValueSet: function(a, tf) { if(!sliderExists(id)) { return false; }; setValueSet(a, tf); },
1296
+ setGlobalVariables: function(json) { affectJSON(json); },
1297
+ removeOnload: function() { removeOnLoadEvent(); }
1298
+ };
1299
+ })();