| 1 | require 'will_paginate/core_ext' |
|---|
| 2 | |
|---|
| 3 | module WillPaginate |
|---|
| 4 | # = Will Paginate view helpers |
|---|
| 5 | # |
|---|
| 6 | # Currently there is only one view helper: +will_paginate+. It renders the |
|---|
| 7 | # pagination links for the given collection. The helper itself is lightweight |
|---|
| 8 | # and serves only as a wrapper around link renderer instantiation; the |
|---|
| 9 | # renderer then does all the hard work of generating the HTML. |
|---|
| 10 | # |
|---|
| 11 | # == Global options for helpers |
|---|
| 12 | # |
|---|
| 13 | # Options for pagination helpers are optional and get their default values from the |
|---|
| 14 | # WillPaginate::ViewHelpers.pagination_options hash. You can write to this hash to |
|---|
| 15 | # override default options on the global level: |
|---|
| 16 | # |
|---|
| 17 | # WillPaginate::ViewHelpers.pagination_options[:prev_label] = 'Previous page' |
|---|
| 18 | # |
|---|
| 19 | # By putting this into your environment.rb you can easily translate link texts to previous |
|---|
| 20 | # and next pages, as well as override some other defaults to your liking. |
|---|
| 21 | module ViewHelpers |
|---|
| 22 | # default options that can be overridden on the global level |
|---|
| 23 | @@pagination_options = { |
|---|
| 24 | :class => 'pagination', |
|---|
| 25 | :prev_label => '« Previous', |
|---|
| 26 | :next_label => 'Next »', |
|---|
| 27 | :inner_window => 4, # links around the current page |
|---|
| 28 | :outer_window => 1, # links around beginning and end |
|---|
| 29 | :separator => ' ', # single space is friendly to spiders and non-graphic browsers |
|---|
| 30 | :param_name => :page, |
|---|
| 31 | :params => nil, |
|---|
| 32 | :renderer => 'WillPaginate::LinkRenderer', |
|---|
| 33 | :page_links => true, |
|---|
| 34 | :container => true |
|---|
| 35 | } |
|---|
| 36 | mattr_reader :pagination_options |
|---|
| 37 | |
|---|
| 38 | # Renders Digg/Flickr-style pagination for a WillPaginate::Collection |
|---|
| 39 | # object. Nil is returned if there is only one page in total; no point in |
|---|
| 40 | # rendering the pagination in that case... |
|---|
| 41 | # |
|---|
| 42 | # ==== Options |
|---|
| 43 | # * <tt>:class</tt> -- CSS class name for the generated DIV (default: "pagination") |
|---|
| 44 | # * <tt>:prev_label</tt> -- default: "« Previous" |
|---|
| 45 | # * <tt>:next_label</tt> -- default: "Next »" |
|---|
| 46 | # * <tt>:inner_window</tt> -- how many links are shown around the current page (default: 4) |
|---|
| 47 | # * <tt>:outer_window</tt> -- how many links are around the first and the last page (default: 1) |
|---|
| 48 | # * <tt>:separator</tt> -- string separator for page HTML elements (default: single space) |
|---|
| 49 | # * <tt>:param_name</tt> -- parameter name for page number in URLs (default: <tt>:page</tt>) |
|---|
| 50 | # * <tt>:params</tt> -- additional parameters when generating pagination links |
|---|
| 51 | # (eg. <tt>:controller => "foo", :action => nil</tt>) |
|---|
| 52 | # * <tt>:renderer</tt> -- class name of the link renderer (default: WillPaginate::LinkRenderer) |
|---|
| 53 | # * <tt>:page_links</tt> -- when false, only previous/next links are rendered (default: true) |
|---|
| 54 | # * <tt>:container</tt> -- toggles rendering of the DIV container for pagination links, set to |
|---|
| 55 | # false only when you are rendering your own pagination markup (default: true) |
|---|
| 56 | # * <tt>:id</tt> -- HTML ID for the container (default: nil). Pass +true+ to have the ID automatically |
|---|
| 57 | # generated from the class name of objects in collection: for example, paginating |
|---|
| 58 | # ArticleComment models would yield an ID of "article_comments_pagination". |
|---|
| 59 | # |
|---|
| 60 | # All options beside listed ones are passed as HTML attributes to the container |
|---|
| 61 | # element for pagination links (the DIV). For example: |
|---|
| 62 | # |
|---|
| 63 | # <%= will_paginate @posts, :id => 'wp_posts' %> |
|---|
| 64 | # |
|---|
| 65 | # ... will result in: |
|---|
| 66 | # |
|---|
| 67 | # <div class="pagination" id="wp_posts"> ... </div> |
|---|
| 68 | # |
|---|
| 69 | # ==== Using the helper without arguments |
|---|
| 70 | # If the helper is called without passing in the collection object, it will |
|---|
| 71 | # try to read from the instance variable inferred by the controller name. |
|---|
| 72 | # For example, calling +will_paginate+ while the current controller is |
|---|
| 73 | # PostsController will result in trying to read from the <tt>@posts</tt> |
|---|
| 74 | # variable. Example: |
|---|
| 75 | # |
|---|
| 76 | # <%= will_paginate :id => true %> |
|---|
| 77 | # |
|---|
| 78 | # ... will result in <tt>@post</tt> collection getting paginated: |
|---|
| 79 | # |
|---|
| 80 | # <div class="pagination" id="posts_pagination"> ... </div> |
|---|
| 81 | # |
|---|
| 82 | def will_paginate(collection = nil, options = {}) |
|---|
| 83 | options, collection = collection, nil if collection.is_a? Hash |
|---|
| 84 | unless collection or !controller |
|---|
| 85 | collection_name = "@#{controller.controller_name}" |
|---|
| 86 | collection = instance_variable_get(collection_name) |
|---|
| 87 | raise ArgumentError, "The #{collection_name} variable appears to be empty. Did you " + |
|---|
| 88 | "forget to specify the collection object for will_paginate?" unless collection |
|---|
| 89 | end |
|---|
| 90 | # early exit if there is nothing to render |
|---|
| 91 | return nil unless collection.page_count > 1 |
|---|
| 92 | options = options.symbolize_keys.reverse_merge WillPaginate::ViewHelpers.pagination_options |
|---|
| 93 | # create the renderer instance |
|---|
| 94 | renderer_class = options[:renderer].to_s.constantize |
|---|
| 95 | renderer = renderer_class.new collection, options, self |
|---|
| 96 | # render HTML for pagination |
|---|
| 97 | renderer.to_html |
|---|
| 98 | end |
|---|
| 99 | end |
|---|
| 100 | |
|---|
| 101 | # This class does the heavy lifting of actually building the pagination |
|---|
| 102 | # links. It is used by +will_paginate+ helper internally, but avoid using it |
|---|
| 103 | # directly (for now) because its API is not set in stone yet. |
|---|
| 104 | class LinkRenderer |
|---|
| 105 | |
|---|
| 106 | def initialize(collection, options, template) |
|---|
| 107 | @collection = collection |
|---|
| 108 | @options = options |
|---|
| 109 | @template = template |
|---|
| 110 | end |
|---|
| 111 | |
|---|
| 112 | def to_html |
|---|
| 113 | links = @options[:page_links] ? windowed_paginator : [] |
|---|
| 114 | # previous/next buttons |
|---|
| 115 | links.unshift page_link_or_span(@collection.previous_page, 'disabled', @options[:prev_label]) |
|---|
| 116 | links.push page_link_or_span(@collection.next_page, 'disabled', @options[:next_label]) |
|---|
| 117 | |
|---|
| 118 | html = links.join(@options[:separator]) |
|---|
| 119 | @options[:container] ? @template.content_tag(:div, html, html_attributes) : html |
|---|
| 120 | end |
|---|
| 121 | |
|---|
| 122 | def html_attributes |
|---|
| 123 | return @html_attributes if @html_attributes |
|---|
| 124 | @html_attributes = @options.except *(WillPaginate::ViewHelpers.pagination_options.keys - [:class]) |
|---|
| 125 | # pagination of Post models will have the ID of "posts_pagination" |
|---|
| 126 | if @options[:container] and @options[:id] === true |
|---|
| 127 | @html_attributes[:id] = @collection.first.class.name.underscore.pluralize + '_pagination' |
|---|
| 128 | end |
|---|
| 129 | @html_attributes |
|---|
| 130 | end |
|---|
| 131 | |
|---|
| 132 | protected |
|---|
| 133 | |
|---|
| 134 | def gap_marker; '...'; end |
|---|
| 135 | |
|---|
| 136 | def windowed_paginator |
|---|
| 137 | inner_window, outer_window = @options[:inner_window].to_i, @options[:outer_window].to_i |
|---|
| 138 | window_from = current_page - inner_window |
|---|
| 139 | window_to = current_page + inner_window |
|---|
| 140 | |
|---|
| 141 | # adjust lower or upper limit if other is out of bounds |
|---|
| 142 | if window_to > total_pages |
|---|
| 143 | window_from -= window_to - total_pages |
|---|
| 144 | window_to = total_pages |
|---|
| 145 | elsif window_from < 1 |
|---|
| 146 | window_to += 1 - window_from |
|---|
| 147 | window_from = 1 |
|---|
| 148 | end |
|---|
| 149 | |
|---|
| 150 | visible = (1..total_pages).to_a |
|---|
| 151 | left_gap = (2 + outer_window)...window_from |
|---|
| 152 | right_gap = (window_to + 1)...(total_pages - outer_window) |
|---|
| 153 | visible -= left_gap.to_a if left_gap.last - left_gap.first > 1 |
|---|
| 154 | visible -= right_gap.to_a if right_gap.last - right_gap.first > 1 |
|---|
| 155 | |
|---|
| 156 | links, prev = [], nil |
|---|
| 157 | |
|---|
| 158 | visible.each do |n| |
|---|
| 159 | # detect gaps: |
|---|
| 160 | links << gap_marker if prev and n > prev + 1 |
|---|
| 161 | links << page_link_or_span(n) |
|---|
| 162 | prev = n |
|---|
| 163 | end |
|---|
| 164 | |
|---|
| 165 | links |
|---|
| 166 | end |
|---|
| 167 | |
|---|
| 168 | def page_link_or_span(page, span_class = 'current', text = nil) |
|---|
| 169 | text ||= page.to_s |
|---|
| 170 | if page and page != current_page |
|---|
| 171 | @template.link_to text, url_options(page) |
|---|
| 172 | else |
|---|
| 173 | @template.content_tag :span, text, :class => span_class |
|---|
| 174 | end |
|---|
| 175 | end |
|---|
| 176 | |
|---|
| 177 | def url_options(page) |
|---|
| 178 | options = { param_name => page } |
|---|
| 179 | # page links should preserve GET parameters |
|---|
| 180 | options = params.merge(options) if @template.request.get? |
|---|
| 181 | options.rec_merge!(@options[:params]) if @options[:params] |
|---|
| 182 | return options |
|---|
| 183 | end |
|---|
| 184 | |
|---|
| 185 | private |
|---|
| 186 | |
|---|
| 187 | def current_page |
|---|
| 188 | @collection.current_page |
|---|
| 189 | end |
|---|
| 190 | |
|---|
| 191 | def total_pages |
|---|
| 192 | @collection.page_count |
|---|
| 193 | end |
|---|
| 194 | |
|---|
| 195 | def param_name |
|---|
| 196 | @param_name ||= @options[:param_name].to_sym |
|---|
| 197 | end |
|---|
| 198 | |
|---|
| 199 | def params |
|---|
| 200 | @params ||= @template.params.to_hash.symbolize_keys |
|---|
| 201 | end |
|---|
| 202 | end |
|---|
| 203 | end |
|---|