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
JavaScript
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