diff --git a/app/components/paginator_component.rb b/app/components/paginator_component.rb
new file mode 100644
index 000000000..b449a04e8
--- /dev/null
+++ b/app/components/paginator_component.rb
@@ -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
diff --git a/app/components/paginator_component/paginator_component.html+meta_links.erb b/app/components/paginator_component/paginator_component.html+meta_links.erb
new file mode 100644
index 000000000..3dd9ae95c
--- /dev/null
+++ b/app/components/paginator_component/paginator_component.html+meta_links.erb
@@ -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 %>
diff --git a/app/components/paginator_component/paginator_component.html+numbered.erb b/app/components/paginator_component/paginator_component.html+numbered.erb
new file mode 100644
index 000000000..41dfb3237
--- /dev/null
+++ b/app/components/paginator_component/paginator_component.html+numbered.erb
@@ -0,0 +1,15 @@
+
+ <%= 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" %>
+
diff --git a/app/components/paginator_component/paginator_component.html+sequential.erb b/app/components/paginator_component/paginator_component.html+sequential.erb
new file mode 100644
index 000000000..97ab2a6f2
--- /dev/null
+++ b/app/components/paginator_component/paginator_component.html+sequential.erb
@@ -0,0 +1,4 @@
+
+ <%= 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" %>
+
diff --git a/app/components/paginator_component/paginator_component.scss b/app/components/paginator_component/paginator_component.scss
new file mode 100644
index 000000000..e080eec54
--- /dev/null
+++ b/app/components/paginator_component/paginator_component.scss
@@ -0,0 +1,10 @@
+.paginator {
+ > * {
+ padding: 0.25rem 0.75rem;
+ }
+
+ > a:hover {
+ background: var(--paginator-arrow-color);
+ color: var(--paginator-arrow-background-color);
+ }
+}
diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb
index 41f733d05..b78d97273 100644
--- a/app/helpers/components_helper.rb
+++ b/app/helpers/components_helper.rb
@@ -50,4 +50,19 @@ module ComponentsHelper
tags = TagListComponent.tags_from_names(tag_names)
render TagListComponent.new(tags: tags, **options).with_variant(:search)
end
+
+ # The / links in the element of the .
+ 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
diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb
deleted file mode 100644
index b1bd2ac7d..000000000
--- a/app/helpers/pagination_helper.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-module PaginationHelper
- def sequential_paginator(records)
- html = '"
- 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 = '"
- 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 << ""
- html << ellipsis_icon
- html << ""
- elsif page == current_page
- html << ""
- html << '' + page.to_s + ''
- html << ""
- else
- html << ""
- html << link_to(page, nav_params_for(page))
- html << ""
- 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
diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css
index 3ce4e9d08..537f5d37d 100644
--- a/app/javascript/src/styles/base/040_colors.css
+++ b/app/javascript/src/styles/base/040_colors.css
@@ -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);
diff --git a/app/javascript/src/styles/common/paginator.scss b/app/javascript/src/styles/common/paginator.scss
deleted file mode 100644
index beee65534..000000000
--- a/app/javascript/src/styles/common/paginator.scss
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss
index 1612571e5..5de2ddf5e 100644
--- a/app/javascript/src/styles/common/utilities.scss
+++ b/app/javascript/src/styles/common/utilities.scss
@@ -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; }
diff --git a/app/javascript/src/styles/specific/z_responsive.scss b/app/javascript/src/styles/specific/z_responsive.scss
index 3236a3ca8..0640f11c2 100644
--- a/app/javascript/src/styles/specific/z_responsive.scss
+++ b/app/javascript/src/styles/specific/z_responsive.scss
@@ -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;
diff --git a/app/logical/pagination_extension.rb b/app/logical/pagination_extension.rb
index 387618172..ce63b3790 100644
--- a/app/logical/pagination_extension.rb
+++ b/app/logical/pagination_extension.rb
@@ -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
diff --git a/app/views/application/_meta_links.html.erb b/app/views/application/_meta_links.html.erb
deleted file mode 100644
index 7b398ed83..000000000
--- a/app/views/application/_meta_links.html.erb
+++ /dev/null
@@ -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 %>
diff --git a/app/views/favorite_groups/show.html.erb b/app/views/favorite_groups/show.html.erb
index 8499e1cbb..d838e7c6e 100644
--- a/app/views/favorite_groups/show.html.erb
+++ b/app/views/favorite_groups/show.html.erb
@@ -1,5 +1,5 @@
<% page_title "Favgroup: #{@favorite_group.pretty_name}" %>
-<%= render "meta_links", collection: @posts %>
+<%= render_meta_links @posts %>
<%= render "secondary_links" %>
diff --git a/app/views/forum_topics/show.html.erb b/app/views/forum_topics/show.html.erb
index 98580574e..921138d9c 100644
--- a/app/views/forum_topics/show.html.erb
+++ b/app/views/forum_topics/show.html.erb
@@ -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" %>
diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb
index e2b4db26b..cc4cb1f3a 100644
--- a/app/views/layouts/default.html.erb
+++ b/app/views/layouts/default.html.erb
@@ -5,7 +5,7 @@
<%= page_title %>
- <%= 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" %>
diff --git a/app/views/pools/show.html.erb b/app/views/pools/show.html.erb
index c88e0eb31..273a723f4 100644
--- a/app/views/pools/show.html.erb
+++ b/app/views/pools/show.html.erb
@@ -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" %>
diff --git a/test/components/paginator_component_test.rb b/test/components/paginator_component_test.rb
new file mode 100644
index 000000000..5b315b88d
--- /dev/null
+++ b/test/components/paginator_component_test.rb
@@ -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
diff --git a/test/functional/posts_controller_test.rb b/test/functional/posts_controller_test.rb
index a1e8fe58a..fc680e21a 100644
--- a/test/functional/posts_controller_test.rb
+++ b/test/functional/posts_controller_test.rb
@@ -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