commit
e048147248
@ -0,0 +1,369 @@ |
||||
import $ from 'jquery' |
||||
import map from 'lodash/map' |
||||
import merge from 'lodash/merge' |
||||
import humps from 'humps' |
||||
import URI from 'urijs' |
||||
import listMorph from '../lib/list_morph' |
||||
import reduceReducers from 'reduce-reducers' |
||||
import { createStore, connectElements } from '../lib/redux_helpers.js' |
||||
import '../app' |
||||
|
||||
const maxPageNumberInOneLine = 7 |
||||
const groupedPagesNumber = 3 |
||||
|
||||
/** |
||||
* |
||||
* This module is a clone of async_listing_load.js adapted for pagination with random access |
||||
* |
||||
*/ |
||||
|
||||
let enableFirstLoading = true |
||||
|
||||
export const asyncInitialState = { |
||||
/* it will consider any query param in the current URI as paging */ |
||||
beyondPageOne: false, |
||||
/* will be sent along with { type: 'JSON' } to controller, useful for dynamically changing parameters */ |
||||
additionalParams: {}, |
||||
/* an array with every html element of the list being shown */ |
||||
items: [], |
||||
/* the key for diffing the elements in the items array */ |
||||
itemKey: null, |
||||
/* represents whether a request is happening or not */ |
||||
loading: false, |
||||
/* if there was an error fetching items */ |
||||
requestError: false, |
||||
/* if response has no items */ |
||||
emptyResponse: false, |
||||
/* current's page number */ |
||||
currentPageNumber: 0 |
||||
} |
||||
|
||||
export function asyncReducer (state = asyncInitialState, action) { |
||||
switch (action.type) { |
||||
case 'ELEMENTS_LOAD': { |
||||
return Object.assign({}, state, { |
||||
nextPagePath: action.nextPagePath, |
||||
currentPagePath: action.nextPagePath |
||||
}) |
||||
} |
||||
case 'ADD_ITEM_KEY': { |
||||
return Object.assign({}, state, { itemKey: action.itemKey }) |
||||
} |
||||
case 'START_REQUEST': { |
||||
let pageNumber = state.currentPageNumber |
||||
if (action.pageNumber) { pageNumber = parseInt(action.pageNumber) } |
||||
|
||||
return Object.assign({}, state, { |
||||
loading: true, |
||||
requestError: false, |
||||
currentPagePath: action.path, |
||||
currentPageNumber: pageNumber, |
||||
items: generateStub(state.items.length) |
||||
}) |
||||
} |
||||
case 'REQUEST_ERROR': { |
||||
return Object.assign({}, state, { requestError: true }) |
||||
} |
||||
case 'FINISH_REQUEST': { |
||||
return Object.assign({}, state, { |
||||
loading: false |
||||
}) |
||||
} |
||||
case 'ITEMS_FETCHED': { |
||||
if (action.nextPageParams !== null) { |
||||
const pageNumber = parseInt(action.nextPageParams.pageNumber) |
||||
if (typeof action.path !== 'undefined') { |
||||
history.replaceState({}, null, URI(action.path).query(humps.decamelizeKeys(action.nextPageParams))) |
||||
} |
||||
delete action.nextPageParams.pageNumber |
||||
|
||||
return Object.assign({}, state, { |
||||
requestError: false, |
||||
emptyResponse: action.items.length === 0, |
||||
items: action.items, |
||||
nextPageParams: humps.decamelizeKeys(action.nextPageParams), |
||||
pagesLimit: parseInt(action.nextPageParams.pagesLimit), |
||||
currentPageNumber: pageNumber, |
||||
beyondPageOne: pageNumber !== 1 |
||||
}) |
||||
} |
||||
return Object.assign({}, state, { |
||||
requestError: false, |
||||
emptyResponse: action.items.length === 0, |
||||
items: action.items, |
||||
nextPageParams: humps.decamelizeKeys(action.nextPageParams), |
||||
pagesLimit: 1, |
||||
currentPageNumber: 1, |
||||
beyondPageOne: false |
||||
}) |
||||
} |
||||
default: |
||||
return state |
||||
} |
||||
} |
||||
|
||||
export const elements = { |
||||
'[data-async-listing]': { |
||||
load ($el) { |
||||
const nextPagePath = $el.data('async-listing') |
||||
|
||||
return { nextPagePath } |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-loading-message]': { |
||||
render ($el, state) { |
||||
if (state.loading) return $el.show() |
||||
|
||||
$el.hide() |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-empty-response-message]': { |
||||
render ($el, state) { |
||||
if ( |
||||
!state.requestError && |
||||
(!state.loading) && |
||||
state.items.length === 0 |
||||
) { |
||||
return $el.show() |
||||
} |
||||
|
||||
$el.hide() |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-error-message]': { |
||||
render ($el, state) { |
||||
if (state.requestError) return $el.show() |
||||
|
||||
$el.hide() |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-items]': { |
||||
render ($el, state, oldState) { |
||||
if (state.items === oldState.items) return |
||||
|
||||
if (state.itemKey) { |
||||
const container = $el[0] |
||||
const newElements = map(state.items, (item) => $(item)[0]) |
||||
listMorph(container, newElements, { key: state.itemKey }) |
||||
return |
||||
} |
||||
|
||||
$el.html(state.items) |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-next-page-button]': { |
||||
render ($el, state) { |
||||
if (state.emptyResponse) { |
||||
return $el.hide() |
||||
} |
||||
|
||||
$el.show() |
||||
if (state.requestError || state.currentPageNumber >= state.pagesLimit || state.loading) { |
||||
return $el.attr('disabled', 'disabled') |
||||
} |
||||
|
||||
$el.attr('disabled', false) |
||||
$el.attr('href', state.nextPagePath) |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-prev-page-button]': { |
||||
render ($el, state) { |
||||
if (state.emptyResponse) { |
||||
return $el.hide() |
||||
} |
||||
|
||||
$el.show() |
||||
if (state.requestError || state.currentPageNumber <= 1 || state.loading) { |
||||
return $el.attr('disabled', 'disabled') |
||||
} |
||||
|
||||
$el.attr('disabled', false) |
||||
$el.attr('href', state.prevPagePath) |
||||
} |
||||
}, |
||||
'[data-async-listing] [pages-numbers-container]': { |
||||
render ($el, state) { |
||||
if (typeof state.pagesLimit !== 'undefined') { pagesNumbersGenerate(state.pagesLimit, $el, state.currentPageNumber, state.loading) } |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-loading-button]': { |
||||
render ($el, state) { |
||||
if (state.loading) return $el.show() |
||||
|
||||
$el.hide() |
||||
} |
||||
}, |
||||
'[data-async-listing] [data-pagination-container]': { |
||||
render ($el, state) { |
||||
if (state.emptyResponse) { |
||||
return $el.hide() |
||||
} |
||||
|
||||
$el.show() |
||||
} |
||||
}, |
||||
'[csv-download]': { |
||||
render ($el, state) { |
||||
if (state.emptyResponse) { |
||||
return $el.hide() |
||||
} |
||||
return $el.show() |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Create a store combining the given reducer and initial state with the async reducer. |
||||
* |
||||
* reducer: The reducer that will be merged with the asyncReducer to add async |
||||
* loading capabilities to a page. Any state changes in the reducer passed will be |
||||
* applied AFTER the asyncReducer. |
||||
* |
||||
* initialState: The initial state to be merged with the async state. Any state |
||||
* values passed here will overwrite the values on asyncInitialState. |
||||
* |
||||
* itemKey: it will be added to the state as the key for diffing the elements and |
||||
* adding or removing with the correct animation. Check list_morph.js for more informantion. |
||||
*/ |
||||
export function createAsyncLoadStore (reducer, initialState, itemKey) { |
||||
const state = merge(asyncInitialState, initialState) |
||||
const store = createStore(reduceReducers(asyncReducer, reducer, state)) |
||||
|
||||
if (typeof itemKey !== 'undefined') { |
||||
store.dispatch({ |
||||
type: 'ADD_ITEM_KEY', |
||||
itemKey |
||||
}) |
||||
} |
||||
|
||||
connectElements({ store, elements }) |
||||
firstPageLoad(store) |
||||
return store |
||||
} |
||||
|
||||
export function refreshPage (store) { |
||||
loadPageByNumber(store, store.getState().currentPageNumber) |
||||
} |
||||
|
||||
export function loadPageByNumber (store, pageNumber) { |
||||
const path = $('[data-async-listing]').data('async-listing') |
||||
store.dispatch({ type: 'START_REQUEST', path, pageNumber }) |
||||
if (URI(path).query() !== '' && typeof store.getState().nextPageParams === 'undefined') { |
||||
$.getJSON(path, { type: 'JSON' }) |
||||
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED', path }, humps.camelizeKeys(response)))) |
||||
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' })) |
||||
.always(() => store.dispatch({ type: 'FINISH_REQUEST' })) |
||||
} else { |
||||
$.getJSON(URI(path).path(), merge({ type: 'JSON', page_number: pageNumber }, store.getState().nextPageParams)) |
||||
.done(response => store.dispatch(Object.assign({ type: 'ITEMS_FETCHED', path }, humps.camelizeKeys(response)))) |
||||
.fail(() => store.dispatch({ type: 'REQUEST_ERROR' })) |
||||
.always(() => store.dispatch({ type: 'FINISH_REQUEST' })) |
||||
} |
||||
} |
||||
|
||||
function firstPageLoad (store) { |
||||
const $element = $('[data-async-listing]') |
||||
function loadItemsNext () { |
||||
loadPageByNumber(store, store.getState().currentPageNumber + 1) |
||||
} |
||||
|
||||
function loadItemsPrev () { |
||||
loadPageByNumber(store, store.getState().currentPageNumber - 1) |
||||
} |
||||
|
||||
if (enableFirstLoading) { |
||||
loadItemsNext() |
||||
} |
||||
|
||||
$element.on('click', '[data-error-message]', (event) => { |
||||
event.preventDefault() |
||||
loadItemsNext() |
||||
}) |
||||
|
||||
$element.on('click', '[data-next-page-button]', (event) => { |
||||
event.preventDefault() |
||||
loadItemsNext() |
||||
}) |
||||
|
||||
$element.on('click', '[data-prev-page-button]', (event) => { |
||||
event.preventDefault() |
||||
loadItemsPrev() |
||||
}) |
||||
|
||||
$element.on('click', '[data-page-number]', (event) => { |
||||
event.preventDefault() |
||||
loadPageByNumber(store, event.target.dataset.pageNumber) |
||||
}) |
||||
|
||||
$element.on('submit', '[input-page-number-form]', (event) => { |
||||
event.preventDefault() |
||||
const $input = event.target.querySelector('#page-number') |
||||
const input = parseInt($input.value) |
||||
const loading = store.getState().loading |
||||
const pagesLimit = store.getState().pagesLimit |
||||
if (!isNaN(input) && input <= pagesLimit && !loading) { loadPageByNumber(store, input) } |
||||
if (!loading || isNaN(input) || input > pagesLimit) { $input.value = '' } |
||||
return false |
||||
}) |
||||
} |
||||
|
||||
const $element = $('[data-async-load]') |
||||
if ($element.length) { |
||||
if (Object.prototype.hasOwnProperty.call($element.data(), 'noFirstLoading')) { |
||||
enableFirstLoading = false |
||||
} |
||||
if (enableFirstLoading) { |
||||
const store = createStore(asyncReducer) |
||||
connectElements({ store, elements }) |
||||
firstPageLoad(store) |
||||
} |
||||
} |
||||
|
||||
function pagesNumbersGenerate (pagesLimit, $container, currentPageNumber, loading) { |
||||
let resultHTML = '' |
||||
if (pagesLimit < 1) { return } |
||||
if (pagesLimit <= maxPageNumberInOneLine) { |
||||
resultHTML = renderPaginationElements(1, pagesLimit, currentPageNumber, loading) |
||||
} else if (currentPageNumber < groupedPagesNumber) { |
||||
resultHTML += renderPaginationElements(1, groupedPagesNumber, currentPageNumber, loading) |
||||
resultHTML += renderPaginationElement('...', false, loading) |
||||
resultHTML += renderPaginationElement(pagesLimit, currentPageNumber === pagesLimit, loading) |
||||
} else if (currentPageNumber > pagesLimit - groupedPagesNumber) { |
||||
resultHTML += renderPaginationElement(1, currentPageNumber === 1, loading) |
||||
resultHTML += renderPaginationElement('...', false, loading) |
||||
resultHTML += renderPaginationElements(pagesLimit - groupedPagesNumber, pagesLimit, currentPageNumber, loading) |
||||
} else { |
||||
resultHTML += renderPaginationElement(1, currentPageNumber === 1, loading) |
||||
const step = parseInt(groupedPagesNumber / 2) |
||||
if (currentPageNumber - step - 1 === 2) { |
||||
resultHTML += renderPaginationElement(2, currentPageNumber === 2, loading) |
||||
} else if (currentPageNumber - step > 2) { |
||||
resultHTML += renderPaginationElement('...', false, loading) |
||||
} |
||||
resultHTML += renderPaginationElements(currentPageNumber - step, currentPageNumber + step, currentPageNumber, loading) |
||||
if (currentPageNumber + step + 1 === pagesLimit - 1) { |
||||
resultHTML += renderPaginationElement(pagesLimit - 1, pagesLimit - 1 === currentPageNumber, loading) |
||||
} else if (currentPageNumber + step < pagesLimit - 1) { |
||||
resultHTML += renderPaginationElement('...', false, loading) |
||||
} |
||||
resultHTML += renderPaginationElement(pagesLimit, currentPageNumber === pagesLimit, loading) |
||||
} |
||||
$container.html(resultHTML) |
||||
} |
||||
|
||||
function renderPaginationElements (start, end, currentPageNumber, loading) { |
||||
let resultHTML = '' |
||||
for (let i = start; i <= end; i++) { |
||||
resultHTML += renderPaginationElement(i, i === currentPageNumber, loading) |
||||
} |
||||
return resultHTML |
||||
} |
||||
|
||||
function renderPaginationElement (text, active, loading) { |
||||
return '<li class="page-item' + (active ? ' active' : '') + (text === '...' || loading ? ' disabled' : '') + '"><a class="page-link page-link-light-hover" data-page-number=' + text + '>' + text + '</a></li>' |
||||
} |
||||
|
||||
function generateStub (size) { |
||||
const stub = '<div data-loading-message data-selector="loading-message" class="tile tile-type-loading"> <div class="row tile-body"> <div class="tile-transaction-type-block col-md-2 d-flex flex-row flex-md-column"> <span class="tile-label"> <span class="tile-loader tile-label-loader"></span> </span> <span class="tile-status-label ml-2 ml-md-0"> <span class="tile-loader tile-label-loader"></span> </span> </div> <div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0"> <span class="tile-loader tile-address-loader"></span> <span class="tile-loader tile-address-loader"></span> </div> <div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-center text-md-right mt-3 mt-md-0 tile-bottom"> <span class="mr-2 mr-md-0 order-1"> <span class="tile-loader tile-label-loader"></span> </span> <span class="mr-2 mr-md-0 order-2"> <span class="tile-loader tile-label-loader"></span> </span> </div> </div> </div>' |
||||
return Array.from(Array(size > 10 ? 10 : 10), () => stub) // I decided to always put 10 lines in order to make page lighter
|
||||
} |
@ -0,0 +1,15 @@ |
||||
<div |
||||
class='pagination-container mlm17 mrm18 <%= if assigns[:position] == "top" do %>position-top<% end %> <%= if assigns[:position] == "bottom" do %>position-bottom<% end %>' |
||||
data-pagination-container> |
||||
<ul class="pagination align-end" pages-numbers-container> |
||||
</ul> |
||||
<ul class="pagination fml5 go-to"> |
||||
<!-- Go to --> |
||||
<li class="page-link no-hover tb ml10" ><%= gettext("Go to")%></li> |
||||
<li class="page-item"><form input-page-number-form><input class="page-number" id="page-number" type=text size=3><input class="d-none" type=submit input-page-number></form></li> |
||||
</ul> |
||||
</div> |
||||
<%= if assigns[:showing_limit] do %> |
||||
<div class="limit mrm18" >(<%= gettext("Only the first")%> <%= assigns[:showing_limit] |> BlockScoutWeb.Cldr.Number.to_string! %> <%= gettext("elements are displayed")%>) |
||||
</div> |
||||
<% end %> |
Loading…
Reference in new issue