1
0
Fork 0
mirror of https://github.com/lrsjng/h5ai synced 2025-12-15 21:32:52 +01:00

Add pagination

* Add pagination buttons at the bottom of the page when the number of
items to be displayed is greater than the selected preference.
* Add a selector to the side bar for users to select their prefered
number of items to be displayed at once per page.
* Sorting should be handled by the pagination module whenever it keeps
the list of items in memory.
* Pagination is hidden (buttons are removed) whenever there is no need
for it, or when the user selected to display everything.
This commit is contained in:
glubsy 2021-01-14 19:21:46 +00:00
parent d81d8a9298
commit d7bd7937e1
7 changed files with 624 additions and 9 deletions

View file

@ -48,6 +48,13 @@
The user selected view mode is also stored local in modern browsers
so that it will be persistent.
- modeToggle: boolean, show a view mode toggle in the toolbar, or "next"
- paginationItems: array of numbers
user selectable amounts of items to display at once until pagination occurs.
The first value will be selected as the default. Values are then sorted automatically.
Value "0" means display ALL items at once, no pagination needed (as an option).
It is recommended to place this value (if desired at all) at the end of the array.
The user selected amount is also stored local in modern browsers
so that is will be persistent.
- setParentFolderLabels: boolean, set parent folder labels to real folder names
- sizes: array of numbers
the first value indicates the default view size. If only one value
@ -72,6 +79,7 @@
"maxIconSize": 40,
"modes": ["details", "grid", "icons"],
"modeToggle": false,
"paginationItems": [100, 50, 200, 300, 400, 500, 0],
"setParentFolderLabels": true,
"sizes": [20, 40, 60, 80, 100, 140, 180, 220, 260, 300],
"theme": "comity",

View file

@ -0,0 +1,103 @@
// @minWidth: 30px;
// @height: 28px;
// @lineHeight: @height;
// @activeHeight: @height + 2;
@inputWidth: 35px;
@inputHeight: 28px;
@buttonMinWidth: 40px;
@buttonHeight: 30px;
@buttonLineHeight: @buttonHeight - 2;
@buttonPadding: 0 8px;
// @navHeight: @activeHeight;
@col-hover-green: #44EEAA;
@col-placeholder-grey: rgb(170, 170, 170);
#pagination_btm {
position: -webkit-sticky; /* Safari */
position: sticky;
bottom: 0;
.nav_buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: center;
position: -webkit-sticky; /* Safari */
position: sticky;
bottom: 0;
}
.nav_buttons button {
width: @buttonMinWidth; // 20px;
height: @buttonHeight; // 30px;
appearance: none;
border: none;
outline: none;
cursor: pointer;
padding: 0;
background-color: @col-link;
margin: 5px;
transition: 0.4s;
color: @col-back;
font-size: @font-size;
text-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2);
&:hover {
background-color: @col-hover-green
}
&:disabled {
background-color: @col-text-disabled-black;
box-shadow: inset 0px 0px 4px rgb(0, 0, 0);
cursor: default;
}
}
.page_input {
display: flex;
align-content: flex-end;
float: left;
margin-left: 5px;
margin-right: 5px;
font-size: @font-size;
> input[type="text"]{
width: @inputWidth;
height: @inputHeight;
background: @col-back;
border-radius: 3px;
border: 1px solid @col-divider-black;
padding: @buttonPadding;
text-align: center;
vertical-align: baseline;
outline: none;
box-shadow: none;
box-sizing: initial;
&::placeholder {
color: @col-placeholder-grey;
opacity: 1; /* Firefox */
}
&:focus::placeholder {
color: transparent;
}
}
> input[type="button"]{
min-width: @buttonMinWidth;
height: @buttonHeight;
line-height: @buttonLineHeight;
background: @col-back;
border-radius: 3px;
border: 1px solid @col-divider-black;
text-align: center;
padding: @buttonPadding;
vertical-align: baseline;
outline: none;
box-shadow: none;
color: @col-text;
cursor: pointer;
&:hover{
background-color: @col-hover-green;
}
}
}
}

View file

@ -18,6 +18,7 @@ const defaultTranslations = {
dateFormat: 'YYYY-MM-DD HH:mm',
details: 'details',
displayAll: "Display ALL",
download: 'download',
empty: 'empty',
files: 'files',
@ -25,11 +26,16 @@ const defaultTranslations = {
folders: 'folders',
grid: 'grid',
icons: 'icons',
info: 'Informations',
language: 'Language',
lastModified: 'Last modified',
name: 'Name',
noMatch: 'no match',
pagination: "Pagination",
pagInputBtn: "GO",
pagInputTxt: "page",
parentDirectory: 'Parent Directory',
perPage: "### per page",
search: 'search',
size: 'Size',
tree: 'Tree',
@ -62,6 +68,9 @@ const update = lang => {
each(currentLang, (value, key) => {
dom('.l10n-' + key).text(value);
dom('.l10n_ph-' + key).attr('placeholder', value);
replace(dom('.l10n_rp-' + key), value);
dom('.l10n_val-' + key).val(value);
dom('.l10n_title-' + key).prop('title', value)
});
format.setDefaultDateFormat(currentLang.dateFormat);
@ -70,6 +79,15 @@ const update = lang => {
});
};
const replace = (elem, value) => {
if (!elem.length) {
return;
}
each(elem, el => {
el.text = value.replace('###', el.value);
});
};
const loadLanguage = isoCode => {
if (loaded[isoCode]) {
return Promise.resolve(loaded[isoCode]);

View file

@ -193,7 +193,7 @@ const init = () => {
if (settings.clickndrag) {
$selectionRect.hide().appTo('#content');
dom('#content')
dom('#view')
.on('mousedown', selectionStart)
.on('drag', ev => ev.preventDefault())
.on('dragstart', ev => ev.preventDefault());

View file

@ -3,6 +3,7 @@ const event = require('../core/event');
const resource = require('../core/resource');
const allsettings = require('../core/settings');
const store = require('../core/store');
const pagination = require('../view/pagination');
const settings = Object.assign({
enabled: false,
@ -22,8 +23,8 @@ const columnClasses = {0: 'label', 1: 'date', 2: 'size'};
const cmpFn = (prop, reverse, ignorecase, natural) => {
return (el1, el2) => {
const item1 = el1._item;
const item2 = el2._item;
const item1 = el1._item === undefined ? el1 : el1._item;
const item2 = el2._item === undefined ? el2 : el2._item;
let res = getTypeOrder(item1) - getTypeOrder(item2);
if (res !== 0) {
@ -58,16 +59,32 @@ const sortItems = (column, reverse) => {
$headers.rmCls('ascending').rmCls('descending');
$header.addCls(reverse ? 'descending' : 'ascending');
if (pagination.isSortHandled(fn)) {
return;
}
dom(toArray(dom('#items .item:not(.folder-parent)')).sort(fn)).appTo('#items');
};
const onContentChanged = () => {
if (pagination.isActive()){
return;
}
let {column, reverse} = getSortOrder();
sortItems(column, reverse);
};
const getSortOrder = () => {
const order = store.get(storekey);
const column = order && order.column || settings.column;
const reverse = order && order.reverse || settings.reverse;
return {column, reverse};
}
sortItems(column, reverse);
};
const getSortFunc = () => {
let {column, reverse} = getSortOrder();
return cmpFn(columnProps[column], reverse, settings.ignorecase, settings.natural);
}
const addToggles = () => {
const $header = dom('#items li.header');
@ -94,3 +111,7 @@ const init = () => {
init();
module.exports = {
getSortFunc
}

View file

@ -0,0 +1,437 @@
const {each, includes, dom, values} = require('../util');
const event = require('../core/event');
const store = require('../core/store');
const allsettings = require('../core/settings');
const base = require('./base');
const paginationItems = [100, 0, 50, 250, 500];
const settings = Object.assign({
paginationItems,
hideParentFolder: false,
}, allsettings.view);
const defaultSize = settings.paginationItems.length ? settings.paginationItems[0] : 0;
const sortedSizes = [...new Set(settings.paginationItems)].sort((a, b) => a - b)
const storekey = 'pagination';
const paginationTpl =
`<div id="pagination_btm" class="pagination">
<div id="nav_btm" class="nav_buttons"></div>
</div>`;
const selectorTpl =
`<div id="pag_sidebar" class="block">
<h1 class="l10n-pagination">Pagination</h1>
<form id="pag_form">
<select id="pag_select" name='Pagination size'>
</select>
<noscript><input type="submit" value="Submit"></noscript>
</form>
</div>`;
const $pagination = dom(paginationTpl);
const btn_cls = {
'btn_first': '<<',
'btn_prev': '<',
'btn_next': '>',
'btn_last': '>>'
};
let pag_active = false;
let pag_buttons = [];
let pag_current_page = 1;
let pag_items;
let pag_payload;
let pag_view;
let pag_count = 0;
let pag_parent_folder;
let pag_rows_pref;
let sortfn;
const setup = (items) => {
updateItems(items);
pag_current_page = 1;
pag_buttons = [];
let $pagination_els = base.$content.find('.nav_buttons');
setupNavigation($pagination_els);
pag_active = true;
updateSortFunc();
sort(sortfn());
setCurrentPage(1);
}
const updateSortFunc = () => {
// Lazy load because sort module needs us loaded beforehand
sortfn = require('../ext/sort').getSortFunc;
}
const updateItems = (items) => {
if (!items){
return; // use cached items instead
}
pag_items = items;
popParentFolder(pag_items);
totalPages();
return;
}
const clear = () => {
if (pag_active){
pag_buttons.forEach(e => e.remove());
pag_buttons = [];
}
pag_active = false;
}
const isActive = () => {
return pag_active;
}
const totalPages = () => {
if (pag_rows_pref == 0){ // ALL
return pag_count = 1;
}
pag_count = Math.ceil(pag_items.length / pag_rows_pref);
return pag_count;
}
const popParentFolder = (items) => {
if (items.length > 0 && !settings.hideParentFolder){
pag_parent_folder = items.shift();
return;
}
pag_parent_folder = undefined;
}
const pushParentFolder = (items) => {
if (pag_parent_folder && items[0] !== pag_parent_folder) {
items.unshift(pag_parent_folder);
}
}
const setCurrentPage = (page) => {
if (!page) {
page = (pag_current_page <= pag_count) ? pag_current_page : pag_count;
}
pag_current_page = page;
const paginatedItems = computeSlice(pag_items, page, pag_rows_pref);
pushParentFolder(paginatedItems);
updateButtons();
if (pag_count <= 1) {
base.$content.find('.nav_buttons').addCls('hidden');
pag_active = false;
} else {
base.$content.find('.nav_buttons').rmCls('hidden');
pag_active = true;
}
pag_view.doSetItems(paginatedItems);
}
const computeSlice = (items, page, rows_per_page) => {
if (!rows_per_page) { // ALL
return items;
}
page--;
const start = rows_per_page * page;
const end = start + rows_per_page;
return items.slice(start, end);
}
const sort = (fn) => {
// We don't need parent folder item, so we don't filterPayload()
pag_items = values(pag_payload.content).sort(fn);
}
const setupNavigation = (container) => {
each(container, key => {
key.innerHTML = "";
});
each(container, el => {
for (let key in btn_cls) {
const btn = paginationButton(key, btn_cls[key]);
el.appendChild(btn);
pag_buttons.push(btn);
}
});
each(container, key => {
// Page status numbers
let div = updatePageStatus(null);
key.insertBefore(div, key.childNodes[2]);
pag_buttons.push(div);
// Manual page number selection
div = document.createElement('div');
div.classList.add('page_input');
let {input_field, input_btn} = pageInputForm();
div.appendChild(input_field);
div.appendChild(input_btn);
key.appendChild(div);
pag_buttons.push(input_field);
pag_buttons.push(input_btn);
});
}
const paginationButton = (classname, arrow) => {
const button = document.createElement('button');
button.innerText = arrow;
button.classList.add('nav_button');
button.id = classname;
switch (classname) {
case 'btn_prev':
button.req_page = () => pag_current_page - 1;
button.disabled = true;
break;
case 'btn_next':
button.req_page = () => pag_current_page + 1;
button.disabled = false;
break;
case 'btn_last':
button.req_page = () => pag_count;
button.disabled = false;
break;
default: // 'btn_first'
button.req_page = () => 1;
button.disabled = true;
}
button.addEventListener('click', function() {
setCurrentPage(this.req_page());
});
return button;
};
const updateButtons = () => {
const prev_buttons = dom('#btn_first, #btn_prev');
const next_buttons = dom('#btn_next, #btn_last');
if (pag_current_page <= 1) {
each(prev_buttons, button => button.disabled = true);
each(next_buttons, button => button.disabled = false);
} else if (pag_current_page >= pag_count && pag_current_page > 1) {
each(next_buttons, button => button.disabled = true);
each(prev_buttons, button => button.disabled = false);
} else {
const nav_buttons = dom('#btn_first, #btn_prev, #btn_next, #btn_last');
each(nav_buttons, button => button.disabled = false);
}
const pag_pos = dom('.pag_pos');
each(pag_pos, el => updatePageStatus(el));
}
const updatePageStatus = (div) => {
const status = pag_current_page.toString().concat('/', pag_count.toString());
if (!div) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(status));
div.classList.add('pag_pos');
return div;
}
return div.innerText = status;
}
const pageInputForm = () => {
const input_field = document.createElement('input');
input_field.type = 'text';
// Use title instead of placeholder due to some translations not fitting in
input_field.classList.add('l10n_title-pagInputTxt'); // input_field.title = 'page';
input_field.placeholder = '...';
const input_btn = document.createElement('input');
input_btn.type = 'button';
input_btn.classList.add('l10n_val-pagInputBtn'); // input_btn.value = 'GO';
input_btn.addEventListener('click', () => {
if (input_field.value !== '' && input_field.value !== pag_current_page) {
let parsed = parseInt(input_field.value, 10);
if (!isNaN(parsed)) {
setCurrentPage(parsed);
}
}
input_field.value = "";
input_field.focus();
});
input_field.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && input_field.value && /[^\s]/.test(input_field.value)) {
if (input_field.value !== pag_current_page) {
e.preventDefault();
let parsed = parseInt(input_field.value, 10);
if (!isNaN(parsed) && parsed !== pag_current_page) {
setCurrentPage(parsed);
}
}
input_field.value = "";
input_field.focus();
};
});
// Only allow digits, new line and max page, no leading zero or spaces
setInputFilter(input_field, (value) => {
return /^[^0\s][\d]*$/.test(value) && value <= pag_count;
});
return {input_field, input_btn};
}
// Restricts input for the given textbox to the given inputFilter function.
// In the future we could use beforeinput instead.
function setInputFilter(textbox, inputFilter) {
["input", "keydown", "keyup", "mousedown", "mouseup", "select",
"contextmenu", "drop"].forEach(function(event) {
textbox.addEventListener(event, function(e) {
if (this.value === '') {
this.oldValue = this.value;
}
if (inputFilter(this.value)) {
this.oldValue = this.value;
this.oldSelectionStart = this.selectionStart;
this.oldSelectionEnd = this.selectionEnd;
} else if (this.hasOwnProperty("oldValue")) {
this.value = this.oldValue;
this.setSelectionRange(this.oldSelectionStart, this.oldSelectionEnd);
} else {
this.value = "";
}
});
});
}
const initPagSelector = () => {
if (settings.paginationItems.length > 0) {
dom(selectorTpl).appTo('#sidebar');
document.querySelector('#pag_select')
.addEventListener('change', onSelect);
for (let option of addOptions(getCachedPref())) {
option.appTo('#pag_select');
}
}
};
function onSelect() {
setPref(parseInt(this.value, 10));
onPagPrefUpdated();
}
const addOptions = (cached_pref) => {
const options = [];
let set_default = false;
for (let size of sortedSizes) {
let element;
if (size === cached_pref && !set_default) {
element = dom(`<option selected value="${size}"></option>`);
set_default = true;
} else {
element = dom(`<option value="${size}"></option>`);
}
element.addCls((size === 0) ? 'l10n-displayAll' : 'l10n_rp-perPage');
options.push(element);
}
return options;
}
const onLocationChanged = () => {
// Workaround to append this to the sidebar at the last position
// since the view module includes us before the other extensions
if (dom('#pag_select').length === 0) {
initPagSelector();
}
}
const setPayload = (payload) => {
// Not a copy, but we probably won't alter it anyway.
pag_payload = payload;
}
const getCachedPref = () => {
if (pag_rows_pref === undefined)
return defaultSize;
return pag_rows_pref;
};
// The module won't work if a view is not set first. We need to reuse some funcs
const setView = (view) => {
pag_view = view;
}
const canHandle = (items) => {
clear();
if (items.length > getCachedPref()) {
// Probably won't alter it, so we don't make a copy to save memory.
setup(items);
return true;
}
return false;
}
const isSortHandled = (fn) => {
if (!pag_active) {
return false;
}
sort(fn);
setCurrentPage();
return true;
}
const onPagPrefUpdated = () => {
if (pag_active) {
totalPages();
setCurrentPage();
return;
}
const pref = getCachedPref();
if (values(pag_payload.content).length > pref && pref != 0) {
setup(pag_view.filterPayload(pag_payload));
}
}
const isRefreshHandled = (item) => {
setPayload(item);
// Block if pagination is active
if (values(item.content).length > getCachedPref()) {
if (pag_active){
updateItems(pag_view.filterPayload(item));
sort(sortfn()); // initial sort
setCurrentPage();
return true;
}
setup(pag_view.filterPayload(item));
return true;
}
// No need for pagination, recreate the items, hide & pass to default logic
if (pag_active){
updateItems(pag_view.filterPayload(item));
setCurrentPage(1);
clear();
return true;
}
// We are not interested in handling the items
return false;
}
const setPref = (size) => {
const stored = store.get(storekey);
size = (size !== undefined) ? size : stored ? stored : defaultSize;
size = includes(settings.paginationItems, size) ? size : defaultSize;
store.put(storekey, size);
pag_rows_pref = size;
}
const init = () => {
setPref();
event.sub('location.changed', onLocationChanged);
};
init();
module.exports = {
$el: $pagination,
canHandle,
isActive,
isRefreshHandled,
isSortHandled,
setPayload,
setView
}

View file

@ -6,6 +6,7 @@ const resource = require('../core/resource');
const store = require('../core/store');
const allsettings = require('../core/settings');
const base = require('./base');
const pagination = require('./pagination');
const modes = ['details', 'grid', 'icons'];
const sizes = [20, 40, 60, 80, 100, 150, 200, 250, 300, 350, 400];
@ -183,6 +184,12 @@ const checkHint = () => {
};
const setItems = items => {
if (!pagination.canHandle(items)) {
doSetItems(items);
}
};
const doSetItems = items => {
const removed = map($items.find('.item'), el => el._item);
$items.find('.item').rm();
@ -218,6 +225,15 @@ const onLocationChanged = item => {
item = location.getItem();
}
pagination.setPayload(item);
const items = filterPayload(item);
setHint('empty');
setItems(items);
};
const filterPayload = item => {
const items = [];
if (item.parent && !settings.hideParentFolder) {
@ -229,12 +245,18 @@ const onLocationChanged = item => {
items.push(child);
}
});
setHint('empty');
setItems(items);
};
return items;
}
const onLocationRefreshed = (item, added, removed) => {
if (added.length === 0 && removed.length === 0){
return;
}
if (pagination.isRefreshHandled(item)) {
return;
}
const add = [];
each(added, child => {
@ -263,6 +285,7 @@ const init = () => {
set();
$view.appTo(base.$content);
pagination.$el.appTo(base.$content);
$hint.hide();
format.setDefaultMetric(settings.binaryPrefix);
@ -277,7 +300,9 @@ init();
module.exports = {
$el: $view,
filterPayload,
setItems,
doSetItems,
changeItems,
setLocation: onLocationChanged,
setHint,
@ -288,3 +313,6 @@ module.exports = {
getSize,
setSize
};
// For code reuse purposes
pagination.setView(module.exports);