From 3b82948dec747940636ca1abff27fe20447df967 Mon Sep 17 00:00:00 2001 From: gpolychronis-amadeus Date: Wed, 20 Nov 2019 17:26:58 +0100 Subject: [PATCH] feat(datepicker): export NgbDatepickerMonthView --- .../datepicker/datepicker.module.ts | 10 + .../custommonth/datepicker-custommonth.html | 14 + .../datepicker-custommonth.module.ts | 13 + .../custommonth/datepicker-custommonth.ts | 27 ++ .../demos/keyboard/datepicker-keyboard.ts | 10 +- .../datepicker-overview.component.html | 13 + .../overview/datepicker-overview.component.ts | 15 + src/datepicker/datepicker-input.spec.ts | 8 +- src/datepicker/datepicker-integration.spec.ts | 63 ++- .../datepicker-keyboard-service.spec.ts | 4 +- src/datepicker/datepicker-keyboard-service.ts | 5 +- src/datepicker/datepicker-month-view.spec.ts | 346 --------------- src/datepicker/datepicker-month-view.ts | 51 --- ...-month-view.scss => datepicker-month.scss} | 2 +- src/datepicker/datepicker-month.spec.ts | 403 ++++++++++++++++++ src/datepicker/datepicker-month.ts | 57 +++ src/datepicker/datepicker-service.spec.ts | 7 + src/datepicker/datepicker-service.ts | 9 + src/datepicker/datepicker.module.ts | 14 +- src/datepicker/datepicker.scss | 2 +- src/datepicker/datepicker.spec.ts | 45 +- src/datepicker/datepicker.ts | 71 +-- src/index.ts | 2 + 23 files changed, 710 insertions(+), 481 deletions(-) create mode 100644 demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.html create mode 100644 demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.module.ts create mode 100644 demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.ts delete mode 100644 src/datepicker/datepicker-month-view.spec.ts delete mode 100644 src/datepicker/datepicker-month-view.ts rename src/datepicker/{datepicker-month-view.scss => datepicker-month.scss} (95%) create mode 100644 src/datepicker/datepicker-month.spec.ts create mode 100644 src/datepicker/datepicker-month.ts diff --git a/demo/src/app/components/datepicker/datepicker.module.ts b/demo/src/app/components/datepicker/datepicker.module.ts index ce6a34b19c..36b28867e3 100644 --- a/demo/src/app/components/datepicker/datepicker.module.ts +++ b/demo/src/app/components/datepicker/datepicker.module.ts @@ -23,6 +23,8 @@ import { NgbdDatepickerFooterTemplateModule } from './demos/footertemplate/datep import { NgbdDatepickerFootertemplate } from './demos/footertemplate/datepicker-footertemplate'; import { NgbdDatepickerI18n } from './demos/i18n/datepicker-i18n'; import { NgbdDatepickerI18nModule } from './demos/i18n/datepicker-i18n.module'; +import { NgbdDatepickerCustommonth } from './demos/custommonth/datepicker-custommonth'; +import { NgbdDatepickerCustommonthModule } from './demos/custommonth/datepicker-custommonth.module'; import { NgbdDatepickerMultiple } from './demos/multiple/datepicker-multiple'; import { NgbdDatepickerMultipleModule } from './demos/multiple/datepicker-multiple.module'; import { NgbdDatepickerPopup } from './demos/popup/datepicker-popup'; @@ -46,6 +48,7 @@ const OVERVIEW = { 'limiting-dates': 'Disabling and limiting dates', 'day-template': 'Day display customization', today: 'Today\'s date', + 'content-template': 'Content Template', 'footer-template': 'Custom footer', range: 'Range selection', i18n: 'Internationalization', @@ -107,6 +110,12 @@ const DEMOS = { code: require('!!raw-loader!./demos/customday/datepicker-customday').default, markup: require('!!raw-loader!./demos/customday/datepicker-customday.html').default }, + custommonth: { + title: 'Custom month layout', + type: NgbdDatepickerCustommonth, + code: require('!!raw-loader!./demos/custommonth/datepicker-custommonth').default, + markup: require('!!raw-loader!./demos/custommonth/datepicker-custommonth.html').default + }, footertemplate: { title: 'Footer template', type: NgbdDatepickerFootertemplate, @@ -165,6 +174,7 @@ export const ROUTES = [ NgbdDatepickerRangePopupModule, NgbdDatepickerAdapterModule, NgbdDatepickerKeyboardModule, + NgbdDatepickerCustommonthModule, ...DEMO_CALENDAR_MODULES ], declarations: [ diff --git a/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.html b/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.html new file mode 100644 index 0000000000..cb2a91816a --- /dev/null +++ b/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.html @@ -0,0 +1,14 @@ +

This datepicker uses a custom month layout.

+ + + +
+ {{i18n.getMonthFullName(monthStruct.month)}} {{monthStruct.year}} + +
+
+
diff --git a/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.module.ts b/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.module.ts new file mode 100644 index 0000000000..5c321db814 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerCustommonth } from './datepicker-custommonth'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDatepickerCustommonth], + exports: [NgbdDatepickerCustommonth], + bootstrap: [NgbdDatepickerCustommonth] +}) +export class NgbdDatepickerCustommonthModule {} diff --git a/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.ts b/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.ts new file mode 100644 index 0000000000..4334fda00c --- /dev/null +++ b/demo/src/app/components/datepicker/demos/custommonth/datepicker-custommonth.ts @@ -0,0 +1,27 @@ +import {Component} from '@angular/core'; +import {NgbDatepickerI18n} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-custommonth', + templateUrl: './datepicker-custommonth.html', + styles: [` + ngb-datepicker { + display: flex; + border: none; + } + .custom-month-view { + margin: 1rem; + display: flex; + flex-direction: column; + align-items: center; + border: 1px solid gray; + border-radius: 1rem 1rem 0 0; + } + .custom-month-view span{ + font-weight: bold; + } + `] +}) +export class NgbdDatepickerCustommonth { + constructor(public i18n: NgbDatepickerI18n) {} +} diff --git a/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts index 4a7502c3b4..ba303be3a1 100644 --- a/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts +++ b/demo/src/app/components/datepicker/demos/keyboard/datepicker-keyboard.ts @@ -1,5 +1,5 @@ import {Component, Injectable} from '@angular/core'; -import {NgbCalendar, NgbDatepicker, NgbDatepickerKeyboardService, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; +import {NgbDatepicker, NgbDatepickerKeyboardService, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; const Key = { PageUp: 'PageUp', @@ -10,14 +10,14 @@ const Key = { @Injectable() export class CustomKeyboardService extends NgbDatepickerKeyboardService { - processKey(event: KeyboardEvent, dp: NgbDatepicker, calendar: NgbCalendar) { + processKey(event: KeyboardEvent, dp: NgbDatepicker) { const state = dp.state; switch (event.code) { case Key.PageUp: - dp.focusDate(calendar.getPrev(state.focusedDate, event.altKey ? 'y' : 'm')); + dp.focusDate(dp.calendar.getPrev(state.focusedDate, event.altKey ? 'y' : 'm')); break; case Key.PageDown: - dp.focusDate(calendar.getNext(state.focusedDate, event.altKey ? 'y' : 'm')); + dp.focusDate(dp.calendar.getNext(state.focusedDate, event.altKey ? 'y' : 'm')); break; case Key.End: dp.focusDate(event.altKey ? state.maxDate : state.lastDate); @@ -26,7 +26,7 @@ export class CustomKeyboardService extends NgbDatepickerKeyboardService { dp.focusDate(event.altKey ? state.minDate : state.firstDate); break; default: - super.processKey(event, dp, calendar); + super.processKey(event, dp); return; } event.preventDefault(); diff --git a/demo/src/app/components/datepicker/overview/datepicker-overview.component.html b/demo/src/app/components/datepicker/overview/datepicker-overview.component.html index e11ff869c1..43c3830351 100644 --- a/demo/src/app/components/datepicker/overview/datepicker-overview.component.html +++ b/demo/src/app/components/datepicker/overview/datepicker-overview.component.html @@ -279,6 +279,19 @@

Input date parsing and formatting

+ + +

+ You can replace the content of the datepicker. + Combined with the NgbDatepickerMonthView you can customize the layout of months as in the + custom month layout example. +

+ + + +
+ +

diff --git a/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts b/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts index e04a2d8adc..f9849550da 100644 --- a/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts +++ b/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts @@ -119,6 +119,21 @@ export class NgbdDatepickerOverviewComponent { `, }), + contentTemplate: Snippet({ + lang: 'html', + code: ` + + +

+ {{i18n.getMonthFullName(monthStruct.month)}} {{monthStruct.year}} + +
+ + + `, + }), todayHTML: Snippet({ lang: 'html', code: ` diff --git a/src/datepicker/datepicker-input.spec.ts b/src/datepicker/datepicker-input.spec.ts index 4c18bb1ac7..e66164a276 100644 --- a/src/datepicker/datepicker-input.spec.ts +++ b/src/datepicker/datepicker-input.spec.ts @@ -289,7 +289,7 @@ describe('NgbInputDatepicker', () => { expect(input.disabled).toBeTruthy(); expect(buttonInDatePicker.disabled).toBeTruthy(); - const dayElements = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + const dayElements = fixture.nativeElement.querySelectorAll('ngb-datepicker-month .ngb-dp-day'); expect(dayElements[1]).toHaveCssClass('disabled'); expect(dayElements[11]).toHaveCssClass('disabled'); expect(dayElements[21]).toHaveCssClass('disabled'); @@ -303,7 +303,7 @@ describe('NgbInputDatepicker', () => { expect(input.disabled).toBeFalsy(); expect(buttonInDatePicker.disabled).toBeFalsy(); - const dayElements2 = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + const dayElements2 = fixture.nativeElement.querySelectorAll('ngb-datepicker-month .ngb-dp-day'); expect(dayElements2[1]).not.toHaveCssClass('disabled'); expect(dayElements2[11]).not.toHaveCssClass('disabled'); expect(dayElements2[21]).not.toHaveCssClass('disabled'); @@ -329,7 +329,7 @@ describe('NgbInputDatepicker', () => { expect(input.disabled).toBeTruthy(); expect(buttonInDatePicker.disabled).toBeTruthy(); - const dayElements = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + const dayElements = fixture.nativeElement.querySelectorAll('ngb-datepicker-month .ngb-dp-day'); expect(dayElements[1]).toHaveCssClass('disabled'); expect(dayElements[11]).toHaveCssClass('disabled'); expect(dayElements[21]).toHaveCssClass('disabled'); @@ -341,7 +341,7 @@ describe('NgbInputDatepicker', () => { expect(input.disabled).toBeFalsy(); expect(buttonInDatePicker.disabled).toBeFalsy(); - const dayElements2 = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + const dayElements2 = fixture.nativeElement.querySelectorAll('ngb-datepicker-month .ngb-dp-day'); expect(dayElements2[1]).not.toHaveCssClass('disabled'); expect(dayElements2[11]).not.toHaveCssClass('disabled'); expect(dayElements2[21]).not.toHaveCssClass('disabled'); diff --git a/src/datepicker/datepicker-integration.spec.ts b/src/datepicker/datepicker-integration.spec.ts index 6481b13379..34d21f987b 100644 --- a/src/datepicker/datepicker-integration.spec.ts +++ b/src/datepicker/datepicker-integration.spec.ts @@ -8,6 +8,7 @@ import {getMonthSelect, getYearSelect} from '../test/datepicker/common'; import {NgbDatepickerI18n, NgbDatepickerI18nDefault} from './datepicker-i18n'; import {NgbDatepicker} from './datepicker'; import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service'; +import {NgbDatepickerMonth} from './datepicker-month'; import {Key} from '../util/key'; describe('ngb-datepicker integration', () => { @@ -116,18 +117,18 @@ describe('ngb-datepicker integration', () => { @Injectable() class CustomKeyboardService extends NgbDatepickerKeyboardService { - processKey(event: KeyboardEvent, service: NgbDatepicker, calendar: NgbCalendar) { + processKey(event: KeyboardEvent, service: NgbDatepicker) { const state = service.state; // tslint:disable-next-line:deprecation switch (event.which) { case Key.PageUp: - service.focusDate(calendar.getPrev(state.focusedDate, event.altKey ? 'y' : 'm', 1)); + service.focusDate(service.calendar.getPrev(state.focusedDate, event.altKey ? 'y' : 'm', 1)); break; case Key.PageDown: - service.focusDate(calendar.getNext(state.focusedDate, event.altKey ? 'y' : 'm', 1)); + service.focusDate(service.calendar.getNext(state.focusedDate, event.altKey ? 'y' : 'm', 1)); break; default: - super.processKey(event, service, calendar); + super.processKey(event, service); return; } event.preventDefault(); @@ -136,8 +137,8 @@ describe('ngb-datepicker integration', () => { } let fixture: ComponentFixture; - let dp: NgbDatepicker; - let ngbCalendar: NgbCalendar; + let calendar: NgbCalendar; + let mv: NgbDatepickerMonth; let startDate: NgbDateStruct = new NgbDate(2018, 1, 1); beforeEach(() => { @@ -151,22 +152,54 @@ describe('ngb-datepicker integration', () => { fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); + calendar = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker).calendar; + mv = fixture.debugElement.query(By.css('ngb-datepicker-month')).injector.get(NgbDatepickerMonth); - dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); - ngbCalendar = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbCalendar as Type); - - spyOn(ngbCalendar, 'getPrev'); + spyOn(calendar, 'getPrev'); }); it('should allow customize keyboard navigation', () => { - dp.onKeyDown({which: Key.PageUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}}); - expect(ngbCalendar.getPrev).toHaveBeenCalledWith(startDate, 'y', 1); - dp.onKeyDown({which: Key.PageUp, shiftKey: true, preventDefault: () => {}, stopPropagation: () => {}}); - expect(ngbCalendar.getPrev).toHaveBeenCalledWith(startDate, 'm', 1); + mv.onKeyDown({which: Key.PageUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}}); + expect(calendar.getPrev).toHaveBeenCalledWith(startDate, 'y', 1); + mv.onKeyDown({which: Key.PageUp, shiftKey: true, preventDefault: () => {}, stopPropagation: () => {}}); + expect(calendar.getPrev).toHaveBeenCalledWith(startDate, 'm', 1); }); it('should allow access to default keyboard navigation', () => { - dp.onKeyDown({which: Key.ArrowUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}}); + mv.onKeyDown({which: Key.ArrowUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}}); + expect(calendar.getPrev).toHaveBeenCalledWith(startDate, 'd', 7); + }); + + }); + + describe('ngb-datepicker-month', () => { + let fixture: ComponentFixture; + let mv: NgbDatepickerMonth; + let startDate: NgbDateStruct = new NgbDate(2018, 1, 1); + let ngbCalendar: NgbCalendar; + + beforeEach(() => { + TestBed.overrideComponent(TestComponent, { + set: { + template: ` + + + + + ` + } + }); + + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + mv = fixture.debugElement.query(By.css('ngb-datepicker-month')).injector.get(NgbDatepickerMonth); + ngbCalendar = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbCalendar as Type); + + spyOn(ngbCalendar, 'getPrev'); + }); + + it('should preserve the functionality of keyboard service', () => { + mv.onKeyDown({which: Key.ArrowUp, altKey: true, preventDefault: () => {}, stopPropagation: () => {}}); expect(ngbCalendar.getPrev).toHaveBeenCalledWith(startDate, 'd', 7); }); }); diff --git a/src/datepicker/datepicker-keyboard-service.spec.ts b/src/datepicker/datepicker-keyboard-service.spec.ts index ba1bb61134..30b661fcb9 100644 --- a/src/datepicker/datepicker-keyboard-service.spec.ts +++ b/src/datepicker/datepicker-keyboard-service.spec.ts @@ -14,7 +14,7 @@ describe('ngb-datepicker-keyboard-service', () => { let service: NgbDatepickerKeyboardService; let calendar: NgbCalendar; let mock: Partial; - let processKey = function(e: KeyboardEvent) { service.processKey(e, mock as NgbDatepicker, calendar); }; + let processKey = function(e: KeyboardEvent) { service.processKey(e, mock as NgbDatepicker); }; let state: NgbDatepickerState = Object.assign({focusedDate: {day: 1, month: 1, year: 2018}}); beforeEach(() => { @@ -23,7 +23,7 @@ describe('ngb-datepicker-keyboard-service', () => { calendar = TestBed.get(NgbCalendar as Type); service = TestBed.get(NgbDatepickerKeyboardService); - mock = {state, focusDate: () => {}, focusSelect: () => {}}; + mock = {state, focusDate: () => {}, focusSelect: () => {}, calendar}; spyOn(mock, 'focusDate'); spyOn(mock, 'focusSelect'); diff --git a/src/datepicker/datepicker-keyboard-service.ts b/src/datepicker/datepicker-keyboard-service.ts index a304529d76..5c1b9d927a 100644 --- a/src/datepicker/datepicker-keyboard-service.ts +++ b/src/datepicker/datepicker-keyboard-service.ts @@ -1,5 +1,4 @@ import {Injectable} from '@angular/core'; -import {NgbCalendar} from './ngb-calendar'; import {NgbDatepicker} from './datepicker'; import {Key} from '../util/key'; @@ -15,8 +14,8 @@ export class NgbDatepickerKeyboardService { /** * Processes a keyboard event. */ - processKey(event: KeyboardEvent, datepicker: NgbDatepicker, calendar: NgbCalendar) { - const state = datepicker.state; + processKey(event: KeyboardEvent, datepicker: NgbDatepicker) { + const {state, calendar} = datepicker; // tslint:disable-next-line:deprecation switch (event.which) { case Key.PageUp: diff --git a/src/datepicker/datepicker-month-view.spec.ts b/src/datepicker/datepicker-month-view.spec.ts deleted file mode 100644 index cd0a3e8f02..0000000000 --- a/src/datepicker/datepicker-month-view.spec.ts +++ /dev/null @@ -1,346 +0,0 @@ -import {TestBed, ComponentFixture} from '@angular/core/testing'; -import {createGenericTestComponent} from '../test/common'; - -import {Component} from '@angular/core'; - -import {NgbDatepickerModule} from './datepicker.module'; -import {NgbDatepickerMonthView} from './datepicker-month-view'; -import {MonthViewModel} from './datepicker-view-model'; -import {NgbDate} from './ngb-date'; -import {NgbDatepickerDayView} from './datepicker-day-view'; - -const createTestComponent = (html: string) => - createGenericTestComponent(html, TestComponent) as ComponentFixture; - -function getWeekdays(element: HTMLElement): HTMLElement[] { - return Array.from(element.querySelectorAll('.ngb-dp-weekday')); -} - -function getWeekNumbers(element: HTMLElement): HTMLElement[] { - return Array.from(element.querySelectorAll('.ngb-dp-week-number')); -} - -function getDates(element: HTMLElement): HTMLElement[] { - return Array.from(element.querySelectorAll('.ngb-dp-day')); -} - -function expectWeekdays(element: HTMLElement, weekdays: string[]) { - const result = getWeekdays(element).map(td => td.innerText.trim()); - expect(result).toEqual(weekdays); -} - -function expectWeekNumbers(element: HTMLElement, weeknumbers: string[]) { - const result = getWeekNumbers(element).map(td => td.innerText.trim()); - expect(result).toEqual(weeknumbers); -} - -function expectDates(element: HTMLElement, dates: string[]) { - const result = getDates(element).map(td => td.innerText.trim()); - expect(result).toEqual(dates); -} - -describe('ngb-datepicker-month-view', () => { - - beforeEach(() => { - TestBed.overrideModule(NgbDatepickerModule, {set: {exports: [NgbDatepickerMonthView, NgbDatepickerDayView]}}); - TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDatepickerModule]}); - }); - - it('should show/hide weekdays', () => { - const fixture = createTestComponent( - ''); - - expectWeekdays(fixture.nativeElement, ['Mo', 'Tu']); - - fixture.componentInstance.showWeekdays = false; - fixture.detectChanges(); - expectWeekdays(fixture.nativeElement, []); - }); - - it('should show/hide week numbers', () => { - const fixture = createTestComponent( - ''); - - expectWeekNumbers(fixture.nativeElement, ['1', '2', '3']); - - fixture.componentInstance.showWeekNumbers = false; - fixture.detectChanges(); - expectWeekNumbers(fixture.nativeElement, []); - }); - - it('should use custom template to display dates', () => { - const fixture = createTestComponent(` - {{ date.day }} - - `); - expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); - }); - - it('should use "date" as an implicit value for the template', () => { - const fixture = createTestComponent(` - {{ d.day }} - - `); - expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); - }); - - it('should send date selection events', () => { - const fixture = createTestComponent(` - {{ date.day }} - - `); - - spyOn(fixture.componentInstance, 'onClick'); - - const dates = getDates(fixture.nativeElement); - dates[1].click(); - - expect(fixture.componentInstance.onClick).toHaveBeenCalledWith(new NgbDate(2016, 8, 1)); - }); - - it('should not send date selection events for hidden and disabled dates', () => { - const fixture = createTestComponent(` - {{ date.day }} - - `); - - spyOn(fixture.componentInstance, 'onClick'); - - const dates = getDates(fixture.nativeElement); - dates[0].click(); // hidden - dates[2].click(); // disabled - - expect(fixture.componentInstance.onClick).not.toHaveBeenCalled(); - }); - - it('should set cursor to pointer or default', () => { - const fixture = createTestComponent(` - {{ date.day }} - - `); - - const dates = getDates(fixture.nativeElement); - // hidden - expect(window.getComputedStyle(dates[0]).getPropertyValue('cursor')).toBe('default'); - // normal - expect(window.getComputedStyle(dates[1]).getPropertyValue('cursor')).toBe('pointer'); - // disabled - expect(window.getComputedStyle(dates[2]).getPropertyValue('cursor')).toBe('default'); - }); - - it('should apply correct CSS classes to days', () => { - const fixture = createTestComponent(` - {{ date.day }} - - `); - - let dates = getDates(fixture.nativeElement); - // hidden - expect(dates[0]).toHaveCssClass('hidden'); - expect(dates[0]).not.toHaveCssClass('disabled'); - expect(dates[0]).not.toHaveCssClass('ngb-dp-today'); - // normal - expect(dates[1]).not.toHaveCssClass('hidden'); - expect(dates[1]).not.toHaveCssClass('disabled'); - expect(dates[1]).not.toHaveCssClass('ngb-dp-today'); - // disabled - expect(dates[2]).not.toHaveCssClass('hidden'); - expect(dates[2]).toHaveCssClass('disabled'); - expect(dates[2]).toHaveCssClass('ngb-dp-today'); - }); - - it('should not display collapsed weeks', () => { - const fixture = createTestComponent(` - {{ date.day }} - - - `); - - expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); - }); - - it('should add correct aria-label attribute', () => { - const fixture = createTestComponent(` - {{ date.day }} - - `); - - let dates = getDates(fixture.nativeElement); - expect(dates[0].getAttribute('aria-label')).toBe('Monday'); - }); -}); - -@Component({selector: 'test-cmp', template: ''}) -class TestComponent { - month: MonthViewModel = { - firstDate: new NgbDate(2016, 8, 1), - lastDate: new NgbDate(2016, 8, 31), - year: 2016, - number: 8, - weekdays: [1, 2], - weeks: [ - // month: 7, 8 - { - number: 1, - days: [ - { - date: new NgbDate(2016, 7, 4), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 7, 4), - date: new NgbDate(2016, 7, 4), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Monday', - hidden: true - }, - { - date: new NgbDate(2016, 8, 1), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 8, 1), - date: new NgbDate(2016, 8, 1), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Monday', - hidden: false - } - ], - collapsed: false - }, - // month: 8, 8 - { - number: 2, - days: [ - { - date: new NgbDate(2016, 8, 2), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 8, 2), - date: new NgbDate(2016, 8, 2), - disabled: true, - focused: false, - selected: false, - today: true - }, - tabindex: -1, - ariaLabel: 'Friday', - hidden: false - }, - { - date: new NgbDate(2016, 8, 3), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 8, 3), - date: new NgbDate(2016, 8, 3), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Saturday', - hidden: false - } - ], - collapsed: false - }, - // month: 8, 9 - { - number: 3, - days: [ - { - date: new NgbDate(2016, 8, 4), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 8, 4), - date: new NgbDate(2016, 8, 4), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Sunday', - hidden: false - }, - { - date: new NgbDate(2016, 9, 1), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 9, 1), - date: new NgbDate(2016, 9, 1), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Saturday', - hidden: true - } - ], - collapsed: false - }, - // month: 9, 9 -> to collapse - { - number: 4, - days: [ - { - date: new NgbDate(2016, 9, 2), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 9, 2), - date: new NgbDate(2016, 9, 2), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Sunday', - hidden: true - }, - { - date: new NgbDate(2016, 9, 3), - context: { - currentMonth: 8, - currentYear: 2016, - $implicit: new NgbDate(2016, 9, 3), - date: new NgbDate(2016, 9, 3), - disabled: false, - focused: false, - selected: false, - today: false - }, - tabindex: -1, - ariaLabel: 'Monday', - hidden: true - } - ], - collapsed: true - } - ] - }; - - showWeekdays = true; - showWeekNumbers = true; - outsideDays = 'visible'; - - onClick = () => {}; -} diff --git a/src/datepicker/datepicker-month-view.ts b/src/datepicker/datepicker-month-view.ts deleted file mode 100644 index 8005b3b5d1..0000000000 --- a/src/datepicker/datepicker-month-view.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {Component, Input, TemplateRef, Output, EventEmitter, ViewEncapsulation} from '@angular/core'; -import {MonthViewModel, DayViewModel} from './datepicker-view-model'; -import {NgbDate} from './ngb-date'; -import {NgbDatepickerI18n} from './datepicker-i18n'; -import {DayTemplateContext} from './datepicker-day-template-context'; - -@Component({ - selector: 'ngb-datepicker-month-view', - host: {'role': 'grid'}, - encapsulation: ViewEncapsulation.None, - styleUrls: ['./datepicker-month-view.scss'], - template: ` -
-
-
- {{ i18n.getWeekdayShortName(w) }} -
-
- -
-
{{ i18n.getWeekNumerals(week.number) }}
-
- - - -
-
-
- ` -}) -export class NgbDatepickerMonthView { - @Input() dayTemplate: TemplateRef; - @Input() month: MonthViewModel; - @Input() showWeekdays; - @Input() showWeekNumbers; - - @Output() select = new EventEmitter(); - - constructor(public i18n: NgbDatepickerI18n) {} - - doSelect(day: DayViewModel) { - if (!day.context.disabled && !day.hidden) { - this.select.emit(day.date); - } - } -} diff --git a/src/datepicker/datepicker-month-view.scss b/src/datepicker/datepicker-month.scss similarity index 95% rename from src/datepicker/datepicker-month-view.scss rename to src/datepicker/datepicker-month.scss index 5ac47e299c..a8e4a7c0f2 100644 --- a/src/datepicker/datepicker-month-view.scss +++ b/src/datepicker/datepicker-month.scss @@ -1,4 +1,4 @@ -ngb-datepicker-month-view { +ngb-datepicker-month { display: block; } diff --git a/src/datepicker/datepicker-month.spec.ts b/src/datepicker/datepicker-month.spec.ts new file mode 100644 index 0000000000..b1bcf48bc8 --- /dev/null +++ b/src/datepicker/datepicker-month.spec.ts @@ -0,0 +1,403 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component, Injectable} from '@angular/core'; + +import {NgbDatepickerModule} from './datepicker.module'; +import {NgbDatepicker, NgbDatepickerMonths, NGB_DATEPICKER_VALUE_ACCESSOR} from './datepicker'; +import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service'; +import {NgbDatepickerService} from './datepicker-service'; +import {NgbDatepickerMonth} from './datepicker-month'; +import {NgbDate} from './ngb-date'; +import {NgbDateStruct} from './ngb-date-struct'; +import {NgbDatepickerDayView} from './datepicker-day-view'; + +const createTestComponent = () => createGenericTestComponent( + ` + + {{ date.day }} + +`, + TestComponent) as ComponentFixture; + +function getWeekdays(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-weekday')); +} + +function getWeekNumbers(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-week-number')); +} + +function getDates(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-day')); +} + +function expectWeekdays(element: HTMLElement, weekdays: string[]) { + const result = getWeekdays(element).map(td => td.innerText.trim()); + expect(result).toEqual(weekdays); +} + +function expectWeekNumbers(element: HTMLElement, weeknumbers: string[]) { + const result = getWeekNumbers(element).map(td => td.innerText.trim()); + expect(result).toEqual(weeknumbers); +} + +function expectDates(element: HTMLElement, dates: string[]) { + const result = getDates(element).map(td => td.innerText.trim()); + expect(result).toEqual(dates); +} + +@Injectable() +class MockDatepickerService extends NgbDatepickerService { + getMonth(struct: NgbDateStruct) { + return { + firstDate: new NgbDate(2016, 8, 1), + lastDate: new NgbDate(2016, 8, 31), + year: 2016, + number: 8, + weekdays: [1, 2], + weeks: [ + // month: 7, 8 + { + number: 1, + days: [ + { + date: new NgbDate(2016, 7, 4), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 7, 4), + date: new NgbDate(2016, 7, 4), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Monday', + hidden: true + }, + { + date: new NgbDate(2016, 8, 1), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 8, 1), + date: new NgbDate(2016, 8, 1), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Monday', + hidden: false + } + ], + collapsed: false + }, + // month: 8, 8 + { + number: 2, + days: [ + { + date: new NgbDate(2016, 8, 2), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 8, 2), + date: new NgbDate(2016, 8, 2), + disabled: true, + focused: false, + selected: false, + today: true + }, + tabindex: -1, + ariaLabel: 'Friday', + hidden: false + }, + { + date: new NgbDate(2016, 8, 3), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 8, 3), + date: new NgbDate(2016, 8, 3), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Saturday', + hidden: false + } + ], + collapsed: false + }, + // month: 8, 9 + { + number: 3, + days: [ + { + date: new NgbDate(2016, 8, 4), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 8, 4), + date: new NgbDate(2016, 8, 4), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Sunday', + hidden: false + }, + { + date: new NgbDate(2016, 9, 1), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 9, 1), + date: new NgbDate(2016, 9, 1), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Saturday', + hidden: true + } + ], + collapsed: false + }, + // month: 9, 9 -> to collapse + { + number: 4, + days: [ + { + date: new NgbDate(2016, 9, 2), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 9, 2), + date: new NgbDate(2016, 9, 2), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Sunday', + hidden: true + }, + { + date: new NgbDate(2016, 9, 3), + context: { + currentMonth: 8, + currentYear: 2016, + $implicit: new NgbDate(2016, 9, 3), + date: new NgbDate(2016, 9, 3), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Monday', + hidden: true + } + ], + collapsed: true + } + ] + }; + } +} + +describe('ngb-datepicker-month', () => { + + beforeEach(() => { + TestBed.overrideModule( + NgbDatepickerModule, + {set: {exports: [NgbDatepicker, NgbDatepickerMonths, NgbDatepickerMonth, NgbDatepickerDayView]}}); + TestBed.overrideComponent(NgbDatepicker, { + set: { + providers: [ + NGB_DATEPICKER_VALUE_ACCESSOR, {provide: NgbDatepickerService, useClass: MockDatepickerService}, + NgbDatepickerKeyboardService + ] + } + }); + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [NgbDatepickerModule], + providers: [{provide: NgbDatepickerService, useClass: MockDatepickerService}] + }); + }); + + it('should show/hide weekdays', () => { + const fixture = createTestComponent(); + fixture.componentInstance.showWeekNumbers = false; + fixture.detectChanges(); + + expectWeekdays(fixture.nativeElement, ['Mo', 'Tu']); + + fixture.componentInstance.showWeekdays = false; + fixture.detectChanges(); + expectWeekdays(fixture.nativeElement, []); + }); + + it('should show/hide week numbers', () => { + const fixture = createTestComponent(); + + expectWeekNumbers(fixture.nativeElement, ['1', '2', '3']); + + fixture.componentInstance.showWeekNumbers = false; + fixture.detectChanges(); + expectWeekNumbers(fixture.nativeElement, []); + }); + + it('should use custom template to display dates', () => { + const fixture = createTestComponent(); + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should use "date" as an implicit value for the template', () => { + const fixture = createTestComponent(); + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should send date selection events', () => { + const fixture = createTestComponent(); + + spyOn(fixture.componentInstance, 'onClick'); + + const dates = getDates(fixture.nativeElement); + dates[1].click(); + + expect(fixture.componentInstance.onClick).toHaveBeenCalledWith(new NgbDate(2016, 8, 1)); + }); + + it('should not send date selection events for hidden and disabled dates', () => { + const fixture = createTestComponent(); + + spyOn(fixture.componentInstance, 'onClick'); + + const dates = getDates(fixture.nativeElement); + dates[0].click(); // hidden + dates[2].click(); // disabled + + expect(fixture.componentInstance.onClick).not.toHaveBeenCalled(); + }); + + it('should set cursor to pointer or default', () => { + const fixture = createTestComponent(); + + const dates = getDates(fixture.nativeElement); + // hidden + expect(window.getComputedStyle(dates[0]).getPropertyValue('cursor')).toBe('default'); + // normal + expect(window.getComputedStyle(dates[1]).getPropertyValue('cursor')).toBe('pointer'); + // disabled + expect(window.getComputedStyle(dates[2]).getPropertyValue('cursor')).toBe('default'); + }); + + it('should apply correct CSS classes to days', () => { + const fixture = createTestComponent(); + + let dates = getDates(fixture.nativeElement); + // hidden + expect(dates[0]).toHaveCssClass('hidden'); + expect(dates[0]).not.toHaveCssClass('disabled'); + expect(dates[0]).not.toHaveCssClass('ngb-dp-today'); + // normal + expect(dates[1]).not.toHaveCssClass('hidden'); + expect(dates[1]).not.toHaveCssClass('disabled'); + expect(dates[1]).not.toHaveCssClass('ngb-dp-today'); + // disabled + expect(dates[2]).not.toHaveCssClass('hidden'); + expect(dates[2]).toHaveCssClass('disabled'); + expect(dates[2]).toHaveCssClass('ngb-dp-today'); + }); + + it('should not display collapsed weeks', () => { + const fixture = createTestComponent(); + + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should add correct aria-label attribute', () => { + const fixture = createTestComponent(); + + let dates = getDates(fixture.nativeElement); + expect(dates[0].getAttribute('aria-label')).toBe('Monday'); + }); + + it('should render custom month layout', () => { + const fixture = createGenericTestComponent( + ` + + + + + `, + TestComponent) as ComponentFixture; + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should render custom month template', () => { + const fixture = createGenericTestComponent( + ` + +
Custom Content
+
+ `, + TestComponent) as ComponentFixture; + expectDates(fixture.nativeElement, []); + expect(fixture.nativeElement.querySelectorAll('.customClass').length).toEqual(1); + expect(fixture.nativeElement.querySelectorAll('.customClass')[0].innerText.trim()).toEqual('Custom Content'); + }); + + it('should handle keyboard events with custom month template', () => { + const fixture = createGenericTestComponent( + ` + +
Custom Content
+
+ `, + TestComponent) as ComponentFixture; + expectDates(fixture.nativeElement, []); + expect(fixture.nativeElement.querySelectorAll('.customClass').length).toEqual(1); + expect(fixture.nativeElement.querySelectorAll('.customClass')[0].innerText.trim()).toEqual('Custom Content'); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + showWeekdays = true; + showWeekNumbers = true; + outsideDays = 'visible'; + + onClick = () => {}; +} diff --git a/src/datepicker/datepicker-month.ts b/src/datepicker/datepicker-month.ts new file mode 100644 index 0000000000..fd6a1c7e3e --- /dev/null +++ b/src/datepicker/datepicker-month.ts @@ -0,0 +1,57 @@ +import {Component, Input, ViewEncapsulation} from '@angular/core'; +import {NgbDatepicker} from './datepicker'; +import {NgbDatepickerI18n} from './datepicker-i18n'; +import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service'; +import {NgbDatepickerService} from './datepicker-service'; +import {MonthViewModel, DayViewModel} from './datepicker-view-model'; +import {NgbDateStruct} from './ngb-date-struct'; + +@Component({ + selector: 'ngb-datepicker-month', + host: {'role': 'grid', '(keydown)': 'onKeyDown($event)'}, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./datepicker-month.scss'], + template: ` +
+
+
+ {{ i18n.getWeekdayShortName(w) }} +
+
+ +
+
{{ i18n.getWeekNumerals(week.number) }}
+
+ + + +
+
+
+ ` +}) +export class NgbDatepickerMonth { + @Input() + set month(month: NgbDateStruct) { + this.viewModel = this._service.getMonth(month); + } + + viewModel: MonthViewModel; + + constructor( + public i18n: NgbDatepickerI18n, public datepicker: NgbDatepicker, + private _keyboardService: NgbDatepickerKeyboardService, private _service: NgbDatepickerService) {} + + onKeyDown(event: KeyboardEvent) { this._keyboardService.processKey(event, this.datepicker); } + + doSelect(day: DayViewModel) { + if (!day.context.disabled && !day.hidden) { + this.datepicker.onDateSelect(day.date); + } + } +} diff --git a/src/datepicker/datepicker-service.spec.ts b/src/datepicker/datepicker-service.spec.ts index 62f11c56d0..1552874ce4 100644 --- a/src/datepicker/datepicker-service.spec.ts +++ b/src/datepicker/datepicker-service.spec.ts @@ -649,6 +649,13 @@ describe('ngb-datepicker-service', () => { expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); }); + it(`should throw if registers a month outside range`, () => { + expect(() => { + service.set({minDate: new NgbDate(2017, 5, 1)}); + service.getMonth(new NgbDate(2015, 5, 1)); + }).toThrowError(); + }); + it(`should rebuild 'months' and 'years' only when year change`, () => { service.focus(new NgbDate(2010, 5, 1)); let months = model.selectBoxes.months; diff --git a/src/datepicker/datepicker-service.ts b/src/datepicker/datepicker-service.ts index d942a18d7e..8bfac4889a 100644 --- a/src/datepicker/datepicker-service.ts +++ b/src/datepicker/datepicker-service.ts @@ -159,6 +159,15 @@ export class NgbDatepickerService { return this._calendar.isValid(ngbDate) ? ngbDate : defaultValue; } + getMonth(struct: NgbDateStruct) { + for (let month of this._state.months) { + if (struct.month === month.number && struct.year === month.year) { + return month; + } + } + throw new Error(`month ${struct.month} of year ${struct.year} not found`); + } + private _nextState(patch: Partial) { const newState = this._updateState(patch); this._patchContexts(newState); diff --git a/src/datepicker/datepicker.module.ts b/src/datepicker/datepicker.module.ts index 8feb350a8c..4397d23e83 100644 --- a/src/datepicker/datepicker.module.ts +++ b/src/datepicker/datepicker.module.ts @@ -1,14 +1,14 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {FormsModule} from '@angular/forms'; -import {NgbDatepicker} from './datepicker'; -import {NgbDatepickerMonthView} from './datepicker-month-view'; +import {NgbDatepicker, NgbDatepickerMonths} from './datepicker'; +import {NgbDatepickerMonth} from './datepicker-month'; import {NgbDatepickerNavigation} from './datepicker-navigation'; import {NgbInputDatepicker} from './datepicker-input'; import {NgbDatepickerDayView} from './datepicker-day-view'; import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; -export {NgbDatepicker, NgbDatepickerNavigateEvent, NgbDatepickerState} from './datepicker'; +export {NgbDatepicker, NgbDatepickerMonths, NgbDatepickerNavigateEvent, NgbDatepickerState} from './datepicker'; export {NgbInputDatepicker} from './datepicker-input'; export {NgbCalendar, NgbPeriod, NgbCalendarGregorian} from './ngb-calendar'; export {NgbCalendarIslamicCivil} from './hijri/ngb-calendar-islamic-civil'; @@ -16,7 +16,7 @@ export {NgbCalendarIslamicUmalqura} from './hijri/ngb-calendar-islamic-umalqura' export {NgbCalendarPersian} from './jalali/ngb-calendar-persian'; export {NgbCalendarHebrew} from './hebrew/ngb-calendar-hebrew'; export {NgbDatepickerI18nHebrew} from './hebrew/datepicker-i18n-hebrew'; -export {NgbDatepickerMonthView} from './datepicker-month-view'; +export {NgbDatepickerMonth} from './datepicker-month'; export {NgbDatepickerDayView} from './datepicker-day-view'; export {NgbDatepickerNavigation} from './datepicker-navigation'; export {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; @@ -33,10 +33,10 @@ export {NgbDatepickerKeyboardService} from './datepicker-keyboard-service'; @NgModule({ declarations: [ - NgbDatepicker, NgbDatepickerMonthView, NgbDatepickerNavigation, NgbDatepickerNavigationSelect, NgbDatepickerDayView, - NgbInputDatepicker + NgbDatepicker, NgbDatepickerMonths, NgbDatepickerMonth, NgbDatepickerNavigation, NgbDatepickerNavigationSelect, + NgbDatepickerDayView, NgbInputDatepicker ], - exports: [NgbDatepicker, NgbInputDatepicker], + exports: [NgbDatepicker, NgbDatepickerMonths, NgbInputDatepicker, NgbDatepickerMonth], imports: [CommonModule, FormsModule], entryComponents: [NgbDatepicker] }) diff --git a/src/datepicker/datepicker.scss b/src/datepicker/datepicker.scss index 0cf3ef5619..74af01d39b 100644 --- a/src/datepicker/datepicker.scss +++ b/src/datepicker/datepicker.scss @@ -3,7 +3,7 @@ ngb-datepicker { border-radius: .25rem; display: inline-block; - &-month-view { + &-month { pointer-events: auto; } diff --git a/src/datepicker/datepicker.spec.ts b/src/datepicker/datepicker.spec.ts index 900e64fe36..fcd243faae 100644 --- a/src/datepicker/datepicker.spec.ts +++ b/src/datepicker/datepicker.spec.ts @@ -6,14 +6,13 @@ import {Component, TemplateRef, DebugElement} from '@angular/core'; import {By} from '@angular/platform-browser'; import {FormsModule, ReactiveFormsModule, FormGroup, FormControl, Validators} from '@angular/forms'; -import {NgbCalendar} from './ngb-calendar'; import {NgbDatepickerModule, NgbDatepickerNavigateEvent} from './datepicker.module'; import {NgbDate} from './ngb-date'; import {NgbDatepickerConfig} from './datepicker-config'; import {NgbDatepicker, NgbDatepickerState} from './datepicker'; import {DayTemplateContext} from './datepicker-day-template-context'; import {NgbDateStruct} from './ngb-date-struct'; -import {NgbDatepickerMonthView} from './datepicker-month-view'; +import {NgbDatepickerMonth} from './datepicker-month'; import {NgbDatepickerDayView} from './datepicker-day-view'; import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service'; import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; @@ -66,7 +65,7 @@ function triggerKeyDown(element: DebugElement, keyCode: number, shiftKey = false } function getMonthContainer(datepicker: DebugElement) { - return datepicker.query(By.css('div.ngb-dp-months')); + return datepicker.query(By.css('ngb-datepicker-month')); } function expectSelectedDate(element: DebugElement, selectedDate: NgbDate) { @@ -407,12 +406,12 @@ describe('ngb-datepicker', () => { it('should display multiple months', () => { const fixture = createTestComponent(``); - let months = fixture.debugElement.queryAll(By.directive(NgbDatepickerMonthView)); + let months = fixture.debugElement.queryAll(By.directive(NgbDatepickerMonth)); expect(months.length).toBe(1); fixture.componentInstance.displayMonths = 3; fixture.detectChanges(); - months = fixture.debugElement.queryAll(By.directive(NgbDatepickerMonthView)); + months = fixture.debugElement.queryAll(By.directive(NgbDatepickerMonth)); expect(months.length).toBe(3); }); @@ -1216,10 +1215,9 @@ describe('ngb-datepicker', () => { let mockState: NgbDatepickerState; let dp: NgbDatepicker; - const mockKeyboardService: NgbDatepickerKeyboardService = { - processKey(event: KeyboardEvent, datepicker: NgbDatepicker, calendar: NgbCalendar) { - mockState = datepicker.state; - } + let mv: NgbDatepickerMonth; + const mockKeyboardService: Partial = { + processKey(event: KeyboardEvent, datepicker: NgbDatepicker) { mockState = datepicker.state; } }; beforeEach(() => { @@ -1231,59 +1229,68 @@ describe('ngb-datepicker', () => { ``); fixture.detectChanges(); dp = fixture.debugElement.query(By.directive(NgbDatepicker)).componentInstance; + mv = fixture.debugElement.query(By.css('ngb-datepicker-month')).injector.get(NgbDatepickerMonth); }); it('should provide an defensive copy of minDate', () => { - dp.onKeyDown({}); + mv.onKeyDown({}); expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31})); expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1})); expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31})); Object.assign(mockState, {minDate: undefined}); - dp.onKeyDown({}); + mv.onKeyDown({}); expect(dp.model.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1})); }); it('should provide an defensive copy of maxDate', () => { - dp.onKeyDown({}); + mv.onKeyDown({}); expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31})); expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1})); expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31})); Object.assign(mockState, {maxDate: undefined}); - dp.onKeyDown({}); + mv.onKeyDown({}); expect(dp.model.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31})); }); it('should provide an defensive copy of firstDate', () => { - dp.onKeyDown({}); + mv.onKeyDown({}); expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31})); expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1})); expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31})); Object.assign(mockState, {firstDate: undefined}); - dp.onKeyDown({}); + mv.onKeyDown({}); expect(dp.model.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); }); it('should provide an defensive copy of lastDate', () => { - dp.onKeyDown({}); + mv.onKeyDown({}); expect(mockState.firstDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); expect(mockState.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31})); expect(mockState.minDate).toEqual(NgbDate.from({year: 2010, month: 1, day: 1})); expect(mockState.maxDate).toEqual(NgbDate.from({year: 2020, month: 12, day: 31})); Object.assign(mockState, {lastDate: undefined}); - dp.onKeyDown({}); + mv.onKeyDown({}); expect(dp.model.lastDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 31})); }); it('should provide an defensive copy of focusedDate', () => { - dp.onKeyDown({}); + mv.onKeyDown({}); expect(mockState.focusedDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); Object.assign(mockState, {focusedDate: undefined}); - dp.onKeyDown({}); + mv.onKeyDown({}); expect(dp.model.focusDate).toEqual(NgbDate.from({year: 2016, month: 8, day: 1})); }); + + it('should prevent overriding of calendar', () => { + try { + (dp)['calendar'] = null; + } catch (e) { + } + expect(dp.calendar).toBeTruthy(); + }); }); }); diff --git a/src/datepicker/datepicker.ts b/src/datepicker/datepicker.ts index 8869549e3f..4d32bdaec4 100644 --- a/src/datepicker/datepicker.ts +++ b/src/datepicker/datepicker.ts @@ -1,10 +1,11 @@ import {fromEvent, merge, Subject} from 'rxjs'; import {filter, take, takeUntil} from 'rxjs/operators'; import { - AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, + ContentChild, + Directive, ElementRef, EventEmitter, forwardRef, @@ -23,7 +24,6 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {NgbCalendar} from './ngb-calendar'; import {NgbDate} from './ngb-date'; import {DatepickerServiceInputs, NgbDatepickerService} from './datepicker-service'; -import {NgbDatepickerKeyboardService} from './datepicker-keyboard-service'; import {DatepickerViewModel, NavigationEvent} from './datepicker-view-model'; import {DayTemplateContext} from './datepicker-day-template-context'; import {NgbDatepickerConfig} from './datepicker-config'; @@ -33,7 +33,7 @@ import {NgbDatepickerI18n} from './datepicker-i18n'; import {isChangedDate, isChangedMonth} from './datepicker-tools'; import {hasClassName} from '../util/util'; -const NGB_DATEPICKER_VALUE_ACCESSOR = { +export const NGB_DATEPICKER_VALUE_ACCESSOR = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NgbDatepicker), multi: true @@ -93,6 +93,19 @@ export interface NgbDatepickerState { * The date currently focused by the datepicker */ readonly focusedDate: NgbDate; + + /** + * The first dates of months in range of current datepicker instance + */ + readonly months: NgbDate[]; +} + +/* + * A directive that marks the content template that customizes the way datepicker months are displayed + */ +@Directive({selector: 'ng-template[ngbDatepickerMonths]'}) +export class NgbDatepickerMonths { + constructor(public templateRef: TemplateRef) {} } /** @@ -107,7 +120,7 @@ export interface NgbDatepickerState { encapsulation: ViewEncapsulation.None, styleUrls: ['./datepicker.scss'], template: ` - +
+ +
+
+ {{ i18n.getMonthFullName(month.number, month.year) }} {{ i18n.getYearNumerals(month.year) }} +
+ +
+
+
-
- -
-
- {{ i18n.getMonthFullName(month.number, month.year) }} {{ i18n.getYearNumerals(month.year) }} -
- - -
-
+
+
@@ -154,10 +162,13 @@ export interface NgbDatepickerState { providers: [NGB_DATEPICKER_VALUE_ACCESSOR, NgbDatepickerService] }) export class NgbDatepicker implements OnDestroy, - OnChanges, OnInit, AfterViewInit, ControlValueAccessor { + OnChanges, OnInit, ControlValueAccessor { model: DatepickerViewModel; + @ViewChild('defaultDayTemplate', {static: true}) private _defaultDayTemplate: TemplateRef; @ViewChild('months', {static: true}) private _monthsEl: ElementRef; + @ContentChild(NgbDatepickerMonths, {static: true}) monthsTemplate: NgbDatepickerMonths; + private _controlValue: NgbDate; private _destroyed$ = new Subject(); private _publicState: NgbDatepickerState = {}; @@ -294,9 +305,8 @@ export class NgbDatepicker implements OnDestroy, constructor( private _service: NgbDatepickerService, private _calendar: NgbCalendar, public i18n: NgbDatepickerI18n, - config: NgbDatepickerConfig, private _keyboardService: NgbDatepickerKeyboardService, cd: ChangeDetectorRef, - private _elementRef: ElementRef, private _ngbDateAdapter: NgbDateAdapter, - private _ngZone: NgZone) { + config: NgbDatepickerConfig, cd: ChangeDetectorRef, private _elementRef: ElementRef, + private _ngbDateAdapter: NgbDateAdapter, private _ngZone: NgZone) { ['dayTemplate', 'dayTemplateData', 'displayMonths', 'firstDayOfWeek', 'footerTemplate', 'markDisabled', 'minDate', 'maxDate', 'navigation', 'outsideDays', 'showWeekdays', 'showWeekNumbers', 'startDate'] .forEach(input => this[input] = config[input]); @@ -313,7 +323,8 @@ export class NgbDatepicker implements OnDestroy, minDate: model.minDate, firstDate: model.firstDate, lastDate: model.lastDate, - focusedDate: model.focusDate + focusedDate: model.focusDate, + months: model.months.map(viewModel => viewModel.firstDate) }; let navigationPrevented = false; @@ -361,6 +372,11 @@ export class NgbDatepicker implements OnDestroy, */ get state(): NgbDatepickerState { return this._publicState; } + /** + * Returns the calendar service used in the specific datepicker instance. + */ + get calendar(): NgbCalendar { return this._calendar; } + /** * Focuses on given date. */ @@ -424,6 +440,9 @@ export class NgbDatepicker implements OnDestroy, this.navigateTo(this.startDate); } + if (!this.dayTemplate) { + this.dayTemplate = this._defaultDayTemplate; + } } ngOnChanges(changes: SimpleChanges) { @@ -447,8 +466,6 @@ export class NgbDatepicker implements OnDestroy, this._service.select(date, {emitEvent: true}); } - onKeyDown(event: KeyboardEvent) { this._keyboardService.processKey(event, this, this._calendar); } - onNavigateDateSelect(date: NgbDate) { this._service.open(date); } onNavigateEvent(event: NavigationEvent) { diff --git a/src/index.ts b/src/index.ts index abdd8fa326..874a31a71a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -60,10 +60,12 @@ export { NgbDatepicker, NgbDatepickerConfig, NgbInputDatepickerConfig, + NgbDatepickerMonths, NgbDatepickerI18n, NgbDatepickerI18nHebrew, NgbDatepickerKeyboardService, NgbDatepickerModule, + NgbDatepickerMonth, NgbDatepickerNavigateEvent, NgbDatepickerState, NgbDateStruct,