UNPKG

novo-elements

Version:

Bullhorn's NOVO Element Repository for Angular 2

501 lines (447 loc) 19.1 kB
// 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'; @Component({ 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 { @Input() minYear: any; @Input() maxYear: any; @Input() start: any; @Input() end: any; @Input() inline: any; @Input() range: any; // Select callback for output @Output() onSelect: EventEmitter<any> = new EventEmitter(false); @ViewChild(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; } }