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 = '
' - - if records.any? - if params[:page] =~ /[ab]/ && !records.is_first_page? - html << '
  • ' + link_to("< Previous", nav_params_for("a#{records[0].id}"), rel: "prev", id: "paginator-prev", "data-shortcut": "a left") + '
  • ' - end - - unless records.is_last_page? - html << '
  • ' + link_to("Next >", nav_params_for("b#{records[-1].id}"), rel: "next", id: "paginator-next", "data-shortcut": "d right") + '
  • ' - end - end - - 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 = '
    ' - window = 4 - - if records.current_page >= 2 - html << "
  • " + link_to(chevron_left_icon, nav_params_for(records.current_page - 1), rel: "prev", id: "paginator-prev", "data-shortcut": "a left") + "
  • " - else - html << "
  • " + chevron_left_icon + "
  • " - 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 << "
  • " + link_to(chevron_right_icon, nav_params_for(records.current_page + 1), rel: "next", id: "paginator-next", "data-shortcut": "d right") + "
  • " - else - html << "
  • " + chevron_right_icon + "
  • " - 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