pagy 3.8.2 → 9.4.0

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 (152) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/apps/calendar.ru +737 -0
  4. data/apps/demo.ru +449 -0
  5. data/apps/index.rb +7 -0
  6. data/apps/keyset_ar.ru +228 -0
  7. data/apps/keyset_s.ru +220 -0
  8. data/apps/rails.ru +217 -0
  9. data/apps/repro.ru +182 -0
  10. data/bin/pagy +98 -0
  11. data/config/pagy.rb +220 -0
  12. data/javascripts/pagy.d.ts +5 -0
  13. data/javascripts/pagy.min.js +4 -0
  14. data/javascripts/pagy.min.js.map +10 -0
  15. data/javascripts/pagy.mjs +100 -0
  16. data/lib/optimist.rb +1022 -0
  17. data/lib/pagy/b64.rb +33 -0
  18. data/lib/pagy/backend.rb +30 -19
  19. data/lib/pagy/calendar/day.rb +41 -0
  20. data/lib/pagy/calendar/month.rb +42 -0
  21. data/lib/pagy/calendar/quarter.rb +49 -0
  22. data/lib/pagy/calendar/unit.rb +103 -0
  23. data/lib/pagy/calendar/week.rb +39 -0
  24. data/lib/pagy/calendar/year.rb +35 -0
  25. data/lib/pagy/calendar.rb +84 -0
  26. data/lib/pagy/console.rb +23 -0
  27. data/lib/pagy/countless.rb +27 -22
  28. data/lib/pagy/exceptions.rb +16 -13
  29. data/lib/pagy/extras/arel.rb +11 -14
  30. data/lib/pagy/extras/array.rb +12 -16
  31. data/lib/pagy/extras/bootstrap.rb +83 -41
  32. data/lib/pagy/extras/bulma.rb +79 -46
  33. data/lib/pagy/extras/calendar.rb +79 -0
  34. data/lib/pagy/extras/countless.rb +20 -25
  35. data/lib/pagy/extras/elasticsearch_rails.rb +59 -38
  36. data/lib/pagy/extras/gearbox.rb +55 -0
  37. data/lib/pagy/extras/headers.rb +38 -23
  38. data/lib/pagy/extras/i18n.rb +19 -18
  39. data/lib/pagy/extras/js_tools.rb +70 -0
  40. data/lib/pagy/extras/jsonapi.rb +88 -0
  41. data/lib/pagy/extras/keyset.rb +30 -0
  42. data/lib/pagy/extras/limit.rb +63 -0
  43. data/lib/pagy/extras/meilisearch.rb +57 -0
  44. data/lib/pagy/extras/metadata.rb +32 -27
  45. data/lib/pagy/extras/overflow.rb +61 -53
  46. data/lib/pagy/extras/pagy.rb +82 -0
  47. data/lib/pagy/extras/searchkick.rb +52 -41
  48. data/lib/pagy/extras/size.rb +40 -0
  49. data/lib/pagy/extras/standalone.rb +60 -0
  50. data/lib/pagy/extras/trim.rb +19 -13
  51. data/lib/pagy/frontend.rb +76 -51
  52. data/lib/pagy/i18n.rb +167 -0
  53. data/lib/pagy/keyset/active_record.rb +44 -0
  54. data/lib/pagy/keyset/sequel.rb +57 -0
  55. data/lib/pagy/keyset.rb +118 -0
  56. data/lib/pagy/shared_methods.rb +26 -0
  57. data/lib/pagy/url_helpers.rb +26 -0
  58. data/lib/pagy.rb +91 -37
  59. data/locales/ar.yml +29 -0
  60. data/locales/be.yml +25 -0
  61. data/locales/bg.yml +21 -0
  62. data/locales/bs.yml +25 -0
  63. data/locales/ca.yml +21 -0
  64. data/locales/ckb.yml +18 -0
  65. data/locales/cs.yml +23 -0
  66. data/locales/da.yml +21 -0
  67. data/locales/de.yml +21 -0
  68. data/locales/dz.yml +17 -0
  69. data/locales/en.yml +21 -0
  70. data/{lib/locales → locales}/es.yml +11 -12
  71. data/locales/fr.yml +21 -0
  72. data/locales/hr.yml +25 -0
  73. data/locales/id.yml +19 -0
  74. data/locales/it.yml +21 -0
  75. data/locales/ja.yml +19 -0
  76. data/locales/km.yml +19 -0
  77. data/locales/ko.yml +17 -0
  78. data/locales/nb.yml +21 -0
  79. data/locales/nl.yml +21 -0
  80. data/locales/nn.yml +21 -0
  81. data/locales/pl.yml +25 -0
  82. data/{lib/locales → locales}/pt-BR.yml +11 -12
  83. data/locales/pt.yml +21 -0
  84. data/locales/ru.yml +25 -0
  85. data/locales/sk.yml +23 -0
  86. data/locales/sr.yml +25 -0
  87. data/locales/sv-SE.yml +21 -0
  88. data/locales/sv.yml +21 -0
  89. data/locales/sw.yml +25 -0
  90. data/locales/ta.yml +21 -0
  91. data/locales/tr.yml +19 -0
  92. data/locales/uk.yml +25 -0
  93. data/locales/vi.yml +17 -0
  94. data/locales/zh-CN.yml +17 -0
  95. data/locales/zh-HK.yml +17 -0
  96. data/locales/zh-TW.yml +17 -0
  97. data/stylesheets/pagy.css +46 -0
  98. data/stylesheets/pagy.scss +48 -0
  99. data/stylesheets/pagy.tailwind.css +21 -0
  100. metadata +95 -67
  101. data/lib/config/pagy.rb +0 -170
  102. data/lib/javascripts/pagy.js +0 -106
  103. data/lib/locales/README.md +0 -35
  104. data/lib/locales/bg.yml +0 -22
  105. data/lib/locales/ca.yml +0 -22
  106. data/lib/locales/da.yml +0 -22
  107. data/lib/locales/de.yml +0 -22
  108. data/lib/locales/en.yml +0 -22
  109. data/lib/locales/fr.yml +0 -22
  110. data/lib/locales/id.yml +0 -20
  111. data/lib/locales/it.yml +0 -22
  112. data/lib/locales/ja.yml +0 -20
  113. data/lib/locales/km.yml +0 -19
  114. data/lib/locales/ko.yml +0 -20
  115. data/lib/locales/nb.yml +0 -22
  116. data/lib/locales/nl.yml +0 -22
  117. data/lib/locales/pl.yml +0 -24
  118. data/lib/locales/ru.yml +0 -24
  119. data/lib/locales/sv-SE.yml +0 -23
  120. data/lib/locales/sv.yml +0 -23
  121. data/lib/locales/tr.yml +0 -20
  122. data/lib/locales/utils/i18n.rb +0 -25
  123. data/lib/locales/utils/loader.rb +0 -34
  124. data/lib/locales/utils/p11n.rb +0 -80
  125. data/lib/locales/zh-CN.yml +0 -20
  126. data/lib/locales/zh-HK.yml +0 -20
  127. data/lib/locales/zh-TW.yml +0 -20
  128. data/lib/pagy/extras/foundation.rb +0 -57
  129. data/lib/pagy/extras/items.rb +0 -65
  130. data/lib/pagy/extras/materialize.rb +0 -59
  131. data/lib/pagy/extras/navs.rb +0 -38
  132. data/lib/pagy/extras/pagy_search.rb +0 -18
  133. data/lib/pagy/extras/semantic.rb +0 -55
  134. data/lib/pagy/extras/shared.rb +0 -53
  135. data/lib/pagy/extras/support.rb +0 -29
  136. data/lib/pagy/extras/uikit.rb +0 -62
  137. data/lib/templates/bootstrap_nav.html.erb +0 -24
  138. data/lib/templates/bootstrap_nav.html.haml +0 -34
  139. data/lib/templates/bootstrap_nav.html.slim +0 -34
  140. data/lib/templates/bulma_nav.html.erb +0 -24
  141. data/lib/templates/bulma_nav.html.haml +0 -32
  142. data/lib/templates/bulma_nav.html.slim +0 -32
  143. data/lib/templates/foundation_nav.html.erb +0 -24
  144. data/lib/templates/foundation_nav.html.haml +0 -34
  145. data/lib/templates/foundation_nav.html.slim +0 -34
  146. data/lib/templates/nav.html.erb +0 -22
  147. data/lib/templates/nav.html.haml +0 -30
  148. data/lib/templates/nav.html.slim +0 -29
  149. data/lib/templates/uikit_nav.html.erb +0 -15
  150. data/lib/templates/uikit_nav.html.haml +0 -28
  151. data/lib/templates/uikit_nav.html.slim +0 -28
  152. data/pagy.gemspec +0 -16
@@ -1,73 +1,81 @@
1
- # See the Pagy documentation: https://ddnexus.github.io/pagy/extras/overflow
2
- # encoding: utf-8
1
+ # See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/overflow
3
2
  # frozen_string_literal: true
4
3
 
5
- class Pagy
4
+ class Pagy # :nodoc:
5
+ DEFAULT[:overflow] = :empty_page
6
6
 
7
- OVERFLOW = true
8
-
9
- VARS[:overflow] = :empty_page
10
-
11
- def overflow?; @overflow end
7
+ # Handles OverflowError exceptions for different classes with different options
8
+ module OverflowExtra
9
+ # Support for Pagy class
10
+ module PagyOverride
11
+ # Is the requested page overflowing?
12
+ def overflow?
13
+ @overflow
14
+ end
12
15
 
13
- alias_method :initialize_without_overflow, :initialize
14
- def initialize_with_overflow(vars)
15
- @overflow ||= false # don't override if :last_page re-run the method after an overflow
16
- initialize_without_overflow(vars)
17
- rescue OverflowError
18
- @overflow = true # add the overflow flag
19
- case @vars[:overflow]
20
- when :exception
21
- raise # same as without the extra
22
- when :last_page
23
- initial_page = @vars[:page] # save the very initial page (even after re-run)
24
- initialize(vars.merge!(page: @last)) # re-run with the last page
25
- @vars[:page] = initial_page # restore the inital page
26
- when :empty_page
27
- @offset = @items = @from = @to = 0 # vars relative to the actual page
28
- @prev = @last # prev relative to the actual page
29
- extend(Series) # special series for :empty_page
30
- else
31
- raise VariableError.new(self), "expected :overflow variable in [:last_page, :empty_page, :exception]; got #{@vars[:overflow].inspect}"
32
- end
33
- end
34
- alias_method :initialize, :initialize_with_overflow
16
+ # Add rescue clause for different behaviors
17
+ def initialize(**vars)
18
+ @overflow ||= false # still true if :last_page re-run the method after an overflow
19
+ super
20
+ rescue OverflowError
21
+ @overflow = true # add the overflow flag
22
+ case @vars[:overflow]
23
+ when :exception
24
+ raise # same as without the extra
25
+ when :last_page
26
+ requested_page = @vars[:page] # save the requested page (even after re-run)
27
+ initialize(**vars, page: @last) # re-run with the last page
28
+ @vars[:page] = requested_page # restore the requested page
29
+ when :empty_page
30
+ @in = @from = @to = 0 # vars relative to the actual page
31
+ if defined?(::Pagy::Calendar::Unit) \
32
+ && is_a?(Calendar::Unit) # only for Calendar::Units instances
33
+ edge = @order == :asc ? @final : @initial # get the edge of the overflow side (neat, but any time would do)
34
+ @from = @to = edge # set both to the edge time (a >=&&< query will get no records)
35
+ end
36
+ @prev = @last # prev relative to the actual page
37
+ extend Series # special series for :empty_page
38
+ else
39
+ raise VariableError.new(self, :overflow, 'to be in [:last_page, :empty_page, :exception]', @vars[:overflow])
40
+ end
41
+ end
35
42
 
36
- module Series
37
- def series(size=@vars[:size])
38
- @page = @last # series for last page
39
- super(size).tap do |s| # call original series
40
- s[s.index(@page.to_s)] = @page # string to integer (i.e. no current page)
41
- @page = @vars[:page] # restore the actual page
43
+ # Special series for empty page
44
+ module Series
45
+ def series(*, **)
46
+ @page = @last # series for last page
47
+ super.tap do |s| # call original series
48
+ s[s.index(@page.to_s)] = @page # string to integer (i.e. no current page)
49
+ @page = @vars[:page] # restore the actual page
50
+ end
51
+ end
42
52
  end
43
53
  end
44
- end
54
+ Pagy.prepend PagyOverride
55
+ Pagy::Calendar::Unit.prepend PagyOverride if defined?(::Pagy::Calendar::Unit)
45
56
 
46
-
47
- # support for Pagy::Countless
48
- if defined?(Pagy::Countless)
49
- class Countless
50
-
51
- alias_method :finalize_without_overflow, :finalize
52
- def finalize_with_overflow(items)
57
+ # Support for Pagy::Countless class
58
+ module CountlessOverride
59
+ # Add rescue clause for different behaviors
60
+ def finalize(fetched_size)
53
61
  @overflow = false
54
- finalize_without_overflow(items)
62
+ super
55
63
  rescue OverflowError
56
- @overflow = true # add the overflow flag
64
+ @overflow = true # add the overflow flag
57
65
  case @vars[:overflow]
58
66
  when :exception
59
- raise # same as without the extra
67
+ raise # same as without the extra
60
68
  when :empty_page
61
- @offset = @items = @from = @to = 0 # vars relative to the actual page
62
- @vars[:size] = [] # no page in the series
69
+ @offset = @limit = @from = @to = 0 # vars relative to the actual page
70
+ @vars[:size] = 0 # no page in the series
63
71
  self
64
72
  else
65
- raise VariableError.new(self), "expected :overflow variable in [:empty_page, :exception]; got #{@vars[:overflow].inspect}"
73
+ raise VariableError.new(self, :overflow, 'to be in [:empty_page, :exception]', @vars[:overflow])
66
74
  end
67
75
  end
68
- alias_method :finalize, :finalize_with_overflow
69
-
70
76
  end
77
+ # :nocov:
78
+ Pagy::Countless.prepend CountlessOverride if defined?(::Pagy::Countless)
79
+ # :nocov:
71
80
  end
72
-
73
81
  end
@@ -0,0 +1,82 @@
1
+ # See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/pagy
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'js_tools'
5
+
6
+ class Pagy # :nodoc:
7
+ # Frontend modules are specially optimized for performance.
8
+ # The resulting code may not look very elegant, but produces the best benchmarks
9
+ module PagyExtra
10
+ # pagy_nav is defined in the Frontend itself
11
+ # Javascript pagination: it returns a nav with a data-pagy attribute used by the pagy.js file
12
+ def pagy_nav_js(pagy, id: nil, aria_label: nil, **vars)
13
+ sequels = pagy.sequels(**vars)
14
+ id = %( id="#{id}") if id
15
+ a = pagy_anchor(pagy, **vars)
16
+ tokens = { 'before' => prev_a(pagy, a),
17
+ 'a' => a.(PAGE_TOKEN, LABEL_TOKEN),
18
+ 'current' => %(<a class="current" role="link" aria-current="page" aria-disabled="true">#{
19
+ LABEL_TOKEN}</a>),
20
+ 'gap' => %(<a class="gap" role="link" aria-disabled="true">#{pagy_t('pagy.gap')}</a>),
21
+ 'after' => next_a(pagy, a) }
22
+
23
+ %(<nav#{id} class="#{'pagy-rjs ' if sequels.size > 1}pagy nav-js" #{
24
+ nav_aria_label(pagy, aria_label:)} #{
25
+ pagy_data(pagy, :nav, tokens, sequels, pagy.label_sequels(sequels))
26
+ }></nav>)
27
+ end
28
+
29
+ # Javascript combo pagination: it returns a nav with a data-pagy attribute used by the pagy.js file
30
+ def pagy_combo_nav_js(pagy, id: nil, aria_label: nil, **vars)
31
+ id = %( id="#{id}") if id
32
+ a = pagy_anchor(pagy, **vars)
33
+ pages = pagy.pages
34
+
35
+ page_input = %(<input name="page" type="number" min="1" max="#{pages}" value="#{pagy.page}" aria-current="page" ) <<
36
+ %(style="text-align: center; width: #{pages.to_s.length + 1}rem; padding: 0;">#{JSTools::A_TAG})
37
+
38
+ %(<nav#{id} class="pagy combo-nav-js" #{
39
+ nav_aria_label(pagy, aria_label:)} #{
40
+ pagy_data(pagy, :combo, pagy_url_for(pagy, PAGE_TOKEN, **vars))}>#{
41
+ prev_a(pagy, a)
42
+ }<label>#{
43
+ pagy_t('pagy.combo_nav_js', page_input:, pages:)
44
+ }</label>#{
45
+ next_a(pagy, a)
46
+ }</nav>)
47
+ end
48
+
49
+ # Return the previous page URL string or nil
50
+ def pagy_prev_url(pagy, **vars)
51
+ pagy_url_for(pagy, pagy.prev, **vars) if pagy.prev
52
+ end
53
+
54
+ # Return the next page URL string or nil
55
+ def pagy_next_url(pagy, **vars)
56
+ pagy_url_for(pagy, pagy.next, **vars) if pagy.next
57
+ end
58
+
59
+ # Return the enabled/disabled previous page anchor tag
60
+ def pagy_prev_a(pagy, text: pagy_t('pagy.prev'), aria_label: pagy_t('pagy.aria_label.prev'), **vars)
61
+ a = pagy_anchor(pagy, **vars)
62
+ prev_a(pagy, a, text:, aria_label:)
63
+ end
64
+
65
+ # Return the enabled/disabled next page anchor tag
66
+ def pagy_next_a(pagy, text: pagy_t('pagy.next'), aria_label: pagy_t('pagy.aria_label.next'), **vars)
67
+ a = pagy_anchor(pagy, **vars)
68
+ next_a(pagy, a, text:, aria_label:)
69
+ end
70
+
71
+ # Conditionally return the previous page link tag
72
+ def pagy_prev_link(pagy, **vars)
73
+ %(<link href="#{pagy_url_for(pagy, pagy.prev, **vars)}"/>) if pagy.prev
74
+ end
75
+
76
+ # Conditionally return the next page link tag
77
+ def pagy_next_link(pagy, **vars)
78
+ %(<link href="#{pagy_url_for(pagy, pagy.next, **vars)}"/>) if pagy.next
79
+ end
80
+ end
81
+ Frontend.prepend PagyExtra
82
+ end
@@ -1,48 +1,59 @@
1
- # See the Pagy documentation: https://ddnexus.github.io/pagy/extras/searchkick
2
- # encoding: utf-8
1
+ # See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/searchkick
3
2
  # frozen_string_literal: true
4
3
 
5
- require 'pagy/extras/pagy_search'
6
-
7
- class Pagy
8
-
9
- # used by the items extra
10
- SEARCHKICK = true
11
-
12
- # create a Pagy object from a Searchkick::Results object
13
- def self.new_from_searchkick(results, vars={})
14
- vars[:items] = results.options[:per_page]
15
- vars[:page] = results.options[:page]
16
- vars[:count] = results.total_count
17
- new(vars)
18
- end
19
-
20
- # Add specialized backend methods to paginate Searchkick::Results
21
- module Backend ; private
22
-
23
- # Return Pagy object and results
24
- def pagy_searchkick(pagy_search_args, vars={})
25
- model, search_args, block, *called = pagy_search_args
26
- vars = pagy_searchkick_get_vars(nil, vars)
27
- search_args[-1][:per_page] = vars[:items]
28
- search_args[-1][:page] = vars[:page]
29
- results = model.search(*search_args, &block)
30
- vars[:count] = results.total_count
31
- pagy = Pagy.new(vars)
32
- # with :last_page overflow we need to re-run the method in order to get the hits
33
- if defined?(OVERFLOW) && pagy.overflow? && pagy.vars[:overflow] == :last_page
34
- return pagy_searchkick(pagy_search_args, vars.merge(page: pagy.page))
4
+ class Pagy # :nodoc:
5
+ DEFAULT[:searchkick_search] ||= :search
6
+ DEFAULT[:searchkick_pagy_search] ||= :pagy_search
7
+
8
+ # Paginate Searchkick::Results objects
9
+ module SearchkickExtra
10
+ module ModelExtension # :nodoc:
11
+ # Return an array used to delay the call of #search
12
+ # after the pagination variables are merged to the options.
13
+ # It also pushes to the same array an optional method call.
14
+ def pagy_searchkick(term = '*', **options, &block)
15
+ [self, term, options, block].tap do |args|
16
+ args.define_singleton_method(:method_missing) { |*a| args += a }
17
+ end
35
18
  end
36
- return pagy, called.empty? ? results : results.send(*called)
19
+ alias_method Pagy::DEFAULT[:searchkick_pagy_search], :pagy_searchkick
37
20
  end
38
-
39
- # Sub-method called only by #pagy_searchkick: here for easy customization of variables by overriding
40
- # the _collection argument is not available when the method is called
41
- def pagy_searchkick_get_vars(_collection, vars)
42
- vars[:items] ||= VARS[:items]
43
- vars[:page] ||= (params[ vars[:page_param] || VARS[:page_param] ] || 1).to_i
44
- vars
21
+ Pagy::Searchkick = ModelExtension
22
+
23
+ # Additions for the Pagy class
24
+ module PagyExtension
25
+ # Create a Pagy object from a Searchkick::Results object
26
+ def new_from_searchkick(results, **vars)
27
+ vars[:limit] = results.options[:per_page]
28
+ vars[:page] = results.options[:page]
29
+ vars[:count] = results.total_count
30
+ new(**vars)
31
+ end
45
32
  end
46
-
33
+ Pagy.extend PagyExtension
34
+
35
+ # Add specialized backend methods to paginate Searchkick::Results
36
+ module BackendAddOn
37
+ private
38
+
39
+ # Return Pagy object and results
40
+ def pagy_searchkick(pagy_search_args, **vars)
41
+ vars[:page] ||= pagy_get_page(vars)
42
+ vars[:limit] ||= pagy_get_limit(vars)
43
+ model, term, options, block, *called = pagy_search_args
44
+ options[:per_page] = vars[:limit]
45
+ options[:page] = vars[:page]
46
+ results = model.send(DEFAULT[:searchkick_search], term, **options, &block)
47
+ vars[:count] = results.total_count
48
+
49
+ pagy = ::Pagy.new(**vars)
50
+ # with :last_page overflow we need to re-run the method in order to get the hits
51
+ return pagy_searchkick(pagy_search_args, **vars, page: pagy.page) \
52
+ if defined?(::Pagy::OverflowExtra) && pagy.overflow? && pagy.vars[:overflow] == :last_page
53
+
54
+ [pagy, called.empty? ? results : results.send(*called)]
55
+ end
56
+ end
57
+ Backend.prepend BackendAddOn
47
58
  end
48
59
  end
@@ -0,0 +1,40 @@
1
+ # See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/size
2
+ # frozen_string_literal: true
3
+
4
+ class Pagy # :nodoc:
5
+ # Implement the legacy bar using the array size.
6
+ # Unless you have very specific requirements, use the faster and better looking default bar.
7
+ module SizeExtra
8
+ # Implements the old series algorithm
9
+ def series(size: @vars[:size], **_)
10
+ return super unless size.is_a?(Array)
11
+ return [] if size == []
12
+ raise VariableError.new(self, :size, 'to be an Array of 4 Integers or []', size) \
13
+ unless size.is_a?(Array) && size.size == 4 && size.all? { |num| !num.negative? rescue false } # rubocop:disable Style/RescueModifier
14
+
15
+ [].tap do |series|
16
+ # This algorithm is up to ~5x faster and ~2.3x lighter than the previous one (pagy < 4.3)
17
+ # However the behavior of the legacy nav bar was taken straight from WillPaginate and Kaminari:
18
+ # it's ill-concieved and complicates the experience of devs and users.
19
+ left_gap_start = 1 + size[0]
20
+ left_gap_end = @page - size[1] - 1
21
+ right_gap_start = @page + size[2] + 1
22
+ right_gap_end = @last - size[3]
23
+ left_gap_end = right_gap_end if left_gap_end > right_gap_end
24
+ right_gap_start = left_gap_start if left_gap_start > right_gap_start
25
+ start = 1
26
+ if (left_gap_end - left_gap_start).positive?
27
+ series.push(*start...left_gap_start, :gap)
28
+ start = left_gap_end + 1
29
+ end
30
+ if (right_gap_end - right_gap_start).positive?
31
+ series.push(*start...right_gap_start, :gap)
32
+ start = right_gap_end + 1
33
+ end
34
+ series.push(*start..@last)
35
+ series[series.index(@page)] = @page.to_s
36
+ end
37
+ end
38
+ end
39
+ prepend SizeExtra
40
+ end
@@ -0,0 +1,60 @@
1
+ # See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/standalone
2
+ # frozen_string_literal: true
3
+
4
+ require 'uri'
5
+
6
+ class Pagy # :nodoc:
7
+ # Use pagy without any request object, nor Rack environment/gem, nor any defined params method,
8
+ # even in the irb/rails console without any app or config.
9
+ module StandaloneExtra
10
+ # Extracted from Rack::Utils and reformatted for rubocop
11
+ # :nocov:
12
+ module QueryUtils
13
+ module_function
14
+
15
+ def build_nested_query(value, prefix = nil)
16
+ case value
17
+ when Array
18
+ value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
19
+ when Hash
20
+ value.map do |k, v|
21
+ build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
22
+ end.delete_if(&:empty?).join('&')
23
+ when nil
24
+ escape(prefix)
25
+ else
26
+ raise ArgumentError, 'value must be a Hash' if prefix.nil?
27
+
28
+ "#{escape(prefix)}=#{escape(value)}"
29
+ end
30
+ end
31
+
32
+ def escape(str)
33
+ URI.encode_www_form_component(str)
34
+ end
35
+ end
36
+ # :nocov:
37
+
38
+ # Return the URL for the page. If there is no pagy.vars[:url]
39
+ # it works exactly as the regular #pagy_url_for, relying on the params method and Rack.
40
+ # If there is a defined pagy.vars[:url] variable it does not need the params method nor Rack.
41
+ def pagy_url_for(pagy, page, fragment: nil, **_)
42
+ return super unless pagy.vars[:url]
43
+
44
+ vars = pagy.vars
45
+ params = vars[:params].is_a?(Hash) ? vars[:params].clone : {} # safe when it gets reused
46
+ pagy_set_query_params(page, vars, params)
47
+ params = vars[:params].(params) if vars[:params].is_a?(Proc)
48
+ query_string = "?#{QueryUtils.build_nested_query(params)}"
49
+ "#{vars[:url]}#{query_string}#{fragment}"
50
+ end
51
+ end
52
+ UrlHelpers.prepend StandaloneExtra
53
+
54
+ # Define a dummy params method if it's not already defined in the including module
55
+ module Backend
56
+ def self.included(controller)
57
+ controller.define_method(:params) { {} } unless controller.method_defined?(:params)
58
+ end
59
+ end
60
+ end
@@ -1,23 +1,29 @@
1
- # See the Pagy documentation: https://ddnexus.github.io/pagy/extras/trim
2
- # encoding: utf-8
1
+ # See the Pagy documentation: https://ddnexus.github.io/pagy/docs/extras/trim
3
2
  # frozen_string_literal: true
4
3
 
5
- class Pagy
4
+ class Pagy # :nodoc:
5
+ DEFAULT[:trim_extra] = true # extra enabled by default
6
6
 
7
- module Frontend
7
+ # Remove the page=1 param from the first page link
8
+ module TrimExtra
9
+ # Override the original pagy_anchor.
10
+ # Call the pagy_trim method for page 1 if the trim_extra is enabled
11
+ def pagy_anchor(pagy, **_)
12
+ a_proc = super
13
+ return a_proc unless pagy.vars[:trim_extra]
8
14
 
9
- TRIM = true # boolean used by *_js helpers
15
+ lambda do |page, text = pagy.label_for(page), **opts|
16
+ a = +a_proc.(page, text, **opts)
17
+ return a unless page.to_s == '1'
10
18
 
11
- alias_method :pagy_link_proc_without_trim, :pagy_link_proc
12
- def pagy_link_proc_with_trim(pagy, link_extra='')
13
- link_proc = pagy_link_proc_without_trim(pagy, link_extra)
14
- re = /[?&]#{pagy.vars[:page_param]}=1\b(?!&)|\b#{pagy.vars[:page_param]}=1&/
15
- lambda do |n, text=n, extra=''|
16
- link = link_proc.call(n, text, extra)
17
- n == 1 ? link.sub(re, '') : link
19
+ pagy_trim(pagy, a) # in method for isolated testing
18
20
  end
19
21
  end
20
- alias_method :pagy_link_proc, :pagy_link_proc_with_trim
21
22
 
23
+ # Remove the :page_param param from the first page anchor
24
+ def pagy_trim(pagy, a)
25
+ a.sub!(/[?&]#{pagy.vars[:page_param]}=1\b(?!&)|\b#{pagy.vars[:page_param]}=1&/, '')
26
+ end
22
27
  end
28
+ Frontend.prepend TrimExtra
23
29
  end
data/lib/pagy/frontend.rb CHANGED
@@ -1,74 +1,99 @@
1
- # See Pagy::Frontend API documentation: https://ddnexus.github.io/pagy/api/frontend
2
- # encoding: utf-8
1
+ # See Pagy::Frontend API documentation: https://ddnexus.github.io/pagy/docs/api/frontend
3
2
  # frozen_string_literal: true
4
3
 
5
- require 'yaml'
4
+ require_relative 'i18n'
5
+ require_relative 'url_helpers'
6
6
 
7
7
  class Pagy
8
+ # Used for search and replace, hardcoded also in the pagy.js file
9
+ PAGE_TOKEN = '__pagy_page__'
10
+ LABEL_TOKEN = '__pagy_label__'
8
11
 
9
- PAGE_PLACEHOLDER = '__pagy_page__' # string used for search and replace, hardcoded also in the pagy.js file
10
-
11
- # I18n static hash loaded at startup, used as default alternative to the i18n gem.
12
- # see https://ddnexus.github.io/pagy/api/frontend#i18n
13
- I18n = eval(Pagy.root.join('locales', 'utils', 'i18n.rb').read) #rubocop:disable Security/Eval
12
+ # Frontend modules are specially optimized for performance.
13
+ # The resulting code may not look very elegant, but produces the best benchmarks
14
+ module Frontend
15
+ include UrlHelpers
14
16
 
15
- module Helpers
16
- # This works with all Rack-based frameworks (Sinatra, Padrino, Rails, ...)
17
- def pagy_url_for(page, pagy, url=false)
18
- p_vars = pagy.vars; params = request.GET.merge(p_vars[:params]); params[p_vars[:page_param].to_s] = page
19
- "#{request.base_url if url}#{request.path}?#{Rack::Utils.build_nested_query(pagy_get_params(params))}#{p_vars[:anchor]}"
17
+ # Return a performance optimized lambda to generate the HTML anchor element (a tag)
18
+ # Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
19
+ def pagy_anchor(pagy, anchor_string: nil, **vars)
20
+ anchor_string &&= %( #{anchor_string})
21
+ left, right = %(<a#{anchor_string} href="#{pagy_url_for(pagy, PAGE_TOKEN, **vars)}").split(PAGE_TOKEN, 2)
22
+ # lambda used by all the helpers
23
+ lambda do |page, text = pagy.label_for(page), classes: nil, aria_label: nil|
24
+ classes = %( class="#{classes}") if classes
25
+ aria_label = %( aria-label="#{aria_label}") if aria_label
26
+ %(#{left}#{page}#{right}#{classes}#{aria_label}>#{text}</a>)
27
+ end
20
28
  end
21
29
 
22
- # Sub-method called only by #pagy_url_for: here for easy customization of params by overriding
23
- def pagy_get_params(params) params end
24
- end
25
-
26
- # All the code here has been optimized for performance: it may not look very pretty
27
- # (as most code dealing with many long strings), but its performance makes it very sexy! ;)
28
- module Frontend
29
-
30
- include Helpers
30
+ # Return examples: "Displaying items 41-60 of 324 in total" or "Displaying Products 41-60 of 324 in total"
31
+ def pagy_info(pagy, id: nil, item_name: nil)
32
+ id = %( id="#{id}") if id
33
+ p_count = pagy.count
34
+ key = if p_count.zero?
35
+ 'pagy.info.no_items'
36
+ elsif pagy.pages == 1
37
+ 'pagy.info.single_page'
38
+ else
39
+ 'pagy.info.multiple_pages'
40
+ end
31
41
 
32
- EMPTY = '' # EMPTY + 'string' is almost as fast as +'string' but is also 1.9 compatible
33
- MARK = PAGE_PLACEHOLDER # backward compatibility in case of helper-overriding in legacy apps
42
+ %(<span#{id} class="pagy info">#{
43
+ pagy_t key, item_name: item_name || pagy_t('pagy.item_name', count: p_count),
44
+ count: p_count, from: pagy.from, to: pagy.to
45
+ }</span>)
46
+ end
34
47
 
35
48
  # Generic pagination: it returns the html with the series of links to the pages
36
- def pagy_nav(pagy)
37
- link, p_prev, p_next = pagy_link_proc(pagy), pagy.prev, pagy.next
49
+ def pagy_nav(pagy, id: nil, aria_label: nil, **vars)
50
+ id = %( id="#{id}") if id
51
+ a = pagy_anchor(pagy, **vars)
38
52
 
39
- html = EMPTY + (p_prev ? %(<span class="page prev">#{link.call p_prev, pagy_t('pagy.nav.prev'), 'aria-label="previous"'}</span> )
40
- : %(<span class="page prev disabled">#{pagy_t('pagy.nav.prev')}</span> ))
41
- pagy.series.each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
42
- html << if item.is_a?(Integer); %(<span class="page">#{link.call item}</span> ) # page link
43
- elsif item.is_a?(String) ; %(<span class="page active">#{item}</span> ) # current page
44
- elsif item == :gap ; %(<span class="page gap">#{pagy_t('pagy.nav.gap')}</span> ) # page gap
53
+ html = %(<nav#{id} class="pagy nav" #{nav_aria_label(pagy, aria_label:)}>#{
54
+ prev_a(pagy, a)})
55
+ pagy.series(**vars).each do |item| # series example: [1, :gap, 7, 8, "9", 10, 11, :gap, 36]
56
+ html << case item
57
+ when Integer
58
+ a.(item)
59
+ when String
60
+ %(<a role="link" aria-disabled="true" aria-current="page" class="current">#{pagy.label_for(item)}</a>)
61
+ when :gap
62
+ %(<a role="link" aria-disabled="true" class="gap">#{pagy_t('pagy.gap')}</a>)
63
+ else
64
+ raise InternalError, "expected item types in series to be Integer, String or :gap; got #{item.inspect}"
45
65
  end
46
66
  end
47
- html << (p_next ? %(<span class="page next">#{link.call p_next, pagy_t('pagy.nav.next'), 'aria-label="next"'}</span>)
48
- : %(<span class="page next disabled">#{pagy_t('pagy.nav.next')}</span>))
49
- %(<nav class="pagy-nav pagination" role="navigation" aria-label="pager">#{html}</nav>)
67
+ html << %(#{next_a(pagy, a)}</nav>)
50
68
  end
51
69
 
52
- # Return examples: "Displaying items 41-60 of 324 in total" of "Displaying Products 41-60 of 324 in total"
53
- def pagy_info(pagy, item_name=nil)
54
- path = if (count = pagy.count) == 0 ; 'pagy.info.no_items'
55
- else pagy.pages == 1 ? 'pagy.info.single_page' : 'pagy.info.multiple_pages'
56
- end
57
- pagy_t(path, item_name: item_name || pagy_t(pagy.vars[:i18n_key], count: count), count: count, from: pagy.from, to: pagy.to)
70
+ # Similar to I18n.t: just ~18x faster using ~10x less memory
71
+ # (@pagy_locale explicitly initialized in order to avoid warning)
72
+ def pagy_t(key, **opts)
73
+ Pagy::I18n.translate(@pagy_locale ||= nil, key, **opts)
58
74
  end
59
75
 
60
- # Returns a performance optimized proc to generate the HTML links
61
- # Benchmarked on a 20 link nav: it is ~22x faster and uses ~18x less memory than rails' link_to
62
- def pagy_link_proc(pagy, link_extra='')
63
- p_prev, p_next = pagy.prev, pagy.next
64
- a, b = %(<a href="#{pagy_url_for(PAGE_PLACEHOLDER, pagy)}" #{pagy.vars[:link_extra]} #{link_extra}).split(PAGE_PLACEHOLDER, 2)
65
- lambda {|n, text=n, extra=''| "#{a}#{n}#{b}#{ if n == p_prev ; ' rel="prev"'
66
- elsif n == p_next ; ' rel="next"'
67
- else '' end } #{extra}>#{text}</a>"}
76
+ private
77
+
78
+ def nav_aria_label(pagy, aria_label: nil)
79
+ aria_label ||= pagy_t('pagy.aria_label.nav', count: pagy.pages)
80
+ %(aria-label="#{aria_label}")
68
81
  end
69
82
 
70
- # Similar to I18n.t: just ~18x faster using ~10x less memory
71
- def pagy_t(path, vars={}) Pagy::I18n.t(@pagy_locale||=nil, path, vars) end
83
+ def prev_a(pagy, a, text: pagy_t('pagy.prev'), aria_label: pagy_t('pagy.aria_label.prev'))
84
+ if (p_prev = pagy.prev)
85
+ a.(p_prev, text, aria_label:)
86
+ else
87
+ %(<a role="link" aria-disabled="true" aria-label="#{aria_label}">#{text}</a>)
88
+ end
89
+ end
72
90
 
91
+ def next_a(pagy, a, text: pagy_t('pagy.next'), aria_label: pagy_t('pagy.aria_label.next'))
92
+ if (p_next = pagy.next)
93
+ a.(p_next, text, aria_label:)
94
+ else
95
+ %(<a role="link" aria-disabled="true" aria-label="#{aria_label}">#{text}</a>)
96
+ end
97
+ end
73
98
  end
74
99
  end