Brazen Paginator

Helper for client side customized pagination

Skrip ini tidak untuk dipasang secara langsung. Ini adalah pustaka skrip lain untuk disertakan dengan direktif meta // @require https://update.greasyfork.org/scripts/424499/1114815/Brazen%20Paginator.js

// ==UserScript==
// @name         Brazen Paginator
// @namespace    brazenvoid
// @version      2.0.0
// @author       brazenvoid
// @license      GPL-3.0-only
// @description  Helper for client side customized pagination
// ==/UserScript==

class BrazenPaginator
{
    /**
     * @typedef {{itemListSelector: JQuery.Selector, itemSelectors?: JQuery.Selector, lastPageUrl: string,
     *            onGetPageNoFromUrl: PaginatorGetPageNoFromUrlHandler, onGetPageUrlFromPageNo: PaginatorGetPageUrlFromPageNoHandler,
     *            onGetPaginationElementForPageNo: PaginatorGetPaginationElementForPageNoHandler, paginationWrapper: JQuery}} PaginatorConfiguration
     */
    
    /**
     * @callback PaginatorAfterPaginationEventHandler
     * @param {BrazenPaginator} paginator
     */

    /**
     * @callback PaginatorGetPageNoFromUrlHandler
     * @param {string} pageUrl
     * @param {BrazenPaginator} paginator
     */

    /**
     * @callback PaginatorGetPageUrlFromPageNoHandler
     * @param {number} pageNo
     * @param {BrazenPaginator} paginator
     */

    /**
     * @callback PaginatorGetPaginationElementForPageNoHandler
     * @param {number} pageNo
     * @param {BrazenPaginator} paginator
     */

    /**
     * @param {PaginatorConfiguration} configuration
     */
    constructor (configuration)
    {
        /**
         * @type {PaginatorConfiguration}
         * @private
         */
        this._config = configuration
        
        /**
         * @type {number}
         * @private
         */
        this._currentPageNo = 0

        /**
         * @type {number}
         * @private
         */
        this._lastPageNo = 0

        /**
         * @type {boolean}
         * @private
         */
        this._pageConcatenated = false

        /**
         * @type {number}
         * @private
         */
        this._paginatedPageNo = 0

        /**
         * @type {JQuery}
         * @private
         */
        this._targetElement = null

        // Events and callbacks

        /**
         * @type {PaginatorAfterPaginationEventHandler}
         * @private
         */
        this._onAfterPagination = null
    }

    _conformUIToNewPaginatedState ()
    {
        if (this._pageConcatenated) {
            this._pageConcatenated = false

            let currentPageElement = this.getPaginationElementForPageNo(this._currentPageNo)
            let newSubsequentPageNo = this._paginatedPageNo + 1
            let newSubsequentPageNoUrl = this.getPageUrlFromPageNo(newSubsequentPageNo)

            // Mutate current page no element to show paginated page numbers

            currentPageElement.text(this._currentPageNo + '-' + this._paginatedPageNo)

            // Get next pages' pagination elements

            let currentNextPageElements = currentPageElement.nextAll()

            if (this._paginatedPageNo === this._lastPageNo) {

                // Delete all pagination elements if last page is paginated

                currentNextPageElements.remove()

            } else {

                // Determine whether the paginated page immediately precedes the last page

                if (newSubsequentPageNo !== this._lastPageNo) {

                    // If not so, determine whether pagination element for the page following the paginated page exists

                    let newSubsequentPageElement = this.getPaginationElementForPageNo(newSubsequentPageNo)
                    if (!newSubsequentPageElement.length) {

                        // If it does not exist then try getting the old next page no element

                        let oldSubsequentPageElement = this.getPaginationElementForPageNo(this._currentPageNo + 1)
                        if (oldSubsequentPageElement.length) {

                            // If it does exist then mutate it for this purpose

                            oldSubsequentPageElement.attr('href', newSubsequentPageNoUrl).text(newSubsequentPageNo)

                        } else {

                            // If even that does not exist, then clone the less desirable alternative; the last page element and mutate it to this use

                            let lastPageElement = this.getPaginationElementForPageNo(this._lastPageNo)
                            lastPageElement.clone().insertAfter(currentPageElement).attr('href', newSubsequentPageNoUrl).text(newSubsequentPageNo)

                        }
                    }

                    // Remove any other pagination elements for already paginated pages

                    currentNextPageElements.each((index, element) => {
                        let paginationLink = $(element)
                        let paginationLinkUrl = paginationLink.attr('href')
                        if (paginationLinkUrl && this.getPageNoFromUrl(paginationLinkUrl) <= this._paginatedPageNo) {
                            paginationLink.remove()
                        }
                    })
                }
            }
            Utilities.callEventHandler(this._onAfterPagination, [this])
        }
    }

    /**
     * @param {number} threshold
     * @param {number} limit
     * @private
     */
    _loadAndParseNextPage (threshold, limit)
    {
        let lastPageHasNotBeenReached = this._paginatedPageNo < this._lastPageNo
        let paginationLimitHasNotBeenMet = limit > 0 && (this._paginatedPageNo - this._currentPageNo) < limit
        
        let compliantItemsAreLessThanTheThreshold =
            this._targetElement.find(this._config.itemSelectors + ':not(.noncompliant-item)').length < threshold

        if (lastPageHasNotBeenReached && paginationLimitHasNotBeenMet && compliantItemsAreLessThanTheThreshold) {

            this._sandbox.load(this.getPageUrlFromPageNo(++this._paginatedPageNo) + ' ' + this._config.itemListSelector, '', () => {
                this._pageConcatenated = true
                this._sandbox.find(this._config.itemSelectors).insertAfter(this._targetElement.find(this._config.itemSelectors + ':last'))
                this._sandbox.empty()
            })
        } else {
            this._conformUIToNewPaginatedState()
        }
    }

    getCurrentPageNo ()
    {
        return this._currentPageNo
    }

    getItemListSelector ()
    {
        return this._config.itemListSelector
    }

    getLastPageNo ()
    {
        return this._lastPageNo
    }

    /**
     * @param {string} pageUrl
     * @return {number}
     */
    getPageNoFromUrl (pageUrl)
    {
        return this._config.onGetPageNoFromUrl(pageUrl , this)
    }

    /**
     * @param {number} pageNo
     * @return {string}
     */
    getPageUrlFromPageNo (pageNo)
    {
        return this._config.onGetPageUrlFromPageNo(pageNo, this)
    }

    getPaginatedPageNo ()
    {
        return this._paginatedPageNo
    }

    /**
     * @param {number} pageNo
     * @return {JQuery}
     */
    getPaginationElementForPageNo (pageNo)
    {
        return this._config.onGetPaginationElementForPageNo(pageNo, this)
    }

    getPaginationWrapper ()
    {
        return this._config.paginationWrapper
    }

    initialize ()
    {
        this._currentPageNo = this.getPageNoFromUrl(window.location.href)
        this._lastPageNo = this.getPageNoFromUrl(this._config.lastPageUrl)
        this._paginatedPageNo = this._currentPageNo
        this._sandbox = $('<div id="brazen-paginator-sandbox" hidden/>').appendTo('body')
        this._targetElement = $(this._config.itemListSelector + ':first')
        return this
    }

    /**
     * @param {PaginatorAfterPaginationEventHandler} handler
     * @return {this}
     */
    onAfterPagination (handler)
    {
        this._onAfterPagination = handler
        return this
    }

    run (threshold, limit)
    {
        if (this._config.paginationWrapper.length && threshold) {
            this._loadAndParseNextPage(threshold, limit)
        }
        return this
    }
}