novo-elements
Version:
Bullhorn's NOVO Element Repository for Angular 2
501 lines (447 loc) • 19.1 kB
text/typescript
// NG2
import { ElementRef, Component, EventEmitter, forwardRef, Input, Output, OnInit, ViewChild, TemplateRef } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { animate, state, style, transition, trigger } from '@angular/animations';
// Vendor
import * as dateFns from 'date-fns';
// APP
import { Helpers } from '../../utils/Helpers';
import { NovoLabelService } from '../../services/novo-label-service';
// Value accessor for the component (supports ngModel)
const DATE_PICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NovoDatePickerElement),
multi: true
};
export interface RangeModal {
startDate: Date;
endDate: Date;
};
export type modelTypes = Date | RangeModal;
export interface Day {
date: Date;
isCurrentMonth?: boolean;
isToday?: boolean;
name?: string;
number?: string | number;
}
export type rangeSelectModes = 'startDate' | 'endDate';
({
selector: 'novo-date-picker',
providers: [DATE_PICKER_VALUE_ACCESSOR],
animations: [
trigger('startDateTextState', [
state('startDate', style({
'opacity': '1.0'
})),
state('endDate', style({
'opacity': '0.6'
})),
transition('startDate <=> endDate', animate('200ms ease-in'))
]),
trigger('endDateTextState', [
state('startDate', style({
'opacity': '0.6'
})),
state('endDate', style({
'opacity': '1.0'
})),
transition('startDate <=> endDate', animate('200ms ease-in'))
]),
trigger('indicatorState', [
state('startDate', style({
'transform': 'translateX(0%)'
})),
state('endDate', style({
'transform': 'translateX(100%)'
})),
transition('startDate <=> endDate', animate('200ms ease-in'))
])
],
template: `
<div class="calendar">
<div class="calendar-top" *ngIf="!inline && !range">
<h4 class="day" [attr.data-automation-id]="heading?.day">{{heading?.day}}</h4>
<h2 class="month" [attr.data-automation-id]="heading?.month">{{heading?.month}}</h2>
<h1 class="date" [attr.data-automation-id]="heading?.date">{{heading?.date}}</h1>
<h3 class="year" [attr.data-automation-id]="heading?.year">{{heading?.year}}</h3>
</div>
<div class="date-range-tabs" *ngIf="range">
<span class="range-tab" (click)="toggleRangeSelect('startDate')" [@startDateTextState]="rangeSelectMode">{{selectedLabel}}</span>
<span class="range-tab" (click)="toggleRangeSelect('endDate')" [@endDateTextState]="rangeSelectMode">{{selected2Label}}</span>
<i class="indicator" [@indicatorState]="rangeSelectMode"></i>
</div>
<div class="calendar-header">
<span class="previous" (click)="prevMonth($event)" data-automation-id="calendar-previous"></span>
<span class="heading">
<span class="month" (click)="open($event, 'months')" [attr.data-automation-id]="heading?.month">{{monthLabel}}</span>
<span class="year" (click)="open($event, 'years')" [attr.data-automation-id]="heading?.year">{{month?.getFullYear()}}</span>
</span>
<span class="next" (click)="nextMonth($event)" data-automation-id="calendar-next"></span>
</div>
<table class="calendar-content days" cellspacing="0" cellpadding="0" [hidden]="!(view=='days')">
<thead>
<tr>
<th *ngFor="let day of weekdays" title="{{day}}" class="weekday" [attr.data-automation-id]="day.substr(0, 2)">{{day.substr(0, 2)}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let week of weeks">
<td *ngFor="let day of week.days" [ngClass]="{
today: day.isToday,
'notinmonth': day.date.getMonth() !== this.month.getMonth(),
selected: isSelected(range, day.date, selected, selected2),
filler: isFiller(range, day.date, selected, selected2),
startfill: isStartFill(range, day.date, selected, selected2),
endfill: isEndFill(range, day.date, selected, selected2),
'selecting-range': isSelectingRange(range, day.date, selected, selected2, hoverDay, rangeSelectMode)
}" (click)="select($event, day, true)" (mouseover)="rangeHover($event, day)">
<button class="day" [attr.data-automation-id]="day.number" [disabled]="isDisabled(day.date, start, end)">{{day.number}}</button>
</td>
</tr>
</tbody>
</table>
<ul class="calendar-content months" [hidden]="view !== 'months'">
<li *ngFor="let month of months;let i = index" (click)="setMonth(i)">
<div class="month" [ngClass]="{selected: i === selected?.getMonth()}" [attr.data-automation-id]="month">{{month}}</div>
</li>
</ul>
<ul class="calendar-content years" [hidden]="view !== 'years'">
<li *ngFor="let year of years" (click)="setYear(year)">
<div class="year" [ngClass]="{selected: year == selected?.getFullYear()}" [attr.data-automation-id]="year">{{year}}</div>
</li>
</ul>
<div class="calendar-footer">
<span (click)="setToday()" class="today" data-automation-id="calendar-today">{{ labels.today }}</span>
</div>
</div>
`
})
export class NovoDatePickerElement implements ControlValueAccessor, OnInit {
() minYear: any;
() maxYear: any;
() start: any;
() end: any;
() inline: any;
() range: any;
// Select callback for output
() onSelect: EventEmitter<any> = new EventEmitter(false);
(TemplateRef) template: TemplateRef<any>;
// List of all the weekdays
weekdays: string[] = [];
// List of all months
months: string[] = [];
// List of all years (generated in ngOnInit)
years: Array<any> = [];
// Default view mode (select days)
view: string = 'days';
heading: any;
model: modelTypes;
month: Date;
monthLabel: string;
weeks: any;
selected: Date;
selectedLabel: string;
selected2: Date;
selected2Label: string;
hoverDay: any;
rangeSelectMode: rangeSelectModes = 'startDate';
onModelChange: Function = () => { };
onModelTouched: Function = () => { };
constructor(public labels: NovoLabelService, private element: ElementRef) { }
ngOnInit() {
// Determine the year array
let now = new Date();
let start = this.minYear ? Number(this.minYear) : now.getFullYear() - 100;
let end = this.maxYear ? Number(this.maxYear) : now.getFullYear() + 10;
for (let i = start; i <= end; i++) {
this.years.push(i);
}
// Set weekdays / months
this.weekdays = this.labels.getWeekdays();
this.months = this.labels.getMonths();
// Set labels
this.selectedLabel = this.labels.startDate;
this.selected2Label = this.labels.endDate;
this.updateView(this.model, false, true);
}
isSelectingRange(range, day, selected, selected2, hoverDay, rangeSelectMode) {
if (range) {
let isRangeModeEndDate = rangeSelectMode === 'endDate' && (selected && selected2 && dateFns.isAfter(day, selected2) && dateFns.isBefore(day, hoverDay));
let isRangeModeStartDate = rangeSelectMode === 'startDate' && (selected && selected2 && dateFns.isBefore(day, selected) && dateFns.isAfter(day, hoverDay));
let isNotSelected = !selected && selected2 && dateFns.isBefore(day, selected2) && dateFns.isAfter(day, hoverDay);
let isNotSelected2 = selected && !selected2 && dateFns.isAfter(day, selected) && dateFns.isBefore(day, hoverDay);
return isNotSelected2 || isNotSelected || isRangeModeStartDate || isRangeModeEndDate;
}
return false;
}
isEndFill(range, day, selected, selected2) {
if (range && selected2) {
return dateFns.isSameDay(day, selected2) && dateFns.isAfter(day, selected);
}
return false;
}
isStartFill(range, day, selected, selected2) {
if (range && selected2) {
return dateFns.isSameDay(day, selected) && dateFns.isBefore(day, selected2);
}
return false;
}
isFiller(range, day, selected, selected2) {
if (range && selected2) {
return dateFns.isAfter(day, selected) && dateFns.isBefore(day, selected2);
}
return false;
}
isSelected(range, day, selected, selected2) {
if (range) {
return day && (selected && (day.getDate() === selected.getDate() && day.getMonth() === selected.getMonth() && day.getFullYear() === selected.getFullYear()) || (selected2 && (day.getDate() === selected2.getDate() && day.getMonth() === selected2.getMonth() && day.getFullYear() === selected2.getFullYear())));
}
return day.getDate() === selected.getDate() && day.getMonth() === selected.getMonth() && day.getFullYear() === selected.getFullYear();
}
isDisabled(day, start, end) {
return dateFns.isBefore(day, start) || dateFns.isAfter(day, end);
}
updateView(date, fireEvents: boolean, markedSelected: boolean) {
if (date && date.startDate === null) {
this.clearRange();
} else {
if (!date) {
this.clearRange();
}
let value: any = date ? new Date(date) : new Date();
value = this.removeTime(value);
this.month = new Date(value);
this.monthLabel = this.labels.formatDateWithFormat(this.month, { month: 'short' });
let start = new Date(value.getTime());
start.setDate(1);
this.removeTime(start.setDate(1));
this.buildMonth(start, this.month);
if (markedSelected) {
this.select(null, { date: value }, fireEvents);
}
}
}
setToday() {
let tmp = new Date();
this.updateView(tmp, true, true);
// Go back to days
this.open(null, 'days');
}
clearRange() {
this.selected = null;
this.selectedLabel = this.labels.startDate;
this.selected2 = null;
this.selected2Label = this.labels.endDate;
}
setMonth(month: number): void {
let date = this.month ? this.month : new Date();
let tmp = dateFns.setMonth(date, month);
this.updateView(tmp, true, false);
// Go back to days
this.open(null, 'days');
}
setYear(year: number): void {
let date = this.month ? this.month : new Date();
let tmp = dateFns.setYear(date, year);
this.updateView(tmp, true, false);
// Go back to days
this.open(null, 'days');
}
select(event: Event, day: Day, fireEvents: boolean) {
Helpers.swallowEvent(event);
if (this.range) {
if (this.rangeSelectMode === 'startDate') {
if (this.selected2 && dateFns.isAfter(day.date, this.selected2)) {
// CLEAR END DATE
this.selected2 = null;
this.selected2Label = this.labels.endDate;
}
// SET START DATE
this.selected = day.date;
this.selectedLabel = this.labels.formatDateWithFormat(this.selected, {
month: 'short',
day: '2-digit',
year: 'numeric'
});
if (event) {
this.rangeSelectMode = 'endDate';
}
} else {
if (this.selected && dateFns.isBefore(day.date, this.selected2)) {
// CLEAR START DATE
this.selected = null;
this.selectedLabel = this.labels.startDate;
}
// SET END DATE
this.selected2 = dateFns.endOfDay(day.date);
this.selected2Label = this.labels.formatDateWithFormat(this.selected2, {
month: 'short',
day: '2-digit',
year: 'numeric'
});
if (event) {
this.rangeSelectMode = 'startDate';
}
}
} else {
this.selected = day.date;
this.selectedLabel = this.labels.formatDateWithFormat(this.selected, {
month: 'short',
day: '2-digit',
year: 'numeric'
});
this.updateHeading();
}
if (fireEvents && this.selected) {
// Emit our output
if (this.range && this.selected && this.selected2) {
this.onSelect.next({
startDate: {
month: this.labels.formatDateWithFormat(this.selected, { month: 'long' }),
year: this.selected.getFullYear(),
day: this.labels.formatDateWithFormat(this.selected, { weekday: 'long' }),
date: this.selected
},
endDate: {
month: this.labels.formatDateWithFormat(this.selected2, { month: 'long' }),
year: this.selected.getFullYear(),
day: this.labels.formatDateWithFormat(this.selected2, { weekday: 'long' }),
date: this.selected
}
});
} else {
this.onSelect.next({
month: this.labels.formatDateWithFormat(this.selected, { month: 'long' }),
year: this.selected.getFullYear(),
day: this.labels.formatDateWithFormat(this.selected, { weekday: 'long' }),
date: this.selected
});
}
if (this.range) {
// Also, update the ngModel
this.onModelChange({
startDate: this.selected,
endDate: this.selected2 ? this.selected2 : null
});
this.model = {
startDate: this.selected,
endDate: this.selected2 ? this.selected2 : null
};
} else {
// Also, update the ngModel
this.onModelChange(this.selected);
this.model = this.selected;
}
}
}
open(event: Event, type: string) {
Helpers.swallowEvent(event);
// If they click the toggle two time in a row, close it (go back to days)
if (type === this.view) {
this.view = 'days';
} else {
this.view = type;
}
// Make sure to scroll the selected one into view
if (this.view === 'years' || this.view === 'months') {
setTimeout(() => {
let container = this.element.nativeElement.querySelector(`.calendar-content.${this.view}`);
let selectedItem = this.element.nativeElement.querySelector(`.calendar-content.${this.view} .${this.view === 'years' ? 'year' : 'month'}.selected`);
if (container && selectedItem) {
container.scrollTop = selectedItem.offsetTop - 100;
}
});
}
this.updateHeading();
}
prevMonth(event: Event): void {
Helpers.swallowEvent(event);
let tmp = dateFns.subMonths(this.month, 1);
this.updateView(tmp, false, false);
}
nextMonth(event: Event): void {
Helpers.swallowEvent(event);
let tmp = dateFns.addMonths(this.month, 1);
this.updateView(tmp, false, false);
}
updateHeading() {
if (!this.selected) {
return;
}
this.heading = {
month: this.labels.formatDateWithFormat(this.selected, { month: 'long' }),
year: this.selected.getFullYear(),
day: this.labels.formatDateWithFormat(this.selected, { weekday: 'long' }),
date: this.selected.getDate()
};
}
/**
* Remove the time aspect of the date
* @param date
* @returns with time stripped out
*/
removeTime(date: any): Date {
let ret = new Date(date);
ret.setHours(12);
ret.setSeconds(0);
ret.setMilliseconds(0);
return ret;
}
buildMonth(start: Date, month: Date) {
// Reset the weeks
this.weeks = [];
// House keeping variables to know when we are done building the month
let done = false,
date = new Date(start.getTime()),
monthIndex = date.getMonth(),
count = 0;
if (date.getDay() !== 0) {
date = dateFns.subDays(date, date.getDay());
}
while (!done) {
// Build the days for the weeks
this.weeks.push({ days: this.buildWeek(new Date(date.getTime()), month) });
// Increment variables for the next iteration
date = dateFns.addDays(date, 7);
done = count++ > 2 && monthIndex !== date.getMonth();
monthIndex = date.getMonth();
}
}
buildWeek(date: Date, month: Date): Array<Object> {
// Build out of the days of the week
let days = [];
// Iterate over the days of the week
for (let i = 0; i < 7; i++) {
// Push a variable on the day array with lots of helpers to make the template easier
days.push({
name: this.weekdays[i],
number: date.getDate(),
isToday: dateFns.isToday(date),
date: date
});
// Increment for the next iteration
date = dateFns.addDays(date, 1);
}
return days;
}
toggleRangeSelect(range: rangeSelectModes): void {
this.rangeSelectMode = range;
}
rangeHover(event: Event, day: Day): void {
this.hoverDay = day.date;
}
// ValueAccessor Functions
writeValue(model: modelTypes): void {
this.model = model;
if (Helpers.isDate(model)) {
this.updateView(model, false, true);
}
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
}