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:
parent
1134128a71
commit
0db69983fe
|
@ -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,11 +37,25 @@ 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++ {
|
||||||
|
@ -52,38 +63,62 @@ func (p *Pagination) Iter() []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
|
||||||
|
end = len(pages) - endLength
|
||||||
|
overflow = Page{
|
||||||
|
Page: start,
|
||||||
|
IsCurrent: p.Page >= start && p.Page <= end,
|
||||||
|
IsOverflow: true,
|
||||||
|
overflowFrom: start,
|
||||||
|
overflowTo: end,
|
||||||
|
}
|
||||||
|
result = []Page{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// If we are currently selected on an overflow page, set the label to match.
|
||||||
|
if overflow.IsCurrent {
|
||||||
|
overflow.Page = p.Page
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we near the end?
|
result = pages[:endLength]
|
||||||
if pageIdx > len(pages)-(config.PagerButtonLimit/2) {
|
result = append(result, overflow)
|
||||||
// We are near the end, return the last N pages.
|
result = append(result, pages[end:]...)
|
||||||
return pages[len(pages)-config.PagerButtonLimit-1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are somewhere in the middle.
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user