import {
Component,
Input,
OnChanges,
TemplateRef,
forwardRef,
OnInit,
SimpleChanges,
EventEmitter,
Output
} from '@angular/core';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
import {Ng2vCalendar} from './ng2v-calendar';
import {Ng2vDate} from './ng2v-date';
import {Ng2vDatepickerService} from './datepicker-service';
import {MonthViewModel, NavigationEvent} from './datepicker-view-model';
import {toInteger} from '../util/util';
import {DayTemplateContext} from './datepicker-day-template-context';
import {Ng2vDatepickerConfig} from './datepicker-config';
import {Ng2vDateStruct} from './ng2v-date-struct';
import {Ng2vDatepickerI18n} from './datepicker-i18n';
const NGB_DATEPICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => Ng2vDatepicker),
multi: true
};
/**
* The payload of the datepicker navigation event
*/
export interface Ng2vDatepickerNavigateEvent {
/**
* Currently displayed month
*/
current: {year: number, month: number};
/**
* Month we're navigating to
*/
next: {year: number, month: number};
}
/**
* A lightweight and highly configurable datepicker directive
*/
@Component({
exportAs: 'ng2vDatepicker',
selector: 'ng2v-datepicker',
host: {'class': 'd-inline-block rounded'},
styles: [`
:host {
border: 1px solid rgba(0, 0, 0, 0.125);
}
.ng2v-dp-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.ng2v-dp-month {
pointer-events: none;
}
ng2v-datepicker-month-view {
pointer-events: auto;
}
.ng2v-dp-month:first-child {
margin-left: 0 !important;
}
.ng2v-dp-month-name {
font-size: larger;
height: 2rem;
line-height: 2rem;
}
`],
template: `
1" class="ng2v-dp-month-name text-center">
{{ i18n.getMonthFullName(month.number) }} {{ month.year }}
`,
providers: [NGB_DATEPICKER_VALUE_ACCESSOR, Ng2vDatepickerService]
})
export class Ng2vDatepicker implements OnChanges,
OnInit, ControlValueAccessor {
_date: Ng2vDate;
_maxDate: Ng2vDate;
_minDate: Ng2vDate;
model: Ng2vDate;
months: MonthViewModel[] = [];
/**
* Reference for the custom template for the day display
*/
@Input() dayTemplate: TemplateRef;
/**
* Number of months to display
*/
@Input() displayMonths: number;
/**
* First day of the week. With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun
*/
@Input() firstDayOfWeek: number;
/**
* Callback to mark a given date as disabled.
* 'Current' contains the month that will be displayed in the view
*/
@Input() markDisabled: (date: Ng2vDateStruct, current: {year: number, month: number}) => boolean;
/**
* Min date for the navigation. If not provided will be 10 years before today or `startDate`
*/
@Input() minDate: Ng2vDateStruct;
/**
* Max date for the navigation. If not provided will be 10 years from today or `startDate`
*/
@Input() maxDate: Ng2vDateStruct;
/**
* Navigation type: `select` (default with select boxes for month and year), `arrows`
* (without select boxes, only navigation arrows) or `none` (no navigation at all)
*/
@Input() navigation: 'select' | 'arrows' | 'none';
/**
* The way to display days that don't belong to current month: `visible` (default),
* `hidden` (not displayed) or `collapsed` (not displayed with empty space collapsed)
*/
@Input() outsideDays: 'visible' | 'collapsed' | 'hidden';
/**
* Whether to display days of the week
*/
@Input() showWeekdays: boolean;
/**
* Whether to display week numbers
*/
@Input() showWeekNumbers: boolean;
/**
* Date to open calendar with.
* With default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.
* If nothing or invalid date provided, calendar will open with current month.
* Use 'navigateTo(date)' as an alternative
*/
@Input() startDate: {year: number, month: number};
/**
* An event fired when navigation happens and currently displayed month changes.
* See Ng2vDatepickerNavigateEvent for the payload info.
*/
@Output() navigate = new EventEmitter();
disabled = false;
onChange = (_: any) => {};
onTouched = () => {};
constructor(
private _service: Ng2vDatepickerService, private _calendar: Ng2vCalendar, public i18n: Ng2vDatepickerI18n,
config: Ng2vDatepickerConfig) {
this.dayTemplate = config.dayTemplate;
this.displayMonths = config.displayMonths;
this.firstDayOfWeek = config.firstDayOfWeek;
this.markDisabled = config.markDisabled;
this.minDate = config.minDate;
this.maxDate = config.maxDate;
this.navigation = config.navigation;
this.outsideDays = config.outsideDays;
this.showWeekdays = config.showWeekdays;
this.showWeekNumbers = config.showWeekNumbers;
this.startDate = config.startDate;
}
getHeaderHeight() {
const h = this.showWeekdays ? 6.25 : 4.25;
return this.displayMonths === 1 || this.navigation !== 'select' ? h - 2 : h;
}
getHeaderMargin() {
const m = this.showWeekdays ? 2 : 0;
return this.displayMonths !== 1 || this.navigation !== 'select' ? m + 2 : m;
}
/**
* Navigates current view to provided date.
* With default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.
* If nothing or invalid date provided calendar will open current month.
* Use 'startDate' input as an alternative
*/
navigateTo(date?: {year: number, month: number}) {
this._setViewWithinLimits(this._service.toValidDate(date));
this._updateData();
}
ngOnInit() {
this._setDates();
this.navigateTo(this._date);
}
ngOnChanges(changes: SimpleChanges) {
this._setDates();
this._setViewWithinLimits(this._date);
if (changes['displayMonths']) {
this.displayMonths = toInteger(this.displayMonths);
}
// we have to force rebuild all months only if any of these inputs changes
if (['startDate', 'minDate', 'maxDate', 'navigation', 'firstDayOfWeek', 'markDisabled', 'displayMonths'].some(
input => !!changes[input])) {
this._updateData(true);
}
}
onDateSelect(date: Ng2vDate) {
this._setViewWithinLimits(date);
this.onTouched();
this.writeValue(date);
this.onChange({year: date.year, month: date.month, day: date.day});
// switch current month
if (this._date.month !== this.months[0].number && this.displayMonths === 1) {
this._updateData();
}
}
onNavigateDateSelect(date: Ng2vDate) {
this._setViewWithinLimits(date);
this._updateData();
}
onNavigateEvent(event: NavigationEvent) {
switch (event) {
case NavigationEvent.PREV:
this._setViewWithinLimits(this._calendar.getPrev(this.months[0].firstDate, 'm'));
break;
case NavigationEvent.NEXT:
this._setViewWithinLimits(this._calendar.getNext(this.months[0].firstDate, 'm'));
break;
}
this._updateData();
}
registerOnChange(fn: (value: any) => any): void { this.onChange = fn; }
registerOnTouched(fn: () => any): void { this.onTouched = fn; }
writeValue(value) { this.model = this._service.toValidDate(value, null); }
setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; }
private _setDates() {
this._maxDate = Ng2vDate.from(this.maxDate);
this._minDate = Ng2vDate.from(this.minDate);
this._date = this._service.toValidDate(this.startDate);
if (!this._calendar.isValid(this._minDate)) {
this._minDate = this._calendar.getPrev(this._date, 'y', 10);
this.minDate = {year: this._minDate.year, month: this._minDate.month, day: this._minDate.day};
}
if (!this._calendar.isValid(this._maxDate)) {
this._maxDate = this._calendar.getNext(this._date, 'y', 11);
this._maxDate = this._calendar.getPrev(this._maxDate);
this.maxDate = {year: this._maxDate.year, month: this._maxDate.month, day: this._maxDate.day};
}
if (this._minDate && this._maxDate && this._maxDate.before(this._minDate)) {
throw new Error(`'maxDate' ${this._maxDate} should be greater than 'minDate' ${this._minDate}`);
}
}
private _setViewWithinLimits(date: Ng2vDate) {
if (this._minDate && date.before(this._minDate)) {
this._date = new Ng2vDate(this._minDate.year, this._minDate.month, 1);
} else if (this._maxDate && date.after(this._maxDate)) {
this._date = new Ng2vDate(this._maxDate.year, this._maxDate.month, 1);
} else {
this._date = new Ng2vDate(date.year, date.month, 1);
}
}
private _updateData(force = false) {
const newMonths = [];
for (let i = 0; i < this.displayMonths; i++) {
const newDate = this._calendar.getNext(this._date, 'm', i);
const index = this.months.findIndex(month => month.firstDate.equals(newDate));
if (force || index === -1) {
newMonths.push(
this._service.generateMonthViewModel(
newDate, this._minDate, this._maxDate, toInteger(this.firstDayOfWeek), this.markDisabled));
} else {
newMonths.push(this.months[index]);
}
}
const newDate = newMonths[0].firstDate;
const oldDate = this.months[0] ? this.months[0].firstDate : null;
this.months = newMonths;
// emitting navigation event if the first month changes
if (!newDate.equals(oldDate)) {
this.navigate.emit({
current: oldDate ? {year: oldDate.year, month: oldDate.month} : null,
next: {year: newDate.year, month: newDate.month}
});
}
}
}