nov-ruby-openid 2.1.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (203) hide show
  1. data/CHANGELOG +215 -0
  2. data/CHANGES-2.1.0 +36 -0
  3. data/INSTALL +47 -0
  4. data/LICENSE +210 -0
  5. data/NOTICE +2 -0
  6. data/README +81 -0
  7. data/Rakefile +98 -0
  8. data/UPGRADE +127 -0
  9. data/VERSION +1 -0
  10. data/contrib/google/ruby-openid-apps-discovery-1.0.gem +0 -0
  11. data/contrib/google/ruby-openid-apps-discovery-1.01.gem +0 -0
  12. data/examples/README +32 -0
  13. data/examples/active_record_openid_store/README +58 -0
  14. data/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb +24 -0
  15. data/examples/active_record_openid_store/XXX_upgrade_open_id_store.rb +26 -0
  16. data/examples/active_record_openid_store/init.rb +8 -0
  17. data/examples/active_record_openid_store/lib/association.rb +10 -0
  18. data/examples/active_record_openid_store/lib/nonce.rb +3 -0
  19. data/examples/active_record_openid_store/lib/open_id_setting.rb +4 -0
  20. data/examples/active_record_openid_store/lib/openid_ar_store.rb +57 -0
  21. data/examples/active_record_openid_store/test/store_test.rb +212 -0
  22. data/examples/discover +49 -0
  23. data/examples/rails_openid/README +153 -0
  24. data/examples/rails_openid/Rakefile +10 -0
  25. data/examples/rails_openid/app/controllers/application.rb +4 -0
  26. data/examples/rails_openid/app/controllers/consumer_controller.rb +122 -0
  27. data/examples/rails_openid/app/controllers/login_controller.rb +45 -0
  28. data/examples/rails_openid/app/controllers/server_controller.rb +265 -0
  29. data/examples/rails_openid/app/helpers/application_helper.rb +3 -0
  30. data/examples/rails_openid/app/helpers/login_helper.rb +2 -0
  31. data/examples/rails_openid/app/helpers/server_helper.rb +9 -0
  32. data/examples/rails_openid/app/views/consumer/index.rhtml +81 -0
  33. data/examples/rails_openid/app/views/layouts/server.rhtml +68 -0
  34. data/examples/rails_openid/app/views/login/index.rhtml +56 -0
  35. data/examples/rails_openid/app/views/server/decide.rhtml +26 -0
  36. data/examples/rails_openid/config/boot.rb +19 -0
  37. data/examples/rails_openid/config/database.yml +74 -0
  38. data/examples/rails_openid/config/environment.rb +54 -0
  39. data/examples/rails_openid/config/environments/development.rb +19 -0
  40. data/examples/rails_openid/config/environments/production.rb +19 -0
  41. data/examples/rails_openid/config/environments/test.rb +19 -0
  42. data/examples/rails_openid/config/routes.rb +24 -0
  43. data/examples/rails_openid/doc/README_FOR_APP +2 -0
  44. data/examples/rails_openid/public/.htaccess +40 -0
  45. data/examples/rails_openid/public/404.html +8 -0
  46. data/examples/rails_openid/public/500.html +8 -0
  47. data/examples/rails_openid/public/dispatch.cgi +12 -0
  48. data/examples/rails_openid/public/dispatch.fcgi +26 -0
  49. data/examples/rails_openid/public/dispatch.rb +12 -0
  50. data/examples/rails_openid/public/favicon.ico +0 -0
  51. data/examples/rails_openid/public/images/openid_login_bg.gif +0 -0
  52. data/examples/rails_openid/public/javascripts/controls.js +750 -0
  53. data/examples/rails_openid/public/javascripts/dragdrop.js +584 -0
  54. data/examples/rails_openid/public/javascripts/effects.js +854 -0
  55. data/examples/rails_openid/public/javascripts/prototype.js +1785 -0
  56. data/examples/rails_openid/public/robots.txt +1 -0
  57. data/examples/rails_openid/script/about +3 -0
  58. data/examples/rails_openid/script/breakpointer +3 -0
  59. data/examples/rails_openid/script/console +3 -0
  60. data/examples/rails_openid/script/destroy +3 -0
  61. data/examples/rails_openid/script/generate +3 -0
  62. data/examples/rails_openid/script/performance/benchmarker +3 -0
  63. data/examples/rails_openid/script/performance/profiler +3 -0
  64. data/examples/rails_openid/script/plugin +3 -0
  65. data/examples/rails_openid/script/process/reaper +3 -0
  66. data/examples/rails_openid/script/process/spawner +3 -0
  67. data/examples/rails_openid/script/process/spinner +3 -0
  68. data/examples/rails_openid/script/runner +3 -0
  69. data/examples/rails_openid/script/server +3 -0
  70. data/examples/rails_openid/test/functional/login_controller_test.rb +18 -0
  71. data/examples/rails_openid/test/functional/server_controller_test.rb +18 -0
  72. data/examples/rails_openid/test/test_helper.rb +28 -0
  73. data/lib/hmac/hmac.rb +112 -0
  74. data/lib/hmac/sha1.rb +11 -0
  75. data/lib/hmac/sha2.rb +25 -0
  76. data/lib/openid.rb +20 -0
  77. data/lib/openid/association.rb +249 -0
  78. data/lib/openid/consumer.rb +395 -0
  79. data/lib/openid/consumer/associationmanager.rb +344 -0
  80. data/lib/openid/consumer/checkid_request.rb +186 -0
  81. data/lib/openid/consumer/discovery.rb +497 -0
  82. data/lib/openid/consumer/discovery_manager.rb +123 -0
  83. data/lib/openid/consumer/html_parse.rb +134 -0
  84. data/lib/openid/consumer/idres.rb +523 -0
  85. data/lib/openid/consumer/responses.rb +148 -0
  86. data/lib/openid/cryptutil.rb +115 -0
  87. data/lib/openid/dh.rb +89 -0
  88. data/lib/openid/extension.rb +39 -0
  89. data/lib/openid/extensions/ax.rb +539 -0
  90. data/lib/openid/extensions/oauth.rb +91 -0
  91. data/lib/openid/extensions/pape.rb +179 -0
  92. data/lib/openid/extensions/sreg.rb +277 -0
  93. data/lib/openid/extensions/ui.rb +53 -0
  94. data/lib/openid/extras.rb +11 -0
  95. data/lib/openid/fetchers.rb +258 -0
  96. data/lib/openid/kvform.rb +136 -0
  97. data/lib/openid/kvpost.rb +58 -0
  98. data/lib/openid/message.rb +553 -0
  99. data/lib/openid/protocolerror.rb +8 -0
  100. data/lib/openid/server.rb +1544 -0
  101. data/lib/openid/store/filesystem.rb +271 -0
  102. data/lib/openid/store/interface.rb +75 -0
  103. data/lib/openid/store/memcache.rb +107 -0
  104. data/lib/openid/store/memory.rb +84 -0
  105. data/lib/openid/store/nonce.rb +68 -0
  106. data/lib/openid/trustroot.rb +349 -0
  107. data/lib/openid/urinorm.rb +75 -0
  108. data/lib/openid/util.rb +110 -0
  109. data/lib/openid/yadis/accept.rb +148 -0
  110. data/lib/openid/yadis/constants.rb +21 -0
  111. data/lib/openid/yadis/discovery.rb +153 -0
  112. data/lib/openid/yadis/filters.rb +205 -0
  113. data/lib/openid/yadis/htmltokenizer.rb +305 -0
  114. data/lib/openid/yadis/parsehtml.rb +45 -0
  115. data/lib/openid/yadis/services.rb +42 -0
  116. data/lib/openid/yadis/xrds.rb +155 -0
  117. data/lib/openid/yadis/xri.rb +90 -0
  118. data/lib/openid/yadis/xrires.rb +99 -0
  119. data/setup.rb +1551 -0
  120. data/test/data/accept.txt +124 -0
  121. data/test/data/dh.txt +29 -0
  122. data/test/data/example-xrds.xml +14 -0
  123. data/test/data/linkparse.txt +587 -0
  124. data/test/data/n2b64 +650 -0
  125. data/test/data/test1-discover.txt +137 -0
  126. data/test/data/test1-parsehtml.txt +152 -0
  127. data/test/data/test_discover/malformed_meta_tag.html +19 -0
  128. data/test/data/test_discover/openid.html +11 -0
  129. data/test/data/test_discover/openid2.html +11 -0
  130. data/test/data/test_discover/openid2_xrds.xml +12 -0
  131. data/test/data/test_discover/openid2_xrds_no_local_id.xml +11 -0
  132. data/test/data/test_discover/openid_1_and_2.html +11 -0
  133. data/test/data/test_discover/openid_1_and_2_xrds.xml +16 -0
  134. data/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml +17 -0
  135. data/test/data/test_discover/openid_and_yadis.html +12 -0
  136. data/test/data/test_discover/openid_no_delegate.html +10 -0
  137. data/test/data/test_discover/openid_utf8.html +11 -0
  138. data/test/data/test_discover/yadis_0entries.xml +12 -0
  139. data/test/data/test_discover/yadis_2_bad_local_id.xml +15 -0
  140. data/test/data/test_discover/yadis_2entries_delegate.xml +22 -0
  141. data/test/data/test_discover/yadis_2entries_idp.xml +21 -0
  142. data/test/data/test_discover/yadis_another_delegate.xml +14 -0
  143. data/test/data/test_discover/yadis_idp.xml +12 -0
  144. data/test/data/test_discover/yadis_idp_delegate.xml +13 -0
  145. data/test/data/test_discover/yadis_no_delegate.xml +11 -0
  146. data/test/data/test_xrds/=j3h.2007.11.14.xrds +25 -0
  147. data/test/data/test_xrds/README +12 -0
  148. data/test/data/test_xrds/delegated-20060809-r1.xrds +34 -0
  149. data/test/data/test_xrds/delegated-20060809-r2.xrds +34 -0
  150. data/test/data/test_xrds/delegated-20060809.xrds +34 -0
  151. data/test/data/test_xrds/no-xrd.xml +7 -0
  152. data/test/data/test_xrds/not-xrds.xml +2 -0
  153. data/test/data/test_xrds/prefixsometimes.xrds +34 -0
  154. data/test/data/test_xrds/ref.xrds +109 -0
  155. data/test/data/test_xrds/sometimesprefix.xrds +34 -0
  156. data/test/data/test_xrds/spoof1.xrds +25 -0
  157. data/test/data/test_xrds/spoof2.xrds +25 -0
  158. data/test/data/test_xrds/spoof3.xrds +37 -0
  159. data/test/data/test_xrds/status222.xrds +9 -0
  160. data/test/data/test_xrds/subsegments.xrds +58 -0
  161. data/test/data/test_xrds/valid-populated-xrds.xml +39 -0
  162. data/test/data/trustroot.txt +153 -0
  163. data/test/data/urinorm.txt +79 -0
  164. data/test/discoverdata.rb +131 -0
  165. data/test/test_accept.rb +170 -0
  166. data/test/test_association.rb +266 -0
  167. data/test/test_associationmanager.rb +917 -0
  168. data/test/test_ax.rb +690 -0
  169. data/test/test_checkid_request.rb +294 -0
  170. data/test/test_consumer.rb +257 -0
  171. data/test/test_cryptutil.rb +119 -0
  172. data/test/test_dh.rb +86 -0
  173. data/test/test_discover.rb +852 -0
  174. data/test/test_discovery_manager.rb +262 -0
  175. data/test/test_extension.rb +46 -0
  176. data/test/test_extras.rb +35 -0
  177. data/test/test_fetchers.rb +565 -0
  178. data/test/test_filters.rb +270 -0
  179. data/test/test_idres.rb +963 -0
  180. data/test/test_kvform.rb +165 -0
  181. data/test/test_kvpost.rb +65 -0
  182. data/test/test_linkparse.rb +101 -0
  183. data/test/test_message.rb +1116 -0
  184. data/test/test_nonce.rb +89 -0
  185. data/test/test_oauth.rb +175 -0
  186. data/test/test_openid_yadis.rb +178 -0
  187. data/test/test_pape.rb +247 -0
  188. data/test/test_parsehtml.rb +80 -0
  189. data/test/test_responses.rb +63 -0
  190. data/test/test_server.rb +2457 -0
  191. data/test/test_sreg.rb +479 -0
  192. data/test/test_stores.rb +298 -0
  193. data/test/test_trustroot.rb +113 -0
  194. data/test/test_ui.rb +93 -0
  195. data/test/test_urinorm.rb +35 -0
  196. data/test/test_util.rb +145 -0
  197. data/test/test_xrds.rb +169 -0
  198. data/test/test_xri.rb +48 -0
  199. data/test/test_xrires.rb +63 -0
  200. data/test/test_yadis_discovery.rb +220 -0
  201. data/test/testutil.rb +127 -0
  202. data/test/util.rb +53 -0
  203. metadata +336 -0
@@ -0,0 +1,19 @@
1
+ # Settings specified here will take precedence over those in config/environment.rb
2
+
3
+ # The test environment is used exclusively to run your application's
4
+ # test suite. You never need to work with it otherwise. Remember that
5
+ # your test database is "scratch space" for the test suite and is wiped
6
+ # and recreated between test runs. Don't rely on the data there!
7
+ config.cache_classes = true
8
+
9
+ # Log error messages when you accidentally call methods on nil.
10
+ config.whiny_nils = true
11
+
12
+ # Show full error reports and disable caching
13
+ config.action_controller.consider_all_requests_local = true
14
+ config.action_controller.perform_caching = false
15
+
16
+ # Tell ActionMailer not to deliver emails to the real world.
17
+ # The :test delivery method accumulates sent emails in the
18
+ # ActionMailer::Base.deliveries array.
19
+ config.action_mailer.delivery_method = :test
@@ -0,0 +1,24 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ # Add your own custom routes here.
3
+ # The priority is based upon order of creation: first created -> highest priority.
4
+
5
+ # Here's a sample route:
6
+ # map.connect 'products/:id', :controller => 'catalog', :action => 'view'
7
+ # Keep in mind you can assign values other than :controller and :action
8
+
9
+ # You can have the root of your site routed by hooking up ''
10
+ # -- just remember to delete public/index.html.
11
+ # map.connect '', :controller => "welcome"
12
+
13
+ map.connect '', :controller => 'login'
14
+ map.connect 'server/xrds', :controller => 'server', :action => 'idp_xrds'
15
+ map.connect 'user/:username', :controller => 'server', :action => 'user_page'
16
+ map.connect 'user/:username/xrds', :controller => 'server', :action => 'user_xrds'
17
+
18
+ # Allow downloading Web Service WSDL as a file with an extension
19
+ # instead of a file named 'wsdl'
20
+ map.connect ':controller/service.wsdl', :action => 'wsdl'
21
+
22
+ # Install the default route as the lowest priority.
23
+ map.connect ':controller/:action/:id'
24
+ end
@@ -0,0 +1,2 @@
1
+ Use this README file to introduce your application and point to useful places in the API for learning more.
2
+ Run "rake appdoc" to generate API documentation for your models and controllers.
@@ -0,0 +1,40 @@
1
+ # General Apache options
2
+ AddHandler fastcgi-script .fcgi
3
+ AddHandler cgi-script .cgi
4
+ Options +FollowSymLinks +ExecCGI
5
+
6
+ # If you don't want Rails to look in certain directories,
7
+ # use the following rewrite rules so that Apache won't rewrite certain requests
8
+ #
9
+ # Example:
10
+ # RewriteCond %{REQUEST_URI} ^/notrails.*
11
+ # RewriteRule .* - [L]
12
+
13
+ # Redirect all requests not available on the filesystem to Rails
14
+ # By default the cgi dispatcher is used which is very slow
15
+ #
16
+ # For better performance replace the dispatcher with the fastcgi one
17
+ #
18
+ # Example:
19
+ # RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
20
+ RewriteEngine On
21
+
22
+ # If your Rails application is accessed via an Alias directive,
23
+ # then you MUST also set the RewriteBase in this htaccess file.
24
+ #
25
+ # Example:
26
+ # Alias /myrailsapp /path/to/myrailsapp/public
27
+ # RewriteBase /myrailsapp
28
+
29
+ RewriteRule ^$ index.html [QSA]
30
+ RewriteRule ^([^.]+)$ $1.html [QSA]
31
+ RewriteCond %{REQUEST_FILENAME} !-f
32
+ RewriteRule ^(.*)$ dispatch.cgi [QSA,L]
33
+
34
+ # In case Rails experiences terminal errors
35
+ # Instead of displaying this message you can supply a file here which will be rendered instead
36
+ #
37
+ # Example:
38
+ # ErrorDocument 500 /500.html
39
+
40
+ ErrorDocument 500 "<h2>Application error</h2>Rails application failed to start properly"
@@ -0,0 +1,8 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2
+ "http://www.w3.org/TR/html4/loose.dtd">
3
+ <html>
4
+ <body>
5
+ <h1>File not found</h1>
6
+ <p>Change this error message for pages not found in public/404.html</p>
7
+ </body>
8
+ </html>
@@ -0,0 +1,8 @@
1
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
2
+ "http://www.w3.org/TR/html4/loose.dtd">
3
+ <html>
4
+ <body>
5
+ <h1>Application error (Apache)</h1>
6
+ <p>Change this error message for exceptions thrown outside of an action (like in Dispatcher setups or broken Ruby code) in public/500.html</p>
7
+ </body>
8
+ </html>
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/ruby1.8
2
+
3
+ #!/usr/local/bin/ruby
4
+
5
+ require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
6
+
7
+ # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
8
+ # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
9
+ require "dispatcher"
10
+
11
+ ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
12
+ Dispatcher.dispatch
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/ruby1.8
2
+
3
+ #!/usr/local/bin/ruby
4
+ #
5
+ # You may specify the path to the FastCGI crash log (a log of unhandled
6
+ # exceptions which forced the FastCGI instance to exit, great for debugging)
7
+ # and the number of requests to process before running garbage collection.
8
+ #
9
+ # By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
10
+ # and the GC period is nil (turned off). A reasonable number of requests
11
+ # could range from 10-100 depending on the memory footprint of your app.
12
+ #
13
+ # Example:
14
+ # # Default log path, normal GC behavior.
15
+ # RailsFCGIHandler.process!
16
+ #
17
+ # # Default log path, 50 requests between GC.
18
+ # RailsFCGIHandler.process! nil, 50
19
+ #
20
+ # # Custom log path, normal GC behavior.
21
+ # RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
22
+ #
23
+ require File.dirname(__FILE__) + "/../config/environment"
24
+ require 'fcgi_handler'
25
+
26
+ RailsFCGIHandler.process!
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/ruby1.8
2
+
3
+ #!/usr/local/bin/ruby
4
+
5
+ require File.dirname(__FILE__) + "/../config/environment" unless defined?(RAILS_ROOT)
6
+
7
+ # If you're using RubyGems and mod_ruby, this require should be changed to an absolute path one, like:
8
+ # "/usr/local/lib/ruby/gems/1.8/gems/rails-0.8.0/lib/dispatcher" -- otherwise performance is severely impaired
9
+ require "dispatcher"
10
+
11
+ ADDITIONAL_LOAD_PATHS.reverse.each { |dir| $:.unshift(dir) if File.directory?(dir) } if defined?(Apache::RubyRun)
12
+ Dispatcher.dispatch
@@ -0,0 +1,750 @@
1
+ // Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
2
+ // (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
3
+ // (c) 2005 Jon Tirsen (http://www.tirsen.com)
4
+ // Contributors:
5
+ // Richard Livsey
6
+ // Rahul Bhargava
7
+ // Rob Wills
8
+ //
9
+ // See scriptaculous.js for full license.
10
+
11
+ // Autocompleter.Base handles all the autocompletion functionality
12
+ // that's independent of the data source for autocompletion. This
13
+ // includes drawing the autocompletion menu, observing keyboard
14
+ // and mouse events, and similar.
15
+ //
16
+ // Specific autocompleters need to provide, at the very least,
17
+ // a getUpdatedChoices function that will be invoked every time
18
+ // the text inside the monitored textbox changes. This method
19
+ // should get the text for which to provide autocompletion by
20
+ // invoking this.getToken(), NOT by directly accessing
21
+ // this.element.value. This is to allow incremental tokenized
22
+ // autocompletion. Specific auto-completion logic (AJAX, etc)
23
+ // belongs in getUpdatedChoices.
24
+ //
25
+ // Tokenized incremental autocompletion is enabled automatically
26
+ // when an autocompleter is instantiated with the 'tokens' option
27
+ // in the options parameter, e.g.:
28
+ // new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
29
+ // will incrementally autocomplete with a comma as the token.
30
+ // Additionally, ',' in the above example can be replaced with
31
+ // a token array, e.g. { tokens: [',', '\n'] } which
32
+ // enables autocompletion on multiple tokens. This is most
33
+ // useful when one of the tokens is \n (a newline), as it
34
+ // allows smart autocompletion after linebreaks.
35
+
36
+ var Autocompleter = {}
37
+ Autocompleter.Base = function() {};
38
+ Autocompleter.Base.prototype = {
39
+ baseInitialize: function(element, update, options) {
40
+ this.element = $(element);
41
+ this.update = $(update);
42
+ this.hasFocus = false;
43
+ this.changed = false;
44
+ this.active = false;
45
+ this.index = 0;
46
+ this.entryCount = 0;
47
+
48
+ if (this.setOptions)
49
+ this.setOptions(options);
50
+ else
51
+ this.options = options || {};
52
+
53
+ this.options.paramName = this.options.paramName || this.element.name;
54
+ this.options.tokens = this.options.tokens || [];
55
+ this.options.frequency = this.options.frequency || 0.4;
56
+ this.options.minChars = this.options.minChars || 1;
57
+ this.options.onShow = this.options.onShow ||
58
+ function(element, update){
59
+ if(!update.style.position || update.style.position=='absolute') {
60
+ update.style.position = 'absolute';
61
+ Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
62
+ }
63
+ Effect.Appear(update,{duration:0.15});
64
+ };
65
+ this.options.onHide = this.options.onHide ||
66
+ function(element, update){ new Effect.Fade(update,{duration:0.15}) };
67
+
68
+ if (typeof(this.options.tokens) == 'string')
69
+ this.options.tokens = new Array(this.options.tokens);
70
+
71
+ this.observer = null;
72
+
73
+ this.element.setAttribute('autocomplete','off');
74
+
75
+ Element.hide(this.update);
76
+
77
+ Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
78
+ Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
79
+ },
80
+
81
+ show: function() {
82
+ if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
83
+ if(!this.iefix &&
84
+ (navigator.appVersion.indexOf('MSIE')>0) &&
85
+ (navigator.userAgent.indexOf('Opera')<0) &&
86
+ (Element.getStyle(this.update, 'position')=='absolute')) {
87
+ new Insertion.After(this.update,
88
+ '<iframe id="' + this.update.id + '_iefix" '+
89
+ 'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
90
+ 'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
91
+ this.iefix = $(this.update.id+'_iefix');
92
+ }
93
+ if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
94
+ },
95
+
96
+ fixIEOverlapping: function() {
97
+ Position.clone(this.update, this.iefix);
98
+ this.iefix.style.zIndex = 1;
99
+ this.update.style.zIndex = 2;
100
+ Element.show(this.iefix);
101
+ },
102
+
103
+ hide: function() {
104
+ this.stopIndicator();
105
+ if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
106
+ if(this.iefix) Element.hide(this.iefix);
107
+ },
108
+
109
+ startIndicator: function() {
110
+ if(this.options.indicator) Element.show(this.options.indicator);
111
+ },
112
+
113
+ stopIndicator: function() {
114
+ if(this.options.indicator) Element.hide(this.options.indicator);
115
+ },
116
+
117
+ onKeyPress: function(event) {
118
+ if(this.active)
119
+ switch(event.keyCode) {
120
+ case Event.KEY_TAB:
121
+ case Event.KEY_RETURN:
122
+ this.selectEntry();
123
+ Event.stop(event);
124
+ case Event.KEY_ESC:
125
+ this.hide();
126
+ this.active = false;
127
+ Event.stop(event);
128
+ return;
129
+ case Event.KEY_LEFT:
130
+ case Event.KEY_RIGHT:
131
+ return;
132
+ case Event.KEY_UP:
133
+ this.markPrevious();
134
+ this.render();
135
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
136
+ return;
137
+ case Event.KEY_DOWN:
138
+ this.markNext();
139
+ this.render();
140
+ if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
141
+ return;
142
+ }
143
+ else
144
+ if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN)
145
+ return;
146
+
147
+ this.changed = true;
148
+ this.hasFocus = true;
149
+
150
+ if(this.observer) clearTimeout(this.observer);
151
+ this.observer =
152
+ setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
153
+ },
154
+
155
+ onHover: function(event) {
156
+ var element = Event.findElement(event, 'LI');
157
+ if(this.index != element.autocompleteIndex)
158
+ {
159
+ this.index = element.autocompleteIndex;
160
+ this.render();
161
+ }
162
+ Event.stop(event);
163
+ },
164
+
165
+ onClick: function(event) {
166
+ var element = Event.findElement(event, 'LI');
167
+ this.index = element.autocompleteIndex;
168
+ this.selectEntry();
169
+ this.hide();
170
+ },
171
+
172
+ onBlur: function(event) {
173
+ // needed to make click events working
174
+ setTimeout(this.hide.bind(this), 250);
175
+ this.hasFocus = false;
176
+ this.active = false;
177
+ },
178
+
179
+ render: function() {
180
+ if(this.entryCount > 0) {
181
+ for (var i = 0; i < this.entryCount; i++)
182
+ this.index==i ?
183
+ Element.addClassName(this.getEntry(i),"selected") :
184
+ Element.removeClassName(this.getEntry(i),"selected");
185
+
186
+ if(this.hasFocus) {
187
+ this.show();
188
+ this.active = true;
189
+ }
190
+ } else {
191
+ this.active = false;
192
+ this.hide();
193
+ }
194
+ },
195
+
196
+ markPrevious: function() {
197
+ if(this.index > 0) this.index--
198
+ else this.index = this.entryCount-1;
199
+ },
200
+
201
+ markNext: function() {
202
+ if(this.index < this.entryCount-1) this.index++
203
+ else this.index = 0;
204
+ },
205
+
206
+ getEntry: function(index) {
207
+ return this.update.firstChild.childNodes[index];
208
+ },
209
+
210
+ getCurrentEntry: function() {
211
+ return this.getEntry(this.index);
212
+ },
213
+
214
+ selectEntry: function() {
215
+ this.active = false;
216
+ this.updateElement(this.getCurrentEntry());
217
+ },
218
+
219
+ updateElement: function(selectedElement) {
220
+ if (this.options.updateElement) {
221
+ this.options.updateElement(selectedElement);
222
+ return;
223
+ }
224
+
225
+ var value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
226
+ var lastTokenPos = this.findLastToken();
227
+ if (lastTokenPos != -1) {
228
+ var newValue = this.element.value.substr(0, lastTokenPos + 1);
229
+ var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
230
+ if (whitespace)
231
+ newValue += whitespace[0];
232
+ this.element.value = newValue + value;
233
+ } else {
234
+ this.element.value = value;
235
+ }
236
+ this.element.focus();
237
+
238
+ if (this.options.afterUpdateElement)
239
+ this.options.afterUpdateElement(this.element, selectedElement);
240
+ },
241
+
242
+ updateChoices: function(choices) {
243
+ if(!this.changed && this.hasFocus) {
244
+ this.update.innerHTML = choices;
245
+ Element.cleanWhitespace(this.update);
246
+ Element.cleanWhitespace(this.update.firstChild);
247
+
248
+ if(this.update.firstChild && this.update.firstChild.childNodes) {
249
+ this.entryCount =
250
+ this.update.firstChild.childNodes.length;
251
+ for (var i = 0; i < this.entryCount; i++) {
252
+ var entry = this.getEntry(i);
253
+ entry.autocompleteIndex = i;
254
+ this.addObservers(entry);
255
+ }
256
+ } else {
257
+ this.entryCount = 0;
258
+ }
259
+
260
+ this.stopIndicator();
261
+
262
+ this.index = 0;
263
+ this.render();
264
+ }
265
+ },
266
+
267
+ addObservers: function(element) {
268
+ Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
269
+ Event.observe(element, "click", this.onClick.bindAsEventListener(this));
270
+ },
271
+
272
+ onObserverEvent: function() {
273
+ this.changed = false;
274
+ if(this.getToken().length>=this.options.minChars) {
275
+ this.startIndicator();
276
+ this.getUpdatedChoices();
277
+ } else {
278
+ this.active = false;
279
+ this.hide();
280
+ }
281
+ },
282
+
283
+ getToken: function() {
284
+ var tokenPos = this.findLastToken();
285
+ if (tokenPos != -1)
286
+ var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
287
+ else
288
+ var ret = this.element.value;
289
+
290
+ return /\n/.test(ret) ? '' : ret;
291
+ },
292
+
293
+ findLastToken: function() {
294
+ var lastTokenPos = -1;
295
+
296
+ for (var i=0; i<this.options.tokens.length; i++) {
297
+ var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
298
+ if (thisTokenPos > lastTokenPos)
299
+ lastTokenPos = thisTokenPos;
300
+ }
301
+ return lastTokenPos;
302
+ }
303
+ }
304
+
305
+ Ajax.Autocompleter = Class.create();
306
+ Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
307
+ initialize: function(element, update, url, options) {
308
+ this.baseInitialize(element, update, options);
309
+ this.options.asynchronous = true;
310
+ this.options.onComplete = this.onComplete.bind(this);
311
+ this.options.defaultParams = this.options.parameters || null;
312
+ this.url = url;
313
+ },
314
+
315
+ getUpdatedChoices: function() {
316
+ entry = encodeURIComponent(this.options.paramName) + '=' +
317
+ encodeURIComponent(this.getToken());
318
+
319
+ this.options.parameters = this.options.callback ?
320
+ this.options.callback(this.element, entry) : entry;
321
+
322
+ if(this.options.defaultParams)
323
+ this.options.parameters += '&' + this.options.defaultParams;
324
+
325
+ new Ajax.Request(this.url, this.options);
326
+ },
327
+
328
+ onComplete: function(request) {
329
+ this.updateChoices(request.responseText);
330
+ }
331
+
332
+ });
333
+
334
+ // The local array autocompleter. Used when you'd prefer to
335
+ // inject an array of autocompletion options into the page, rather
336
+ // than sending out Ajax queries, which can be quite slow sometimes.
337
+ //
338
+ // The constructor takes four parameters. The first two are, as usual,
339
+ // the id of the monitored textbox, and id of the autocompletion menu.
340
+ // The third is the array you want to autocomplete from, and the fourth
341
+ // is the options block.
342
+ //
343
+ // Extra local autocompletion options:
344
+ // - choices - How many autocompletion choices to offer
345
+ //
346
+ // - partialSearch - If false, the autocompleter will match entered
347
+ // text only at the beginning of strings in the
348
+ // autocomplete array. Defaults to true, which will
349
+ // match text at the beginning of any *word* in the
350
+ // strings in the autocomplete array. If you want to
351
+ // search anywhere in the string, additionally set
352
+ // the option fullSearch to true (default: off).
353
+ //
354
+ // - fullSsearch - Search anywhere in autocomplete array strings.
355
+ //
356
+ // - partialChars - How many characters to enter before triggering
357
+ // a partial match (unlike minChars, which defines
358
+ // how many characters are required to do any match
359
+ // at all). Defaults to 2.
360
+ //
361
+ // - ignoreCase - Whether to ignore case when autocompleting.
362
+ // Defaults to true.
363
+ //
364
+ // It's possible to pass in a custom function as the 'selector'
365
+ // option, if you prefer to write your own autocompletion logic.
366
+ // In that case, the other options above will not apply unless
367
+ // you support them.
368
+
369
+ Autocompleter.Local = Class.create();
370
+ Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
371
+ initialize: function(element, update, array, options) {
372
+ this.baseInitialize(element, update, options);
373
+ this.options.array = array;
374
+ },
375
+
376
+ getUpdatedChoices: function() {
377
+ this.updateChoices(this.options.selector(this));
378
+ },
379
+
380
+ setOptions: function(options) {
381
+ this.options = Object.extend({
382
+ choices: 10,
383
+ partialSearch: true,
384
+ partialChars: 2,
385
+ ignoreCase: true,
386
+ fullSearch: false,
387
+ selector: function(instance) {
388
+ var ret = []; // Beginning matches
389
+ var partial = []; // Inside matches
390
+ var entry = instance.getToken();
391
+ var count = 0;
392
+
393
+ for (var i = 0; i < instance.options.array.length &&
394
+ ret.length < instance.options.choices ; i++) {
395
+
396
+ var elem = instance.options.array[i];
397
+ var foundPos = instance.options.ignoreCase ?
398
+ elem.toLowerCase().indexOf(entry.toLowerCase()) :
399
+ elem.indexOf(entry);
400
+
401
+ while (foundPos != -1) {
402
+ if (foundPos == 0 && elem.length != entry.length) {
403
+ ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
404
+ elem.substr(entry.length) + "</li>");
405
+ break;
406
+ } else if (entry.length >= instance.options.partialChars &&
407
+ instance.options.partialSearch && foundPos != -1) {
408
+ if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
409
+ partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
410
+ elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
411
+ foundPos + entry.length) + "</li>");
412
+ break;
413
+ }
414
+ }
415
+
416
+ foundPos = instance.options.ignoreCase ?
417
+ elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
418
+ elem.indexOf(entry, foundPos + 1);
419
+
420
+ }
421
+ }
422
+ if (partial.length)
423
+ ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
424
+ return "<ul>" + ret.join('') + "</ul>";
425
+ }
426
+ }, options || {});
427
+ }
428
+ });
429
+
430
+ // AJAX in-place editor
431
+ //
432
+ // see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
433
+
434
+ // Use this if you notice weird scrolling problems on some browsers,
435
+ // the DOM might be a bit confused when this gets called so do this
436
+ // waits 1 ms (with setTimeout) until it does the activation
437
+ Field.scrollFreeActivate = function(field) {
438
+ setTimeout(function() {
439
+ Field.activate(field);
440
+ }, 1);
441
+ }
442
+
443
+ Ajax.InPlaceEditor = Class.create();
444
+ Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
445
+ Ajax.InPlaceEditor.prototype = {
446
+ initialize: function(element, url, options) {
447
+ this.url = url;
448
+ this.element = $(element);
449
+
450
+ this.options = Object.extend({
451
+ okText: "ok",
452
+ cancelText: "cancel",
453
+ savingText: "Saving...",
454
+ clickToEditText: "Click to edit",
455
+ okText: "ok",
456
+ rows: 1,
457
+ onComplete: function(transport, element) {
458
+ new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
459
+ },
460
+ onFailure: function(transport) {
461
+ alert("Error communicating with the server: " + transport.responseText.stripTags());
462
+ },
463
+ callback: function(form) {
464
+ return Form.serialize(form);
465
+ },
466
+ handleLineBreaks: true,
467
+ loadingText: 'Loading...',
468
+ savingClassName: 'inplaceeditor-saving',
469
+ loadingClassName: 'inplaceeditor-loading',
470
+ formClassName: 'inplaceeditor-form',
471
+ highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
472
+ highlightendcolor: "#FFFFFF",
473
+ externalControl: null,
474
+ ajaxOptions: {}
475
+ }, options || {});
476
+
477
+ if(!this.options.formId && this.element.id) {
478
+ this.options.formId = this.element.id + "-inplaceeditor";
479
+ if ($(this.options.formId)) {
480
+ // there's already a form with that name, don't specify an id
481
+ this.options.formId = null;
482
+ }
483
+ }
484
+
485
+ if (this.options.externalControl) {
486
+ this.options.externalControl = $(this.options.externalControl);
487
+ }
488
+
489
+ this.originalBackground = Element.getStyle(this.element, 'background-color');
490
+ if (!this.originalBackground) {
491
+ this.originalBackground = "transparent";
492
+ }
493
+
494
+ this.element.title = this.options.clickToEditText;
495
+
496
+ this.onclickListener = this.enterEditMode.bindAsEventListener(this);
497
+ this.mouseoverListener = this.enterHover.bindAsEventListener(this);
498
+ this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
499
+ Event.observe(this.element, 'click', this.onclickListener);
500
+ Event.observe(this.element, 'mouseover', this.mouseoverListener);
501
+ Event.observe(this.element, 'mouseout', this.mouseoutListener);
502
+ if (this.options.externalControl) {
503
+ Event.observe(this.options.externalControl, 'click', this.onclickListener);
504
+ Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
505
+ Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
506
+ }
507
+ },
508
+ enterEditMode: function(evt) {
509
+ if (this.saving) return;
510
+ if (this.editing) return;
511
+ this.editing = true;
512
+ this.onEnterEditMode();
513
+ if (this.options.externalControl) {
514
+ Element.hide(this.options.externalControl);
515
+ }
516
+ Element.hide(this.element);
517
+ this.createForm();
518
+ this.element.parentNode.insertBefore(this.form, this.element);
519
+ Field.scrollFreeActivate(this.editField);
520
+ // stop the event to avoid a page refresh in Safari
521
+ if (evt) {
522
+ Event.stop(evt);
523
+ }
524
+ return false;
525
+ },
526
+ createForm: function() {
527
+ this.form = document.createElement("form");
528
+ this.form.id = this.options.formId;
529
+ Element.addClassName(this.form, this.options.formClassName)
530
+ this.form.onsubmit = this.onSubmit.bind(this);
531
+
532
+ this.createEditField();
533
+
534
+ if (this.options.textarea) {
535
+ var br = document.createElement("br");
536
+ this.form.appendChild(br);
537
+ }
538
+
539
+ okButton = document.createElement("input");
540
+ okButton.type = "submit";
541
+ okButton.value = this.options.okText;
542
+ this.form.appendChild(okButton);
543
+
544
+ cancelLink = document.createElement("a");
545
+ cancelLink.href = "#";
546
+ cancelLink.appendChild(document.createTextNode(this.options.cancelText));
547
+ cancelLink.onclick = this.onclickCancel.bind(this);
548
+ this.form.appendChild(cancelLink);
549
+ },
550
+ hasHTMLLineBreaks: function(string) {
551
+ if (!this.options.handleLineBreaks) return false;
552
+ return string.match(/<br/i) || string.match(/<p>/i);
553
+ },
554
+ convertHTMLLineBreaks: function(string) {
555
+ return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
556
+ },
557
+ createEditField: function() {
558
+ var text;
559
+ if(this.options.loadTextURL) {
560
+ text = this.options.loadingText;
561
+ } else {
562
+ text = this.getText();
563
+ }
564
+
565
+ if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
566
+ this.options.textarea = false;
567
+ var textField = document.createElement("input");
568
+ textField.type = "text";
569
+ textField.name = "value";
570
+ textField.value = text;
571
+ textField.style.backgroundColor = this.options.highlightcolor;
572
+ var size = this.options.size || this.options.cols || 0;
573
+ if (size != 0) textField.size = size;
574
+ this.editField = textField;
575
+ } else {
576
+ this.options.textarea = true;
577
+ var textArea = document.createElement("textarea");
578
+ textArea.name = "value";
579
+ textArea.value = this.convertHTMLLineBreaks(text);
580
+ textArea.rows = this.options.rows;
581
+ textArea.cols = this.options.cols || 40;
582
+ this.editField = textArea;
583
+ }
584
+
585
+ if(this.options.loadTextURL) {
586
+ this.loadExternalText();
587
+ }
588
+ this.form.appendChild(this.editField);
589
+ },
590
+ getText: function() {
591
+ return this.element.innerHTML;
592
+ },
593
+ loadExternalText: function() {
594
+ Element.addClassName(this.form, this.options.loadingClassName);
595
+ this.editField.disabled = true;
596
+ new Ajax.Request(
597
+ this.options.loadTextURL,
598
+ Object.extend({
599
+ asynchronous: true,
600
+ onComplete: this.onLoadedExternalText.bind(this)
601
+ }, this.options.ajaxOptions)
602
+ );
603
+ },
604
+ onLoadedExternalText: function(transport) {
605
+ Element.removeClassName(this.form, this.options.loadingClassName);
606
+ this.editField.disabled = false;
607
+ this.editField.value = transport.responseText.stripTags();
608
+ },
609
+ onclickCancel: function() {
610
+ this.onComplete();
611
+ this.leaveEditMode();
612
+ return false;
613
+ },
614
+ onFailure: function(transport) {
615
+ this.options.onFailure(transport);
616
+ if (this.oldInnerHTML) {
617
+ this.element.innerHTML = this.oldInnerHTML;
618
+ this.oldInnerHTML = null;
619
+ }
620
+ return false;
621
+ },
622
+ onSubmit: function() {
623
+ // onLoading resets these so we need to save them away for the Ajax call
624
+ var form = this.form;
625
+ var value = this.editField.value;
626
+
627
+ // do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
628
+ // which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
629
+ // to be displayed indefinitely
630
+ this.onLoading();
631
+
632
+ new Ajax.Updater(
633
+ {
634
+ success: this.element,
635
+ // don't update on failure (this could be an option)
636
+ failure: null
637
+ },
638
+ this.url,
639
+ Object.extend({
640
+ parameters: this.options.callback(form, value),
641
+ onComplete: this.onComplete.bind(this),
642
+ onFailure: this.onFailure.bind(this)
643
+ }, this.options.ajaxOptions)
644
+ );
645
+ // stop the event to avoid a page refresh in Safari
646
+ if (arguments.length > 1) {
647
+ Event.stop(arguments[0]);
648
+ }
649
+ return false;
650
+ },
651
+ onLoading: function() {
652
+ this.saving = true;
653
+ this.removeForm();
654
+ this.leaveHover();
655
+ this.showSaving();
656
+ },
657
+ showSaving: function() {
658
+ this.oldInnerHTML = this.element.innerHTML;
659
+ this.element.innerHTML = this.options.savingText;
660
+ Element.addClassName(this.element, this.options.savingClassName);
661
+ this.element.style.backgroundColor = this.originalBackground;
662
+ Element.show(this.element);
663
+ },
664
+ removeForm: function() {
665
+ if(this.form) {
666
+ if (this.form.parentNode) Element.remove(this.form);
667
+ this.form = null;
668
+ }
669
+ },
670
+ enterHover: function() {
671
+ if (this.saving) return;
672
+ this.element.style.backgroundColor = this.options.highlightcolor;
673
+ if (this.effect) {
674
+ this.effect.cancel();
675
+ }
676
+ Element.addClassName(this.element, this.options.hoverClassName)
677
+ },
678
+ leaveHover: function() {
679
+ if (this.options.backgroundColor) {
680
+ this.element.style.backgroundColor = this.oldBackground;
681
+ }
682
+ Element.removeClassName(this.element, this.options.hoverClassName)
683
+ if (this.saving) return;
684
+ this.effect = new Effect.Highlight(this.element, {
685
+ startcolor: this.options.highlightcolor,
686
+ endcolor: this.options.highlightendcolor,
687
+ restorecolor: this.originalBackground
688
+ });
689
+ },
690
+ leaveEditMode: function() {
691
+ Element.removeClassName(this.element, this.options.savingClassName);
692
+ this.removeForm();
693
+ this.leaveHover();
694
+ this.element.style.backgroundColor = this.originalBackground;
695
+ Element.show(this.element);
696
+ if (this.options.externalControl) {
697
+ Element.show(this.options.externalControl);
698
+ }
699
+ this.editing = false;
700
+ this.saving = false;
701
+ this.oldInnerHTML = null;
702
+ this.onLeaveEditMode();
703
+ },
704
+ onComplete: function(transport) {
705
+ this.leaveEditMode();
706
+ this.options.onComplete.bind(this)(transport, this.element);
707
+ },
708
+ onEnterEditMode: function() {},
709
+ onLeaveEditMode: function() {},
710
+ dispose: function() {
711
+ if (this.oldInnerHTML) {
712
+ this.element.innerHTML = this.oldInnerHTML;
713
+ }
714
+ this.leaveEditMode();
715
+ Event.stopObserving(this.element, 'click', this.onclickListener);
716
+ Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
717
+ Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
718
+ if (this.options.externalControl) {
719
+ Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
720
+ Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
721
+ Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
722
+ }
723
+ }
724
+ };
725
+
726
+ // Delayed observer, like Form.Element.Observer,
727
+ // but waits for delay after last key input
728
+ // Ideal for live-search fields
729
+
730
+ Form.Element.DelayedObserver = Class.create();
731
+ Form.Element.DelayedObserver.prototype = {
732
+ initialize: function(element, delay, callback) {
733
+ this.delay = delay || 0.5;
734
+ this.element = $(element);
735
+ this.callback = callback;
736
+ this.timer = null;
737
+ this.lastValue = $F(this.element);
738
+ Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
739
+ },
740
+ delayedListener: function(event) {
741
+ if(this.lastValue == $F(this.element)) return;
742
+ if(this.timer) clearTimeout(this.timer);
743
+ this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
744
+ this.lastValue = $F(this.element);
745
+ },
746
+ onTimerEvent: function() {
747
+ this.timer = null;
748
+ this.callback(this.element, $F(this.element));
749
+ }
750
+ };