pagy 9.4.0 → 43.2.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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/apps/calendar.ru +548 -552
  3. data/apps/demo.ru +224 -181
  4. data/apps/index.rb +3 -1
  5. data/apps/keynav+root_key.ru +321 -0
  6. data/apps/keynav.ru +260 -0
  7. data/apps/{keyset_ar.ru → keyset.ru} +26 -32
  8. data/apps/{keyset_s.ru → keyset_sequel.ru} +23 -29
  9. data/apps/rails.ru +51 -48
  10. data/apps/repro.ru +51 -47
  11. data/bin/pagy +20 -21
  12. data/config/pagy.rb +35 -207
  13. data/javascripts/ai_widget.js +90 -0
  14. data/javascripts/pagy.js +153 -0
  15. data/javascripts/pagy.js.map +10 -0
  16. data/javascripts/pagy.min.js +2 -4
  17. data/javascripts/pagy.mjs +113 -63
  18. data/javascripts/wand.js +1172 -0
  19. data/lib/pagy/classes/calendar/calendar.rb +96 -0
  20. data/lib/pagy/{calendar → classes/calendar}/day.rb +7 -11
  21. data/lib/pagy/{calendar → classes/calendar}/month.rb +5 -10
  22. data/lib/pagy/{calendar → classes/calendar}/quarter.rb +10 -15
  23. data/lib/pagy/classes/calendar/unit.rb +93 -0
  24. data/lib/pagy/{calendar → classes/calendar}/week.rb +5 -9
  25. data/lib/pagy/{calendar → classes/calendar}/year.rb +5 -6
  26. data/lib/pagy/classes/exceptions.rb +25 -0
  27. data/lib/pagy/classes/keyset/active_record.rb +11 -0
  28. data/lib/pagy/classes/keyset/adapters/active_record.rb +50 -0
  29. data/lib/pagy/classes/keyset/adapters/sequel.rb +63 -0
  30. data/lib/pagy/classes/keyset/keynav/active_record.rb +13 -0
  31. data/lib/pagy/classes/keyset/keynav/sequel.rb +13 -0
  32. data/lib/pagy/classes/keyset/keynav.rb +79 -0
  33. data/lib/pagy/classes/keyset/keyset.rb +126 -0
  34. data/lib/pagy/classes/keyset/sequel.rb +11 -0
  35. data/lib/pagy/classes/offset/countish.rb +17 -0
  36. data/lib/pagy/classes/offset/countless.rb +61 -0
  37. data/lib/pagy/classes/offset/offset.rb +56 -0
  38. data/lib/pagy/classes/offset/search.rb +32 -0
  39. data/lib/pagy/classes/request.rb +36 -0
  40. data/lib/pagy/console.rb +3 -20
  41. data/lib/pagy/modules/abilities/configurable.rb +36 -0
  42. data/lib/pagy/modules/abilities/countable.rb +23 -0
  43. data/lib/pagy/modules/abilities/linkable.rb +60 -0
  44. data/lib/pagy/modules/abilities/rangeable.rb +16 -0
  45. data/lib/pagy/modules/abilities/shiftable.rb +12 -0
  46. data/lib/pagy/{b64.rb → modules/b64.rb} +3 -7
  47. data/lib/pagy/modules/console.rb +24 -0
  48. data/lib/pagy/modules/i18n/i18n.rb +48 -0
  49. data/lib/pagy/modules/i18n/p11n/arabic.rb +29 -0
  50. data/lib/pagy/modules/i18n/p11n/east_slavic.rb +26 -0
  51. data/lib/pagy/modules/i18n/p11n/one_other.rb +15 -0
  52. data/lib/pagy/modules/i18n/p11n/one_upto_two_other.rb +15 -0
  53. data/lib/pagy/modules/i18n/p11n/other.rb +13 -0
  54. data/lib/pagy/modules/i18n/p11n/polish.rb +26 -0
  55. data/lib/pagy/modules/i18n/p11n/west_slavic.rb +22 -0
  56. data/lib/pagy/modules/i18n/p11n.rb +16 -0
  57. data/lib/pagy/modules/searcher.rb +17 -0
  58. data/lib/pagy/toolbox/helpers/anchor_tags.rb +25 -0
  59. data/lib/pagy/toolbox/helpers/bootstrap/input_nav_js.rb +24 -0
  60. data/lib/pagy/toolbox/helpers/bootstrap/previous_next_html.rb +18 -0
  61. data/lib/pagy/toolbox/helpers/bootstrap/series_nav.rb +29 -0
  62. data/lib/pagy/toolbox/helpers/bootstrap/series_nav_js.rb +21 -0
  63. data/lib/pagy/toolbox/helpers/bulma/input_nav_js.rb +21 -0
  64. data/lib/pagy/toolbox/helpers/bulma/previous_next_html.rb +19 -0
  65. data/lib/pagy/toolbox/helpers/bulma/series_nav.rb +28 -0
  66. data/lib/pagy/toolbox/helpers/bulma/series_nav_js.rb +20 -0
  67. data/lib/pagy/toolbox/helpers/data_hash.rb +27 -0
  68. data/lib/pagy/toolbox/helpers/headers_hash.rb +29 -0
  69. data/lib/pagy/toolbox/helpers/info_tag.rb +26 -0
  70. data/lib/pagy/toolbox/helpers/input_nav_js.rb +18 -0
  71. data/lib/pagy/toolbox/helpers/limit_tag_js.rb +23 -0
  72. data/lib/pagy/toolbox/helpers/loader.rb +33 -0
  73. data/lib/pagy/toolbox/helpers/page_url.rb +18 -0
  74. data/lib/pagy/toolbox/helpers/series_nav.rb +28 -0
  75. data/lib/pagy/toolbox/helpers/series_nav_js.rb +18 -0
  76. data/lib/pagy/toolbox/helpers/support/a_lambda.rb +35 -0
  77. data/lib/pagy/toolbox/helpers/support/data_pagy_attribute.rb +15 -0
  78. data/lib/pagy/toolbox/helpers/support/nav_aria_label_attribute.rb +12 -0
  79. data/lib/pagy/toolbox/helpers/support/series.rb +38 -0
  80. data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +20 -0
  81. data/lib/pagy/toolbox/helpers/support/wrap_series_nav.rb +17 -0
  82. data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +37 -0
  83. data/lib/pagy/toolbox/helpers/urls_hash.rb +13 -0
  84. data/lib/pagy/toolbox/paginators/calendar.rb +29 -0
  85. data/lib/pagy/toolbox/paginators/countish.rb +37 -0
  86. data/lib/pagy/toolbox/paginators/countless.rb +20 -0
  87. data/lib/pagy/toolbox/paginators/elasticsearch_rails.rb +50 -0
  88. data/lib/pagy/toolbox/paginators/keynav_js.rb +26 -0
  89. data/lib/pagy/toolbox/paginators/keyset.rb +15 -0
  90. data/lib/pagy/toolbox/paginators/meilisearch.rb +31 -0
  91. data/lib/pagy/toolbox/paginators/method.rb +35 -0
  92. data/lib/pagy/toolbox/paginators/offset.rb +18 -0
  93. data/lib/pagy/toolbox/paginators/searchkick.rb +31 -0
  94. data/lib/pagy.rb +59 -97
  95. data/locales/ar.yml +9 -6
  96. data/locales/be.yml +9 -6
  97. data/locales/bg.yml +9 -6
  98. data/locales/bs.yml +9 -6
  99. data/locales/ca.yml +9 -6
  100. data/locales/ckb.yml +8 -6
  101. data/locales/cs.yml +9 -6
  102. data/locales/da.yml +9 -6
  103. data/locales/de.yml +9 -6
  104. data/locales/dz.yml +9 -6
  105. data/locales/en.yml +9 -6
  106. data/locales/es.yml +9 -6
  107. data/locales/fr.yml +9 -6
  108. data/locales/hr.yml +9 -6
  109. data/locales/id.yml +10 -9
  110. data/locales/it.yml +9 -6
  111. data/locales/ja.yml +10 -9
  112. data/locales/km.yml +10 -9
  113. data/locales/ko.yml +9 -6
  114. data/locales/nb.yml +9 -6
  115. data/locales/nl.yml +9 -6
  116. data/locales/nn.yml +9 -6
  117. data/locales/pl.yml +9 -6
  118. data/locales/pt-BR.yml +10 -7
  119. data/locales/pt.yml +10 -7
  120. data/locales/ru.yml +9 -6
  121. data/locales/sk.yml +9 -6
  122. data/locales/sr.yml +9 -6
  123. data/locales/sv-SE.yml +9 -6
  124. data/locales/sv.yml +9 -6
  125. data/locales/sw.yml +12 -9
  126. data/locales/ta.yml +9 -6
  127. data/locales/tr.yml +17 -12
  128. data/locales/uk.yml +9 -6
  129. data/locales/vi.yml +9 -6
  130. data/locales/zh-CN.yml +9 -6
  131. data/locales/zh-HK.yml +9 -6
  132. data/locales/zh-TW.yml +9 -6
  133. data/stylesheets/pagy-tailwind.css +64 -0
  134. data/stylesheets/pagy.css +63 -27
  135. metadata +118 -52
  136. data/javascripts/pagy.min.js.map +0 -10
  137. data/lib/pagy/backend.rb +0 -44
  138. data/lib/pagy/calendar/unit.rb +0 -103
  139. data/lib/pagy/calendar.rb +0 -84
  140. data/lib/pagy/countless.rb +0 -38
  141. data/lib/pagy/exceptions.rb +0 -25
  142. data/lib/pagy/extras/arel.rb +0 -28
  143. data/lib/pagy/extras/array.rb +0 -19
  144. data/lib/pagy/extras/bootstrap.rb +0 -97
  145. data/lib/pagy/extras/bulma.rb +0 -93
  146. data/lib/pagy/extras/calendar.rb +0 -79
  147. data/lib/pagy/extras/countless.rb +0 -32
  148. data/lib/pagy/extras/elasticsearch_rails.rb +0 -71
  149. data/lib/pagy/extras/gearbox.rb +0 -55
  150. data/lib/pagy/extras/headers.rb +0 -54
  151. data/lib/pagy/extras/i18n.rb +0 -26
  152. data/lib/pagy/extras/js_tools.rb +0 -70
  153. data/lib/pagy/extras/jsonapi.rb +0 -88
  154. data/lib/pagy/extras/keyset.rb +0 -30
  155. data/lib/pagy/extras/limit.rb +0 -63
  156. data/lib/pagy/extras/meilisearch.rb +0 -57
  157. data/lib/pagy/extras/metadata.rb +0 -42
  158. data/lib/pagy/extras/overflow.rb +0 -81
  159. data/lib/pagy/extras/pagy.rb +0 -82
  160. data/lib/pagy/extras/searchkick.rb +0 -59
  161. data/lib/pagy/extras/size.rb +0 -40
  162. data/lib/pagy/extras/standalone.rb +0 -60
  163. data/lib/pagy/extras/trim.rb +0 -29
  164. data/lib/pagy/frontend.rb +0 -99
  165. data/lib/pagy/i18n.rb +0 -167
  166. data/lib/pagy/keyset/active_record.rb +0 -44
  167. data/lib/pagy/keyset/sequel.rb +0 -57
  168. data/lib/pagy/keyset.rb +0 -118
  169. data/lib/pagy/shared_methods.rb +0 -26
  170. data/lib/pagy/url_helpers.rb +0 -26
  171. data/stylesheets/pagy.scss +0 -48
  172. data/stylesheets/pagy.tailwind.css +0 -21
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative '../../modules/b64'
5
+
6
+ class Pagy
7
+ # Implement wicked-fast keyset pagination for big data
8
+ class Keyset < Pagy
9
+ path = Pathname.new(__dir__)
10
+ autoload :ActiveRecord, path.join('active_record')
11
+ autoload :Sequel, path.join('sequel')
12
+ autoload :Keynav, path.join('keynav')
13
+
14
+ class TypeError < ::TypeError; end
15
+
16
+ # Initialize Keyset* and Keyset::Keynav* classes and subclasses
17
+ def self.new(set, **)
18
+ # Subclass instances run only the initializer
19
+ if /::(?:ActiveRecord|Sequel)$/.match?(name) # check without triggering autoload
20
+ return allocate.tap { |instance| instance.send(:initialize, set, **) }
21
+ end
22
+
23
+ subclass = if defined?(::ActiveRecord) && set.is_a?(::ActiveRecord::Relation)
24
+ self::ActiveRecord
25
+ elsif defined?(::Sequel) && set.is_a?(::Sequel::Dataset)
26
+ self::Sequel
27
+ else
28
+ raise TypeError, "expected an ActiveRecord::Relation or Sequel::Dataset; got #{set.class}"
29
+ end
30
+ subclass.new(set, **)
31
+ end
32
+
33
+ def initialize(set, **) # rubocop:disable Lint/MissingSuper
34
+ assign_options(**)
35
+ assign_and_check(limit: 1)
36
+ @set = set
37
+ @keyset = @options[:keyset] || extract_keyset
38
+ raise InternalError, 'the set must be ordered' if @keyset.empty?
39
+
40
+ assign_page
41
+ self.next
42
+ end
43
+
44
+ # Return the array of records for the current page
45
+ def records
46
+ @records ||= begin
47
+ ensure_select
48
+ fetch_records
49
+ end
50
+ end
51
+
52
+ # Return the next page (i.e., the cutoff of the current page)
53
+ def next
54
+ records
55
+ return unless @more
56
+
57
+ @next ||= B64.urlsafe_encode(extract_cutoff.to_json)
58
+ end
59
+
60
+ protected
61
+
62
+ def keyset? = true
63
+
64
+ def assign_page
65
+ return unless (@page = @options[:page])
66
+
67
+ @prior_cutoff = JSON.parse(B64.urlsafe_decode(@page))
68
+ end
69
+
70
+ def fetch_records
71
+ apply_where(compose_predicate, arguments_from(@prior_cutoff)) if @prior_cutoff
72
+ @set.limit(@limit + 1).to_a.tap do |records|
73
+ @more = records.size > @limit && !records.pop.nil?
74
+ end
75
+ end
76
+
77
+ # Compose the parameterized predicate used to extract the page records.
78
+ #
79
+ # For example, with a set like Pet.order(animal: :asc, name: :desc, id: :asc)
80
+ # it returns a union of intersections:
81
+ #
82
+ # ("pets"."animal" = :animal AND "pets"."name" = :name AND "pets"."id" > :id) OR
83
+ # ("pets"."animal" = :animal AND "pets"."name" < :name) OR
84
+ # ("pets"."animal" > :animal)
85
+ #
86
+ # When :tuple_comparison is enabled, and if the order is all :asc or all :desc,
87
+ # with a set like Pet.order(:animal, :name, :id) it returns the following string:
88
+ #
89
+ # ("pets"."animal", "pets"."name", "pets"."id") > (:animal, :name, :id)
90
+ #
91
+ def compose_predicate(prefix = nil)
92
+ operator = { asc: '>', desc: '<' }
93
+ directions = @keyset.values
94
+ identifier = quoted_identifiers(@set.model.table_name)
95
+ placeholder = @keyset.to_h { |column| [column, ":#{prefix}#{column}"] }
96
+ if @options[:tuple_comparison] && (directions.all?(:asc) || directions.all?(:desc))
97
+ "(#{identifier.values.join(', ')}) #{operator[directions.first]} (#{placeholder.values.join(', ')})"
98
+ else
99
+ keyset = @keyset.to_a
100
+ union = []
101
+ until keyset.empty?
102
+ last_column, last_direction = keyset.pop
103
+ intersection = +'('
104
+ intersection << (keyset.map { |column, _d| "#{identifier[column]} = #{placeholder[column]}" } \
105
+ << "#{identifier[last_column]} #{operator[last_direction]} #{placeholder[last_column]}").join(' AND ')
106
+ intersection << ')'
107
+ union << intersection
108
+ end
109
+ union.join(' OR ')
110
+ end
111
+ end
112
+
113
+ # Return the prefixed arguments from a cutoff
114
+ def arguments_from(cutoff, prefix = nil)
115
+ attributes = typecast(@keyset.keys.zip(cutoff).to_h)
116
+ prefix ? attributes.transform_keys { |key| :"#{prefix}#{key}" } : attributes
117
+ end
118
+
119
+ # Extract the cutoff from the last record (only called if @more)
120
+ def extract_cutoff
121
+ attributes = keyset_attributes_from(@records.last)
122
+ @options[:pre_serialize]&.(attributes)
123
+ attributes.values
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'adapters/sequel'
4
+
5
+ class Pagy
6
+ class Keyset
7
+ class Sequel < Keyset
8
+ include Adapters::Sequel
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ class Offset
5
+ # Offset pagination with memoized count
6
+ class Countish < Offset
7
+ protected
8
+
9
+ # Return page+count or page+count+epoch
10
+ def compose_page_param(page)
11
+ value = "#{page || 1}+#{@count}"
12
+ value << "+#{@options[:epoch]}" if @options[:epoch]
13
+ EscapedValue.new(value)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ class Offset
5
+ # Offset pagination without a count
6
+ class Countless < Offset
7
+ def initialize(**) # rubocop:disable Lint/MissingSuper
8
+ assign_options(**)
9
+ assign_and_check(limit: 1, page: 1)
10
+ @page = upto_max_pages(@page)
11
+ @last = upto_max_pages(@options[:last]) unless @options[:headless]
12
+ assign_offset
13
+ end
14
+
15
+ def records(collection)
16
+ return super if @options[:headless]
17
+
18
+ fetched = collection.offset(@offset).limit(@limit + 1).to_a # eager load limit + 1
19
+ finalize(fetched.size) # finalize the pagy object
20
+ fetched[0, @limit] # ignore the extra item
21
+ end
22
+
23
+ protected
24
+
25
+ def countless? = true
26
+
27
+ def upto_max_pages(value)
28
+ return value unless value && @options[:max_pages]
29
+
30
+ [value, @options[:max_pages]].min
31
+ end
32
+
33
+ # Finalize the instance variables based on the fetched size
34
+ def finalize(fetched_size)
35
+ # empty records (trigger the right info message for known 0 count)
36
+ @count = 0 if fetched_size.zero? && @page == 1
37
+ return self unless in_range? { fetched_size.positive? || @page == 1 }
38
+
39
+ past = @last && @page < @last # this page has been past
40
+ more = fetched_size > @limit # more pages after this one
41
+ @last = upto_max_pages(more ? @page + 1 : @page) unless past && more
42
+ @in = [fetched_size, @limit].min
43
+ @from = @in.zero? ? 0 : @offset + 1
44
+ @to = @offset + @in
45
+ assign_previous_and_next
46
+ self
47
+ end
48
+
49
+ # Called by false in_range?
50
+ def assign_empty_page_variables
51
+ @in = @from = @to = 0
52
+ target_last = [@page - 1, 1].max
53
+ @last = [@last || target_last, target_last].min
54
+ @previous = @last
55
+ end
56
+
57
+ # Support easy countless page param overriding (for legacy param and behavior)
58
+ def compose_page_param(page) = EscapedValue.new("#{page || 1}+#{@last}")
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../modules/abilities/shiftable'
4
+ require_relative '../../modules/abilities/rangeable'
5
+
6
+ class Pagy
7
+ # Implements Offset Pagination
8
+ class Offset < Pagy
9
+ DEFAULT = { page: 1 }.freeze
10
+
11
+ autoload :Countless, Pathname.new(__dir__).join('countless')
12
+ autoload :Countish, Pathname.new(__dir__).join('countish')
13
+
14
+ include Rangeable
15
+ include Shiftable
16
+
17
+ def initialize(**) # rubocop:disable Lint/MissingSuper
18
+ assign_options(**)
19
+ assign_and_check(limit: 1, count: 0, page: 1)
20
+ assign_last
21
+ assign_offset
22
+ return unless in_range? { @page <= @last }
23
+
24
+ @from = [@offset + 1, @count].min
25
+ @to = [@offset + @limit, @count].min
26
+ @in = [@to - @from + 1, @count].min
27
+ assign_previous_and_next
28
+ end
29
+
30
+ attr_reader :offset, :count, :from, :to, :in, :previous, :last
31
+ alias pages last
32
+
33
+ def records(collection)
34
+ collection.offset(@offset).limit(@limit)
35
+ end
36
+
37
+ protected
38
+
39
+ def offset? = true
40
+
41
+ def assign_last
42
+ @last = [(@count.to_f / @limit).ceil, 1].max
43
+ @last = @options[:max_pages] if @options[:max_pages] && @last > @options[:max_pages]
44
+ end
45
+
46
+ def assign_offset
47
+ @offset = (@limit * (@page - 1))
48
+ end
49
+
50
+ # Called by false in_range?
51
+ def assign_empty_page_variables
52
+ @in = @from = @to = 0 # options relative to the actual page
53
+ @previous = @last # @previous relative to the actual page
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module Search
5
+ class Arguments < Array
6
+ def respond_to_missing? = true
7
+
8
+ def method_missing(*) = push(*)
9
+ end
10
+
11
+ # Collect the search arguments to pass to the actual search
12
+ def pagy_search(term = nil, **options, &block)
13
+ Arguments.new([self, term, options, block])
14
+ end
15
+ end
16
+
17
+ # Search classes do not use OFFSET for querying a DB;
18
+ # however, they use the same positional technique used by Offset
19
+ class SearchBase < Offset
20
+ DEFAULT = { search_method: :search }.freeze
21
+
22
+ def search? = true
23
+ end
24
+
25
+ class ElasticsearchRails < SearchBase; end
26
+
27
+ class Meilisearch < SearchBase
28
+ DEFAULT = { search_method: :ms_search }.freeze
29
+ end
30
+
31
+ class Searchkick < SearchBase; end
32
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Decouple the request from the env, allowing non-rack apps to use pagy by passing a hash.
5
+ # Resolve the :page and :limit options from params.
6
+ class Request
7
+ def initialize(options)
8
+ @options = options
9
+ request = @options[:request]
10
+ @base_url, @path, @params, @cookie =
11
+ if request.is_a?(Hash)
12
+ request.values_at(:base_url, :path, :params, :cookie)
13
+ else
14
+ [request.base_url, request.path, request.GET.merge(request.POST).to_h, request.cookies['pagy']]
15
+ end
16
+ end
17
+
18
+ attr_reader :base_url, :path, :params, :cookie
19
+
20
+ def resolve_page(force_integer: true)
21
+ page_key = @options[:page_key] || DEFAULT[:page_key]
22
+ page = @params.dig(@options[:root_key], page_key) || @params[page_key]
23
+ page = nil if page == '' # fix for app-generated queries like ?page=
24
+ force_integer ? (page || 1).to_i : page
25
+ end
26
+
27
+ def resolve_limit
28
+ limit_key = @options[:limit_key] || DEFAULT[:limit_key]
29
+ return @options[:limit] || DEFAULT[:limit] \
30
+ unless @options[:client_max_limit] &&
31
+ (requested_limit = @params.dig(@options[:root_key], limit_key) || @params[limit_key])
32
+
33
+ [requested_limit.to_i, @options[:client_max_limit]].min
34
+ end
35
+ end
36
+ end
data/lib/pagy/console.rb CHANGED
@@ -1,23 +1,6 @@
1
- # See Pagy::Console API documentation: https://ddnexus.github.io/pagy/docs/api/console
2
1
  # frozen_string_literal: true
3
2
 
4
- require_relative '../pagy' # so you can require just the extra in the console
5
- require_relative 'extras/standalone'
3
+ # Console loader
6
4
 
7
- class Pagy
8
- # Provide a ready to use pagy environment when included in irb/rails console
9
- module Console
10
- # Include Backend, Frontend and set the default URL
11
- def self.included(main)
12
- main.include(Backend)
13
- main.include(Frontend)
14
- DEFAULT[:url] = 'http://www.example.com/subdir'
15
- end
16
-
17
- # Require the extras passed as arguments
18
- def pagy_extras(*extras)
19
- extras.each { |extra| require "pagy/extras/#{extra}" }
20
- puts "Required extras: #{extras.map(&:inspect).join(', ')}"
21
- end
22
- end
23
- end
5
+ require 'pagy'
6
+ include Pagy::Console # rubocop:disable Style/MixinUsage
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Add configurstion methods
5
+ module Configurable
6
+ # Sync the pagy javascript targets
7
+ def sync_javascript(destination, *targets)
8
+ names = %w[pagy.mjs pagy.js pagy.js.map pagy.min.js]
9
+ targets = names if targets.empty?
10
+ targets.each { |filename| FileUtils.cp(ROOT.join('javascripts', filename), destination) }
11
+ (names - targets).each { |filename| FileUtils.rm_f(File.join(destination, filename)) }
12
+ end
13
+
14
+ # Generate the script and style tags to help development
15
+ def dev_tools(wand_scale: 1)
16
+ <<~HTML
17
+ <script id="pagy-ai-widget">
18
+ #{ROOT.join('javascripts/ai_widget.js').read}
19
+ </script>
20
+ <script id="pagy-wand" data-scale="#{wand_scale}">
21
+ #{ROOT.join('javascripts/wand.js').read}
22
+ </script>
23
+ <style id="pagy-wand-default">
24
+ #{ROOT.join('stylesheets/pagy.css').read}
25
+ </style>
26
+ HTML
27
+ end
28
+
29
+ # Setup pagy for using the i18n gem
30
+ def translate_with_the_slower_i18n_gem!
31
+ send(:remove_const, :I18n)
32
+ send(:const_set, :I18n, ::I18n)
33
+ ::I18n.load_path += Dir[ROOT.join('locales/*.yml')]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Provide the helpers to count a collection
5
+ module Countable
6
+ module_function
7
+
8
+ # Get the collection count
9
+ def get_count(collection, options)
10
+ return collection.size if collection.instance_of?(Array)
11
+ return collection.count unless defined?(::ActiveRecord) && collection.is_a?(::ActiveRecord::Relation)
12
+
13
+ count = if options[:count_over] && !collection.group_values.empty?
14
+ # COUNT(*) OVER ()
15
+ sql = Arel.star.count.over(Arel::Nodes::Grouping.new([]))
16
+ collection.unscope(:order).pick(sql).to_i
17
+ else
18
+ collection.count(:all)
19
+ end
20
+ count.is_a?(Hash) ? count.size : count
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+
5
+ class Pagy
6
+ # Support spaces in placeholder params
7
+ class EscapedValue < String; end
8
+
9
+ # Provide the helpers to handle the url and anchor
10
+ module Linkable
11
+ module QueryUtils
12
+ module_function
13
+
14
+ # Extracted from Rack::Utils and reformatted for rubocop
15
+ # Allow unescaped Pagy::RawQueryValue
16
+ def build_nested_query(value, prefix = nil)
17
+ case value
18
+ when Array
19
+ value.map { |v| build_nested_query(v, "#{prefix}[]") }.join('&')
20
+ when Hash
21
+ value.map do |k, v|
22
+ build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
23
+ end.delete_if(&:empty?).join('&')
24
+ when nil
25
+ escape(prefix)
26
+ else
27
+ raise ArgumentError, 'value must be a Hash' if prefix.nil?
28
+
29
+ escaped_value = value.is_a?(EscapedValue) ? value : escape(value)
30
+ "#{escape(prefix)}=#{escaped_value}"
31
+ end
32
+ end
33
+
34
+ def escape(str)
35
+ URI.encode_www_form_component(str)
36
+ end
37
+ end
38
+
39
+ protected
40
+
41
+ # Overriddable by classes with composite page param
42
+ def compose_page_param(page) = page
43
+
44
+ # Return the URL for the page, relying on the Pagy::Request
45
+ def compose_page_url(page, **options)
46
+ root_key, page_key, limit_key, client_max_limit, limit, querify, absolute, path, fragment =
47
+ @options.merge(options)
48
+ .values_at(:root_key, :page_key, :limit_key, :client_max_limit, :limit, :querify, :absolute, :path, :fragment)
49
+ params = @request.params.clone(freeze: false)
50
+ (root_key ? params[root_key] ||= {} : params).tap do |h|
51
+ { page_key => compose_page_param(page),
52
+ limit_key => client_max_limit && limit }.each { |k, v| v ? h[k] = v : h.delete(k) }
53
+ end
54
+ querify&.(params) # Must modify the params: the returned value is ignored
55
+ query_string = QueryUtils.build_nested_query(params).sub(/\A(?=.)/, '?')
56
+ fragment &&= "##{fragment.delete_prefix('#')}"
57
+ "#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Add method supporting range checking, range error and rescue
5
+ module Rangeable
6
+ # Check if in range
7
+ def in_range?
8
+ return @in_range if defined?(@in_range)
9
+ return true if (@in_range = yield)
10
+ raise RangeError.new(self, :page, "in 1..#{@last}", @page) if @options[:raise_range_error]
11
+
12
+ assign_empty_page_variables
13
+ @in_range = false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module Shiftable
5
+ protected
6
+
7
+ def assign_previous_and_next
8
+ @previous = @page - 1 unless @page == 1
9
+ @next = @page + 1 unless @page == @last
10
+ end
11
+ end
12
+ end
@@ -1,17 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Pagy # :nodoc:
3
+ class Pagy
4
4
  # Cheap Base64 specialized methods to avoid dependencies
5
5
  module B64
6
6
  module_function
7
7
 
8
- def encode(bin)
9
- [bin].pack('m0')
10
- end
8
+ def encode(bin) = [bin].pack('m0')
11
9
 
12
- def decode(str)
13
- str.unpack1('m0')
14
- end
10
+ def decode(str) = str.unpack1('m0')
15
11
 
16
12
  def urlsafe_encode(bin)
17
13
  str = encode(bin)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ # Provide a ready to use pagy environment when included in irb/rails console
5
+ module Console
6
+ class Collection < Array
7
+ def initialize(arr = Array(1..1000))
8
+ super
9
+ @collection = clone
10
+ end
11
+
12
+ def offset(value) = tap { @collection = self[value..] }
13
+ def limit(value) = @collection[0, value]
14
+ def count(*) = size
15
+ end
16
+
17
+ include Method
18
+
19
+ # Direct reference to request.params via a method
20
+ def request = @request ||= { base_url: 'http://www.example.com', path: '/path', params: { example: '123' } }
21
+ def params = request[:params]
22
+ def collection = Collection
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require_relative 'p11n'
5
+
6
+ class Pagy
7
+ # Pagy i18n implementation, compatible with the I18n gem, just a lot faster and lighter
8
+ module I18n
9
+ extend self
10
+
11
+ def pathnames = @pathnames ||= [ROOT.join('locales')]
12
+ def locales = @locales ||= {}
13
+
14
+ # Store the variable for the duration of a single request
15
+ def locale=(value)
16
+ Thread.current[:pagy_locale] = value
17
+ end
18
+
19
+ def locale = Thread.current[:pagy_locale] || 'en'
20
+
21
+ # Translate and pluralize the key with the locale entries
22
+ def translate(key, **options)
23
+ data, p11n = locales[locale] || self.load
24
+ translation = data[key] || (options[:count] && data[key += ".#{p11n.plural_for(options[:count])}"]) \
25
+ or return %([translation missing: "#{key}"])
26
+ translation.gsub(/%{[^}]+?}/) { |match| options[:"#{match[2..-2]}"] || match }
27
+ end
28
+
29
+ private
30
+
31
+ def load
32
+ path = pathnames.reverse.map { |p| p.join("#{locale}.yml") }.find(&:exist?)
33
+ raise Errno::ENOENT, "missing dictionary file for #{locale.inspect} locale" unless path
34
+
35
+ dictionary = YAML.load_file(path)[locale]
36
+ p11n = dictionary['pagy'].delete('p11n')
37
+ locales[locale] = [flatten_to_dot_keys(dictionary), Object.const_get("Pagy::I18n::P11n::#{p11n}")]
38
+ end
39
+
40
+ # Create a flat hash with dotted notation keys
41
+ # e.g. { 'a' => { 'b' => {'c' => 3, 'd' => 4 }}} -> { 'a.b.c' => 3, 'a.b.d' => 4 }
42
+ def flatten_to_dot_keys(initial, prefix = '')
43
+ initial.reduce({}) do |hash, (key, value)|
44
+ hash.merge!(value.is_a?(Hash) ? flatten_to_dot_keys(value, "#{prefix}#{key}.") : { "#{prefix}#{key}" => value })
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module Arabic
7
+ module_function
8
+
9
+ def plural_for(n = 0)
10
+ mod100 = n % 100
11
+ case
12
+ when n == 0 # rubocop:disable Style/NumericPredicate
13
+ :zero
14
+ when n == 1
15
+ :one
16
+ when n == 2
17
+ :two
18
+ when (3..10).to_a.include?(mod100)
19
+ :few
20
+ when (11..99).to_a.include?(mod100)
21
+ :many
22
+ else
23
+ :other
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Pagy
4
+ module I18n
5
+ module P11n
6
+ module EastSlavic
7
+ module_function
8
+
9
+ def plural_for(n = 0)
10
+ mod10 = n % 10
11
+ mod100 = n % 100
12
+ case
13
+ when mod10 == 1 && mod100 != 11
14
+ :one
15
+ when (2..4).to_a.include?(mod10) && !(12..14).to_a.include?(mod100)
16
+ :few
17
+ when mod10 == 0 || (5..9).to_a.include?(mod10) || (11..14).to_a.include?(mod100) # rubocop:disable Style/NumericPredicate
18
+ :many
19
+ else
20
+ :other
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end