Skip to content

Commit

Permalink
WIP: improved a lot of things, but still incomplete
Browse files Browse the repository at this point in the history
  • Loading branch information
ddnexus committed Dec 17, 2024
1 parent 2a1781f commit 988eb1c
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 165 deletions.
2 changes: 1 addition & 1 deletion gem/apps/repro.ru
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ end
# pagy initializer
require 'pagy/extras/pagy'
require 'pagy/extras/limit'
# require 'pagy/extras/trim'
require 'pagy/extras/trim'
require 'pagy/extras/overflow'
Pagy::DEFAULT[:overflow] = :empty_page
Pagy::DEFAULT.freeze
Expand Down
4 changes: 2 additions & 2 deletions gem/javascripts/pagy.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions gem/javascripts/pagy.min.js.map

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion gem/javascripts/pagy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ const Pagy = (() => {
let filled;
if (typeof item === "number") {
filled = fillIn(tokens.a, item.toString(), label);
if (typeof opts?.page_param === "string" && item === 1) {
filled = trim(filled, opts.page_param);
}
} else if (item === "gap") {
filled = tokens.gap;
} else {
filled = fillIn(tokens.current, item, label);
}
html += typeof opts?.page_param === "string" && item == 1 ? trim(filled, opts.page_param) : filled;
html += filled;
});
html += tokens.after;
el.innerHTML = "";
Expand Down
2 changes: 0 additions & 2 deletions gem/lib/pagy/extras/js_tools.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,12 @@ def label_sequels(sequels = self.sequels)
module FrontendAddOn
if defined?(::Oj)
# Return a data tag with the base64 encoded JSON-serialized args generated with the faster oj gem
# Base64 encoded JSON is smaller than HTML escaped JSON
def pagy_data(_pagy, *args)
%(data-pagy="#{B64.encode(Oj.dump(args, mode: :strict))}")
end
else
require 'json'
# Return a data tag with the base64 encoded JSON-serialized args generated with the slower to_json
# Base64 encoded JSON is smaller than HTML escaped JSON
def pagy_data(_pagy, *args)
%(data-pagy="#{B64.encode(args.to_json)}")
end
Expand Down
83 changes: 20 additions & 63 deletions gem/lib/pagy/extras/keyset_for_ui.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,85 +5,42 @@
require_relative 'js_tools'

class Pagy # :nodoc:
DEFAULT[:cache_key_param] = :cache_key
DEFAULT[:cutoffs_param] = :cutoffs
CUTOFFS_TOKEN = '__pagy_cutoffs__'

# Add keyset UI Compatible methods
module KeysetForUIExtra
private

# Return Pagy::KeysetForUI object and paginated records
def pagy_keyset_for_ui(set, **vars)
vars[:page] ||= pagy_get_page(vars) # numeric page
vars[:limit] ||= pagy_get_limit(vars)
vars[:cache_key] ||= params[vars[:cache_key_param] || DEFAULT[:cache_key_param]] ||
pagy_cache_new_key
cutoffs, cutoff_vars = pagy_get_cutoff_and_vars(vars)
vars[:cutoffs] = cutoff_vars
vars[:page] ||= pagy_get_page(vars) # numeric page
vars[:limit] ||= pagy_get_limit(vars)
vars[:cutoffs_param] ||= DEFAULT[:cutoffs_param]
vars[:params] ||= { vars[:cutoffs_param] => CUTOFFS_TOKEN } # replaced on the client side
vars[:cutoffs] ||= begin
cutoffs = params[vars[:cutoffs_param]]
JSON.parse(B64.urlsafe_decode(cutoffs)) if cutoffs
end
pagy = KeysetForUI.new(set, **vars)
# if last known page & not the last set page
if vars[:page] == cutoff_vars[0] && (cutoff = pagy.cutoff)
cutoffs.push(cutoff)
end
pagy_cache_write(vars[:cache_key], cutoffs) # adds the updated
[pagy, pagy.records]
end

def pagy_get_cutoff_and_vars(vars)
cutoffs = pagy_cache_read(vars[:cache_key]) || [nil] # cutoffs keyed by page number, so [0] is never used
pages = cutoffs.size # the page size is the number of the cutoffs so far
page = vars[:page]
if page > pages
raise OverflowError.new(self, :page, "in 1..#{pages}", page) \
unless DEFAULT[:reset_overflow] || vars[:reset_overflow]

# reset pagination (TODO: check if it's ok moved here)
page = 1
cutoffs = [nil]
end
# cutoff_vars:
# [0] last/pages: known cutoff size (pages/last): 1 when none (i.e. page #1)
# [1] prev_cutoff: nil for page 1 (i.e. begins from begin of set)
# [2] cutoff: known page; nil for last page
[cutoffs, [pages, cutoffs[page - 1], cutoffs[page]]]
end

# Return 1B-max random key shortened to a base 64 number that's not yet in the cache
def pagy_cache_new_key
cache_key = nil
until cache_key
key = B64.convert(rand(1_000_000_000))
cache_key = key unless pagy_cache_read(key)
end
cache_key
end

def pagy_cache_push(key, cutoff)
cutoffs = pagy_cache_read(key).push(cutoff)
pagy_cache_write(key, cutoffs)
end

def pagy_cache_read(key) = session[key]

def pagy_cache_write(key, value) = session[key] = value
end
Backend.prepend KeysetForUIExtra

# Module overriding UrlHelper
module UrlHelpersOverride
# Override UrlHelper method
def pagy_set_query_params(page, vars, query_params)
super
query_params[vars[:cache_key_param].to_s] = vars[:cache_key]
end
end
UrlHelpers.prepend UrlHelpersOverride

# Add the cutoff to the pagy_data
# Add the update to the pagy_data
module JSToolsOverride
def pagy_data(pagy, *args)
args << { cutoff: pagy.cutoff } if pagy.is_a?(::Pagy::KeysetForUi)
if pagy.is_a?(::Pagy::KeysetForUi)
opts = args.last
if opts.is_a?(::Hash)
opts[:update] = pagy.update
else
args << { update: pagy.update }
end
end
super
end
end
JSTools.prepend JSToolsOverride
Frontend.prepend JSToolsOverride
end
11 changes: 9 additions & 2 deletions gem/lib/pagy/extras/trim.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,16 @@ def pagy_trim(pagy, a)
# Add the page_pram to the pagy_data
module JSToolsOverride
def pagy_data(pagy, *args)
args << pagy.vars.slice(:page_param) if pagy.vars[:trim_extra]
if pagy.vars[:trim_extra]
opts = args.last
if opts.is_a?(::Hash)
opts[:page_param] = pagy.vars[:page_param]
else
args << pagy.vars.slice(:page_param)
end
end
super
end
end
JSTools.prepend JSToolsOverride
Frontend.prepend JSToolsOverride
end
17 changes: 9 additions & 8 deletions gem/lib/pagy/keyset_for_ui.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
require_relative 'keyset'

class Pagy # :nodoc:
# Use keyset pagination with numeric pages
# supporting pagy_nav and other frontend helpers.
# Use keyset pagination with numeric pages supporting pagy_nav and other frontend helpers.
class KeysetForUI < Keyset
class ActiveRecord < KeysetForUI
include ActiveRecordAdapter
Expand All @@ -19,7 +18,7 @@ class Sequel < KeysetForUI
CUTOFF_PREFIX = 'cutoff_' # Prefix for cutoff_args

include SharedUIMethods
attr_reader :cutoff
attr_reader :update

# Finalize the instance variables needed for the UI
def initialize(set, **vars)
Expand All @@ -30,9 +29,10 @@ def initialize(set, **vars)
@in = @records.size
end

# Get the cutoff from the cache
# Get the cutoff from the client
def assign_cutoffs
@last, @prev_cutoff, @cutoff = @vars[:cutoffs] || [1]
# @key, is from the client and sent back as-is in order to id the requests of the same set
@key, @last, @prev_cutoff, @cutoff = @vars[:cutoffs] || [nil, 1]
raise OverflowError.new(self, :page, "in 1..#{@last}", @page) if @page > @last
end

Expand Down Expand Up @@ -105,7 +105,7 @@ def cutoff_to_args(cutoff)

# Add the default variables required by the Frontend
def default
{ **super, **DEFAULT.slice(:ends, :page, :size, :cache_key_param) }
{ **super, **DEFAULT.slice(:ends, :page, :size, :cutoffs_param) }
end

# Remove the LIMIT if @cutoff
Expand All @@ -118,15 +118,16 @@ def fetch_records
@set.limit(nil).to_a
end

# Return the next page number, and cache the cutoff if it's missing from the cache (only last known page)
# Return the next page number, prepare the @update for the client when it's new page/cutoff
def next
records
return if !@more || (@vars[:max_pages] && @page >= @vars[:max_pages])

@next ||= (@page + 1).tap do
unless @cutoff
@cutoff = keyset_attributes_from(@records.last).values
@last += 1
@update = [@key, [@last, 0, @cutoff]] # key and splice arguments for the client cutoffs
@last += 1 # reflect the added cutoff
end
end
end
Expand Down
23 changes: 18 additions & 5 deletions src/pagy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ type NavArgs = readonly [Tokens, Sequels, null | LabelSequels, OptionArgs?]
type ComboArgs = readonly [string, OptionArgs?]
type SelectorArgs = readonly [number, string, OptionArgs?]
type JsonArgs = ['nav', NavArgs] | ['combo', ComboArgs] | ['selector', SelectorArgs]
type Cutoffs = readonly [string | number]
type Cutoff = readonly [string | number | boolean]
type SpliceArgs = [number, number, Cutoff]
type Update = [string, SpliceArgs]

interface OptionArgs {
readonly page_param?:string
readonly cutoffs?:Cutoffs
readonly update?:Update
}

interface Tokens {
Expand All @@ -27,6 +29,7 @@ const Pagy = (() => {
.forEach(el => el.pagyRender())));
// Init the *_nav_js helpers
const initNav = (el:NavElement, [tokens, sequels, labelSequels, opts]:NavArgs) => {
if (Array.isArray(opts?.update)) { update(opts.update) }
const container = el.parentElement ?? el;
const widths = Object.keys(sequels).map(w => parseInt(w)).sort((a, b) => b - a);
let lastWidth = -1;
Expand All @@ -35,29 +38,39 @@ const Pagy = (() => {
(el.pagyRender = function () {
const width = widths.find(w => w < container.clientWidth) || 0;
if (width === lastWidth) { return } // no change: abort
let html = tokens.before; // already trimmed in html
let html = tokens.before; // already trimmed by ruby in html
const series = sequels[width.toString()];
const labels = labelSequels?.[width.toString()] ?? series.map(l => l.toString());
series.forEach((item, i) => {
const label = labels[i];
let filled;
if (typeof item === "number") {
filled = fillIn(tokens.a, item.toString(), label);
if (typeof opts?.cutoffs === "string") { filled = filled.replace(/__pagy_cutoffs__/g, cutoffsFor(item)) }
if (typeof opts?.page_param === "string" && item === 1) { filled = trim(filled, opts.page_param) }
} else if (item === "gap") {
filled = tokens.gap;
} else { // active page
filled = fillIn(tokens.current, item, label);
}
html += (typeof opts?.page_param === "string" && item == 1) ? trim(filled, opts.page_param) : filled;
html += filled;
});
html += tokens.after;
html += tokens.after; // already trimmed by ruby in html
el.innerHTML = "";
el.insertAdjacentHTML("afterbegin", html);
lastWidth = width;
})();
if (el.classList.contains("pagy-rjs")) { rjsObserver.observe(container) }
};

const update = ([key, spliceArgs]:Update) => {

};

const cutoffsFor = (page:number) => {
return page.toString('base64url');
};

// Init the *_combo_nav_js helpers
const initCombo = (el:Element, [url_token, opts]:ComboArgs) =>
initInput(el, inputValue => [inputValue, url_token.replace(/__pagy_page__/, inputValue)], opts);
Expand Down
52 changes: 52 additions & 0 deletions test/pagy/extras/keyset_for_ui_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require_relative '../../test_helper'
require 'pagy/extras/keyset_for_ui'
require 'pagy/extras/limit'

require_relative '../../files/models'
require_relative '../../mock_helpers/app'

describe 'pagy/extras/keyset_for_ui' do
[Pet, PetSequel].each do |model|
describe 'pagy_keyset_for_ui' do
it 'works for page 1' do
app = MockApp.new(params: {})
pagy, records = app.send(:pagy_keyset_for_ui,
model.order(:id),
tuple_comparison: true,
limit: 10)
_(pagy).must_be_kind_of Pagy::KeysetForUI
_(records.size).must_equal 10
_(pagy.next).must_equal 2
_(pagy.update).must_equal [nil, 1, 0, [10]]
end
it 'works for page 2' do
app = MockApp.new(params: {cutoffs: Pagy::B64.urlsafe_encode(['key', 2, [10]].to_json)})
pagy, records = app.send(:pagy_keyset_for_ui,
model.order(:id),
page: 2,
tuple_comparison: true,
limit: 10)
_(pagy).must_be_kind_of Pagy::KeysetForUI
_(records.size).must_equal 10
_(records.first.id).must_equal 11
_(pagy.next).must_equal 3
_(pagy.update).must_equal ['key', 2, 0, [20]]
end
it 'works for page 5' do
app = MockApp.new(params: {cutoffs: Pagy::B64.urlsafe_encode(['key', 5, [40]].to_json)})
pagy, records = app.send(:pagy_keyset_for_ui,
model.order(:id),
page: 5,
tuple_comparison: true,
limit: 10)
_(pagy).must_be_kind_of Pagy::KeysetForUI
_(records.size).must_equal 10
_(records.first.id).must_equal 41
_(pagy.next).must_be_nil
_(pagy.update).must_be_nil
end
end
end
end
Loading

0 comments on commit 988eb1c

Please sign in to comment.