diff --git a/apps/docs/docs/components/pagination/_both.component.html b/apps/docs/docs/components/pagination/_both.component.html new file mode 100644 index 00000000..de67c8ca --- /dev/null +++ b/apps/docs/docs/components/pagination/_both.component.html @@ -0,0 +1,3 @@ + diff --git a/apps/docs/docs/components/pagination/_both.component.ts b/apps/docs/docs/components/pagination/_both.component.ts new file mode 100644 index 00000000..b8598de6 --- /dev/null +++ b/apps/docs/docs/components/pagination/_both.component.ts @@ -0,0 +1,10 @@ +import { PaginationComponent } from 'flowbite-angular/pagination'; + +import { Component } from '@angular/core'; + +@Component({ + selector: 'flowbite-demo-pagination-both', + imports: [PaginationComponent], + templateUrl: './_both.component.html', +}) +export class FlowbiteBothComponent {} diff --git a/apps/docs/docs/components/pagination/_custom.component.html b/apps/docs/docs/components/pagination/_custom.component.html new file mode 100644 index 00000000..19f614d8 --- /dev/null +++ b/apps/docs/docs/components/pagination/_custom.component.html @@ -0,0 +1,9 @@ + diff --git a/apps/docs/docs/components/pagination/_custom.component.ts b/apps/docs/docs/components/pagination/_custom.component.ts new file mode 100644 index 00000000..7d963383 --- /dev/null +++ b/apps/docs/docs/components/pagination/_custom.component.ts @@ -0,0 +1,10 @@ +import { PaginationComponent } from 'flowbite-angular/pagination'; + +import { Component } from '@angular/core'; + +@Component({ + selector: 'flowbite-demo-pagination-custom', + imports: [PaginationComponent], + templateUrl: './_custom.component.html', +}) +export class FlowbiteCustomComponent {} diff --git a/apps/docs/docs/components/pagination/_default.component.html b/apps/docs/docs/components/pagination/_default.component.html new file mode 100644 index 00000000..1edfc8d3 --- /dev/null +++ b/apps/docs/docs/components/pagination/_default.component.html @@ -0,0 +1 @@ + diff --git a/apps/docs/docs/components/pagination/_default.component.ts b/apps/docs/docs/components/pagination/_default.component.ts new file mode 100644 index 00000000..3658c97a --- /dev/null +++ b/apps/docs/docs/components/pagination/_default.component.ts @@ -0,0 +1,10 @@ +import { PaginationComponent } from 'flowbite-angular/pagination'; + +import { Component } from '@angular/core'; + +@Component({ + selector: 'flowbite-demo-pagination-default', + imports: [PaginationComponent], + templateUrl: './_default.component.html', +}) +export class FlowbiteDefaultComponent {} diff --git a/apps/docs/docs/components/pagination/_text.component.html b/apps/docs/docs/components/pagination/_text.component.html new file mode 100644 index 00000000..1bccc97d --- /dev/null +++ b/apps/docs/docs/components/pagination/_text.component.html @@ -0,0 +1,3 @@ + diff --git a/apps/docs/docs/components/pagination/_text.component.ts b/apps/docs/docs/components/pagination/_text.component.ts new file mode 100644 index 00000000..399b549e --- /dev/null +++ b/apps/docs/docs/components/pagination/_text.component.ts @@ -0,0 +1,10 @@ +import { PaginationComponent } from 'flowbite-angular/pagination'; + +import { Component } from '@angular/core'; + +@Component({ + selector: 'flowbite-demo-pagination-text', + imports: [PaginationComponent], + templateUrl: './_text.component.html', +}) +export class FlowbiteTextComponent {} diff --git a/apps/docs/docs/components/pagination/index.md b/apps/docs/docs/components/pagination/index.md new file mode 100644 index 00000000..25811e20 --- /dev/null +++ b/apps/docs/docs/components/pagination/index.md @@ -0,0 +1,51 @@ +--- +keyword: PaginationPage +--- + +## Default pagination + +{{ NgDocActions.demo('flowbiteDefaultComponent', {container: false}) }} + +```angular-html file="./_default.component.html" group="default" name="html" + +``` + +```angular-ts file="./_default.component.ts" group="default" name="typescript" + +``` + +## Pagination with text + +{{ NgDocActions.demo('flowbiteTextComponent', {container: false}) }} + +```angular-html file="./_text.component.html" group="text" name="html" + +``` + +```angular-ts file="./_text.component.ts"#L1 group="text" name="typescript" + +``` + +## Pagination with text and icon + +{{ NgDocActions.demo('flowbiteBothComponent', {container: false}) }} + +```angular-html file="./_both.component.html" group="both" name="html" + +``` + +```angular-ts file="./_both.component.ts"#L1 group="both" name="typescript" + +``` + +## Customized pagination + +{{ NgDocActions.demo('flowbiteCustomComponent', {container: false}) }} + +```angular-html file="./_custom.component.html" group="custom" name="html" + +``` + +```angular-ts file="./_custom.component.ts"#L1 group="custom" name="typescript" + +``` diff --git a/apps/docs/docs/components/pagination/ng-doc.page.ts b/apps/docs/docs/components/pagination/ng-doc.page.ts new file mode 100644 index 00000000..c5f84115 --- /dev/null +++ b/apps/docs/docs/components/pagination/ng-doc.page.ts @@ -0,0 +1,27 @@ +import ComponentCategory from '../ng-doc.category'; +import { FlowbiteBothComponent } from './_both.component'; +import { FlowbiteCustomComponent } from './_custom.component'; +import { FlowbiteDefaultComponent } from './_default.component'; +import { FlowbiteTextComponent } from './_text.component'; + +import type { NgDocPage } from '@ng-doc/core'; + +/** + * Use the pagination component to show a list of buttons to navigate in your tables + * + * @status:info NEW + */ +const pagination: NgDocPage = { + title: 'Pagination', + mdFile: './index.md', + category: ComponentCategory, + order: 10, + demos: { + flowbiteDefaultComponent: FlowbiteDefaultComponent, + flowbiteTextComponent: FlowbiteTextComponent, + flowbiteBothComponent: FlowbiteBothComponent, + flowbiteCustomComponent: FlowbiteCustomComponent, + }, +}; + +export default pagination; diff --git a/apps/docs/docs/getting-started/quickstart/ng-doc.page.ts b/apps/docs/docs/getting-started/quickstart/ng-doc.page.ts index 600ae44c..0d5fc9a6 100644 --- a/apps/docs/docs/getting-started/quickstart/ng-doc.page.ts +++ b/apps/docs/docs/getting-started/quickstart/ng-doc.page.ts @@ -5,7 +5,7 @@ import type { NgDocPage } from '@ng-doc/core'; /** * Get started with flowbite-angular by including it into your project using NPM * - * @status:alert UPDATES + * @status:success UPDATES */ const Quickstart: NgDocPage = { title: 'Quickstart', diff --git a/apps/docs/docs/getting-started/versioning/index.md b/apps/docs/docs/getting-started/versioning/index.md index 37c571c0..0d9ceb35 100644 --- a/apps/docs/docs/getting-started/versioning/index.md +++ b/apps/docs/docs/getting-started/versioning/index.md @@ -6,6 +6,8 @@ keyword: VersioningPage | Flowbite-angular version | Angular version | TailwindCSS version | | ------------------------ | --------------- | ------------------- | +| 1.4.0 | >=18.0.0 | ^3.0.0 | +| 1.3.0 | >=18.0.0 | ^3.0.0 | | 1.2.0 | >=18.0.0 | ^3.0.0 | | 1.1.1 | 18.0.0 | ^3.0.24 | | 1.0.0 | 18.0.0 | ^3.0.24 | diff --git a/apps/docs/docs/getting-started/versioning/ng-doc.page.ts b/apps/docs/docs/getting-started/versioning/ng-doc.page.ts index 161c026c..5bbf3680 100644 --- a/apps/docs/docs/getting-started/versioning/ng-doc.page.ts +++ b/apps/docs/docs/getting-started/versioning/ng-doc.page.ts @@ -6,7 +6,7 @@ import type { NgDocPage } from '@ng-doc/core'; * Here is a reference table that matches versions of flowbite-angular with its Angular version. It * also shows the TailwindCSS version used. * - * @status:alert NEW + * @status:info NEW */ const Versioning: NgDocPage = { title: 'Versioning', diff --git a/apps/docs/docs/ng-doc.api.ts b/apps/docs/docs/ng-doc.api.ts index 00fd14c8..20fa44af 100644 --- a/apps/docs/docs/ng-doc.api.ts +++ b/apps/docs/docs/ng-doc.api.ts @@ -32,6 +32,7 @@ const api: NgDocApi = { 'libs/flowbite-angular/indicator/index.ts', 'libs/flowbite-angular/modal/index.ts', 'libs/flowbite-angular/navbar/index.ts', + 'libs/flowbite-angular/pagination/index.ts', 'libs/flowbite-angular/scroll-top/index.ts', 'libs/flowbite-angular/sidebar/index.ts', 'libs/flowbite-angular/theme/index.ts', diff --git a/libs/flowbite-angular/core/flowbite.theme.init.ts b/libs/flowbite-angular/core/flowbite.theme.init.ts index e73e8cf5..f68c5585 100644 --- a/libs/flowbite-angular/core/flowbite.theme.init.ts +++ b/libs/flowbite-angular/core/flowbite.theme.init.ts @@ -118,6 +118,12 @@ import { navbarToggleTheme, NavbarToggleThemeService, } from 'flowbite-angular/navbar'; +import { + FLOWBITE_PAGINATION_THEME_TOKEN, + paginationDefaultValueProvider, + paginationTheme, + PaginationThemeService, +} from 'flowbite-angular/pagination'; import { FLOWBITE_SCROLL_TOP_THEME_TOKEN, scrollTopDefaultValueProvider, @@ -263,6 +269,10 @@ export function initFlowbite(): EnvironmentProviders { provide: NavbarThemeService, useClass: NavbarThemeService, }, + { + provide: PaginationThemeService, + useClass: PaginationThemeService, + }, { provide: ScrollTopThemeService, useClass: ScrollTopThemeService, @@ -391,6 +401,10 @@ export function initFlowbite(): EnvironmentProviders { provide: FLOWBITE_NAVBAR_THEME_TOKEN, useValue: navbarTheme, }, + { + provide: FLOWBITE_PAGINATION_THEME_TOKEN, + useValue: paginationTheme, + }, { provide: FLOWBITE_SCROLL_TOP_THEME_TOKEN, useValue: scrollTopTheme, @@ -447,6 +461,7 @@ export function initFlowbite(): EnvironmentProviders { navbarIconButtonDefaultValueProvider, navbarContentDefaultValueProvider, navbarBrandDefaultThemeProvider, + paginationDefaultValueProvider, scrollTopDefaultValueProvider, sidebarDefaultValueProvider, sidebarToggleDefaultValueProvider, diff --git a/libs/flowbite-angular/pagination/README.md b/libs/flowbite-angular/pagination/README.md new file mode 100644 index 00000000..50e27d7e --- /dev/null +++ b/libs/flowbite-angular/pagination/README.md @@ -0,0 +1,4 @@ +# flowbite-angular/pagination + +Secondary entry point of `flowbite-angular`. It can be used by importing from +`flowbite-angular/pagination`. diff --git a/libs/flowbite-angular/pagination/index.ts b/libs/flowbite-angular/pagination/index.ts new file mode 100644 index 00000000..c4d931d4 --- /dev/null +++ b/libs/flowbite-angular/pagination/index.ts @@ -0,0 +1,23 @@ +export { + PaginationComponent, + paginationDefaultValueProvider, + FLOWBITE_PAGINATION_CURRENT_PAGE_DEFAULT_VALUE, + FLOWBITE_PAGINATION_BUTTON_PROPERTIES_DEFAULT_VALUE, + FLOWBITE_PAGINATION_CUSTOM_STYLE_DEFAULT_VALUE, + FLOWBITE_PAGINATION_PREVIOUS_ICON_DEFAULT_VALUE, + FLOWBITE_PAGINATION_NEXT_ICON_DEFAULT_VALUE, + FLOWBITE_PAGINATION_FIRST_ICON_DEFAULT_VALUE, + FLOWBITE_PAGINATION_LAST_ICON_DEFAULT_VALUE, + FLOWBITE_PAGINATION_TABS_DEFAULT_VALUE, + FLOWBITE_PAGINATION_PAGE_SIZE_DEFAULT_VALUE, + FLOWBITE_PAGINATION_HAS_FIRST_LAST_DEFAULT_VALUE, + FLOWBITE_PAGINATION_HAS_PREV_NEXT_DEFAULT_VALUE, + FLOWBITE_PAGINATION_NAVIGATION_MODE_DEFAULT_VALUE, + FLOWBITE_PAGINATION_SIZE_DEFAULT_VALUE, +} from './pagination.component'; +export type { PaginationProperties, PaginationClass, PaginationTheme } from './pagination.theme'; +export { paginationTheme } from './pagination.theme'; +export { + PaginationThemeService, + FLOWBITE_PAGINATION_THEME_TOKEN, +} from './pagination.theme.service'; diff --git a/libs/flowbite-angular/pagination/ng-package.json b/libs/flowbite-angular/pagination/ng-package.json new file mode 100644 index 00000000..1f7f6a0d --- /dev/null +++ b/libs/flowbite-angular/pagination/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "./index.ts" + } +} diff --git a/libs/flowbite-angular/pagination/pagination.component.ts b/libs/flowbite-angular/pagination/pagination.component.ts new file mode 100644 index 00000000..4f46bed4 --- /dev/null +++ b/libs/flowbite-angular/pagination/pagination.component.ts @@ -0,0 +1,534 @@ +import type { + PaginationClass, + PaginationNavigation, + PaginationSizes, + PaginationTheme, +} from './pagination.theme'; +import { PaginationThemeService } from './pagination.theme.service'; + +import type { DeepPartial } from 'flowbite-angular'; +import { BaseComponent } from 'flowbite-angular'; +import { ButtonComponent, type ButtonProperties, type ButtonTheme } from 'flowbite-angular/button'; +import { IconComponent, IconRegistry } from 'flowbite-angular/icon'; +import { + CHEVRON_DOUBLE_LEFT_SVG_ICON, + CHEVRON_DOUBLE_RIGHT_SVG_ICON, + CHEVRON_LEFT_SVG_ICON, + CHEVRON_RIGHT_SVG_ICON, +} from 'flowbite-angular/utils'; + +import { NgTemplateOutlet } from '@angular/common'; +import type { TemplateRef } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + InjectionToken, + makeEnvironmentProviders, + model, + ViewEncapsulation, +} from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +export const FLOWBITE_PAGINATION_CURRENT_PAGE_DEFAULT_VALUE = new InjectionToken( + 'FLOWBITE_PAGINATION_CURRENT_PAGE_DEFAULT_VALUE' +); + +export const FLOWBITE_PAGINATION_CUSTOM_STYLE_DEFAULT_VALUE = new InjectionToken< + DeepPartial +>('FLOWBITE_PAGINATION_CUSTOM_STYLE_DEFAULT_VALUE'); + +export const FLOWBITE_PAGINATION_PREVIOUS_ICON_DEFAULT_VALUE = new InjectionToken< + TemplateRef | undefined +>('FLOWBITE_PAGINATION_PREVIOUS_ICON_DEFAULT_VALUE'); + +export const FLOWBITE_PAGINATION_NEXT_ICON_DEFAULT_VALUE = new InjectionToken< + TemplateRef | undefined +>('FLOWBITE_PAGINATION_NEXT_ICON_DEFAULT_VALUE'); + +export const FLOWBITE_PAGINATION_FIRST_ICON_DEFAULT_VALUE = new InjectionToken< + TemplateRef | undefined +>('FLOWBITE_PAGINATION_FIRST_ICON_DEFAULT_VALUE'); + +export const FLOWBITE_PAGINATION_LAST_ICON_DEFAULT_VALUE = new InjectionToken< + TemplateRef | undefined +>('FLOWBITE_PAGINATION_LAST_ICON_DEFAULT_VALUE'); + +export const FLOWBITE_PAGINATION_TABS_DEFAULT_VALUE = new InjectionToken( + 'FLOWBITE_PAGINATION_TABS_DEFAULT_VALUE' +); + +export const FLOWBITE_PAGINATION_PAGE_SIZE_DEFAULT_VALUE = new InjectionToken( + 'FLOWBITE_PAGINATION_PAGE_SIZE_DEFAULT_VALUE' +); + +export const FLOWBITE_PAGINATION_HAS_FIRST_LAST_DEFAULT_VALUE = new InjectionToken( + 'FLOWBITE_PAGINATION_HAS_FIRST_LAST_DEFAULT_VALUE' +); + +export const FLOWBITE_PAGINATION_HAS_PREV_NEXT_DEFAULT_VALUE = new InjectionToken( + 'FLOWBITE_PAGINATION_HAS_PREV_NEXT_DEFAULT_VALUE' +); + +export const FLOWBITE_PAGINATION_NAVIGATION_MODE_DEFAULT_VALUE = new InjectionToken< + keyof PaginationNavigation +>('FLOWBITE_PAGINATION_NAVIGATION_MODE_DEFAULT_VALUE'); + +export const FLOWBITE_PAGINATION_SIZE_DEFAULT_VALUE = new InjectionToken( + 'FLOWBITE_PAGINATION_SIZE_DEFAULT_VALUE' +); + +export const FLOWBITE_PAGINATION_BUTTON_PROPERTIES_DEFAULT_VALUE = new InjectionToken< + DeepPartial +>('FLOWBITE_PAGINATION_BUTTON_PROPERTIES_DEFAULT_VALUE'); + +export const paginationDefaultValueProvider = makeEnvironmentProviders([ + { + provide: FLOWBITE_PAGINATION_CURRENT_PAGE_DEFAULT_VALUE, + useValue: 1, + }, + { + provide: FLOWBITE_PAGINATION_CUSTOM_STYLE_DEFAULT_VALUE, + useValue: {}, + }, + { + provide: FLOWBITE_PAGINATION_PREVIOUS_ICON_DEFAULT_VALUE, + useValue: undefined, + }, + { + provide: FLOWBITE_PAGINATION_NEXT_ICON_DEFAULT_VALUE, + useValue: undefined, + }, + { + provide: FLOWBITE_PAGINATION_FIRST_ICON_DEFAULT_VALUE, + useValue: undefined, + }, + { + provide: FLOWBITE_PAGINATION_LAST_ICON_DEFAULT_VALUE, + useValue: undefined, + }, + { + provide: FLOWBITE_PAGINATION_TABS_DEFAULT_VALUE, + useValue: 5, + }, + { + provide: FLOWBITE_PAGINATION_PAGE_SIZE_DEFAULT_VALUE, + useValue: 25, + }, + { + provide: FLOWBITE_PAGINATION_HAS_FIRST_LAST_DEFAULT_VALUE, + useValue: true, + }, + { + provide: FLOWBITE_PAGINATION_HAS_PREV_NEXT_DEFAULT_VALUE, + useValue: true, + }, + { + provide: FLOWBITE_PAGINATION_NAVIGATION_MODE_DEFAULT_VALUE, + useValue: 'icon', + }, + { + provide: FLOWBITE_PAGINATION_SIZE_DEFAULT_VALUE, + useValue: 'md', + }, + { + provide: FLOWBITE_PAGINATION_BUTTON_PROPERTIES_DEFAULT_VALUE, + useValue: { + color: 'primary', + fill: 'outline', + customStyle: { + root: { + base: { + default: + 'cursor-pointer !leading-tight !inline-flex !items-center !py-1.5 !ms-0 !place-content-center !rounded-none first:!rounded-l-lg last:!rounded-r-lg data-[active=false]:!text-inherit data-[active=false]:!border-inherit data-[active=false]:hover:!bg-gray-100 data-[active=false]:dark:hover:!bg-gray-700 data-[active=true]:!bg-opacity-75', + }, + size: { + sm: 'text-sm px-3 h-8', + md: 'text-base px-4 h-10', + }, + }, + } as DeepPartial, + } as DeepPartial, + }, +]); + +@Component({ + selector: 'flowbite-pagination', + standalone: true, + imports: [IconComponent, NgTemplateOutlet, ButtonComponent], + template: ` + @if (hasFirstLast()) { + + @if (['icon', 'both'].includes(navigationMode())) { + @if (firstIcon()) { + + } @else { + + } + } + @if (['text', 'both'].includes(navigationMode())) { + First + } + + } + + @if (hasPrevNext()) { + + @if (['icon', 'both'].includes(navigationMode())) { + @if (previousIcon()) { + + } @else { + + } + } + @if (['text', 'both'].includes(navigationMode())) { + Previous + } + + } + + @for (page of visiblePages(); track $index) { + + {{ page }} + + } + + @if (hasPrevNext()) { + + @if (['text', 'both'].includes(navigationMode())) { + Next + } + @if (['icon', 'both'].includes(navigationMode())) { + @if (lastIcon()) { + + } @else { + + } + } + + } + + @if (hasFirstLast()) { + + @if (['text', 'both'].includes(navigationMode())) { + Last + } + @if (['icon', 'both'].includes(navigationMode())) { + @if (lastIcon()) { + + } @else { + + } + } + + } + `, + encapsulation: ViewEncapsulation.None, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PaginationComponent extends BaseComponent { + /** + * Service injected used to generate class + */ + public readonly themeService = inject(PaginationThemeService); + /** + * `IconRegistry` service + */ + public readonly iconRegistry = inject(IconRegistry); + /** + * `DomSanitizer` service + */ + public readonly domSanitizer = inject(DomSanitizer); + + /** + * Value of the total items + */ + public readonly totalItems = model(); + /** + * Value of the total pages + */ + public readonly totalPages = model(); + /** + * Value of the current page + * + * @default 1 + */ + public readonly currentPage = model(inject(FLOWBITE_PAGINATION_CURRENT_PAGE_DEFAULT_VALUE)); + /** + * Value of how many tabs are displayed + * + * @default 5 + */ + public readonly tabs = model(inject(FLOWBITE_PAGINATION_TABS_DEFAULT_VALUE)); + /** + * Value of how many items are in a tab + * + * @default 25 + */ + public readonly pageSize = model(inject(FLOWBITE_PAGINATION_PAGE_SIZE_DEFAULT_VALUE)); + /** + * Whether to show or hide previous and next buttons + * + * @default true + */ + public readonly hasPrevNext = model(inject(FLOWBITE_PAGINATION_HAS_PREV_NEXT_DEFAULT_VALUE)); + /** + * Whether to show or hide first and last buttons + * + * @default true + */ + public readonly hasFirstLast = model(inject(FLOWBITE_PAGINATION_HAS_FIRST_LAST_DEFAULT_VALUE)); + /** + * Value of the navigation button's type + * + * @default icon + */ + public readonly navigationMode = model(inject(FLOWBITE_PAGINATION_NAVIGATION_MODE_DEFAULT_VALUE)); + /** + * Value of the component's size + * + * @default md + */ + public readonly size = model(inject(FLOWBITE_PAGINATION_SIZE_DEFAULT_VALUE)); + /** + * Value of the previous icon + * + * @default undefined + */ + public readonly previousIcon = model(inject(FLOWBITE_PAGINATION_PREVIOUS_ICON_DEFAULT_VALUE)); + /** + * Value of the next icon + * + * @default undefined + */ + public readonly nextIcon = model(inject(FLOWBITE_PAGINATION_NEXT_ICON_DEFAULT_VALUE)); + /** + * Value of the first icon + * + * @default undefined + */ + public readonly firstIcon = model(inject(FLOWBITE_PAGINATION_FIRST_ICON_DEFAULT_VALUE)); + /** + * Value of the last icon + * + * @default undefined + */ + public readonly lastIcon = model(inject(FLOWBITE_PAGINATION_LAST_ICON_DEFAULT_VALUE)); + /** + * Value of the aria-label + * + * @default Pagination navigation + */ + public readonly ariaLabel = model('Pagination navigation'); + /** + * Set the custom style for this pagination + */ + public readonly customStyle = model(inject(FLOWBITE_PAGINATION_CUSTOM_STYLE_DEFAULT_VALUE)); + /** + * Set the properties of all buttons in navigation + * + * @default { + color: 'primary', + fill: 'outline', + customStyle: { + root: { + base: { + default: + 'cursor-pointer !leading-tight !inline-flex !items-center !py-1.5 !ms-0 !place-content-center !rounded-none first:!rounded-l-lg last:!rounded-r-lg data-[active=false]:!text-inherit data-[active=false]:!border-inherit data-[active=false]:hover:!bg-gray-100 data-[active=false]:dark:hover:!bg-gray-700 data-[active=true]:!bg-opacity-75', + }, + size: { + sm: 'text-sm px-3 h-8', + md: 'text-base px-4 h-10', + }, + }, + }, + } + */ + public readonly buttonProperties = model( + inject(FLOWBITE_PAGINATION_BUTTON_PROPERTIES_DEFAULT_VALUE) + ); + + /** + * Value of the first visible page + */ + public readonly firstPageToShow = computed(() => { + if (this.currentPage() <= Math.floor(this.tabs() / 2)) { + return 1; + } + + if (this.currentPage() > this.maxPages() - Math.ceil(this.tabs() / 2)) { + return this.maxPages() - this.visiblePagesCount() + 1; + } + + return this.currentPage() - Math.floor(this.tabs() / 2); + }); + + /** + * Value of the maximum pages. If `totalPages` is given, it will be + * equal to that; otherwise, it is calculated from `totalItems`. + */ + public readonly maxPages = computed(() => { + if (this.totalPages() !== undefined) { + /** + * Note that if we return just `this.totalPages()`, the type of the computed + * will be `Signal`, even though there is a check. + * So instead of that, we return `this.totalPages()!` to ensure + * the type is always `Signal`. + */ + return this.totalPages()!; + } + + /** + * The same applies here, except there is no need to check for undefined, + * because if `totalPages` is undefined, `totalItems` must have + * a valid number value. We check it in the init function. + */ + return Math.max(Math.ceil(this.totalItems()! / this.pageSize()), 1); + }); + + /** + * Array of the visible page tabs + */ + public readonly visiblePages = computed(() => { + return Array.from({ length: this.visiblePagesCount() }, (_, i) => this.firstPageToShow() + i); + }); + + /** + * Real value of the current page + * + * If the given `currentPage` is bigger than the `maxPages`, + * the last page will be the active one + */ + public readonly visibleCurrentPage = computed(() => { + return Math.min(this.currentPage(), this.maxPages()); + }); + + /** + * Value of how many page tabs to display + */ + public readonly visiblePagesCount = computed(() => { + return Math.min(this.tabs(), this.maxPages()); + }); + + //#region BaseComponent implementation + public override fetchClass(): PaginationClass { + return this.themeService.getClasses({ + customStyle: this.customStyle(), + size: this.size(), + }); + } + + public override init(): void { + if (this.totalPages() === undefined && this.totalItems() === undefined) { + throw new Error('Either `totalPages` or `totalItems` must have a value'); + } + + this.iconRegistry.addRawSvgIconInNamepsace( + 'flowbite-angular', + 'chevron-left', + this.domSanitizer.bypassSecurityTrustHtml(CHEVRON_LEFT_SVG_ICON) + ); + + this.iconRegistry.addRawSvgIconInNamepsace( + 'flowbite-angular', + 'chevron-double-left', + this.domSanitizer.bypassSecurityTrustHtml(CHEVRON_DOUBLE_LEFT_SVG_ICON) + ); + + this.iconRegistry.addRawSvgIconInNamepsace( + 'flowbite-angular', + 'chevron-right', + this.domSanitizer.bypassSecurityTrustHtml(CHEVRON_RIGHT_SVG_ICON) + ); + + this.iconRegistry.addRawSvgIconInNamepsace( + 'flowbite-angular', + 'chevron-double-right', + this.domSanitizer.bypassSecurityTrustHtml(CHEVRON_DOUBLE_RIGHT_SVG_ICON) + ); + } + //#endregion + + /** + * Sets the value of the `currentPage` if it's between 1 and `maxPages` + * @param page number of the active page + */ + public goToPage(page: number) { + if (page < 1 || page > this.maxPages()) return; + this.currentPage.set(page); + } + + /** + * Decreases the value of `currentPage` if it's more than 1 + */ + public goToPreviousPage() { + if (this.visibleCurrentPage() <= 1) return; + this.currentPage.update((value) => value - 1); + } + + /** + * Increases the value of `currentPage` if it's less than `maxPages` + */ + public goToNextPage() { + if (this.visibleCurrentPage() >= this.maxPages()) return; + this.currentPage.update((value) => value + 1); + } + + /** + * Sets the value of `currentPage` to 1 + */ + public goToFirstPage() { + this.currentPage.set(1); + } + + /** + * Sets the value of `currentPage` equal to `maxPages` + */ + public goToLastPage() { + this.currentPage.set(this.maxPages()); + } +} diff --git a/libs/flowbite-angular/pagination/pagination.theme.service.ts b/libs/flowbite-angular/pagination/pagination.theme.service.ts new file mode 100644 index 00000000..4c4db798 --- /dev/null +++ b/libs/flowbite-angular/pagination/pagination.theme.service.ts @@ -0,0 +1,37 @@ +import type { PaginationClass, PaginationProperties, PaginationTheme } from './pagination.theme'; + +import type { FlowbiteThemeService } from 'flowbite-angular'; +import { mergeTheme } from 'flowbite-angular/utils'; + +import { inject, Injectable, InjectionToken } from '@angular/core'; +import { twMerge } from 'tailwind-merge'; + +/** + * `InjectionToken` used to import `PaginationTheme` value + * + * @example + * ``` + * var theme = inject(FLOWBITE_PAGINATION_THEME) + * ``` + */ +export const FLOWBITE_PAGINATION_THEME_TOKEN = new InjectionToken( + 'FLOWBITE_PAGINATION_THEME_TOKEN' +); + +@Injectable({ + providedIn: 'root', +}) +export class PaginationThemeService implements FlowbiteThemeService { + public readonly baseTheme = inject(FLOWBITE_PAGINATION_THEME_TOKEN); + + public getClasses(properties: PaginationProperties): PaginationClass { + const theme: PaginationTheme = mergeTheme(this.baseTheme, properties.customStyle); + + const output: PaginationClass = { + rootClass: twMerge(theme.root.base, theme.root.size[properties.size]), + iconClass: twMerge(theme.icon.size[properties.size]), + }; + + return output; + } +} diff --git a/libs/flowbite-angular/pagination/pagination.theme.ts b/libs/flowbite-angular/pagination/pagination.theme.ts new file mode 100644 index 00000000..40749cb9 --- /dev/null +++ b/libs/flowbite-angular/pagination/pagination.theme.ts @@ -0,0 +1,65 @@ +import type { DeepPartial, FlowbiteClass, FlowbiteSizes } from 'flowbite-angular'; +import { createTheme } from 'flowbite-angular/utils'; + +/** + * Available navigation types for `PaginationButtonDirective` + */ +export interface PaginationNavigation { + icon: string; + text: string; + both: string; +} + +/** + * Available sizes for `PaginationComponent` + */ +export interface PaginationSizes extends Pick { + [key: string]: string; +} + +/** + * Required properties for class generation of `PaginationComponent` + */ +export interface PaginationProperties { + size: keyof PaginationSizes; + customStyle: DeepPartial; +} + +/** + * Theme definition for `PaginationComponent` + */ +export interface PaginationTheme { + root: { + base: string; + size: PaginationSizes; + }; + icon: { + size: PaginationSizes; + }; +} + +/** + * Default theme for `PaginationComponent` + */ +export const paginationTheme: PaginationTheme = createTheme({ + root: { + base: 'group inline-flex -space-x-px border-gray-300 dark:border-gray-700', + size: { + sm: 'text-sm', + md: 'text-base', + }, + }, + icon: { + size: { + sm: 'w-5 h-5', + md: 'w-6 h-6', + }, + }, +}); + +/** + * Generated class definition for `PaginationComponent` + */ +export interface PaginationClass extends FlowbiteClass { + iconClass: string; +} diff --git a/libs/flowbite-angular/utils/icon.list.ts b/libs/flowbite-angular/utils/icon.list.ts index 190a6567..e883e579 100644 --- a/libs/flowbite-angular/utils/icon.list.ts +++ b/libs/flowbite-angular/utils/icon.list.ts @@ -17,12 +17,30 @@ export const CHEVRON_DOWN_SVG_ICON = ` `; +export const CHEVRON_LEFT_SVG_ICON = ` + + + +`; + export const CHEVRON_RIGHT_SVG_ICON = ` `; +export const CHEVRON_DOUBLE_LEFT_SVG_ICON = ` + + + +`; + +export const CHEVRON_DOUBLE_RIGHT_SVG_ICON = ` + + + +`; + export const SUN_SVG_ICON = `