ajax 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +291 -0
  3. data/Rakefile +49 -0
  4. data/VERSION +1 -0
  5. data/app/controllers/ajax_controller.rb +3 -0
  6. data/app/views/ajax/framework.html.erb +7 -0
  7. data/config/initializers/ajax.rb +14 -0
  8. data/lib/ajax.rb +79 -0
  9. data/lib/ajax/action_controller.rb +154 -0
  10. data/lib/ajax/action_view.rb +56 -0
  11. data/lib/ajax/helpers.rb +15 -0
  12. data/lib/ajax/helpers/request_helper.rb +76 -0
  13. data/lib/ajax/helpers/robot_helper.rb +31 -0
  14. data/lib/ajax/helpers/url_helper.rb +47 -0
  15. data/lib/ajax/railtie.rb +7 -0
  16. data/lib/ajax/routes.rb +12 -0
  17. data/lib/ajax/spec/extension.rb +34 -0
  18. data/lib/ajax/spec/helpers.rb +95 -0
  19. data/lib/ajax/tasks.rb +1 -0
  20. data/lib/rack-ajax.rb +60 -0
  21. data/lib/rack-ajax/decision_tree.rb +60 -0
  22. data/lib/rack-ajax/parser.rb +115 -0
  23. data/public/images/loading-icon-large.gif +0 -0
  24. data/public/images/loading-icon-small.gif +0 -0
  25. data/public/javascripts/ajax.js +529 -0
  26. data/public/javascripts/jquery.address-1.1.js +450 -0
  27. data/public/javascripts/jquery.address-1.1.min.js +11 -0
  28. data/public/javascripts/jquery.address-1.2.js +528 -0
  29. data/public/javascripts/jquery.address-1.2.min.js +25 -0
  30. data/public/javascripts/jquery.address-1.2rc.js +599 -0
  31. data/public/javascripts/jquery.address-1.2rc.min.js +27 -0
  32. data/public/javascripts/jquery.json-2.2.js +178 -0
  33. data/public/javascripts/jquery.json-2.2.min.js +31 -0
  34. data/rails/init.rb +4 -0
  35. data/rails/install.rb +23 -0
  36. data/rails/uninstall.rb +1 -0
  37. data/spec/ajax/helpers_spec.rb +102 -0
  38. data/spec/ajax/request_helper_spec.rb +33 -0
  39. data/spec/integration/ajax_spec.rb +146 -0
  40. data/spec/rack-ajax/parser_spec.rb +62 -0
  41. data/spec/spec.opts +1 -0
  42. data/spec/spec_helper.rb +18 -0
  43. data/tasks/ajax_tasks.rake +15 -0
  44. metadata +106 -0
@@ -0,0 +1,154 @@
1
+ module Ajax
2
+ module ActionController
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+ alias_method_chain :render, :ajax
6
+ alias_method_chain :redirect_to_full_url, :ajax
7
+
8
+ append_after_filter :process_response_headers
9
+ end
10
+ klass.extend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+
15
+ # Set a custom response header if the request is AJAX.
16
+ #
17
+ # Call with <tt>key</tt> and optional <tt>value</tt>. Pass a
18
+ # block to yield a dynamic value.
19
+ #
20
+ # Accepts :only and :except conditions because we create
21
+ # an after_filter.
22
+ def ajax_header(*args, &block)
23
+ return unless Ajax.is_enabled?
24
+
25
+ options = args.extract_options!
26
+ key, value = args.shift, args.shift
27
+ value = block_given? ? Proc.new : value
28
+
29
+ prepend_after_filter(options) do |controller|
30
+ if controller.request.xhr?
31
+ value = value.is_a?(Proc) ? controller.instance_eval(&value) : value
32
+ Ajax.set_header(controller.response, key, value)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Set the layout to use for AJAX requests.
38
+ #
39
+ # By default we look in layouts/ajax/ for this controllers default
40
+ # layout and render that. If it can't be found, the default layout
41
+ # is used.
42
+ def ajax_layout(template_name)
43
+ write_inheritable_attribute(:ajax_layout, template_name)
44
+ end
45
+ end
46
+
47
+ protected
48
+
49
+ # Redirect to hashed URLs unless the path is excepted.
50
+ #
51
+ # Store the URL that we are redirecting to in the session.
52
+ # If we then have a request for the root URL we know
53
+ # to render this URL into it.
54
+ #
55
+ # If redirecting back to the referer, use the referer
56
+ # in the Ajax-Info header because it includes the
57
+ # hashed part of the URL. Otherwise the referer is
58
+ # always the root url.
59
+ #
60
+ # For AJAX requests, respond with an AJAX-suitable
61
+ # redirect.
62
+ def redirect_to_full_url_with_ajax(url, status)
63
+ return redirect_to_full_url_without_ajax(url, status) unless Ajax.is_enabled?
64
+ raise DoubleRenderError if performed?
65
+
66
+ if url == request.headers["Referer"] && !request.headers['Ajax-Info'].blank?
67
+ url = request.headers['Ajax-Info']['referer']
68
+ Ajax.logger.debug("[ajax] using referer #{url} from Ajax-Info")
69
+ end
70
+
71
+ if !Ajax.exclude_path?(url) && !Ajax.is_hashed_url?(url)
72
+ url = Ajax.hashed_url_from_traditional(url)
73
+ Ajax.logger.info("[ajax] rewrote redirect to #{url}")
74
+ end
75
+
76
+ session[:redirected_to] = url
77
+ if request.xhr?
78
+ render(:update) { |page| page.redirect_to(url) }
79
+ else
80
+ redirect_to_full_url_without_ajax(url, status)
81
+ end
82
+ end
83
+
84
+ # Convert the Ajax-Info hash to JSON before the request is sent.
85
+ def process_response_headers
86
+ case response.headers['Ajax-Info']
87
+ when Hash
88
+ response.headers['Ajax-Info'] = response.headers['Ajax-Info'].to_json
89
+ end
90
+ end
91
+
92
+ #
93
+ # Intercept rendering to customize the headers and layout handling
94
+ #
95
+ def render_with_ajax(options = nil, extra_options = {}, &block)
96
+ return render_without_ajax(options, extra_options, &block) unless Ajax.is_enabled?
97
+
98
+ original_args = [options, extra_options]
99
+ if request.xhr?
100
+
101
+ # Options processing taken from ActionController::Base#render
102
+ if options.nil?
103
+ options = { :template => default_template, :layout => true }
104
+ elsif options == :update
105
+ options = extra_options.merge({ :update => true })
106
+ elsif options.is_a?(String) || options.is_a?(Symbol)
107
+ case options.to_s.index('/')
108
+ when 0
109
+ extra_options[:file] = options
110
+ when nil
111
+ extra_options[:action] = options
112
+ else
113
+ extra_options[:template] = options
114
+ end
115
+ options = extra_options
116
+ elsif !options.is_a?(Hash)
117
+ extra_options[:partial] = options
118
+ options = extra_options
119
+ end
120
+
121
+ default = pick_layout(options)
122
+ default = default.path_without_format_and_extension unless default.nil?
123
+ ajax_layout = layout_for_ajax(default)
124
+ ajax_layout = ajax_layout.path_without_format_and_extension unless ajax_layout.nil?
125
+ options[:layout] = ajax_layout unless ajax_layout.nil?
126
+
127
+ # Send the current layout and controller in a custom response header
128
+ Ajax.set_header(response, :layout, ajax_layout)
129
+ Ajax.set_header(response, :controller, self.class.controller_name)
130
+ end
131
+ render_without_ajax(options, extra_options, &block)
132
+ end
133
+
134
+ # Return the layout to use for an AJAX request, or the default layout if one
135
+ # cannot be found. If no default is known, <tt>layouts/ajax/application</tt> is used.
136
+ #
137
+ # If no ajax_layout is set, look for the default layout in <tt>layouts/ajax</tt>.
138
+ # If the layout cannot be found, use the default.
139
+ #
140
+ # FIXME: Use hard-coded html layout extension because <tt>default_template_format</tt>
141
+ # is sometimes :js which means the layout isn't found.
142
+ def layout_for_ajax(default) #:nodoc:
143
+ ajax_layout = self.class.read_inheritable_attribute(:ajax_layout)
144
+ if ajax_layout.nil? || !(ajax_layout =~ /^layouts\/ajax/)
145
+ find_layout("layouts/ajax/#{default.sub(/layouts(\/)?/, '')}", 'html') unless default.nil?
146
+ else
147
+ ajax_layout
148
+ end
149
+ rescue ::ActionView::MissingTemplate
150
+ Ajax.logger.info("[ajax] no layout found in layouts/ajax using #{default}")
151
+ default
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,56 @@
1
+ module Ajax
2
+ module ActionView
3
+ def self.included(klass)
4
+ klass.class_eval do
5
+ alias_method_chain :link_to, :ajax if method_defined?(:link_to)
6
+ end
7
+ klass.send(:include, Helpers)
8
+ end
9
+
10
+ module Helpers
11
+
12
+ # Set a custom response header if the request is AJAX.
13
+ def ajax_header(key, value)
14
+ return unless Ajax.is_enabled? && request.xhr?
15
+ Ajax.set_header(response, key, value)
16
+ end
17
+ end
18
+
19
+ protected
20
+
21
+ # Include an attribute on all outgoing links to mark them as Ajax deep links.
22
+ #
23
+ # The deep link will be the path and query string from the href.
24
+ #
25
+ # To specify a different deep link pass <tt>:data-deep-link => '/deep/link/path'</tt>
26
+ # in the <tt>link_to</tt> <tt>html_options</tt>.
27
+ #
28
+ # To turn off deep linking for a URL, pass <tt>:traditional => true</tt> or
29
+ # <tt>:data-deep-link => nil</tt>.
30
+ #
31
+ # Any paths matching the paths in Ajax.exclude_paths will automatically be
32
+ # linked to traditionally.
33
+ def link_to_with_ajax(*args, &block)
34
+ if Ajax.is_enabled? && !block_given?
35
+ options = args.second || {}
36
+ html_options = args.third
37
+ html_options = (html_options || {}).stringify_keys
38
+
39
+ # Insert the deep link unless the URL is traditional
40
+ if !html_options.has_key?('data-deep-link') && !html_options.delete('traditional')
41
+ path = url_for(options)
42
+
43
+ # Is this path to be excluded?
44
+ unless Ajax.exclude_path?(path)
45
+ if path.match(%r[^(http:\/\/[^\/]*)(\/?.*)])
46
+ path = $2
47
+ end
48
+ html_options['data-deep-link'] = path
49
+ end
50
+ end
51
+ args[2] = html_options
52
+ end
53
+ link_to_without_ajax(*args, &block)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ require 'ajax/helpers/request_helper'
2
+ require 'ajax/helpers/robot_helper'
3
+ require 'ajax/helpers/url_helper'
4
+
5
+ module Ajax #:nodoc:
6
+ module Helpers #:nodoc:
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ extend RequestHelper
10
+ extend RobotHelper
11
+ extend UrlHelper
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
1
+ module Ajax
2
+ module Helpers
3
+ module RequestHelper
4
+ # Recursive merge values
5
+ DEEP_MERGE = lambda do |key, v1, v2|
6
+ if v1.is_a?(Hash) && v2.is_a?(Hash)
7
+ v1.merge(v2, &DEEP_MERGE)
8
+ elsif v1.is_a?(Array) && v2.is_a?(Array)
9
+ v1.concat(v2)
10
+ else
11
+ [v1, v2].compact.first
12
+ end
13
+ end
14
+
15
+ # Hash and/or Array values are merged so you can set multiple values
16
+ def set_header(object, key, value)
17
+ headers = object.is_a?(::ActionController::Response) ? object.headers : object
18
+ unless headers["Ajax-Info"].is_a?(Hash)
19
+ headers["Ajax-Info"] = {}
20
+ end
21
+
22
+ # Deep merge hashes
23
+ if headers["Ajax-Info"].has_key?(key.to_s) &&
24
+ value.is_a?(Hash) &&
25
+ headers["Ajax-Info"][key.to_s].is_a?(Hash)
26
+ value = headers["Ajax-Info"][key.to_s].merge(value, &DEEP_MERGE)
27
+ end
28
+
29
+ # Concat arrays
30
+ if headers["Ajax-Info"].has_key?(key.to_s) &&
31
+ value.is_a?(Array) &&
32
+ headers["Ajax-Info"][key.to_s].is_a?(Array)
33
+ value = headers["Ajax-Info"][key.to_s].concat(value)
34
+ end
35
+
36
+ headers["Ajax-Info"][key.to_s] = value
37
+ end
38
+
39
+ def get_header(object, key)
40
+ headers = object.is_a?(::ActionController::Request) ? object.headers : object
41
+ unless headers["Ajax-Info"].is_a?(Hash)
42
+ headers["Ajax-Info"] = {}
43
+ end
44
+ headers['Ajax-Info'][key.to_s]
45
+ end
46
+
47
+ # Set one or more paths that can be accessed directly without the AJAX framework.
48
+ #
49
+ # Useful for excluding pages with HTTPS content on them from being loaded
50
+ # via AJAX.
51
+ #
52
+ # <tt>paths</tt> a list of String or Regexp instances that are matched
53
+ # against each REQUEST_PATH.
54
+ #
55
+ # The string and regex paths are modified to match full URLs by prepending
56
+ # them with the appropriate regular expression.
57
+ def exclude_paths(paths=nil)
58
+ if !instance_variable_defined?(:@exclude_paths)
59
+ @exclude_paths = []
60
+ end
61
+ (paths || []).each do |path|
62
+ @exclude_paths << /^(\w+\:\/\/[^\/]+\/?)?#{path.to_s}$/
63
+ end
64
+ @exclude_paths
65
+ end
66
+
67
+ # Return a boolean indicating whether or not to exclude a path from the
68
+ # AJAX redirect.
69
+ def exclude_path?(path)
70
+ !!((@exclude_paths || []).find do |excluded|
71
+ !!excluded.match(path)
72
+ end)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,31 @@
1
+ module Ajax
2
+ module Helpers
3
+ module RobotHelper
4
+ ROBOTS = [
5
+ {:name => 'Googlebot', :user_agent_regex => /\bGooglebot\b/i, :sample_agent_string => 'Googlebot'},
6
+ {:name => 'Baidu', :user_agent_regex => /\bBaidu\b/i, :sample_agent_string => 'Baidu'},
7
+ {:name => 'Gigabot', :user_agent_regex => /\bGigabot\b/i, :sample_agent_string => 'Gigabot'},
8
+ {:name => 'libwww-perl', :user_agent_regex => /\blibwww-perl\b/i, :sample_agent_string => 'libwww-perl'},
9
+ {:name => 'lwp-trivial', :user_agent_regex => /\blwp-trivial\b/i, :sample_agent_string => 'lwp-trivial'},
10
+ {:name => 'MSNBot', :user_agent_regex => /\bmsnbot\b/i, :sample_agent_string => 'msnbot'},
11
+ {:name => 'SiteUptime', :user_agent_regex => /\bSiteUptime\b/i, :sample_agent_string => 'SiteUptime'},
12
+ {:name => 'Slurp', :user_agent_regex => /\bSlurp\b/i, :sample_agent_string => 'Slurp'},
13
+ {:name => 'WordPress', :user_agent_regex => /\bWordPress\b/i, :sample_agent_string => 'WordPress'},
14
+ {:name => 'ZIBB', :user_agent_regex => /\bZIBB\b/i, :sample_agent_string => 'ZIBB'},
15
+ {:name => 'ZyBorg', :user_agent_regex => /\bZyBorg\b/i, :sample_agent_string => 'ZyBorg'},
16
+ ]
17
+
18
+ def robot_for(user_agent)
19
+ ROBOTS.each do |r|
20
+ return r if user_agent =~ r[:user_agent_regex]
21
+ end
22
+ nil
23
+ end
24
+
25
+ # Call with a User Agent string
26
+ def is_robot?(user_agent)
27
+ !!self.robot_for(user_agent)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,47 @@
1
+ module Ajax
2
+ module Helpers
3
+ module UrlHelper
4
+
5
+ # Return a boolean indicating whether the given URL points to the
6
+ # root path.
7
+ def url_is_root?(url)
8
+ !!(URI.parse(url).path =~ %r[^\/?$])
9
+ end
10
+
11
+ # The URL is hashed if the fragment part starts with a /
12
+ #
13
+ # For example, http://lol.com#/Rihanna
14
+ def is_hashed_url?(url)
15
+ !!(URI.parse(url).fragment =~ %r[^\/])
16
+ end
17
+
18
+ # Return a hashed URL using the fragment of <tt>url</tt>
19
+ def hashed_url_from_fragment(url)
20
+ url_host(url) + ('/#/' + (URI.parse(url).fragment || '')).gsub(/\/\//, '/')
21
+ end
22
+
23
+ # Return a traditional URL from the fragment of <tt>url</tt>
24
+ def traditional_url_from_fragment(url)
25
+ url_host(url) + ('/' + (URI.parse(url).fragment || '')).gsub(/\/\//, '/')
26
+ end
27
+
28
+ # Return a hashed URL formed from a traditional <tt>url</tt>
29
+ def hashed_url_from_traditional(url)
30
+ uri = URI.parse(url)
31
+ hashed_url = url_host(url) + ('/#/' + (uri.path || '')).gsub(/\/\//, '/')
32
+ hashed_url += ('?' + uri.query) unless uri.query.nil?
33
+ hashed_url
34
+ end
35
+
36
+ protected
37
+
38
+ def url_host(url)
39
+ if url.match(/^(\w+\:\/\/[^\/]+)\/?/)
40
+ $1
41
+ else
42
+ ''
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,7 @@
1
+ module SitemapGenerator
2
+ class Railtie < Rails::Railtie
3
+ rake_tasks do
4
+ load(File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'tasks', 'ajax_tasks.rake')))
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ module Ajax
2
+ module Routes
3
+ # In your <tt>config/routes.rb</tt> file call:
4
+ # Ajax::Routes.draw(map)
5
+ # Passing in the routing <tt>map</tt> object.
6
+ #
7
+ # Adds an <tt>ajax_framework_path</tt> pointing to <tt>/ajax/framework</tt>
8
+ def self.draw(map)
9
+ map.ajax_framework "/ajax/framework", :controller => 'ajax', :action => 'framework'
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,34 @@
1
+ module Ajax
2
+ module Spec
3
+ module Extension
4
+
5
+ def integrate_ajax
6
+ Ajax.enabled = true
7
+ end
8
+
9
+ def disable_ajax
10
+ Ajax.enabled = false
11
+ end
12
+
13
+ def mock_ajax
14
+ integrate_ajax
15
+ Ajax.mocked = true
16
+ end
17
+
18
+ def unmock_ajax
19
+ disable_ajax
20
+ Ajax.mocked = false
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module ActiveSupport
27
+ class TestCase
28
+ include Ajax::Spec::Extension
29
+
30
+ before(:all) do
31
+ ::Ajax.enabled = false
32
+ end if method_defined?(:before)
33
+ end
34
+ end
@@ -0,0 +1,95 @@
1
+ require 'uri'
2
+
3
+ module Ajax
4
+ module Spec
5
+ module Helpers
6
+
7
+ def create_app
8
+ @app = Class.new { def call(env); true; end }.new
9
+ end
10
+
11
+ def call_rack(url, request_method='GET', env={}, &block)
12
+ env(url, request_method, env)
13
+ @rack = Rack::Ajax.new(@app, &block)
14
+ @response = @rack.call(@env)
15
+ end
16
+
17
+ def should_respond_with(msg)
18
+ should_be_a_valid_response
19
+ response_body.should == msg
20
+ end
21
+
22
+ def should_redirect_to(location, code=302)
23
+ should_be_a_valid_response
24
+ response_code.should == code
25
+ response_headers['Location'].should == location
26
+ end
27
+
28
+ def should_set_ajax_response_header(key, value)
29
+ response_headers['Ajax-Info'][key].should == value
30
+ end
31
+
32
+ def should_set_ajax_request_header(key, value)
33
+ @env['Ajax-Info'][key].should == value
34
+ end
35
+
36
+ def should_rewrite_to(url)
37
+ should_be_a_valid_response
38
+
39
+ # Check custom headers
40
+ response_body_as_hash['REQUEST_URI'].should == url
41
+ end
42
+
43
+ def should_not_modify_request
44
+ should_be_a_valid_response
45
+ response_code.should == 200
46
+
47
+ # If we have the original headers from a call to call_rack()
48
+ # check that they haven't changed. Otherwise, just make sure
49
+ # that we don't have the custom rewrite header.
50
+ if !@env.nil?
51
+ @env.each { |k,v| response_body_as_hash.should == v }
52
+ end
53
+ end
54
+
55
+ # Response must be [code, {headers}, ['Response']]
56
+ # Headers must contain the Content-Type header
57
+ def should_be_a_valid_response
58
+ return if @response.is_a?(::ActionController::Response)
59
+ @response.should be_a_kind_of(Array)
60
+ @response.size.should == 3
61
+ @response[0].should be_a_kind_of(Integer)
62
+ @response[1].should be_a_kind_of(Hash)
63
+ @response[1]['Content-Type'].should =~ %r[^text\/\w+]
64
+ @response[2].should be_a_kind_of(Array)
65
+ @response[2][0].should be_a_kind_of(String)
66
+ end
67
+
68
+ def env(uri, request_method, options={})
69
+ uri = URI.parse(uri)
70
+ @env = {
71
+ 'REQUEST_URI' => uri.to_s,
72
+ 'PATH_INFO' => uri.path,
73
+ 'QUERY_STRING' => uri.query,
74
+ 'REQUEST_METHOD' => request_method
75
+ }.merge!(options)
76
+ end
77
+
78
+ def response_body
79
+ @response.is_a?(::ActionController::Response) ? @response.body : @response[2][0]
80
+ end
81
+
82
+ def response_code
83
+ @response.is_a?(::ActionController::Response) ? @response.status.to_i : @response[0]
84
+ end
85
+
86
+ def response_headers
87
+ @response.is_a?(::ActionController::Response) ? @response.headers.to_hash : @response[1]
88
+ end
89
+
90
+ def response_body_as_hash
91
+ @response_body_as_hash ||= YAML.load(response_body)
92
+ end
93
+ end
94
+ end
95
+ end