Broke out pagination into a separate Svelte subcomponent
This commit is contained in:
parent
df1edd38a8
commit
61b0b5c91c
5 changed files with 175 additions and 121 deletions
132
src/components/Table/Pagination.svelte
Normal file
132
src/components/Table/Pagination.svelte
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts">
|
||||
import { intcomma } from 'journalize';
|
||||
/**
|
||||
* The current page number.
|
||||
* @type {number}
|
||||
*/
|
||||
export let pageNumber: number = 1;
|
||||
|
||||
/**
|
||||
* The default page size.
|
||||
* @type {number}
|
||||
*/
|
||||
export let pageSize: number = 25;
|
||||
|
||||
/**
|
||||
* The number of records in the current page.
|
||||
* @type {number}
|
||||
*/
|
||||
export let pageLength: number = null;
|
||||
|
||||
/**
|
||||
* The total number of records in the data set.
|
||||
* @type {number}
|
||||
*/
|
||||
export let n: number = null;
|
||||
|
||||
function goToPreviousPage() {
|
||||
if (pageNumber > 1) {
|
||||
pageNumber -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextPage() {
|
||||
if (pageNumber < Math.ceil(n / pageSize)) {
|
||||
pageNumber += 1;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav aria-label="pagination" class="pagination">
|
||||
<button on:click="{goToPreviousPage}" disabled="{pageNumber === 1}"
|
||||
><div class="icon-wrapper">
|
||||
<svg
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 6 11"
|
||||
><path
|
||||
d="m1.76 5.134 3.887-3.887a.71.71 0 0 0 0-1.027.709.709 0 0 0-1.027 0l-4.4 4.4a.71.71 0 0 0 0 1.027l4.4 4.4c.147.147.367.22.513.22a.79.79 0 0 0 .513-.22.71.71 0 0 0 0-1.027L1.76 5.133Z"
|
||||
></path></svg
|
||||
> <span class="visually-hidden">Previous page</span>
|
||||
</div></button
|
||||
>
|
||||
<span class="label" aria-label="page {pageNumber}" aria-current="page"
|
||||
>{pageNumber * pageSize - pageSize + 1}-{pageNumber * pageSize -
|
||||
pageSize +
|
||||
pageLength} of {intcomma(n)}</span
|
||||
>
|
||||
<button
|
||||
on:click="{goToNextPage}"
|
||||
disabled="{pageNumber === Math.ceil(n / pageSize)}"
|
||||
><div class="icon-wrapper">
|
||||
<svg
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 7 11"
|
||||
><path
|
||||
d="m6.013 4.987-4.4-4.4a.71.71 0 0 0-1.027 0 .709.709 0 0 0 0 1.027L4.473 5.5.586 9.387a.71.71 0 0 0 0 1.027c.147.147.293.22.513.22.22 0 .367-.073.514-.22l4.4-4.4a.71.71 0 0 0 0-1.027Z"
|
||||
></path></svg
|
||||
> <span class="visually-hidden">Next page</span>
|
||||
</div></button
|
||||
>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../scss/colours/thematic/tr';
|
||||
@import '../../scss/fonts/variables';
|
||||
|
||||
nav.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 1rem;
|
||||
font-family: $font-family-display;
|
||||
font-weight: 400;
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid;
|
||||
border-color: $tr-contrast-grey;
|
||||
border-radius: 8px;
|
||||
background: $white;
|
||||
color: $tr-medium-grey;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
&:disabled {
|
||||
border-color: $tr-light-grey;
|
||||
color: $tr-light-grey;
|
||||
cursor: default;
|
||||
}
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s ease;
|
||||
.icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
.label {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -9,6 +9,8 @@
|
|||
// @ts-ignore
|
||||
import truncateDocs from './stories/docs/truncate.md?raw';
|
||||
// @ts-ignore
|
||||
import paginateDocs from './stories/docs/paginate.md?raw';
|
||||
// @ts-ignore
|
||||
import searchDocs from './stories/docs/search.md?raw';
|
||||
// @ts-ignore
|
||||
import filterDocs from './stories/docs/filter.md?raw';
|
||||
|
|
@ -80,6 +82,18 @@
|
|||
}}"
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Paginate"
|
||||
{...withStoryDocs(paginateDocs)}
|
||||
args="{{
|
||||
data: pressFreedom,
|
||||
title: 'Press Freedom Index',
|
||||
paginated: true,
|
||||
source:
|
||||
"Source: <a href='https://en.wikipedia.org/wiki/Press_Freedom_Index'>Reporters Without Borders</a>",
|
||||
}}"
|
||||
/>
|
||||
|
||||
<Story
|
||||
name="Search"
|
||||
{...withStoryDocs(searchDocs)}
|
||||
|
|
@ -88,7 +102,7 @@
|
|||
paginated: true,
|
||||
searchable: true,
|
||||
title: 'Press Freedom Index',
|
||||
notes:
|
||||
source:
|
||||
"Source: <a href='https://en.wikipedia.org/wiki/Press_Freedom_Index'>Reporters Without Borders</a>",
|
||||
}}"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -132,10 +132,10 @@
|
|||
|
||||
/** Import local helpers */
|
||||
import Block from '../Block/Block.svelte';
|
||||
import Pagination from './Pagination.svelte';
|
||||
import {
|
||||
filterArray,
|
||||
paginateArray,
|
||||
numberWithCommas,
|
||||
uniqueAttr,
|
||||
isNumeric,
|
||||
} from './utils.js';
|
||||
|
|
@ -150,15 +150,13 @@
|
|||
let filterValue = '';
|
||||
$: filteredData = filterArray(data, searchText, filterField, filterValue);
|
||||
$: sortedData = sortArray(filteredData, sortField, sortDirection);
|
||||
$: currentPageData = () => {
|
||||
if (truncated) {
|
||||
return showAll ? sortedData : sortedData.slice(0, truncateLength + 1);
|
||||
} else if (paginated) {
|
||||
return paginateArray(sortedData, pageSize, pageNumber);
|
||||
} else {
|
||||
return sortedData;
|
||||
}
|
||||
};
|
||||
$: currentPageData = truncated
|
||||
? showAll
|
||||
? sortedData
|
||||
: sortedData.slice(0, truncateLength + 1)
|
||||
: paginated
|
||||
? paginateArray(sortedData, pageSize, pageNumber)
|
||||
: sortedData;
|
||||
|
||||
// Estimate the text alignment of our fields. Strings go left. Numbers go right.
|
||||
function getAlignment(value) {
|
||||
|
|
@ -191,18 +189,6 @@
|
|||
sortDirection = sortDirection === 'ascending' ? 'descending' : 'ascending';
|
||||
}
|
||||
|
||||
function goToPreviousPage() {
|
||||
if (pageNumber > 1) {
|
||||
pageNumber -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextPage() {
|
||||
if (pageNumber < Math.ceil(filteredData.length / pageSize)) {
|
||||
pageNumber += 1;
|
||||
}
|
||||
}
|
||||
|
||||
function sortArray(array, column, direction) {
|
||||
if (!sortable) return array;
|
||||
return array.sort((a, b) => {
|
||||
|
|
@ -318,7 +304,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody class="table--tbody">
|
||||
{#each currentPageData() as item, idx}
|
||||
{#each currentPageData as item, idx}
|
||||
<tr data-row-index="{idx}">
|
||||
{#each includedFields as field}
|
||||
<td
|
||||
|
|
@ -358,46 +344,12 @@
|
|||
</nav>
|
||||
{/if}
|
||||
{#if paginated}
|
||||
<nav aria-label="pagination" class="pagination">
|
||||
<button on:click="{goToPreviousPage}" disabled="{pageNumber === 1}"
|
||||
><div class="icon-wrapper">
|
||||
<svg
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 6 11"
|
||||
><path
|
||||
d="m1.76 5.134 3.887-3.887a.71.71 0 0 0 0-1.027.709.709 0 0 0-1.027 0l-4.4 4.4a.71.71 0 0 0 0 1.027l4.4 4.4c.147.147.367.22.513.22a.79.79 0 0 0 .513-.22.71.71 0 0 0 0-1.027L1.76 5.133Z"
|
||||
></path></svg
|
||||
> <span class="visually-hidden">Previous page</span>
|
||||
</div></button
|
||||
>
|
||||
<span class="label" aria-label="page {pageNumber}" aria-current="page"
|
||||
>{pageNumber * pageSize - pageSize + 1}-{pageNumber * pageSize -
|
||||
pageSize +
|
||||
currentPageData.length} of {numberWithCommas(
|
||||
filteredData.length
|
||||
)}</span
|
||||
>
|
||||
<button
|
||||
on:click="{goToNextPage}"
|
||||
disabled="{pageNumber === Math.ceil(filteredData.length / pageSize)}"
|
||||
><div class="icon-wrapper">
|
||||
<svg
|
||||
class="icon"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 7 11"
|
||||
><path
|
||||
d="m6.013 4.987-4.4-4.4a.71.71 0 0 0-1.027 0 .709.709 0 0 0 0 1.027L4.473 5.5.586 9.387a.71.71 0 0 0 0 1.027c.147.147.293.22.513.22.22 0 .367-.073.514-.22l4.4-4.4a.71.71 0 0 0 0-1.027Z"
|
||||
></path></svg
|
||||
> <span class="visually-hidden">Next page</span>
|
||||
</div></button
|
||||
>
|
||||
</nav>
|
||||
{/if}
|
||||
<Pagination
|
||||
bind:pageNumber
|
||||
bind:pageSize
|
||||
bind:pageLength="{currentPageData.length}"
|
||||
bind:n="{sortedData.length}"
|
||||
/>{/if}
|
||||
</section>
|
||||
</Block>
|
||||
|
||||
|
|
@ -554,55 +506,4 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
nav.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
font-size: 1rem;
|
||||
font-family: $font-family-display;
|
||||
font-weight: 400;
|
||||
button {
|
||||
padding: 5px 10px;
|
||||
border: 1px solid;
|
||||
border-color: $tr-contrast-grey;
|
||||
border-radius: 8px;
|
||||
background: $white;
|
||||
color: $tr-medium-grey;
|
||||
cursor: pointer;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
&:disabled {
|
||||
border-color: $tr-light-grey;
|
||||
color: $tr-light-grey;
|
||||
cursor: default;
|
||||
}
|
||||
.icon-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
transition: all 0.15s ease;
|
||||
.icon {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
.label {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
13
src/components/Table/stories/docs/paginate.md
Normal file
13
src/components/Table/stories/docs/paginate.md
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
When you table has lots of rows you should consider breaking it up into pages. This can be done by setting the `paginated` option.
|
||||
|
||||
When it is enabled, readers can leaf through the data using a set of buttons below the table. By default there are 25 records per page. You can change the number by adjusting the `pageSize` option.
|
||||
|
||||
This is a good option when publishing large tables for readers to explore. It works well with interactive features like searching and filters.
|
||||
|
||||
```svelte
|
||||
<Table
|
||||
data="{yourData}"
|
||||
paginated="{true}"
|
||||
source="{'<a href='https://en.wikipedia.org/wiki/Press_Freedom_Index'>Reporters Without Borders</a>'}"
|
||||
/>
|
||||
```
|
||||
|
|
@ -16,12 +16,6 @@ export function paginateArray(array, pageSize, pageNumber) {
|
|||
return array.slice((pageNumber - 1) * pageSize, pageNumber * pageSize);
|
||||
}
|
||||
|
||||
export function numberWithCommas(n) {
|
||||
const parts = n.toString().split('.');
|
||||
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
return parts.join('.');
|
||||
}
|
||||
|
||||
export function uniqueAttr(array, attr) {
|
||||
return array.map((e) => e[attr]).filter(unique);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue