views: factor out paginator component.
* Refactor the paginator into a ViewComponent.
* Fix inconsistent spacing between paginator items.
* Fix a bug where the sequential paginator generated the wrong next /
previous page links in the <link rel="{next|prev}"> tags in the <head>.
* Always include the final page as a hidden html element, so that it can
be unhidden with custom CSS.
* Make it easier to change the pagination window.
This commit is contained in:
44
app/components/paginator_component.rb
Normal file
44
app/components/paginator_component.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PaginatorComponent < ApplicationComponent
|
||||
attr_reader :records, :window, :params
|
||||
delegate :current_page, :prev_page, :next_page, :total_pages, :paginator_mode, :paginator_page_limit, to: :records
|
||||
delegate :ellipsis_icon, :chevron_left_icon, :chevron_right_icon, to: :helpers
|
||||
|
||||
def initialize(records:, params:, window: 4)
|
||||
@records = records
|
||||
@window = window
|
||||
@params = params
|
||||
end
|
||||
|
||||
def use_sequential_paginator?
|
||||
paginator_mode != :numbered || current_page >= paginator_page_limit
|
||||
end
|
||||
|
||||
def pages
|
||||
last_page = total_pages
|
||||
left = (current_page - window).clamp(2..)
|
||||
right = (current_page + window).clamp(..last_page - 1)
|
||||
|
||||
[
|
||||
1,
|
||||
("..." unless left == 2),
|
||||
(left..right).to_a,
|
||||
("..." unless right == last_page - 1),
|
||||
last_page
|
||||
].flatten.compact
|
||||
end
|
||||
|
||||
def link_to_page(anchor, page = anchor, **options)
|
||||
if page.nil?
|
||||
tag.span anchor, **options
|
||||
else
|
||||
hidden = paginator_mode == :numbered && page > paginator_page_limit
|
||||
link_to anchor, url_for_page(page), **options, hidden: hidden
|
||||
end
|
||||
end
|
||||
|
||||
def url_for_page(page)
|
||||
url_for(**params.merge(page: page).permit!)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
<% content_for(:html_header) do %>
|
||||
<% if prev_page.present? %>
|
||||
<%= tag.link rel: "prev", href: url_for_page(prev_page) %>
|
||||
<% end %>
|
||||
|
||||
<% if next_page.present? %>
|
||||
<%= tag.link rel: "next", href: url_for_page(next_page) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="paginator numbered-paginator mt-8 mb-4 space-x-2 flex justify-center items-center">
|
||||
<%= link_to_page chevron_left_icon, prev_page, class: "paginator-prev", rel: "prev", "data-shortcut": "a left" %>
|
||||
|
||||
<% pages.each do |page| %>
|
||||
<% if page == "..." %>
|
||||
<%= ellipsis_icon class: "paginator-ellipsis text-muted desktop-only" %>
|
||||
<% elsif page == current_page %>
|
||||
<%= tag.span page, class: "paginator-current font-bold" %>
|
||||
<% else %>
|
||||
<%= link_to_page page, class: "paginator-page desktop-only" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to_page chevron_right_icon, next_page, class: "paginator-next", rel: "next", "data-shortcut": "d right" %>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<div class="paginator sequential-paginator mt-8 mb-4 space-x-2 flex justify-center items-center">
|
||||
<%= link_to_page chevron_left_icon, prev_page, class: "paginator-prev", rel: "prev", "data-shortcut": "a left" %>
|
||||
<%= link_to_page chevron_right_icon, next_page, class: "paginator-next", rel: "next", "data-shortcut": "d right" %>
|
||||
</div>
|
||||
10
app/components/paginator_component/paginator_component.scss
Normal file
10
app/components/paginator_component/paginator_component.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
.paginator {
|
||||
> * {
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
> a:hover {
|
||||
background: var(--paginator-arrow-color);
|
||||
color: var(--paginator-arrow-background-color);
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,19 @@ module ComponentsHelper
|
||||
tags = TagListComponent.tags_from_names(tag_names)
|
||||
render TagListComponent.new(tags: tags, **options).with_variant(:search)
|
||||
end
|
||||
|
||||
# The <link rel="next"> / <link rel="prev"> links in the <meta> element of the <head>.
|
||||
def render_meta_links(records)
|
||||
render PaginatorComponent.new(records: records, params: params).with_variant(:meta_links)
|
||||
end
|
||||
|
||||
def numbered_paginator(records)
|
||||
paginator = PaginatorComponent.new(records: records, params: params)
|
||||
|
||||
if paginator.use_sequential_paginator?
|
||||
render paginator.with_variant(:sequential)
|
||||
else
|
||||
render paginator.with_variant(:numbered)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
module PaginationHelper
|
||||
def sequential_paginator(records)
|
||||
html = '<div class="paginator"><menu>'
|
||||
|
||||
if records.any?
|
||||
if params[:page] =~ /[ab]/ && !records.is_first_page?
|
||||
html << '<li>' + link_to("< Previous", nav_params_for("a#{records[0].id}"), rel: "prev", id: "paginator-prev", "data-shortcut": "a left") + '</li>'
|
||||
end
|
||||
|
||||
unless records.is_last_page?
|
||||
html << '<li>' + link_to("Next >", nav_params_for("b#{records[-1].id}"), rel: "next", id: "paginator-next", "data-shortcut": "d right") + '</li>'
|
||||
end
|
||||
end
|
||||
|
||||
html << "</menu></div>"
|
||||
html.html_safe
|
||||
end
|
||||
|
||||
def use_sequential_paginator?(records, page_limit)
|
||||
params[:page] =~ /[ab]/ || records.current_page >= page_limit
|
||||
end
|
||||
|
||||
def numbered_paginator(records, page_limit: CurrentUser.user.page_limit)
|
||||
if use_sequential_paginator?(records, page_limit)
|
||||
return sequential_paginator(records)
|
||||
end
|
||||
|
||||
html = '<div class="paginator"><menu>'
|
||||
window = 4
|
||||
|
||||
if records.current_page >= 2
|
||||
html << "<li class='arrow'>" + link_to(chevron_left_icon, nav_params_for(records.current_page - 1), rel: "prev", id: "paginator-prev", "data-shortcut": "a left") + "</li>"
|
||||
else
|
||||
html << "<li class='arrow'><span>" + chevron_left_icon + "</span></li>"
|
||||
end
|
||||
|
||||
if records.total_pages <= (window * 2) + 5
|
||||
1.upto(records.total_pages) do |page|
|
||||
html << numbered_paginator_item(page, records.current_page, page_limit)
|
||||
end
|
||||
|
||||
elsif records.current_page <= window + 2
|
||||
1.upto(records.current_page + window) do |page|
|
||||
html << numbered_paginator_item(page, records.current_page, page_limit)
|
||||
end
|
||||
html << numbered_paginator_item("...", records.current_page, page_limit)
|
||||
html << numbered_paginator_final_item(records.total_pages, records.current_page, page_limit)
|
||||
elsif records.current_page >= records.total_pages - (window + 1)
|
||||
html << numbered_paginator_item(1, records.current_page, page_limit)
|
||||
html << numbered_paginator_item("...", records.current_page, page_limit)
|
||||
(records.current_page - window).upto(records.total_pages) do |page|
|
||||
html << numbered_paginator_item(page, records.current_page, page_limit)
|
||||
end
|
||||
else
|
||||
html << numbered_paginator_item(1, records.current_page, page_limit)
|
||||
html << numbered_paginator_item("...", records.current_page, page_limit)
|
||||
if records.present?
|
||||
right_window = records.current_page + window
|
||||
else
|
||||
right_window = records.current_page
|
||||
end
|
||||
(records.current_page - window).upto(right_window) do |page|
|
||||
html << numbered_paginator_item(page, records.current_page, page_limit)
|
||||
end
|
||||
if records.present?
|
||||
html << numbered_paginator_item("...", records.current_page, page_limit)
|
||||
html << numbered_paginator_final_item(records.total_pages, records.current_page, page_limit)
|
||||
end
|
||||
end
|
||||
|
||||
if records.current_page < records.total_pages && records.present?
|
||||
html << "<li class='arrow'>" + link_to(chevron_right_icon, nav_params_for(records.current_page + 1), rel: "next", id: "paginator-next", "data-shortcut": "d right") + "</li>"
|
||||
else
|
||||
html << "<li class='arrow'><span>" + chevron_right_icon + "</span></li>"
|
||||
end
|
||||
|
||||
html << "</menu></div>"
|
||||
html.html_safe
|
||||
end
|
||||
|
||||
def numbered_paginator_final_item(total_pages, current_page, page_limit)
|
||||
if total_pages <= page_limit
|
||||
numbered_paginator_item(total_pages, current_page, page_limit)
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
def numbered_paginator_item(page, current_page, page_limit)
|
||||
return "" if page.to_i > page_limit
|
||||
|
||||
html = []
|
||||
if page == "..."
|
||||
html << "<li class='more'>"
|
||||
html << ellipsis_icon
|
||||
html << "</li>"
|
||||
elsif page == current_page
|
||||
html << "<li class='current-page'>"
|
||||
html << '<span>' + page.to_s + '</span>'
|
||||
html << "</li>"
|
||||
else
|
||||
html << "<li class='numbered-page'>"
|
||||
html << link_to(page, nav_params_for(page))
|
||||
html << "</li>"
|
||||
end
|
||||
html.join.html_safe
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def nav_params_for(page)
|
||||
query_params = params.except(:controller, :action, :id).merge(page: page).permit!
|
||||
{ params: query_params }
|
||||
end
|
||||
end
|
||||
@@ -183,7 +183,6 @@
|
||||
|
||||
--wiki-page-other-name-background-color: #EEE;
|
||||
|
||||
--paginator-ellipsis-color: grey;
|
||||
--paginator-arrow-background-color: white;
|
||||
--paginator-arrow-color: var(--link-color);
|
||||
|
||||
@@ -377,7 +376,6 @@ body[data-current-user-theme="dark"] {
|
||||
|
||||
--paginator-arrow-background-color: white;
|
||||
--paginator-arrow-color: var(--link-color);
|
||||
--paginator-ellipsis-color: var(--grey-4);
|
||||
|
||||
--post-artist-commentary-container-background: var(--grey-3);
|
||||
--post-artist-commentary-container-border: 1px solid var(--grey-3);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
div.paginator {
|
||||
display: block;
|
||||
padding: 2em 0 1em 0;
|
||||
text-align: center;
|
||||
|
||||
li {
|
||||
a {
|
||||
margin: 0 0.25em;
|
||||
padding: 0.25em 0.75em;
|
||||
}
|
||||
|
||||
&.more {
|
||||
color: var(--paginator-ellipsis-color);
|
||||
}
|
||||
|
||||
a.arrow:hover {
|
||||
background: var(--paginator-arrow-background-color);
|
||||
color: var(--paginator-arrow-color);
|
||||
}
|
||||
|
||||
a:hover {
|
||||
background: var(--paginator-arrow-color);
|
||||
color: var(--paginator-arrow-background-color);
|
||||
}
|
||||
|
||||
span {
|
||||
margin: 0 0.25em;
|
||||
padding: 0.25em 0.75em;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,14 @@ $spacer: 0.25rem; /* 4px */
|
||||
.flex { display: flex; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-muted { color: var(--muted-text-color); }
|
||||
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
.mx-2 { margin-left: 2 * $spacer; margin-right: 2 * $spacer; }
|
||||
|
||||
.mt-2 { margin-top: 2 * $spacer; }
|
||||
.mt-4 { margin-top: 4 * $spacer; }
|
||||
.mt-8 { margin-top: 8 * $spacer; }
|
||||
|
||||
.mb-2 { margin-bottom: 2 * $spacer; }
|
||||
.mb-4 { margin-bottom: 4 * $spacer; }
|
||||
@@ -31,6 +33,7 @@ $spacer: 0.25rem; /* 4px */
|
||||
|
||||
.h-10 { height: 10 * $spacer; }
|
||||
|
||||
.space-x-2 > * + * { margin-left: 2 * $spacer; }
|
||||
.space-x-4 > * + * { margin-left: 4 * $spacer; }
|
||||
.space-y-4 > * + * { margin-top: 4 * $spacer; }
|
||||
|
||||
@@ -38,6 +41,7 @@ $spacer: 0.25rem; /* 4px */
|
||||
|
||||
.flex-auto { flex: 1 1 auto; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
|
||||
@media screen and (min-width: 660px) {
|
||||
.md\:inline-block { display: inline-block; }
|
||||
|
||||
@@ -41,22 +41,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
div.paginator {
|
||||
font-size: var(--text-lg);
|
||||
padding: 1em 0 0;
|
||||
|
||||
li {
|
||||
a, span {
|
||||
margin: 0 0.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.numbered-page, &.more {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#posts #posts-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
module PaginationExtension
|
||||
class PaginationError < StandardError; end
|
||||
|
||||
attr_accessor :current_page, :records_per_page, :paginator_count, :paginator_mode
|
||||
attr_accessor :current_page, :records_per_page, :paginator_count, :paginator_mode, :paginator_page_limit
|
||||
|
||||
def paginate(page, limit: nil, max_limit: 1000, page_limit: CurrentUser.user.page_limit, count: nil, search_count: nil)
|
||||
@records_per_page = limit || Danbooru.config.posts_per_page
|
||||
@records_per_page = @records_per_page.to_i.clamp(1, max_limit)
|
||||
@paginator_page_limit = page_limit
|
||||
|
||||
if count.present?
|
||||
@paginator_count = count
|
||||
@@ -65,10 +66,8 @@ module PaginationExtension
|
||||
nil
|
||||
elsif paginator_mode == :numbered
|
||||
current_page - 1
|
||||
elsif paginator_mode == :sequential_before && records.present?
|
||||
elsif records.present?
|
||||
"a#{records.first.id}"
|
||||
elsif paginator_mode == :sequential_after && records.present?
|
||||
"b#{records.last.id}"
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -81,10 +80,8 @@ module PaginationExtension
|
||||
nil
|
||||
elsif paginator_mode == :numbered
|
||||
current_page + 1
|
||||
elsif paginator_mode == :sequential_before && records.present?
|
||||
elsif records.present?
|
||||
"b#{records.last.id}"
|
||||
elsif paginator_mode == :sequential_after && records.present?
|
||||
"a#{records.first.id}"
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<%# collection %>
|
||||
|
||||
<% content_for(:html_header) do %>
|
||||
<% if collection.try(:prev_page) %>
|
||||
<%= tag.link rel: "prev", href: url_for(nav_params_for(collection.prev_page)) %>
|
||||
<% end %>
|
||||
|
||||
<% if collection.try(:next_page) %>
|
||||
<%= tag.link rel: "next", href: url_for(nav_params_for(collection.next_page)) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,5 +1,5 @@
|
||||
<% page_title "Favgroup: #{@favorite_group.pretty_name}" %>
|
||||
<%= render "meta_links", collection: @posts %>
|
||||
<%= render_meta_links @posts %>
|
||||
<%= render "secondary_links" %>
|
||||
|
||||
<div id="c-favorite-groups">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<% meta_description(DText.excerpt(@forum_topic.original_post&.body)) %>
|
||||
|
||||
<% atom_feed_tag(@forum_topic.title, forum_topic_url(@forum_topic.id, format: :atom)) %>
|
||||
<%= render "meta_links", collection: @forum_posts %>
|
||||
<%= render_meta_links @forum_posts %>
|
||||
<%= render "secondary_links" %>
|
||||
|
||||
<div id="c-forum-topics">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<title><%= page_title %></title>
|
||||
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
||||
<%= render "meta_links", collection: @current_item %>
|
||||
<%= render_meta_links @current_item if @current_item.respond_to?(:paginate) %>
|
||||
<%= tag.link rel: "canonical", href: canonical_url %>
|
||||
<%= tag.link rel: "search", type: "application/opensearchdescription+xml", href: opensearch_url(format: :xml, version: 2), title: "Search posts" %>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<% page_title @pool.pretty_name %>
|
||||
<% meta_description("#{number_with_delimiter(@pool.post_count)} posts. #{DText.excerpt(@pool.description)}") %>
|
||||
|
||||
<%= render "meta_links", collection: @posts %>
|
||||
<%= render_meta_links @posts %>
|
||||
<%= render "secondary_links" %>
|
||||
|
||||
<div id="c-pools">
|
||||
|
||||
64
test/components/paginator_component_test.rb
Normal file
64
test/components/paginator_component_test.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
require "test_helper"
|
||||
|
||||
class PaginatorComponentTest < ViewComponent::TestCase
|
||||
def render_paginator(variant, page, limit: 3, page_limit: 100)
|
||||
with_variant(variant) do
|
||||
tags = Tag.paginate(page, limit: limit, page_limit: page_limit)
|
||||
params = ActionController::Parameters.new(controller: :tags, action: :index)
|
||||
return render_inline(PaginatorComponent.new(records: tags, params: params))
|
||||
end
|
||||
end
|
||||
|
||||
def assert_page(expected_page, link)
|
||||
href = link.attr("href").value
|
||||
uri = Addressable::URI.parse(href)
|
||||
page = uri.query_values["page"]
|
||||
|
||||
assert_equal(expected_page, page)
|
||||
end
|
||||
|
||||
context "The PaginatorComponent" do
|
||||
setup do
|
||||
@tags = create_list(:tag, 10)
|
||||
end
|
||||
|
||||
context "when using sequential pagination" do
|
||||
should "work with an aN page" do
|
||||
html = render_paginator(:sequential, "a#{@tags[5].id}", limit: 3)
|
||||
|
||||
assert_page("a#{@tags[5+3].id}", html.css("a[rel=prev]"))
|
||||
assert_page("b#{@tags[5+1].id}", html.css("a[rel=next]"))
|
||||
end
|
||||
|
||||
should "work with a bN page" do
|
||||
html = render_paginator(:sequential, "b#{@tags[5].id}", limit: 3)
|
||||
|
||||
assert_page("a#{@tags[5-1].id}", html.css("a[rel=prev]"))
|
||||
assert_page("b#{@tags[5-3].id}", html.css("a[rel=next]"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when using numbered pagination" do
|
||||
should "work for page 1" do
|
||||
html = render_paginator(:numbered, 1, limit: 3)
|
||||
|
||||
assert_css("span.paginator-prev")
|
||||
assert_page("2", html.css("a.paginator-next"))
|
||||
end
|
||||
|
||||
should "work for page 2" do
|
||||
html = render_paginator(:numbered, 2, limit: 3)
|
||||
|
||||
assert_page("1", html.css("a.paginator-prev"))
|
||||
assert_page("3", html.css("a.paginator-next"))
|
||||
end
|
||||
|
||||
should "work for page 4" do
|
||||
html = render_paginator(:numbered, 4, limit: 3)
|
||||
|
||||
assert_page("3", html.css("a.paginator-prev"))
|
||||
assert_css("span.paginator-next")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,32 +22,32 @@ class PostsControllerTest < ActionDispatch::IntegrationTest
|
||||
get posts_path(page: "a0")
|
||||
assert_response :success
|
||||
assert_select ".post-preview", count: 3
|
||||
assert_select "#paginator-prev", count: 0
|
||||
assert_select "#paginator-next", count: 1
|
||||
assert_select "a.paginator-prev", count: 0
|
||||
assert_select "a.paginator-next", count: 1
|
||||
end
|
||||
|
||||
should "work with page=b0" do
|
||||
get posts_path(page: "b0")
|
||||
assert_response :success
|
||||
assert_select ".post-preview", count: 0
|
||||
assert_select "#paginator-prev", count: 0
|
||||
assert_select "#paginator-next", count: 0
|
||||
assert_select "a.paginator-prev", count: 0
|
||||
assert_select "a.paginator-next", count: 0
|
||||
end
|
||||
|
||||
should "work with page=b100000" do
|
||||
get posts_path(page: "b100000")
|
||||
assert_response :success
|
||||
assert_select ".post-preview", count: 3
|
||||
assert_select "#paginator-prev", count: 1
|
||||
assert_select "#paginator-next", count: 0
|
||||
assert_select "a.paginator-prev", count: 1
|
||||
assert_select "a.paginator-next", count: 0
|
||||
end
|
||||
|
||||
should "work with page=a100000" do
|
||||
get posts_path(page: "a100000")
|
||||
assert_response :success
|
||||
assert_select ".post-preview", count: 0
|
||||
assert_select "#paginator-prev", count: 0
|
||||
assert_select "#paginator-next", count: 0
|
||||
assert_select "a.paginator-prev", count: 0
|
||||
assert_select "a.paginator-next", count: 0
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user