kaminari-core 1.0.0.beta2

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