UNPKG

lucy-calendar

Version:

LucyCalendar is a powerful and flexible date picker library for Angular applications, specifically designed for Ethiopian dates. It provides a user-friendly interface for selecting dates and supports various customization options to fit your needs.

680 lines (671 loc) 45.5 kB
import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i0 from '@angular/core'; import { EventEmitter, Component, Input, Output, ViewChild, HostListener, Directive } from '@angular/core'; import { FormsModule } from '@angular/forms'; var EpochOffset; (function (EpochOffset) { EpochOffset[EpochOffset["AmeteAlem"] = -285019] = "AmeteAlem"; EpochOffset[EpochOffset["AmeteMihret"] = 1723856] = "AmeteMihret"; EpochOffset[EpochOffset["Coptic"] = 1824665] = "Coptic"; EpochOffset[EpochOffset["Gregorian"] = 1721426] = "Gregorian"; EpochOffset[EpochOffset["Unset"] = -1] = "Unset"; })(EpochOffset || (EpochOffset = {})); const nMonths = 12; const monthDays = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const JD_EPOCH_OFFSET_AMETE_ALEM = -285019; // ዓ/ዓ const JD_EPOCH_OFFSET_AMETE_MIHRET = 1723856; // ዓ/ም const JD_EPOCH_OFFSET_COPTIC = 1824665; const JD_EPOCH_OFFSET_GREGORIAN = 1721426; const monthNames = [ "መስከረም", "ጥቅምት", "ህዳር", "ታህሳስ", "ጥር", "የካቲት", "መጋቢት", "ሚይዚያ", "ግንቦት", "ሰኔ", "ሐምሌ", "ነሐሴ", "ጳጉሜ" ]; const dayNames = ["እሁድ", "ሰኞ", "ማክሰኞ", "ረቡዕ", "ሐሙስ", "ዓርብ", "ቅዳሜ"]; function toGregorian(param) { const { year, month, day } = param; const era = year <= 0 ? JD_EPOCH_OFFSET_AMETE_ALEM : JD_EPOCH_OFFSET_AMETE_MIHRET; let jdn = EthiopicToJdn({ year: param.year, month: param.month, day: param.day, era }); const date = JdnToGregorian(jdn); return new Date(Date.UTC(date[0], date[1] - 1, date[2])); } function toEthiopian(gcDate) { const jdn = GregorianToJdn(gcDate.getFullYear(), gcDate.getMonth() + 1, gcDate.getDate()); return JdnToEthiopic(jdn, GuessEraFromJDN(jdn)); } function Quotient(i, j) { return Math.floor(i / j); } function Mod(i, j) { return i - j * Quotient(i, j); } function GuessEraFromJDN(jdn) { return (jdn >= (JD_EPOCH_OFFSET_AMETE_MIHRET + 365)) ? JD_EPOCH_OFFSET_AMETE_MIHRET : JD_EPOCH_OFFSET_AMETE_ALEM; } function isEthiopianLeapYear(year) { return year % 4 === 3; // Ethiopian leap years are multiples of 4 with a remainder of 3 } function IsGregorianLeap(year) { return (year % 4 === 0) && ((year % 100 !== 0) || (year % 400 === 0)); } function GregorianToJdn(year, month, day) { const s = Quotient(year, 4) - Quotient(year - 1, 4) - Quotient(year, 100) + Quotient(year - 1, 100) + Quotient(year, 400) - Quotient(year - 1, 400); const t = Quotient(14 - month, 12); const n = 31 * t * (month - 1) + (1 - t) * (59 + s + 30 * (month - 3) + Quotient((3 * month - 7), 5)) + day - 1; const j = JD_EPOCH_OFFSET_GREGORIAN + 365 * (year - 1) + Quotient(year - 1, 4) - Quotient(year - 1, 100) + Quotient(year - 1, 400) + n; return j; } function JdnToEthiopic(jdn, era) { era = era ?? GuessEraFromJDN(jdn); const r = Mod((jdn - era), 1461); const n = Mod(r, 365) + 365 * Quotient(r, 1460); const year = 4 * Quotient((jdn - era), 1461) + Quotient(r, 365) - Quotient(r, 1460); ; const month = Quotient(n, 30) + 1; const day = Mod(n, 30) + 1; return { year, month: Number(month), day: Number(day) }; } function EthCopticToJdn(year, month, day, era) { return (era + 365) + 365 * (year - 1) + Quotient(year, 4) + 30 * month + day - 31; } function JdnToGregorian(j) { const r2000 = Mod((j - JD_EPOCH_OFFSET_GREGORIAN), 730485); const r400 = Mod((j - JD_EPOCH_OFFSET_GREGORIAN), 146097); const r100 = Mod(r400, 36524); const r4 = Mod(r100, 1461); let n = Mod(r4, 365) + 365 * Quotient(r4, 1460); const s = Quotient(r4, 1095); const aprime = 400 * Quotient((j - JD_EPOCH_OFFSET_GREGORIAN), 146097) + 100 * Quotient(r400, 36524) + 4 * Quotient(r100, 1461) + Quotient(r4, 365) - Quotient(r4, 1460) - Quotient(r2000, 730484); ; const year = aprime + 1; const t = Quotient((364 + s - n), 306); const month = t * (Quotient(n, 31) + 1) + (1 - t) * (Quotient((5 * (n - s) + 13), 153) + 1); n += 1 - Quotient(r2000, 730484); let day = n; if ((r100 == 0) && (n == 0) && (r400 != 0)) { let month = 12; day = 31; } else { monthDays[2] = (IsGregorianLeap(year)) ? 29 : 28; for (let i = 1; i <= nMonths; ++i) { if (n <= monthDays[i]) { day = n; break; } n -= monthDays[i]; } } return [year, month, day]; } function EthiopicToJdn(param) { return EthCopticToJdn(param.year, param.month, param.day, param.era); } function CopticToGregorian(year, month, day) { let jdn = EthiopicToJdn({ year, month, day, era: JD_EPOCH_OFFSET_COPTIC }); return JdnToGregorian(jdn); } function GregorianToCoptic(year, month, day) { let jdn = GregorianToJdn(year, month, day); return JdnToEthiopic(jdn, JD_EPOCH_OFFSET_COPTIC); } function CopticToJdn(year, month, day) { return EthCopticToJdn(year, month, day, JD_EPOCH_OFFSET_COPTIC); } class DropdownComponent { options = []; // List of dropdown options selected; // Selected value selectedChange = new EventEmitter(); // Event when selection changes /** * Optional function to convert an option to a displayable string. */ displayFn; dropdownOpen = false; dropdownContainer; constructor() { } ngOnInit() { if (!this.selected && this.options.length > 0) { this.selected = this.options[0]; // Default to first option if no selection } // Scroll to the selected option if it exists setTimeout(() => { const selectedIndex = this.options.indexOf(this.selected); if (selectedIndex >= 0) { const dropdownElement = this.dropdownContainer.nativeElement.querySelector('ul'); const selectedElement = dropdownElement?.children[selectedIndex]; if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest' }); } } }); } toggleDropdown(event) { console.log('Dropdown clicked. Selected item:', this.selected); this.dropdownOpen = !this.dropdownOpen; if (this.dropdownOpen && this.selected) { setTimeout(() => { const selectedIndex = this.options.indexOf(this.selected); if (selectedIndex >= 0) { const dropdownElement = this.dropdownContainer.nativeElement.querySelector('ul'); const selectedElement = dropdownElement.children[selectedIndex]; if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest' }); } } }); } } onOptionClick(option, index) { this.selected = option; this.selectedChange.emit(option); this.dropdownOpen = false; } // Close dropdown when clicking outside onClickOutside(event) { if (this.dropdownContainer && !this.dropdownContainer.nativeElement.contains(event.target)) { this.dropdownOpen = false; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DropdownComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: DropdownComponent, isStandalone: true, selector: "lucy-dropdown", inputs: { options: "options", selected: "selected", displayFn: "displayFn" }, outputs: { selectedChange: "selectedChange" }, host: { listeners: { "document:click": "onClickOutside($event)" } }, viewQueries: [{ propertyName: "dropdownContainer", first: true, predicate: ["dropdownContainer"], descendants: true }], ngImport: i0, template: "<div class=\"relative inline-block w-full\" #dropdownContainer>\r\n <!-- Dropdown Button -->\r\n <button (click)=\"toggleDropdown($event)\"\r\n class=\"w-full px-3 py-2 text-center text-gray-700 bg-white border border-gray-300 rounded-lg shadow-sm focus:outline-none\">\r\n {{ displayFn ? displayFn(selected) : selected }}\r\n <svg class=\"w-4 h-4 inline-block ml-2\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\" />\r\n </svg>\r\n </button>\r\n\r\n <!-- Dropdown List -->\r\n <div *ngIf=\"dropdownOpen\"\r\n class=\"absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto custom-scrollbar\">\r\n <ul>\r\n <li *ngFor=\"let option of options; let i = index\" (click)=\"onOptionClick(option, i)\"\r\n class=\"px-4 py-2 cursor-pointer hover:bg-gray-100\" [ngClass]=\"{'bg-blue-100': option === selected}\">\r\n {{ displayFn ? displayFn(option) : option }}\r\n </li>\r\n </ul>\r\n </div>\r\n</div>", styles: [".custom-scrollbar::-webkit-scrollbar{width:0px}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#0003;border-radius:4px;border:2px solid transparent;background-clip:content-box}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background-color:#0006}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: DropdownComponent, decorators: [{ type: Component, args: [{ selector: 'lucy-dropdown', standalone: true, imports: [CommonModule], template: "<div class=\"relative inline-block w-full\" #dropdownContainer>\r\n <!-- Dropdown Button -->\r\n <button (click)=\"toggleDropdown($event)\"\r\n class=\"w-full px-3 py-2 text-center text-gray-700 bg-white border border-gray-300 rounded-lg shadow-sm focus:outline-none\">\r\n {{ displayFn ? displayFn(selected) : selected }}\r\n <svg class=\"w-4 h-4 inline-block ml-2\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\" />\r\n </svg>\r\n </button>\r\n\r\n <!-- Dropdown List -->\r\n <div *ngIf=\"dropdownOpen\"\r\n class=\"absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-lg shadow-lg max-h-60 overflow-y-auto custom-scrollbar\">\r\n <ul>\r\n <li *ngFor=\"let option of options; let i = index\" (click)=\"onOptionClick(option, i)\"\r\n class=\"px-4 py-2 cursor-pointer hover:bg-gray-100\" [ngClass]=\"{'bg-blue-100': option === selected}\">\r\n {{ displayFn ? displayFn(option) : option }}\r\n </li>\r\n </ul>\r\n </div>\r\n</div>", styles: [".custom-scrollbar::-webkit-scrollbar{width:0px}.custom-scrollbar::-webkit-scrollbar-thumb{background-color:#0003;border-radius:4px;border:2px solid transparent;background-clip:content-box}.custom-scrollbar::-webkit-scrollbar-thumb:hover{background-color:#0006}.custom-scrollbar::-webkit-scrollbar-track{background:transparent}\n"] }] }], ctorParameters: () => [], propDecorators: { options: [{ type: Input }], selected: [{ type: Input }], selectedChange: [{ type: Output }], displayFn: [{ type: Input }], dropdownContainer: [{ type: ViewChild, args: ['dropdownContainer'] }], onClickOutside: [{ type: HostListener, args: ['document:click', ['$event']] }] } }); class LucyCalendarComponent { ngOnChanges(changes) { // if (changes['value'] && !changes['value'].firstChange) { // this.value = changes['value'].currentValue; // this.parseDate(); // // this.valueChange.emit(this.value); // this.dateValue = toGregorian({ year: this.selectedYear, month: this.selectedMonth, day: this.selectedDay }); // this.emitChange(); // } // else if (changes['dateValue'] && !changes['dateValue'].firstChange && changes['dateValue'].currentValue?.getTime() !== this.dateValue?.getTime()) { this.dateValue = changes['dateValue'].currentValue; if (this.dateValue === null) return; const et = toEthiopian(this.dateValue); this.selectedYear = et.year; this.selectedMonth = et.month; this.selectedDay = et.day; // this.dateValueChange.emit(this.dateValue); // this.value = this.formatDate(); this.emitChange(); } } ngOnInit() { if (this.dateValue) { this.selectedYear = this.dateValue.getFullYear(); this.selectedMonth = this.dateValue.getMonth(); this.selectedDay = this.dateValue.getDate(); } // if (this.value) { // this.parseDate(); // } // if (this.selectedDay !== 0) { // this.selectDate(this.selectedDay); // } else { // this.value = ''; // } this.filteredMonths = this.availableMonths.filter(m => !this.isMonthOptionDisabled(m)); this.filteredYears = this.availableYears.filter(y => !this.isYearOptionDisabled(y)); } label = 'Select Date'; // @Input() value: string | null = null; // New input for value valueChange = new EventEmitter(); // Output event emitter for value dateValue = null; dateValueChange = new EventEmitter(); // Output event emitter for grValue placeholder = null; min = null; max = null; dateFormat = 'DD/MM/YYYY'; // New input for date format // @Input() disabled: boolean = false; // New input for disabled state // @Input() readonly: boolean = true; // New input for readonly state calendarVisible = false; currentDate = new Date(); selectedYear = toEthiopian(this.currentDate).year; // Start with 2015 (Ethiopian year 2008) selectedMonth = 1; // Start with Meskerem (January in Ethiopian calendar) selectedDay = 0; monthNames = [ "መስከረም", "ጥቅምት", "ህዳር", "ታህሳስ", "ጥር", "የካቲት", "መጋቢት", "ሚይዚያ", "ግንቦት", "ሰኔ", "ሐምሌ", "ነሐሴ", "ጳጉሜ" ]; dayNames = ["እሁድ", "ሰኞ", "ማክሰኞ", "ረቡዕ", "ሐሙስ", "ዓርብ", "ቅዳሜ"]; availableYears = Array.from({ length: 101 }, (_, i) => this.currentDate.getFullYear() - 50 + i); filteredYears = this.availableYears.filter(y => !this.isYearOptionDisabled(y)); availableMonths = Array.from({ length: 13 }, (_, i) => i + 1); filteredMonths = this.availableMonths.filter(month => !this.isMonthOptionDisabled(month)); refreshMonthOptions() { this.filteredMonths = this.availableMonths.filter(month => !this.isMonthOptionDisabled(month)); } ; refreshYearOptions() { this.filteredYears = this.availableYears.filter(y => !this.isYearOptionDisabled(y)); } emitChange() { this.dateValueChange.emit(this.dateValue); // Emit the new date value // this.valueChange.emit(this.value); } toggleCalendar() { this.calendarVisible = !this.calendarVisible; if (this.dateValue) { const et = toEthiopian(this.dateValue); this.selectedYear = et.year; this.selectedMonth = et.month; this.selectedDay = et.day; } // if (this.value) { // this.parseDate(); // } } monthDisplay = (month) => this.monthNames[month - 1]; /* Month numbers are 1-indexed so adjust for array (0-indexed)*/ selectMonthYear(month, year) { this.selectedMonth = month; this.selectedYear = year; this.dateValue = toGregorian({ year, month, day: 1 }); // this.value = this.formatDate(); this.emitChange(); } onMonthChanges(month) { this.selectMonthYear(month, this.selectedYear); this.refreshYearOptions(); } onYearChanges(year) { this.selectMonthYear(this.selectedMonth, year); this.refreshMonthOptions(); } prevMonth() { this.selectedMonth = (this.selectedMonth - 1 + 13) % 13 || 13; this.dateValue = toGregorian({ year: this.selectedYear, month: this.selectedMonth, day: 1 }); // this.value = this.formatDate(); this.emitChange(); } nextMonth() { this.selectedMonth = (this.selectedMonth + 1) % 13 || 13; this.dateValue = toGregorian({ year: this.selectedYear, month: this.selectedMonth, day: 1 }); // this.value = this.formatDate(); this.emitChange(); } getLeadingEmptyDays() { const firstDay = toGregorian({ year: this.selectedYear, month: this.selectedMonth, day: 1 }); return Array(firstDay.getDay()).fill(null); } get daysInMonth() { const daysInEthiopianMonth = this.selectedMonth === 13 ? (isEthiopianLeapYear(this.selectedYear) ? 6 : 5) : 30; // Pagumē has 6 days in a leap year return Array.from({ length: daysInEthiopianMonth }, (_, i) => i + 1); } selectDate(day) { this.selectedDay = day; this.dateValue = toGregorian({ year: this.selectedYear, month: this.selectedMonth, day: day }); // this.value = this.formatDate(); this.calendarVisible = false; this.emitChange(); } clearDate() { // if (this.disabled) return; // Prevent clearing date if disabled this.dateValue = null; // this.value = null; this.selectedDay = 0; this.calendarVisible = false; this.emitChange(); } selectToday() { const today = toEthiopian(new Date()); this.selectedYear = today.year; this.selectedMonth = today.month; this.selectedDay = today.day; this.selectDate(today.day); } isDayDisabled(day) { if (this.max === null) return false; const date = toGregorian({ year: this.selectedYear, month: this.selectedMonth, day: day }); return date > this.max; } isNextMonthDisabled() { if (this.max === null) return false; const nextMonthDate = toGregorian({ year: this.selectedYear, month: this.selectedMonth + 1, day: 1 }); return nextMonthDate > this.max; } isPrevMonthDisabled() { if (this.min === null) return false; const prevMonthDate = toGregorian({ year: this.selectedYear, month: this.selectedMonth - 1, day: 1 }); return prevMonthDate < this.min; } isMonthOptionDisabled(monthIndex) { const monthDate = toGregorian({ year: this.selectedYear, month: monthIndex, day: 1 }); return (this.max !== null && (monthDate > this.max || (this.selectedYear === this.max.getFullYear() && monthIndex > this.max.getMonth()))) || (this.min !== null && (monthDate < this.min || (this.selectedYear === this.min.getFullYear() && monthIndex < this.min.getMonth()))); } isYearOptionDisabled(year) { const yearDate = toGregorian({ year: year, month: 1, day: 1 }); return (this.max !== null && yearDate > this.max) || (this.min !== null && yearDate < this.min); } formatDate() { const formattedMonth = this.padZero(this.selectedMonth); const formattedDay = this.padZero(this.selectedDay); return this.dateFormat .replace(/YYYY/i, this.selectedYear.toString()) .replace(/MM/i, formattedMonth) .replace(/dd/i, formattedDay); } // parseDate() { // if (!this.value || !this.dateFormat) return; // // Detect separator from dateFormat (supports /, -, ., and others) // const separator = this.dateFormat.match(/[^a-zA-Z0-9]/)?.[0] || '/'; // const dateParts = this.value.split(separator).map(Number); // const formatParts = this.dateFormat.split(separator); // if (dateParts.length !== formatParts.length) { // console.error('Date format mismatch'); // return; // } // formatParts.forEach((part, index) => { // const partType = part.toUpperCase()[0]; // switch (partType) { // case 'Y': // this.selectedYear = dateParts[index]; // break; // case 'M': // this.selectedMonth = dateParts[index]; // break; // case 'D': // this.selectedDay = dateParts[index]; // break; // } // }); // } padZero(num) { return num.toString().padStart(2, '0'); } onClickOutside(event) { const target = event.target; if (!target.closest('.relative') && !target.closest('lucy-calendar') && !target.classList.contains('calendar-icon') && !target.classList.contains('lucy-host')) { this.calendarVisible = false; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LucyCalendarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: LucyCalendarComponent, isStandalone: true, selector: "lucy-calendar", inputs: { label: "label", dateValue: "dateValue", placeholder: "placeholder", min: "min", max: "max", dateFormat: "dateFormat", calendarVisible: "calendarVisible" }, outputs: { valueChange: "valueChange", dateValueChange: "dateValueChange" }, host: { listeners: { "document:click": "onClickOutside($event)" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"relative\">\r\n <!-- <lucy-date [label]=\"label\" [placeholder]=\"placeholder\" [dateFormat]=\"dateFormat\" [selectedDay]=\"selectedDay\"\r\n [readonly]=\"readonly\" [disabled]=\"disabled\" [dateFormat]=\"dateFormat\"></lucy-date> -->\r\n <!-- Calendar Dropdown -->\r\n <div id=\"calendar\" class=\"absolute mt-1 w-auto bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-10\"\r\n [ngClass]=\"{'hidden': !calendarVisible}\">\r\n <div class=\"flex justify-between items-center mb-2\">\r\n <button (click)=\"prevMonth()\"\r\n class=\"text-gray-500 hover:text-gray-700 disabled:text-gray-300 disabled:cursor-not-allowed\"\r\n [disabled]=\"isPrevMonthDisabled()\">\r\n <svg class=\"h-6 w-6 text-gray-500 hover:text-gray-700 disabled:text-gray-300\" fill=\"none\" stroke=\"currentColor\"\r\n viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\r\n </svg>\r\n </button>\r\n <!-- (click)=\"toggleMonthYearSelection()\" -->\r\n <button class=\"font-medium text-gray-700\">\r\n {{ monthNames[selectedMonth - 1] }} {{ selectedYear }}\r\n </button>\r\n <button (click)=\"nextMonth()\"\r\n class=\"text-gray-500 hover:text-gray-700 disabled:text-gray-300 disabled:cursor-not-allowed\"\r\n [disabled]=\"isNextMonthDisabled()\">\r\n <svg class=\"h-6 w-6 text-gray-500 hover:text-gray-700 disabled:text-gray-300\" fill=\"none\" stroke=\"currentColor\"\r\n viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <div class=\"grid grid-cols-2 gap-2\">\r\n <lucy-dropdown [options]=\"filteredMonths\" [selected]=\"selectedMonth\" (selectedChange)=\"onMonthChanges($event)\"\r\n [displayFn]=\"monthDisplay\">\r\n </lucy-dropdown>\r\n <lucy-dropdown [options]=\"filteredYears\" [selected]=\"selectedYear\"\r\n (selectedChange)=\"onYearChanges($event)\"></lucy-dropdown>\r\n </div>\r\n\r\n <!-- Days of the Month -->\r\n <div class=\"grid grid-cols-7 gap-1 text-center text-sm\">\r\n <div *ngFor=\"let dayName of dayNames\" class=\"text-gray-500\">{{ dayName }}</div>\r\n </div>\r\n\r\n <div class=\"grid grid-cols-7 gap-1 text-center text-sm pt-2\">\r\n <div *ngFor=\"let empty of getLeadingEmptyDays()\"></div>\r\n <button *ngFor=\"let day of daysInMonth\" class=\"py-1 rounded hover:bg-indigo-100 focus:bg-indigo-200\"\r\n [ngClass]=\"{'bg-blue-200': day === selectedDay, 'bg-gray-200 text-gray-400 cursor-not-allowed': isDayDisabled(day)}\"\r\n [disabled]=\"isDayDisabled(day)\" (click)=\"selectDate(day)\">\r\n {{ day }}\r\n </button>\r\n </div>\r\n\r\n <div class=\"flex justify-between mt-2\">\r\n <button (click)=\"clearDate()\"\r\n class=\"px-2 py-1 border border-red-500 text-red-500 rounded text-xs hover:bg-red-100\">Clear</button>\r\n <button (click)=\"selectToday()\"\r\n class=\"px-2 py-1 border border-blue-500 text-blue-500 rounded text-xs hover:bg-blue-100\">Today</button>\r\n </div>\r\n </div>\r\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "ngmodule", type: FormsModule }, { kind: "component", type: DropdownComponent, selector: "lucy-dropdown", inputs: ["options", "selected", "displayFn"], outputs: ["selectedChange"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LucyCalendarComponent, decorators: [{ type: Component, args: [{ selector: 'lucy-calendar', standalone: true, imports: [CommonModule, FormsModule, DropdownComponent], template: "<div class=\"relative\">\r\n <!-- <lucy-date [label]=\"label\" [placeholder]=\"placeholder\" [dateFormat]=\"dateFormat\" [selectedDay]=\"selectedDay\"\r\n [readonly]=\"readonly\" [disabled]=\"disabled\" [dateFormat]=\"dateFormat\"></lucy-date> -->\r\n <!-- Calendar Dropdown -->\r\n <div id=\"calendar\" class=\"absolute mt-1 w-auto bg-white border border-gray-300 rounded-lg shadow-lg p-4 z-10\"\r\n [ngClass]=\"{'hidden': !calendarVisible}\">\r\n <div class=\"flex justify-between items-center mb-2\">\r\n <button (click)=\"prevMonth()\"\r\n class=\"text-gray-500 hover:text-gray-700 disabled:text-gray-300 disabled:cursor-not-allowed\"\r\n [disabled]=\"isPrevMonthDisabled()\">\r\n <svg class=\"h-6 w-6 text-gray-500 hover:text-gray-700 disabled:text-gray-300\" fill=\"none\" stroke=\"currentColor\"\r\n viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M15 19l-7-7 7-7\"></path>\r\n </svg>\r\n </button>\r\n <!-- (click)=\"toggleMonthYearSelection()\" -->\r\n <button class=\"font-medium text-gray-700\">\r\n {{ monthNames[selectedMonth - 1] }} {{ selectedYear }}\r\n </button>\r\n <button (click)=\"nextMonth()\"\r\n class=\"text-gray-500 hover:text-gray-700 disabled:text-gray-300 disabled:cursor-not-allowed\"\r\n [disabled]=\"isNextMonthDisabled()\">\r\n <svg class=\"h-6 w-6 text-gray-500 hover:text-gray-700 disabled:text-gray-300\" fill=\"none\" stroke=\"currentColor\"\r\n viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\r\n </svg>\r\n </button>\r\n </div>\r\n\r\n <div class=\"grid grid-cols-2 gap-2\">\r\n <lucy-dropdown [options]=\"filteredMonths\" [selected]=\"selectedMonth\" (selectedChange)=\"onMonthChanges($event)\"\r\n [displayFn]=\"monthDisplay\">\r\n </lucy-dropdown>\r\n <lucy-dropdown [options]=\"filteredYears\" [selected]=\"selectedYear\"\r\n (selectedChange)=\"onYearChanges($event)\"></lucy-dropdown>\r\n </div>\r\n\r\n <!-- Days of the Month -->\r\n <div class=\"grid grid-cols-7 gap-1 text-center text-sm\">\r\n <div *ngFor=\"let dayName of dayNames\" class=\"text-gray-500\">{{ dayName }}</div>\r\n </div>\r\n\r\n <div class=\"grid grid-cols-7 gap-1 text-center text-sm pt-2\">\r\n <div *ngFor=\"let empty of getLeadingEmptyDays()\"></div>\r\n <button *ngFor=\"let day of daysInMonth\" class=\"py-1 rounded hover:bg-indigo-100 focus:bg-indigo-200\"\r\n [ngClass]=\"{'bg-blue-200': day === selectedDay, 'bg-gray-200 text-gray-400 cursor-not-allowed': isDayDisabled(day)}\"\r\n [disabled]=\"isDayDisabled(day)\" (click)=\"selectDate(day)\">\r\n {{ day }}\r\n </button>\r\n </div>\r\n\r\n <div class=\"flex justify-between mt-2\">\r\n <button (click)=\"clearDate()\"\r\n class=\"px-2 py-1 border border-red-500 text-red-500 rounded text-xs hover:bg-red-100\">Clear</button>\r\n <button (click)=\"selectToday()\"\r\n class=\"px-2 py-1 border border-blue-500 text-blue-500 rounded text-xs hover:bg-blue-100\">Today</button>\r\n </div>\r\n </div>\r\n</div>" }] }], propDecorators: { label: [{ type: Input }], valueChange: [{ type: Output }], dateValue: [{ type: Input }], dateValueChange: [{ type: Output }], placeholder: [{ type: Input }], min: [{ type: Input }], max: [{ type: Input }], dateFormat: [{ type: Input }], calendarVisible: [{ type: Input }], onClickOutside: [{ type: HostListener, args: ['document:click', ['$event']] }] } }); class LucyCalendarDirective { el; viewContainerRef; renderer; label = 'Select Date'; value = null; valueChange = new EventEmitter(); dateValue = null; dateValueChange = new EventEmitter(); placeholder = null; min = null; max = null; dateFormat = 'dd/mm/yyyy'; // @Input() disabled: boolean = false; // @Input() readonly: boolean = true; componentRef; isCalendarOpen = false; // Track open/closed state calendarElement = null; constructor(el, viewContainerRef, renderer) { this.el = el; this.viewContainerRef = viewContainerRef; this.renderer = renderer; this.componentRef = this.viewContainerRef.createComponent(LucyCalendarComponent); this.calendarElement = this.componentRef.location.nativeElement; } // Open/close on input click onClick() { this.componentRef.instance.toggleCalendar(); } // Close when clicking outside the input or calendar onDocumentClick(event) { const target = event.target; const clickedInsideInput = this.el.nativeElement.contains(target); const clickedInsideCalendar = this.calendarElement?.contains(target); if (!clickedInsideInput && !clickedInsideCalendar && this.isCalendarOpen) { this.closeCalendar(); } } ngOnInit() { // Create the button element const button = this.renderer.createElement('button'); // this.renderer.setAttribute(button, 'type', 'button'); if (this.el.nativeElement.disabled) { this.renderer.setAttribute(button, 'disabled', 'true'); } if (this.el.nativeElement.readonly) { this.renderer.setAttribute(button, 'readonly', 'true'); } this.renderer.setAttribute(button, 'class', 'absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300'); // Create the SVG element const svg = this.renderer.createElement('svg', 'svg'); this.renderer.setAttribute(svg, 'xmlns', 'http://www.w3.org/2000/svg'); this.renderer.setAttribute(svg, 'fill', 'none'); this.renderer.setAttribute(svg, 'viewBox', '0 0 24 24'); this.renderer.setAttribute(svg, 'stroke-width', '1.5'); this.renderer.setAttribute(svg, 'stroke', 'currentColor'); this.renderer.setAttribute(svg, 'class', 'h-6 w-6 text-gray-500 hover:text-gray-700 calendar-icon'); // set the SVG attributes const path = this.renderer.createElement('path', 'svg'); this.renderer.setAttribute(path, 'class', 'calendar-icon'); this.renderer.setAttribute(path, 'stroke-linecap', 'round'); this.renderer.setAttribute(path, 'stroke-linejoin', 'round'); this.renderer.setAttribute(path, 'd', 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z'); // Append the path to the SVG this.renderer.appendChild(svg, path); // Append the SVG to the button this.renderer.appendChild(button, svg); // append class to the host element this.renderer.addClass(this.el.nativeElement, 'block'); this.renderer.addClass(this.el.nativeElement, 'lucy-host'); const parentDiv = this.renderer.createElement('div'); this.renderer.addClass(parentDiv, 'relative'); const computedWidth = window.getComputedStyle(this.el.nativeElement).width; this.renderer.setStyle(parentDiv, 'width', computedWidth); const parent = this.renderer.parentNode(this.el.nativeElement); this.renderer.insertBefore(parent, parentDiv, this.el.nativeElement); this.renderer.appendChild(parentDiv, this.el.nativeElement); this.renderer.appendChild(parentDiv, button); this.renderer.appendChild(this.el.nativeElement.parentElement, this.calendarElement); this.renderer.listen(button, 'click', () => { this.componentRef.instance.toggleCalendar(); this.renderer.appendChild(this.el.nativeElement.parentElement, this.calendarElement); }); this.openCalendar(); } ngOnChanges(changes) { if (changes['dateValue'] && !changes['dateValue'].firstChange) { this.componentRef.instance.ngOnChanges(changes); } } ngOnDestroy() { this.closeCalendar(); } openCalendar() { // Pass inputs to the calendar component // this.componentRef.instance.label = this.label; // this.componentRef.instance.value = this.value; this.componentRef.instance.dateValue = this.dateValue; this.componentRef.instance.placeholder = this.placeholder; this.componentRef.instance.min = this.min; this.componentRef.instance.max = this.max; this.componentRef.instance.dateFormat = this.dateFormat; // Handle outputs // this.componentRef.instance.valueChange.subscribe((value: string | null) => { // this.renderer.setProperty(this.el.nativeElement, 'value', value ?? this.placeholder ?? this.dateFormat); // if (value === this.value) return // this.valueChange.emit(value); // }); this.componentRef.instance.dateValueChange.subscribe((date) => { // setTimeout(() => { // this.value = this.componentRef.instance.formatDate(); // }); const et = date !== null ? this.componentRef.instance.formatDate() : null; this.renderer.setProperty(this.el.nativeElement, 'value', et ?? this.placeholder ?? this.dateFormat); // this.valueChange.emit(this.value); this.dateValueChange.emit(date); this.valueChange.emit(et); }); this.componentRef.instance.emitChange(); // Append to the input's parent (not document.body) this.renderer.appendChild(this.el.nativeElement.parentElement, this.calendarElement); // } } closeCalendar() { this.componentRef.destroy(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LucyCalendarDirective, deps: [{ token: i0.ElementRef }, { token: i0.ViewContainerRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.13", type: LucyCalendarDirective, isStandalone: true, selector: "[lucyCalendar]", inputs: { label: "label", value: "value", dateValue: "dateValue", placeholder: "placeholder", min: "min", max: "max", dateFormat: "dateFormat" }, outputs: { valueChange: "valueChange", dateValueChange: "dateValueChange" }, host: { listeners: { "click": "onClick()", "document:click": "onDocumentClick($event)" } }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LucyCalendarDirective, decorators: [{ type: Directive, args: [{ selector: '[lucyCalendar]', standalone: true, }] }], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ViewContainerRef }, { type: i0.Renderer2 }], propDecorators: { label: [{ type: Input }], value: [{ type: Input }], valueChange: [{ type: Output }], dateValue: [{ type: Input }], dateValueChange: [{ type: Output }], placeholder: [{ type: Input }], min: [{ type: Input }], max: [{ type: Input }], dateFormat: [{ type: Input }], onClick: [{ type: HostListener, args: ['click'] }], onDocumentClick: [{ type: HostListener, args: ['document:click', ['$event']] }] } }); class LucyDateComponent { label = ''; placeholder = null; dateFormat = 'DD/MM/YYYY'; selectedDay = 0; selectedYear = 0; selectedMonth = 0; readonly = true; disabled = false; value = null; valueChange = new EventEmitter(); dateValue = new Date(); dateValueChange = new EventEmitter(); showCalendar = false; toggleCalendar() { this.showCalendar = !this.showCalendar; } dateChange(date) { if (date === null) return; const et = toEthiopian(date); this.valueChange.emit(this.formatDate(et.year, et.month, et.day)); this.dateValueChange.emit(date); } formatDate(year, month, day) { const formattedMonth = this.padZero(month); const formattedDay = this.padZero(day); return this.dateFormat .replace(/YYYY/i, year.toString()) .replace(/MM/i, formattedMonth) .replace(/dd/i, formattedDay); } padZero(num) { return num.toString().padStart(2, '0'); } ngOnChanges(changes) { if (changes['dateValue']) { this.dateValue = changes['dateValue'].currentValue; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LucyDateComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.13", type: LucyDateComponent, isStandalone: true, selector: "lucy-date", inputs: { label: "label", placeholder: "placeholder", dateFormat: "dateFormat", selectedDay: "selectedDay", selectedYear: "selectedYear", selectedMonth: "selectedMonth", readonly: "readonly", disabled: "disabled", value: "value", dateValue: "dateValue" }, outputs: { valueChange: "valueChange", dateValueChange: "dateValueChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"relative\">\r\n <label for=\"date-picker\" class=\"block text-sm font-medium text-gray-700 mb-1\">\r\n {{ label }}\r\n </label>\r\n\r\n <div class=\"relative\">\r\n <input type=\"text\" id=\"date-picker\" [attr.readonly]=\"readonly ? true : null\"\r\n class=\"block w-full px-4 py-2 text-gray-700 bg-white border disabled:cursor-not-allowed disabled:text-red-700 border-gray-300 rounded-lg shadow-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-800\"\r\n [placeholder]=\"placeholder ?? dateFormat\" [(value)]=\"value\" [attr.disabled]=\"disabled ? true : null\"\r\n (click)=\"readonly ? toggleCalendar() : null\" />\r\n <button\r\n class=\"absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300\"\r\n [disabled]=\"disabled\" (click)=\"toggleCalendar()\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"h-6 w-6 text-gray-500 hover:text-gray-700 disabled:hover:text-gray-500\" fill=\"none\"\r\n viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n d=\"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\" />\r\n </svg>\r\n </button>\r\n </div>\r\n <lucy-calendar [calendarVisible]=\"showCalendar\" [label]=\"label\" [placeholder]=\"placeholder\" [dateValue]=\"dateValue\"\r\n (dateValueChange)=\"dateChange($event)\" (valueChange)=\"valueChange.emit($event)\" [dateFormat]=\"dateFormat\"\r\n [dateFormat]=\"dateFormat\"></lucy-calendar>\r\n <!-- } -->\r\n</div>", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: LucyCalendarComponent, selector: "lucy-calendar", inputs: ["label", "dateValue", "placeholder", "min", "max", "dateFormat", "calendarVisible"], outputs: ["valueChange", "dateValueChange"] }, { kind: "ngmodule", type: FormsModule }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LucyDateComponent, decorators: [{ type: Component, args: [{ selector: 'lucy-date', standalone: true, imports: [CommonModule, LucyCalendarComponent, FormsModule], template: "<div class=\"relative\">\r\n <label for=\"date-picker\" class=\"block text-sm font-medium text-gray-700 mb-1\">\r\n {{ label }}\r\n </label>\r\n\r\n <div class=\"relative\">\r\n <input type=\"text\" id=\"date-picker\" [attr.readonly]=\"readonly ? true : null\"\r\n class=\"block w-full px-4 py-2 text-gray-700 bg-white border disabled:cursor-not-allowed disabled:text-red-700 border-gray-300 rounded-lg shadow-sm cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-800\"\r\n [placeholder]=\"placeholder ?? dateFormat\" [(value)]=\"value\" [attr.disabled]=\"disabled ? true : null\"\r\n (click)=\"readonly ? toggleCalendar() : null\" />\r\n <button\r\n class=\"absolute inset-y-0 right-0 flex items-center pr-3 cursor-pointer disabled:cursor-not-allowed disabled:text-gray-300\"\r\n [disabled]=\"disabled\" (click)=\"toggleCalendar()\">\r\n <svg xmlns=\"http://www.w3.org/2000/svg\"\r\n class=\"h-6 w-6 text-gray-500 hover:text-gray-700 disabled:hover:text-gray-500\" fill=\"none\"\r\n viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\"\r\n d=\"M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z\" />\r\n </svg>\r\n </button>\r\n </div>\r\n <lucy-calendar [calendarVisible]=\"showCalendar\" [label]=\"label\" [placeholder]=\"placeholder\" [dateValue]=\"dateValue\"\r\n (dateValueChange)=\"dateChange($event)\" (valueChange)=\"valueChange.emit($event)\" [dateFormat]=\"dateFormat\"\r\n [dateFormat]=\"dateFormat\"></lucy-calendar>\r\n <!-- } -->\r\n</div>" }] }], propDecorators: { label: [{ type: Input }], placeholder: [{ type: Input }], dateFormat: [{ type: Input }], selectedDay: [{ type: Input }], selectedYear: [{ type: Input }], selectedMonth: [{ type: Input }], readonly: [{ type: Input }], disabled: [{ type: Input }], value: [{ type: Input }], valueChange: [{ type: Output }], dateValue: [{ type: Input }], dateValueChange: [{ type: Output }] } }); /* * Public API Surface of lucy-calendar */ /** * Generated bundle index. Do not edit. */ export { LucyCalendarComponent, LucyCalendarDirective, LucyDateComponent, dayNames, isEthiopianLeapYear, monthNames, toEthiopian, toGregorian }; //# sourceMappingURL=lucy-calendar.mjs.map