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 = ` `; 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); } } }