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;
- }
}
}