From 571a00b54aad79d6ab40dbc25826998ea2048841 Mon Sep 17 00:00:00 2001 From: Dmitriy Shekhovtsov Date: Wed, 30 Aug 2017 17:18:01 +0300 Subject: [PATCH] feat(datepicker): added month and year view (#2540) feat(datepicker): added min\max dates boundaries --- .travis.yml | 3 +- .../datepicker-section.component.ts | 6 +- .../demos/bs-popup/date-picker-popup.html | 6 +- .../demos/bs-popup/date-picker-popup.ts | 6 +- demo/src/assets/css/style.css | 3 +- demo/src/ng-api-doc.ts | 22 +- package.json | 24 +- src/bs-moment/format-functions.ts | 2 +- src/bs-moment/types.ts | 4 + src/bs-moment/utils/date-compare.ts | 64 ++++ src/bs-moment/utils/date-getters.ts | 22 +- .../base/bs-datepicker-container.ts | 58 ++++ src/datepicker/bs-datepicker-config.ts | 4 - src/datepicker/bs-datepicker.component.ts | 69 +++-- src/datepicker/bs-datepicker.config.ts | 25 ++ src/datepicker/bs-datepicker.module.ts | 27 +- src/datepicker/bs-datepicker.scss | 143 +++------ .../bs-daterangepicker.component.ts | 56 +++- src/datepicker/engine/calc-days-calendar.ts | 24 ++ src/datepicker/engine/calc-month-view.ts | 26 -- ...ag-month-view.ts => flag-days-calendar.ts} | 51 +-- src/datepicker/engine/flag-months-calendar.ts | 45 +++ src/datepicker/engine/flag-years-calendar.ts | 47 +++ ...-month-view.ts => format-days-calendar.ts} | 16 +- .../engine/format-months-calendar.ts | 27 ++ .../engine/format-years-calendar.ts | 38 +++ src/datepicker/engine/view-mode.ts | 5 + src/datepicker/models/index.ts | 87 ++++-- src/datepicker/reducer/_defaults.ts | 16 +- .../reducer/bs-datepicker.actions.ts | 64 ++-- .../reducer/bs-datepicker.effects.ts | 221 ++++++++++++- .../reducer/bs-datepicker.reducer.ts | 291 +++++++++++++++--- src/datepicker/reducer/bs-datepicker.state.ts | 76 +++-- .../themes/bs/bs-calendar-layout.component.ts | 22 ++ .../bs/bs-current-date-view.component.ts | 9 + .../bs/bs-custom-dates-view.component.ts | 21 ++ .../bs/bs-datepicker-container.component.ts | 137 +++------ .../bs-datepicker-day-decorator.directive.ts | 4 +- .../bs/bs-datepicker-month-view.component.ts | 49 --- ...bs-datepicker-navigation-view.component.ts | 47 ++- .../bs/bs-datepicker-navigation-view.spec.ts | 24 +- .../themes/bs/bs-datepicker-view.component.ts | 49 --- .../themes/bs/bs-datepicker-view.html | 59 ++++ .../bs-daterangepicker-container.component.ts | 148 +++------ .../bs/bs-days-calendar-view.component.ts | 79 +++++ .../bs/bs-months-calendar-view.component.ts | 61 ++++ .../themes/bs/bs-timepicker-view.component.ts | 27 ++ .../bs/bs-years-calendar-view.component.ts | 64 ++++ src/datepicker/utils/bs-calendar-utils.ts | 16 + src/datepicker/utils/matrix-utils.ts | 25 ++ src/datepicker/utils/scss/variables.scss | 4 + tslint.json | 3 +- 52 files changed, 1741 insertions(+), 685 deletions(-) create mode 100644 src/bs-moment/utils/date-compare.ts create mode 100644 src/datepicker/base/bs-datepicker-container.ts delete mode 100644 src/datepicker/bs-datepicker-config.ts create mode 100644 src/datepicker/bs-datepicker.config.ts create mode 100644 src/datepicker/engine/calc-days-calendar.ts delete mode 100644 src/datepicker/engine/calc-month-view.ts rename src/datepicker/engine/{flag-month-view.ts => flag-days-calendar.ts} (53%) create mode 100644 src/datepicker/engine/flag-months-calendar.ts create mode 100644 src/datepicker/engine/flag-years-calendar.ts rename src/datepicker/engine/{format-month-view.ts => format-days-calendar.ts} (68%) create mode 100644 src/datepicker/engine/format-months-calendar.ts create mode 100644 src/datepicker/engine/format-years-calendar.ts create mode 100644 src/datepicker/engine/view-mode.ts create mode 100644 src/datepicker/themes/bs/bs-calendar-layout.component.ts create mode 100644 src/datepicker/themes/bs/bs-current-date-view.component.ts create mode 100644 src/datepicker/themes/bs/bs-custom-dates-view.component.ts delete mode 100644 src/datepicker/themes/bs/bs-datepicker-month-view.component.ts delete mode 100644 src/datepicker/themes/bs/bs-datepicker-view.component.ts create mode 100644 src/datepicker/themes/bs/bs-datepicker-view.html create mode 100644 src/datepicker/themes/bs/bs-days-calendar-view.component.ts create mode 100644 src/datepicker/themes/bs/bs-months-calendar-view.component.ts create mode 100644 src/datepicker/themes/bs/bs-timepicker-view.component.ts create mode 100644 src/datepicker/themes/bs/bs-years-calendar-view.component.ts create mode 100644 src/datepicker/utils/matrix-utils.ts diff --git a/.travis.yml b/.travis.yml index b532e7da1e..29d95d87a1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,8 +20,7 @@ script: - npm i ./dist - npm run demo.build # istanbul is broken, should be fixed - #- npm run test-coverage - - ./node_modules/.bin/ng test -sr + - npm run test-coverage after_success: - ./node_modules/.bin/codecov diff --git a/demo/src/app/components/+datepicker/datepicker-section.component.ts b/demo/src/app/components/+datepicker/datepicker-section.component.ts index 84b12408e1..a32c8320e9 100644 --- a/demo/src/app/components/+datepicker/datepicker-section.component.ts +++ b/demo/src/app/components/+datepicker/datepicker-section.component.ts @@ -36,11 +36,11 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');

Notebale change is additional css for it "/datepicker/bs-datepicker.css"

In nearest time will be added:

diff --git a/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.html b/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.html index 8059e3e166..36f7b343d7 100644 --- a/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.html +++ b/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.html @@ -1,10 +1,12 @@
{{bsValue}}
-
@@ -13,9 +15,9 @@
{{bsRangeValue}}
- diff --git a/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.ts b/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.ts index c10dea6af9..334bbf64ec 100644 --- a/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.ts +++ b/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.ts @@ -5,6 +5,8 @@ import { Component } from '@angular/core'; templateUrl: './date-picker-popup.html' }) export class DemoDatePickerPopupComponent { - public bsValue: any ; - public bsRangeValue: any = [new Date(2017, 7, 4), new Date(2017, 7, 20)]; + minDate = new Date(2017, 5, 10); + maxDate = new Date(2018, 9, 15); + bsValue: any ; + bsRangeValue: any = [new Date(2017, 7, 4), new Date(2017, 7, 20)]; } diff --git a/demo/src/assets/css/style.css b/demo/src/assets/css/style.css index e3d0918931..b1d458a27b 100644 --- a/demo/src/assets/css/style.css +++ b/demo/src/assets/css/style.css @@ -150,7 +150,7 @@ a:hover { position: fixed; bottom: 0; left: 0; - z-index: 999; + /*z-index: 999;*/ -webkit-transition: left .5s ease; -moz-transition: left .5s ease; -ms-transition: left .5s ease; @@ -585,6 +585,7 @@ a:hover { .isOpenMenu #main-menu { left: 0; + z-index: 999; } #main { diff --git a/demo/src/ng-api-doc.ts b/demo/src/ng-api-doc.ts index 2af2df9573..23548dd4e6 100644 --- a/demo/src/ng-api-doc.ts +++ b/demo/src/ng-api-doc.ts @@ -67,17 +67,17 @@ export const ngdoc: any = { "description": "", "selector": "alert,ngx-alert", "inputs": [ + { + "name": "dismissOnTimeout", + "type": "string | number", + "description": "

Number in milliseconds, after which alert will be closed

\n" + }, { "name": "dismissible", "defaultValue": "false", "type": "boolean", "description": "

If set, displays an inline "Close" button

\n" }, - { - "name": "dismissOnTimeout", - "type": "string | number", - "description": "

Number in milliseconds, after which alert will be closed

\n" - }, { "name": "type", "defaultValue": "warning", @@ -111,18 +111,18 @@ export const ngdoc: any = { "description": "", "methods": [], "properties": [ - { - "name": "dismissible", - "defaultValue": "false", - "type": "boolean", - "description": "

is alerts are dismissible by default

\n" - }, { "name": "dismissOnTimeout", "defaultValue": "undefined", "type": "number", "description": "

default time before alert will dismiss

\n" }, + { + "name": "dismissible", + "defaultValue": "false", + "type": "boolean", + "description": "

is alerts are dismissible by default

\n" + }, { "name": "type", "defaultValue": "warning", diff --git a/package.json b/package.json index 14d19589b6..4d5170ad8c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "disable-lint": "tslint \"**/*.ts\" -c tslint.json --fix --type-check -t prose -e \"node_modules/**\" -e \"dist/**\" -e \"temp/**\" -e \"scripts/docs/**\"", "flow.changelog": "conventional-changelog -i CHANGELOG.md -s -p angular", "flow.github-release": "conventional-github-releaser -p angular", - "build": "ngm build -p src --clean", + "build": "run-s build.ngm build.sass", + "build.sass": "node-sass --recursive src --output dist --source-map true --source-map-contents sass", + "build.ngm": "ngm build -p src --clean", "build.watch": "ngm build -p src --watch --skip-bundles", "start": "ng serve --aot --host 0.0.0.0", "generate-bs4": "node scripts/generate-bs4.js", @@ -67,7 +69,7 @@ "@angular/forms": "^2.3.1 || >=4.0.0" }, "devDependencies": { - "@angular/cli": "1.3.1", + "@angular/cli": "1.3.2", "@angular/common": "^2.4.4", "@angular/compiler": "^2.4.4", "@angular/compiler-cli": "^2.4.4", @@ -79,10 +81,10 @@ "@angular/platform-browser-dynamic": "^2.4.4", "@angular/router": "^3.4.4", "@angular/tsc-wrapped": "0.5.1", - "@types/jasmine": "2.5.53", + "@types/jasmine": "2.5.54", "@types/marked": "0.3.0", - "@types/node": "8.0.24", - "@types/webpack": "3.0.9", + "@types/node": "8.0.25", + "@types/webpack": "3.0.10", "bootstrap": "3.3.7", "classlist-polyfill": "1.2.0", "codecov": "2.3.0", @@ -97,8 +99,8 @@ "google-code-prettify": "1.0.5", "html-loader": "0.5.1", "intl": "^1.2.5", - "jasmine": "2.7.0", - "jasmine-core": "2.7.0", + "jasmine": "2.8.0", + "jasmine-core": "2.8.0", "jasmine-data-provider": "2.2.0", "jasmine-spec-reporter": "4.2.1", "karma": "1.7.0", @@ -108,26 +110,26 @@ "karma-jasmine": "^1.0.2", "karma-jasmine-html-reporter": "^0.2.2", "karma-remap-istanbul": "0.6.0", - "karma-sauce-launcher": "1.1.0", + "karma-sauce-launcher": "1.2.0", "lite-server": "2.3.0", "lodash": "4.17.4", "markdown-loader": "github:valorkin/markdown-loader", "marked": "0.3.6", "ng2-page-scroll": "4.0.0-beta.7", "ngm-cli": "0.6.1", - "npm-run-all": "4.0.2", + "npm-run-all": "4.1.1", "protractor": "5.1.2", "reflect-metadata": "0.1.10", "require-dir": "0.3.2", "rxjs": "5.4.3", "ts-helpers": "^1.1.1", - "tslint": "5.6.0", + "tslint": "5.7.0", "tslint-config-valorsoft": "2.1.0", "typedoc": "0.8.0", "typescript": "2.4.2", "wallaby-webpack": "0.0.39", "webdriver-manager": "12.0.6", "webpack-bundle-analyzer": "2.9.0", - "zone.js": "0.8.16" + "zone.js": "0.8.17" } } diff --git a/src/bs-moment/format-functions.ts b/src/bs-moment/format-functions.ts index db9db9df32..28d35689cc 100644 --- a/src/bs-moment/format-functions.ts +++ b/src/bs-moment/format-functions.ts @@ -1,7 +1,7 @@ import { Locale } from './locale/locale.class'; import { zeroFill } from './utils'; import { isFunction } from './utils/type-checks'; -import { DateFormatterFn } from '../datepicker/models/index'; +import { DateFormatterFn } from './types'; export let formatFunctions: { [key: string]: (date: Date, locale: Locale) => string } = {}; export let formatTokenFunctions: { [key: string]: DateFormatterFn } = {}; diff --git a/src/bs-moment/types.ts b/src/bs-moment/types.ts index 4d4975ca34..af20250610 100644 --- a/src/bs-moment/types.ts +++ b/src/bs-moment/types.ts @@ -1,3 +1,5 @@ +import { Locale } from './locale/locale.class'; + export type UnitOfTime = 'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'seconds' | 'milliseconds'; @@ -9,3 +11,5 @@ export interface TimeUnit { minute?: number; seconds?: number; } + +export type DateFormatterFn = (date: Date, format: string, locale?: Locale) => string; diff --git a/src/bs-moment/utils/date-compare.ts b/src/bs-moment/utils/date-compare.ts new file mode 100644 index 0000000000..3b506930c4 --- /dev/null +++ b/src/bs-moment/utils/date-compare.ts @@ -0,0 +1,64 @@ +import { UnitOfTime } from '../types'; +import { endOf, startOf } from './start-end-of'; + +export function isAfter(date1: Date, date2: Date, units: UnitOfTime = 'milliseconds'): boolean { + if (!date1 || !date2) { + return false; + } + + if (units === 'milliseconds') { + return date1.valueOf() > date2.valueOf(); + } + + return date2.valueOf() < startOf(date1, units).valueOf(); +} + +export function isBefore(date1: Date, date2: Date, units: UnitOfTime = 'milliseconds'): boolean { + if (!date1 || !date2) { + return false; + } + + if (units === 'milliseconds') { + return date1.valueOf() < date2.valueOf(); + } + + return endOf(date1, units).valueOf() < date2.valueOf(); +} + +export function isBetween(date: Date, + from: Date, + to: Date, + units: UnitOfTime, + inclusivity = '()'): boolean { + const leftBound = inclusivity[0] === '(' + ? isAfter(date, from, units) : + !isBefore(date, from, units); + const rightBound = inclusivity[1] === ')' + ? isBefore(date, to, units) + : !isAfter(date, to, units); + + return leftBound && rightBound; +} + +export function isSame(date1: Date, date2: Date, units: UnitOfTime = 'milliseconds'): boolean { + if (!date1 || !date2) { + return false; + } + + if (units === 'milliseconds') { + return date1.valueOf() === date2.valueOf(); + } + + const inputMs = date2.valueOf(); + + return startOf(date1, units).valueOf() <= inputMs + && inputMs <= endOf(date1, units).valueOf(); +} + +export function isSameOrAfter(date1: Date, date2: Date, units?: UnitOfTime): boolean { + return isSame(date1, date2, units) || isAfter(date1, date2, units); +} + +export function isSameOrBefore(date1: Date, date2: Date, units?: UnitOfTime): boolean { + return isSame(date1, date2, units) || isBefore(date1, date2, units); +} diff --git a/src/bs-moment/utils/date-getters.ts b/src/bs-moment/utils/date-getters.ts index 7b56026f3a..282d2694b9 100644 --- a/src/bs-moment/utils/date-getters.ts +++ b/src/bs-moment/utils/date-getters.ts @@ -1,4 +1,3 @@ - import { createDate } from './date-setters'; export function getHours(date: Date, isUTC = false): number { @@ -47,6 +46,23 @@ export function isFirstDayOfWeek(date: Date, firstDayOfWeek: number): boolean { } export function isSameMonth(date1: Date, date2: Date) { - if (!date1 || !date2) {return false;} - return getFullYear(date1) === getFullYear(date2) && getMonth(date1) === getMonth(date2); + if (!date1 || !date2) {return false; } + + return isSameYear(date1, date2) && getMonth(date1) === getMonth(date2); +} + +export function isSameYear(date1: Date, date2: Date) { + if (!date1 || !date2) {return false; } + + return getFullYear(date1) === getFullYear(date2); } + +export function isSameDay(date1: Date, date2: Date): boolean { + if (!date1 || !date2) { + return false; + } + + return isSameYear(date1, date2) && isSameMonth(date1, date2) + && getDate(date1) === getDate(date2); +} + diff --git a/src/datepicker/base/bs-datepicker-container.ts b/src/datepicker/base/bs-datepicker-container.ts new file mode 100644 index 0000000000..11085fca03 --- /dev/null +++ b/src/datepicker/base/bs-datepicker-container.ts @@ -0,0 +1,58 @@ +// datepicker container component +/* tslint:disable no-empty */ +import { BsCustomDates } from '../themes/bs/bs-custom-dates-view.component'; +import { BsDatepickerEffects } from '../reducer/bs-datepicker.effects'; +import { Observable } from 'rxjs/Observable'; +import { + BsDatepickerViewMode, BsNavigationEvent, CalendarCellViewModel, CellHoverEvent, DatepickerRenderOptions, + DaysCalendarViewModel, DayViewModel, + MonthsCalendarViewModel, + YearsCalendarViewModel +} from '../models/index'; + +export abstract class BsDatepickerContainer { + /** @deperecated */ + _customRangesFish: BsCustomDates[] = [ + {label: 'today', value: new Date()}, + {label: 'today1', value: new Date()}, + {label: 'today2', value: new Date()}, + {label: 'today3', value: new Date()} + ]; + + _effects: BsDatepickerEffects; + + set minDate(value: Date) { + this._effects.setMinDate(value); + } + + set maxDate(value: Date) { + this._effects.setMaxDate(value); + } + + + viewMode: Observable; + daysCalendar: Observable; + monthsCalendar: Observable; + yearsCalendar: Observable; + options: Observable; + + setViewMode(event: BsDatepickerViewMode): void {} + + navigateTo(event: BsNavigationEvent): void {} + + dayHoverHandler(event: CellHoverEvent): void {} + + monthHoverHandler(event: CellHoverEvent): void {} + + yearHoverHandler(event: CellHoverEvent): void {} + + daySelectHandler(day: DayViewModel): void {}; + + monthSelectHandler(event: CalendarCellViewModel): void {} + + yearSelectHandler(event: CalendarCellViewModel): void {} + + _stopPropagation(event: any): void { + event.stopPropagation(); + } +} diff --git a/src/datepicker/bs-datepicker-config.ts b/src/datepicker/bs-datepicker-config.ts deleted file mode 100644 index 41d7a6847f..0000000000 --- a/src/datepicker/bs-datepicker-config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injectable } from '@angular/core'; - -@Injectable() -export class BsDatepickerConfig {} diff --git a/src/datepicker/bs-datepicker.component.ts b/src/datepicker/bs-datepicker.component.ts index b3149a2855..e787cb0895 100644 --- a/src/datepicker/bs-datepicker.component.ts +++ b/src/datepicker/bs-datepicker.component.ts @@ -2,11 +2,11 @@ import { Component, ComponentRef, ElementRef, EventEmitter, - Input, + Input, OnChanges, OnDestroy, OnInit, Output, - Renderer, + Renderer, SimpleChanges, ViewContainerRef } from '@angular/core'; import { ComponentLoader } from '../component-loader/component-loader.class'; @@ -14,13 +14,14 @@ import { ComponentLoaderFactory } from '../component-loader/component-loader.fac import { BsDatepickerContainerComponent } from './themes/bs/bs-datepicker-container.component'; import { Subscription } from 'rxjs/Subscription'; import 'rxjs/add/operator/filter'; +import { BsDatepickerConfig } from './bs-datepicker.config'; @Component({ selector: 'bs-datepicker,[bsDatepicker]', exportAs: 'bsDatepicker', template: ' ' }) -export class BsDatepickerComponent implements OnInit, OnDestroy { +export class BsDatepickerComponent implements OnInit, OnDestroy, OnChanges { /** * Placement of a popover. Accepts: "top", "bottom", "left", "right" */ @@ -59,7 +60,6 @@ export class BsDatepickerComponent implements OnInit, OnDestroy { */ @Output() onHidden: EventEmitter; - // here will be parsed options and set defaults // @Input() config: BsDatePickerOptions; // configChange: EventEmitter = new EventEmitter(); @@ -71,25 +71,50 @@ export class BsDatepickerComponent implements OnInit, OnDestroy { this.bsValueChange.emit(value); } + @Input() minDate: Date; + @Input() maxDate: Date; + @Output() bsValueChange: EventEmitter = new EventEmitter(); - protected subscriptions: Subscription[] = []; + protected _subs: Subscription[] = []; private _datepicker: ComponentLoader; private _datepickerRef: ComponentRef; - constructor(_elementRef: ElementRef, + constructor(private _config: BsDatepickerConfig, + _elementRef: ElementRef, _renderer: Renderer, _viewContainerRef: ViewContainerRef, cis: ComponentLoaderFactory) { + Object.assign(this, this._config); this._datepicker = cis .createLoader(_elementRef, _viewContainerRef, _renderer); - // .provide({provide: PopoverConfig, useValue: _config}); - // Object.assign(this, _config); this.onShown = this._datepicker.onShown; this.onHidden = this._datepicker.onHidden; } + ngOnInit(): any { + this._datepicker.listen({ + outsideClick: this.outsideClick, + triggers: this.triggers, + show: () => this.show() + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (!this._datepickerRef || !this._datepickerRef.instance) { + return; + } + + if (changes.minDate) { + this._datepickerRef.instance.minDate = this.minDate; + } + + if (changes.maxDate) { + this._datepickerRef.instance.maxDate = this.maxDate; + } + } + /** * Opens an element’s datepicker. This is considered a “manual” triggering of * the datepicker. @@ -99,23 +124,26 @@ export class BsDatepickerComponent implements OnInit, OnDestroy { return; } + const config = Object.assign({}, this._config, { + value: this._bsValue, + minDate: this.minDate || this._config.minDate, + maxDate: this.maxDate || this._config.maxDate + }); + this._datepickerRef = this._datepicker + .provide({provide: BsDatepickerConfig, useValue: config}) .attach(BsDatepickerContainerComponent) .to(this.container) .position({attachment: this.placement}) .show({placement: this.placement}); - // link with datepicker - // set initial value of picker - this._datepickerRef.instance.value = this._bsValue; - // if date changes from external source (model -> view) - this.bsValueChange.subscribe((value: Date) => { + this._subs.push(this.bsValueChange.subscribe((value: Date) => { this._datepickerRef.instance.value = value; - }); + })); // if date changes from picker (view -> model) - this.subscriptions.push(this._datepickerRef.instance + this._subs.push(this._datepickerRef.instance .valueChange.subscribe((value: Date) => { this.bsValue = value; this.hide(); @@ -130,6 +158,9 @@ export class BsDatepickerComponent implements OnInit, OnDestroy { if (this.isOpen) { this._datepicker.hide(); } + for (const sub of this._subs) { + sub.unsubscribe(); + } } /** @@ -144,14 +175,6 @@ export class BsDatepickerComponent implements OnInit, OnDestroy { this.show(); } - ngOnInit(): any { - this._datepicker.listen({ - outsideClick: this.outsideClick, - triggers: this.triggers, - show: () => this.show() - }); - } - ngOnDestroy(): any { this._datepicker.dispose(); } diff --git a/src/datepicker/bs-datepicker.config.ts b/src/datepicker/bs-datepicker.config.ts new file mode 100644 index 0000000000..715db9529d --- /dev/null +++ b/src/datepicker/bs-datepicker.config.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { DatepickerFormatOptions, DatepickerRenderOptions } from './models/index'; + +@Injectable() +export class BsDatepickerConfig implements DatepickerRenderOptions, + DatepickerFormatOptions { + + value?: Date | Date[]; + minDate?: Date; + maxDate?: Date; + + // DatepickerRenderOptions + displayMonths = 1; + showWeekNumbers = true; + + // DatepickerFormatOptions + locale = 'en'; + monthTitle = 'MMMM'; + yearTitle = 'YYYY'; + dayLabel = 'D'; + monthLabel = 'MMMM'; + yearLabel = 'YYYY'; + weekNumbers = 'w'; + +} diff --git a/src/datepicker/bs-datepicker.module.ts b/src/datepicker/bs-datepicker.module.ts index ec8672d8de..7723035f04 100644 --- a/src/datepicker/bs-datepicker.module.ts +++ b/src/datepicker/bs-datepicker.module.ts @@ -3,10 +3,8 @@ import { isDevMode, ModuleWithProviders, NgModule } from '@angular/core'; import { BsDatepickerActions } from './reducer/bs-datepicker.actions'; import { BsDatepickerStore } from './reducer/bs-datepicker.store'; import { BsDatepickerContainerComponent } from './themes/bs/bs-datepicker-container.component'; -import { BsDatepickerMonthViewComponent } from './themes/bs/bs-datepicker-month-view.component'; import { BsDatepickerNavigationViewComponent } from './themes/bs/bs-datepicker-navigation-view.component'; -import { BsDatepickerViewComponent } from './themes/bs/bs-datepicker-view.component'; -import { BsDatepickerConfig } from './bs-datepicker-config'; +import { BsDaysCalendarViewComponent } from './themes/bs/bs-days-calendar-view.component'; import { BsDatepickerEffects } from './reducer/bs-datepicker.effects'; import { BsDaterangepickerContainerComponent } from './themes/bs/bs-daterangepicker-container.component'; import { BsDaterangepickerComponent } from './bs-daterangepicker.component'; @@ -14,16 +12,32 @@ import { BsDatepickerComponent } from './bs-datepicker.component'; import { ComponentLoaderFactory } from '../component-loader/component-loader.factory'; import { PositioningService } from '../positioning/positioning.service'; import { BsDatepickerDayDecoratorComponent } from './themes/bs/bs-datepicker-day-decorator.directive'; +import { BsMonthCalendarViewComponent } from './themes/bs/bs-months-calendar-view.component'; +import { BsYearsCalendarViewComponent } from './themes/bs/bs-years-calendar-view.component'; +import { BsCustomDatesViewComponent } from './themes/bs/bs-custom-dates-view.component'; +import { BsCurrentDateViewComponent } from './themes/bs/bs-current-date-view.component'; +import { BsTimepickerViewComponent } from './themes/bs/bs-timepicker-view.component'; +import { BsDatepickerConfig } from './bs-datepicker.config'; +import { BsCalendarLayoutComponent } from './themes/bs/bs-calendar-layout.component'; @NgModule({ imports: [CommonModule], declarations: [ - BsDatepickerMonthViewComponent, - BsDatepickerViewComponent, - BsDatepickerNavigationViewComponent, BsDatepickerDayDecoratorComponent, + BsCurrentDateViewComponent, + BsDatepickerNavigationViewComponent, + BsTimepickerViewComponent, + + BsCalendarLayoutComponent, + BsDaysCalendarViewComponent, + BsMonthCalendarViewComponent, + BsYearsCalendarViewComponent, + + BsCustomDatesViewComponent, + BsDatepickerContainerComponent, BsDaterangepickerContainerComponent, + BsDatepickerComponent, BsDaterangepickerComponent ], @@ -39,6 +53,7 @@ export class BsDatepickerModule { PLEASE, read changelog`); } } + static forRoot(): ModuleWithProviders { return { ngModule: BsDatepickerModule, diff --git a/src/datepicker/bs-datepicker.scss b/src/datepicker/bs-datepicker.scss index 88b3c36e82..8de51dc523 100644 --- a/src/datepicker/bs-datepicker.scss +++ b/src/datepicker/bs-datepicker.scss @@ -3,15 +3,15 @@ /* .bs-datepicker */ .bs-datepicker { - display: inline-block; - vertical-align: top; - min-width: 279px; - min-height: 250px; + display: flex; + align-items: stretch; + flex-flow: row wrap; background: $main-bg; - box-shadow: 0 10px 20px rgba(84, 112, 139, 0.1); + box-shadow: 0 0 10px 0 $main-box-shadow; position: relative; z-index: 1; + &:after { clear: both; content: ''; @@ -97,27 +97,14 @@ padding: 0 13px; } } - - /* .bs-datepicker-head.years button.current, - .bs-datepicker-head.months button.current */ - &.years, - &.months { - button.current { - width: 155px; - padding: 0; - } - } } - &-head, - &-btns { + &-head { button { - &:hover, - &.colored:hover:after { + &:hover { background-color: rgba(0, 0, 0, 0.1); } - :active, - &.colored:active:after { + &:active { background-color: rgba(0, 0, 0, 0.2); } } @@ -127,7 +114,8 @@ &-body { padding: 10px; border-radius: 0 0 3px 3px; - min-height: 230px; + min-height: 232px; + min-width: 278px; border: 1px solid $border-color; .days.weeks { @@ -158,9 +146,6 @@ display: block; margin: 0 auto; font-size: 13px; - width: 32px; - height: 32px; - line-height: 32px; border-radius: 50%; position: relative; /*z-index: 1;*/ @@ -258,6 +243,10 @@ } span { + width: 32px; + height: 32px; + line-height: 32px; + &.select-start { z-index: 2; } @@ -305,7 +294,6 @@ } } - &.months, &.years { td { span { @@ -324,28 +312,20 @@ } } } - } - } - /* .bs-timepicker */ - &.bs-timepicker { - &:after { - content: ''; - display: block; - clear: both; - } + &.months { + td { + height: 52px; - .bs-datepicker-body { - border: 1px solid $border-color; - float: left; + span { + padding: 6px; + border-radius: 15px; + } + } + } } } - &.bs-timepicker, - &.bs-padding { - padding: 15px; - } - /* .current-timedate */ .current-timedate { color: $font-color-03; @@ -376,9 +356,11 @@ /* .bs-datepicker-multiple */ &-multiple { display: inline-block; - border-radius: 4px; - box-shadow: 0 3px 11px rgba(33, 37, 39, 0.2); - background-color: $main-bg; + border-radius: 4px 0 0 4px; + + & + & { + margin-left: 2px; + } .bs-datepicker { box-shadow: none; @@ -410,58 +392,18 @@ } /* .bs-datepicker-btns */ - &-container + &-btns { - border-top: 1px solid $border-color; + &-container { + padding: 15px; } - &-btns { - padding: 10px 0; - text-align: right; - clear: both; - border-top: 1px solid $border-color; - - button { - padding: 0 16px; - border: 0; - border-radius: 15px; - height: 30px; - color: $font-color-03; - position: relative; - - &:after { - content: ""; - transition: 0.3s; - border-radius: 15px; - width: 100%; - height: 100%; - position: absolute; - top: 0; - left: 0; - } - - span { - position: relative; - z-index: 1; - } - - &.colored { - color: $font-color-01; - } - - &:not(.colored) { - background: transparent; - - &:hover { - text-decoration: underline; - } - } - } + /*.bs-datepicker-custom-range */ + &-custom-range { + padding: 15px; + background: $custom-range-bg; } /* .bs-datepicker-predefined-btns */ &-predefined-btns { - padding: 15px; - button { width: 100%; display: block; @@ -476,7 +418,7 @@ transition: 0.3s; &:active, - &hover { + &:hover { background-color: $btn-bg2-hover; } } @@ -486,12 +428,25 @@ .is-other-month { color: rgba(0, 0, 0, 0.25); } + + /* .bs-datepicker-buttons */ + &-buttons { + display: flex; + flex-flow: row wrap; + justify-content: flex-end; + padding-top: 10px; + border-top: 1px solid $border-color; + + .btn-default { + margin-left: 10px; + } + } } /* .bs-timepicker */ .bs-timepicker { &-container { - text-align: center; + padding: 10px 0; } &-label { diff --git a/src/datepicker/bs-daterangepicker.component.ts b/src/datepicker/bs-daterangepicker.component.ts index 845d707988..c50ea9d2b0 100644 --- a/src/datepicker/bs-daterangepicker.component.ts +++ b/src/datepicker/bs-daterangepicker.component.ts @@ -1,19 +1,20 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ComponentRef, ElementRef, Renderer, - ViewContainerRef + ViewContainerRef, SimpleChanges, OnChanges } from '@angular/core'; import { BsDaterangepickerContainerComponent } from './themes/bs/bs-daterangepicker-container.component'; import { Subscription } from 'rxjs/Subscription'; import { ComponentLoaderFactory } from '../component-loader/component-loader.factory'; import { ComponentLoader } from '../component-loader/component-loader.class'; +import { BsDatepickerConfig } from './bs-datepicker.config'; @Component({ selector: 'bs-daterangepicker,[bsDaterangepicker]', exportAs: 'bsDaterangepicker', template: ' ' }) -export class BsDaterangepickerComponent implements OnInit, OnDestroy { +export class BsDaterangepickerComponent implements OnInit, OnDestroy, OnChanges { /** * Placement of a popover. Accepts: "top", "bottom", "left", "right" */ @@ -64,6 +65,9 @@ export class BsDaterangepickerComponent implements OnInit, OnDestroy { this.bsValueChange.emit(value); } + @Input() minDate: Date; + @Input() maxDate: Date; + @Output() bsValueChange: EventEmitter = new EventEmitter(); protected subscriptions: Subscription[] = []; @@ -71,18 +75,40 @@ export class BsDaterangepickerComponent implements OnInit, OnDestroy { private _datepicker: ComponentLoader; private _datepickerRef: ComponentRef; - constructor(_elementRef: ElementRef, + constructor(private _config: BsDatepickerConfig, + _elementRef: ElementRef, _renderer: Renderer, _viewContainerRef: ViewContainerRef, cis: ComponentLoaderFactory) { this._datepicker = cis .createLoader(_elementRef, _viewContainerRef, _renderer); - // .provide({provide: PopoverConfig, useValue: _config}); - // Object.assign(this, _config); + Object.assign(this, _config); this.onShown = this._datepicker.onShown; this.onHidden = this._datepicker.onHidden; } + ngOnInit(): any { + this._datepicker.listen({ + outsideClick: this.outsideClick, + triggers: this.triggers, + show: () => this.show() + }); + } + + ngOnChanges(changes: SimpleChanges): void { + if (!this._datepickerRef || !this._datepickerRef.instance) { + return; + } + + if (changes.minDate) { + this._datepickerRef.instance.minDate = this.minDate; + } + + if (changes.maxDate) { + this._datepickerRef.instance.maxDate = this.maxDate; + } + } + /** * Opens an element’s datepicker. This is considered a “manual” triggering of * the datepicker. @@ -92,16 +118,20 @@ export class BsDaterangepickerComponent implements OnInit, OnDestroy { return; } + const config = Object.assign({}, this._config, { + displayMonths: 2, + value: this._bsValue, + minDate: this.minDate || this._config.minDate, + maxDate: this.maxDate || this._config.maxDate + }); + this._datepickerRef = this._datepicker + .provide({provide: BsDatepickerConfig, useValue: config}) .attach(BsDaterangepickerContainerComponent) .to(this.container) .position({attachment: this.placement}) .show({placement: this.placement}); - // link with datepicker - // set initial value of picker - this._datepickerRef.instance.value = this._bsValue; - // if date changes from external source (model -> view) this.subscriptions.push(this.bsValueChange.subscribe((value: Date[]) => { this._datepickerRef.instance.value = value; @@ -139,14 +169,6 @@ export class BsDaterangepickerComponent implements OnInit, OnDestroy { this.show(); } - ngOnInit(): any { - this._datepicker.listen({ - outsideClick: this.outsideClick, - triggers: this.triggers, - show: () => this.show() - }); - } - ngOnDestroy(): any { this._datepicker.dispose(); } diff --git a/src/datepicker/engine/calc-days-calendar.ts b/src/datepicker/engine/calc-days-calendar.ts new file mode 100644 index 0000000000..175955f6cf --- /dev/null +++ b/src/datepicker/engine/calc-days-calendar.ts @@ -0,0 +1,24 @@ +// user and model input should handle parsing and validating input values +// should accept some options +// todo: split out formatting +import { DaysCalendarModel, MonthViewOptions } from '../models/index'; +import { getFirstDayOfMonth } from '../../bs-moment/utils/date-getters'; +import { getStartingDayOfCalendar } from '../utils/bs-calendar-utils'; +import { createMatrix } from '../utils/matrix-utils'; + +export function calcDaysCalendar(startingDate: Date, options: MonthViewOptions): DaysCalendarModel { + const firstDay = getFirstDayOfMonth(startingDate); + const initialDate = getStartingDayOfCalendar(firstDay, options); + + const matrixOptions = { + width: options.width, + height: options.height, + initialDate, shift: {day: 1} + }; + const daysMatrix = createMatrix(matrixOptions, date => date); + + return { + daysMatrix, + month: firstDay + }; +} diff --git a/src/datepicker/engine/calc-month-view.ts b/src/datepicker/engine/calc-month-view.ts deleted file mode 100644 index 982f68bb5f..0000000000 --- a/src/datepicker/engine/calc-month-view.ts +++ /dev/null @@ -1,26 +0,0 @@ -// user and model input should handle parsing and validating input values -// should accept some options -// todo: split out formatting -import { DaysCalendarModel, MonthViewOptions } from '../models/index'; -import { getFirstDayOfMonth } from '../../bs-moment/utils/date-getters'; -import { getStartingDayOfCalendar } from '../utils/bs-calendar-utils'; -import { shiftDate } from '../../bs-moment/utils/date-setters'; - -export function calculateMonthModel(date: Date, options: MonthViewOptions): DaysCalendarModel { - const firstDay = getFirstDayOfMonth(date); - - let prevValue = getStartingDayOfCalendar(firstDay, options); - const daysCalendar = new Array(options.height); - for (let i = 0; i < options.height; i++) { - daysCalendar[i] = new Array(options.width); - for (let j = 0; j < options.width; j++) { - daysCalendar[i][j] = prevValue; - prevValue = shiftDate(prevValue, {day: 1}); - } - } - - return { - daysMatrix: daysCalendar, - month: firstDay - }; -} diff --git a/src/datepicker/engine/flag-month-view.ts b/src/datepicker/engine/flag-days-calendar.ts similarity index 53% rename from src/datepicker/engine/flag-month-view.ts rename to src/datepicker/engine/flag-days-calendar.ts index eccd677603..ed8c4a0086 100644 --- a/src/datepicker/engine/flag-month-view.ts +++ b/src/datepicker/engine/flag-days-calendar.ts @@ -1,7 +1,12 @@ -import { DayViewModel, MonthViewModel, WeekViewModel } from '../models/index'; -import { isSameMonth } from '../../bs-moment/utils/date-getters'; +import { DayViewModel, DaysCalendarViewModel, WeekViewModel } from '../models/index'; +import { isSameDay, isSameMonth } from '../../bs-moment/utils/date-getters'; +import { isSameOrAfter, isSameOrBefore } from '../../bs-moment/utils/date-compare'; +import { isMonthDisabled } from '../utils/bs-calendar-utils'; +import { shiftDate } from '../../bs-moment/utils/date-setters'; -export interface FlagMonthViewOptions { +export interface FlagDaysCalendarOptions { + minDate: Date; + maxDate: Date; hoveredDate: Date; selectedDate: Date; selectedRange: Date[]; @@ -9,31 +14,36 @@ export interface FlagMonthViewOptions { monthIndex: number; } -export function flagMonthView(formattedMonth: MonthViewModel, - options: FlagMonthViewOptions): MonthViewModel { +export function flagDaysCalendar(formattedMonth: DaysCalendarViewModel, + options: FlagDaysCalendarOptions): DaysCalendarViewModel { formattedMonth.weeks .forEach((week: WeekViewModel, weekIndex: number) => { week.days.forEach((day: DayViewModel, dayIndex: number) => { // datepicker const isOtherMonth = !isSameMonth(day.date, formattedMonth.month); - const isHovered = !isOtherMonth && isSameDate(day.date, options.hoveredDate); + const isHovered = !isOtherMonth && isSameDay(day.date, options.hoveredDate); // date range picker - const isSelectionStart = !isOtherMonth && isSameDate(day.date, options.selectedRange[0]); - const isSelectionEnd = !isOtherMonth && isSameDate(day.date, options.selectedRange[1]); + const isSelectionStart = !isOtherMonth && isSameDay(day.date, options.selectedRange[0]); + const isSelectionEnd = !isOtherMonth && isSameDay(day.date, options.selectedRange[1]); - const isSelected = !isOtherMonth && isSameDate(day.date, options.selectedDate) || + const isSelected = !isOtherMonth && isSameDay(day.date, options.selectedDate) || isSelectionStart || isSelectionEnd; const isInRange = !isOtherMonth && isDateInRange(day.date, options.selectedRange, options.hoveredDate); + + const isDisabled = isSameOrBefore(day.date, options.minDate, 'day') + || isSameOrAfter(day.date, options.maxDate, 'day'); + // decide update or not - const newDay = Object.assign(/*{},*/ day, { + const newDay = Object.assign({}, day, { isOtherMonth, isHovered, isSelected, isSelectionStart, isSelectionEnd, - isInRange + isInRange, + isDisabled }); if (day.isOtherMonth !== newDay.isOtherMonth || @@ -41,6 +51,7 @@ export function flagMonthView(formattedMonth: MonthViewModel, day.isSelected !== newDay.isSelected || day.isSelectionStart !== newDay.isSelectionStart || day.isSelectionEnd !== newDay.isSelectionEnd || + day.isDisabled !== newDay.isDisabled || day.isInRange !== newDay.isInRange) { week.days[dayIndex] = newDay; } @@ -52,17 +63,17 @@ export function flagMonthView(formattedMonth: MonthViewModel, && options.monthIndex !== options.displayMonths; formattedMonth.hideRightArrow = options.monthIndex < options.displayMonths && (options.monthIndex + 1) !== options.displayMonths; - return formattedMonth; -} -function isSameDate(date: Date, selectedDate: Date): boolean { - if (!date || !selectedDate) { - return false; - } + formattedMonth.disableLeftArrow = isMonthDisabled( + shiftDate(formattedMonth.month, {month: -1}), + options.minDate, + options.maxDate); + formattedMonth.disableRightArrow = isMonthDisabled( + shiftDate(formattedMonth.month, {month: 1}), + options.minDate, + options.maxDate); - return date.getFullYear() === selectedDate.getFullYear() - && date.getMonth() === selectedDate.getMonth() - && date.getDate() === selectedDate.getDate(); + return formattedMonth; } function isDateInRange(date: Date, selectedRange: Date[], hoveredDate: Date): boolean { diff --git a/src/datepicker/engine/flag-months-calendar.ts b/src/datepicker/engine/flag-months-calendar.ts new file mode 100644 index 0000000000..02ba2a7e14 --- /dev/null +++ b/src/datepicker/engine/flag-months-calendar.ts @@ -0,0 +1,45 @@ +import { isSameMonth } from '../../bs-moment/utils/date-getters'; +import { MonthsCalendarViewModel, CalendarCellViewModel } from '../models/index'; +import { isMonthDisabled, isYearDisabled } from '../utils/bs-calendar-utils'; +import { shiftDate } from '../../bs-moment/utils/date-setters'; + +export interface FlagMonthCalendarOptions { + minDate: Date; + maxDate: Date; + hoveredMonth: Date; + displayMonths: number; + monthIndex: number; +} + +export function flagMonthsCalendar(monthCalendar: MonthsCalendarViewModel, + options: FlagMonthCalendarOptions): MonthsCalendarViewModel { + monthCalendar.months + .forEach((months: CalendarCellViewModel[], rowIndex: number) => { + months.forEach((month: CalendarCellViewModel, monthIndex: number) => { + const isHovered = isSameMonth(month.date, options.hoveredMonth); + const isDisabled = isMonthDisabled(month.date, options.minDate, options.maxDate); + const newMonth = Object.assign(/*{},*/ month, {isHovered, isDisabled}); + if (month.isHovered !== newMonth.isHovered + || month.isDisabled !== newMonth.isDisabled) { + monthCalendar.months[rowIndex][monthIndex] = newMonth; + } + }); + }); + + // todo: add check for linked calendars + monthCalendar.hideLeftArrow = options.monthIndex > 0 + && options.monthIndex !== options.displayMonths; + monthCalendar.hideRightArrow = options.monthIndex < options.displayMonths + && (options.monthIndex + 1) !== options.displayMonths; + + monthCalendar.disableLeftArrow = isYearDisabled( + shiftDate(monthCalendar.months[0][0].date, {year: -1}), + options.minDate, + options.maxDate); + monthCalendar.disableRightArrow = isYearDisabled( + shiftDate(monthCalendar.months[0][0].date, {year: 1}), + options.minDate, + options.maxDate); + + return monthCalendar; +} diff --git a/src/datepicker/engine/flag-years-calendar.ts b/src/datepicker/engine/flag-years-calendar.ts new file mode 100644 index 0000000000..bd9dbbb02b --- /dev/null +++ b/src/datepicker/engine/flag-years-calendar.ts @@ -0,0 +1,47 @@ +import { isSameYear } from '../../bs-moment/utils/date-getters'; +import { YearsCalendarViewModel, CalendarCellViewModel } from '../models/index'; +import { isYearDisabled } from '../utils/bs-calendar-utils'; +import { shiftDate } from '../../bs-moment/utils/date-setters'; + +export interface FlagYearsCalendarOptions { + minDate: Date; + maxDate: Date; + hoveredYear: Date; + displayMonths: number; + yearIndex: number; +} + +export function flagYearsCalendar(yearsCalendar: YearsCalendarViewModel, options: FlagYearsCalendarOptions): YearsCalendarViewModel { + yearsCalendar.years + .forEach((years: CalendarCellViewModel[], rowIndex: number) => { + years.forEach((year: CalendarCellViewModel, yearIndex: number) => { + const isHovered = isSameYear(year.date, options.hoveredYear); + const isDisabled = isYearDisabled(year.date, options.minDate, options.maxDate); + + const newMonth = Object.assign(/*{},*/ year, {isHovered, isDisabled}); + if (year.isHovered !== newMonth.isHovered + || year.isDisabled !== newMonth.isDisabled) { + yearsCalendar.years[rowIndex][yearIndex] = newMonth; + } + }); + }); + + // todo: add check for linked calendars + yearsCalendar.hideLeftArrow = options.yearIndex > 0 + && options.yearIndex !== options.displayMonths; + yearsCalendar.hideRightArrow = options.yearIndex < options.displayMonths + && (options.yearIndex + 1) !== options.displayMonths; + + yearsCalendar.disableLeftArrow = isYearDisabled( + shiftDate(yearsCalendar.years[0][0].date, {year: -1}), + options.minDate, + options.maxDate); + const i = yearsCalendar.years.length - 1; + const j = yearsCalendar.years[i].length - 1; + yearsCalendar.disableRightArrow = isYearDisabled( + shiftDate(yearsCalendar.years[i][j].date, {year: 1}), + options.minDate, + options.maxDate); + + return yearsCalendar; +} diff --git a/src/datepicker/engine/format-month-view.ts b/src/datepicker/engine/format-days-calendar.ts similarity index 68% rename from src/datepicker/engine/format-month-view.ts rename to src/datepicker/engine/format-days-calendar.ts index 056712a843..f5fa6fdea0 100644 --- a/src/datepicker/engine/format-month-view.ts +++ b/src/datepicker/engine/format-days-calendar.ts @@ -1,10 +1,12 @@ -import { DatepickerFormatOptions, DaysCalendarModel, MonthViewModel, MonthViewOptions } from '../models/index'; +import { + DatepickerFormatOptions, DaysCalendarModel, DaysCalendarViewModel +} from '../models/index'; import { formatDate } from '../../bs-moment/format'; import { getLocale } from '../../bs-moment/locale/locales.service'; -export function formatMonthView(daysCalendar: DaysCalendarModel, - formatOptions: DatepickerFormatOptions, - monthIndex: number): MonthViewModel { +export function formatDaysCalendar(daysCalendar: DaysCalendarModel, + formatOptions: DatepickerFormatOptions, + monthIndex: number): DaysCalendarViewModel { return { month: daysCalendar.month, monthTitle: formatDate(daysCalendar.month, formatOptions.monthTitle, formatOptions.locale), @@ -19,10 +21,12 @@ export function formatMonthView(daysCalendar: DaysCalendarModel, monthIndex, weekIndex, dayIndex })) }) - ), + ) }; } export function getWeekNumbers(daysMatrix: Date[][], format: string, locale: string): string[] { - return daysMatrix.map((days: Date[]) => days[0] ? formatDate(days[0], format, locale) : ''); + return daysMatrix.map((days: Date[]) => days[0] + ? formatDate(days[0], format, locale) + : ''); } diff --git a/src/datepicker/engine/format-months-calendar.ts b/src/datepicker/engine/format-months-calendar.ts new file mode 100644 index 0000000000..ba35d1fc49 --- /dev/null +++ b/src/datepicker/engine/format-months-calendar.ts @@ -0,0 +1,27 @@ +import { + DatepickerFormatOptions, MonthsCalendarViewModel, CalendarCellViewModel +} from '../models/index'; +import { startOf } from '../../bs-moment/utils/start-end-of'; +import { shiftDate } from '../../bs-moment/utils/date-setters'; +import { formatDate } from '../../bs-moment/format'; +import { createMatrix } from '../utils/matrix-utils'; + +const height = 4; +const width = 3; +const shift = {month: 1}; + +export function formatMonthsCalendar(viewDate: Date, formatOptions: DatepickerFormatOptions): MonthsCalendarViewModel { + const initialDate = startOf(viewDate, 'year'); + const matrixOptions = {width, height, initialDate, shift}; + const monthMatrix = createMatrix(matrixOptions, + date => ({ + date, + label: formatDate(date, formatOptions.monthLabel, formatOptions.locale) + })); + + return { + months: monthMatrix, + monthTitle: '', + yearTitle: formatDate(viewDate, formatOptions.yearTitle, formatOptions.locale) + }; +} diff --git a/src/datepicker/engine/format-years-calendar.ts b/src/datepicker/engine/format-years-calendar.ts new file mode 100644 index 0000000000..3edf40c52f --- /dev/null +++ b/src/datepicker/engine/format-years-calendar.ts @@ -0,0 +1,38 @@ +import { + DatepickerFormatOptions, YearsCalendarViewModel, CalendarCellViewModel +} from '../models/index'; +import { shiftDate } from '../../bs-moment/utils/date-setters'; +import { formatDate } from '../../bs-moment/format'; +import { TimeUnit } from '../../bs-moment/types'; +import { createMatrix } from '../utils/matrix-utils'; + +const height = 4; +const width = 4; +export const yearsPerCalendar = height * width; +const initialShift = (Math.floor(yearsPerCalendar / 2) - 1) * -1; +const shift = {year: 1}; + +export function formatYearsCalendar(viewDate: Date, formatOptions: DatepickerFormatOptions): YearsCalendarViewModel { + + const initialDate = shiftDate(viewDate, {year: initialShift}); + const matrixOptions = {width, height, initialDate, shift}; + const yearsMatrix = createMatrix(matrixOptions, + date => ({ + date, + label: formatDate(date, formatOptions.yearLabel, formatOptions.locale) + })); + const yearTitle = formatYearRangeTitle(yearsMatrix, formatOptions); + + return { + years: yearsMatrix, + monthTitle: '', + yearTitle + }; +} + +function formatYearRangeTitle(yearsMatrix: CalendarCellViewModel[][], formatOptions: DatepickerFormatOptions): string { + const from = formatDate(yearsMatrix[0][0].date, formatOptions.yearTitle, formatOptions.locale); + const to = formatDate(yearsMatrix[height - 1][width - 1].date, formatOptions.yearTitle, formatOptions.locale); + + return `${from} - ${to}`; +} diff --git a/src/datepicker/engine/view-mode.ts b/src/datepicker/engine/view-mode.ts new file mode 100644 index 0000000000..8309f03647 --- /dev/null +++ b/src/datepicker/engine/view-mode.ts @@ -0,0 +1,5 @@ +import { BsDatepickerViewMode } from '../models/index'; + +export function canSwitchMode(mode: BsDatepickerViewMode): boolean { + return true; +} diff --git a/src/datepicker/models/index.ts b/src/datepicker/models/index.ts index 60200f7003..cca9d30db7 100644 --- a/src/datepicker/models/index.ts +++ b/src/datepicker/models/index.ts @@ -1,17 +1,32 @@ -import { Locale } from '../../bs-moment/locale/locale.class'; import { TimeUnit } from '../../bs-moment/types'; +import { Observable } from 'rxjs/Observable'; +import { EventEmitter } from '@angular/core'; +import { BsDatepickerEffects } from '../reducer/bs-datepicker.effects'; +import { BsCustomDates } from '../themes/bs/bs-custom-dates-view.component'; -export interface DaysCalendarModel { - daysMatrix: Date[][]; - month: Date; +export type BsDatepickerViewMode = 'day' | 'month' | 'year'; + +/** *************** */ +// navigation bar settings +export interface NavigationViewModel { + monthTitle: string; + yearTitle: string; + hideLeftArrow?: boolean; + hideRightArrow?: boolean; + disableLeftArrow?: boolean; + disableRightArrow?: boolean; } -export interface DayViewModel { +export interface CalendarCellViewModel { date: Date; label: string; - // flag step isDisabled?: boolean; isHovered?: boolean; +} + +/** *************** */ +// days matrix: day cell view model +export interface DayViewModel extends CalendarCellViewModel { isOtherMonth?: boolean; isInRange?: boolean; isSelectionStart?: boolean; @@ -27,30 +42,58 @@ export interface WeekViewModel { days: DayViewModel[]; } -export interface MonthViewModel { +// todo: split navigation settings +export interface DaysCalendarViewModel extends NavigationViewModel { weeks: WeekViewModel[]; - // format step + // additional information month: Date; - monthTitle: string; - yearTitle: string; weekNumbers: string[]; weekdays: string[]; - // flag step - hideLeftArrow?: boolean; - hideRightArrow?: boolean; } +/** *************** */ +// months calendar +export interface MonthsCalendarViewModel extends NavigationViewModel { + months: CalendarCellViewModel[][]; +} + +/** *************** */ +// years calendar +export interface YearsCalendarViewModel extends NavigationViewModel { + years: CalendarCellViewModel[][]; +} + +/** *************** */ + +// math model +/** *************** */ + +// days Date's array +export interface DaysCalendarModel { + daysMatrix: Date[][]; + month: Date; +} + +/** *************** */ +// some func options export interface MonthViewOptions { width?: number; height?: number; firstDayOfWeek?: number; } +/** *************** */ +// rendering options export interface DatepickerFormatOptions { locale: string; + monthTitle: string; yearTitle: string; + dayLabel: string; + monthLabel: string; + yearLabel: string; + weekNumbers: string; } @@ -59,15 +102,23 @@ export interface DatepickerRenderOptions { displayMonths?: number; } -export type DateFormatterFn = (date: Date, format: string, locale?: Locale) => string; - +/** *************** */ // events +/** *************** */ +export enum BsNavigationDirection {UP, DOWN} +// used for navigation events, to change view date in state export interface BsNavigationEvent { - step: TimeUnit; + direction?: BsNavigationDirection; + step?: TimeUnit; +} + +export interface BsViewNavigationEvent { + unit?: TimeUnit; + viewMode: BsDatepickerViewMode; } -export interface DayHoverEvent { - day: DayViewModel; +export interface CellHoverEvent { + cell: CalendarCellViewModel; isHovered: boolean; } diff --git a/src/datepicker/reducer/_defaults.ts b/src/datepicker/reducer/_defaults.ts index 65e03afded..55bba53a72 100644 --- a/src/datepicker/reducer/_defaults.ts +++ b/src/datepicker/reducer/_defaults.ts @@ -2,19 +2,5 @@ import { DatepickerFormatOptions, DatepickerRenderOptions, MonthViewOptions } fr export const defaultMonthOptions: MonthViewOptions = { width: 7, - height: 6, - firstDayOfWeek: 1 -}; - -export const defaultFormatOptions: DatepickerFormatOptions = { - locale: 'en', - monthTitle: 'MMMM', - yearTitle: 'YYYY', - dayLabel: 'D', - weekNumbers: 'w' -}; - -export const defaultRenderOptions: DatepickerRenderOptions = { - displayMonths: 1, - showWeekNumbers: true + height: 6 }; diff --git a/src/datepicker/reducer/bs-datepicker.actions.ts b/src/datepicker/reducer/bs-datepicker.actions.ts index 327eb2977e..1e85b5b01e 100644 --- a/src/datepicker/reducer/bs-datepicker.actions.ts +++ b/src/datepicker/reducer/bs-datepicker.actions.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { DatepickerRenderOptions, DayHoverEvent } from '../models/index'; -import { Action } from '../../mini-ngrx/index'; import { TimeUnit } from '../../bs-moment/types'; +import { Action } from '../../mini-ngrx/index'; +import { BsDatepickerViewMode, BsViewNavigationEvent, CellHoverEvent, DatepickerRenderOptions } from '../models/index'; @Injectable() export class BsDatepickerActions { @@ -9,48 +9,54 @@ export class BsDatepickerActions { static readonly FORMAT = '[datepicker] format datepicker values'; static readonly FLAG = '[datepicker] set flags'; static readonly SELECT = '[datepicker] select date'; - static readonly STEP_NAVIGATION = '[datepicker] shift view date'; - static readonly RENDER_OPTIONS = '[datepicker] update render options'; + static readonly NAVIGATE_OFFSET = '[datepicker] shift view date'; + static readonly NAVIGATE_TO = '[datepicker] change view date'; + static readonly SET_OPTIONS = '[datepicker] update render options'; static readonly HOVER = '[datepicker] hover date'; + static readonly CHANGE_VIEWMODE = '[datepicker] switch view mode'; + + static readonly SET_MIN_DATE = '[datepicker] set min date'; + static readonly SET_MAX_DATE = '[datepicker] set max date'; static readonly SELECT_RANGE = '[daterangepicker] select dates range'; - calculate(viewDate: Date): Action { - return { - type: BsDatepickerActions.CALCULATE, - payload: viewDate - }; - } + calculate(): Action {return {type: BsDatepickerActions.CALCULATE}; } - format(): Action { + format(): Action {return {type: BsDatepickerActions.FORMAT}; } + + flag(): Action { return {type: BsDatepickerActions.FLAG}; } + + select(date: Date): Action { return { - type: BsDatepickerActions.FORMAT + type: BsDatepickerActions.SELECT, + payload: date }; } - flag(): Action { + changeViewMode(event: BsDatepickerViewMode): Action { return { - type: BsDatepickerActions.FLAG + type: BsDatepickerActions.CHANGE_VIEWMODE, + payload: event }; } - select(date: Date): Action { + navigateTo(event: BsViewNavigationEvent): Action { return { - type: BsDatepickerActions.SELECT, - payload: date + type: BsDatepickerActions.NAVIGATE_TO, + payload: event }; } navigateStep(step: TimeUnit): Action { return { - type: BsDatepickerActions.STEP_NAVIGATION, + type: BsDatepickerActions.NAVIGATE_OFFSET, payload: step }; } - renderOptions(options: DatepickerRenderOptions): Action { + setOptions(options: DatepickerRenderOptions): Action { return { - type: BsDatepickerActions.RENDER_OPTIONS, + type: BsDatepickerActions.SET_OPTIONS, payload: options }; } @@ -63,10 +69,24 @@ export class BsDatepickerActions { }; } - hover(event: DayHoverEvent): Action { + hoverDay(event: CellHoverEvent): Action { return { type: BsDatepickerActions.HOVER, - payload: event.isHovered ? event.day.date : null + payload: event.isHovered ? event.cell.date : null + }; + } + + minDate(date: Date): Action { + return { + type: BsDatepickerActions.SET_MIN_DATE, + payload: date + }; + } + + maxDate(date: Date): Action { + return { + type: BsDatepickerActions.SET_MAX_DATE, + payload: date }; } } diff --git a/src/datepicker/reducer/bs-datepicker.effects.ts b/src/datepicker/reducer/bs-datepicker.effects.ts index 981a61988c..4c757f0a27 100644 --- a/src/datepicker/reducer/bs-datepicker.effects.ts +++ b/src/datepicker/reducer/bs-datepicker.effects.ts @@ -1,19 +1,214 @@ import { Injectable } from '@angular/core'; -import { BsDatepickerStore } from './bs-datepicker.store'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/map'; +import { Observable } from 'rxjs/Observable'; +import { getFullYear, getMonth } from '../../bs-moment/utils/date-getters'; +import { BsDatepickerContainer } from '../base/bs-datepicker-container'; +import { BsDatepickerConfig } from '../bs-datepicker.config'; +import { + BsDatepickerViewMode, + BsNavigationEvent, + CalendarCellViewModel, + CellHoverEvent, + DatepickerRenderOptions, + DaysCalendarViewModel, + DayViewModel, + MonthsCalendarViewModel, + YearsCalendarViewModel +} from '../models/index'; import { BsDatepickerActions } from './bs-datepicker.actions'; +import { BsDatepickerStore } from './bs-datepicker.store'; +import { Subscription } from 'rxjs/Subscription'; @Injectable() export class BsDatepickerEffects { - // constructor(private _bsDatepickerStore: BsDatepickerStore, - // private _actions: BsDatepickerActions) { - // this.onMonthCalendarCalculation(); - // } - // - // onMonthCalendarCalculation() { - // this._bsDatepickerStore - // .select(state => state.monthModel) - // .filter(monthModel => !!monthModel) - // .subscribe(month => - // this._bsDatepickerStore.dispatch(this._actions.format())); - // } + + viewMode: Observable; + daysCalendar: Observable; + monthsCalendar: Observable; + yearsCalendar: Observable; + options: Observable; + + private _store: BsDatepickerStore; + private _subs: Subscription[] = []; + + constructor(private _actions: BsDatepickerActions) { + } + + init(_bsDatepickerStore: BsDatepickerStore): BsDatepickerEffects { + this._store = _bsDatepickerStore; + + return this; + } + + /** setters */ + + setValue(value: Date) { + this._store.dispatch(this._actions.select(value)); + } + + setMinDate(value: Date): BsDatepickerEffects { + this._store.dispatch(this._actions.minDate(value)); + + return this; + } + + setMaxDate(value: Date): BsDatepickerEffects { + this._store.dispatch(this._actions.maxDate(value)); + + return this; + } + + /* Set rendering options */ + setOptions(_config: BsDatepickerConfig): BsDatepickerEffects { + this._store.dispatch(this._actions.setOptions(_config)); + + return this; + } + + /** view to mode bindings */ + setBindings(container: BsDatepickerContainer): BsDatepickerEffects { + container.daysCalendar = this._store + .select(state => state.flaggedMonths) + .filter(months => !!months); + + // month calendar + container.monthsCalendar = this._store + .select(state => state.flaggedMonthsCalendar) + .filter(months => !!months); + + // year calendar + container.yearsCalendar = this._store + .select(state => state.yearsCalendarFlagged) + .filter(years => !!years); + + container.viewMode = this._store + .select(state => state.view.mode); + + container.options = this._store.select(state => state.showWeekNumbers) + .map(showWeekNumbers => ({showWeekNumbers})); + + return this; + } + + /** event handlers*/ + setEventHandlers(container: BsDatepickerContainer): BsDatepickerEffects { + container.setViewMode = (event: BsDatepickerViewMode): void => { + this._store.dispatch(this._actions.changeViewMode(event)); + }; + + container.navigateTo = (event: BsNavigationEvent): void => { + this._store.dispatch(this._actions.navigateStep(event.step)); + }; + + container.dayHoverHandler = (event: CellHoverEvent): void => { + const _cell = event.cell as DayViewModel; + if (_cell.isOtherMonth || _cell.isDisabled) { + return; + } + + this._store.dispatch(this._actions.hoverDay(event)); + _cell.isHovered = event.isHovered; + }; + + container.monthHoverHandler = (event: CellHoverEvent): void => { + event.cell.isHovered = event.isHovered; + }; + + container.yearHoverHandler = (event: CellHoverEvent): void => { + event.cell.isHovered = event.isHovered; + }; + + /** select handlers */ + // container.daySelectHandler = (day: DayViewModel): void => { + // if (day.isOtherMonth || day.isDisabled) { + // return; + // } + // this._store.dispatch(this._actions.select(day.date)); + // }; + + container.monthSelectHandler = (event: CalendarCellViewModel): void => { + if (event.isDisabled) { return; } + this._store.dispatch(this._actions.navigateTo({ + unit: {month: getMonth(event.date)}, + viewMode: 'day' + })); + }; + + container.yearSelectHandler = (event: CalendarCellViewModel): void => { + if (event.isDisabled) { return; } + this._store.dispatch(this._actions.navigateTo({ + unit: {year: getFullYear(event.date)}, + viewMode: 'month' + })); + }; + + return this; + } + + registerDatepickerSideEffects(): BsDatepickerEffects { + this._subs.push( + this._store.select(state => state.view) + .subscribe(view => { + this._store.dispatch(this._actions.calculate()); + })); + + // format calendar values on month model change + this._subs.push( + this._store + .select(state => state.monthsModel) + .filter(monthModel => !!monthModel) + .subscribe(month => + this._store.dispatch(this._actions.format()))); + + // flag day values + this._subs.push( + this._store + .select(state => state.formattedMonths) + .filter(month => !!month) + .subscribe(month => + this._store.dispatch(this._actions.flag()))); + + // flag day values + this._subs.push( + this._store.select(state => state.selectedDate) + .filter(selectedDate => !!selectedDate) + .subscribe(selectedDate => + this._store.dispatch(this._actions.flag()))); + + // flag for date range picker + this._subs.push( + this._store.select(state => state.selectedRange) + .filter(selectedRange => !!selectedRange) + .subscribe(selectedRange => + this._store.dispatch(this._actions.flag()))); + + // monthsCalendar + this._subs.push( + this._store + .select(state => state.monthsCalendar) + .subscribe(() => this._store.dispatch(this._actions.flag()))); + + // years calendar + this._subs.push( + this._store + .select(state => state.yearsCalendarModel) + .filter(state => !!state) + .subscribe(() => this._store.dispatch(this._actions.flag()))); + + // on hover + this._subs.push( + this._store.select(state => state.hoveredDate) + .filter(hoveredDate => !!hoveredDate) + .subscribe(hoveredDate => + this._store.dispatch(this._actions.flag()))); + + return this; + } + + destroy(): void { + for (const sub of this._subs) { + sub.unsubscribe(); + } + } } diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts index 5ba1cbe80b..10fa14f962 100644 --- a/src/datepicker/reducer/bs-datepicker.reducer.ts +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -1,56 +1,70 @@ -import { BsDatepickerState, initialDatepickerState } from './bs-datepicker.state'; +import { + BsDatepickerState, initialDatepickerState +} from './bs-datepicker.state'; import { Action } from '../../mini-ngrx/index'; import { BsDatepickerActions } from './bs-datepicker.actions'; -import { calculateMonthModel } from '../engine/calc-month-view'; -import { formatMonthView } from '../engine/format-month-view'; -import { flagMonthView } from '../engine/flag-month-view'; -import { shiftDate } from '../../bs-moment/utils/date-setters'; +import { calcDaysCalendar } from '../engine/calc-days-calendar'; +import { formatDaysCalendar } from '../engine/format-days-calendar'; +import { flagDaysCalendar } from '../engine/flag-days-calendar'; +import { shiftDate, setDate } from '../../bs-moment/utils/date-setters'; +import { canSwitchMode } from '../engine/view-mode'; +import { formatMonthsCalendar } from '../engine/format-months-calendar'; +import { flagMonthsCalendar } from '../engine/flag-months-calendar'; +import { + formatYearsCalendar, yearsPerCalendar +} from '../engine/format-years-calendar'; +import { flagYearsCalendar } from '../engine/flag-years-calendar'; +import { BsViewNavigationEvent, DatepickerFormatOptions } from '../models/index'; +import { isArray } from '../../bs-moment/utils/type-checks'; export function bsDatepickerReducer(state = initialDatepickerState, action: Action): BsDatepickerState { + if (!(/hover/.test(action.type))) { + console.log(action); + } switch (action.type) { -/* - case (BsDatepickerActions.INIT): { - const locale = getLocale(state.formatOptions.locale); - const monthViewOptions = Object.assign({}, state.monthViewOptions, {firstDayOfWeek: locale.firstDayOfWeek()}); - const monthModel = calculateMonthModel(state.viewDate, monthViewOptions); - return Object.assign({}, state, {locale, monthViewOptions, monthModel}); - } -*/ - - case (BsDatepickerActions.CALCULATE): { - const displayMonths = state.renderOptions.displayMonths; - const monthsModel = new Array(displayMonths); - let viewDate = state.viewDate; - - for (let monthIndex = 0; monthIndex < displayMonths; monthIndex++) { - // todo: for unlinked calendars it will be harder - monthsModel[monthIndex] = calculateMonthModel(viewDate, state.monthViewOptions); - viewDate = shiftDate(viewDate, {month: 1}); - } - return Object.assign({}, state, {monthsModel}); + case(BsDatepickerActions.CALCULATE): { + return calculateReducer(state); } - case (BsDatepickerActions.FORMAT): { - const formattedMonths = state.monthsModel - .map((month, monthIndex) => formatMonthView(month, state.formatOptions, monthIndex)); - return Object.assign({}, state, {formattedMonths}); + case(BsDatepickerActions.FORMAT): { + return formatReducer(state, action); } - case (BsDatepickerActions.FLAG): { - const flaggedMonths = state.formattedMonths - .map((formattedMonth, monthIndex) => flagMonthView(formattedMonth, { - hoveredDate: state.hoveredDate, - selectedDate: state.selectedDate, - selectedRange: state.selectedRange, - displayMonths: state.renderOptions.displayMonths, - monthIndex - })); - return Object.assign({}, state, {flaggedMonths}); + case(BsDatepickerActions.FLAG): { + return flagReducer(state, action); } - case(BsDatepickerActions.STEP_NAVIGATION): { - const viewDate = shiftDate(state.viewDate, action.payload); - return Object.assign({}, state, {viewDate}); + case(BsDatepickerActions.NAVIGATE_OFFSET): { + const date = shiftDate(state.view.date, action.payload); + const newState = { + view: { + mode: state.view.mode, + date + } + }; + + return Object.assign({}, state, newState); + } + + case(BsDatepickerActions.NAVIGATE_TO): { + const payload: BsViewNavigationEvent = action.payload; + + const date = setDate(state.view.date, payload.unit); + const mode = payload.viewMode; + const newState = {view: {date, mode}}; + + return Object.assign({}, state, newState); + } + + case(BsDatepickerActions.CHANGE_VIEWMODE): { + if (!canSwitchMode(action.payload)) { + return state; + } + const date = state.view.date; + const mode = action.payload; + const newState = {view: {date, mode}}; + + return Object.assign({}, state, newState); } case(BsDatepickerActions.HOVER): { @@ -58,11 +72,42 @@ export function bsDatepickerReducer(state = initialDatepickerState, action: Acti } case(BsDatepickerActions.SELECT): { - return Object.assign({}, state, {selectedDate: action.payload}); + const newState = { + selectedDate: action.payload, + view: state.view + }; + + if (action.payload) { + newState.view = { + date: action.payload, + mode: state.view.mode + }; + } + + return Object.assign({}, state, newState); } - case(BsDatepickerActions.RENDER_OPTIONS): { - return Object.assign({}, state, {renderOptions: action.payload}); + case(BsDatepickerActions.SET_OPTIONS): { + const newState = action.payload; + // looks not really good + if (newState.value) { + newState.view = state.view; + if (isArray(newState.value)) { + newState.view = { + mode: state.view.mode, + date: newState.value[0] + }; + newState.selectedRange = newState.value; + } else { + newState.view = { + mode: state.view.mode, + date: newState.value + }; + newState.selectedDate = newState.value; + } + } + + return Object.assign({}, state, newState); } // date range picker @@ -70,6 +115,160 @@ export function bsDatepickerReducer(state = initialDatepickerState, action: Acti return Object.assign({}, state, {selectedRange: action.payload}); } - default: return state; + case(BsDatepickerActions.SET_MIN_DATE): { + return Object.assign({}, state, { + minDate: action.payload + }); + } + case(BsDatepickerActions.SET_MAX_DATE): { + return Object.assign({}, state, { + maxDate: action.payload + }); + } + + default: + return state; + } +} + +function calculateReducer(state: BsDatepickerState): BsDatepickerState { + // how many calendars + const displayMonths = state.displayMonths; + // use selected date on initial rendering if set + let viewDate = state.view.date; + + if (state.view.mode === 'day') { + const monthsModel = new Array(displayMonths); + for (let monthIndex = 0; monthIndex < displayMonths; monthIndex++) { + // todo: for unlinked calendars it will be harder + monthsModel[monthIndex] = calcDaysCalendar(viewDate, state.monthViewOptions); + viewDate = shiftDate(viewDate, {month: 1}); + } + + return Object.assign({}, state, {monthsModel}); } + + if (state.view.mode === 'month') { + const monthsCalendar = new Array(displayMonths); + for (let calendarIndex = 0; calendarIndex < displayMonths; calendarIndex++) { + // todo: for unlinked calendars it will be harder + monthsCalendar[calendarIndex] = formatMonthsCalendar(viewDate, getFormatOptions(state)); + viewDate = shiftDate(viewDate, {year: 1}); + } + + return Object.assign({}, state, {monthsCalendar}); + } + + if (state.view.mode === 'year') { + const yearsCalendarModel = new Array(displayMonths); + + for (let calendarIndex = 0; calendarIndex < displayMonths; calendarIndex++) { + // todo: for unlinked calendars it will be harder + yearsCalendarModel[calendarIndex] = formatYearsCalendar(viewDate, getFormatOptions(state)); + viewDate = shiftDate(viewDate, {year: yearsPerCalendar}); + } + + return Object.assign({}, state, {yearsCalendarModel}); + } + + return state; +} + +function formatReducer(state: BsDatepickerState, action: Action): BsDatepickerState { + if (state.view.mode === 'day') { + const formattedMonths = state.monthsModel + .map((month, monthIndex) => formatDaysCalendar(month, getFormatOptions(state), monthIndex)); + + return Object.assign({}, state, {formattedMonths}); + } + + // how many calendars + const displayMonths = state.displayMonths; + // check initial rendering + // use selected date on initial rendering if set + let viewDate = state.view.date; + + if (state.view.mode === 'month') { + const monthsCalendar = new Array(displayMonths); + for (let calendarIndex = 0; calendarIndex < displayMonths; calendarIndex++) { + // todo: for unlinked calendars it will be harder + monthsCalendar[calendarIndex] = formatMonthsCalendar(viewDate, getFormatOptions(state)); + viewDate = shiftDate(viewDate, {year: 1}); + } + + return Object.assign({}, state, {monthsCalendar}); + } + + if (state.view.mode === 'year') { + const yearsCalendarModel = new Array(displayMonths); + for (let calendarIndex = 0; calendarIndex < displayMonths; calendarIndex++) { + // todo: for unlinked calendars it will be harder + yearsCalendarModel[calendarIndex] = formatYearsCalendar(viewDate, getFormatOptions(state)); + viewDate = shiftDate(viewDate, {year: 16}); + } + + return Object.assign({}, state, {yearsCalendarModel}); + } + + return state; +} + +function flagReducer(state: BsDatepickerState, action: Action): BsDatepickerState { + if (state.view.mode === 'day') { + const flaggedMonths = state.formattedMonths + .map((formattedMonth, monthIndex) => flagDaysCalendar(formattedMonth, { + minDate: state.minDate, + maxDate: state.maxDate, + hoveredDate: state.hoveredDate, + selectedDate: state.selectedDate, + selectedRange: state.selectedRange, + displayMonths: state.displayMonths, + monthIndex + })); + + return Object.assign({}, state, {flaggedMonths}); + } + + if (state.view.mode === 'month') { + const flaggedMonthsCalendar = state.monthsCalendar + .map((formattedMonth, monthIndex) => flagMonthsCalendar(formattedMonth, { + minDate: state.minDate, + maxDate: state.maxDate, + hoveredMonth: state.hoveredMonth, + displayMonths: state.displayMonths, + monthIndex + })); + + return Object.assign({}, state, {flaggedMonthsCalendar}); + } + + if (state.view.mode === 'year') { + const yearsCalendarFlagged = state.yearsCalendarModel + .map((formattedMonth, yearIndex) => flagYearsCalendar(formattedMonth, { + minDate: state.minDate, + maxDate: state.maxDate, + hoveredYear: state.hoveredYear, + displayMonths: state.displayMonths, + yearIndex + })); + + return Object.assign({}, state, {yearsCalendarFlagged}); + } + + return state; +} + +function getFormatOptions(state: BsDatepickerState): DatepickerFormatOptions { + return { + locale: state.locale, + + monthTitle: state.monthTitle, + yearTitle: state.yearTitle, + + dayLabel: state.dayLabel, + monthLabel: state.monthLabel, + yearLabel: state.yearLabel, + + weekNumbers: state.weekNumbers + }; } diff --git a/src/datepicker/reducer/bs-datepicker.state.ts b/src/datepicker/reducer/bs-datepicker.state.ts index 26693a9b8b..1e5ceb17a8 100644 --- a/src/datepicker/reducer/bs-datepicker.state.ts +++ b/src/datepicker/reducer/bs-datepicker.state.ts @@ -1,32 +1,70 @@ import { - DatepickerFormatOptions, DatepickerRenderOptions, DaysCalendarModel, MonthViewModel, - MonthViewOptions + BsDatepickerViewMode, + DatepickerFormatOptions, DatepickerRenderOptions, DaysCalendarModel, + DaysCalendarViewModel, MonthsCalendarViewModel, + MonthViewOptions, YearsCalendarViewModel } from '../models/index'; -import { defaultFormatOptions, defaultMonthOptions, defaultRenderOptions } from './_defaults'; +import { defaultMonthOptions } from './_defaults'; +import { BsDatepickerConfig } from '../bs-datepicker.config'; + +export interface BsDatepickerViewState { + date: Date; + mode: BsDatepickerViewMode; +} + +export class BsDatepickerState implements DatepickerRenderOptions, DatepickerFormatOptions { + // date picker + selectedDate?: Date; + // daterange picker + selectedRange?: Date[]; -export class BsDatepickerState { // initial date of calendar, today by default - viewDate: Date; + view: BsDatepickerViewState; + + // bounds + minDate?: Date; + maxDate?: Date; + hoveredDate?: Date; - selectedDate?: Date; + hoveredMonth?: Date; + hoveredYear?: Date; + // days calendar monthsModel?: DaysCalendarModel[]; - formattedMonths?: MonthViewModel[]; - flaggedMonths?: MonthViewModel[]; + formattedMonths?: DaysCalendarViewModel[]; + flaggedMonths?: DaysCalendarViewModel[]; + + // months calendar + monthsCalendar?: MonthsCalendarViewModel[]; + flaggedMonthsCalendar?: MonthsCalendarViewModel[]; + // years calendar + yearsCalendarModel?: YearsCalendarViewModel[]; + yearsCalendarFlagged?: YearsCalendarViewModel[]; + + // options monthViewOptions: MonthViewOptions; - formatOptions: DatepickerFormatOptions; - renderOptions: DatepickerRenderOptions; + // DatepickerRenderOptions + showWeekNumbers?: boolean; + displayMonths?: number; - // daterange picker - selectedRange?: Date[]; + // DatepickerFormatOptions + locale: string; + + monthTitle: string; + yearTitle: string; + + dayLabel: string; + monthLabel: string; + yearLabel: string; + + weekNumbers: string; } -export const initialDatepickerState: BsDatepickerState = { - viewDate: new Date(), - selectedRange: [], - monthViewOptions: defaultMonthOptions, - formatOptions: defaultFormatOptions, - renderOptions: defaultRenderOptions -}; +export const initialDatepickerState: BsDatepickerState = Object.assign( + new BsDatepickerConfig(), { + view: {date: new Date(), mode: 'day'} as BsDatepickerViewState, + selectedRange: [], + monthViewOptions: defaultMonthOptions + }); diff --git a/src/datepicker/themes/bs/bs-calendar-layout.component.ts b/src/datepicker/themes/bs/bs-calendar-layout.component.ts new file mode 100644 index 0000000000..a15a4cecb6 --- /dev/null +++ b/src/datepicker/themes/bs/bs-calendar-layout.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'bs-calendar-layout', + template: ` + + + + +
+ +
+ +
+ +
+ + + + ` +}) +export class BsCalendarLayoutComponent {} diff --git a/src/datepicker/themes/bs/bs-current-date-view.component.ts b/src/datepicker/themes/bs/bs-current-date-view.component.ts new file mode 100644 index 0000000000..bf4df639a6 --- /dev/null +++ b/src/datepicker/themes/bs/bs-current-date-view.component.ts @@ -0,0 +1,9 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'bs-current-date', + template: `
{{ title }}
` +}) +export class BsCurrentDateViewComponent { + @Input() title: string; +} diff --git a/src/datepicker/themes/bs/bs-custom-dates-view.component.ts b/src/datepicker/themes/bs/bs-custom-dates-view.component.ts new file mode 100644 index 0000000000..1f019c2ac8 --- /dev/null +++ b/src/datepicker/themes/bs/bs-custom-dates-view.component.ts @@ -0,0 +1,21 @@ +import { Component, ChangeDetectionStrategy, Input } from '@angular/core'; + +export interface BsCustomDates { + label: string; + value: Date | Date[]; +} + +@Component({ + selector: 'bs-custom-date-view', + template: ` +
+ + +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class BsCustomDatesViewComponent { + @Input() isCustomRangeShown: true; + @Input() ranges: BsCustomDates[]; +} diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts index ff792b1eae..a922cd2ccd 100644 --- a/src/datepicker/themes/bs/bs-datepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -1,115 +1,70 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; +import { Component, EventEmitter, OnDestroy, OnInit } from '@angular/core'; +import { BsDatepickerContainer } from '../../base/bs-datepicker-container'; + +import { BsDatepickerConfig } from '../../bs-datepicker.config'; +import { DayViewModel } from '../../models/index'; import { BsDatepickerActions } from '../../reducer/bs-datepicker.actions'; -import { - BsNavigationEvent, DatepickerRenderOptions, DayHoverEvent, DayViewModel, - MonthViewModel -} from '../../models/index'; -import 'rxjs/add/operator/filter'; +import { BsDatepickerEffects } from '../../reducer/bs-datepicker.effects'; +import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; +import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'bs-datepicker-container', - providers: [BsDatepickerStore], - template: ` - - `, + providers: [BsDatepickerStore, BsDatepickerEffects], + templateUrl: './bs-datepicker-view.html', host: { '(click)': '_stopPropagation($event)', style: 'position: absolute; display: block;' } }) -export class BsDatepickerContainerComponent { - @Input() +export class BsDatepickerContainerComponent + extends BsDatepickerContainer + implements OnInit, OnDestroy { + set value(value: Date) { - this._bsDatepickerStore.dispatch(this._actions.select(value)); + this._effects.setValue(value); } - @Output() valueChange = new EventEmitter(); - - months: MonthViewModel[]; - options: DatepickerRenderOptions; - - constructor(private _bsDatepickerStore: BsDatepickerStore, - private _actions: BsDatepickerActions) { - // data binding state <--> model - this._bsDatepickerStore.select(state => state.flaggedMonths) - .filter(months => !!months) - .subscribe(months => this.months = months); - - this._bsDatepickerStore.select(state => state.renderOptions) - .filter(options => !!options) - .subscribe(options => this.options = options); - - // set render options - this._bsDatepickerStore.dispatch(this._actions.renderOptions({ - displayMonths: 1, - showWeekNumbers: true - })); - - // on selected date change - this._bsDatepickerStore.select(state => state.selectedDate) - .subscribe(date => this.valueChange.emit(date)); - - // TODO: extract effects - // calculate month model on view model change - this._bsDatepickerStore - .select(state => state.viewDate) - .subscribe(viewDate => - this._bsDatepickerStore.dispatch(this._actions.calculate(viewDate))); - - // format calendar values on month model change - this._bsDatepickerStore - .select(state => state.monthsModel) - .filter(monthModel => !!monthModel) - .subscribe(month => - this._bsDatepickerStore.dispatch(this._actions.format())); - - // flag day values - this._bsDatepickerStore - .select(state => state.formattedMonths) - .filter(month => !!month) - .subscribe(month => - this._bsDatepickerStore.dispatch(this._actions.flag())); - - // flag day values - this._bsDatepickerStore.select(state => state.selectedDate) - .filter(selectedDate => !!selectedDate) - .subscribe(selectedDate => - this._bsDatepickerStore.dispatch(this._actions.flag())); + valueChange: EventEmitter = new EventEmitter(); - // on hover - this._bsDatepickerStore.select(state => state.hoveredDate) - .filter(hoveredDate => !!hoveredDate) - .subscribe(hoveredDate => - this._bsDatepickerStore.dispatch(this._actions.flag())); + _subs: Subscription[] = []; + constructor(private _config: BsDatepickerConfig, + private _store: BsDatepickerStore, + private _actions: BsDatepickerActions, + _effects: BsDatepickerEffects) { + super(); + this._effects = _effects; } - navigateTo(event: BsNavigationEvent): void { - this._bsDatepickerStore.dispatch(this._actions.navigateStep(event.step)); + ngOnInit(): void { + this._effects + .init(this._store) + // intial state options + .setOptions(this._config) + // data binding view --> model + .setBindings(this) + // set event handlers + .setEventHandlers(this) + .registerDatepickerSideEffects(); + + // todo: move it somewhere else + // on selected date change + this._subs.push(this._store + .select(state => state.selectedDate) + .subscribe(date => this.valueChange.emit(date))); } - hoverHandler(event: DayHoverEvent): void { - if (event.day.isOtherMonth) { + daySelectHandler(day: DayViewModel): void { + if (day.isOtherMonth || day.isDisabled) { return; } - this._bsDatepickerStore.dispatch(this._actions.hover(event)); - event.day.isHovered = event.isHovered; + this._store.dispatch(this._actions.select(day.date)); } - selectHandler(day: DayViewModel): void { - if (day.isOtherMonth) { - return; + ngOnDestroy(): void { + for (const sub of this._subs) { + sub.unsubscribe(); } - this._bsDatepickerStore.dispatch(this._actions.select(day.date)); - } - - _stopPropagation(event: any): void { - event.stopPropagation(); + this._effects.destroy(); } } diff --git a/src/datepicker/themes/bs/bs-datepicker-day-decorator.directive.ts b/src/datepicker/themes/bs/bs-datepicker-day-decorator.directive.ts index 9337eb765c..1cf475debb 100644 --- a/src/datepicker/themes/bs/bs-datepicker-day-decorator.directive.ts +++ b/src/datepicker/themes/bs/bs-datepicker-day-decorator.directive.ts @@ -1,5 +1,5 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, Directive } from '@angular/core'; -import { DayHoverEvent, DayViewModel } from '../../models/index'; +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { DayViewModel } from '../../models/index'; @Component({ selector: '[bsDatepickerDayDecorator]', diff --git a/src/datepicker/themes/bs/bs-datepicker-month-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-month-view.component.ts deleted file mode 100644 index 41550c1f9b..0000000000 --- a/src/datepicker/themes/bs/bs-datepicker-month-view.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { DatepickerRenderOptions, DayHoverEvent, DayViewModel, MonthViewModel } from '../../models/index'; - -@Component({ - selector: `bs-datepicker-month-view`, - // FIX: day select and hover should mutate day or use separate component - // changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - - - - - - - - - - - - - -
{{ month.weekdays[i] }} -
{{ month.weekNumbers[i] }} - - {{ day.label }} -
- ` -}) -export class BsDatepickerMonthViewComponent { - @Input() month: MonthViewModel; - @Input() options: DatepickerRenderOptions; - - @Output() onSelect = new EventEmitter(); - @Output() onHover = new EventEmitter(); - - selectDay(event: DayViewModel): void { - this.onSelect.emit(event); - } - - hoverDay(day: DayViewModel, isHovered: boolean): void { - this.onHover.emit({day, isHovered}); - } -} - diff --git a/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts index efa5aa3441..ac9eaccbf4 100644 --- a/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts +++ b/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts @@ -1,30 +1,49 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { BsNavigationEvent, MonthViewModel } from '../../models/index'; -import { TimeUnit } from '../../../bs-moment/types'; +import { + ChangeDetectionStrategy, Component, EventEmitter, Input, Output +} from '@angular/core'; +import { + BsDatepickerViewMode, BsNavigationDirection, DaysCalendarViewModel +} from '../../models/index'; @Component({ selector: 'bs-datepicker-navigation-view', changeDetection: ChangeDetectionStrategy.OnPush, template: ` - - + + + ` }) export class BsDatepickerNavigationViewComponent { - @Input() month: MonthViewModel; - @Output() onNavigate = new EventEmitter(); + @Input() calendar: DaysCalendarViewModel; + + @Output() onNavigate = new EventEmitter(); + @Output() onViewMode = new EventEmitter(); + + navTo(down: boolean): void { + this.onNavigate.emit(down + ? BsNavigationDirection.DOWN + : BsNavigationDirection.UP); + } - navTo(step: TimeUnit): void { - this.onNavigate.emit({step}); + view(viewMode: BsDatepickerViewMode): void { + this.onViewMode.emit(viewMode); } } diff --git a/src/datepicker/themes/bs/bs-datepicker-navigation-view.spec.ts b/src/datepicker/themes/bs/bs-datepicker-navigation-view.spec.ts index 3326adf124..408a038a48 100644 --- a/src/datepicker/themes/bs/bs-datepicker-navigation-view.spec.ts +++ b/src/datepicker/themes/bs/bs-datepicker-navigation-view.spec.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; -import { BsNavigationEvent, MonthViewModel } from '../../models/index'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { BsNavigationDirection, BsNavigationEvent, DaysCalendarViewModel } from '../../models/index'; import { BsDatepickerNavigationViewComponent } from './bs-datepicker-navigation-view.component'; type TestFixture = ComponentFixture; @@ -25,12 +25,12 @@ function getNextNavButton(fixture: TestFixture): HTMLElement { .querySelector(nextNavSelector) as HTMLElement; } -function getNavEvent(fixture: TestFixture): BsNavigationEvent { +function getNavEvent(fixture: TestFixture): BsNavigationDirection { return fixture.componentInstance._navTo; } -function setMonth(fixture: TestFixture, month: Partial): void { - fixture.componentInstance.month = month as MonthViewModel; +function setMonth(fixture: TestFixture, month: Partial): void { + fixture.componentInstance.month = month as DaysCalendarViewModel; fixture.detectChanges(); } @@ -77,16 +77,16 @@ describe('datepicker: bs-datepicker-navigation-view', () => { expect(next.style.visibility).toBe('hidden'); }); - it('on prev nav button click should decrease month on 1', () => { + it('on prev nav button click should nav DOWN', () => { const prev = getPrevNavButton(fixture); prev.click(); - expect(getNavEvent(fixture).step.month).toBe(-1); + expect(getNavEvent(fixture)).toBe(BsNavigationDirection.DOWN); }); - it('on next nav button click should increase month on 1', () => { + it('on next nav button click should nav UP', () => { const next = getNextNavButton(fixture); next.click(); - expect(getNavEvent(fixture).step.month).toBe(1); + expect(getNavEvent(fixture)).toBe(BsNavigationDirection.UP); }); }); @@ -94,15 +94,15 @@ describe('datepicker: bs-datepicker-navigation-view', () => { selector: 'test-cmp', template: ` ` }) class TestComponent { - month: MonthViewModel; - _navTo: BsNavigationEvent; + month: DaysCalendarViewModel; + _navTo: BsNavigationDirection; - navTo(event: BsNavigationEvent): void { + navTo(event: BsNavigationDirection): void { this._navTo = event; } } diff --git a/src/datepicker/themes/bs/bs-datepicker-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-view.component.ts deleted file mode 100644 index 4589964b19..0000000000 --- a/src/datepicker/themes/bs/bs-datepicker-view.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; -import { - BsNavigationEvent, DatepickerRenderOptions, DayHoverEvent, DayViewModel, - MonthViewModel -} from '../../models/index'; - -@Component({ - selector: 'bs-datepicker-view', - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` -
-
- -
-
- -
-
- ` -}) -export class BsDatepickerViewComponent { - @Input() months: MonthViewModel[]; - - @Input() options: DatepickerRenderOptions; - - @Output() onNavigate = new EventEmitter(); - @Output() onSelect = new EventEmitter(); - @Output() onHover = new EventEmitter(); - - navigateTo(event: BsNavigationEvent): void { - this.onNavigate.emit(event); - } - - hoverHandler(event: DayHoverEvent): void { - this.onHover.emit(event); - } - - selectHandler(event: DayViewModel): void { - this.onSelect.emit(event); - } -} diff --git a/src/datepicker/themes/bs/bs-datepicker-view.html b/src/datepicker/themes/bs/bs-datepicker-view.html new file mode 100644 index 0000000000..f4467b26d3 --- /dev/null +++ b/src/datepicker/themes/bs/bs-datepicker-view.html @@ -0,0 +1,59 @@ + +
+
+ + +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ + +
+ +
+ + +
+ +
+
diff --git a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts index 664193578b..458f96dad6 100644 --- a/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts +++ b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts @@ -1,138 +1,82 @@ -import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; +import { Component, EventEmitter, OnInit } from '@angular/core'; +import { BsDatepickerContainer } from '../../base/bs-datepicker-container'; +import { BsDatepickerConfig } from '../../bs-datepicker.config'; +import { DayViewModel } from '../../models/index'; import { BsDatepickerActions } from '../../reducer/bs-datepicker.actions'; -import { - BsNavigationEvent, DatepickerRenderOptions, DayHoverEvent, DayViewModel, - MonthViewModel -} from '../../models/index'; -import 'rxjs/add/operator/filter'; +import { BsDatepickerEffects } from '../../reducer/bs-datepicker.effects'; +import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; @Component({ selector: 'bs-daterangepicker-container', - providers: [BsDatepickerStore], - template: ` - - `, + providers: [BsDatepickerStore, BsDatepickerEffects], + templateUrl: './bs-datepicker-view.html', host: { '(click)': '_stopPropagation($event)', style: 'position: absolute; display: block;' } }) -export class BsDaterangepickerContainerComponent implements OnInit { - @Input() +export class BsDaterangepickerContainerComponent + extends BsDatepickerContainer + implements OnInit { + set value(value: Date[]) { - this._bsDatepickerStore.dispatch(this._actions.selectRange(value || [])); + this._store.dispatch(this._actions.selectRange(value || [])); } - @Output() valueChange = new EventEmitter(); + valueChange = new EventEmitter(); - months: MonthViewModel[]; - options: DatepickerRenderOptions; _rangeStack: Date[] = []; - constructor(private _bsDatepickerStore: BsDatepickerStore, - private _actions: BsDatepickerActions) { - // data binding state <--> model - this._bsDatepickerStore.select(state => state.flaggedMonths) - .filter(months => !!months) - .subscribe(months => this.months = months); - - this._bsDatepickerStore.select(state => state.renderOptions) - .filter(options => !!options) - .subscribe(options => this.options = options); - - // set render options - this._bsDatepickerStore.dispatch(this._actions.renderOptions({ - displayMonths: 2, - showWeekNumbers: true - })); + constructor(private _config: BsDatepickerConfig, + private _store: BsDatepickerStore, + private _actions: BsDatepickerActions, + _effects: BsDatepickerEffects) { + super(); + this._effects = _effects; + } + ngOnInit(): void { + this._effects + .init(this._store) + // intial state options + // todo: fix this, split configs + .setOptions(this._config) + // data binding view --> model + .setBindings(this) + // set event handlers + .setEventHandlers(this) + .registerDatepickerSideEffects(); + + // todo: move it somewhere else // on selected date change - this._bsDatepickerStore.select(state => state.selectedRange) + this._store.select(state => state.selectedRange) .subscribe(date => this.valueChange.emit(date)); - - // TODO: extract effects - // calculate month model on view model change - this._bsDatepickerStore - .select(state => state.viewDate) - .subscribe(viewDate => - this._bsDatepickerStore.dispatch(this._actions.calculate(viewDate))); - - // format calendar values on month model change - this._bsDatepickerStore - .select(state => state.monthsModel) - .filter(monthModel => !!monthModel) - .subscribe(month => - this._bsDatepickerStore.dispatch(this._actions.format())); - - // flag day values - this._bsDatepickerStore - .select(state => state.formattedMonths) - .filter(month => !!month) - .subscribe(month => - this._bsDatepickerStore.dispatch(this._actions.flag())); - - // flag day values - this._bsDatepickerStore.select(state => state.selectedRange) - .filter(selectedRange => !!selectedRange) - .subscribe(selectedRange => - this._bsDatepickerStore.dispatch(this._actions.flag())); - - // on hover - this._bsDatepickerStore.select(state => state.hoveredDate) - .filter(hoveredDate => !!hoveredDate) - .subscribe(hoveredDate => - this._bsDatepickerStore.dispatch(this._actions.flag())); } - ngOnInit() { - // this._bsDatepickerStore.dispatch(this._actions.init()); - } - - navigateTo(event: BsNavigationEvent): void { - this._bsDatepickerStore.dispatch(this._actions.navigateStep(event.step)); - } - - hoverHandler(event: DayHoverEvent): void { - if (event.day.isOtherMonth) { - return; - } - this._bsDatepickerStore.dispatch(this._actions.hover(event)); - event.day.isHovered = event.isHovered; - } - - selectHandler(day: DayViewModel): void { - if (day.isOtherMonth) { + daySelectHandler(day: DayViewModel): void { + if (day.isOtherMonth || day.isDisabled) { return; } + // if only one date is already selected + // and user clicks on previous date + // start selection from new date + // but if new date is after initial one + // than finish selection if (this._rangeStack.length === 1) { - if (day.date >= this._rangeStack[0]) { - this._rangeStack = [this._rangeStack[0], day.date]; - } else { - this._rangeStack = [day.date]; - } + this._rangeStack = day.date >= this._rangeStack[0] + ? [this._rangeStack[0], day.date] + : [day.date]; } if (this._rangeStack.length === 0) { this._rangeStack = [day.date]; } - this._bsDatepickerStore.dispatch(this._actions.selectRange(this._rangeStack)); + this._store.dispatch(this._actions.selectRange(this._rangeStack)); if (this._rangeStack.length === 2) { this._rangeStack = []; } } - - _stopPropagation(event: any): void { - event.stopPropagation(); - } } diff --git a/src/datepicker/themes/bs/bs-days-calendar-view.component.ts b/src/datepicker/themes/bs/bs-days-calendar-view.component.ts new file mode 100644 index 0000000000..761acbd639 --- /dev/null +++ b/src/datepicker/themes/bs/bs-days-calendar-view.component.ts @@ -0,0 +1,79 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + BsDatepickerViewMode, + BsNavigationDirection, + BsNavigationEvent, + CellHoverEvent, + DatepickerRenderOptions, + DaysCalendarViewModel, + DayViewModel +} from '../../models/index'; + +@Component({ + selector: 'bs-days-calendar-view', + // changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + + + + + + + + + + + + + + +
{{ calendar.weekdays[i] }} +
+ {{ calendar.weekNumbers[i] }} + + {{ day.label }} +
+ +
+ ` +}) +export class BsDaysCalendarViewComponent { + @Input() calendar: DaysCalendarViewModel; + @Input() options: DatepickerRenderOptions; + + @Output() onNavigate = new EventEmitter(); + @Output() onViewMode = new EventEmitter(); + + @Output() onSelect = new EventEmitter(); + @Output() onHover = new EventEmitter(); + + navigateTo(event: BsNavigationDirection): void { + const step = BsNavigationDirection.DOWN === event ? -1 : 1; + this.onNavigate.emit({step: {month: step}}); + } + + changeViewMode(event: BsDatepickerViewMode): void { + this.onViewMode.emit(event); + } + + selectDay(event: DayViewModel): void { + this.onSelect.emit(event); + } + + hoverDay(cell: DayViewModel, isHovered: boolean): void { + this.onHover.emit({cell, isHovered}); + } +} diff --git a/src/datepicker/themes/bs/bs-months-calendar-view.component.ts b/src/datepicker/themes/bs/bs-months-calendar-view.component.ts new file mode 100644 index 0000000000..b569a9ac30 --- /dev/null +++ b/src/datepicker/themes/bs/bs-months-calendar-view.component.ts @@ -0,0 +1,61 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + BsDatepickerViewMode, BsNavigationDirection, + BsNavigationEvent, + DatepickerRenderOptions, CellHoverEvent, MonthsCalendarViewModel, + CalendarCellViewModel +} from '../../models/index'; + +@Component({ + selector: 'bs-month-calendar-view', + template: ` + + + + + + + + + +
+ {{ month.label }} +
+
+ ` +}) +export class BsMonthCalendarViewComponent { + @Input() calendar: MonthsCalendarViewModel; + + @Output() onNavigate = new EventEmitter(); + @Output() onViewMode = new EventEmitter(); + + @Output() onSelect = new EventEmitter(); + @Output() onHover = new EventEmitter(); + + navigateTo(event: BsNavigationDirection): void { + const step = BsNavigationDirection.DOWN === event ? -1 : 1; + this.onNavigate.emit({step: {year: step}}); + } + + viewMonth(month: CalendarCellViewModel) { + this.onSelect.emit(month); + } + + hoverMonth(cell: CalendarCellViewModel, isHovered: boolean) { + this.onHover.emit({cell, isHovered}); + } + + changeViewMode(event: BsDatepickerViewMode): void { + this.onViewMode.emit(event); + } +} diff --git a/src/datepicker/themes/bs/bs-timepicker-view.component.ts b/src/datepicker/themes/bs/bs-timepicker-view.component.ts new file mode 100644 index 0000000000..a711cacf98 --- /dev/null +++ b/src/datepicker/themes/bs/bs-timepicker-view.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'bs-timepicker', + template:` +
+
+ + + +
+
+ + + +
+ +
+` +}) +export class BsTimepickerViewComponent { + ampm = 'ok'; + hours = 0; + minutes = 0; +} diff --git a/src/datepicker/themes/bs/bs-years-calendar-view.component.ts b/src/datepicker/themes/bs/bs-years-calendar-view.component.ts new file mode 100644 index 0000000000..1e312e47c2 --- /dev/null +++ b/src/datepicker/themes/bs/bs-years-calendar-view.component.ts @@ -0,0 +1,64 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { yearsPerCalendar } from '../../engine/format-years-calendar'; +import { + BsDatepickerViewMode, + BsNavigationDirection, + BsNavigationEvent, + CalendarCellViewModel, + CellHoverEvent, + YearsCalendarViewModel +} from '../../models/index'; + +@Component({ + selector: 'bs-years-calendar-view', + template: ` + + + + + + + + + +
+ {{ year.label }} +
+
+ ` +}) +export class BsYearsCalendarViewComponent { + @Input() calendar: YearsCalendarViewModel; + + @Output() onNavigate = new EventEmitter(); + @Output() onViewMode = new EventEmitter(); + + @Output() onSelect = new EventEmitter(); + @Output() onHover = new EventEmitter(); + + navigateTo(event: BsNavigationDirection): void { + const step = BsNavigationDirection.DOWN === event ? -1 : 1; + this.onNavigate.emit({step: {year: step * yearsPerCalendar}}); + } + + viewYear(year: CalendarCellViewModel) { + this.onSelect.emit(year); + } + + hoverYear(cell: CalendarCellViewModel, isHovered: boolean) { + this.onHover.emit({cell, isHovered}); + } + + changeViewMode(event: BsDatepickerViewMode): void { + this.onViewMode.emit(event); + } +} diff --git a/src/datepicker/utils/bs-calendar-utils.ts b/src/datepicker/utils/bs-calendar-utils.ts index 18dc1bb445..d6c9fb6b37 100644 --- a/src/datepicker/utils/bs-calendar-utils.ts +++ b/src/datepicker/utils/bs-calendar-utils.ts @@ -1,5 +1,7 @@ import { getDayOfWeek, isFirstDayOfWeek } from '../../bs-moment/utils/date-getters'; import { shiftDate } from '../../bs-moment/utils/date-setters'; +import { isSameOrAfter, isSameOrBefore } from '../../bs-moment/utils/date-compare'; +import { endOf, startOf } from '../../bs-moment/utils/start-end-of'; export function getStartingDayOfCalendar(date: Date, options: {firstDayOfWeek?: number}): Date { if (isFirstDayOfWeek(date, options.firstDayOfWeek)) { @@ -10,3 +12,17 @@ export function getStartingDayOfCalendar(date: Date, options: {firstDayOfWeek?: return shiftDate(date, {day: -weekDay}); } + +export function isMonthDisabled(date: Date, min: Date, max: Date): boolean { + const minBound = min && isSameOrBefore(endOf(date, 'month'), min, 'day'); + const maxBound = max && isSameOrAfter(startOf(date, 'month'), max, 'day'); + + return minBound || maxBound; +} + +export function isYearDisabled(date: Date, min: Date, max: Date): boolean { + const minBound = min && isSameOrBefore(endOf(date, 'year'), min, 'day'); + const maxBound = max && isSameOrAfter(startOf(date, 'year'), max, 'day'); + + return minBound || maxBound; +} diff --git a/src/datepicker/utils/matrix-utils.ts b/src/datepicker/utils/matrix-utils.ts new file mode 100644 index 0000000000..143fb28a99 --- /dev/null +++ b/src/datepicker/utils/matrix-utils.ts @@ -0,0 +1,25 @@ +import { TimeUnit } from '../../bs-moment/types'; +import { shiftDate } from '../../bs-moment/utils/date-setters'; + +export type CreateMatrixCb = (date: Date) => T; + +export interface MatrixOptions { + height: number; + width: number; + initialDate: Date; + shift: TimeUnit; +} + +export function createMatrix(options: MatrixOptions, fn: CreateMatrixCb): T[][] { + let prevValue = options.initialDate; + const matrix: T[][] = new Array(options.height); + for (let i = 0; i < options.height; i++) { + matrix[i] = new Array(options.width); + for (let j = 0; j < options.width; j++) { + matrix[i][j] = fn(prevValue); + prevValue = shiftDate(prevValue, options.shift); + } + } + + return matrix; +} diff --git a/src/datepicker/utils/scss/variables.scss b/src/datepicker/utils/scss/variables.scss index 58e24ca33b..d3904bc6c5 100644 --- a/src/datepicker/utils/scss/variables.scss +++ b/src/datepicker/utils/scss/variables.scss @@ -1,4 +1,8 @@ $main-bg: #fff; +$custom-range-bg: #eee; + +$main-box-shadow: #aaa; + $font-color-01: #fff; $font-color-02: #9aaec1; $font-color-03: #54708b; diff --git a/tslint.json b/tslint.json index 24d48334bb..39222b8254 100644 --- a/tslint.json +++ b/tslint.json @@ -14,6 +14,7 @@ "directive-selector": false, "component-selector": false, "no-magic-numbers": false, - "no-any": false + "no-any": false, + "prefer-object-spread": false } }