Revise Pagination Widget

The pager widget will now show a dropdown menu of overflow pages in the
middle. This allows easy access to the First and Last pages and an
ability to select from any of the middle pages to jump to quickly.
This commit is contained in:
Noah Petherbridge 2024-07-07 12:45:42 -07:00
parent 1134128a71
commit 0db69983fe
3 changed files with 100 additions and 33 deletions

View File

@ -1,11 +1,14 @@
package models package models
import ( import (
"fmt"
"math" "math"
"math/rand"
"net/http" "net/http"
"strconv" "strconv"
"code.nonshy.com/nonshy/website/pkg/config" "code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/encryption"
) )
// Pagination result object. // Pagination result object.
@ -19,12 +22,6 @@ type Pagination struct {
lastPage bool lastPage bool
} }
// Page for Iter.
type Page struct {
Page int
IsCurrent bool
}
// Load the page from form or query parameters. // Load the page from form or query parameters.
func (p *Pagination) ParsePage(r *http.Request) { func (p *Pagination) ParsePage(r *http.Request) {
raw := r.FormValue("page") raw := r.FormValue("page")
@ -40,50 +37,88 @@ func (p *Pagination) ParsePage(r *http.Request) {
} }
} }
// Page for Iter.
type Page struct {
Page int // label to show in the button
IsCurrent bool // highlight the currently selected page button
// this "button" is a drop-down menu of page numbers in the middle
IsOverflow bool
overflowFrom int
overflowTo int
}
// Iter the pages, for templates. // Iter the pages, for templates.
//
// If there are fewer than 7 pages (configurable) this returns a simple slice of buttons
// for each of the 7 pages to be drawn. If there are a LOT of pages, the middle button of
// the pager should be a drop-down menu to pick from the pages within.
func (p *Pagination) Iter() []Page { func (p *Pagination) Iter() []Page {
var ( var (
pages = []Page{} pages = []Page{}
pageIdx int total = p.Pages()
total = p.Pages()
) )
for i := 1; i <= total; i++ { for i := 1; i <= total; i++ {
pages = append(pages, Page{ pages = append(pages, Page{
Page: i, Page: i,
IsCurrent: i == p.Page, IsCurrent: i == p.Page,
}) })
if i == p.Page {
pageIdx = i
}
} }
// Do we have A LOT of pages? // Do we have A LOT of pages?
if len(pages) > config.PagerButtonLimit { if len(pages) > config.PagerButtonLimit+1 {
// We return a slide only N pages long. Where is our current page in the offset? // The left half of the buttons should be pages 1 thru N.
if pageIdx <= config.PagerButtonLimit/2 { // The right half are the final pages M thru Last.
// We are near the front, return the first N pages. // In the middle will be the overflow drop-down of middle pages.
return pages[:config.PagerButtonLimit+1] var (
} endLength = config.PagerButtonLimit / 2
start = endLength + 1
// Are we near the end? end = len(pages) - endLength
if pageIdx > len(pages)-(config.PagerButtonLimit/2) { overflow = Page{
// We are near the end, return the last N pages. Page: start,
return pages[len(pages)-config.PagerButtonLimit-1:] IsCurrent: p.Page >= start && p.Page <= end,
} IsOverflow: true,
overflowFrom: start,
// We are somewhere in the middle. overflowTo: end,
var result = []Page{}
for i := pageIdx - (config.PagerButtonLimit / 2) - 1; i < pageIdx+(config.PagerButtonLimit/2); i++ {
if i >= 0 && i < len(pages) {
result = append(result, pages[i])
} }
result = []Page{}
)
// If we are currently selected on an overflow page, set the label to match.
if overflow.IsCurrent {
overflow.Page = p.Page
} }
result = pages[:endLength]
result = append(result, overflow)
result = append(result, pages[end:]...)
return result return result
} }
return pages return pages
} }
// IterOverflow: if the Page represents an overflow drop-down menu, iterate the members of the menu
// for easy template integration.
func (p Page) IterOverflow() []Page {
var result = []Page{}
for i := p.overflowFrom; i <= p.overflowTo; i++ {
result = append(result, Page{
Page: i,
IsCurrent: p.Page == i,
})
}
return result
}
// UniqueSerialID will return a unique JavaScript ID.
//
// It is used in front-end pages such as for Pagination drop-down menus, which may appear multiple times
// on a page and each use needs a unique ID attribute to connect the button to the dropdown.
func (p *Pagination) UniqueSerialID() string {
return encryption.Hash([]byte(fmt.Sprintf("%d", rand.Intn(9000000))))
}
func (p Pagination) Pages() int { func (p Pagination) Pages() int {
if p.PerPage == 0 { if p.PerPage == 0 {
return 0 return 0

View File

@ -82,6 +82,14 @@ document.addEventListener('DOMContentLoaded', () => {
})); }));
})(); })();
// Dropdown menus.
(document.querySelectorAll(".dropdown") || []).forEach(node => {
const button = node.querySelector("button");
button.addEventListener("click", (e) => {
node.classList.toggle("is-active");
})
});
// Common event handlers for bulma modals. // Common event handlers for bulma modals.
(document.querySelectorAll(".modal-background, .modal-close, .photo-modal") || []).forEach(node => { (document.querySelectorAll(".modal-background, .modal-close, .photo-modal") || []).forEach(node => {
const target = node.closest(".modal"); const target = node.closest(".modal");

View File

@ -13,18 +13,42 @@ See also: template_funcs.go for the SimplePager wrapper function.
{{if .Pager.Pages}} {{if .Pager.Pages}}
<nav class="pagination" role="navigation" aria-label="pagination"> <nav class="pagination" role="navigation" aria-label="pagination">
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous" <a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Previous}}">Previous</a> href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Previous}}"
style="font-size: smaller">Previous</a>
<a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next" <a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next"
href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Next}}">Next page</a> href="{{.Request.URL.Path}}?{{QueryPlus "page" .Pager.Next}}"
style="font-size: smaller">Next page</a>
<ul class="pagination-list"> <ul class="pagination-list">
{{$Root := .}} {{$Root := .}}
{{$DropdownID := .Pager.UniqueSerialID}}
{{range .Pager.Iter}} {{range .Pager.Iter}}
<li> <li>
<!-- Overflow menu in the middle? -->
{{if .IsOverflow}}
<div class="dropdown px-1">
<div class="dropdown-trigger">
<button class="button{{if .IsCurrent}} is-link{{end}}" aria-haspopup="true" aria-controls="{{$DropdownID}}" style="font-size: smaller">
{{.Page}} <i class="fas fa-angle-down ml-1" aria-hidden="true"></i>
</button>
</div>
<div class="dropdown-menu" id="{{$DropdownID}}" role="menu">
<div class="dropdown-content" style="max-height: 250px; overflow: auto">
{{range .IterOverflow}}
<a class="dropdown-item{{if .IsCurrent}} is-active{{end}}"
aria-label="Page {{.Page}}"
href="{{$Root.Request.URL.Path}}?{{QueryPlus "page" .Page}}">{{.Page}}</a>
{{end}}
</div>
</div>
</div>
{{else}}
<a class="pagination-link{{if .IsCurrent}} is-current{{end}}" <a class="pagination-link{{if .IsCurrent}} is-current{{end}}"
style="font-size: smaller"
aria-label="Page {{.Page}}" aria-label="Page {{.Page}}"
href="{{$Root.Request.URL.Path}}?{{QueryPlus "page" .Page}}"> href="{{$Root.Request.URL.Path}}?{{QueryPlus "page" .Page}}">
{{.Page}} {{.Page}}
</a> </a>
{{end}}
</li> </li>
{{end}} {{end}}
</ul> </ul>