From af6ab947d13cbcd3fdb5ee8d2dcfffdecec2bdbd Mon Sep 17 00:00:00 2001 From: Cinder Date: Mon, 3 Feb 2025 21:42:12 -0800 Subject: [PATCH] [Pagination] Rework the pagination styles (#886) Now with mobile layouts! --- app/helpers/icon_helper.rb | 29 +++++ app/helpers/pagination_helper.rb | 93 +++++++++------- app/javascript/src/javascripts/paginator.js | 2 +- app/javascript/src/javascripts/shortcuts.js | 1 + .../src/styles/common/paginator.scss | 103 ++++++++++++++---- .../src/styles/specific/post_index.scss | 4 - 6 files changed, 168 insertions(+), 64 deletions(-) create mode 100644 app/helpers/icon_helper.rb diff --git a/app/helpers/icon_helper.rb b/app/helpers/icon_helper.rb new file mode 100644 index 000000000..1b9b7e7dc --- /dev/null +++ b/app/helpers/icon_helper.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module IconHelper + PATHS = { + chevron_left: %(), + chevron_right: %(), + ellipsis: %(), + }.freeze + + def svg_icon(name, *args) + options = args.extract_options! + width = options[:width] || 24 + height = options[:height] || 24 + + tag.svg( + "xmlns": "http://www.w3.org/2000/svg", + "width": width, + "height": height, + "viewbox": "0 0 24 24", + "fill": "none", + "stroke": "currentColor", + "stroke-width": 2, + "stroke-linecap": "round", + "stroke-linejoin": "round", + ) do + raw(PATHS[name]) # rubocop:disable Rails/OutputSafety + end + end +end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index 8cec434ea..2e65d042f 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -2,17 +2,23 @@ module PaginationHelper def sequential_paginator(records) - with_paginator_wrapper do + tag.div(class: "paginator") do return "" if records.try(:none?) html = "".html_safe - unless records.is_first_page? - html << tag.li(link_to("< Previous", nav_params_for("a#{records[0].id}"), rel: "prev", id: "paginator-prev", data: { shortcut: "a left" })) + + # Previous + html << link_to(records.is_first_page? ? "#" : nav_params_for("a#{records[0].id}"), class: "prev", id: "paginator-prev", rel: "prev", data: { shortcut: "a left", disabled: records.is_first_page? }) do + concat svg_icon(:chevron_left) + concat tag.span("Prev") end - unless records.is_last_page? - html << tag.li(link_to("Next >", nav_params_for("b#{records[-1].id}"), rel: "next", id: "paginator-next", data: { shortcut: "d right" })) + # Next + html << link_to(records.is_last_page? ? "#" : nav_params_for("b#{records[-1].id}"), class: "next", id: "paginator-next", rel: "next", data: { shortcut: "d right", disabled: records.is_last_page? }) do + concat tag.span("Next") + concat svg_icon(:chevron_right) end + html end end @@ -22,64 +28,73 @@ module PaginationHelper return sequential_paginator(records) end - with_paginator_wrapper do + tag.div(class: "paginator", data: { total: [records.total_pages, records.max_numbered_pages].min, current: records.current_page }) do html = "".html_safe - icon_left = tag.i(class: "fa-solid fa-chevron-left") - if records.current_page >= 2 - html << tag.li(class: "arrow") { link_to(icon_left, nav_params_for(records.current_page - 1), rel: "prev", id: "paginator-prev", data: { shortcut: "a left" }) } - else - html << tag.li(class: "arrow") { tag.span(icon_left) } + + # Previous + has_prev = records.current_page < 2 + html << link_to(has_prev ? "#" : nav_params_for(records.current_page - 1), class: "prev", id: "paginator-prev", rel: "prev", data: { shortcut: "a left", disabled: has_prev }) do + concat svg_icon(:chevron_left) + concat tag.span("Prev") end - paginator_pages(records).each do |page| - html << numbered_paginator_item(page, records) + # Break + html << tag.div(class: "break") + + # Numbered + paginator_pages(records).each do |page, klass| + html << numbered_paginator_item(page, klass, records) end - icon_right = tag.i(class: "fa-solid fa-chevron-right") - if records.current_page < records.total_pages - html << tag.li(class: "arrow") { link_to(icon_right, nav_params_for(records.current_page + 1), rel: "next", id: "paginator-next", data: { shortcut: "d right" }) } - else - html << tag.li(class: "arrow") { tag.span(icon_right) } + # Next + has_next = records.current_page >= records.total_pages + html << link_to(has_next ? "#" : nav_params_for(records.current_page + 1), class: "next", id: "paginator-next", rel: "next", data: { shortcut: "d right", disabled: has_next }) do + concat tag.span("Next") + concat svg_icon(:chevron_right) end + html end end private - def with_paginator_wrapper(&) - tag.div(class: "paginator") do - tag.menu(&) - end - end - def paginator_pages(records) - window = 4 + small_window = 2 + large_window = 4 last_page = [records.total_pages, records.max_numbered_pages].min - left = [2, records.current_page - window].max - right = [records.current_page + window, last_page - 1].min + left_sm = [2, records.current_page - small_window].max + left_lg = [2, records.current_page - large_window].max + right_sm = [records.current_page + small_window, last_page - 1].min + right_lg = [records.current_page + large_window, last_page - 1].min + small_range = left_sm..right_sm - [ - 1, - ("..." unless left == 2), - (left..right).to_a, - ("..." unless right == last_page - 1), - (last_page unless last_page <= 1), - ].flatten.compact + result = [ + [1, "first"], + ] + result.push([0, "spacer"]) unless left_lg == 2 + (left_lg..right_lg).each do |page| + result.push([page, small_range.member?(page) ? "sm" : "lg"]) + end + result.push([0, "spacer"]) unless right_lg == last_page - 1 + result.push([last_page, "last"]) unless last_page <= 1 + + result end - def numbered_paginator_item(page, records) + def numbered_paginator_item(page, klass, records) return "" if page.to_i > records.max_numbered_pages html = "".html_safe - if page == "..." - html << tag.li(class: "more") { link_to(tag.i(class: "fa-solid fa-ellipsis"), nav_params_for(0)) } + if page == 0 + html << link_to(svg_icon(:ellipsis), nav_params_for(0), class: "spacer") elsif page == records.current_page - html << tag.li(class: "current-page") { tag.span(page) } + html << tag.span(page, class: "page current") else - html << tag.li(class: "numbered-page") { link_to(page, nav_params_for(page)) } + html << link_to(page, nav_params_for(page), class: "page #{klass}") end + html end diff --git a/app/javascript/src/javascripts/paginator.js b/app/javascript/src/javascripts/paginator.js index 4abe73237..434750a2c 100644 --- a/app/javascript/src/javascripts/paginator.js +++ b/app/javascript/src/javascripts/paginator.js @@ -12,7 +12,7 @@ Paginator.init_fasttravel = function (button) { }; $(() => { - for (const one of $(".paginator li.more a").get()) + for (const one of $(".paginator a.spacer").get()) Paginator.init_fasttravel($(one)); }); diff --git a/app/javascript/src/javascripts/shortcuts.js b/app/javascript/src/javascripts/shortcuts.js index 4e3aa6e56..869c2f136 100644 --- a/app/javascript/src/javascripts/shortcuts.js +++ b/app/javascript/src/javascripts/shortcuts.js @@ -28,6 +28,7 @@ Shortcuts.initialize_data_shortcuts = function () { Shortcuts.keydown(keys, namespace, event => { const e = $(`[data-shortcut="${keys}"]`).get(0); + if ($e.data("disabled")) return; if ($e.is("input, textarea")) { $e.trigger("focus").selectEnd(); } else { diff --git a/app/javascript/src/styles/common/paginator.scss b/app/javascript/src/styles/common/paginator.scss index c006084b6..a3d74c076 100644 --- a/app/javascript/src/styles/common/paginator.scss +++ b/app/javascript/src/styles/common/paginator.scss @@ -1,35 +1,98 @@ +.paginator { + display: flex; + flex-wrap: wrap; + justify-content: space-evenly; + background-color: themed("color-foreground"); + border-radius: 0.25rem; -div.paginator { - display: block; - padding: 2em 0 1em 0; - text-align: center; - clear: both; - - menu { + & > a, & > span { display: flex; + box-sizing: border-box; justify-content: center; + align-items: center; + min-width: 2.15rem; + + font-size: 1rem; + line-height: 1rem; + padding: 0.75rem 0.3rem; // otherwise large page numbers wrap + + border-radius: 0.25rem; + + &:hover { + background: themed("color-section"); + } } - li { - a { - margin: 0 0.25em; - padding: 0.25em 0.75em; + & > a[data-disabled="true"] { + color: var(--color-text); + pointer-events: none; + } + + // Ordering + // Oh boy + .page { + order: 20; + &.lg { display: none; } + &.current { cursor: default; } + } + .prev { + order: 1; + margin-right: auto; + } + .spacer { + order: 20; + padding: 0; + + &:last-child { display: none; } + svg { + height: 1rem; + transform: rotate(90deg); + } + } + .next { + order: 9; + margin-left: auto; + } + .break { + order: 10; + width: 100%; + } + + + // Tablet + @include window-larger-than(35rem) { + justify-content: center; + gap: 0.125rem; + + a, span { + order: 0 !important; + min-width: 2.25rem; + font-size: 0.9rem; } - a:hover { - background: $paginator-hover-background; - color: $paginator-hover-color; + .break { display: none; } + .spacer { + padding: inherit; + svg { transform: unset; } } - &.more { - color: $paginator-more-color; + .prev { margin-right: 1rem; } + .next { margin-left: 1rem; } + } + + @include window-larger-than(50rem) { + a, span { + padding: 0.75rem 0.5rem; + font-size: 0.95rem; } - span { - margin: 0 0.25em; - padding: 0.25em 0.75em; - font-weight: bold; + .prev, .next { + span { display: none; } } } + + @include window-larger-than(65rem) { + a.page.lg { display: flex; } + } } diff --git a/app/javascript/src/styles/specific/post_index.scss b/app/javascript/src/styles/specific/post_index.scss index da8caad4f..5859ae3de 100644 --- a/app/javascript/src/styles/specific/post_index.scss +++ b/app/javascript/src/styles/specific/post_index.scss @@ -33,10 +33,6 @@ display: flex; flex-flow: column; gap: 1em; - - .paginator { - padding: 1em 0; - } } }