| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package frontend |
| |
| import ( |
| "net/http" |
| "net/url" |
| "strconv" |
| |
| "golang.org/x/pkgsite/internal/log" |
| ) |
| |
| // pagination holds information related to paginated display. It is intended to |
| // be part of a view model struct. |
| // |
| // Given a sequence of results with offsets 0, 1, 2 ... (typically from a |
| // database query), we paginate it by dividing it into numbered pages |
| // 1, 2, 3, .... Each page except possibly the last has the same number of results. |
| type pagination struct { |
| baseURL *url.URL // URL common to all pages |
| Limit int // the maximum number of results on a page |
| ResultCount int // number of results on this page |
| TotalCount int // total number of results |
| Approximate bool // whether or not the total count is approximate |
| Page int // number of the current page |
| PrevPage int // " " " previous page, usually Page-1 but zero if Page == 1 |
| NextPage int // " " " next page, usually Page+1, but zero on the last page |
| Offset int // offset of the first item on the current page |
| Pages []int // consecutive page numbers to be displayed for navigation |
| Limits []int // limits to be displayed |
| } |
| |
| // PageURL constructs a URL that displays the given page. |
| // It adds a "page" query parameter to the base URL. |
| func (p pagination) PageURL(page int) string { |
| newQuery := p.baseURL.Query() |
| newQuery.Set("page", strconv.Itoa(page)) |
| p.baseURL.RawQuery = newQuery.Encode() |
| return p.baseURL.String() |
| } |
| |
| // URL constructs a URL that adds limit and mode query parameters to the base |
| // URL. Passing a zero value omits the parameter. |
| func (p pagination) URL(limit int, mode, q string) string { |
| newQuery := p.baseURL.Query() |
| if limit != 0 { |
| newQuery.Set("limit", strconv.Itoa(limit)) |
| } |
| if mode != "" { |
| newQuery.Set("m", mode) |
| } |
| if q != "" { |
| newQuery.Set("q", q) |
| } |
| p.baseURL.RawQuery = newQuery.Encode() |
| return p.baseURL.String() |
| } |
| |
| // newPagination constructs a pagination. Call it after some results have been |
| // obtained. |
| // resultCount is the number of results in the current page. |
| // totalCount is the total number of results. |
| func newPagination(params paginationParams, resultCount, totalCount int) pagination { |
| return pagination{ |
| baseURL: params.baseURL, |
| TotalCount: totalCount, |
| ResultCount: resultCount, |
| Offset: params.offset(), |
| Limit: params.limit, |
| Page: params.page, |
| PrevPage: prev(params.page), |
| NextPage: next(params.page, params.limit, totalCount), |
| Pages: pagesToLink(params.page, numPages(params.limit, totalCount), defaultNumPagesToLink), |
| Limits: []int{10, 30, maxSearchPageSize}, |
| } |
| } |
| |
| // paginationParams holds pagination parameters extracted from the request. |
| type paginationParams struct { |
| baseURL *url.URL |
| page int // the number of the page to display |
| limit int // the maximum number of results to display on the page |
| } |
| |
| // offset returns the offset of the first result on the page. |
| func (p paginationParams) offset() int { |
| return offset(p.page, p.limit) |
| } |
| |
| // newPaginationParams extracts pagination params from the request. |
| func newPaginationParams(r *http.Request, defaultLimit int) paginationParams { |
| positiveParam := func(key string, dflt int) (val int) { |
| var err error |
| if a := r.FormValue(key); a != "" { |
| val, err = strconv.Atoi(a) |
| if err != nil { |
| log.Errorf(r.Context(), "strconv.Atoi(%q) for page: %v", a, err) |
| } |
| } |
| if val < 1 { |
| val = dflt |
| } |
| return val |
| } |
| return paginationParams{ |
| baseURL: r.URL, |
| page: positiveParam("page", 1), |
| limit: positiveParam("limit", defaultLimit), |
| } |
| } |
| |
| const defaultNumPagesToLink = 5 |
| |
| // pagesToLink returns the page numbers that will be displayed. Given a |
| // page, it returns a slice containing numPagesToLink integers in ascending |
| // order and optimizes for page to be in the middle of that range. The max |
| // value of an integer in the return slice will be less than numPages. |
| func pagesToLink(page, numPages, numPagesToLink int) []int { |
| var pages []int |
| start := page - (numPagesToLink / 2) |
| if (numPages - start) < numPagesToLink { |
| start = numPages - numPagesToLink + 1 |
| } |
| if start < 1 { |
| start = 1 |
| } |
| |
| for i := start; (i < start+numPagesToLink) && (i <= numPages); i++ { |
| pages = append(pages, i) |
| } |
| return pages |
| } |
| |
| // numPages is the total number of pages needed to display all the results, |
| // given the specified maximum page size and the total number of results. |
| func numPages(pageSize, totalCount int) int { |
| return (totalCount + pageSize - 1) / pageSize |
| } |
| |
| // offset returns the offset of the first result on page, assuming all previous |
| // pages were of size limit. |
| func offset(page, limit int) int { |
| if page <= 1 { |
| return 0 |
| } |
| return (page - 1) * limit |
| } |
| |
| // prev returns the number of the page before the given page, or zero if the |
| // given page is 1 or smaller. |
| func prev(page int) int { |
| if page <= 1 { |
| return 0 |
| } |
| return page - 1 |
| } |
| |
| // next returns the number of the page after the given page, or zero if page is is the last page or larger. |
| // limit and totalCount are used to calculate the last page (see numPages). |
| func next(page, limit, totalCount int) int { |
| if page >= numPages(limit, totalCount) { |
| return 0 |
| } |
| return page + 1 |
| } |