mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
fixed bugs, added extra text on "no results found" to suggest server searching, and conditionally disable the button based on search content and current sort. Activities page still broken. Also fixed up cache generation, only one should ever run now, as should sorting. Two time thresholds exist, one to trigger a re-sync but do it in the background (i.e. send the old one to the requester), and one to re-sync and wait for it.
335 lines
13 KiB
TypeScript
335 lines
13 KiB
TypeScript
import { _get, _post, addLoader, removeLoader } from "./common";
|
|
import { Search, SearchConfiguration, SearchableItems } from "./search";
|
|
|
|
declare var window: GlobalWindow;
|
|
|
|
export class RecordCounter {
|
|
private _container: HTMLElement;
|
|
private _totalRecords: HTMLElement;
|
|
private _loadedRecords: HTMLElement;
|
|
private _shownRecords: HTMLElement;
|
|
private _selectedRecords: HTMLElement;
|
|
private _total: number;
|
|
private _loaded: number;
|
|
private _shown: number;
|
|
private _selected: number;
|
|
constructor(container: HTMLElement) {
|
|
this._container = container;
|
|
this._container.innerHTML = `
|
|
<span class="records-total"></span>
|
|
<span class="records-loaded"></span>
|
|
<span class="records-shown"></span>
|
|
<span class="records-selected"></span>
|
|
`;
|
|
this._totalRecords = document.getElementsByClassName("records-total")[0] as HTMLElement;
|
|
this._loadedRecords = document.getElementsByClassName("records-loaded")[0] as HTMLElement;
|
|
this._shownRecords = document.getElementsByClassName("records-shown")[0] as HTMLElement;
|
|
this._selectedRecords = document.getElementsByClassName("records-selected")[0] as HTMLElement;
|
|
this.total = 0;
|
|
this.loaded = 0;
|
|
this.shown = 0;
|
|
}
|
|
|
|
reset() {
|
|
this.total = 0;
|
|
this.loaded = 0;
|
|
this.shown = 0;
|
|
this.selected = 0;
|
|
}
|
|
|
|
// Sets the total using a PageCountDTO-returning API endpoint.
|
|
getTotal(endpoint: string) {
|
|
_get(endpoint, null, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4 || req.status != 200) return;
|
|
this.total = req.response["count"] as number;
|
|
});
|
|
}
|
|
|
|
get total(): number { return this._total; }
|
|
set total(v: number) {
|
|
this._total = v;
|
|
this._totalRecords.textContent = window.lang.var("strings", "totalRecords", `${v}`);
|
|
}
|
|
|
|
get loaded(): number { return this._loaded; }
|
|
set loaded(v: number) {
|
|
this._loaded = v;
|
|
this._loadedRecords.textContent = window.lang.var("strings", "loadedRecords", `${v}`);
|
|
}
|
|
|
|
get shown(): number { return this._shown; }
|
|
set shown(v: number) {
|
|
this._shown = v;
|
|
this._shownRecords.textContent = window.lang.var("strings", "shownRecords", `${v}`);
|
|
}
|
|
|
|
get selected(): number { return this._selected; }
|
|
set selected(v: number) {
|
|
this._selected = v;
|
|
if (v == 0) this._selectedRecords.textContent = ``;
|
|
else this._selectedRecords.textContent = window.lang.var("strings", "selectedRecords", `${v}`);
|
|
}
|
|
}
|
|
|
|
export interface PaginatedListConfig {
|
|
loader: HTMLElement;
|
|
loadMoreButton: HTMLButtonElement;
|
|
loadAllButton: HTMLButtonElement;
|
|
refreshButton: HTMLButtonElement;
|
|
filterArea: HTMLElement;
|
|
searchOptionsHeader: HTMLElement;
|
|
searchBox: HTMLInputElement;
|
|
recordCounter: HTMLElement;
|
|
totalEndpoint: string;
|
|
getPageEndpoint: string;
|
|
itemsPerPage: number;
|
|
maxItemsLoadedForSearch: number;
|
|
newElementsFromPage: (resp: paginatedDTO) => void;
|
|
updateExistingElementsFromPage: (resp: paginatedDTO) => void;
|
|
defaultSortField: string;
|
|
defaultSortAscending: boolean;
|
|
pageLoadCallback?: (req: XMLHttpRequest) => void;
|
|
}
|
|
|
|
export abstract class PaginatedList {
|
|
protected _c: PaginatedListConfig;
|
|
|
|
protected _search: Search;
|
|
|
|
protected _counter: RecordCounter;
|
|
|
|
protected _hasLoaded: boolean;
|
|
protected _lastLoad: number;
|
|
protected _page: number = 0;
|
|
protected _lastPage: boolean;
|
|
get lastPage(): boolean { return this._lastPage };
|
|
set lastPage(v: boolean) {
|
|
this._lastPage = v;
|
|
if (v) {
|
|
this._c.loadAllButton.classList.add("unfocused");
|
|
this._c.loadMoreButton.textContent = window.lang.strings("noMoreResults");
|
|
this._search.setServerSearchButtonsDisabled(this._search.inServerSearch);
|
|
this._c.loadMoreButton.disabled = true;
|
|
} else {
|
|
this._c.loadMoreButton.textContent = window.lang.strings("loadMore");
|
|
this._c.loadMoreButton.disabled = false;
|
|
this._search.setServerSearchButtonsDisabled(false);
|
|
this._c.loadAllButton.classList.remove("unfocused");
|
|
}
|
|
}
|
|
|
|
protected _previousPageSize = 0;
|
|
|
|
// Stores a PaginatedReqDTO-implementing thing.
|
|
// A standard PaginatedReqDTO will be overridden entirely,
|
|
// but a ServerSearchDTO will keep it's fields.
|
|
protected _searchParams: PaginatedReqDTO;
|
|
defaultParams = (): PaginatedReqDTO => {
|
|
return {
|
|
limit: 0,
|
|
page: 0,
|
|
sortByField: "",
|
|
ascending: false
|
|
};
|
|
}
|
|
|
|
constructor(c: PaginatedListConfig) {
|
|
this._c = c;
|
|
this._counter = new RecordCounter(this._c.recordCounter);
|
|
this._hasLoaded = false;
|
|
|
|
this._c.loadMoreButton.onclick = () => this.loadMore(null, false);
|
|
this._c.loadAllButton.onclick = () => {
|
|
addLoader(this._c.loadAllButton, true);
|
|
this.loadMore(null, true);
|
|
};
|
|
/* this._keepSearchingButton.onclick = () => {
|
|
addLoader(this._keepSearchingButton, true);
|
|
this.loadMore(() => removeLoader(this._keepSearchingButton, true));
|
|
}; */
|
|
// Since this.reload doesn't exist, we need an arrow function to wrap it.
|
|
// FIXME: Make sure it works though!
|
|
this._c.refreshButton.onclick = () => this.reload();
|
|
}
|
|
|
|
initSearch = (searchConfig: SearchConfiguration) => {
|
|
const previousCallback = searchConfig.onSearchCallback;
|
|
searchConfig.onSearchCallback = (visibleCount: number, newItems: boolean, loadAll: boolean) => {
|
|
this._counter.shown = visibleCount;
|
|
|
|
// if (this._search.inSearch && !this.lastPage) this._c.loadAllButton.classList.remove("unfocused");
|
|
// else this._c.loadAllButton.classList.add("unfocused");
|
|
|
|
if (this._search.sortField == this._c.defaultSortField && this._search.ascending == this._c.defaultSortAscending) {
|
|
this._search.setServerSearchButtonsDisabled(!this._search.inSearch)
|
|
} else {
|
|
this._search.setServerSearchButtonsDisabled(false)
|
|
}
|
|
|
|
// FIXME: Figure out why this makes sense and make it clearer.
|
|
if ((visibleCount < this._c.itemsPerPage && this._counter.loaded < this._c.maxItemsLoadedForSearch && !this.lastPage) || loadAll) {
|
|
if (!newItems ||
|
|
this._previousPageSize != visibleCount ||
|
|
(visibleCount == 0 && !this.lastPage) ||
|
|
loadAll
|
|
) {
|
|
this.loadMore(() => {}, loadAll);
|
|
}
|
|
}
|
|
this._previousPageSize = visibleCount;
|
|
if (previousCallback) previousCallback(visibleCount, newItems, loadAll);
|
|
};
|
|
const previousServerSearch = searchConfig.searchServer;
|
|
searchConfig.searchServer = (params: PaginatedReqDTO, newSearch: boolean) => {
|
|
this._searchParams = params;
|
|
if (newSearch) this.reload();
|
|
else this.loadMore(null, false);
|
|
|
|
if (previousServerSearch) previousServerSearch(params, newSearch);
|
|
};
|
|
searchConfig.clearServerSearch = () => {
|
|
console.log("Clearing server search");
|
|
this._page = 0;
|
|
this.reload();
|
|
}
|
|
this._search = new Search(searchConfig);
|
|
this._search.generateFilterList();
|
|
this.lastPage = false;
|
|
};
|
|
|
|
// Sets the elements with "name"s in "elements" as visible or not.
|
|
public abstract setVisibility: (elements: string[], visible: boolean) => void;
|
|
|
|
// Removes all elements, and reloads the first page.
|
|
// FIXME: Share more code between reload and loadMore, and go over the logic, it's messy.
|
|
public abstract reload: () => void;
|
|
protected _reload = (
|
|
callback?: (req: XMLHttpRequest) => void
|
|
) => {
|
|
this._lastLoad = Date.now();
|
|
this.lastPage = false;
|
|
|
|
this._counter.reset();
|
|
this._counter.getTotal(this._c.totalEndpoint);
|
|
|
|
// Reload all currently visible elements, i.e. Load a new page of size (limit*(page+1)).
|
|
let limit = this._c.itemsPerPage;
|
|
if (this._page != 0) {
|
|
limit *= this._page+1;
|
|
}
|
|
|
|
let params = this._search.inServerSearch ? this._searchParams : this.defaultParams();
|
|
params.limit = limit;
|
|
params.page = 0;
|
|
if (params.sortByField == "") {
|
|
params.sortByField = this._c.defaultSortField;
|
|
params.ascending = this._c.defaultSortAscending;
|
|
}
|
|
|
|
_post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4) return;
|
|
if (req.status != 200) {
|
|
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
|
|
if (callback) callback(req);
|
|
return;
|
|
}
|
|
|
|
this._hasLoaded = true;
|
|
// Allow refreshes every 15s
|
|
this._c.refreshButton.disabled = true;
|
|
setTimeout(() => this._c.refreshButton.disabled = false, 15000);
|
|
|
|
let resp = req.response as paginatedDTO;
|
|
|
|
this.lastPage = resp.last_page;
|
|
|
|
this._c.updateExistingElementsFromPage(resp);
|
|
|
|
this._counter.loaded = this._search.ordering.length;
|
|
|
|
this._search.onSearchBoxChange(true);
|
|
if (this._search.inSearch) {
|
|
// this._c.loadAllButton.classList.remove("unfocused");
|
|
} else {
|
|
this._counter.shown = this._counter.loaded;
|
|
this.setVisibility(this._search.ordering, true);
|
|
// this._search.showHideNotFoundPanel(false);
|
|
}
|
|
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
|
|
if (callback) callback(req);
|
|
}, true);
|
|
}
|
|
|
|
// Loads the next page. If "loadAll", all pages will be loaded until the last is reached.
|
|
public abstract loadMore: (callback: () => void, loadAll: boolean) => void;
|
|
protected _loadMore = (
|
|
loadAll: boolean = false,
|
|
callback?: (req: XMLHttpRequest) => void
|
|
) => {
|
|
this._lastLoad = Date.now();
|
|
this._c.loadMoreButton.disabled = true;
|
|
const timeout = setTimeout(() => {
|
|
this._c.loadMoreButton.disabled = false;
|
|
}, 1000);
|
|
this._page += 1;
|
|
|
|
let params = this._search.inServerSearch ? this._searchParams : this.defaultParams();
|
|
params.limit = this._c.itemsPerPage;
|
|
params.page = this._page;
|
|
if (params.sortByField == "") {
|
|
params.sortByField = this._c.defaultSortField;
|
|
params.ascending = this._c.defaultSortAscending;
|
|
}
|
|
|
|
_post(this._c.getPageEndpoint, params, (req: XMLHttpRequest) => {
|
|
if (req.readyState != 4) return;
|
|
if (req.status != 200) {
|
|
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
|
|
if (callback) callback(req);
|
|
return;
|
|
}
|
|
|
|
let resp = req.response as paginatedDTO;
|
|
|
|
// Check before setting this.lastPage so we have a chance to cancel the timeout.
|
|
if (resp.last_page) {
|
|
clearTimeout(timeout);
|
|
removeLoader(this._c.loadAllButton);
|
|
}
|
|
|
|
this.lastPage = resp.last_page;
|
|
|
|
this._c.newElementsFromPage(resp);
|
|
|
|
this._counter.loaded = this._search.ordering.length;
|
|
|
|
if (this._search.inSearch || loadAll) {
|
|
if (this.lastPage) {
|
|
loadAll = false;
|
|
}
|
|
this._search.onSearchBoxChange(true, loadAll);
|
|
} else {
|
|
this.setVisibility(this._search.ordering, true);
|
|
this._search.setNotFoundPanelVisibility(false);
|
|
}
|
|
if (this._c.pageLoadCallback) this._c.pageLoadCallback(req);
|
|
if (callback) callback(req);
|
|
}, true)
|
|
}
|
|
|
|
// Should be assigned to window.onscroll whenever the list is in view.
|
|
detectScroll = () => {
|
|
if (!this._hasLoaded || this.lastPage) return;
|
|
// console.log(window.innerHeight + document.documentElement.scrollTop, document.scrollingElement.scrollHeight);
|
|
if (Math.abs(window.innerHeight + document.documentElement.scrollTop - document.scrollingElement.scrollHeight) < 50) {
|
|
// window.notifications.customSuccess("scroll", "Reached bottom.");
|
|
// Wait .5s between loads
|
|
if (this._lastLoad + 500 > Date.now()) return;
|
|
this.loadMore(null, false);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|