kaminari-core 1.0.0.beta2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.md +31 -0
  4. data/app/views/kaminari/_first_page.html.erb +11 -0
  5. data/app/views/kaminari/_first_page.html.haml +9 -0
  6. data/app/views/kaminari/_first_page.html.slim +10 -0
  7. data/app/views/kaminari/_gap.html.erb +8 -0
  8. data/app/views/kaminari/_gap.html.haml +8 -0
  9. data/app/views/kaminari/_gap.html.slim +9 -0
  10. data/app/views/kaminari/_last_page.html.erb +11 -0
  11. data/app/views/kaminari/_last_page.html.haml +9 -0
  12. data/app/views/kaminari/_last_page.html.slim +10 -0
  13. data/app/views/kaminari/_next_page.html.erb +11 -0
  14. data/app/views/kaminari/_next_page.html.haml +9 -0
  15. data/app/views/kaminari/_next_page.html.slim +10 -0
  16. data/app/views/kaminari/_page.html.erb +12 -0
  17. data/app/views/kaminari/_page.html.haml +10 -0
  18. data/app/views/kaminari/_page.html.slim +11 -0
  19. data/app/views/kaminari/_paginator.html.erb +25 -0
  20. data/app/views/kaminari/_paginator.html.haml +18 -0
  21. data/app/views/kaminari/_paginator.html.slim +19 -0
  22. data/app/views/kaminari/_prev_page.html.erb +11 -0
  23. data/app/views/kaminari/_prev_page.html.haml +9 -0
  24. data/app/views/kaminari/_prev_page.html.slim +10 -0
  25. data/config/locales/kaminari.yml +23 -0
  26. data/kaminari-core.gemspec +23 -0
  27. data/lib/generators/kaminari/config_generator.rb +18 -0
  28. data/lib/generators/kaminari/templates/kaminari_config.rb +12 -0
  29. data/lib/generators/kaminari/views_generator.rb +134 -0
  30. data/lib/kaminari/config.rb +28 -0
  31. data/lib/kaminari/core.rb +23 -0
  32. data/lib/kaminari/core/version.rb +6 -0
  33. data/lib/kaminari/engine.rb +5 -0
  34. data/lib/kaminari/exceptions.rb +4 -0
  35. data/lib/kaminari/helpers/helper_methods.rb +169 -0
  36. data/lib/kaminari/helpers/paginator.rb +189 -0
  37. data/lib/kaminari/helpers/tags.rb +137 -0
  38. data/lib/kaminari/models/array_extension.rb +72 -0
  39. data/lib/kaminari/models/configuration_methods.rb +57 -0
  40. data/lib/kaminari/models/page_scope_methods.rb +80 -0
  41. data/lib/kaminari/railtie.rb +8 -0
  42. metadata +113 -0
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/configurable'
3
+
4
+ module Kaminari
5
+ # Configures global settings for Kaminari
6
+ # Kaminari.configure do |config|
7
+ # config.default_per_page = 10
8
+ # end
9
+ include ActiveSupport::Configurable
10
+
11
+ config.instance_eval do
12
+ self.default_per_page = 25
13
+ self.max_per_page = nil
14
+ self.window = 4
15
+ self.outer_window = 0
16
+ self.left = 0
17
+ self.right = 0
18
+ self.page_method_name = :page
19
+ self.param_name = :page
20
+ self.max_pages = nil
21
+ self.params_on_first_page = false
22
+
23
+ # If param_name was given as a callable object, call it when returning
24
+ def param_name
25
+ self[:param_name].respond_to?(:call) ? self[:param_name].call : self[:param_name]
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module Kaminari
3
+ end
4
+
5
+ # load Rails/Railtie
6
+ begin
7
+ require 'rails'
8
+ rescue LoadError
9
+ #do nothing
10
+ end
11
+
12
+ # load Kaminari components
13
+ require 'kaminari/config'
14
+ require 'kaminari/exceptions'
15
+ require 'kaminari/helpers/paginator'
16
+ require 'kaminari/models/page_scope_methods'
17
+ require 'kaminari/models/configuration_methods'
18
+ require 'kaminari/models/array_extension'
19
+
20
+ if defined? ::Rails::Railtie
21
+ require 'kaminari/railtie'
22
+ require 'kaminari/engine'
23
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Kaminari
3
+ module Core
4
+ VERSION = '1.0.0.beta2'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module Kaminari #:nodoc:
3
+ class Engine < ::Rails::Engine #:nodoc:
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Kaminari
3
+ class ZeroPerPageOperation < ZeroDivisionError; end
4
+ end
@@ -0,0 +1,169 @@
1
+ module Kaminari
2
+ module Helpers
3
+ module HelperMethods
4
+ # A helper that renders the pagination links.
5
+ #
6
+ # <%= paginate @articles %>
7
+ #
8
+ # ==== Options
9
+ # * <tt>:window</tt> - The "inner window" size (4 by default).
10
+ # * <tt>:outer_window</tt> - The "outer window" size (0 by default).
11
+ # * <tt>:left</tt> - The "left outer window" size (0 by default).
12
+ # * <tt>:right</tt> - The "right outer window" size (0 by default).
13
+ # * <tt>:params</tt> - url_for parameters for the links (:controller, :action, etc.)
14
+ # * <tt>:param_name</tt> - parameter name for page number in the links (:page by default)
15
+ # * <tt>:remote</tt> - Ajax? (false by default)
16
+ # * <tt>:paginator_class</tt> - Specify a custom Paginator (Kaminari::Helpers::Paginator by default)
17
+ # * <tt>:template</tt> - Specify a custom template renderer for rendering the Paginator (receiver by default)
18
+ # * <tt>:ANY_OTHER_VALUES</tt> - Any other hash key & values would be directly passed into each tag as :locals value.
19
+ def paginate(scope, paginator_class: Kaminari::Helpers::Paginator, template: nil, **options)
20
+ options[:total_pages] ||= scope.total_pages
21
+ options.reverse_merge! current_page: scope.current_page, per_page: scope.limit_value, remote: false
22
+
23
+ paginator = paginator_class.new (template || self), options
24
+ paginator.to_s
25
+ end
26
+
27
+ # A simple "Twitter like" pagination link that creates a link to the previous page.
28
+ #
29
+ # ==== Examples
30
+ # Basic usage:
31
+ #
32
+ # <%= link_to_previous_page @items, 'Previous Page' %>
33
+ #
34
+ # Ajax:
35
+ #
36
+ # <%= link_to_previous_page @items, 'Previous Page', remote: true %>
37
+ #
38
+ # By default, it renders nothing if there are no more results on the previous page.
39
+ # You can customize this output by passing a block.
40
+ #
41
+ # <%= link_to_previous_page @users, 'Previous Page' do %>
42
+ # <span>At the Beginning</span>
43
+ # <% end %>
44
+ def link_to_previous_page(scope, name, **options)
45
+ prev_page = path_to_prev_page(scope, options)
46
+
47
+ options.except! :params, :param_name
48
+ options[:rel] ||= 'prev'
49
+
50
+ link_to_if prev_page, name, prev_page, options do
51
+ yield if block_given?
52
+ end
53
+ end
54
+ alias link_to_prev_page link_to_previous_page
55
+
56
+ # A simple "Twitter like" pagination link that creates a link to the next page.
57
+ #
58
+ # ==== Examples
59
+ # Basic usage:
60
+ #
61
+ # <%= link_to_next_page @items, 'Next Page' %>
62
+ #
63
+ # Ajax:
64
+ #
65
+ # <%= link_to_next_page @items, 'Next Page', remote: true %>
66
+ #
67
+ # By default, it renders nothing if there are no more results on the next page.
68
+ # You can customize this output by passing a block.
69
+ #
70
+ # <%= link_to_next_page @users, 'Next Page' do %>
71
+ # <span>No More Pages</span>
72
+ # <% end %>
73
+ def link_to_next_page(scope, name, **options)
74
+ next_page = path_to_next_page(scope, options)
75
+
76
+ options.except! :params, :param_name
77
+ options[:rel] ||= 'next'
78
+
79
+ link_to_if next_page, name, next_page, options do
80
+ yield if block_given?
81
+ end
82
+ end
83
+
84
+ # Renders a helpful message with numbers of displayed vs. total entries.
85
+ # Ported from mislav/will_paginate
86
+ #
87
+ # ==== Examples
88
+ # Basic usage:
89
+ #
90
+ # <%= page_entries_info @posts %>
91
+ # #-> Displaying posts 6 - 10 of 26 in total
92
+ #
93
+ # By default, the message will use the humanized class name of objects
94
+ # in collection: for instance, "project types" for ProjectType models.
95
+ # The namespace will be cutted out and only the last name will be used.
96
+ # Override this with the <tt>:entry_name</tt> parameter:
97
+ #
98
+ # <%= page_entries_info @posts, entry_name: 'item' %>
99
+ # #-> Displaying items 6 - 10 of 26 in total
100
+ def page_entries_info(collection, entry_name: nil)
101
+ entry_name = if entry_name
102
+ entry_name.pluralize(collection.size)
103
+ else
104
+ collection.entry_name(count: collection.size).downcase
105
+ end
106
+
107
+ if collection.total_pages < 2
108
+ t('helpers.page_entries_info.one_page.display_entries', entry_name: entry_name, count: collection.total_count)
109
+ else
110
+ t('helpers.page_entries_info.more_pages.display_entries', entry_name: entry_name, first: collection.offset_value + 1, last: [collection.offset_value + collection.limit_value, collection.total_count].min, total: collection.total_count)
111
+ end.html_safe
112
+ end
113
+
114
+ # Renders rel="next" and rel="prev" links to be used in the head.
115
+ #
116
+ # ==== Examples
117
+ # Basic usage:
118
+ #
119
+ # In head:
120
+ # <head>
121
+ # <title>My Website</title>
122
+ # <%= yield :head %>
123
+ # </head>
124
+ #
125
+ # Somewhere in body:
126
+ # <% content_for :head do %>
127
+ # <%= rel_next_prev_link_tags @items %>
128
+ # <% end %>
129
+ #
130
+ # #-> <link rel="next" href="/items/page/3" /><link rel="prev" href="/items/page/1" />
131
+ #
132
+ def rel_next_prev_link_tags(scope, options = {})
133
+ next_page = path_to_next_page(scope, options)
134
+ prev_page = path_to_prev_page(scope, options)
135
+
136
+ output = String.new
137
+ output << tag(:link, rel: "next", href: next_page) if next_page
138
+ output << tag(:link, rel: "prev", href: prev_page) if prev_page
139
+ output.html_safe
140
+ end
141
+
142
+ # A helper that calculates the path to the next page.
143
+ #
144
+ # ==== Examples
145
+ # Basic usage:
146
+ #
147
+ # <%= path_to_next_page @items %>
148
+ # #-> /items?page=2
149
+ #
150
+ # It will return `nil` if there is no next page.
151
+ def path_to_next_page(scope, options = {})
152
+ Kaminari::Helpers::NextPage.new(self, options.reverse_merge(current_page: scope.current_page)).url if scope.next_page
153
+ end
154
+
155
+ # A helper that calculates the path to the previous page.
156
+ #
157
+ # ==== Examples
158
+ # Basic usage:
159
+ #
160
+ # <%= path_to_prev_page @items %>
161
+ # #-> /items
162
+ #
163
+ # It will return `nil` if there is no previous page.
164
+ def path_to_prev_page(scope, options = {})
165
+ Kaminari::Helpers::PrevPage.new(self, options.reverse_merge(current_page: scope.current_page)).url if scope.prev_page
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/inflector'
3
+ require 'kaminari/helpers/tags'
4
+
5
+ module Kaminari
6
+ module Helpers
7
+ # The main container tag
8
+ class Paginator < Tag
9
+ def initialize(template, window: nil, outer_window: nil, left: nil, right: nil, inner_window: nil, **options) #:nodoc:
10
+ outer_window ||= Kaminari.config.outer_window
11
+ left ||= Kaminari.config.left
12
+ right ||= Kaminari.config.right
13
+ @window_options = {window: window || inner_window || Kaminari.config.window, left: left.zero? ? outer_window : left, right: right.zero? ? outer_window : right}
14
+
15
+ @template, @options, @theme, @views_prefix, @last = template, options, options[:theme], options[:views_prefix], nil
16
+ @window_options.merge! @options
17
+ @window_options[:current_page] = @options[:current_page] = PageProxy.new(@window_options, @options[:current_page], nil)
18
+
19
+ #XXX Using parent template's buffer class for rendering each partial here. This might cause problems if the handler mismatches
20
+ @output_buffer = if defined?(::ActionView::OutputBuffer)
21
+ ::ActionView::OutputBuffer.new
22
+ elsif template.instance_variable_get(:@output_buffer)
23
+ template.instance_variable_get(:@output_buffer).class.new
24
+ else
25
+ ActiveSupport::SafeBuffer.new
26
+ end
27
+ end
28
+
29
+ # render given block as a view template
30
+ def render(&block)
31
+ instance_eval(&block) if @options[:total_pages] > 1
32
+ @output_buffer
33
+ end
34
+
35
+ # enumerate each page providing PageProxy object as the block parameter
36
+ # Because of performance reason, this doesn't actually enumerate all pages but pages that are seemingly relevant to the paginator.
37
+ # "Relevant" pages are:
38
+ # * pages inside the left outer window plus one for showing the gap tag
39
+ # * pages inside the inner window plus one on the left plus one on the right for showing the gap tags
40
+ # * pages inside the right outer window plus one for showing the gap tag
41
+ def each_relevant_page
42
+ return to_enum(:each_relevant_page) unless block_given?
43
+
44
+ relevant_pages(@window_options).each do |page|
45
+ yield PageProxy.new(@window_options, page, @last)
46
+ end
47
+ end
48
+ alias each_page each_relevant_page
49
+
50
+ def relevant_pages(options)
51
+ left_window_plus_one = [*1..options[:left] + 1]
52
+ right_window_plus_one = [*options[:total_pages] - options[:right]..options[:total_pages]]
53
+ inside_window_plus_each_sides = [*options[:current_page] - options[:window] - 1..options[:current_page] + options[:window] + 1]
54
+
55
+ (left_window_plus_one | inside_window_plus_each_sides | right_window_plus_one).sort.reject {|x| (x < 1) || (x > options[:total_pages])}
56
+ end
57
+ private :relevant_pages
58
+
59
+ def page_tag(page)
60
+ @last = Page.new @template, @options.merge(page: page)
61
+ end
62
+
63
+ %w[first_page prev_page next_page last_page gap].each do |tag|
64
+ eval <<-DEF, nil, __FILE__, __LINE__ + 1
65
+ def #{tag}_tag
66
+ @last = #{tag.classify}.new @template, @options
67
+ end
68
+ DEF
69
+ end
70
+
71
+ def to_s #:nodoc:
72
+ Thread.current[:kaminari_rendering] = true
73
+ super @window_options.merge paginator: self
74
+ ensure
75
+ Thread.current[:kaminari_rendering] = false
76
+ end
77
+
78
+ # delegates view helper methods to @template
79
+ def method_missing(name, *args, &block)
80
+ @template.respond_to?(name) ? @template.send(name, *args, &block) : super
81
+ end
82
+ private :method_missing
83
+
84
+ # Wraps a "page number" and provides some utility methods
85
+ class PageProxy
86
+ include Comparable
87
+
88
+ def initialize(options, page, last) #:nodoc:
89
+ @options, @page, @last = options, page, last
90
+ end
91
+
92
+ # the page number
93
+ def number
94
+ @page
95
+ end
96
+
97
+ # current page or not
98
+ def current?
99
+ @page == @options[:current_page]
100
+ end
101
+
102
+ # the first page or not
103
+ def first?
104
+ @page == 1
105
+ end
106
+
107
+ # the last page or not
108
+ def last?
109
+ @page == @options[:total_pages]
110
+ end
111
+
112
+ # the previous page or not
113
+ def prev?
114
+ @page == @options[:current_page] - 1
115
+ end
116
+
117
+ # the next page or not
118
+ def next?
119
+ @page == @options[:current_page] + 1
120
+ end
121
+
122
+ # relationship with the current page
123
+ def rel
124
+ if next?
125
+ 'next'
126
+ elsif prev?
127
+ 'prev'
128
+ end
129
+ end
130
+
131
+ # within the left outer window or not
132
+ def left_outer?
133
+ @page <= @options[:left]
134
+ end
135
+
136
+ # within the right outer window or not
137
+ def right_outer?
138
+ @options[:total_pages] - @page < @options[:right]
139
+ end
140
+
141
+ # inside the inner window or not
142
+ def inside_window?
143
+ (@options[:current_page] - @page).abs <= @options[:window]
144
+ end
145
+
146
+ # Current page is an isolated gap or not
147
+ def single_gap?
148
+ ((@page == @options[:current_page] - @options[:window] - 1) && (@page == @options[:left] + 1)) ||
149
+ ((@page == @options[:current_page] + @options[:window] + 1) && (@page == @options[:total_pages] - @options[:right]))
150
+ end
151
+
152
+ # The page number exceeds the range of pages or not
153
+ def out_of_range?
154
+ @page > @options[:total_pages]
155
+ end
156
+
157
+ # The last rendered tag was "truncated" or not
158
+ def was_truncated?
159
+ @last.is_a? Gap
160
+ end
161
+
162
+ #Should we display the link tag?
163
+ def display_tag?
164
+ left_outer? || right_outer? || inside_window? || single_gap?
165
+ end
166
+
167
+ def to_i #:nodoc:
168
+ number
169
+ end
170
+
171
+ def to_s #:nodoc:
172
+ number.to_s
173
+ end
174
+
175
+ def +(other) #:nodoc:
176
+ to_i + other.to_i
177
+ end
178
+
179
+ def -(other) #:nodoc:
180
+ to_i - other.to_i
181
+ end
182
+
183
+ def <=>(other) #:nodoc:
184
+ to_i <=> other.to_i
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+ module Kaminari
3
+ module Helpers
4
+ PARAM_KEY_BLACKLIST = [:authenticity_token, :commit, :utf8, :_method, :script_name].freeze
5
+
6
+ # A tag stands for an HTML tag inside the paginator.
7
+ # Basically, a tag has its own partial template file, so every tag can be
8
+ # rendered into String using its partial template.
9
+ #
10
+ # The template file should be placed in your app/views/kaminari/ directory
11
+ # with underscored class name (besides the "Tag" class. Tag is an abstract
12
+ # class, so _tag partial is not needed).
13
+ # e.g.) PrevLink -> app/views/kaminari/_prev_link.html.erb
14
+ #
15
+ # When no matching template were found in your app, the engine's pre
16
+ # installed template will be used.
17
+ # e.g.) Paginator -> $GEM_HOME/kaminari-x.x.x/app/views/kaminari/_paginator.html.erb
18
+ class Tag
19
+ def initialize(template, params: {}, param_name: nil, theme: nil, views_prefix: nil, **options) #:nodoc:
20
+ @template, @theme, @views_prefix, @options = template, theme, views_prefix, options
21
+ @param_name = param_name || Kaminari.config.param_name
22
+ @params = template.params
23
+ # @params in Rails 5 no longer inherits from Hash
24
+ @params = @params.to_unsafe_h if @params.respond_to?(:to_unsafe_h)
25
+ @params = @params.with_indifferent_access
26
+ @params.except!(*PARAM_KEY_BLACKLIST)
27
+ @params.merge! params
28
+ end
29
+
30
+ def to_s(locals = {}) #:nodoc:
31
+ formats = (@template.respond_to?(:formats) ? @template.formats : Array(@template.params[:format])) + [:html]
32
+ @template.render partial: partial_path, locals: @options.merge(locals), formats: formats
33
+ end
34
+
35
+ def page_url_for(page)
36
+ params = params_for(page)
37
+ params[:only_path] = true
38
+ @template.url_for params
39
+ end
40
+
41
+ private
42
+
43
+ def params_for(page)
44
+ page_params = Rack::Utils.parse_nested_query("#{@param_name}=#{page}")
45
+ page_params = @params.deep_merge(page_params)
46
+
47
+ if !Kaminari.config.params_on_first_page && (page <= 1)
48
+ # This converts a hash:
49
+ # from: {other: "params", page: 1}
50
+ # to: {other: "params", page: nil}
51
+ # (when @param_name == "page")
52
+ #
53
+ # from: {other: "params", user: {name: "yuki", page: 1}}
54
+ # to: {other: "params", user: {name: "yuki", page: nil}}
55
+ # (when @param_name == "user[page]")
56
+ @param_name.to_s.scan(/[\w\.]+/)[0..-2].inject(page_params){|h, k| h[k] }[$&] = nil
57
+ end
58
+
59
+ page_params
60
+ end
61
+
62
+ def partial_path
63
+ [
64
+ @views_prefix,
65
+ "kaminari",
66
+ @theme,
67
+ self.class.name.demodulize.underscore
68
+ ].compact.join("/")
69
+ end
70
+ end
71
+
72
+ # Tag that contains a link
73
+ module Link
74
+ # target page number
75
+ def page
76
+ raise 'Override page with the actual page value to be a Page.'
77
+ end
78
+ # the link's href
79
+ def url
80
+ page_url_for page
81
+ end
82
+ def to_s(locals = {}) #:nodoc:
83
+ locals[:url] = url
84
+ super locals
85
+ end
86
+ end
87
+
88
+ # A page
89
+ class Page < Tag
90
+ include Link
91
+ # target page number
92
+ def page
93
+ @options[:page]
94
+ end
95
+ def to_s(locals = {}) #:nodoc:
96
+ locals[:page] = page
97
+ super locals
98
+ end
99
+ end
100
+
101
+ # Link with page number that appears at the leftmost
102
+ class FirstPage < Tag
103
+ include Link
104
+ def page #:nodoc:
105
+ 1
106
+ end
107
+ end
108
+
109
+ # Link with page number that appears at the rightmost
110
+ class LastPage < Tag
111
+ include Link
112
+ def page #:nodoc:
113
+ @options[:total_pages]
114
+ end
115
+ end
116
+
117
+ # The "previous" page of the current page
118
+ class PrevPage < Tag
119
+ include Link
120
+ def page #:nodoc:
121
+ @options[:current_page] - 1
122
+ end
123
+ end
124
+
125
+ # The "next" page of the current page
126
+ class NextPage < Tag
127
+ include Link
128
+ def page #:nodoc:
129
+ @options[:current_page] + 1
130
+ end
131
+ end
132
+
133
+ # Non-link tag that stands for skipped pages...
134
+ class Gap < Tag
135
+ end
136
+ end
137
+ end