Enhancing Oracle APEX Interactive Report Pagination with Custom JavaScript

Enhancing Oracle APEX Interactive Report Pagination with Custom JavaScript

Introduction

Oracle APEX provides Interactive Reports (IR) with default pagination options, but in some cases, the navigation might lack the intuitive controls users expect, such as first/last buttons and a row range selector. In this blog, I'll show you how to enhance your Interactive Report pagination using custom JavaScript and ensure that it dynamically adjusts when filters or other changes are applied.

How This Works with the Current IR Pagination?

Oracle APEX’s default pagination offers simple "Next" and "Previous" buttons, which work fine for basic navigation but fall short when users need more control over how they navigate through large datasets. To improve this:

  • We will add first and last buttons to quickly navigate to the start and end of the data.

  • Introduce a dropdown to allow users to select specific ranges of rows directly.

This JavaScript will extend the existing X - Y of Z pagination controls and can be initialized for all Interactive Reports on a specific page. When added to the global page (Page 0), it will affect all Interactive Reports in the application.

Step 1: The Custom JavaScript for Enhanced Pagination

The first step involves writing the JavaScript that will power the custom pagination logic. This script will handle the creation of new pagination controls, including the dropdown for row ranges and the buttons for navigating to the first and last pages.

var s4s = s4s || {};
s4s.apex = s4s.apex || {};

var s4s_apex_ir_pagination = {
    'firstPageTitle': 'First Page',
    'lastPageTitle': 'Last Page',

    // Create the dropdown for row range selection
    'createPaginationSelect': function (currentPageStart, rowsPerPage, totalRows, totalPages) {
        // Create the <select> element
        let paginationSelect = document.createElement('select');
        paginationSelect.className = 'a-IRR-pagination-select u-TF-item u-TF-item--select s4s-pagination-item';

        // Loop through pages and create <option> elements
        for (let i = 0; i < totalPages; i++) {
            let startRow = i * rowsPerPage + 1;
            let endRow = Math.min((i + 1) * rowsPerPage, totalRows);

            // Create <option> element
            let option = document.createElement('option');
            option.value = startRow;
            option.textContent = s4s_apex_ir_pagination.toSeparatedPageNumber(startRow) + ' - ' + s4s_apex_ir_pagination.toSeparatedPageNumber(endRow) + ' of ' + totalRows;

            // Set selected attribute for the current page
            if (startRow === currentPageStart) {
                option.textContent = 'row(s) ' + option.textContent;
                option.selected = true;
            }

            // Append the option to the select
            paginationSelect.appendChild(option);
        }

        return paginationSelect;
    },

    // Create a pagination control (first/last page buttons)
    'createPaginationItem': function (regionId, startRow, rowsPerPage, pageTitle, pageIcon, pageDisabled) {
        // Create the <li> element
        let paginationItem = document.createElement('li');
        paginationItem.className = 'a-IRR-pagination-item s4s-pagination-item';

        // Create the button inside the <li> element
        let paginationButton = s4s_apex_ir_pagination.createPaginationButton(regionId, startRow, rowsPerPage, pageTitle, pageIcon, pageDisabled);

        // Append the button to the <li>
        paginationItem.appendChild(paginationButton);

        return paginationItem;
    },

    // Create the pagination button element
    'createPaginationButton': function (regionId, startRow, rowsPerPage, pageTitle, pageIcon, pageDisabled) {
        // Create the button element
        let button = document.createElement('button');
        button.className = 'a-Button a-IRR-button a-IRR-button--pagination';
        button.setAttribute('data-pagination', s4s_apex_ir_pagination.getPaginationData(startRow, rowsPerPage));
        button.setAttribute('aria-label', pageTitle);
        button.setAttribute('title', pageTitle);
        button.setAttribute('aria-controls', regionId);
        button.type = 'button';

        // Create the icon element inside the button
        let icon = document.createElement('span');
        icon.className = pageIcon;
        icon.setAttribute('aria-hidden', 'true');
        button.appendChild(icon);

        // Disable the button if necessary
        if (pageDisabled) {
            button.disabled = true;
        }

        return button;
    },

    // Inject custom pagination controls into the Interactive Report's pagination area
    'addPaginationControls': function (regionId, rowsPerPage, currentPageStart, totalRows, totalPages) {
        let regionElement = document.getElementById(regionId);

        // Remove previous pagination items created by this script
        let paginationItems = regionElement.querySelectorAll('.s4s-pagination-item');
        paginationItems.forEach(item => item.remove());

        // Find the existing pagination container
        let pagination = regionElement.querySelector('.a-IRR-pagination');

        // Find the pagination label and hide it
        let paginationLabel = pagination.querySelector('.a-IRR-pagination-label');
        paginationLabel.style.display = 'none';

        // Create and insert the pagination select dropdown
        let paginationSelect = s4s_apex_ir_pagination.createPaginationSelect(currentPageStart, rowsPerPage, totalRows, totalPages);
        paginationLabel.after(paginationSelect);

        // Create the dropdown pagination button
        let paginationButton = s4s_apex_ir_pagination.createPaginationButton(regionId, currentPageStart, rowsPerPage, '@', 'fa fa-chevron-down', false);
        paginationButton.style.display = 'none';
        paginationLabel.after(paginationButton);

        // Create the first and last pagination buttons
        let firstButton = s4s_apex_ir_pagination.createPaginationItem(regionId, 1, rowsPerPage, s4s_apex_ir_pagination.firstPageTitle, 'fa fa-page-first', currentPageStart === 1);
        let lastButton = s4s_apex_ir_pagination.createPaginationItem(regionId, (totalPages - 1) * rowsPerPage + 1, rowsPerPage, s4s_apex_ir_pagination.lastPageTitle, 'fa fa-page-last', (currentPageStart + rowsPerPage) > totalRows);

        // Insert first and last buttons
        pagination.insertBefore(firstButton, pagination.firstChild);
        pagination.appendChild(lastButton);
    },

    // Initialize the pagination enhancements
    'initIRPagination': function () {
        document.querySelectorAll('.a-IRR-container').forEach(function (container) {
            let label = container.querySelector('.a-IRR-pagination-label');
            if (!label) return;

            // Extract pagination details from the label (start row, total rows, etc.)
            let match = new RegExp(/(\d+)\s*-\s*(\d+)\s*[^\d]+\s*(\d+)/gi).exec(label.textContent.trim().replace(/,/g, ''));
            if (!match) return;

            let regionId = container.id.slice(0, -3);
            let rowsPerPage = parseInt(document.getElementById(regionId + '_row_select').value);
            let currentPageStart = parseInt(match[1]);
            let totalRows = parseInt(match[3]);

            // Inject the enhanced pagination controls
            s4s_apex_ir_pagination.addPaginationControls(regionId, rowsPerPage, currentPageStart, totalRows, Math.ceil(totalRows / rowsPerPage));
        });

        // Add event listeners to dropdown changes for pagination update
        document.querySelectorAll('.a-IRR-pagination-select').forEach(function (select) {
            select.addEventListener('change', function () {
                let regionId = select.closest('.a-IRR-container').id.slice(0, -3);
                let rowsPerPage = parseInt(document.getElementById(regionId + '_row_select').value);
                select.previousElementSibling.setAttribute('data-pagination', s4s_apex_ir_pagination.getPaginationData(select.value, rowsPerPage));
                select.previousElementSibling.click();
            });
        });
    },

    // Utility to format numbers with commas
    'toSeparatedPageNumber': function (num) {
        return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    },

    // Utility to construct pagination data
    'getPaginationData': function (startRow, rowsPerPage) {
        return 'pgR_min_row=' + startRow + 'max_rows=' + rowsPerPage + 'rows_fetched=' + rowsPerPage;
    }
};

s4s.apex.ir_pagination = s4s_apex_ir_pagination;

Pagination Module Definition:

  1. Pagination Initialization (initIRPagination):

    • This method automatically detects all Interactive Reports on the page, reads their current pagination settings, and injects the new controls (dropdown for row ranges, "First" and "Last" buttons).
  2. Dynamic Row Ranges:

    • The createPaginationSelect method creates a dropdown based on the total number of pages, allowing users to jump to specific row ranges.
  3. Navigation Buttons:

    • createPaginationItem adds the "First" and "Last" buttons, which are disabled if the user is already on the first or last page, respectively.
  4. Updating the Report:

    • When the user interacts with the controls, the pagination settings are updated, and the report refreshes to show the desired page.

Step 2: Include the Script in Your APEX Page

Frist of all make sure the JavaScript is available for your Oracle APEX page, by adding this to your Static Application Files or upload it to your Static File Server and include it in your Page Attributes > JavaScript > File URLs.

To use this script, add it to your page under Page Attributes > JavaScript > Function and Global Variable Declaration. This will ensure it loads and runs on the desired page.

s4s.apex.ir_pagination.initIRPagination();

Step 3: Ensure Recalculation on "After Refresh" Event

When users filter or sort the report, the pagination settings (number of rows, start row, total rows, etc.) change. Therefore, the JavaScript enhancement needs to be re-applied after each refresh. We'll handle this by triggering the script on the "After Refresh" event for the region to ensure the pagination dynamically recalculates and updates after the report is reloaded.

  • In your Oracle APEX page, create a Dynamic Action on your Interactive Report region.

  • Set the event to After Refresh.

  • Set the action to Execute JavaScript Code and enter the following:

s4s.apex.ir_pagination.initIRPagination();

Step 4: Handling Scenarios with Pagination Disabled

This is covered, the code will stop executing if it’s unable to find the pagination label.

💡
Tip: For scenarios where pagination is disabled, Oracle APEX provides an internal optimization feature called APEX$USE_NO_PAGINATION. This is particularly useful for handling reports with smaller datasets where showing all rows at once won't impact performance, but it can cause issues with larger datasets.

Conclusion

This custom JavaScript solution provides a more robust and user-friendly pagination experience for Oracle APEX Interactive Reports, adding first/last page buttons and a row range dropdown to the existing controls. Once initialized, it will automatically enhance all Interactive Reports with X - Y of Z pagination on a page or across the application if added to the global page (Page 0).

By integrating it with the "After Refresh" event, the pagination controls will dynamically recalculate every time the report is filtered, sorted, or refreshed, ensuring the user interface remains accurate and up-to-date.

For reports with pagination disabled, remember to leverage the internal APEX$USE_NO_PAGINATION optimization feature introduced in APEX 18.2, but use it cautiously to avoid performance bottlenecks with large datasets.