Source

lib/components/pagination/pagination.component.ts

import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { chevronBackOutline, chevronForwardOutline } from 'ionicons/icons';
import { ForAngularModule } from '../../for-angular.module';
import { NgxBaseComponent } from '../../engine/NgxBaseComponent';
import { EventConstants, KeyValue, StringOrBoolean } from '../../engine';
import { PaginationCustomEvent } from './constants';

/**
 * @description A pagination component for navigating through multiple pages of content.
 * @summary This component provides a user interface for paginated content navigation,
 * displaying page numbers and navigation controls. It supports customizable page counts,
 * current page tracking, and emits events when users navigate between pages.
 *
 * The component intelligently handles large numbers of pages by showing a subset of page
 * numbers with ellipses to indicate skipped pages, ensuring the UI remains clean and usable
 * even with many pages.
 *
 * @mermaid
 * sequenceDiagram
 *   participant U as User
 *   participant P as PaginationComponent
 *   participant E as External Component
 *
 *   U->>P: Click page number
 *   P->>P: navigate(page)
 *   P->>P: handleClick(direction, page)
 *   P->>E: Emit clickEvent with PaginationCustomEvent
 *
 *   U->>P: Click next button
 *   P->>P: next()
 *   P->>P: handleClick('next')
 *   P->>E: Emit clickEvent with PaginationCustomEvent
 *
 *   U->>P: Click previous button
 *   P->>P: previous()
 *   P->>P: handleClick('previous')
 *   P->>E: Emit clickEvent with PaginationCustomEvent
 *
 * @example
 * <ngx-decaf-pagination
 *   [pages]="10"
 *   [current]="3"
 *   (clickEvent)="handlePageChange($event)">
 * </ngx-decaf-pagination>
 *
 * @extends {NgxBaseComponent}
 * @implements {OnInit}
 */
@Component({
  selector: 'ngx-decaf-pagination',
  templateUrl: './pagination.component.html',
  styleUrls: ['./pagination.component.scss'],
  imports: [
    ForAngularModule,
    IonIcon
  ],
  standalone: true,

})
export class PaginationComponent extends NgxBaseComponent implements OnInit {

  /**
   * @description Controls whether the component uses translation services.
   * @summary When set to true, the component will attempt to use translation services
   * for any text content. This allows for internationalization of the pagination component.
   *
   * @type {StringOrBoolean}
   * @default true
   * @memberOf PaginationComponent
   */
  override translatable: StringOrBoolean = true;

  /**
   * @description The total number of pages to display in the pagination component.
   * @summary Specifies the total number of pages available for navigation. This is a required
   * input that determines how many page numbers will be generated and displayed.
   *
   * @type {number}
   * @required
   * @memberOf PaginationComponent
   */
  @Input({ required: true })
  totalPages!: number;

  /**
   * @description The currently active page number.
   * @summary Specifies which page is currently active or selected. This value is used
   * to highlight the current page in the UI and as a reference point for navigation.
   *
   * @type {number}
   * @default 1
   * @memberOf PaginationComponent
   */
  @Input()
  current = 1;

  /**
   * @description Array of page objects for rendering in the template.
   * @summary Contains the processed page data used for rendering the pagination UI.
   * Each object includes an index (page number) and text representation.
   *
   * @type {KeyValue[]}
   * @memberOf PaginationComponent
   */
  pages!: KeyValue[];

  /**
   * @description The last page number in the pagination.
   * @summary Stores the number of the last page for boundary checking during navigation.
   *
   * @type {number}
   * @memberOf PaginationComponent
   */
  last!: number;

  /**
   * @description Event emitter for pagination navigation events.
   * @summary Emits a custom event when users navigate between pages, either by clicking
   * on page numbers or using the next/previous buttons. The event contains information
   * about the navigation direction and the target page number.
   *
   * @type {EventEmitter<PaginationCustomEvent>}
   * @memberOf PaginationComponent
   */
  @Output()
  clickEvent: EventEmitter<PaginationCustomEvent> = new EventEmitter<PaginationCustomEvent>();

  /**
   * @constructor
   * @description Initializes a new instance of the PaginationComponent.
   * Calls the parent constructor with the component name for generate base locale string.
   */
  constructor() {
    super("PaginationComponent");
     addIcons({chevronBackOutline, chevronForwardOutline});
  }

  /**
   * @description Initializes the component after Angular sets the input properties.
   * @summary Sets up the component by initializing the locale settings based on the
   * translatable property, generating the page numbers based on the total pages and
   * current page, and storing the last page number for boundary checking.
   *
   * @mermaid
   * sequenceDiagram
   *   participant A as Angular Lifecycle
   *   participant P as PaginationComponent
   *
   *   A->>P: ngOnInit()
   *   P->>P: getLocale(translatable)
   *   P->>P: Set locale
   *   P->>P: getPages(data, current)
   *   P->>P: Set pages array
   *   P->>P: Set last page number
   *
   * @returns {void}
   * @memberOf PaginationComponent
   */
  ngOnInit(): void {
    this.locale = this.getLocale(this.translatable);
    this.pages = this.getPages(this.totalPages, this.current) as KeyValue[];
    this.last = this.totalPages;
  }

  /**
   * @description Handles click events on pagination controls.
   * @summary Processes user interactions with the pagination component, updating the
   * current page if specified and emitting an event with navigation details. This method
   * is called when users click on page numbers or navigation buttons.
   *
   * @param {('next' | 'previous')} direction - The direction of navigation
   * @param {number} [page] - Optional page number to navigate to directly
   * @returns {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant P as PaginationComponent
   *   participant E as External Component
   *
   *   U->>P: Click pagination control
   *   P->>P: handleClick(direction, page?)
   *   alt page is provided
   *     P->>P: Update current page
   *   end
   *   P->>E: Emit clickEvent with direction and page
   *
   * @memberOf PaginationComponent
   */
  handleClick(direction: 'next' | 'previous', page?: number): void {
    if(page)
      this.current = page;
    this.clickEvent.emit({
      name: EventConstants.CLICK,
      data: {
        direction,
        page: this.current
      },
      component: this.componentName
    } as PaginationCustomEvent);
  }

  /**
   * @description Generates the array of page objects for display.
   * @summary Creates an array of page objects based on the total number of pages and
   * the current page. For small page counts (≤5), all pages are shown. For larger page
   * counts, a subset is shown with ellipses to indicate skipped pages. This ensures
   * the pagination UI remains clean and usable even with many pages.
   *
   * @param {number} total - The total number of pages
   * @param {number} [current] - The current active page (defaults to this.current)
   * @returns {KeyValue[]} Array of page objects with index and text properties
   *
   * @mermaid
   * flowchart TD
   *   A[Start] --> B{total <= 5?}
   *   B -->|Yes| C[Show all pages]
   *   B -->|No| D[Show first page]
   *   D --> E[Show last pages]
   *   E --> F[Add ellipses for skipped pages]
   *   C --> G[Return pages array]
   *   F --> G
   *
   * @memberOf PaginationComponent
   */
  getPages(total: number, current?: number): KeyValue[] {
    if (!current) current = this.current;

    const pages: KeyValue[] = [];

    function getPage(index: number | null, text = '') {
        if (pages.some(item => item['index'] === index)) return;
        pages.push({ index, text: index != null ? index.toString().padStart(2, '0') : text });
    }

    if (total <= 5) {
        for (let i = 1; i <= total; i++) getPage(i);
    } else {
      // Adiciona os dois primeiros
      getPage(1);
      getPage(2);

      // Adiciona "..." entre os blocos
      if (current && current > 3) getPage(null, '...');

      // Adiciona a página atual (se estiver no meio)
      if (current && current > 2 && current < total - 1) getPage(current);

      // Adiciona "..." entre os blocos
      if (current && current < total - 2) getPage(null, '...');

      // Adiciona os dois últimos
      getPage(total - 1);
      getPage(total);
    }

    return pages;
  }

  /**
   * @description Gets the current active page number.
   * @summary Returns the current page number that is active in the pagination component.
   * This method provides a way to access the current page state from outside the component.
   *
   * @returns {number} The current page number
   * @memberOf PaginationComponent
   */
  getCurrent(): number {
    return this.current;
  }

  /**
   * @description Navigates to the next page.
   * @summary Increments the current page number if not at the last page and triggers
   * the click event handler with 'next' direction. This method is typically called
   * when the user clicks on the "next" button in the pagination UI.
   *
   * @returns {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant P as PaginationComponent
   *
   *   U->>P: Click next button
   *   P->>P: next()
   *   alt page <= max pages
   *     P->>P: Increment current page
   *     P->>P: handleClick('next')
   *   end
   *
   * @memberOf PaginationComponent
   */
  next(): void {
    const page = this.current + 1;
    if(page <= Object.keys(this.pages)?.length || 0) {
      this.current = page;
      this.handleClick('next');
    }
  }

  /**
   * @description Navigates to the previous page.
   * @summary Decrements the current page number if not at the first page and triggers
   * the click event handler with 'previous' direction. This method is typically called
   * when the user clicks on the "previous" button in the pagination UI.
   *
   * @returns {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant P as PaginationComponent
   *
   *   U->>P: Click previous button
   *   P->>P: previous()
   *   alt page > 0
   *     P->>P: Decrement current page
   *     P->>P: handleClick('previous')
   *   end
   *
   * @memberOf PaginationComponent
   */
  previous(): void {
    const page = this.current - 1;
    if(page > 0) {
      this.current = page;
      this.handleClick('previous');
    }
  }

  /**
   * @description Navigates to a specific page number.
   * @summary Updates the current page to the specified page number and triggers
   * the click event handler with the appropriate direction. This method is typically
   * called when the user clicks directly on a page number in the pagination UI.
   *
   * @param {number | null} page - The page number to navigate to
   * @returns {void}
   *
   * @mermaid
   * sequenceDiagram
   *   participant U as User
   *   participant P as PaginationComponent
   *
   *   U->>P: Click page number
   *   P->>P: navigate(page)
   *   alt page is not null and different from current
   *     P->>P: Determine direction (next/previous)
   *     P->>P: handleClick(direction, page)
   *   end
   *
   * @memberOf PaginationComponent
   */
  navigate(page: number | null): void {
    if(page !== null && this.current !== page as number)
      this.handleClick(page > this.current ? 'next' : 'previous', page);
  }
}