pagy 9.3.5 → 43.0.0.rc1
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.
- checksums.yaml +4 -4
- data/apps/calendar.ru +547 -551
- data/apps/demo.ru +221 -178
- data/apps/index.rb +3 -1
- data/apps/keynav.ru +258 -0
- data/apps/{keyset_ar.ru → keyset.ru} +27 -32
- data/apps/{keyset_s.ru → keyset_sequel.ru} +24 -29
- data/apps/rails.ru +51 -48
- data/apps/repro.ru +46 -43
- data/bin/pagy +20 -21
- data/config/pagy.rb +35 -207
- data/javascripts/ai_widget.js +76 -0
- data/javascripts/pagy.js +151 -0
- data/javascripts/pagy.js.map +10 -0
- data/javascripts/pagy.min.js +1 -4
- data/javascripts/pagy.mjs +111 -63
- data/javascripts/wand.js +1166 -0
- data/lib/pagy/classes/calendar/calendar.rb +98 -0
- data/lib/pagy/{calendar → classes/calendar}/day.rb +7 -11
- data/lib/pagy/{calendar → classes/calendar}/month.rb +5 -10
- data/lib/pagy/{calendar → classes/calendar}/quarter.rb +10 -15
- data/lib/pagy/classes/calendar/unit.rb +92 -0
- data/lib/pagy/{calendar → classes/calendar}/week.rb +5 -9
- data/lib/pagy/{calendar → classes/calendar}/year.rb +5 -6
- data/lib/pagy/classes/exceptions.rb +33 -0
- data/lib/pagy/classes/keyset/active_record.rb +11 -0
- data/lib/pagy/classes/keyset/adapters/active_record.rb +50 -0
- data/lib/pagy/classes/keyset/adapters/sequel.rb +63 -0
- data/lib/pagy/classes/keyset/keynav/active_record.rb +13 -0
- data/lib/pagy/classes/keyset/keynav/sequel.rb +13 -0
- data/lib/pagy/classes/keyset/keynav.rb +77 -0
- data/lib/pagy/classes/keyset/keyset.rb +126 -0
- data/lib/pagy/classes/keyset/sequel.rb +11 -0
- data/lib/pagy/classes/offset/countless.rb +56 -0
- data/lib/pagy/classes/offset/offset.rb +54 -0
- data/lib/pagy/classes/offset/search.rb +38 -0
- data/lib/pagy/classes/request.rb +36 -0
- data/lib/pagy/modules/abilities/configurable.rb +36 -0
- data/lib/pagy/modules/abilities/linkable.rb +59 -0
- data/lib/pagy/modules/abilities/rangeable.rb +15 -0
- data/lib/pagy/modules/abilities/shiftable.rb +12 -0
- data/lib/pagy/{b64.rb → modules/b64.rb} +3 -7
- data/lib/pagy/modules/console.rb +38 -0
- data/lib/pagy/modules/i18n/i18n.rb +48 -0
- data/lib/pagy/modules/i18n/p11n/arabic.rb +29 -0
- data/lib/pagy/modules/i18n/p11n/east_slavic.rb +26 -0
- data/lib/pagy/modules/i18n/p11n/one_other.rb +15 -0
- data/lib/pagy/modules/i18n/p11n/one_upto_two_other.rb +15 -0
- data/lib/pagy/modules/i18n/p11n/other.rb +13 -0
- data/lib/pagy/modules/i18n/p11n/polish.rb +26 -0
- data/lib/pagy/modules/i18n/p11n/west_slavic.rb +22 -0
- data/lib/pagy/modules/i18n/p11n.rb +16 -0
- data/lib/pagy/modules/searcher.rb +20 -0
- data/lib/pagy/toolbox/helpers/anchor_tags.rb +25 -0
- data/lib/pagy/toolbox/helpers/bootstrap/input_nav_js.rb +24 -0
- data/lib/pagy/toolbox/helpers/bootstrap/previous_next_html.rb +18 -0
- data/lib/pagy/toolbox/helpers/bootstrap/series_nav.rb +29 -0
- data/lib/pagy/toolbox/helpers/bootstrap/series_nav_js.rb +21 -0
- data/lib/pagy/toolbox/helpers/bulma/input_nav_js.rb +21 -0
- data/lib/pagy/toolbox/helpers/bulma/previous_next_html.rb +19 -0
- data/lib/pagy/toolbox/helpers/bulma/series_nav.rb +28 -0
- data/lib/pagy/toolbox/helpers/bulma/series_nav_js.rb +20 -0
- data/lib/pagy/toolbox/helpers/data_hash.rb +26 -0
- data/lib/pagy/toolbox/helpers/headers_hash.rb +20 -0
- data/lib/pagy/toolbox/helpers/info_tag.rb +26 -0
- data/lib/pagy/toolbox/helpers/input_nav_js.rb +19 -0
- data/lib/pagy/toolbox/helpers/limit_tag_js.rb +23 -0
- data/lib/pagy/toolbox/helpers/loader.rb +33 -0
- data/lib/pagy/toolbox/helpers/page_url.rb +23 -0
- data/lib/pagy/toolbox/helpers/series_nav.rb +29 -0
- data/lib/pagy/toolbox/helpers/series_nav_js.rb +19 -0
- data/lib/pagy/toolbox/helpers/support/a_lambda.rb +34 -0
- data/lib/pagy/toolbox/helpers/support/data_pagy_attribute.rb +15 -0
- data/lib/pagy/toolbox/helpers/support/nav_aria_label_attribute.rb +12 -0
- data/lib/pagy/toolbox/helpers/support/series.rb +38 -0
- data/lib/pagy/toolbox/helpers/support/wrap_input_nav_js.rb +20 -0
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav.rb +17 -0
- data/lib/pagy/toolbox/helpers/support/wrap_series_nav_js.rb +36 -0
- data/lib/pagy/toolbox/helpers/urls_hash.rb +13 -0
- data/lib/pagy/toolbox/paginators/calendar.rb +30 -0
- data/lib/pagy/toolbox/paginators/countless.rb +25 -0
- data/lib/pagy/toolbox/paginators/elasticsearch_rails.rb +32 -0
- data/lib/pagy/toolbox/paginators/keynav_js.rb +29 -0
- data/lib/pagy/toolbox/paginators/keyset.rb +18 -0
- data/lib/pagy/toolbox/paginators/meilisearch.rb +32 -0
- data/lib/pagy/toolbox/paginators/method.rb +26 -0
- data/lib/pagy/toolbox/paginators/offset.rb +33 -0
- data/lib/pagy/toolbox/paginators/searchkick.rb +32 -0
- data/lib/pagy.rb +59 -97
- data/locales/ar.yml +9 -6
- data/locales/be.yml +9 -6
- data/locales/bg.yml +9 -6
- data/locales/bs.yml +9 -6
- data/locales/ca.yml +9 -6
- data/locales/ckb.yml +8 -6
- data/locales/cs.yml +9 -6
- data/locales/da.yml +9 -6
- data/locales/de.yml +9 -6
- data/locales/dz.yml +9 -6
- data/locales/en.yml +9 -6
- data/locales/es.yml +9 -6
- data/locales/fr.yml +9 -6
- data/locales/hr.yml +9 -6
- data/locales/id.yml +9 -6
- data/locales/it.yml +9 -6
- data/locales/ja.yml +9 -6
- data/locales/km.yml +9 -6
- data/locales/ko.yml +9 -6
- data/locales/nb.yml +9 -6
- data/locales/nl.yml +9 -6
- data/locales/nn.yml +9 -6
- data/locales/pl.yml +9 -6
- data/locales/pt-BR.yml +10 -7
- data/locales/pt.yml +10 -7
- data/locales/ru.yml +9 -6
- data/locales/sr.yml +9 -6
- data/locales/sv-SE.yml +9 -6
- data/locales/sv.yml +9 -6
- data/locales/sw.yml +12 -9
- data/locales/ta.yml +12 -9
- data/locales/tr.yml +9 -6
- data/locales/uk.yml +9 -6
- data/locales/vi.yml +9 -6
- data/locales/zh-CN.yml +9 -6
- data/locales/zh-HK.yml +9 -6
- data/locales/zh-TW.yml +9 -6
- data/stylesheets/pagy-tailwind.css +64 -0
- data/stylesheets/pagy.css +63 -27
- metadata +112 -51
- data/javascripts/pagy.min.js.map +0 -10
- data/lib/pagy/backend.rb +0 -44
- data/lib/pagy/calendar/unit.rb +0 -103
- data/lib/pagy/calendar.rb +0 -84
- data/lib/pagy/console.rb +0 -23
- data/lib/pagy/countless.rb +0 -38
- data/lib/pagy/exceptions.rb +0 -25
- data/lib/pagy/extras/arel.rb +0 -28
- data/lib/pagy/extras/array.rb +0 -19
- data/lib/pagy/extras/bootstrap.rb +0 -97
- data/lib/pagy/extras/bulma.rb +0 -93
- data/lib/pagy/extras/calendar.rb +0 -79
- data/lib/pagy/extras/countless.rb +0 -32
- data/lib/pagy/extras/elasticsearch_rails.rb +0 -71
- data/lib/pagy/extras/gearbox.rb +0 -55
- data/lib/pagy/extras/headers.rb +0 -54
- data/lib/pagy/extras/i18n.rb +0 -26
- data/lib/pagy/extras/js_tools.rb +0 -70
- data/lib/pagy/extras/jsonapi.rb +0 -88
- data/lib/pagy/extras/keyset.rb +0 -30
- data/lib/pagy/extras/limit.rb +0 -63
- data/lib/pagy/extras/meilisearch.rb +0 -57
- data/lib/pagy/extras/metadata.rb +0 -42
- data/lib/pagy/extras/overflow.rb +0 -81
- data/lib/pagy/extras/pagy.rb +0 -82
- data/lib/pagy/extras/searchkick.rb +0 -59
- data/lib/pagy/extras/size.rb +0 -40
- data/lib/pagy/extras/standalone.rb +0 -60
- data/lib/pagy/extras/trim.rb +0 -29
- data/lib/pagy/frontend.rb +0 -99
- data/lib/pagy/i18n.rb +0 -166
- data/lib/pagy/keyset/active_record.rb +0 -44
- data/lib/pagy/keyset/sequel.rb +0 -57
- data/lib/pagy/keyset.rb +0 -118
- data/lib/pagy/shared_methods.rb +0 -26
- data/lib/pagy/url_helpers.rb +0 -26
- data/stylesheets/pagy.scss +0 -48
- 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,56 @@
|
|
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
|
+
assign_last
|
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 @options[:max_pages]
|
29
|
+
|
30
|
+
[value, @options[:max_pages]].min
|
31
|
+
end
|
32
|
+
|
33
|
+
def assign_last
|
34
|
+
return unless @options[:last]
|
35
|
+
|
36
|
+
@last = upto_max_pages(@options[:last].to_i)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Finalize the instance variables based on the fetched size
|
40
|
+
def finalize(fetched_size)
|
41
|
+
return self unless in_range? { fetched_size.positive? || @page == 1 }
|
42
|
+
|
43
|
+
if @last && @page < @last # visited page
|
44
|
+
@last = @page unless fetched_size > @limit # set last if last page
|
45
|
+
else
|
46
|
+
@last = upto_max_pages(fetched_size > @limit ? @page + 1 : @page)
|
47
|
+
end
|
48
|
+
@in = [fetched_size, @limit].min
|
49
|
+
@from = @in.zero? ? 0 : @offset + 1
|
50
|
+
@to = @offset + @in
|
51
|
+
assign_previous_and_next
|
52
|
+
self
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,54 @@
|
|
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
|
+
|
13
|
+
include Rangeable
|
14
|
+
include Shiftable
|
15
|
+
|
16
|
+
def initialize(**) # rubocop:disable Lint/MissingSuper
|
17
|
+
assign_options(**)
|
18
|
+
assign_and_check(limit: 1, count: 0, page: 1)
|
19
|
+
assign_last
|
20
|
+
assign_offset
|
21
|
+
return unless in_range? { @page <= @last }
|
22
|
+
|
23
|
+
@from = [@offset + 1, @count].min
|
24
|
+
@to = [@offset + @limit, @count].min
|
25
|
+
@in = [@to - @from + 1, @count].min
|
26
|
+
assign_previous_and_next
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :offset, :count, :from, :to, :in, :previous, :last
|
30
|
+
|
31
|
+
def records(collection)
|
32
|
+
collection.offset(@offset).limit(@limit)
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
|
37
|
+
def offset? = true
|
38
|
+
|
39
|
+
def assign_last
|
40
|
+
@last = [(@count.to_f / @limit).ceil, 1].max
|
41
|
+
@last = @options[:max_pages] if @options[:max_pages] && @last > @options[:max_pages]
|
42
|
+
end
|
43
|
+
|
44
|
+
def assign_offset
|
45
|
+
@offset = (@limit * (@page - 1))
|
46
|
+
end
|
47
|
+
|
48
|
+
# Called by false in_range?
|
49
|
+
def assign_empty_page_variables
|
50
|
+
@in = @from = @to = 0 # options relative to the actual page
|
51
|
+
@previous = @last # @previous relative to the actual page
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,38 @@
|
|
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
|
26
|
+
# Get the count from different versions of ElasticsearchRails
|
27
|
+
def self.total_count(results)
|
28
|
+
total = results.instance_eval { respond_to?(:raw_response) ? raw_response['hits']['total'] : response['hits']['total'] }
|
29
|
+
total.is_a?(Hash) ? total['value'] : total
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Meilisearch < SearchBase
|
34
|
+
DEFAULT = { search_method: :ms_search }.freeze
|
35
|
+
end
|
36
|
+
|
37
|
+
class Searchkick < SearchBase; end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Pagy
|
4
|
+
# Decouple the reuest from the env, allowing non-rack apps to use pagy by passing a hash.
|
5
|
+
# Resolve :page and :limit, supporting the :jsonapi option. Support for URL composition.
|
6
|
+
class Request
|
7
|
+
def initialize(request, options = {})
|
8
|
+
@base_url, @path, @query, @cookie =
|
9
|
+
if request.is_a?(Hash)
|
10
|
+
request.values_at(:base_url, :path, :query, :cookie)
|
11
|
+
else
|
12
|
+
[request.base_url, request.path, request.GET, request.cookies['pagy']]
|
13
|
+
end
|
14
|
+
@jsonapi = @query['page'] && options[:jsonapi]
|
15
|
+
raise JsonapiReservedParamError, @query['page'] if @jsonapi && !@query['page'].respond_to?(:fetch)
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :base_url, :path, :query, :cookie
|
19
|
+
|
20
|
+
def resolve_page(options, force_integer: true)
|
21
|
+
page_key = options[:page_key] || DEFAULT[:page_key]
|
22
|
+
page = @jsonapi ? @query['page'][page_key] : @query[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(options)
|
28
|
+
limit_key = options[:limit_key] || DEFAULT[:limit_key]
|
29
|
+
return options[:limit] || DEFAULT[:limit] \
|
30
|
+
unless options[:client_max_limit] &&
|
31
|
+
(requested_limit = @jsonapi ? @query['page'][limit_key] : @query[limit_key])
|
32
|
+
|
33
|
+
[requested_limit.to_i, options[:client_max_limit]].min
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -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
|
+
document.addEventListener('wand-positioned', #{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,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
class Pagy
|
6
|
+
# Provide the helpers to handle the url and anchor
|
7
|
+
module Linkable
|
8
|
+
module QueryUtils
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# Extracted from Rack::Utils and reformatted for rubocop
|
12
|
+
# Add the 'unescaped' param, and use it for simple and safe url-templating.
|
13
|
+
# All string keyed hashes
|
14
|
+
def build_nested_query(value, prefix = nil, unescaped = [])
|
15
|
+
case value
|
16
|
+
when Array
|
17
|
+
value.map { |v| build_nested_query(v, "#{prefix}[]", unescaped) }.join('&')
|
18
|
+
when Hash
|
19
|
+
value.map do |k, v|
|
20
|
+
new_k = prefix ? "#{prefix}[#{escape(k)}]" : escape(k)
|
21
|
+
unescaped[unescaped.find_index(k)] = new_k if unescaped.size.positive? && new_k != k && unescaped.include?(k)
|
22
|
+
build_nested_query(v, new_k, unescaped)
|
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
|
+
return "#{escape(prefix)}=#{value}" if unescaped.include?(prefix)
|
29
|
+
|
30
|
+
"#{escape(prefix)}=#{escape(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
|
+
# Return the URL for the page, relying on the Pagy::Request
|
42
|
+
def compose_page_url(page, limit_token: nil, **options)
|
43
|
+
jsonapi, page_key, limit_key, limit, client_max_limit, querify, absolute, path, fragment =
|
44
|
+
@options.merge(options)
|
45
|
+
.values_at(:jsonapi, :page_key, :limit_key, :limit, :client_max_limit, :querify, :absolute, :path, :fragment)
|
46
|
+
query = @request.query.clone(freeze: false)
|
47
|
+
query.delete(jsonapi ? 'page' : page_key)
|
48
|
+
factors = {}.tap do |h|
|
49
|
+
h[page_key] = countless? ? "#{page || 1}+#{@last}" : page
|
50
|
+
h[limit_key] = limit_token || limit if client_max_limit
|
51
|
+
end.compact # No empty params
|
52
|
+
query.merge!(jsonapi ? { 'page' => factors } : factors) if factors.size.positive?
|
53
|
+
querify&.(query) # Must modify the query: the returned value is ignored
|
54
|
+
query_string = QueryUtils.build_nested_query(query, nil, [page_key, limit_key])
|
55
|
+
query_string = "?#{query_string}" unless query_string.empty?
|
56
|
+
"#{@request.base_url if absolute}#{path || @request.path}#{query_string}#{fragment}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,15 @@
|
|
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) || (@in_range = yield)
|
9
|
+
raise RangeError.new(self, :page, "in 1..#{@last}", @page) if @options[:raise_range_error]
|
10
|
+
|
11
|
+
assign_empty_page_variables
|
12
|
+
false
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -1,17 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class Pagy
|
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,38 @@
|
|
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 Request
|
7
|
+
attr_accessor :base_url, :path, :params
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@base_url = 'http://www.example.com'
|
11
|
+
@path = '/path'
|
12
|
+
@params = { example: '123' }
|
13
|
+
end
|
14
|
+
|
15
|
+
def GET = @params # rubocop:disable Naming/MethodName
|
16
|
+
|
17
|
+
def cookies = {}
|
18
|
+
end
|
19
|
+
|
20
|
+
class Collection < Array
|
21
|
+
def initialize(arr = Array(1..1000))
|
22
|
+
super
|
23
|
+
@collection = clone
|
24
|
+
end
|
25
|
+
|
26
|
+
def offset(value) = tap { @collection = self[value..] }
|
27
|
+
def limit(value) = @collection[0, value]
|
28
|
+
def count(*) = size
|
29
|
+
end
|
30
|
+
|
31
|
+
include Method
|
32
|
+
|
33
|
+
# Direct reference to request.params via a method
|
34
|
+
def params = request.params
|
35
|
+
def request = @request ||= Request.new
|
36
|
+
def collection = Collection
|
37
|
+
end
|
38
|
+
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
|