diff --git a/.angular-cli.json b/.angular-cli.json index b52807775a..7f646ebc57 100644 --- a/.angular-cli.json +++ b/.angular-cli.json @@ -18,6 +18,7 @@ "prefix": "", "mobile": false, "styles": [ + "../../src/datepicker/bs-datepicker.css" ], "scripts": [], "environmentSource": "environments/environment.ts", diff --git a/.travis.yml b/.travis.yml index c5d6dc4072..b532e7da1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ sudo: false language: node_js -node_js: "7" +node_js: "8" dist: precise env: @@ -19,7 +19,9 @@ script: - rm -rf node_modules/ngx-bootstrap - npm i ./dist - npm run demo.build - - npm run test-coverage + # istanbul is broken, should be fixed + #- npm run test-coverage + - ./node_modules/.bin/ng test -sr after_success: - ./node_modules/.bin/codecov diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 80dc539422..143ef6255f 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -10,24 +10,7 @@ import { MainMenuComponent } from './common/main-menu/main-menu.component'; import { TopMenuComponent } from './common/top-menu/top-menu.component'; import { SearchFilterPipe } from './common/main-menu/search-filter.pipe'; import { AppFooterComponent } from './common/app-footer/app-footer.component'; -// will be lazy loaded later -// import { DemoAccordionModule } from './components/+accordion'; -// import { DemoAlertsModule } from './components/+alerts'; -// import { DemoButtonsModule } from './components/+buttons'; -// import { DemoCarouselModule } from './components/+carousel'; -// import { DemoCollapseModule } from './components/+collapse'; -// import { DemoDatepickerModule } from './components/+datepicker'; -// import { DemoDropdownModule } from './components/+dropdown'; -// import { DemoModalModule } from './components/+modal'; -// import { DemoPaginationModule } from './components/+pagination'; -// import { DemoPopoverModule } from './components/+popover/index'; -// import { DemoProgressbarModule } from './components/+progressbar'; -// import { DemoRatingModule } from './components/+rating'; -// import { DemoSortableModule } from './components/+sortable'; -// import { DemoTabsModule } from './components/+tabs'; -// import { DemoTimepickerModule } from './components/+timepicker/index'; -// import { DemoTooltipModule } from './components/+tooltip/index'; -// import { DemoTypeaheadModule } from './components/+typeahead/index'; + import { NgApiDocModule } from './api-docs/index'; import { NgApiDoc } from './api-docs/api-docs.model'; import { ngdoc } from '../ng-api-doc'; @@ -46,25 +29,7 @@ import { ngdoc } from '../ng-api-doc'; BrowserModule, FormsModule, RouterModule.forRoot(routes, {useHash: true}), - Ng2PageScrollModule.forRoot(), - // will be lazy loaded later on - // DemoAccordionModule, - // DemoAlertsModule, - // DemoButtonsModule, - // DemoCarouselModule, - // DemoCollapseModule, - // DemoDatepickerModule, - // DemoDropdownModule, - // DemoModalModule, - // DemoPaginationModule, - // DemoPopoverModule, - // DemoProgressbarModule, - // DemoRatingModule, - // DemoSortableModule, - // DemoTabsModule, - // DemoTimepickerModule, - // DemoTooltipModule, - // DemoTypeaheadModule + Ng2PageScrollModule.forRoot() ], providers: [ {provide: NgApiDoc, useValue: ngdoc} diff --git a/demo/src/app/app.routing.ts b/demo/src/app/app.routing.ts index 1ac627dca5..8d5a73525c 100644 --- a/demo/src/app/app.routing.ts +++ b/demo/src/app/app.routing.ts @@ -1,21 +1,4 @@ import { GettingStartedComponent } from './getting-started/getting-started.component'; -// import { AccordionSectionComponent } from './components/accordion/accordion-section.component'; -// import { AlertsSectionComponent } from './components/+alerts/alerts-section.component'; -// import { ButtonsSectionComponent } from './components/+buttons/buttons-section.component'; -// import { CarouselSectionComponent } from './components/+carousel/carousel-section.component'; -// import { CollapseSectionComponent } from './components/+collapse/collapse-section.component'; -// import { DatepickerSectionComponent } from './components/+datepicker/datepicker-section.component'; -// import { DropdownSectionComponent } from './components/+dropdown/dropdown-section.component'; -// import { ModalSectionComponent } from './components/+modal/modal-section.component'; -// import { ProgressbarSectionComponent } from './components/+progressbar/progressbar-section.component'; -// import { PaginationSectionComponent } from './components/+pagination/pagination-section.component'; -// import { RatingSectionComponent } from './components/+rating/rating-section.component'; -// import { SortableSectionComponent } from './components/+sortable/sortable-section.component'; -// import { TabsSectionComponent } from './components/+tabs/tabs-section.component'; -// import { TimepickerSectionComponent } from './components/+timepicker/timepicker-section.component'; -// import { TooltipSectionComponent } from './components/+tooltip/tooltip-section.component'; -// import { TypeaheadSectionComponent } from './components/+typeahead/typeahead-section.component'; -// import { PopoverSectionComponent } from './components/+popover/popover-section.component'; export const routes = [ { diff --git a/demo/src/app/components/+datepicker/datepicker-section.component.ts b/demo/src/app/components/+datepicker/datepicker-section.component.ts index d1cd7bb491..0aa4375274 100644 --- a/demo/src/app/components/+datepicker/datepicker-section.component.ts +++ b/demo/src/app/components/+datepicker/datepicker-section.component.ts @@ -30,7 +30,11 @@ let titleDoc = require('html-loader!markdown-loader!./docs/title.md');

Examples

- + + + + + diff --git a/demo/src/app/components/+datepicker/demo-datepicker.module.ts b/demo/src/app/components/+datepicker/demo-datepicker.module.ts index 3be1a70b95..f0d5d18b84 100644 --- a/demo/src/app/components/+datepicker/demo-datepicker.module.ts +++ b/demo/src/app/components/+datepicker/demo-datepicker.module.ts @@ -2,20 +2,23 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; -import { DatepickerModule } from 'ngx-bootstrap/datepicker'; +import { DatepickerModule, BsDatepickerModule } from 'ngx-bootstrap/datepicker'; import { SharedModule } from '../../shared'; import { DatepickerSectionComponent } from './datepicker-section.component'; import { DEMO_COMPONENTS } from './demos'; import { routes } from './demo-datepicker.routes'; +import { DemoDatePickerPopupComponent } from './demos/bs-popup/date-picker-popup'; @NgModule({ declarations:[ + DemoDatePickerPopupComponent, DatepickerSectionComponent, ...DEMO_COMPONENTS ], imports:[ DatepickerModule.forRoot(), + BsDatepickerModule.forRoot(), CommonModule, FormsModule, SharedModule, 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 new file mode 100644 index 0000000000..c66c0c2f4f --- /dev/null +++ b/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.html @@ -0,0 +1,9 @@ +
{{bsValue}}
+ + + +
+ +
{{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 new file mode 100644 index 0000000000..0066e72377 --- /dev/null +++ b/demo/src/app/components/+datepicker/demos/bs-popup/date-picker-popup.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'demo-date-picker-popup', + templateUrl: './date-picker-popup.html' +}) +export class DemoDatePickerPopupComponent { + public bsValue: any = new Date(2017, 7, 4); + public bsRangeValue: any = [new Date(2017, 7, 4), new Date(2017, 7, 20)]; +} diff --git a/demo/src/app/components/+datepicker/demos/datepicker-demo.component.ts b/demo/src/app/components/+datepicker/demos/datepicker-demo.component.ts index 2f37a57bab..b1c6581b81 100644 --- a/demo/src/app/components/+datepicker/demos/datepicker-demo.component.ts +++ b/demo/src/app/components/+datepicker/demos/datepicker-demo.component.ts @@ -1,5 +1,4 @@ import { Component } from '@angular/core'; -import * as moment from 'moment'; @Component({ selector: 'datepicker-demo', @@ -41,8 +40,7 @@ export class DatepickerDemoComponent { } public d20090824(): void { - this.dt = moment('2009-08-24', 'YYYY-MM-DD') - .toDate(); + this.dt = new Date(2009,7,24); } public disableTomorrow(): void { diff --git a/demo/src/app/components/+datepicker/demos/index.ts b/demo/src/app/components/+datepicker/demos/index.ts index 6ba54cda43..17a76d259c 100644 --- a/demo/src/app/components/+datepicker/demos/index.ts +++ b/demo/src/app/components/+datepicker/demos/index.ts @@ -8,5 +8,9 @@ export const DEMOS = { old: { component: require('!!raw-loader?lang=typescript!./datepicker-demo.component.ts'), html: require('!!raw-loader?lang=markup!./datepicker-demo.component.html') + }, + pop: { + component: require('!!raw-loader?lang=typescript!./bs-popup/date-picker-popup.ts'), + html: require('!!raw-loader?lang=markup!./bs-popup/date-picker-popup.html') } }; diff --git a/demo/src/ng-api-doc.ts b/demo/src/ng-api-doc.ts index 1b1871a684..a52f0552d7 100644 --- a/demo/src/ng-api-doc.ts +++ b/demo/src/ng-api-doc.ts @@ -131,6 +131,20 @@ export const ngdoc: any = { } ] }, + "LocaleOptionsFormat": { + "fileName": "src/bs-moment/locale/locale.class.ts", + "className": "LocaleOptionsFormat", + "description": "", + "methods": [], + "properties": [] + }, + "LocaleData": { + "fileName": "src/datepicker/models/index.ts", + "className": "LocaleData", + "description": "", + "methods": [], + "properties": [] + }, "ButtonCheckboxDirective": { "fileName": "src/buttons/button-checkbox.directive.ts", "className": "ButtonCheckboxDirective", @@ -290,13 +304,13 @@ export const ngdoc: any = { }, { "name": "getCurrentSlideIndex", - "description": "

Finds and returns index of currently displayed slide\n@returns {number}

\n", + "description": "

Finds and returns index of currently displayed slide

\n", "args": [], "returnType": "number" }, { "name": "isLast", - "description": "

Defines, whether the specified index is last in collection\n@returns {boolean}

\n", + "description": "

Defines, whether the specified index is last in collection

\n", "args": [ { "name": "index", @@ -304,6 +318,44 @@ export const ngdoc: any = { } ], "returnType": "boolean" + }, + { + "name": "findNextSlideIndex", + "description": "

Defines next slide index, depending of direction

\n", + "args": [ + { + "name": "direction", + "type": "Direction" + }, + { + "name": "force", + "type": "boolean" + } + ], + "returnType": "number" + }, + { + "name": "_select", + "description": "

Sets a slide, which specified through index, as active

\n", + "args": [ + { + "name": "index", + "type": "number" + } + ], + "returnType": "void" + }, + { + "name": "restartTimer", + "description": "

Starts loop of auto changing of slides

\n", + "args": [], + "returnType": "any" + }, + { + "name": "resetTimer", + "description": "

Stops loop of auto changing of slides

\n", + "args": [], + "returnType": "void" } ] }, @@ -423,28 +475,158 @@ export const ngdoc: any = { "fileName": "src/component-loader/component-loader.factory.ts", "className": "ComponentLoaderFactory", "description": "", + "methods": [], + "properties": [] + }, + "BsDatepickerConfig": { + "fileName": "src/datepicker/bs-datepicker-config.ts", + "className": "BsDatepickerConfig", + "description": "", + "methods": [], + "properties": [] + }, + "BsDatepickerComponent": { + "fileName": "src/datepicker/bs-datepicker.component.ts", + "className": "BsDatepickerComponent", + "description": "", + "selector": "bs-datepicker", + "exportAs": "bsDatepicker", + "inputs": [ + { + "name": "container", + "defaultValue": "body", + "type": "string", + "description": "

A selector specifying the element the popover should be appended to.\nCurrently only supports "body".

\n" + }, + { + "name": "isOpen", + "type": "boolean", + "description": "

Returns whether or not the popover is currently being shown

\n" + }, + { + "name": "placement", + "defaultValue": "bottom", + "type": "\"top\" | \"bottom\" | \"left\" | \"right\"", + "description": "

Placement of a popover. Accepts: "top", "bottom", "left", "right"

\n" + }, + { + "name": "triggers", + "defaultValue": "click", + "type": "string", + "description": "

Specifies events that should trigger. Supports a space separated list of\nevent names.

\n" + }, + { + "name": "value", + "type": "Date", + "description": "" + } + ], + "outputs": [ + { + "name": "onHidden", + "description": "

Emits an event when the popover is hidden

\n" + }, + { + "name": "onShown", + "description": "

Emits an event when the popover is shown

\n" + }, + { + "name": "valueChange", + "description": "" + } + ], + "properties": [], "methods": [ { - "name": "createLoader", - "description": "

@returns {ComponentLoader}

\n", - "args": [ - { - "name": "_elementRef", - "type": "ElementRef" - }, - { - "name": "_viewContainerRef", - "type": "ViewContainerRef" - }, - { - "name": "_renderer", - "type": "Renderer" - } - ], - "returnType": "ComponentLoader" + "name": "show", + "description": "

Opens an element’s datepicker. This is considered a “manual” triggering of\nthe datepicker.

\n", + "args": [], + "returnType": "void" + }, + { + "name": "hide", + "description": "

Closes an element’s datepicker. This is considered a “manual” triggering of\nthe datepicker.

\n", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles an element’s datepicker. This is considered a “manual” triggering of\nthe datepicker.

\n", + "args": [], + "returnType": "void" + } + ] + }, + "BsDaterangepickerComponent": { + "fileName": "src/datepicker/bs-daterangepicker.component.ts", + "className": "BsDaterangepickerComponent", + "description": "", + "selector": "bs-daterangepicker", + "inputs": [ + { + "name": "container", + "defaultValue": "body", + "type": "string", + "description": "

A selector specifying the element the popover should be appended to.\nCurrently only supports "body".

\n" + }, + { + "name": "isOpen", + "type": "boolean", + "description": "

Returns whether or not the popover is currently being shown

\n" + }, + { + "name": "placement", + "defaultValue": "bottom", + "type": "\"top\" | \"bottom\" | \"left\" | \"right\"", + "description": "

Placement of a popover. Accepts: "top", "bottom", "left", "right"

\n" + }, + { + "name": "triggers", + "defaultValue": "click", + "type": "string", + "description": "

Specifies events that should trigger. Supports a space separated list of\nevent names.

\n" + }, + { + "name": "value", + "type": "Date[]", + "description": "" } ], - "properties": [] + "outputs": [ + { + "name": "onHidden", + "description": "

Emits an event when the popover is hidden

\n" + }, + { + "name": "onShown", + "description": "

Emits an event when the popover is shown

\n" + }, + { + "name": "valueChange", + "description": "" + } + ], + "properties": [], + "methods": [ + { + "name": "show", + "description": "

Opens an element’s datepicker. This is considered a “manual” triggering of\nthe datepicker.

\n", + "args": [], + "returnType": "void" + }, + { + "name": "hide", + "description": "

Closes an element’s datepicker. This is considered a “manual” triggering of\nthe datepicker.

\n", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles an element’s datepicker. This is considered a “manual” triggering of\nthe datepicker.

\n", + "args": [], + "returnType": "void" + } + ] }, "DatePickerInnerComponent": { "fileName": "src/datepicker/datepicker-inner.component.ts", @@ -729,6 +911,83 @@ export const ngdoc: any = { "properties": [], "methods": [] }, + "FlagMonthViewOptions": { + "fileName": "src/datepicker/engine/flag-month-view.ts", + "className": "FlagMonthViewOptions", + "description": "", + "methods": [], + "properties": [] + }, + "DaysCalendarModel": { + "fileName": "src/datepicker/models/index.ts", + "className": "DaysCalendarModel", + "description": "", + "methods": [], + "properties": [] + }, + "DayViewModel": { + "fileName": "src/datepicker/models/index.ts", + "className": "DayViewModel", + "description": "", + "methods": [], + "properties": [] + }, + "WeekViewModel": { + "fileName": "src/datepicker/models/index.ts", + "className": "WeekViewModel", + "description": "", + "methods": [], + "properties": [] + }, + "MonthViewModel": { + "fileName": "src/datepicker/models/index.ts", + "className": "MonthViewModel", + "description": "", + "methods": [], + "properties": [] + }, + "MonthViewOptions": { + "fileName": "src/datepicker/models/index.ts", + "className": "MonthViewOptions", + "description": "", + "methods": [], + "properties": [] + }, + "DatepickerFormatOptions": { + "fileName": "src/datepicker/models/index.ts", + "className": "DatepickerFormatOptions", + "description": "", + "methods": [], + "properties": [] + }, + "DatepickerRenderOptions": { + "fileName": "src/datepicker/models/index.ts", + "className": "DatepickerRenderOptions", + "description": "", + "methods": [], + "properties": [] + }, + "TimeUnit": { + "fileName": "src/datepicker/models/index.ts", + "className": "TimeUnit", + "description": "", + "methods": [], + "properties": [] + }, + "BsNavigationEvent": { + "fileName": "src/datepicker/models/index.ts", + "className": "BsNavigationEvent", + "description": "", + "methods": [], + "properties": [] + }, + "DayHoverEvent": { + "fileName": "src/datepicker/models/index.ts", + "className": "DayHoverEvent", + "description": "", + "methods": [], + "properties": [] + }, "MonthPickerComponent": { "fileName": "src/datepicker/monthpicker.component.ts", "className": "MonthPickerComponent", @@ -739,6 +998,179 @@ export const ngdoc: any = { "properties": [], "methods": [] }, + "BsDatepickerActions": { + "fileName": "src/datepicker/reducer/bs-datepicker.actions.ts", + "className": "BsDatepickerActions", + "description": "", + "methods": [], + "properties": [] + }, + "BsDatepickerEffects": { + "fileName": "src/datepicker/reducer/bs-datepicker.effects.ts", + "className": "BsDatepickerEffects", + "description": "", + "methods": [], + "properties": [] + }, + "BsDatepickerStore": { + "fileName": "src/datepicker/reducer/bs-datepicker.store.ts", + "className": "BsDatepickerStore", + "description": "", + "methods": [], + "properties": [] + }, + "BsDatepickerContainerComponent": { + "fileName": "src/datepicker/themes/bs/bs-datepicker-container.component.ts", + "className": "BsDatepickerContainerComponent", + "description": "", + "selector": "bs-datepicker-container", + "inputs": [ + { + "name": "value", + "type": "Date", + "description": "" + } + ], + "outputs": [ + { + "name": "valueChange", + "description": "" + } + ], + "properties": [], + "methods": [] + }, + "BsDatepickerDayViewComponent": { + "fileName": "src/datepicker/themes/bs/bs-datepicker-day-view.component.ts", + "className": "BsDatepickerDayViewComponent", + "description": "", + "selector": "bs-datepicker-day-view", + "inputs": [ + { + "name": "day", + "type": "DayViewModel", + "description": "" + } + ], + "outputs": [ + { + "name": "onHover", + "description": "" + }, + { + "name": "onSelect", + "description": "" + } + ], + "properties": [], + "methods": [] + }, + "BsDatepickerMonthViewComponent": { + "fileName": "src/datepicker/themes/bs/bs-datepicker-month-view.component.ts", + "className": "BsDatepickerMonthViewComponent", + "description": "", + "selector": "bs-datepicker-month-view", + "inputs": [ + { + "name": "month", + "type": "MonthViewModel", + "description": "" + }, + { + "name": "options", + "type": "DatepickerRenderOptions", + "description": "" + } + ], + "outputs": [ + { + "name": "onHover", + "description": "" + }, + { + "name": "onSelect", + "description": "" + } + ], + "properties": [], + "methods": [] + }, + "BsDatepickerNavigationViewComponent": { + "fileName": "src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts", + "className": "BsDatepickerNavigationViewComponent", + "description": "", + "selector": "bs-datepicker-navigation-view", + "inputs": [ + { + "name": "month", + "type": "MonthViewModel", + "description": "" + } + ], + "outputs": [ + { + "name": "onNavigate", + "description": "" + } + ], + "properties": [], + "methods": [] + }, + "BsDatepickerViewComponent": { + "fileName": "src/datepicker/themes/bs/bs-datepicker-view.component.ts", + "className": "BsDatepickerViewComponent", + "description": "", + "selector": "bs-datepicker-view", + "inputs": [ + { + "name": "months", + "type": "MonthViewModel[]", + "description": "" + }, + { + "name": "options", + "type": "DatepickerRenderOptions", + "description": "" + } + ], + "outputs": [ + { + "name": "onHover", + "description": "" + }, + { + "name": "onNavigate", + "description": "" + }, + { + "name": "onSelect", + "description": "" + } + ], + "properties": [], + "methods": [] + }, + "BsDaterangepickerContainerComponent": { + "fileName": "src/datepicker/themes/bs/bs-daterangepicker-container.component.ts", + "className": "BsDaterangepickerContainerComponent", + "description": "", + "selector": "bs-daterangepicker-container", + "inputs": [ + { + "name": "value", + "type": "Date[]", + "description": "" + } + ], + "outputs": [ + { + "name": "valueChange", + "description": "" + } + ], + "properties": [], + "methods": [] + }, "YearPickerComponent": { "fileName": "src/datepicker/yearpicker.component.ts", "className": "YearPickerComponent", @@ -898,7 +1330,7 @@ export const ngdoc: any = { "properties": [ { "name": "dropdownMenu", - "type": "any", + "type": "Promise>", "description": "

Content to be displayed as popover.

\n" } ] @@ -929,6 +1361,12 @@ export const ngdoc: any = { } ], "returnType": "BsModalRef" + }, + { + "name": "checkScrollbar", + "description": "

AFTER PR MERGE MODAL.COMPONENT WILL BE USING THIS CODE\nScroll bar tricks

\n", + "args": [], + "returnType": "void" } ], "properties": [] @@ -1078,11 +1516,23 @@ export const ngdoc: any = { ], "returnType": "void" }, + { + "name": "showElement", + "description": "

Show dialog

\n", + "args": [], + "returnType": "void" + }, { "name": "focusOtherModal", "description": "

Events tricks

\n", "args": [], "returnType": "void" + }, + { + "name": "checkScrollbar", + "description": "

Scroll bar tricks

\n", + "args": [], + "returnType": "void" } ] }, @@ -1391,13 +1841,6 @@ export const ngdoc: any = { } ] }, - "Positioning": { - "fileName": "src/positioning/ng-positioning.ts", - "className": "Positioning", - "description": "

@copyright Valor Software\n@copyright Angular ng-bootstrap team

\n", - "methods": [], - "properties": [] - }, "PositioningOptions": { "fileName": "src/positioning/positioning.service.ts", "className": "PositioningOptions", @@ -1416,7 +1859,7 @@ export const ngdoc: any = { }, { "name": "element", - "type": "string | ElementRef | HTMLElement", + "type": "string | HTMLElement | ElementRef", "description": "

The DOM element, ElementRef, or a selector string of an element which will be moved

\n" }, { @@ -1426,7 +1869,7 @@ export const ngdoc: any = { }, { "name": "target", - "type": "string | ElementRef | HTMLElement", + "type": "string | HTMLElement | ElementRef", "description": "

The DOM element, ElementRef, or a selector string of an element which the element will be attached to

\n" }, { @@ -1672,7 +2115,7 @@ export const ngdoc: any = { "outputs": [ { "name": "onChange", - "description": "

fired on array change (reordering, insert, remove), same as ngModelChange.\n Returns new items collection as a payload.

\n" + "description": "

fired on array change (reordering, insert, remove), same as ngModelChange.\nReturns new items collection as a payload.

\n" } ], "properties": [], @@ -2076,59 +2519,59 @@ export const ngdoc: any = { "name": "tooltipAnimation", "defaultValue": "true", "type": "boolean", - "description": "

@deprecated - removed, will be added to configuration

\n" + "description": "" }, { "name": "tooltipAppendToBody", "type": "boolean", - "description": "

@deprecated - please use container="body" instead

\n" + "description": "" }, { "name": "tooltipClass", "type": "string", - "description": "

@deprecated - will replaced with customClass

\n" + "description": "" }, { "name": "tooltipContext", "type": "any", - "description": "

@deprecated - removed

\n" + "description": "" }, { "name": "tooltipEnable", "type": "boolean", - "description": "

@deprecated - please use isDisabled instead

\n" + "description": "" }, { "name": "tooltipFadeDuration", "defaultValue": "150", "type": "number", - "description": "

@deprecated

\n" + "description": "" }, { "name": "tooltipHtml", "type": "string | TemplateRef", - "description": "

@deprecated - please use tooltip instead

\n" + "description": "" }, { "name": "tooltipIsOpen", "type": "boolean", - "description": "

@deprecated - please use isOpen instead

\n" + "description": "" }, { "name": "tooltipPlacement", "type": "string", - "description": "

@deprecated - please use placement instead

\n" + "description": "" }, { "name": "tooltipPopupDelay", "defaultValue": "0", "type": "number", - "description": "

@deprecated

\n" + "description": "" }, { "name": "tooltipTrigger", "type": "string | string[]", - "description": "

@deprecated - please use triggers instead

\n" + "description": "" }, { "name": "triggers", @@ -2151,26 +2594,10 @@ export const ngdoc: any = { }, { "name": "tooltipStateChanged", - "description": "

@deprecated

\n" - } - ], - "properties": [ - { - "name": "_appendToBody", - "type": "boolean", - "description": "

@deprecated - please use container="body" instead

\n" - }, - { - "name": "_enable", - "type": "boolean", - "description": "

@deprecated - please use isDisabled instead

\n" - }, - { - "name": "_isOpen", - "type": "boolean", - "description": "

@deprecated - please use isOpen instead

\n" + "description": "" } ], + "properties": [], "methods": [ { "name": "toggle", @@ -2310,12 +2737,5 @@ export const ngdoc: any = { } ], "methods": [] - }, - "Trigger": { - "fileName": "src/utils/trigger.class.ts", - "className": "Trigger", - "description": "

@copyright Valor Software\n@copyright Angular ng-bootstrap team

\n", - "methods": [], - "properties": [] } }; diff --git a/package.json b/package.json index 6dafc89757..ecde384ab1 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@angular/forms": "^2.3.1 || >=4.0.0" }, "devDependencies": { - "@angular/cli": "^1.0.6", + "@angular/cli": "1.3.0", "@angular/common": "^2.4.4", "@angular/compiler": "^2.4.4", "@angular/compiler-cli": "^2.4.4", @@ -113,17 +113,17 @@ "markdown-loader": "github:valorkin/markdown-loader", "marked": "0.3.6", "ng2-page-scroll": "4.0.0-beta.7", - "ngm-cli": "0.5.2", + "ngm-cli": "0.6.1", "npm-run-all": "4.0.2", "protractor": "5.1.1", "reflect-metadata": "0.1.10", "require-dir": "0.3.1", - "rxjs": "5.3.0", + "rxjs": "5.4.3", "ts-helpers": "^1.1.1", "tslint": "4.5.1", "tslint-config-valorsoft": "1.2.0", "typedoc": "0.5.9", - "typescript": "2.0.10", + "typescript": "2.4.2", "wallaby-webpack": "0.0.37", "webdriver-manager": "12.0.4", "webpack-bundle-analyzer": "2.8.2", diff --git a/src/bs-moment/LICENSE b/src/bs-moment/LICENSE new file mode 100644 index 0000000000..86b546c2ff --- /dev/null +++ b/src/bs-moment/LICENSE @@ -0,0 +1,26 @@ +The MIT License (MIT) + +Copyright (c) Valor Software +Copyright (c) Dmitriy Shekhovtsov +Copyright (c) JS Foundation and other contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/bs-moment/format-functions.ts b/src/bs-moment/format-functions.ts new file mode 100644 index 0000000000..697b33e1dc --- /dev/null +++ b/src/bs-moment/format-functions.ts @@ -0,0 +1,63 @@ +import { Locale } from './locale/locale.class'; +import { DateFormatterFn } from '../datepicker/models/index'; +import { zeroFill } from './utils'; +import { isFunction } from './utils/type-checks'; + +export let formatFunctions: {[key:string]: (date: Date, locale: Locale) => string} = {}; +export let formatTokenFunctions: { [key: string]: DateFormatterFn } = {}; + +export const formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; + +// token: 'M' +// padded: ['MM', 2] +// ordinal: 'Mo' +// callback: function () { this.month() + 1 } +export function addFormatToken(token: string, + padded: {[key: number]: any}, + ordinal: string, + callback: DateFormatterFn): void { + let func: DateFormatterFn = callback; + if (token) { + formatTokenFunctions[token] = func; + } + if (padded as {[key: number]: any}) { + let key = padded[0] as string; + formatTokenFunctions[key] = function (date: Date, format: string, locale?: Locale): string { + return zeroFill(func.apply(null, arguments), padded[1] as number, padded[2] as boolean); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function (date: Date, format: string, locale: Locale): string { + // todo: fix this + return locale.ordinal(func.apply(null, arguments), token); + }; + } +} + +export function makeFormatFunction(format: string): (date: Date, locale: Locale) => string { + const array: string[] = format.match(formattingTokens); + const length = array.length; + const formatArr: (string[] | DateFormatterFn[]) = new Array(length); + for (let i = 0; i < length; i++) { + formatArr[i] = formatTokenFunctions[array[i]] + ? formatTokenFunctions[array[i]] + : removeFormattingTokens(array[i]); + } + + return function (date: Date, locale: Locale): string { + let output = ''; + for (let j = 0; j < length; j++) { + output += isFunction(formatArr[j] as DateFormatterFn) + ? (formatArr[j] as DateFormatterFn).call(null, date, format, locale) + : formatArr[j]; + } + return output; + }; +} + +function removeFormattingTokens(input: string): string { + if (input.match(/\[[\s\S]/)) { + return input.replace(/^\[|\]$/g, ''); + } + return input.replace(/\\/g, ''); +} diff --git a/src/bs-moment/format.ts b/src/bs-moment/format.ts new file mode 100644 index 0000000000..a39b17ad6a --- /dev/null +++ b/src/bs-moment/format.ts @@ -0,0 +1,28 @@ +// moment.js +// version : 2.18.1 +// authors : Tim Wood, Iskren Chernev, Moment.js contributors +// license : MIT +// momentjs.com + +import { formatFunctions, makeFormatFunction } from './format-functions'; +import './locale'; +import { Locale } from './locale/locale.class'; +import { getLocale } from './locale/locales.service'; +import './units'; +import { isDateValid } from './utils/type-checks'; + +export function formatDate(date: Date, format: string, locale = 'en'): string { + const _locale = getLocale(locale); + const output = formatMoment(date, format, _locale); + return _locale.postformat(output); +} + +// format date using native date object +export function formatMoment(date: Date, format: string, locale: Locale) { + if (!isDateValid(date)) { + return locale.invalidDate; + } + + formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format); + return formatFunctions[format](date, locale); +} diff --git a/src/bs-moment/i18n/ar.ts b/src/bs-moment/i18n/ar.ts new file mode 100644 index 0000000000..40a92af378 --- /dev/null +++ b/src/bs-moment/i18n/ar.ts @@ -0,0 +1,131 @@ +// moment.js locale configuration +// locale : Arabic [ar] +// author : Abdel Said: https://github.com/abdelsaid +// author : Ahmed Elkhatib +// author : forabi https://github.com/forabi + +const symbolMap: { [key: string]: string } = { + '1': '١', + '2': '٢', + '3': '٣', + '4': '٤', + '5': '٥', + '6': '٦', + '7': '٧', + '8': '٨', + '9': '٩', + '0': '٠' +}; +const numberMap: { [key: string]: string } = { + '١': '1', + '٢': '2', + '٣': '3', + '٤': '4', + '٥': '5', + '٦': '6', + '٧': '7', + '٨': '8', + '٩': '9', + '٠': '0' +}; +const pluralForm = function (n: number): number { + return n === 0 ? 0 : n === 1 ? 1 : n === 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5; +}; +const plurals = { + s: ['أقل من ثانية', 'ثانية واحدة', ['ثانيتان', 'ثانيتين'], '%d ثوان', '%d ثانية', '%d ثانية'], + m: ['أقل من دقيقة', 'دقيقة واحدة', ['دقيقتان', 'دقيقتين'], '%d دقائق', '%d دقيقة', '%d دقيقة'], + h: ['أقل من ساعة', 'ساعة واحدة', ['ساعتان', 'ساعتين'], '%d ساعات', '%d ساعة', '%d ساعة'], + d: ['أقل من يوم', 'يوم واحد', ['يومان', 'يومين'], '%d أيام', '%d يومًا', '%d يوم'], + M: ['أقل من شهر', 'شهر واحد', ['شهران', 'شهرين'], '%d أشهر', '%d شهرا', '%d شهر'], + y: ['أقل من عام', 'عام واحد', ['عامان', 'عامين'], '%d أعوام', '%d عامًا', '%d عام'] +}; +const pluralize = function (u) { + return function (number, withoutSuffix, string, isFuture) { + const f = pluralForm(number); + let str = plurals[u][pluralForm(number)]; + if (f === 2) { + str = str[withoutSuffix ? 0 : 1]; + } + return str.replace(/%d/i, number); + }; +}; +const months = [ + 'كانون الثاني يناير', + 'شباط فبراير', + 'آذار مارس', + 'نيسان أبريل', + 'أيار مايو', + 'حزيران يونيو', + 'تموز يوليو', + 'آب أغسطس', + 'أيلول سبتمبر', + 'تشرين الأول أكتوبر', + 'تشرين الثاني نوفمبر', + 'كانون الأول ديسمبر' +]; + +export const arLocale = { + abbr: 'ar', + months: months, + monthsShort: months, + weekdays: 'الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت'.split('_'), + weekdaysShort: 'أحد_إثنين_ثلاثاء_أربعاء_خميس_جمعة_سبت'.split('_'), + weekdaysMin: 'ح_ن_ث_ر_خ_ج_س'.split('_'), + weekdaysParseExact: true, + longDateFormat: { + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'D/\u200FM/\u200FYYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd D MMMM YYYY HH:mm' + }, + meridiemParse: /ص|م/, + isPM: function (input) { + return 'م' === input; + }, + meridiem: function (hour, minute, isLower) { + if (hour < 12) { + return 'ص'; + } else { + return 'م'; + } + }, + calendar: { + sameDay: '[اليوم عند الساعة] LT', + nextDay: '[غدًا عند الساعة] LT', + nextWeek: 'dddd [عند الساعة] LT', + lastDay: '[أمس عند الساعة] LT', + lastWeek: 'dddd [عند الساعة] LT', + sameElse: 'L' + }, + relativeTime: { + future: 'بعد %s', + past: 'منذ %s', + s: pluralize('s'), + m: pluralize('m'), + mm: pluralize('m'), + h: pluralize('h'), + hh: pluralize('h'), + d: pluralize('d'), + dd: pluralize('d'), + M: pluralize('M'), + MM: pluralize('M'), + y: pluralize('y'), + yy: pluralize('y') + }, + preparse: function (string) { + return string.replace(/[١٢٣٤٥٦٧٨٩٠]/g, function (match) { + return numberMap[match]; + }).replace(/،/g, ','); + }, + postformat: function (string) { + return string.replace(/\d/g, function (match) { + return symbolMap[match]; + }).replace(/,/g, '،'); + }, + week: { + dow: 6, // Saturday is the first day of the week. + doy: 12 // The week that contains Jan 1st is the first week of the year. + } +}; diff --git a/src/bs-moment/locale/en.ts b/src/bs-moment/locale/en.ts new file mode 100644 index 0000000000..24b936086a --- /dev/null +++ b/src/bs-moment/locale/en.ts @@ -0,0 +1,14 @@ +import { getSetGlobalLocale } from './locales.service'; +import { toInt } from '../utils/type-checks'; + +getSetGlobalLocale('en', { + dayOfMonthOrdinalParse: /\d{1,2}(th|st|nd|rd)/, + ordinal(num: number): string { + const b = num % 10; + const output = (toInt(num % 100 / 10) === 1) ? 'th' : + (b === 1) ? 'st' : + (b === 2) ? 'nd' : + (b === 3) ? 'rd' : 'th'; + return num + output; + } +}); diff --git a/src/bs-moment/locale/index.ts b/src/bs-moment/locale/index.ts new file mode 100644 index 0000000000..3286a63a30 --- /dev/null +++ b/src/bs-moment/locale/index.ts @@ -0,0 +1 @@ +import './en'; diff --git a/src/bs-moment/locale/locale.class.ts b/src/bs-moment/locale/locale.class.ts new file mode 100644 index 0000000000..f4cebeb60e --- /dev/null +++ b/src/bs-moment/locale/locale.class.ts @@ -0,0 +1,149 @@ +import { weekOfYear } from '../units/week-calendar-utils'; +import { isArray, isFunction } from '../utils/type-checks'; +import { getDayOfWeek, getMonth } from '../utils/date-getters'; + +export interface LocaleOptionsFormat { + format: string[]; + standalone: string[]; + isFormat: RegExp; +} + +export type LocaleOptions = string[] | LocaleOptionsFormat; + +const MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/; +export const defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); +export const defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); +export const defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); +export const defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); +export const defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + +export interface LocaleData { + [key: string]: any; + + invalidDate?: string; + abbr?: string; + + months?: LocaleOptions; + monthsShort?: string[]; + weekdays?: LocaleOptions; + weekdaysMin?: string[]; + weekdaysShort?: string[]; + week?: { dow: number, doy: number }; + + dayOfMonthOrdinalParse?: RegExp; + ordinal?: (num: number) => string; + postformat?: (num: string) => string; +} + +export class Locale { + [key: string]: any; + _abbr: string; + _config: LocaleData; + invalidDate: string; + + private _months: LocaleOptions; + private _monthsShort: LocaleOptions; + + private _weekdays: LocaleOptions; + private _weekdaysShort: string[]; + private _weekdaysMin: string[]; + private _week: { dow: number, doy: number }; + + private _ordinal: string; + + constructor(config: LocaleData) { + if (!!config) { + this.set(config); + } + } + + set (config: LocaleData): void { + for (const i in config) { + if (!config.hasOwnProperty(i)) { + continue; + } + const prop = (config[i] as any); + const key = isFunction(prop) ? i : `_${i}`; + this[key] = prop; + } + + this._config = config; + } + + // Months + // LOCALES + months(date?: Date, format?: string): string | string[] { + if (!date) { + return isArray(this._months) + ? (this._months as string[]) + : (this._months as LocaleOptionsFormat).standalone; + } + + if (isArray(this._months)) { + return (this._months as string[])[getMonth(date)]; + } + + const key = ((this._months as LocaleOptionsFormat).isFormat || MONTHS_IN_FORMAT) + .test(format) ? 'format' : 'standalone'; + return ((this._months as any)[key] as string[])[getMonth(date)]; + } + + monthsShort(date?: Date, format?: string): string | string[] { + if (!date) { + return isArray(this._monthsShort) + ? (this._monthsShort as string[]) + : (this._monthsShort as LocaleOptionsFormat).standalone; + } + + if (isArray(this._monthsShort)) { + return (this._monthsShort as string[])[getMonth(date)]; + } + let key = MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'; + return ((this._monthsShort as any)[key] as string[])[getMonth(date)]; + } + + // Days of week +// LOCALES + + weekdays(date?: Date, format?: string): string | string[] { + const _isArray = isArray(this._weekdays as string[]); + if (!date) { + return _isArray + ? this._weekdays as string[] + : (this._weekdays as LocaleOptionsFormat).standalone; + } + + if (_isArray) { + return (this._weekdays as string[])[getDayOfWeek(date)]; + } + + const _key = (this._weekdays as LocaleOptionsFormat).isFormat.test(format) ? 'format' : 'standalone'; + return ((this._weekdays as any)[_key] as string[])[getDayOfWeek(date)]; + } + + weekdaysMin(date?: Date): string | string[] { + return (date) ? this._weekdaysShort[getDayOfWeek(date)] : this._weekdaysShort; + } + + weekdaysShort(date?: Date): string | string[] { + return (date) ? this._weekdaysMin[getDayOfWeek(date)] : this._weekdaysMin; + } + + week(date: Date): number { + return weekOfYear(date, this._week.dow, this._week.doy).week; + } + + firstDayOfWeek(): number { + return this._week.dow; + } + + firstDayOfYear(): number { + return this._week.doy; + } + + ordinal(num: number, token?: string): string { + return this._ordinal.replace('%d', num.toString(10)); + } + + postformat(str: string) { return str; } +} diff --git a/src/bs-moment/locale/locale.defaults.ts b/src/bs-moment/locale/locale.defaults.ts new file mode 100644 index 0000000000..27e72143dd --- /dev/null +++ b/src/bs-moment/locale/locale.defaults.ts @@ -0,0 +1,32 @@ +import { + defaultLocaleMonths, defaultLocaleMonthsShort, defaultLocaleWeekdays, defaultLocaleWeekdaysMin, + defaultLocaleWeekdaysShort, + LocaleData +} from './locale.class'; + +export const defaultInvalidDate = 'Invalid date'; + +export const defaultLocaleWeek = { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. +}; + +export const baseConfig: LocaleData = { + // calendar: defaultCalendar, + // longDateFormat: defaultLongDateFormat, + invalidDate: defaultInvalidDate, + // ordinal: defaultOrdinal, + // dayOfMonthOrdinalParse: defaultDayOfMonthOrdinalParse, + // relativeTime: defaultRelativeTime, + + months: defaultLocaleMonths, + monthsShort: defaultLocaleMonthsShort, + + week: defaultLocaleWeek, + + weekdays: defaultLocaleWeekdays, + weekdaysMin: defaultLocaleWeekdaysMin, + weekdaysShort: defaultLocaleWeekdaysShort, + + // meridiemParse: defaultLocaleMeridiemParse +}; diff --git a/src/bs-moment/locale/locales.service.ts b/src/bs-moment/locale/locales.service.ts new file mode 100644 index 0000000000..b6e4d66e5e --- /dev/null +++ b/src/bs-moment/locale/locales.service.ts @@ -0,0 +1,95 @@ +// internal storage for locale config files +import { Locale, LocaleData } from './locale.class'; +import { baseConfig } from './locale.defaults'; +import { hasOwnProp, isObject, isUndefined } from '../utils/type-checks'; + +const locales: { [key: string]: Locale } = {}; +const localeFamilies: { [key: string]: Locale } = {}; +let globalLocale: Locale; + +function chooseLocale(name: string) { + return locales[name]; +} + +// returns locale data +export function getLocale(key: string): Locale { + + if (!key) { + return globalLocale; + } + + return chooseLocale(key); +} + +export function listLocales(): string[] { + return Object.keys(locales); +} + +export function mergeConfigs(parentConfig: LocaleData, childConfig: LocaleData) { + const res: { [key: string]: any } = Object.assign({}, parentConfig); + + for (const childProp in childConfig) { + if (!hasOwnProp(childConfig, childProp)) { + continue; + } + if (isObject(parentConfig[childProp]) && isObject(childConfig[childProp])) { + (res[childProp]) = {}; + Object.assign(res[childProp], parentConfig[childProp]); + Object.assign(res[childProp], childConfig[childProp]); + } else if (childConfig[childProp] != null) { + (res[childProp] ) = childConfig[childProp]; + } else { + delete res[childProp]; + } + } + for (const parentProp in parentConfig) { + if (hasOwnProp(parentConfig, parentProp) && + !hasOwnProp(childConfig, parentProp) && + isObject(parentConfig[parentProp])) { + // make sure changes to properties don't modify parent config + (res[parentProp] ) = Object.assign({}, res[parentProp]); + } + } + return res; +} + +// This function will load locale and then set the global locale. If +// no arguments are passed in, it will simply return the current global +// locale key. +export function getSetGlobalLocale(key: string, values?: LocaleData): string { + let data: Locale; + if (key) { + data = isUndefined(values) ? getLocale(key) : defineLocale(key, values); + + if (data) { + globalLocale = data; + } + } + + return globalLocale._abbr; +} + +export function defineLocale(name: string, config?: LocaleData): Locale { + if (config === null) { + // useful for testing + delete locales[name]; + return null; + } + + config.abbr = name; + + locales[name] = new Locale(mergeConfigs(baseConfig, config)); + + if (localeFamilies[name]) { + localeFamilies[name].forEach(function (x: Locale) { + defineLocale(x.name, x.config); + }); + } + + // backwards compat for now: also set the locale + // make sure we set the locale AFTER all child locales have been + // created, so we won't end up with the child locale set. + getSetGlobalLocale(name); + + return locales[name]; +} diff --git a/src/bs-moment/units/day-of-month.ts b/src/bs-moment/units/day-of-month.ts new file mode 100644 index 0000000000..a0a1af36ec --- /dev/null +++ b/src/bs-moment/units/day-of-month.ts @@ -0,0 +1,6 @@ +import { addFormatToken } from '../format-functions'; +import { getDate } from '../utils/date-getters'; + +addFormatToken('D', ['DD', 2], 'Do', function (date: Date): string { + return getDate(date).toString(10); +}); diff --git a/src/bs-moment/units/day-of-week.ts b/src/bs-moment/units/day-of-week.ts new file mode 100644 index 0000000000..6e9566e63c --- /dev/null +++ b/src/bs-moment/units/day-of-week.ts @@ -0,0 +1,35 @@ +import { addFormatToken } from '../format-functions'; +import { Locale } from '../locale/locale.class'; +import { getDayOfWeek } from '../utils/date-getters'; + +// FORMATTING +addFormatToken('d', null, 'do', function (date: Date): string { + return getDayOfWeek(date).toString(10); +}); + +addFormatToken('dd', null, null, function (date: Date, format: string, locale?: Locale): string { + return locale.weekdaysMin(date) as string; +}); + +addFormatToken('ddd', null, null, function (date: Date, format: string, locale?: Locale): string { + return locale.weekdaysShort(date) as string; +}); + +addFormatToken('dddd', null, null, function (date: Date, format: string, locale?: Locale): string { + return locale.weekdays(date, format) as string; +}); + +addFormatToken('e', null, null, function (date: Date): string { + return getDayOfWeek(date).toString(10); +}); +addFormatToken('E', null, null, function (date: Date): string { + return getISODayOfWeek(date).toString(10); +}); + +export function getLocaleDayOfWeek(date: Date, locale: Locale): number { + return (getDayOfWeek(date) + 7 - locale.firstDayOfWeek()) % 7; +} + +export function getISODayOfWeek(date: Date): number { + return getDayOfWeek(date) || 7; +} diff --git a/src/bs-moment/units/day-of-year.ts b/src/bs-moment/units/day-of-year.ts new file mode 100644 index 0000000000..e344317317 --- /dev/null +++ b/src/bs-moment/units/day-of-year.ts @@ -0,0 +1,13 @@ +import { addFormatToken } from '../format-functions'; + +// FORMATTING +addFormatToken('DDD', ['DDDD', 3], 'DDDo', function (date: Date): string { + return getDayOfYear(date).toString(10); +}); + +export function getDayOfYear(date: Date) { + const start = new Date(date.getFullYear(), 0, 0); + const diff = date.getTime() - start.getTime(); + const oneDay = 1000 * 60 * 60 * 24; + return Math.round(diff / oneDay); +} diff --git a/src/bs-moment/units/index.ts b/src/bs-moment/units/index.ts new file mode 100644 index 0000000000..b72bd38374 --- /dev/null +++ b/src/bs-moment/units/index.ts @@ -0,0 +1,7 @@ +import './day-of-month'; +import './day-of-week'; +import './day-of-year'; +import './month'; +import './week'; +import './week-calendar-utils'; +import './year'; diff --git a/src/bs-moment/units/month.ts b/src/bs-moment/units/month.ts new file mode 100644 index 0000000000..8e698edc03 --- /dev/null +++ b/src/bs-moment/units/month.ts @@ -0,0 +1,28 @@ +import { addFormatToken } from '../format-functions'; +import { isLeapYear } from './year'; +import { Locale } from '../locale/locale.class'; +import { mod } from '../utils'; +import { getMonth } from '../utils/date-getters'; + +export function daysInMonth(year: number, month: number): number { + if (isNaN(year) || isNaN(month)) { + return NaN; + } + const modMonth = mod(month, 12); + year += (month - modMonth) / 12; + return modMonth === 1 ? (isLeapYear(year) ? 29 : 28) : (31 - modMonth % 7 % 2); +} + +// FORMATTING + +addFormatToken('M', ['MM', 2], 'Mo', function (date: Date, format: string): string { + return (getMonth(date) + 1).toString(); +}); + +addFormatToken('MMM', null, null, function (date: Date, format: string, locale?: Locale): string { + return locale.monthsShort(date, format) as string; +}); + +addFormatToken('MMMM', null, null, function (date: Date, format: string, locale?: Locale): string { + return locale.months(date, format) as string; +}); diff --git a/src/bs-moment/units/week-calendar-utils.ts b/src/bs-moment/units/week-calendar-utils.ts new file mode 100644 index 0000000000..14a9b50ada --- /dev/null +++ b/src/bs-moment/units/week-calendar-utils.ts @@ -0,0 +1,75 @@ +/** + * + * @param {number} year + * @param {number} dow - start-of-first-week + * @param {number} doy - start-of-year + * @returns {number} + */ +import { createUTCDate } from '../utils'; +import { daysInYear } from './year'; +import { getDayOfYear } from './day-of-year'; +import { getFullYear } from '../utils/date-getters'; + +function firstWeekOffset(year: number, dow: number, doy: number): number { + // first-week day -- which january is always in the first week (4 for iso, 1 for other) + const fwd = 7 + dow - doy; + // first-week day local weekday -- which local weekday is fwd + const fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; + + return -fwdlw + fwd - 1; +} + +// https://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday +export function dayOfYearFromWeeks(year: number, week: number, weekday: number, + dow: number, doy: number): { year: number, dayOfYear: number } { + const localWeekday = (7 + weekday - dow) % 7; + const weekOffset = firstWeekOffset(year, dow, doy); + const dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset; + let resYear: number; + let resDayOfYear: number; + + if (dayOfYear <= 0) { + resYear = year - 1; + resDayOfYear = daysInYear(resYear) + dayOfYear; + } else if (dayOfYear > daysInYear(year)) { + resYear = year + 1; + resDayOfYear = dayOfYear - daysInYear(year); + } else { + resYear = year; + resDayOfYear = dayOfYear; + } + + return { + year: resYear, + dayOfYear: resDayOfYear + }; +} + +export function weekOfYear(date: Date, dow: number, doy: number): { week: number, year: number } { + const weekOffset = firstWeekOffset(getFullYear(date), dow, doy); + const week = Math.floor((getDayOfYear(date) - weekOffset - 1) / 7) + 1; + let resWeek: number; + let resYear: number; + + if (week < 1) { + resYear = getFullYear(date) - 1; + resWeek = week + weeksInYear(resYear, dow, doy); + } else if (week > weeksInYear(getFullYear(date), dow, doy)) { + resWeek = week - weeksInYear(getFullYear(date), dow, doy); + resYear = getFullYear(date) + 1; + } else { + resYear = getFullYear(date); + resWeek = week; + } + + return { + week: resWeek, + year: resYear + }; +} + +export function weeksInYear(year: number, dow: number, doy: number): number { + const weekOffset = firstWeekOffset(year, dow, doy); + const weekOffsetNext = firstWeekOffset(year + 1, dow, doy); + return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; +} diff --git a/src/bs-moment/units/week.ts b/src/bs-moment/units/week.ts new file mode 100644 index 0000000000..cb65863f3c --- /dev/null +++ b/src/bs-moment/units/week.ts @@ -0,0 +1,18 @@ +import { addFormatToken } from '../format-functions'; +import { Locale } from '../locale/locale.class'; +import { weekOfYear } from './week-calendar-utils'; + +addFormatToken('w', ['ww', 2], 'wo', function (date: Date, format: string, locale: Locale): string { + return getWeek(date, locale).toString(10); +}); +addFormatToken('W', ['WW', 2], 'Wo', function (date: Date): string { + return getISOWeek(date).toString(10); +}); + +export function getWeek(date: Date, locale: Locale): number { + return locale.week(date); +} + +export function getISOWeek(date: Date): number { + return weekOfYear(date, 1, 4).week; +} diff --git a/src/bs-moment/units/year.ts b/src/bs-moment/units/year.ts new file mode 100644 index 0000000000..6696dbaac4 --- /dev/null +++ b/src/bs-moment/units/year.ts @@ -0,0 +1,29 @@ +import { addFormatToken } from '../format-functions'; +import { getFullYear } from '../utils/date-getters'; + +// FORMATTING + +function getYear(date: Date): string { + return getFullYear(date).toString(); +} + +addFormatToken('Y', null, null, function (date: Date): string { + const y = getFullYear(date); + return y <= 9999 ? '' + y : '+' + y; +}); + +addFormatToken(null, ['YY', 2], null, function (date: Date): string { + return (getFullYear(date) % 100).toString(10); +}); + +addFormatToken(null, ['YYYY', 4], null, getYear); +addFormatToken(null, ['YYYYY', 5], null, getYear); +addFormatToken(null, ['YYYYYY', 6, true], null, getYear); + +export function daysInYear(year: number): number { + return isLeapYear(year) ? 366 : 365; +} + +export function isLeapYear(year: number): boolean { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} diff --git a/src/bs-moment/utils.ts b/src/bs-moment/utils.ts new file mode 100644 index 0000000000..5c76a21613 --- /dev/null +++ b/src/bs-moment/utils.ts @@ -0,0 +1,27 @@ +export function zeroFill(number: number, targetLength: number, forceSign: boolean): string { + const absNumber = '' + Math.abs(number); + const zerosToFill = targetLength - absNumber.length; + const sign = number >= 0; + return (sign ? (forceSign ? '+' : '') : '-') + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber; +} + +export function mod(n: number, x: number): number { + return ((n % x) + x) % x; +} + +export function absFloor(number: number): number { + return number < 0 + ? Math.ceil(number) || 0 + : Math.floor(number); +} + +export function createUTCDate (y?: number, m?: number, d?: number, h?: number, M?: number, s?: number, ms?: number) { + const date = new Date(Date.UTC.apply(null, arguments)); + + // the Date.UTC function remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + return date; +} diff --git a/src/bs-moment/utils/date-getters.ts b/src/bs-moment/utils/date-getters.ts new file mode 100644 index 0000000000..339e244350 --- /dev/null +++ b/src/bs-moment/utils/date-getters.ts @@ -0,0 +1,38 @@ +import { createDate } from '../../datepicker/utils/date-utils'; + +export function getDayOfWeek(date: Date, isUTC = false): number { + return isUTC ? date.getUTCDay() : date.getDay(); +} +export function getDate(date: Date, isUTC = false): number { + return isUTC ? date.getUTCDate() : date.getDate(); +} + +export function getMonth(date: Date, isUTC = false): number { + return isUTC ? date.getUTCMonth() : date.getMonth(); +} + +export function getFullYear(date: Date, isUTC = false): number { + return isUTC ? date.getUTCFullYear() : date.getFullYear(); +} + +export function getFirstDayOfMonth(date: Date): Date { + return createDate(date.getFullYear(), date.getMonth(), 1, + date.getHours(), date.getMinutes(), date.getSeconds()); +} + +export function daysInMonth(date: Date): number { + return _daysInMonth(date.getFullYear(), date.getMonth()); +} + +export function _daysInMonth(year: number, month: number): number { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); +} + +export function isFirstDayOfWeek(date: Date, firstDayOfWeek: number): boolean { + return date.getDay() === firstDayOfWeek; +} + +export function isSameMonth(date1: Date, date2: Date) { + if (!date1 || !date2) {return false;} + return getFullYear(date1) === getFullYear(date2) && getMonth(date1) === getMonth(date2); +} diff --git a/src/bs-moment/utils/type-checks.ts b/src/bs-moment/utils/type-checks.ts new file mode 100644 index 0000000000..8be16cf8a3 --- /dev/null +++ b/src/bs-moment/utils/type-checks.ts @@ -0,0 +1,37 @@ +import { absFloor } from '../utils'; + +export function isDateValid(date: Date): boolean { + return date && !isNaN(date.getTime()); +} +export function isFunction(fn: Function): fn is Function { + return fn instanceof Function || Object.prototype.toString.call(fn) === '[object Function]'; +} + +export function isArray(input: any): boolean { + return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]'; +} + +export function hasOwnProp(a: any/*object*/, b: string | number): boolean { + return Object.prototype.hasOwnProperty.call(a, b); +} + +export function isObject(input: any/*object*/): boolean { + // IE8 will treat undefined and null as object if it wasn't for + // input != null + return input != null && Object.prototype.toString.call(input) === '[object Object]'; +} + +export function isUndefined(input: any): boolean { + return input === void 0; +} + +export function toInt(argumentForCoercion: string | number): number { + const coercedNumber = +argumentForCoercion; + let value = 0; + + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + value = absFloor(coercedNumber); + } + + return value; +} diff --git a/src/datepicker/bs-datepicker-config.ts b/src/datepicker/bs-datepicker-config.ts new file mode 100644 index 0000000000..41d7a6847f --- /dev/null +++ b/src/datepicker/bs-datepicker-config.ts @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000000..71f321c50e --- /dev/null +++ b/src/datepicker/bs-datepicker.component.ts @@ -0,0 +1,141 @@ +import { + Component, ComponentRef, + ElementRef, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + Renderer, + ViewContainerRef +} from '@angular/core'; +import { ComponentLoader } from '../component-loader/component-loader.class'; +import { ComponentLoaderFactory } from '../component-loader/component-loader.factory'; +import { BsDatepickerContainerComponent } from './themes/bs/bs-datepicker-container.component'; +import { Subscription } from 'rxjs/Subscription'; +import 'rxjs/add/operator/filter'; + +@Component({ + selector: 'bs-datepicker', + exportAs: 'bsDatepicker', + template: '' +}) +export class BsDatepickerComponent implements OnInit, OnDestroy { + /** + * Placement of a popover. Accepts: "top", "bottom", "left", "right" + */ + @Input() placement: 'top' | 'bottom' | 'left' | 'right' = 'bottom'; + /** + * Specifies events that should trigger. Supports a space separated list of + * event names. + */ + @Input() triggers = 'click'; + /** + * A selector specifying the element the popover should be appended to. + * Currently only supports "body". + */ + @Input() container = 'body'; + + /** + * Returns whether or not the popover is currently being shown + */ + @Input() + public get isOpen(): boolean { + return this._datepicker.isShown; + } + + public set isOpen(value: boolean) { + if (value) { this.show(); } else { this.hide(); } + } + + /** + * Emits an event when the popover is shown + */ + @Output() onShown: EventEmitter; + /** + * Emits an event when the popover is hidden + */ + @Output() onHidden: EventEmitter; + + // here will be parsed options and set defaults + // @Input() config: BsDatePickerOptions; + // configChange: EventEmitter = new EventEmitter(); + + @Input() value: Date; + @Output() valueChange: EventEmitter = new EventEmitter(); + + protected subscriptions: Subscription[] = []; + + private _datepicker: ComponentLoader; + private _datepickerRef: ComponentRef; + + constructor(_elementRef: ElementRef, + _renderer: Renderer, + _viewContainerRef: ViewContainerRef, + cis: ComponentLoaderFactory) { + 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; + + this.valueChange + .filter(value => !!value) + .subscribe(value => this.hide()); + } + + /** + * Opens an element’s datepicker. This is considered a “manual” triggering of + * the datepicker. + */ + show(): void { + if (this._datepicker.isShown) { + return; + } + + this._datepickerRef = this._datepicker + .attach(BsDatepickerContainerComponent) + .to(this.container) + .position({attachment: this.placement}) + .show({placement: this.placement}); + + // link with datepicker + this._datepickerRef.instance.value = this.value; + this.subscriptions.push(this._datepickerRef.instance + .valueChange.subscribe((value: Date) => this.valueChange.emit(value))); + } + + /** + * Closes an element’s datepicker. This is considered a “manual” triggering of + * the datepicker. + */ + hide(): void { + if (this.isOpen) { + this._datepicker.hide(); + } + } + + /** + * Toggles an element’s datepicker. This is considered a “manual” triggering of + * the datepicker. + */ + toggle(): void { + if (this.isOpen) { + return this.hide(); + } + + this.show(); + } + + ngOnInit(): any { + this._datepicker.listen({ + triggers: this.triggers, + show: () => this.show() + }); + } + + ngOnDestroy(): any { + this._datepicker.dispose(); + } +} diff --git a/src/datepicker/bs-datepicker.css b/src/datepicker/bs-datepicker.css new file mode 100644 index 0000000000..36397545f9 --- /dev/null +++ b/src/datepicker/bs-datepicker.css @@ -0,0 +1,553 @@ +.bs-datepicker { + display: inline-block; + vertical-align: top; + min-width: 279px; + min-height: 250px; + background: #fff; + box-shadow: 0 10px 20px rgba(84, 112, 139, 0.1); + position: relative; + z-index: 1; +} + +.bs-datepicker:after { + clear: both; + content: ''; + display: block; +} + +.bs-datepicker bs-day-picker { + float: left; +} + +.bs-datepicker-head { + min-width: 270px; + height: 50px; + padding: 10px; + border-radius: 3px 3px 0 0; + text-align: justify; +} + +.bs-datepicker-head:after { + content: ""; + display: inline-block; + vertical-align: top; + width: 100%; +} + +.bs-datepicker-head button { + display: inline-block; + vertical-align: top; + padding: 0; + height: 30px; + line-height: 30px; + border: 0; + background: transparent; + text-align: center; + cursor: pointer; + color: #fff; + transition: 0.3s; +} + +.bs-datepicker-head button.next, +.bs-datepicker-head button.previous { + border-radius: 50%; + width: 30px; + height: 30px; +} + +.bs-datepicker-head button.next span, +.bs-datepicker-head button.previous span { + font-size: 28px; + line-height: 1; + display: inline-block; + position: relative; + top: -1px; +} + +.bs-datepicker-head button.current { + border-radius: 15px; + max-width: 155px; + padding: 0 13px; +} + +.bs-datepicker-head.years button.current, +.bs-datepicker-head.months button.current { + width: 155px; + padding: 0; +} + +.bs-datepicker button:hover, +.bs-datepicker button:focus, +.bs-datepicker button:active, +.bs-datepicker input:hover, +.bs-datepicker input:focus, +.bs-datepicker input:active, +.bs-datepicker-btns button:hover, +.bs-datepicker-btns button:focus, +.bs-datepicker-btns button:active, +.bs-datepicker-predefined-btns button:active, +.bs-datepicker-predefined-btns button:focus { + outline: none; +} + +.bs-datepicker-head button:hover, +.bs-datepicker-btns button.colored:hover:after { + background-color: rgba(0, 0, 0, 0.1); +} + +.bs-datepicker-head button:active, +.bs-datepicker-btns button.colored:active:after { + background-color: rgba(0, 0, 0, 0.2); +} + +.bs-datepicker-body { + padding: 10px; + border-radius: 0 0 3px 3px; + height: 230px; + border: 1px solid #eee; +} + +.bs-datepicker-body .days.weeks { + position: relative; + z-index: 1; +} + +.bs-datepicker-body table { + width: 100%; + border-collapse: separate; +} + +.bs-datepicker-body table th { + font-size: 13px; + color: #9aaec1; + font-weight: 400; + text-align: center; +} + +.bs-datepicker-body table td { + color: #54708b; + text-align: center; + position: relative; +} + +.bs-datepicker-body table td span { + display: block; + margin: 0 auto; + font-size: 13px; + width: 32px; + height: 32px; + line-height: 32px; + border-radius: 50%; + position: relative; + /*z-index: 1;*/ + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.bs-datepicker-body table td:not(.disabled) span { + cursor: pointer; +} + +.bs-datepicker-body table.days td span.is-highlighted:not(.disabled):not(.selected), +.bs-datepicker-body table:not(.days) td span.is-highlighted:not(.disabled):not(.selected), +.bs-datepicker-body table.days td.is-highlighted:not(.disabled):not(.selected) span, +.bs-datepicker-body table:not(.days) td.is-highlighted:not(.disabled):not(.selected) span { + background-color: #e9edf0; + transition: 0s; +} + +.bs-datepicker-body table td span.disabled, +.bs-datepicker-body table td.disabled span { + color: #9aaec1; +} + +.bs-datepicker-body table td span.selected, +.bs-datepicker-body table td.selected span { + color: #fff; +} + +.bs-datepicker-body table td.active { + position: relative; +} + +.bs-datepicker-body table td span.active.select-start:after, +.bs-datepicker-body table td span.active.select-end:after, +.bs-datepicker-body table td.active.select-start span:after, +.bs-datepicker-body table td.active.select-end span:after { + content: ""; + display: block; + position: absolute; + z-index: -1; + width: 100%; + height: 100%; + transition: 0.3s; + top: 0; + border-radius: 50%; +} + +.bs-datepicker-body table td:before, +.bs-datepicker-body table td span:before { + content: ""; + display: block; + position: absolute; + z-index: -1; + top: 6px; + bottom: 6px; + left: -2px; + right: -2px; + box-sizing: content-box; + background: transparent; +} +.bs-datepicker-body table.days span.select-start { + z-index: 2; +} + +.bs-datepicker-body table.days td.active:not(.select-start):before, +.bs-datepicker-body table.days td.in-range:not(.select-start):before, +.bs-datepicker-body table.days span.active:not(.select-start):before, +.bs-datepicker-body table.days span.in-range:not(.select-start):before { + background: #e9edf0; +} + +.bs-datepicker-body table.days span.is-highlighted.in-range:before, +.bs-datepicker-body table.days span.in-range.select-end:before { + background: none; + right: 0; + left: 0; +} + +.bs-datepicker-body table td.active.select-start + td.active:before { + left: -20%; +} + +.bs-datepicker-body table.days td.select-start + td.select-end:before, +.bs-datepicker-body table.days td.select-start + td.is-highlighted:before, +.bs-datepicker-body table.days td.active + td.is-highlighted:before, +.bs-datepicker-body table.days td.active + td.select-end:before, +.bs-datepicker-body table.days td.in-range + td.is-highlighted:before, +.bs-datepicker-body table.days td.in-range + td.select-end:before { + background: #e9edf0; + width: 100%; +} + +.bs-datepicker-body table tr td:last-child.active:before { + border-radius: 0 3px 3px 0; + width: 125%; + left: -25%; +} + +.bs-datepicker-body table.weeks tr td:nth-child(2).active:before { + border-radius: 3px 0 0 3px; + left: 0; + width: 100%; +} + +.bs-datepicker-body table:not(.weeks) tr td:first-child:before { + border-radius: 3px 0 0 3px; +} + +.bs-datepicker-body table td.active.select-start:before { + left: 35%; +} + +.bs-datepicker-body table td.active.select-end:before { + left: -85%; +} + +.bs-datepicker-body table td span[class*="select-"] +.bs-datepicker-body table td[class*="select-"] span { + border-radius: 50%; + color: #fff; +} + +.label-default ~ .bs-datepicker-body table td span.selected, +.label-default ~ .bs-datepicker-body table td span[class*="select-"]:after, +.label-default ~ .bs-datepicker-body table td.selected span, +.label-default ~ .bs-datepicker-body table td[class*="select-"] span:after { + background-color: #777; +} + +.label-primary ~ .bs-datepicker-body table td span.selected, +.label-primary ~ .bs-datepicker-body table td span[class*="select-"]:after, +.label-primary ~ .bs-datepicker-body table td.selected span, +.label-primary ~ .bs-datepicker-body table td[class*="select-"] span:after { + background-color: #337ab7; +} + +.label-success ~ .bs-datepicker-body table td span.selected, +.label-success ~ .bs-datepicker-body table td span[class*="select-"]:after, +.label-success ~ .bs-datepicker-body table td.selected span, +.label-success ~ .bs-datepicker-body table td[class*="select-"] span:after { + background-color: #5cb85c; +} + +.label-danger ~ .bs-datepicker-body table td span.selected, +.label-danger ~ .bs-datepicker-body table td span[class*="select-"]:after, +.label-danger ~ .bs-datepicker-body table td.selected span, +.label-danger ~ .bs-datepicker-body table td[class*="select-"] span:after { + background-color: #d9534f; +} + +.label-warning ~ .bs-datepicker-body table td span.selected, +.label-warning ~ .bs-datepicker-body table td span[class*="select-"]:after, +.label-warning ~ .bs-datepicker-body table td.selected span, +.label-warning ~ .bs-datepicker-body table td[class*="select-"] span:after { + background-color: #f0ad4e; +} + +.label-info ~ .bs-datepicker-body table td span.selected, +.label-info ~ .bs-datepicker-body table td span[class*="select-"]:after, +.label-info ~ .bs-datepicker-body table td.selected span, +.label-info ~ .bs-datepicker-body table td[class*="select-"] span:after { + background-color: #5bc0de; +} + +.label-default ~ .bs-datepicker-body table td.week span { + color: #777; +} + +.label-primary ~ .bs-datepicker-body table td.week span { + color: #337ab7; +} + +.label-success ~ .bs-datepicker-body table td.week span { + color: #5cb85c; +} + +.label-danger ~ .bs-datepicker-body table td.week span { + color: #d9534f; +} + +.label-warning ~ .bs-datepicker-body table td.week span { + color: #f0ad4e; +} + +.label-info ~ .bs-datepicker-body table td.week span { + color: #5bc0de; +} + +.bs-datepicker-body table.months td span, +.bs-datepicker-body table.years td span { + width: 46px; + height: 46px; + line-height: 45px; + margin: 0 auto; +} + +.bs-datepicker-body table.months tr:not(:last-child) td span, +.bs-datepicker-body table.years tr:not(:last-child) td span { + margin-bottom: 8px; +} + +.bs-datepicker.bs-timepicker:after { + content: ''; + display: block; + clear: both; +} + +.bs-datepicker.bs-timepicker, +.bs-datepicker.bs-padding { + padding: 15px; +} + +.bs-datepicker.bs-timepicker .bs-datepicker-body { + border: 1px solid #eee; + float: left; +} + +.bs-datepicker .current-timedate { + color: #54708b; + font-size: 15px; + text-align: center; + height: 30px; + line-height: 30px; + border-radius: 20px; + border: 1px solid #eee; + margin-bottom: 10px; + cursor: pointer; + text-transform: uppercase; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; +} + +.bs-datepicker .current-timedate span:not(:empty):before { + content: ""; + width: 15px; + height: 16px; + display: inline-block; + margin-right: 4px; + vertical-align: text-bottom; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAQCAYAAADJViUEAAABMklEQVQoU9VTwW3CQBCcOUgBtEBKSAukAnBKME+wFCAlYIhk8sQlxFABtJAScAsuAPBEewYcxCP8ouxrPDsza61uiVN1o6RNHD4htSCmq49RfO71BvMJqBBkITRf1kmUW49nQRC9h1I5AZlBClaL8aP1fKgOOxCx8aSLs+Q19eZuNO8QmPqJRtDFguy7OAcDbJPs+/BKVPDIPrvD2ZJgWAmVe7O0rI0Vqs1seyWUXpuJoppYCa5L+U++NpNPkr5OE2oMdARsb3gykJT5ydZcL8Z9Ww60nxg2LhjON9li9OwXZzo+xLbp3nC2s9CL2RrueGyVrgwNm8HpsCzZ9EEW6kqXlo1GQe03FzP/7W8Hl0dBtu7Bf7zt6mIwvX1RvzDCm7+q3mAW0Dl/GPdUCeXrZLT9BrDrGkm4qlPvAAAAAElFTkSuQmCC); +} + +.bs-timepicker-container { + margin-top: 10px; + text-align: center; +} + +.bs-timepicker-label { + color: #54708b; + margin-bottom: 10px; +} + +.bs-timepicker-controls { + display: inline-block; + vertical-align: top; + margin-right: 10px; +} + +.bs-timepicker-controls button { + width: 20px; + height: 20px; + border-radius: 50%; + border: 0; + background-color: #e9edf0; + color: #54708b; + font-size: 16px; + font-weight: 700; + vertical-align: middle; + line-height: 0; + padding: 0; + transition: 0.3s; +} + +.bs-timepicker-controls button:hover { + background-color: #d5dadd; +} + +.bs-timepicker-controls input { + width: 35px; + height: 25px; + border-radius: 13px; + text-align: center; + border: 1px solid #e9edf0; +} + +.bs-timepicker .switch-time-format { + text-transform: uppercase; + min-width: 54px; + height: 25px; + border-radius: 20px; + border: 1px solid #e9edf0; + background: #fff; + color: #54708b; + font-size: 13px; +} + +.bs-timepicker .switch-time-format img { + vertical-align: initial; + margin-left: 4px; +} + +.bs-datepicker-multiple { + display: inline-block; + border-radius: 4px; + box-shadow: 0 3px 11px rgba(33, 37, 39, 0.2); + background-color: #fff; +} + +.bs-datepicker-multiple .bs-datepicker { + box-shadow: none; + position: relative; +} + +.bs-datepicker-multiple .bs-datepicker:not(:last-child) { + padding-right: 10px; +} + +.bs-datepicker-multiple .bs-datepicker + .bs-datepicker:after { + content: ""; + display: block; + width: 14px; + height: 10px; + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAKCAYAAABrGwT5AAAA1ElEQVQoU42RsQrCUAxF77VuDu7O4oMWW//BURBBpZvgKk4uIrjoqKOTf+DopIO4uYggtFTfw3+pkQqCW1/G5J7kJiFy4m5MxUlxAzgIPHX+lzMPzupRYlYgxiR7vqsOP8YKzsTx0yxFMCUZ+q7aZzlr+OvgoWcAFyAHgat2jLWu48252DdqAihDJGSSJNUUxYmQjs3+hPQBlAh2rG2LCOPnaw3IiGDX99TRCs7ASJsNhUOA7d/LcuHvRG22FIZvsNXw1MX6VZExCilOQKEfeLXr/10+aC9Ho7arh7oAAAAASUVORK5CYII=); + position: absolute; + top: 25px; + left: -8px; +} + +.bs-datepicker-container + .bs-datepicker-btns { + border-top: 1px solid #e9edf0; +} + +.bs-datepicker-btns { + padding: 10px 0; + text-align: right; + clear: both; + border-top: 1px solid #eee; +} + +.bs-datepicker-btns button { + padding: 0 16px; + border: 0; + border-radius: 15px; + height: 30px; + color: #54708b; + position: relative; +} + +.bs-datepicker-btns button:after { + content: ""; + transition: 0.3s; + border-radius: 15px; + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; +} + +.bs-datepicker-btns button span { + position: relative; + z-index: 1; +} + +.bs-datepicker-btns button.colored { + color: #fff; +} + +.bs-datepicker-btns button:not(.colored) { + background: transparent; +} + +.bs-datepicker-btns button:not(.colored):hover { + text-decoration: underline; +} + +.bs-datepicker-multiple .left { + float: left; +} + +.bs-datepicker-multiple .right { + float: right; +} + +.bs-datepicker-predefined-btns { + padding: 15px; +} + +.bs-datepicker-predefined-btns button { + width: 100%; + display: block; + height: 30px; + background-color: #72899f; + border-radius: 4px; + color: #fff; + border: 0; + margin-bottom: 10px; + padding: 0 18px; + text-align: left; + transition: 0.3s; +} + +.bs-datepicker-predefined-btns button:active, +.bs-datepicker-predefined-btns button:hover { + background-color: #54708b; +} + +.bs-datepicker .is-other-month { + color: rgba(0, 0, 0, 0.25); +} + diff --git a/src/datepicker/bs-datepicker.module.ts b/src/datepicker/bs-datepicker.module.ts new file mode 100644 index 0000000000..c780d6d42d --- /dev/null +++ b/src/datepicker/bs-datepicker.module.ts @@ -0,0 +1,43 @@ +import { CommonModule } from '@angular/common'; +import { 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 { BsDatepickerDayViewComponent } from './themes/bs/bs-datepicker-day-view.component'; +import { BsDatepickerConfig } from './bs-datepicker-config'; +import { BsDatepickerEffects } from './reducer/bs-datepicker.effects'; +import { BsDaterangepickerContainerComponent } from './themes/bs/bs-daterangepicker-container.component'; +import { BsDaterangepickerComponent } from './bs-daterangepicker.component'; +import { BsDatepickerComponent } from './bs-datepicker.component'; +import { ComponentLoaderFactory } from '../component-loader/component-loader.factory'; +import { PositioningService } from '../positioning/positioning.service'; + +@NgModule({ + imports: [CommonModule], + declarations: [ + BsDatepickerMonthViewComponent, + BsDatepickerViewComponent, + BsDatepickerNavigationViewComponent, + BsDatepickerDayViewComponent, + BsDatepickerContainerComponent, + BsDaterangepickerContainerComponent, + BsDatepickerComponent, + BsDaterangepickerComponent + ], + entryComponents: [BsDatepickerContainerComponent, BsDaterangepickerContainerComponent], + exports: [BsDatepickerContainerComponent, BsDaterangepickerContainerComponent, + BsDatepickerComponent, BsDaterangepickerComponent] +}) +export class BsDatepickerModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: BsDatepickerModule, + providers: [ + ComponentLoaderFactory, PositioningService, + BsDatepickerStore, BsDatepickerActions, BsDatepickerConfig, BsDatepickerEffects] + }; + } +} diff --git a/src/datepicker/bs-daterangepicker.component.ts b/src/datepicker/bs-daterangepicker.component.ts new file mode 100644 index 0000000000..0de11cc4fc --- /dev/null +++ b/src/datepicker/bs-daterangepicker.component.ts @@ -0,0 +1,130 @@ +import { + Component, EventEmitter, Input, OnDestroy, OnInit, Output, ComponentRef, ElementRef, + Renderer, + ViewContainerRef +} 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'; + +@Component({selector: 'bs-daterangepicker', template: ''}) +export class BsDaterangepickerComponent implements OnInit, OnDestroy { + /** + * Placement of a popover. Accepts: "top", "bottom", "left", "right" + */ + @Input() placement: 'top' | 'bottom' | 'left' | 'right' = 'bottom'; + /** + * Specifies events that should trigger. Supports a space separated list of + * event names. + */ + @Input() triggers = 'click'; + /** + * A selector specifying the element the popover should be appended to. + * Currently only supports "body". + */ + @Input() container = 'body'; + + /** + * Returns whether or not the popover is currently being shown + */ + @Input() + public get isOpen(): boolean { + return this._datepicker.isShown; + } + + public set isOpen(value: boolean) { + if (value) { this.show(); } else { this.hide(); } + } + + /** + * Emits an event when the popover is shown + */ + @Output() onShown: EventEmitter; + /** + * Emits an event when the popover is hidden + */ + @Output() onHidden: EventEmitter; + + // here will be parsed options and set defaults + // @Input() config: BsDatePickerOptions; + // configChange: EventEmitter = new EventEmitter(); + + @Input() value: Date[]; + @Output() valueChange: EventEmitter = new EventEmitter(); + + protected subscriptions: Subscription[] = []; + + private _datepicker: ComponentLoader; + private _datepickerRef: ComponentRef; + + constructor(_elementRef: ElementRef, + _renderer: Renderer, + _viewContainerRef: ViewContainerRef, + cis: ComponentLoaderFactory) { + 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; + + this.valueChange + .filter(range => range && range[0] && !!range[1]) + .subscribe(range => this.hide()); + } + + /** + * Opens an element’s datepicker. This is considered a “manual” triggering of + * the datepicker. + */ + show(): void { + if (this._datepicker.isShown) { + return; + } + + this._datepickerRef = this._datepicker + .attach(BsDaterangepickerContainerComponent) + .to(this.container) + .position({attachment: this.placement}) + .show({placement: this.placement}); + + // link with datepicker + this._datepickerRef.instance.value = this.value; + this.subscriptions.push(this._datepickerRef.instance + .valueChange.subscribe((value: Date[]) => this.valueChange.emit(value))); + } + + /** + * Closes an element’s datepicker. This is considered a “manual” triggering of + * the datepicker. + */ + hide(): void { + if (this.isOpen) { + this._datepicker.hide(); + } + } + + /** + * Toggles an element’s datepicker. This is considered a “manual” triggering of + * the datepicker. + */ + toggle(): void { + if (this.isOpen) { + return this.hide(); + } + + this.show(); + } + + ngOnInit(): any { + this._datepicker.listen({ + triggers: this.triggers, + show: () => this.show() + }); + } + + ngOnDestroy(): any { + this._datepicker.dispose(); + } +} diff --git a/src/datepicker/engine/calc-month-view.ts b/src/datepicker/engine/calc-month-view.ts new file mode 100644 index 0000000000..f0d4f3769a --- /dev/null +++ b/src/datepicker/engine/calc-month-view.ts @@ -0,0 +1,26 @@ +// 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 { changeDate } from '../utils/date-utils'; + +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 = changeDate(prevValue, {day: 1}); + } + } + + return { + daysMatrix: daysCalendar, + month: firstDay + }; +} diff --git a/src/datepicker/engine/flag-month-view.ts b/src/datepicker/engine/flag-month-view.ts new file mode 100644 index 0000000000..13b4761327 --- /dev/null +++ b/src/datepicker/engine/flag-month-view.ts @@ -0,0 +1,82 @@ +import { DayViewModel, MonthViewModel, WeekViewModel } from '../models/index'; +import { isSameMonth } from '../../bs-moment/utils/date-getters'; + +export interface FlagMonthViewOptions { + hoveredDate: Date; + selectedDate: Date; + selectedRange: Date[]; + displayMonths: number; + monthIndex: number; +} + +export function flagMonthView(formattedMonth: MonthViewModel, + options: FlagMonthViewOptions): MonthViewModel { + 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); + // date range picker + const isSelectionStart = !isOtherMonth && isSameDate(day.date, options.selectedRange[0]); + const isSelectionEnd = !isOtherMonth && isSameDate(day.date, options.selectedRange[1]); + + const isSelected = !isOtherMonth && isSameDate(day.date, options.selectedDate) || + isSelectionStart || isSelectionEnd; + + const isInRange = !isOtherMonth && isDateInRange(day.date, options.selectedRange, options.hoveredDate); + // decide update or not + const newDay = Object.assign({}, day, { + isOtherMonth, + isHovered, + isSelected, + isSelectionStart, + isSelectionEnd, + isInRange + }); + + if (day.isOtherMonth !== newDay.isOtherMonth || + day.isHovered !== newDay.isHovered || + day.isSelected !== newDay.isSelected || + day.isSelectionStart !== newDay.isSelectionStart || + day.isSelectionEnd !== newDay.isSelectionEnd || + day.isInRange !== newDay.isInRange) { + week.days[dayIndex] = newDay; + } + }); + }); + + // todo: add check for linked calendars + formattedMonth.hideLeftArrow = options.monthIndex > 0 + && 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; + } + + return date.getFullYear() === selectedDate.getFullYear() + && date.getMonth() === selectedDate.getMonth() + && date.getDate() === selectedDate.getDate(); +} + +function isDateInRange(date: Date, selectedRange: Date[], hoveredDate: Date): boolean { + if (!date || !selectedRange[0]) { + return false; + } + + if (selectedRange[1]) { + return date > selectedRange[0] && date <= selectedRange[1]; + } + + if (hoveredDate) { + return date > selectedRange[0] && date <= hoveredDate; + } + + return false; +} diff --git a/src/datepicker/engine/format-month-view.ts b/src/datepicker/engine/format-month-view.ts new file mode 100644 index 0000000000..056712a843 --- /dev/null +++ b/src/datepicker/engine/format-month-view.ts @@ -0,0 +1,28 @@ +import { DatepickerFormatOptions, DaysCalendarModel, MonthViewModel, MonthViewOptions } 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 { + return { + month: daysCalendar.month, + monthTitle: formatDate(daysCalendar.month, formatOptions.monthTitle, formatOptions.locale), + yearTitle: formatDate(daysCalendar.month, formatOptions.yearTitle, formatOptions.locale), + weekNumbers: getWeekNumbers(daysCalendar.daysMatrix, formatOptions.weekNumbers, formatOptions.locale), + weekdays: getLocale(formatOptions.locale).weekdaysShort() as string[], + weeks: daysCalendar.daysMatrix + .map((week: Date[], weekIndex: number) => ({ + days: week.map((date: Date, dayIndex: number) => ({ + date, + label: formatDate(date, formatOptions.dayLabel, formatOptions.locale), + 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) : ''); +} diff --git a/src/datepicker/index.ts b/src/datepicker/index.ts index d75cc47c17..03a1a56953 100644 --- a/src/datepicker/index.ts +++ b/src/datepicker/index.ts @@ -1,15 +1,11 @@ -/* - todo: general: - 1. Popup - 2. Keyboard support - 3. custom-class attribute support - 4. date-disabled attribute support - 5. template-url attribute support - */ export { DatePickerComponent } from './datepicker.component'; export { DatepickerModule } from './datepicker.module'; -export { DayPickerComponent } from './daypicker.component' -export { MonthPickerComponent } from './monthpicker.component' -export { YearPickerComponent } from './yearpicker.component' -export { DateFormatter } from './date-formatter' +export { DayPickerComponent } from './daypicker.component'; +export { MonthPickerComponent } from './monthpicker.component'; +export { YearPickerComponent } from './yearpicker.component'; +export { DateFormatter } from './date-formatter'; export { DatepickerConfig } from './datepicker.config'; + +export { BsDatepickerModule } from './bs-datepicker.module'; +export { BsDatepickerComponent } from './bs-datepicker.component'; +export { BsDaterangepickerComponent } from './bs-daterangepicker.component'; diff --git a/src/datepicker/models/index.ts b/src/datepicker/models/index.ts new file mode 100644 index 0000000000..436c78033f --- /dev/null +++ b/src/datepicker/models/index.ts @@ -0,0 +1,87 @@ +import { Locale } from '../../bs-moment/locale/locale.class'; + +export interface DaysCalendarModel { + daysMatrix: Date[][]; + month: Date; +} + +export interface DayViewModel { + date: Date; + label: string; + // flag step + isDisabled?: boolean; + isHovered?: boolean; + isOtherMonth?: boolean; + isInRange?: boolean; + isSelectionStart?: boolean; + isSelectionEnd?: boolean; + isSelected?: boolean; + // day index + monthIndex?: number; + weekIndex?: number; + dayIndex?: number; +} + +export interface WeekViewModel { + days: DayViewModel[]; +} + +export interface MonthViewModel { + weeks: WeekViewModel[]; + // format step + month: Date; + monthTitle: string; + yearTitle: string; + weekNumbers: string[]; + weekdays: string[]; + // flag step + hideLeftArrow?: boolean; + hideRightArrow?: boolean; +} + +export interface MonthViewOptions { + width?: number; + height?: number; + firstDayOfWeek?: number; +} + +export interface DatepickerFormatOptions { + locale: string; + monthTitle: string; + yearTitle: string; + dayLabel: string; + weekNumbers: string; +} + +export interface DatepickerRenderOptions { + showWeekNumbers?: boolean; + displayMonths?: number; +} + +export interface TimeUnit { + year?: number; + month?: number; + day?: number; + hour?: number; + minute?: number; + seconds?: number; +} + +export type DateFormatterFn = (date: Date, format: string, locale?: Locale) => string; + +export interface LocaleData { + invalidDate: string; + postformat: (str: string) => string; + ordinal: (str: string) => string; +} + +// events + +export interface BsNavigationEvent { + step: TimeUnit; +} + +export interface DayHoverEvent { + day: DayViewModel; + isHovered: boolean; +} diff --git a/src/datepicker/reducer/_defaults.ts b/src/datepicker/reducer/_defaults.ts new file mode 100644 index 0000000000..65e03afded --- /dev/null +++ b/src/datepicker/reducer/_defaults.ts @@ -0,0 +1,20 @@ +import { DatepickerFormatOptions, DatepickerRenderOptions, MonthViewOptions } from '../models/index'; + +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 +}; diff --git a/src/datepicker/reducer/bs-datepicker.actions.ts b/src/datepicker/reducer/bs-datepicker.actions.ts new file mode 100644 index 0000000000..556f81b29e --- /dev/null +++ b/src/datepicker/reducer/bs-datepicker.actions.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@angular/core'; +import { DatepickerRenderOptions, DayHoverEvent, TimeUnit } from '../models/index'; +import { Action } from '../../mini-ngrx/index'; + +@Injectable() +export class BsDatepickerActions { + static readonly CALCULATE = '[datepicker] calculate dates matrix'; + 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 HOVER = '[datepicker] hover date'; + + static readonly SELECT_RANGE = '[daterangepicker] select dates range'; + + calculate(viewDate: Date): Action { + return { + type: BsDatepickerActions.CALCULATE, + payload: viewDate + }; + } + + format(): Action { + return { + type: BsDatepickerActions.FORMAT + }; + } + + flag(): Action { + return { + type: BsDatepickerActions.FLAG + }; + } + + select(date: Date): Action { + return { + type: BsDatepickerActions.SELECT, + payload: date + }; + } + + navigateStep(step: TimeUnit): Action { + return { + type: BsDatepickerActions.STEP_NAVIGATION, + payload: step + }; + } + + renderOptions(options: DatepickerRenderOptions): Action { + return { + type: BsDatepickerActions.RENDER_OPTIONS, + payload: options + }; + } + + // date range picker + selectRange(value: Date[]): Action { + return { + type: BsDatepickerActions.SELECT_RANGE, + payload: value + }; + } + + hover(event: DayHoverEvent): Action { + return { + type: BsDatepickerActions.HOVER, + payload: event.isHovered ? event.day.date : null + }; + } +} diff --git a/src/datepicker/reducer/bs-datepicker.effects.ts b/src/datepicker/reducer/bs-datepicker.effects.ts new file mode 100644 index 0000000000..981a61988c --- /dev/null +++ b/src/datepicker/reducer/bs-datepicker.effects.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@angular/core'; +import { BsDatepickerStore } from './bs-datepicker.store'; +import { BsDatepickerActions } from './bs-datepicker.actions'; + +@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())); + // } +} diff --git a/src/datepicker/reducer/bs-datepicker.reducer.ts b/src/datepicker/reducer/bs-datepicker.reducer.ts new file mode 100644 index 0000000000..4480d9e14b --- /dev/null +++ b/src/datepicker/reducer/bs-datepicker.reducer.ts @@ -0,0 +1,75 @@ +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 { changeDate } from '../utils/date-utils'; +import { flagMonthView } from '../engine/flag-month-view'; + +export function bsDatepickerReducer(state = initialDatepickerState, action: Action): BsDatepickerState { + 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 = changeDate(viewDate, {month: 1}); + } + return Object.assign({}, state, {monthsModel}); + } + + case (BsDatepickerActions.FORMAT): { + const formattedMonths = state.monthsModel + .map((month, monthIndex) => formatMonthView(month, state.formatOptions, monthIndex)); + return Object.assign({}, state, {formattedMonths}); + } + + 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.STEP_NAVIGATION): { + const viewDate = changeDate(state.viewDate, action.payload); + return Object.assign({}, state, {viewDate}); + } + + case(BsDatepickerActions.HOVER): { + return Object.assign({}, state, {hoveredDate: action.payload}); + } + + case(BsDatepickerActions.SELECT): { + return Object.assign({}, state, {selectedDate: action.payload}); + } + + case(BsDatepickerActions.RENDER_OPTIONS): { + return Object.assign({}, state, {renderOptions: action.payload}); + } + + // date range picker + case(BsDatepickerActions.SELECT_RANGE): { + return Object.assign({}, state, {selectedRange: action.payload}); + } + + default: return state; + } +} diff --git a/src/datepicker/reducer/bs-datepicker.state.ts b/src/datepicker/reducer/bs-datepicker.state.ts new file mode 100644 index 0000000000..26693a9b8b --- /dev/null +++ b/src/datepicker/reducer/bs-datepicker.state.ts @@ -0,0 +1,32 @@ +import { + DatepickerFormatOptions, DatepickerRenderOptions, DaysCalendarModel, MonthViewModel, + MonthViewOptions +} from '../models/index'; +import { defaultFormatOptions, defaultMonthOptions, defaultRenderOptions } from './_defaults'; + +export class BsDatepickerState { + // initial date of calendar, today by default + viewDate: Date; + hoveredDate?: Date; + selectedDate?: Date; + + monthsModel?: DaysCalendarModel[]; + formattedMonths?: MonthViewModel[]; + flaggedMonths?: MonthViewModel[]; + + monthViewOptions: MonthViewOptions; + + formatOptions: DatepickerFormatOptions; + renderOptions: DatepickerRenderOptions; + + // daterange picker + selectedRange?: Date[]; +} + +export const initialDatepickerState: BsDatepickerState = { + viewDate: new Date(), + selectedRange: [], + monthViewOptions: defaultMonthOptions, + formatOptions: defaultFormatOptions, + renderOptions: defaultRenderOptions +}; diff --git a/src/datepicker/reducer/bs-datepicker.store.ts b/src/datepicker/reducer/bs-datepicker.store.ts new file mode 100644 index 0000000000..bda7112ed3 --- /dev/null +++ b/src/datepicker/reducer/bs-datepicker.store.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { MiniStore } from '../../mini-ngrx/store.class'; +import { BsDatepickerState, initialDatepickerState } from './bs-datepicker.state'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Action } from '../../mini-ngrx/index'; +import { MiniState } from '../../mini-ngrx/state.class'; +import { bsDatepickerReducer } from './bs-datepicker.reducer'; + +@Injectable() +export class BsDatepickerStore extends MiniStore { + constructor() { + const _dispatcher = new BehaviorSubject({type: '[datepicker] dispatcher init'}); + const state = new MiniState(initialDatepickerState, _dispatcher, bsDatepickerReducer); + super(_dispatcher, bsDatepickerReducer, state); + } +} diff --git a/src/datepicker/themes/bs/bs-datepicker-container.component.ts b/src/datepicker/themes/bs/bs-datepicker-container.component.ts new file mode 100644 index 0000000000..d8adef94d4 --- /dev/null +++ b/src/datepicker/themes/bs/bs-datepicker-container.component.ts @@ -0,0 +1,112 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; +import { BsDatepickerActions } from '../../reducer/bs-datepicker.actions'; +import { + BsNavigationEvent, DatepickerRenderOptions, DayHoverEvent, DayViewModel, + MonthViewModel +} from '../../models/index'; +import 'rxjs/add/operator/filter'; + +@Component({ + selector: 'bs-datepicker-container', + providers: [BsDatepickerStore], + template: ` + + `, + host: { + style: 'position: absolute; display: block;' + } +}) +export class BsDatepickerContainerComponent { + @Input() + set value(value: Date) { + this._bsDatepickerStore.dispatch(this._actions.select(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())); + + // on hover + this._bsDatepickerStore.select(state => state.hoveredDate) + .filter(hoveredDate => !!hoveredDate) + .subscribe(hoveredDate => + this._bsDatepickerStore.dispatch(this._actions.flag())); + } + + 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) { + return; + } + this._bsDatepickerStore.dispatch(this._actions.select(day.date)); + } +} diff --git a/src/datepicker/themes/bs/bs-datepicker-day-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-day-view.component.ts new file mode 100644 index 0000000000..4aff0766c4 --- /dev/null +++ b/src/datepicker/themes/bs/bs-datepicker-day-view.component.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { DayHoverEvent, DayViewModel } from '../../models/index'; + +@Component({ + selector: 'bs-datepicker-day-view', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + {{day.label}} + ` +}) +export class BsDatepickerDayViewComponent { + @Input() day: DayViewModel; + + @Output() onSelect = new EventEmitter(); + @Output() onHover = new EventEmitter(); + + selectDay(day: DayViewModel): void { + this.onSelect.emit(day); + } + + hoverDay(day: DayViewModel, isHovered: boolean): void { + this.onHover.emit({day, isHovered}); + } +} diff --git a/src/datepicker/themes/bs/bs-datepicker-month-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-month-view.component.ts new file mode 100644 index 0000000000..3e9fcf9186 --- /dev/null +++ b/src/datepicker/themes/bs/bs-datepicker-month-view.component.ts @@ -0,0 +1,49 @@ +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] }} + + +
+ ` +}) +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(event: DayHoverEvent): void { + this.onHover.emit(event); + } +} + diff --git a/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts new file mode 100644 index 0000000000..07cfdb4759 --- /dev/null +++ b/src/datepicker/themes/bs/bs-datepicker-navigation-view.component.ts @@ -0,0 +1,31 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { BsNavigationEvent, MonthViewModel, TimeUnit } from '../../models/index'; + +@Component({ + selector: 'bs-datepicker-navigation-view', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + + ` +}) +export class BsDatepickerNavigationViewComponent { + @Input() month: MonthViewModel; + @Output() onNavigate = new EventEmitter(); + + navTo(step: TimeUnit): void { + this.onNavigate.emit({step}); + } + + viewMode(v: string) {} +} diff --git a/src/datepicker/themes/bs/bs-datepicker-view.component.ts b/src/datepicker/themes/bs/bs-datepicker-view.component.ts new file mode 100644 index 0000000000..e308d2c8a9 --- /dev/null +++ b/src/datepicker/themes/bs/bs-datepicker-view.component.ts @@ -0,0 +1,49 @@ +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-daterangepicker-container.component.ts b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts new file mode 100644 index 0000000000..deeb4bd804 --- /dev/null +++ b/src/datepicker/themes/bs/bs-daterangepicker-container.component.ts @@ -0,0 +1,133 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { BsDatepickerStore } from '../../reducer/bs-datepicker.store'; +import { BsDatepickerActions } from '../../reducer/bs-datepicker.actions'; +import { + BsNavigationEvent, DatepickerRenderOptions, DayHoverEvent, DayViewModel, + MonthViewModel +} from '../../models/index'; +import 'rxjs/add/operator/filter'; + +@Component({ + selector: 'bs-daterangepicker-container', + providers: [BsDatepickerStore], + template: ` + + `, + host: { + style: 'position: absolute; display: block;' + } +}) +export class BsDaterangepickerContainerComponent implements OnInit { + @Input() + set value(value: Date[]) { + this._bsDatepickerStore.dispatch(this._actions.selectRange(value)); + } + + @Output() 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 + })); + + // on selected date change + this._bsDatepickerStore.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) { + return; + } + + if (this._rangeStack.length === 1) { + if (day.date >= this._rangeStack[0]) { + this._rangeStack = [this._rangeStack[0], day.date]; + } else { + this._rangeStack = [day.date]; + } + } + + if (this._rangeStack.length === 0) { + this._rangeStack = [day.date]; + } + + this._bsDatepickerStore.dispatch(this._actions.selectRange(this._rangeStack)); + + if (this._rangeStack.length === 2) { + this._rangeStack = []; + } + } +} diff --git a/src/datepicker/utils/bs-calendar-utils.ts b/src/datepicker/utils/bs-calendar-utils.ts new file mode 100644 index 0000000000..6f28d02fae --- /dev/null +++ b/src/datepicker/utils/bs-calendar-utils.ts @@ -0,0 +1,11 @@ +import { getDayOfWeek, isFirstDayOfWeek } from '../../bs-moment/utils/date-getters'; +import { changeDate } from './date-utils'; + +export function getStartingDayOfCalendar(date: Date, options: {firstDayOfWeek?: number}): Date { + if (isFirstDayOfWeek(date, options.firstDayOfWeek)) { + return date; + } + + const weekDay = getDayOfWeek(date); + return changeDate(date, {day: -weekDay}); +} diff --git a/src/datepicker/utils/date-utils.ts b/src/datepicker/utils/date-utils.ts new file mode 100644 index 0000000000..fba50508f4 --- /dev/null +++ b/src/datepicker/utils/date-utils.ts @@ -0,0 +1,21 @@ +import { TimeUnit } from '../models/index'; + +const defaultTimeUnit: TimeUnit = { + year: 0, month: 0, day: 0, hour: 0, minute: 0, seconds: 0 +}; + +export function createDate(year?: number, month = 0, day = 1, hour = 0, minute = 0, seconds = 0): Date { + const _date = new Date(); + return new Date(year || _date.getFullYear(), month, day, hour, minute, seconds); +} + +export function changeDate(date: Date, unit: TimeUnit): Date { + const _unit = Object.assign({}, defaultTimeUnit, unit); + return createDate(date.getFullYear() + _unit.year, + date.getMonth() + _unit.month, + date.getDate() + _unit.day, + date.getHours() + _unit.hour, + date.getMinutes() + _unit.minute, + date.getSeconds() + _unit.seconds + ); +} diff --git a/src/index.ts b/src/index.ts index 9fc9de76a5..c16be5412b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,8 @@ export { CollapseDirective, CollapseModule } from './collapse'; export { DateFormatter, DatePickerComponent, DatepickerConfig, DatepickerModule, - DayPickerComponent, MonthPickerComponent, YearPickerComponent + DayPickerComponent, MonthPickerComponent, YearPickerComponent, + BsDatepickerModule } from './datepicker'; export {