ajax 0.1.2

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 (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