@viren_chudasama/year-picker
Version:
A simple Bootstrap-based year picker for Angular
377 lines (366 loc) • 17.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, EventEmitter, HostListener, Input, Output, Component, NgModule } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
class YearPickerService {
constructor() { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [] });
class YearPickerComponent {
elementRef;
customClass = '';
minYear = 1900;
maxYear = new Date().getFullYear();
selectedYear = null;
yearSelected = new EventEmitter();
currentYearView = new Date().getFullYear();
yearRows = [];
yearsPerPage = 9;
isOpen = false;
disabled = false;
onDocumentClick(event) {
if (this.isOpen && !this.elementRef.nativeElement.contains(event.target)) {
this.isOpen = false;
}
}
// ControlValueAccessor methods
onChange = () => { };
onTouched = () => { };
constructor(elementRef) {
this.elementRef = elementRef;
}
ngOnInit() {
this.currentYearView = this.selectedYear || new Date().getFullYear();
this.clampCurrentYearView();
this.generateYearGrid();
}
togglePicker() {
if (!this.disabled) {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.currentYearView = this.selectedYear || new Date().getFullYear();
this.clampCurrentYearView(); // Ensure currentYearView is valid
this.generateYearGrid();
}
}
}
generateYearGrid() {
// Calculate the start year for the grid
const startYear = Math.floor(this.currentYearView / this.yearsPerPage) * this.yearsPerPage;
this.yearRows = [];
let years = [];
// Generate years within the minYear and maxYear boundaries
for (let i = 0; i < this.yearsPerPage; i++) {
const year = startYear + i;
// Only include years within the valid range
if (year >= this.minYear && year <= this.maxYear) {
years.push(year);
}
// Group years into rows of 3
if (years.length === 3 || i === this.yearsPerPage - 1) {
if (years.length > 0) {
this.yearRows.push([...years]);
years = [];
}
}
}
// If no years were added (e.g., startYear is way off), reset to a valid range
if (this.yearRows.length === 0) {
this.currentYearView = this.maxYear;
this.generateYearGrid(); // Recursively call to regenerate with a valid currentYearView
}
}
clampCurrentYearView() {
if (this.currentYearView < this.minYear) {
this.currentYearView = this.minYear;
}
else if (this.currentYearView > this.maxYear) {
this.currentYearView = this.maxYear;
}
}
selectYear(year) {
this.selectedYear = year;
this.onChange(year);
this.onTouched();
this.yearSelected.emit(year);
this.isOpen = false;
}
navigateToPreviousYears() {
this.currentYearView -= this.yearsPerPage;
this.clampCurrentYearView();
this.generateYearGrid();
}
navigateToNextYears() {
this.currentYearView = Number(this.currentYearView);
this.currentYearView += this.yearsPerPage;
this.clampCurrentYearView();
this.generateYearGrid();
}
isYearDisabled(year) {
return year < this.minYear || year > this.maxYear;
}
isYearSelected(year) {
return this.selectedYear === year;
}
// ControlValueAccessor interface implementation
writeValue(value) {
this.selectedYear = value;
if (value) {
this.currentYearView = value;
this.clampCurrentYearView();
this.generateYearGrid();
}
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
if (!isDisabled) {
// When re-enabled, reset currentYearView to a valid value
this.currentYearView = this.selectedYear || new Date().getFullYear();
this.clampCurrentYearView();
this.generateYearGrid();
}
}
// Keyboard navigation
onKeyDown(event) {
if (!this.isOpen) {
if (event.key === "Enter" || event.key === " ") {
this.togglePicker();
event.preventDefault();
}
return;
}
switch (event.key) {
case "Escape":
this.isOpen = false;
event.preventDefault();
break;
case "ArrowLeft":
this.handleArrowLeft();
event.preventDefault();
break;
case "ArrowRight":
this.handleArrowRight();
event.preventDefault();
break;
case "ArrowUp":
this.handleArrowUp();
event.preventDefault();
break;
case "ArrowDown":
this.handleArrowDown();
event.preventDefault();
break;
case "Enter":
case " ":
if (this.currentYearView >= this.minYear && this.currentYearView <= this.maxYear) {
this.selectYear(this.currentYearView);
}
event.preventDefault();
break;
case "Home":
this.currentYearView = this.minYear;
this.generateYearGrid();
event.preventDefault();
break;
case "End":
this.currentYearView = this.maxYear;
this.generateYearGrid();
event.preventDefault();
break;
case "PageUp":
this.navigateToPreviousYears();
event.preventDefault();
break;
case "PageDown":
this.navigateToNextYears();
event.preventDefault();
break;
}
}
handleArrowLeft() {
if (this.currentYearView > this.minYear) {
this.currentYearView--;
this.generateYearGrid();
}
}
handleArrowRight() {
if (this.currentYearView < this.maxYear) {
this.currentYearView++;
this.generateYearGrid();
}
}
handleArrowUp() {
if (this.currentYearView - 4 >= this.minYear) {
this.currentYearView -= 4;
this.generateYearGrid();
}
}
handleArrowDown() {
if (this.currentYearView + 4 <= this.maxYear) {
this.currentYearView += 4;
this.generateYearGrid();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.4", type: YearPickerComponent, isStandalone: true, selector: "lib-year-picker", inputs: { customClass: "customClass", minYear: "minYear", maxYear: "maxYear", selectedYear: "selectedYear", disabled: "disabled" }, outputs: { yearSelected: "yearSelected" }, host: { listeners: { "document:click": "onDocumentClick($event)" } }, ngImport: i0, template: `
<div class="year-picker-container position-relative" (keydown)="onKeyDown($event)">
<div class="input-group">
<input type="text" [value]="selectedYear || ''" readonly [disabled]="disabled"
class="form-control rounded-pill rounded-end-0 custom-year-picker" placeholder="Select Year"
(click)="togglePicker()" aria-haspopup="true" [attr.aria-expanded]="isOpen" tabindex="0"
/>
<span class="input-group-text rounded-pill rounded-start-0 text-secondary" [class.disabled]="disabled"
(click)="disabled? null:togglePicker()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar"
viewBox="0 0 16 16">
<path
d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z" />
</svg>
</span>
</div>
<div class="dropdown-menu w-100 p-0 shadow" [class.show]="isOpen" style="min-width: 16rem;">
<div class="d-flex align-items-center justify-content-between p-2 border-bottom">
<button type="button" class="btn btn-sm btn-link text-decoration-none" (click)="navigateToPreviousYears()"
[disabled]="minYear >= yearRows[0]?.[0]" aria-label="Previous years">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
</svg>
</button>
<span class="fw-bold">
{{ (yearRows[0]?.[0] >= minYear && yearRows[0]?.[0] <= maxYear) ? yearRows[0]?.[0] : minYear }} -
{{ (yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1] <= maxYear && yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1] >= minYear) ? yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1] : maxYear }}
</span>
<button type="button" class="btn btn-sm btn-link text-decoration-none" (click)="navigateToNextYears()"
[disabled]="maxYear <= yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1]"
aria-label="Next years">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</svg>
</button>
</div>
<div class="p-2">
<div class="row g-1" *ngFor="let row of yearRows">
<div class="col-4" *ngFor="let year of row">
<button type="button" class="btn btn-sm w-100" [class.btn-primary]="isYearSelected(year)"
[class.btn-outline-secondary]="!isYearSelected(year) && !isYearDisabled(year)"
[class.disabled]="isYearDisabled(year)" [disabled]="isYearDisabled(year)"
(click)="selectYear(year)" tabindex="0">
{{ year }}
</button>
</div>
</div>
</div>
</div>
</div>
`, isInline: true, styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerComponent, decorators: [{
type: Component,
args: [{ selector: 'lib-year-picker', standalone: true, imports: [CommonModule], template: `
<div class="year-picker-container position-relative" (keydown)="onKeyDown($event)">
<div class="input-group">
<input type="text" [value]="selectedYear || ''" readonly [disabled]="disabled"
class="form-control rounded-pill rounded-end-0 custom-year-picker" placeholder="Select Year"
(click)="togglePicker()" aria-haspopup="true" [attr.aria-expanded]="isOpen" tabindex="0"
/>
<span class="input-group-text rounded-pill rounded-start-0 text-secondary" [class.disabled]="disabled"
(click)="disabled? null:togglePicker()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-calendar"
viewBox="0 0 16 16">
<path
d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z" />
</svg>
</span>
</div>
<div class="dropdown-menu w-100 p-0 shadow" [class.show]="isOpen" style="min-width: 16rem;">
<div class="d-flex align-items-center justify-content-between p-2 border-bottom">
<button type="button" class="btn btn-sm btn-link text-decoration-none" (click)="navigateToPreviousYears()"
[disabled]="minYear >= yearRows[0]?.[0]" aria-label="Previous years">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-left" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z" />
</svg>
</button>
<span class="fw-bold">
{{ (yearRows[0]?.[0] >= minYear && yearRows[0]?.[0] <= maxYear) ? yearRows[0]?.[0] : minYear }} -
{{ (yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1] <= maxYear && yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1] >= minYear) ? yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1] : maxYear }}
</span>
<button type="button" class="btn btn-sm btn-link text-decoration-none" (click)="navigateToNextYears()"
[disabled]="maxYear <= yearRows[yearRows.length - 1]?.[yearRows[yearRows.length - 1].length - 1]"
aria-label="Next years">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-chevron-right" viewBox="0 0 16 16">
<path fill-rule="evenodd"
d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" />
</svg>
</button>
</div>
<div class="p-2">
<div class="row g-1" *ngFor="let row of yearRows">
<div class="col-4" *ngFor="let year of row">
<button type="button" class="btn btn-sm w-100" [class.btn-primary]="isYearSelected(year)"
[class.btn-outline-secondary]="!isYearSelected(year) && !isYearDisabled(year)"
[class.disabled]="isYearDisabled(year)" [disabled]="isYearDisabled(year)"
(click)="selectYear(year)" tabindex="0">
{{ year }}
</button>
</div>
</div>
</div>
</div>
</div>
` }]
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { customClass: [{
type: Input
}], minYear: [{
type: Input
}], maxYear: [{
type: Input
}], selectedYear: [{
type: Input
}], yearSelected: [{
type: Output
}], disabled: [{
type: Input
}], onDocumentClick: [{
type: HostListener,
args: ['document:click', ['$event']]
}] } });
class YearPickerModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.4", ngImport: i0, type: YearPickerModule, imports: [CommonModule, YearPickerComponent], exports: [YearPickerComponent] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerModule, imports: [CommonModule, YearPickerComponent] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.4", ngImport: i0, type: YearPickerModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, YearPickerComponent],
exports: [YearPickerComponent]
}]
}] });
/*
* Public API Surface of year-picker
*/
/**
* Generated bundle index. Do not edit.
*/
export { YearPickerComponent, YearPickerModule, YearPickerService };
//# sourceMappingURL=year-picker.mjs.map