├── src ├── assets │ └── .gitkeep ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── typings.d.ts ├── app │ ├── ranges-footer.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.module.ts │ ├── app.component.ts │ ├── ranges-footer.component.ts │ └── app.component.html ├── index.html ├── main.ts ├── tsconfig.app.json ├── tsconfig.spec.json ├── test.ts ├── styles.scss └── polyfills.ts ├── saturn-datepicker ├── src │ ├── datepicker │ │ ├── calendar-footer.html │ │ ├── _datepicker-theme.scss │ │ ├── README.md │ │ ├── index.ts │ │ ├── tsconfig-build.json │ │ ├── calendar-body.css │ │ ├── datepicker-errors.ts │ │ ├── multi-year-view.html │ │ ├── datepicker-content.css │ │ ├── public-api.ts │ │ ├── year-view.html │ │ ├── datepicker-toggle.css │ │ ├── datepicker-toggle.html │ │ ├── calendar-header.html │ │ ├── month-view.html │ │ ├── datepicker-content.html │ │ ├── calendar.css │ │ ├── datepicker-animations.ts │ │ ├── calendar.html │ │ ├── datepicker-intl.ts │ │ ├── datepicker-module.ts │ │ ├── calendar-body.html │ │ ├── BUILD.bazel │ │ ├── datepicker-toggle.ts │ │ ├── calendar-body.ts │ │ ├── year-view.ts │ │ ├── multi-year-view.ts │ │ ├── month-view.ts │ │ └── calendar.ts │ ├── public-api.ts │ ├── datetime │ │ ├── date-formats.ts │ │ ├── native-date-formats.ts │ │ ├── index.ts │ │ ├── date-adapter.ts │ │ ├── native-date-adapter.ts │ │ └── native-date-adapter.spec.ts │ ├── bundle.css │ └── _theming.scss ├── screenshot.png ├── package.json └── README.md ├── screenshot.png ├── ng-package.json ├── e2e ├── tsconfig.e2e.json ├── app.po.ts └── app.e2e-spec.ts ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── protractor.conf.js ├── renovate.json ├── LICENSE ├── rename.php ├── .angular-cli.json ├── karma.conf.js ├── package.json ├── tslint.json ├── README.md └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar-footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/_datepicker-theme.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaturnTeam/saturn-datepicker/HEAD/screenshot.png -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaturnTeam/saturn-datepicker/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "ngPackage": { 3 | "lib": { 4 | "styleIncludePaths": ["./src"] 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /saturn-datepicker/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaturnTeam/saturn-datepicker/HEAD/saturn-datepicker/screenshot.png -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/README.md: -------------------------------------------------------------------------------- 1 | Please see the official documentation at https://material.angular.io/components/component/datepicker 2 | -------------------------------------------------------------------------------- /src/app/ranges-footer.component.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class NgPackagedPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .code { 2 | background-color: rgba(27,31,35,.05); 3 | border-radius: 3px; 4 | font-size: 85%; 5 | margin: 0; 6 | padding: .2em .4em; 7 | } 8 | section > div { 9 | margin-bottom: 15px; 10 | } 11 | 12 | .inlineCalendarContainer { 13 | width: 300px; 14 | } 15 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | export * from './public-api'; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /saturn-datepicker/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | export * from './datetime/index'; 10 | export * from './datepicker/index'; 11 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NgPackaged 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule); 12 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NgPackagedPage } from './app.po'; 2 | 3 | describe('ng-packaged App', () => { 4 | let page: NgPackagedPage; 5 | 6 | beforeEach(() => { 7 | page = new NgPackagedPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig-build", 3 | "files": [ 4 | "public-api.ts", 5 | "../typings.d.ts" 6 | ], 7 | "angularCompilerOptions": { 8 | "annotateForClosureCompiler": true, 9 | "strictMetadataEmit": true, 10 | "flatModuleOutFile": "index.js", 11 | "flatModuleId": "@angular/material/datepicker", 12 | "skipTemplateCodegen": true, 13 | "fullTemplateTypeCheck": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "angularCompilerOptions": { 4 | "paths": { 5 | "saturn-datepicker": [ "../saturn-datepicker/src/public_api.ts" ] 6 | } 7 | }, 8 | "compilerOptions": { 9 | "outDir": "../out-tsc/app", 10 | "module": "es2015", 11 | "baseUrl": "", 12 | "types": [], 13 | "paths": { 14 | "saturn-datepicker": [ "../saturn-datepicker/src/public_api.ts" ] 15 | } 16 | }, 17 | "exclude": [ 18 | "test.ts", 19 | "**/*.spec.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /saturn-datepicker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saturn-datepicker", 3 | "version": "8.0.6", 4 | "repository": "https://github.com/SaturnTeam/saturn-datepicker", 5 | "keywords": ["angular", "material", "datepicker", "range datepicker"], 6 | "author": "saturn ", 7 | "license": "MIT", 8 | "private": false, 9 | "peerDependencies": { 10 | }, 11 | "ngPackage": { 12 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 13 | "lib": { 14 | "entryFile": "src/public-api.ts" 15 | }, 16 | "dest": "../dist" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "baseUrl": "", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ], 12 | "paths": { 13 | "saturn-datepicker": [ 14 | "../dist/saturn-datepicker" 15 | ] 16 | } 17 | }, 18 | "files": [ 19 | "test.ts", 20 | "polyfills.ts" 21 | ], 22 | "include": [ 23 | "../saturn-datepicker/**/*.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "tmp" 28 | ] 29 | } -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar-body.css: -------------------------------------------------------------------------------- 1 | .mat-calendar-body{min-width:224px}.mat-calendar-body-label{height:0;line-height:0;text-align:left;padding-left:4.71429%;padding-right:4.71429%}.mat-calendar-body-cell{position:relative;height:0;line-height:0;text-align:center;outline:0;cursor:pointer}.mat-calendar-body-disabled{cursor:default}.mat-calendar-body-cell-content{position:absolute;top:5%;left:5%;display:flex;align-items:center;justify-content:center;box-sizing:border-box;width:90%;height:90%;line-height:1;border-width:1px;border-style:solid;border-radius:999px}[dir=rtl] .mat-calendar-body-label{text-align:right} -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | /** @docs-private */ 10 | export function createMissingDateImplError(provider: string) { 11 | return Error( 12 | `SatDatepicker: No provider found for ${provider}. You must import one of the following ` + 13 | `modules at your application root: SatNativeDateModule, MatMomentDateModule, or provide a ` + 14 | `custom implementation.`); 15 | } 16 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/multi-year-view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "outDir": "./dist/out-tsc", 6 | "baseUrl": "./src", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": false, 11 | "experimentalDecorators": true, 12 | "target": "es5", 13 | "typeRoots": [ 14 | "node_modules/@types" 15 | ], 16 | "lib": [ 17 | "es2016", 18 | "dom" 19 | ], 20 | "paths": { 21 | "saturn-datepicker": [ "../dist/saturn-datepicker" ] 22 | } 23 | }, 24 | "exclude": [ 25 | ".ng_build" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datetime/date-formats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {InjectionToken} from '@angular/core'; 10 | 11 | 12 | export type MatDateFormats = { 13 | parse: { 14 | dateInput: any 15 | }, 16 | display: { 17 | dateInput: any, 18 | monthYearLabel: any, 19 | dateA11yLabel: any, 20 | monthYearA11yLabel: any, 21 | } 22 | }; 23 | 24 | 25 | export const MAT_DATE_FORMATS = new InjectionToken('mat-date-formats'); 26 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { MyLibModule } from '@my/lib'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | imports: [ 9 | MyLibModule.forRoot() 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | })); 16 | 17 | it('should create the app', async(() => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.debugElement.componentInstance; 20 | expect(app).toBeTruthy(); 21 | })); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | testem.log 34 | /typings 35 | 36 | # e2e 37 | /e2e/*.js 38 | /e2e/*.map 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-content.css: -------------------------------------------------------------------------------- 1 | .mat-datepicker-content{box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);display:block}.mat-datepicker-content .mat-calendar{width:296px;height:354px}.mat-datepicker-content-touch{box-shadow:0 0 0 0 rgba(0,0,0,.2),0 0 0 0 rgba(0,0,0,.14),0 0 0 0 rgba(0,0,0,.12);display:block;max-height:80vh;overflow:auto;margin:-24px}.mat-datepicker-content-touch .mat-calendar{min-width:250px;min-height:312px;max-width:750px;max-height:788px}@media all and (orientation:landscape){.mat-datepicker-content-touch .mat-calendar{width:64vh;height:80vh}}@media all and (orientation:portrait){.mat-datepicker-content-touch .mat-calendar{width:80vw;height:100vw}} -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/public-api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | export * from './datepicker-module'; 10 | export * from './calendar'; 11 | export * from './calendar-body'; 12 | export * from './datepicker'; 13 | export * from './datepicker-animations'; 14 | export * from './datepicker-input'; 15 | export * from './datepicker-intl'; 16 | export * from './datepicker-toggle'; 17 | export * from './month-view'; 18 | export * from './year-view'; 19 | export {SatMultiYearView, yearsPerPage, yearsPerRow} from './multi-year-view'; 20 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/year-view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datetime/native-date-formats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {MatDateFormats} from './date-formats'; 10 | 11 | 12 | export const MAT_NATIVE_DATE_FORMATS: MatDateFormats = { 13 | parse: { 14 | dateInput: null, 15 | }, 16 | display: { 17 | dateInput: {year: 'numeric', month: 'numeric', day: 'numeric'}, 18 | monthYearLabel: {year: 'numeric', month: 'short'}, 19 | dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'}, 20 | monthYearA11yLabel: {year: 'numeric', month: 'long'}, 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-toggle.css: -------------------------------------------------------------------------------- 1 | .mat-form-field-appearance-legacy .mat-form-field-prefix .mat-datepicker-toggle-default-icon,.mat-form-field-appearance-legacy .mat-form-field-suffix .mat-datepicker-toggle-default-icon{width:1em}.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-datepicker-toggle-default-icon,.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-datepicker-toggle-default-icon{display:block;width:1.5em;height:1.5em}.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-prefix .mat-icon-button .mat-datepicker-toggle-default-icon,.mat-form-field:not(.mat-form-field-appearance-legacy) .mat-form-field-suffix .mat-icon-button .mat-datepicker-toggle-default-icon{margin:auto} -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-toggle.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './e2e/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "pinVersions": false, 3 | "ignoreUnstable": false, 4 | "semanticCommits": true, 5 | "semanticPrefix": "build:", 6 | "commitMessage": "{{semanticPrefix}}update {{depName}} to version {{newVersion}}", 7 | "packageFiles": [ 8 | "package.json", 9 | "saturn-datepicker/package.json" 10 | ], 11 | "packages": [ 12 | { 13 | "packagePattern": "^@angular\/.*", 14 | "groupName": "angular", 15 | "pinVersions": false 16 | }, 17 | { 18 | "packagePattern": "^@types\/.*", 19 | "groupName": "type definitions", 20 | "pinVersions": false 21 | }, 22 | { 23 | "packagePattern": "^jasmine.*", 24 | "groupName": "jasmine", 25 | "pinVersions": false 26 | }, 27 | { 28 | "packagePattern": "^karma.*", 29 | "groupName": "karma", 30 | "pinVersions": false 31 | }, 32 | { 33 | "packagePattern": "^protractor.*", 34 | "groupName": "protractor", 35 | "pinVersions": false 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar-header.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 10 | 11 |
12 | 13 | 14 | 15 | 19 | 20 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/month-view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 22 | 23 |
{{day.narrow}}
24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Herges 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datetime/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {PlatformModule} from '@angular/cdk/platform'; 10 | import {NgModule} from '@angular/core'; 11 | import {DateAdapter} from './date-adapter'; 12 | import {MAT_DATE_FORMATS} from './date-formats'; 13 | import {NativeDateAdapter} from './native-date-adapter'; 14 | import {MAT_NATIVE_DATE_FORMATS} from './native-date-formats'; 15 | 16 | export * from './date-adapter'; 17 | export * from './date-formats'; 18 | export * from './native-date-adapter'; 19 | export * from './native-date-formats'; 20 | 21 | 22 | @NgModule({ 23 | imports: [PlatformModule], 24 | providers: [ 25 | {provide: DateAdapter, useClass: NativeDateAdapter}, 26 | ], 27 | }) 28 | export class NativeDateModule {} 29 | 30 | 31 | @NgModule({ 32 | imports: [NativeDateModule], 33 | providers: [{provide: MAT_DATE_FORMATS, useValue: MAT_NATIVE_DATE_FORMATS}], 34 | }) 35 | export class SatNativeDateModule {} 36 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { MatButtonModule, MatDatepickerModule, MatFormFieldModule, MatInputModule, MatNativeDateModule } from '@angular/material'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { SatDatepickerModule } from '../../saturn-datepicker/src/datepicker'; 7 | import { SatNativeDateModule } from '../../saturn-datepicker/src/datetime'; 8 | import { AppComponent } from './app.component'; 9 | import { RangesFooter } from './ranges-footer.component'; 10 | 11 | 12 | @NgModule({ 13 | declarations: [ 14 | AppComponent, 15 | RangesFooter 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | BrowserAnimationsModule, ReactiveFormsModule, MatDatepickerModule, 20 | MatNativeDateModule, MatFormFieldModule, MatInputModule, 21 | MatButtonModule, SatDatepickerModule, SatNativeDateModule 22 | ], 23 | entryComponents: [RangesFooter], 24 | providers: [], 25 | bootstrap: [AppComponent] 26 | }) 27 | export class AppModule { 28 | } 29 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('../saturn-datepicker', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-content.html: -------------------------------------------------------------------------------- 1 | 26 | 27 | -------------------------------------------------------------------------------- /rename.php: -------------------------------------------------------------------------------- 1 | 0 || strrpos($file, 'ts') > 0) { 33 | $contents = file_get_contents($dir . $file); 34 | $contents = str_replace($sources, $targets, $contents); 35 | file_put_contents($dir . $file, $contents); 36 | // echo $contents; 37 | } 38 | } 39 | } 40 | replace('src/material/datepicker/'); 41 | replace('src/material/core/datetime/'); 42 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | import { RangesFooter } from './ranges-footer.component'; 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.scss'] 9 | }) 10 | export class AppComponent { 11 | @ViewChild('inlineRangePicker', {static: false}) inlineRangePicker; 12 | @ViewChild('inlineSingleDatePicker', {static: false}) inlineSingleDatePicker; 13 | 14 | form: FormGroup; 15 | rangesFooter = RangesFooter; 16 | inlineRange; 17 | inlineBeginDate; 18 | selectedDate; 19 | 20 | constructor(fb: FormBuilder) { 21 | this.form = fb.group({ 22 | date: [{begin: new Date(2018, 7, 5), end: new Date(2018, 7, 25)}] 23 | }); 24 | } 25 | 26 | inlineRangeChange($event) { 27 | this.inlineRange = $event; 28 | } 29 | 30 | inlineBeginChange($event) { 31 | this.inlineBeginDate = $event; 32 | } 33 | 34 | inlineSingleDateChange($event) { 35 | this.selectedDate = $event; 36 | } 37 | 38 | resetRange() { 39 | this.inlineRange = undefined; 40 | this.inlineBeginDate = undefined; 41 | 42 | this.inlineRangePicker._reset(); 43 | } 44 | 45 | resetSingleDate() { 46 | this.selectedDate = undefined; 47 | this.inlineRangePicker._reset(); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar.css: -------------------------------------------------------------------------------- 1 | .mat-calendar{display:block}.mat-calendar-header{padding:8px 8px 0 8px}.mat-calendar-content{padding:0 8px 8px 8px;outline:0}.mat-calendar-controls{display:flex;margin:5% calc(33% / 7 - 16px)}.mat-calendar-spacer{flex:1 1 auto}.mat-calendar-period-button{min-width:0}.mat-calendar-arrow{display:inline-block;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top-width:5px;border-top-style:solid;margin:0 0 0 5px;vertical-align:middle}.mat-calendar-arrow.mat-calendar-invert{transform:rotate(180deg)}[dir=rtl] .mat-calendar-arrow{margin:0 5px 0 0}.mat-calendar-next-button,.mat-calendar-previous-button{position:relative}.mat-calendar-next-button::after,.mat-calendar-previous-button::after{top:0;left:0;right:0;bottom:0;position:absolute;content:'';margin:15.5px;border:0 solid currentColor;border-top-width:2px}[dir=rtl] .mat-calendar-next-button,[dir=rtl] .mat-calendar-previous-button{transform:rotate(180deg)}.mat-calendar-previous-button::after{border-left-width:2px;transform:translateX(2px) rotate(-45deg)}.mat-calendar-next-button::after{border-right-width:2px;transform:translateX(-2px) rotate(45deg)}.mat-calendar-table{border-spacing:0;border-collapse:collapse;width:100%}.mat-calendar-table-header th{text-align:center;padding:0 0 8px 0}.mat-calendar-table-header-divider{position:relative;height:1px}.mat-calendar-table-header-divider::after{content:'';position:absolute;top:0;left:-8px;right:-8px;height:1px} -------------------------------------------------------------------------------- /saturn-datepicker/src/bundle.css: -------------------------------------------------------------------------------- 1 | :not(.mat-calendar-body-disabled):hover > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-semi-selected), .cdk-keyboard-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-semi-selected), .cdk-program-focused .mat-calendar-body-active > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-semi-selected) { 2 | background-color: rgba(0, 0, 0, 0.04); 3 | } 4 | :not(.mat-calendar-body-disabled):hover > .mat-calendar-body-semi-selected, .cdk-keyboard-focused .mat-calendar-body-active > .mat-calendar-body-semi-selected, .cdk-program-focused .mat-calendar-body-active > .mat-calendar-body-semi-selected { 5 | background-color: #3f51b5; 6 | color: white; 7 | } 8 | .mat-calendar-body-begin-range:not(.mat-calendar-body-end-range) { 9 | border-radius: 100% 0 0 100%; 10 | background-color: #c5cae9; 11 | } 12 | .mat-calendar-body-end-range:not(.mat-calendar-body-begin-range) { 13 | border-radius: 0 100% 100% 0; 14 | background-color: #c5cae9; 15 | } 16 | .mat-calendar-body > tr .mat-calendar-cell-semi-selected ~ .mat-calendar-cell-semi-selected { 17 | border-radius: 0; 18 | } 19 | .mat-calendar-cell-semi-selected { 20 | background-color: #c5cae9; 21 | } 22 | .mat-calendar-cell-over > .mat-calendar-body-cell-content { 23 | border: #3f51b5 1px solid; 24 | } -------------------------------------------------------------------------------- /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "range" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.css" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json", 40 | "exclude": "**/node_modules/**" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json", 44 | "exclude": "**/node_modules/**" 45 | }, 46 | { 47 | "project": "e2e/tsconfig.e2e.json", 48 | "exclude": "**/node_modules/**" 49 | } 50 | ], 51 | "test": { 52 | "karma": { 53 | "config": "./karma.conf.js" 54 | } 55 | }, 56 | "defaults": { 57 | "styleExt": "css", 58 | "component": {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | { pattern: './src/test.ts', watched: false } 20 | ], 21 | preprocessors: { 22 | './src/test.ts': ['@angular-devkit/build-angular'] 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | angularCli: { 32 | environment: 'dev' 33 | }, 34 | reporters: config.angularCli && config.angularCli.codeCoverage 35 | ? ['progress', 'coverage-istanbul'] 36 | : ['progress', 'kjhtml'], 37 | port: 9876, 38 | colors: true, 39 | logLevel: config.LOG_INFO, 40 | autoWatch: true, 41 | browsers: ['Chrome'], 42 | singleRun: false 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-animations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | import { 9 | animate, 10 | state, 11 | style, 12 | transition, 13 | trigger, 14 | AnimationTriggerMetadata, 15 | } from '@angular/animations'; 16 | 17 | /** 18 | * Animations used by the Material datepicker. 19 | * @docs-private 20 | */ 21 | export const matDatepickerAnimations: { 22 | readonly transformPanel: AnimationTriggerMetadata; 23 | readonly fadeInCalendar: AnimationTriggerMetadata; 24 | } = { 25 | /** Transforms the height of the datepicker's calendar. */ 26 | transformPanel: trigger('transformPanel', [ 27 | state('void', style({ 28 | opacity: 0, 29 | transform: 'scale(1, 0.8)' 30 | })), 31 | transition('void => enter', animate('120ms cubic-bezier(0, 0, 0.2, 1)', style({ 32 | opacity: 1, 33 | transform: 'scale(1, 1)' 34 | }))), 35 | transition('* => void', animate('100ms linear', style({opacity: 0}))) 36 | ]), 37 | 38 | /** Fades in the content of the calendar. */ 39 | fadeInCalendar: trigger('fadeInCalendar', [ 40 | state('void', style({opacity: 0})), 41 | state('enter', style({opacity: 1})), 42 | 43 | // TODO(crisbeto): this animation should be removed since it isn't quite on spec, but we 44 | // need to keep it until #12440 gets in, otherwise the exit animation will look glitchy. 45 | transition('void => *', animate('120ms 100ms cubic-bezier(0.55, 0, 0.55, 0.2)')) 46 | ]) 47 | }; 48 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 21 | 22 | 23 | 32 | 33 | 34 | 43 | 44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /saturn-datepicker/src/_theming.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | @mixin sat-datepicker-theme($theme) { 4 | @include mat-datepicker-theme($theme); 5 | $primary: map-get($theme, primary); 6 | $foreground: map-get($theme, foreground); 7 | $background: map-get($theme, background); 8 | 9 | $mat-datepicker-selected-today-box-shadow-width: 1px; 10 | $mat-datepicker-selected-fade-amount: 0.6; 11 | $mat-datepicker-today-fade-amount: 0.2; 12 | 13 | :not(.mat-calendar-body-disabled):hover, 14 | .cdk-keyboard-focused .mat-calendar-body-active, 15 | .cdk-program-focused .mat-calendar-body-active { 16 | & > .mat-calendar-body-cell-content:not(.mat-calendar-body-selected):not(.mat-calendar-body-semi-selected) { 17 | background-color: mat-color($background, hover); 18 | } 19 | 20 | & > .mat-calendar-body-semi-selected { 21 | background-color: mat-color($primary); 22 | color: mat-color($primary, default-contrast); 23 | } 24 | } 25 | 26 | .mat-calendar-body-begin-range:not(.mat-calendar-body-end-range) { 27 | border-radius: 100% 0 0 100%; 28 | background-color: mat-color($primary, 100); 29 | } 30 | 31 | .mat-calendar-body-end-range:not(.mat-calendar-body-begin-range) { 32 | border-radius: 0 100% 100% 0; 33 | background-color: mat-color($primary, 100); 34 | } 35 | 36 | .mat-calendar-body > tr .mat-calendar-cell-semi-selected ~ .mat-calendar-cell-semi-selected { 37 | border-radius: 0; 38 | } 39 | 40 | .mat-calendar-cell-semi-selected { 41 | background-color: mat-color($primary, 100); 42 | } 43 | 44 | .mat-calendar-cell-over > .mat-calendar-body-cell-content { 45 | border: mat-color($primary) 1px solid; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | @import '../saturn-datepicker/src/theming'; 3 | // Plus imports for other components in your app. 4 | 5 | // Include the common styles for Angular Material. We include this here so that you only 6 | // have to load a single css file for Angular Material in your app. 7 | // Be sure that you only ever include this mixin once! 8 | @include mat-core(); 9 | 10 | // Define the palettes for your theme using the Material Design palettes available in palette.scss 11 | // (imported above). For each palette, you can optionally specify a default, lighter, and darker 12 | // hue. Available color palettes: https://www.google.com/design/spec/style/color.html 13 | $eltex-primary: mat-palette($mat-indigo); 14 | $eltex-accent: mat-palette($mat-pink, A200, A100, A400); 15 | 16 | // The warn palette is optional (defaults to red). 17 | $eltex-warn: mat-palette($mat-red); 18 | 19 | // Create the theme object (a Sass map containing all of the palettes). 20 | $eltex-theme: mat-light-theme($eltex-primary, $eltex-accent, $eltex-warn); 21 | $eltex-foreground: map-get($eltex-theme, foreground); 22 | $eltex-background: map-get($eltex-theme, background); 23 | // Include theme styles for core and each component used in your app. 24 | // Alternatively, you can import and @include the theme mixins for each component 25 | // that you are using. 26 | @include angular-material-theme($eltex-theme); 27 | 28 | @include mat-datepicker-theme($eltex-theme); 29 | @include sat-datepicker-theme($eltex-theme); 30 | 31 | 32 | .mat-datepicker-content .mat-calendar.range-datepicker { 33 | height: auto; 34 | 35 | .mat-calendar-footer { 36 | padding-bottom: 1rem; 37 | padding-left: 0.5rem; 38 | margin-left: -0.5rem; 39 | button { 40 | margin-left: 0.5rem; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-packaged", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve --project=dev", 9 | "build": "ng build", 10 | "build:app": "cpr dist/saturn-datepicker src/saturn-datepicker && ng build packages && rimraf src/@my", 11 | "build:lib": "rimraf dist && ng-packagr -p saturn-datepicker/package.json && cp saturn-datepicker/src/bundle.css saturn-datepicker/src/_theming.scss dist", 12 | "publish": "(cd dist; npm publish)", 13 | "test": "ng test", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e" 16 | }, 17 | "dependencies": { 18 | "@angular/animations": "10.1.0", 19 | "@angular/cdk": "^10.2.0", 20 | "@angular/common": "10.1.0", 21 | "@angular/compiler": "10.1.0", 22 | "@angular/core": "10.1.0", 23 | "@angular/forms": "10.1.0", 24 | "@angular/material": "10.2.0", 25 | "@angular/platform-browser": "10.1.0", 26 | "@angular/platform-browser-dynamic": "10.1.0", 27 | "rxjs": "~6.6.2", 28 | "zone.js": "~0.10.3" 29 | }, 30 | "devDependencies": { 31 | "@angular-devkit/build-angular": "0.1001.0", 32 | "@angular-devkit/schematics": "10.1.0", 33 | "@angular/cli": "10.1.0", 34 | "@angular/compiler-cli": "10.1.0", 35 | "@angular/language-service": "10.1.0", 36 | "@types/jasmine": "~3.0.0", 37 | "@types/node": "^10.14.18", 38 | "codelyzer": "^5.1.0", 39 | "cpr": "^3.0.0", 40 | "jasmine-core": "~3.3.0", 41 | "jasmine-spec-reporter": "~4.2.0", 42 | "karma": "^3.1.4", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-cli": "~1.0.1", 45 | "karma-coverage-istanbul-reporter": "^2.1.0", 46 | "karma-jasmine": "~2.0.0", 47 | "karma-jasmine-html-reporter": "^1.0.0", 48 | "moment": "^2.24.0", 49 | "ng-packagr": "^10.1.0", 50 | "protractor": "^5.4.2", 51 | "rimraf": "^2.7.1", 52 | "ts-node": "~7.0.0", 53 | "tsickle": "0.37.0", 54 | "tslib": "^1.9.0", 55 | "tslint": "~5.11.0", 56 | "typescript": "~3.9.7" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-intl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {Injectable} from '@angular/core'; 10 | import {Subject} from 'rxjs'; 11 | 12 | 13 | /** Datepicker data that requires internationalization. */ 14 | @Injectable({providedIn: 'root'}) 15 | export class SatDatepickerIntl { 16 | /** 17 | * Stream that emits whenever the labels here are changed. Use this to notify 18 | * components if the labels have changed after initialization. 19 | */ 20 | readonly changes: Subject = new Subject(); 21 | 22 | /** A label for the calendar popup (used by screen readers). */ 23 | calendarLabel: string = 'Calendar'; 24 | 25 | /** A label for the button used to open the calendar popup (used by screen readers). */ 26 | openCalendarLabel: string = 'Open calendar'; 27 | 28 | /** A label for the previous month button (used by screen readers). */ 29 | prevMonthLabel: string = 'Previous month'; 30 | 31 | /** A label for the next month button (used by screen readers). */ 32 | nextMonthLabel: string = 'Next month'; 33 | 34 | /** A label for the previous year button (used by screen readers). */ 35 | prevYearLabel: string = 'Previous year'; 36 | 37 | /** A label for the next year button (used by screen readers). */ 38 | nextYearLabel: string = 'Next year'; 39 | 40 | /** A label for the previous multi-year button (used by screen readers). */ 41 | prevMultiYearLabel: string = 'Previous 20 years'; 42 | 43 | /** A label for the next multi-year button (used by screen readers). */ 44 | nextMultiYearLabel: string = 'Next 20 years'; 45 | 46 | /** A label for the 'switch to month view' button (used by screen readers). */ 47 | switchToMonthViewLabel: string = 'Choose date'; 48 | 49 | /** A label for the 'switch to year view' button (used by screen readers). */ 50 | switchToMultiYearViewLabel: string = 'Choose month and year'; 51 | } 52 | -------------------------------------------------------------------------------- /src/app/ranges-footer.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component } from '@angular/core'; 2 | import * as moment from 'moment'; 3 | import { Subject } from 'rxjs'; 4 | import { takeUntil } from 'rxjs/operators'; 5 | import { SatCalendar, SatCalendarFooter, SatDatepicker } from '../../saturn-datepicker/src/datepicker'; 6 | import { DateAdapter } from '../../saturn-datepicker/src/datetime'; 7 | 8 | @Component({ 9 | templateUrl: './ranges-footer.component.html' 10 | }) 11 | export class RangesFooter implements SatCalendarFooter { 12 | public ranges: Array<{key: string, label: string}> = [ 13 | {key: 'today', label: 'Today'}, 14 | {key: 'thisWeek', label: 'This Week'}, 15 | ]; 16 | private destroyed = new Subject(); 17 | 18 | constructor( 19 | private calendar: SatCalendar, 20 | private datePicker: SatDatepicker, 21 | private dateAdapter: DateAdapter, 22 | cdr: ChangeDetectorRef 23 | ) { 24 | calendar.stateChanges 25 | .pipe(takeUntil(this.destroyed)) 26 | .subscribe(() => cdr.markForCheck()) 27 | } 28 | 29 | setRange(range: string) { 30 | switch (range) { 31 | case 'today': 32 | this.calendar.beginDate = this.dateAdapter.deserialize(new Date()); 33 | this.calendar.endDate = this.dateAdapter.deserialize(new Date()); 34 | this.calendar.activeDate = this.calendar.beginDate; 35 | break; 36 | case 'thisWeek': 37 | const today = moment(); 38 | this.calendar.beginDate = this.dateAdapter.deserialize(today.weekday(0).toDate()); 39 | this.calendar.endDate = this.dateAdapter.deserialize(today.weekday(6).toDate()); 40 | break; 41 | } 42 | this.calendar.activeDate = this.calendar.beginDate; 43 | this.calendar.beginDateSelectedChange.emit(this.calendar.beginDate); 44 | this.calendar.dateRangesChange.emit({begin: this.calendar.beginDate, end: this.calendar.endDate}); 45 | this.datePicker.close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {A11yModule} from '@angular/cdk/a11y'; 10 | import {OverlayModule} from '@angular/cdk/overlay'; 11 | import {PortalModule} from '@angular/cdk/portal'; 12 | import {CommonModule} from '@angular/common'; 13 | import {NgModule} from '@angular/core'; 14 | import {MatButtonModule} from '@angular/material/button'; 15 | import {MatDialogModule} from '@angular/material/dialog'; 16 | import {SatCalendar, SatCalendarHeader, SatCalendarFooter} from './calendar'; 17 | import {SatCalendarBody} from './calendar-body'; 18 | import { 19 | SatDatepicker, 20 | SatDatepickerContent, 21 | MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER, 22 | } from './datepicker'; 23 | import {SatDatepickerInput} from './datepicker-input'; 24 | import {SatDatepickerIntl} from './datepicker-intl'; 25 | import {SatDatepickerToggle, SatDatepickerToggleIcon} from './datepicker-toggle'; 26 | import {SatMonthView} from './month-view'; 27 | import {SatMultiYearView} from './multi-year-view'; 28 | import {SatYearView} from './year-view'; 29 | 30 | 31 | @NgModule({ 32 | imports: [ 33 | CommonModule, 34 | MatButtonModule, 35 | MatDialogModule, 36 | OverlayModule, 37 | A11yModule, 38 | PortalModule, 39 | ], 40 | exports: [ 41 | SatCalendar, 42 | SatCalendarBody, 43 | SatDatepicker, 44 | SatDatepickerContent, 45 | SatDatepickerInput, 46 | SatDatepickerToggle, 47 | SatDatepickerToggleIcon, 48 | SatMonthView, 49 | SatYearView, 50 | SatMultiYearView, 51 | SatCalendarHeader, 52 | SatCalendarFooter, 53 | ], 54 | declarations: [ 55 | SatCalendar, 56 | SatCalendarBody, 57 | SatDatepicker, 58 | SatDatepickerContent, 59 | SatDatepickerInput, 60 | SatDatepickerToggle, 61 | SatDatepickerToggleIcon, 62 | SatMonthView, 63 | SatYearView, 64 | SatMultiYearView, 65 | SatCalendarHeader, 66 | SatCalendarFooter, 67 | ], 68 | providers: [ 69 | SatDatepickerIntl, 70 | MAT_DATEPICKER_SCROLL_STRATEGY_FACTORY_PROVIDER, 71 | ], 72 | entryComponents: [ 73 | SatDatepickerContent, 74 | SatCalendarHeader, 75 | SatCalendarFooter, 76 | ] 77 | }) 78 | export class SatDatepickerModule {} 79 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | import 'core-js/es6/reflect'; 45 | 46 | 47 | 48 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 49 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 50 | 51 | 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js/dist/zone'; // Included with Angular CLI. 57 | 58 | 59 | 60 | /*************************************************************************************************** 61 | * APPLICATION IMPORTS 62 | */ 63 | 64 | /** 65 | * Date, currency, decimal and percent pipes. 66 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 67 | */ 68 | // import 'intl'; // Run `npm install --save intl`. 69 | /** 70 | * Need to import at least one locale-data with intl. 71 | */ 72 | // import 'intl/locale-data/jsonp/en'; 73 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar-body.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | {{label}} 11 | 12 | 13 | 14 | 15 | 16 | 22 | 28 | {{_firstRowOffset >= labelMinRequiredCells ? label : ''}} 29 | 30 | 49 |
53 | {{item.displayValue}} 54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/BUILD.bazel: -------------------------------------------------------------------------------- 1 | package(default_visibility=["//visibility:public"]) 2 | load("@angular//:index.bzl", "ng_module") 3 | load("@io_bazel_rules_sass//sass:sass.bzl", "sass_library", "sass_binary") 4 | 5 | 6 | ng_module( 7 | name = "datepicker", 8 | srcs = glob(["**/*.ts"], exclude=["**/*.spec.ts"]), 9 | module_name = "@angular/material/datepicker", 10 | assets = [ 11 | ":datepicker_content_css", 12 | ":datepicker_toggle_css", 13 | ":calendar_body_css", 14 | ":calendar_css", 15 | ], 16 | deps = [ 17 | "//src/lib/core", 18 | "//src/lib/button", 19 | "//src/lib/dialog", 20 | "//src/lib/input", 21 | "//src/cdk/a11y", 22 | "//src/cdk/bidi", 23 | "//src/cdk/coercion", 24 | "//src/cdk/keycodes", 25 | "//src/cdk/portal", 26 | "//src/cdk/overlay", 27 | ], 28 | tsconfig = ":tsconfig-build.json", 29 | ) 30 | 31 | # TODO(jelbourn): replace this w/ sass_library when it supports acting like a filegroup 32 | filegroup( 33 | name = "datepicker_scss_partials", 34 | srcs = glob(["**/_*.scss"]), 35 | ) 36 | 37 | sass_binary( 38 | name = "datepicker_content_scss", 39 | src = "datepicker-content.scss", 40 | deps = ["//src/lib/core:core_scss_lib"], 41 | ) 42 | 43 | sass_binary( 44 | name = "datepicker_toggle_scss", 45 | src = "datepicker-toggle.scss", 46 | deps = ["//src/lib/core:core_scss_lib"], 47 | ) 48 | 49 | sass_binary( 50 | name = "calendar_scss", 51 | src = "calendar.scss", 52 | deps = ["//src/lib/core:core_scss_lib"], 53 | ) 54 | 55 | sass_binary( 56 | name = "calendar_body_scss", 57 | src = "calendar-body.scss", 58 | deps = ["//src/lib/core:core_scss_lib"], 59 | ) 60 | 61 | # TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir. 62 | # Copy the output of the sass_binary such that the filename and path match what we expect. 63 | genrule( 64 | name = "datepicker_content_css", 65 | srcs = [":datepicker_content_scss"], 66 | outs = ["datepicker-content.css"], 67 | cmd = "cat $(locations :datepicker_content_scss) > $@", 68 | ) 69 | 70 | # TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir. 71 | # Copy the output of the sass_binary such that the filename and path match what we expect. 72 | genrule( 73 | name = "datepicker_toggle_css", 74 | srcs = [":datepicker_toggle_scss"], 75 | outs = ["datepicker-toggle.css"], 76 | cmd = "cat $(locations :datepicker_toggle_scss) > $@", 77 | ) 78 | 79 | # TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir. 80 | # Copy the output of the sass_binary such that the filename and path match what we expect. 81 | genrule( 82 | name = "calendar_css", 83 | srcs = [":calendar_scss"], 84 | outs = ["calendar.css"], 85 | cmd = "cat $(locations :calendar_scss) > $@", 86 | ) 87 | 88 | # TODO(jelbourn): remove this when sass_binary supports specifying an output filename and dir. 89 | # Copy the output of the sass_binary such that the filename and path match what we expect. 90 | genrule( 91 | name = "calendar_body_css", 92 | srcs = [":calendar_body_scss"], 93 | outs = ["calendar-body.css"], 94 | cmd = "cat $(locations :calendar_body_scss) > $@", 95 | ) 96 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [ 16 | true 17 | ], 18 | "import-spacing": true, 19 | "indent": [ 20 | true, 21 | "spaces" 22 | ], 23 | "interface-over-type-literal": true, 24 | "label-position": true, 25 | "max-line-length": [ 26 | true, 27 | 140 28 | ], 29 | "member-access": false, 30 | "member-ordering": [ 31 | true, 32 | "static-before-instance", 33 | "variables-before-functions" 34 | ], 35 | "no-arg": true, 36 | "no-bitwise": true, 37 | "no-console": [ 38 | true, 39 | "debug", 40 | "info", 41 | "time", 42 | "timeEnd", 43 | "trace" 44 | ], 45 | "no-construct": true, 46 | "no-debugger": true, 47 | "no-empty": false, 48 | "no-empty-interface": true, 49 | "no-eval": true, 50 | "no-inferrable-types": [ 51 | true, 52 | "ignore-params" 53 | ], 54 | "no-shadowed-variable": true, 55 | "no-string-literal": false, 56 | "no-string-throw": true, 57 | "no-switch-case-fall-through": true, 58 | "no-trailing-whitespace": true, 59 | "no-unused-expression": true, 60 | "no-use-before-declare": true, 61 | "no-var-keyword": true, 62 | "object-literal-sort-keys": false, 63 | "one-line": [ 64 | true, 65 | "check-open-brace", 66 | "check-catch", 67 | "check-else", 68 | "check-whitespace" 69 | ], 70 | "prefer-const": true, 71 | "quotemark": [ 72 | true, 73 | "single" 74 | ], 75 | "radix": true, 76 | "semicolon": [ 77 | "always" 78 | ], 79 | "triple-equals": [ 80 | true, 81 | "allow-null-check" 82 | ], 83 | "typedef-whitespace": [ 84 | true, 85 | { 86 | "call-signature": "nospace", 87 | "index-signature": "nospace", 88 | "parameter": "nospace", 89 | "property-declaration": "nospace", 90 | "variable-declaration": "nospace" 91 | } 92 | ], 93 | "typeof-compare": true, 94 | "unified-signatures": true, 95 | "variable-name": false, 96 | "whitespace": [ 97 | true, 98 | "check-branch", 99 | "check-decl", 100 | "check-operator", 101 | "check-separator", 102 | "check-type" 103 | ], 104 | "directive-selector": [ 105 | true, 106 | "attribute", 107 | "app", 108 | "camelCase" 109 | ], 110 | "component-selector": [ 111 | true, 112 | "element", 113 | "app", 114 | "kebab-case" 115 | ], 116 | "use-input-property-decorator": true, 117 | "use-output-property-decorator": true, 118 | "use-host-property-decorator": true, 119 | "no-input-rename": true, 120 | "no-output-rename": true, 121 | "use-life-cycle-interface": true, 122 | "use-pipe-transform-interface": true, 123 | "component-class-suffix": true, 124 | "directive-class-suffix": true, 125 | "no-access-missing-member": true, 126 | "templates-use-public": true, 127 | "invoke-injectable": true 128 | } 129 | } -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/datepicker-toggle.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {coerceBooleanProperty} from '@angular/cdk/coercion'; 10 | import { 11 | AfterContentInit, 12 | Attribute, 13 | ChangeDetectionStrategy, 14 | ChangeDetectorRef, 15 | Component, 16 | ContentChild, 17 | Directive, 18 | Input, 19 | OnChanges, 20 | OnDestroy, 21 | SimpleChanges, 22 | ViewEncapsulation, 23 | ViewChild, 24 | } from '@angular/core'; 25 | import {MatButton} from '@angular/material/button'; 26 | import {merge, of as observableOf, Subscription} from 'rxjs'; 27 | import {SatDatepicker} from './datepicker'; 28 | import {SatDatepickerIntl} from './datepicker-intl'; 29 | 30 | 31 | /** Can be used to override the icon of a `matDatepickerToggle`. */ 32 | @Directive({ 33 | selector: '[matDatepickerToggleIcon]' 34 | }) 35 | export class SatDatepickerToggleIcon {} 36 | 37 | 38 | @Component({ 39 | selector: 'sat-datepicker-toggle', 40 | templateUrl: 'datepicker-toggle.html', 41 | styleUrls: ['datepicker-toggle.css'], 42 | host: { 43 | 'class': 'mat-datepicker-toggle', 44 | // Always set the tabindex to -1 so that it doesn't overlap with any custom tabindex the 45 | // consumer may have provided, while still being able to receive focus. 46 | '[attr.tabindex]': '-1', 47 | '[class.mat-datepicker-toggle-active]': 'datepicker && datepicker.opened', 48 | '[class.mat-accent]': 'datepicker && datepicker.color === "accent"', 49 | '[class.mat-warn]': 'datepicker && datepicker.color === "warn"', 50 | '(focus)': '_button.focus()', 51 | }, 52 | exportAs: 'matDatepickerToggle', 53 | encapsulation: ViewEncapsulation.None, 54 | changeDetection: ChangeDetectionStrategy.OnPush, 55 | }) 56 | export class SatDatepickerToggle implements AfterContentInit, OnChanges, OnDestroy { 57 | private _stateChanges = Subscription.EMPTY; 58 | 59 | /** Datepicker instance that the button will toggle. */ 60 | @Input('for') datepicker: SatDatepicker; 61 | 62 | /** Tabindex for the toggle. */ 63 | @Input() tabIndex: number | null; 64 | 65 | /** Whether the toggle button is disabled. */ 66 | @Input() 67 | get disabled(): boolean { 68 | if (this._disabled === undefined && this.datepicker) { 69 | return this.datepicker.disabled; 70 | } 71 | 72 | return !!this._disabled; 73 | } 74 | set disabled(value: boolean) { 75 | this._disabled = coerceBooleanProperty(value); 76 | } 77 | private _disabled: boolean; 78 | 79 | /** Whether ripples on the toggle should be disabled. */ 80 | @Input() disableRipple: boolean; 81 | 82 | /** Custom icon set by the consumer. */ 83 | @ContentChild(SatDatepickerToggleIcon, {static: false}) _customIcon: SatDatepickerToggleIcon; 84 | 85 | /** Underlying button element. */ 86 | @ViewChild('button', {static: false}) _button: MatButton; 87 | 88 | constructor( 89 | public _intl: SatDatepickerIntl, 90 | private _changeDetectorRef: ChangeDetectorRef, 91 | @Attribute('tabindex') defaultTabIndex: string) { 92 | 93 | const parsedTabIndex = Number(defaultTabIndex); 94 | this.tabIndex = (parsedTabIndex || parsedTabIndex === 0) ? parsedTabIndex : null; 95 | } 96 | 97 | ngOnChanges(changes: SimpleChanges) { 98 | if (changes['datepicker']) { 99 | this._watchStateChanges(); 100 | } 101 | } 102 | 103 | ngOnDestroy() { 104 | this._stateChanges.unsubscribe(); 105 | } 106 | 107 | ngAfterContentInit() { 108 | this._watchStateChanges(); 109 | } 110 | 111 | _open(event: Event): void { 112 | if (this.datepicker && !this.disabled) { 113 | this.datepicker.open(); 114 | event.stopPropagation(); 115 | } 116 | } 117 | 118 | private _watchStateChanges() { 119 | const datepickerDisabled = this.datepicker ? this.datepicker._disabledChange : observableOf(); 120 | const inputDisabled = this.datepicker && this.datepicker._datepickerInput ? 121 | this.datepicker._datepickerInput._disabledChange : observableOf(); 122 | const datepickerToggled = this.datepicker ? 123 | merge(this.datepicker.openedStream, this.datepicker.closedStream) : 124 | observableOf(); 125 | 126 | this._stateChanges.unsubscribe(); 127 | this._stateChanges = merge( 128 | this._intl.changes, 129 | datepickerDisabled, 130 | inputDisabled, 131 | datepickerToggled 132 | ).subscribe(() => this._changeDetectorRef.markForCheck()); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Material range datepicker 2 | Material datepicker with range support 3 | ## What is this? 4 | 5 | This is patched version of Material Datepicker for Angular with range selection support. 6 | I created this repository and this package due to it takes a lot of time to contribute to material2 repository: 7 | [Issue #4763 in material2 repo](https://github.com/angular/material2/issues/4763) from 2017-present.
8 | 9 | [![Material date range picker](screenshot.png)](https://stackblitz.com/edit/angular-4cfnyl) 10 | 11 | ## [DEMO with extra features examples](https://stackblitz.com/edit/angular-4cfnyl) 12 | ## Advantages 13 | 1) You can change order of views: month, year and multi-year 14 | 2) You can keep calendar to be opened after selecting a date (in singular range mode) or a range of dates (in range mode) 15 | 3) **You can use all original material attributes: min, max, formControl and others** 16 | 17 | ## Changelog 18 | ## 8.0.6 19 | Fixed randomly selected date when using `rangeHoverEffect = false` and changing between months 20 | ## 8.0.5 21 | Works with angular 9.0 22 | ## 8.0.4 23 | Fix building process 24 | ## 8.0.2 25 | Added option to switch off `rangeHoverEffect` 26 | ## 8.0.1 27 | Fixed loading issue on es2015 targets 28 | ## 8.0.0 29 | Updated to 8.2.0 material code 30 | ## 7.4.0 31 | Inline usage of calendar. See demo. 32 | Thanks to [@beyondsanity](https://github.com/beyondsanity) for this feature 33 | ## 7.3.0 34 | Introducing footer component for calendar. See demo for example usage. 35 | Thanks [@paullryan](https://github.com/paullryan) for this feature 36 | ## 7.2.1 37 | Update to datepicker material 7.3.1 38 | ## 7.2.0 39 | Select first date on close feature 40 | ## 7.1.0 41 | Range selection have a preview now. 42 | ## 6.1.0 43 | Merged #31 44 | * Add option to sort views when clicking on period label button (month -> year or year -> month) 45 | * Add option to enable closing datepicker after date selection #30 46 | 47 | ## It's awesome, but how to use it? 48 | 49 | As easy as pie. 50 | Installation: `yarn add saturn-datepicker` or `npm install saturn-datepicker` 51 | Import `SatDatepickerModule`, `SatNativeDateModule` and `MatDatepickerModule` 52 | ```angular2html 53 | 54 | 58 | 59 | 60 | 61 | ``` 62 | 63 | Add styles: 64 | * If you are using CSS: copy-paste or include somehow the file `saturn-datepicker/bundle.css` 65 | * If you are using SCSS (preferable): 66 | ```scss 67 | @import '~saturn-datepicker/theming'; 68 | @include sat-datepicker-theme($theme); // material theme data structure https://material.angular.io/guide/theming#defining-a-custom-theme 69 | ``` 70 | 71 | ## ngModel/formControl value have this interface: 72 | ```typescript 73 | export interface SatDatepickerRangeValue { 74 | begin: D | null; 75 | end: D | null; 76 | } 77 | ``` 78 | ## FAQ 79 | ### How to change date format or locale? 80 | As same as for material, but with more code, just import constants from 'saturn-datepicker'. 81 | 82 | Also you need to install `@angular/material-moment-adapter` package. 83 | ``` 84 | import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, SatDatepickerModule } from 'saturn-datepicker' 85 | import { MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from '@angular/material-moment-adapter' 86 | 87 | 88 | @NgModule({ 89 | imports: [ 90 | SatDatepickerModule, 91 | ], 92 | providers: [ 93 | MyReportsService, 94 | {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}, 95 | {provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS}, 96 | ], 97 | }) 98 | export class MyModule { 99 | } 100 | 101 | For advanced formatting, please look through material documentation. 102 | 103 | Also you can see [#78](https://github.com/SaturnTeam/saturn-datepicker/issues/78) [#81](https://github.com/SaturnTeam/saturn-datepicker/issues/81) [#53](https://github.com/SaturnTeam/saturn-datepicker/issues/53) 104 | 105 | ``` 106 | 107 | ### How to add option to sort views when clicking on period label button ? 108 | `orderPeriodLabel` option sort the label period views. 109 | - Default [multi-year]: multi-year then back to month 110 | - Month [month]: month > year > multi-year 111 | 112 | ```angular2html 113 | 114 | 115 | 119 | 120 | 121 | ``` 122 | 123 | ### How disable closing datepicker after date selection ? 124 | `closeAfterSelection` option enables or disables datepicker closing after date selections. By default the option is true 125 | 126 | ```angular2html 127 | 128 | 129 | 133 | 134 | 135 | ``` 136 | 137 | ### In range mode, how to select the first date selected if the user closes the picker without selecting another ? 138 | `selectFirstDateOnClose` option enables or disables the selection of the first date when closing the datepicker before selecting another date. 139 | By default the option is false 140 | 141 | ```angular2html 142 | 143 | 144 | 148 | 149 | 150 | ``` 151 | 152 | ### In range mode, how to disable the mouseover effect ? 153 | `rangeHoverEffect` option enables or disables the mouseover listener on days when the rangeMode parameter is used and is enabled. 154 | By default the option is true 155 | 156 | ```angular2html 157 | 158 | 159 | 164 | 165 | 166 | ``` 167 | 168 | --- 169 | Licence: MIT 170 | -------------------------------------------------------------------------------- /saturn-datepicker/README.md: -------------------------------------------------------------------------------- 1 | # Material range datepicker 2 | Material datepicker with range support 3 | ## What is this? 4 | 5 | This is patched version of Material Datepicker for Angular with range selection support. 6 | I created this repository and this package due to it takes a lot of time to contribute to material2 repository: 7 | [Issue #4763 in material2 repo](https://github.com/angular/material2/issues/4763) from 2017-present.
8 | 9 | [![Material date range picker](screenshot.png)](https://stackblitz.com/edit/angular-4cfnyl) 10 | 11 | ## [DEMO with extra features examples](https://stackblitz.com/edit/angular-4cfnyl) 12 | ## Advantages 13 | 1) You can change order of views: month, year and multi-year 14 | 2) You can keep calendar to be opened after selecting a date (in singular range mode) or a range of dates (in range mode) 15 | 3) **You can use all original material attributes: min, max, formControl and others** 16 | 17 | ## Changelog 18 | ## 8.0.6 19 | Fixed randomly selected date when using `rangeHoverEffect = false` and changing between months 20 | ## 8.0.5 21 | Works with angular 9.0 22 | ## 8.0.4 23 | Fix building process 24 | ## 8.0.2 25 | Added option to switch off `rangeHoverEffect` 26 | ## 8.0.1 27 | Fixed loading issue on es2015 targets 28 | ## 8.0.0 29 | Updated to 8.2.0 material code 30 | ## 7.4.0 31 | Inline usage of calendar. See demo. 32 | Thanks to [@beyondsanity](https://github.com/beyondsanity) for this feature 33 | ## 7.3.0 34 | Introducing footer component for calendar. See demo for example usage. 35 | Thanks [@paullryan](https://github.com/paullryan) for this feature 36 | ## 7.2.1 37 | Update to datepicker material 7.3.1 38 | ## 7.2.0 39 | Select first date on close feature 40 | ## 7.1.0 41 | Range selection have a preview now. 42 | ## 6.1.0 43 | Merged #31 44 | * Add option to sort views when clicking on period label button (month -> year or year -> month) 45 | * Add option to enable closing datepicker after date selection #30 46 | 47 | ## It's awesome, but how to use it? 48 | 49 | As easy as pie. 50 | Installation: `yarn add saturn-datepicker` or `npm install saturn-datepicker` 51 | Import `SatDatepickerModule`, `SatNativeDateModule` and `MatDatepickerModule` 52 | ```angular2html 53 | 54 | 58 | 59 | 60 | 61 | ``` 62 | 63 | Add styles: 64 | * If you are using CSS: copy-paste or include somehow the file `saturn-datepicker/bundle.css` 65 | * If you are using SCSS (preferable): 66 | ```scss 67 | @import '~saturn-datepicker/theming'; 68 | @include sat-datepicker-theme($theme); // material theme data structure https://material.angular.io/guide/theming#defining-a-custom-theme 69 | ``` 70 | 71 | ## ngModel/formControl value have this interface: 72 | ```typescript 73 | export interface SatDatepickerRangeValue { 74 | begin: D | null; 75 | end: D | null; 76 | } 77 | ``` 78 | ## FAQ 79 | ### How to change date format or locale? 80 | As same as for material, but with more code, just import constants from 'saturn-datepicker'. 81 | 82 | Also you need to install `@angular/material-moment-adapter` package. 83 | ``` 84 | import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE, SatDatepickerModule } from 'saturn-datepicker' 85 | import { MAT_MOMENT_DATE_FORMATS, MomentDateAdapter } from '@angular/material-moment-adapter' 86 | 87 | 88 | @NgModule({ 89 | imports: [ 90 | SatDatepickerModule, 91 | ], 92 | providers: [ 93 | MyReportsService, 94 | {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}, 95 | {provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS}, 96 | ], 97 | }) 98 | export class MyModule { 99 | } 100 | 101 | For advanced formatting, please look through material documentation. 102 | 103 | Also you can see [#78](https://github.com/SaturnTeam/saturn-datepicker/issues/78) [#81](https://github.com/SaturnTeam/saturn-datepicker/issues/81) [#53](https://github.com/SaturnTeam/saturn-datepicker/issues/53) 104 | 105 | ``` 106 | 107 | ### How to add option to sort views when clicking on period label button ? 108 | `orderPeriodLabel` option sort the label period views. 109 | - Default [multi-year]: multi-year then back to month 110 | - Month [month]: month > year > multi-year 111 | 112 | ```angular2html 113 | 114 | 115 | 119 | 120 | 121 | ``` 122 | 123 | ### How disable closing datepicker after date selection ? 124 | `closeAfterSelection` option enables or disables datepicker closing after date selections. By default the option is true 125 | 126 | ```angular2html 127 | 128 | 129 | 133 | 134 | 135 | ``` 136 | 137 | ### In range mode, how to select the first date selected if the user closes the picker without selecting another ? 138 | `selectFirstDateOnClose` option enables or disables the selection of the first date when closing the datepicker before selecting another date. 139 | By default the option is false 140 | 141 | ```angular2html 142 | 143 | 144 | 148 | 149 | 150 | ``` 151 | 152 | ### In range mode, how to disable the mouseover effect ? 153 | `rangeHoverEffect` option enables or disables the mouseover listener on days when the rangeMode parameter is used and is enabled. 154 | By default the option is true 155 | 156 | ```angular2html 157 | 158 | 159 | 164 | 165 | 166 | ``` 167 | 168 | --- 169 | Licence: MIT 170 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |

SatDatepicker features

3 |
4 |

Features from original datepicker 5 |

6 | 7 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

Range selection

19 |
20 | rangeMode option enables or disables range mode. Default is false 21 |
22 | 23 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |

Close after selection

35 |
36 | closeAfterSelection option enables or disables datepicker closing after date 37 | selections. By 38 | default the option is true 39 |
40 | 41 | 45 | 47 | 48 | 49 | 50 |
51 |
52 |
53 |

Calendar view order

54 |
55 | orderPeriodLabel option sorts the label period views. 56 |
    57 |
  • Default [multi-year]: multi-year then back to month 58 |
  • 59 |
  • Month [month]: month > year > multi-year
  • 60 |
61 |
62 | 63 | 67 | 69 | 70 | 71 | 72 |
73 |
74 |
75 |

Select first date on close

76 |
77 | selectFirstDateOnClose option enables or disables the selection of the first date when closing 78 | the datepicker before selecting another date. 79 | By default the option is false 80 |
81 | 82 | 86 | 88 | 89 | 90 | 91 |
92 |
93 |
94 |

Custom Footer (Default Ranges)

95 |
96 | calendarFooterComponent option allows you to define a footer in the calendar simalar to custom headers. 97 |
98 | 99 | 103 | 107 | 108 | 109 | 110 |
111 |
112 |
113 |

Inline Usage - Range

114 |
115 | The sat-calendar component with its dateRangesChange output binding can be used as an inline range date picker. Validation NOT WORKING. 116 |
117 | 118 | {{ inlineRange.begin | date }} 119 | {{ inlineRange.end | date }} 120 | 121 | 122 | 123 | {{ inlineBeginDate | date }} 124 | 125 | 126 |
127 | 131 | 132 |
133 |
134 |
135 |
136 |

Inline Usage - Single Date

137 |
138 | The sat-calendar component with its selectedChange output binding can be used as an inline date picker. Validation NOT WORKING. 139 |
140 | 141 | {{ selectedDate | date }} 142 | 143 | 144 |
145 | 149 | 150 |
151 |
152 |
153 |
154 |

Disable range mouseover effect

155 |
156 | rangeHoverEffect option enables or disables the mouseover listener on days when the rangeMode parameter is used and is enabled. 157 | By default the option is true 158 |
159 | 160 | 164 | 165 | 166 | 167 |
168 |
169 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "dev": { 7 | "root": "", 8 | "projectType": "application", 9 | "architect": { 10 | "build": { 11 | "builder": "@angular-devkit/build-angular:browser", 12 | "options": { 13 | "outputPath": "dist/app", 14 | "index": "src/index.html", 15 | "main": "src/main.ts", 16 | "tsConfig": "src/tsconfig.app.json", 17 | "polyfills": "src/polyfills.ts", 18 | "assets": [ 19 | { 20 | "glob": "**/*", 21 | "input": "src/assets", 22 | "output": "/assets" 23 | }, 24 | { 25 | "glob": "favicon.ico", 26 | "input": "src", 27 | "output": "/" 28 | } 29 | ], 30 | "styles": [ 31 | "src/styles.scss" 32 | ], 33 | "scripts": [] 34 | }, 35 | "configurations": { 36 | "production": { 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true, 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ] 52 | } 53 | } 54 | }, 55 | "serve": { 56 | "builder": "@angular-devkit/build-angular:dev-server", 57 | "options": { 58 | "browserTarget": "dev:build" 59 | }, 60 | "configurations": { 61 | "production": { 62 | "browserTarget": "dev:build:production" 63 | } 64 | } 65 | }, 66 | "extract-i18n": { 67 | "builder": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "browserTarget": "dev:build" 70 | } 71 | }, 72 | "test": { 73 | "builder": "@angular-devkit/build-angular:karma", 74 | "options": { 75 | "main": "src/test.ts", 76 | "karmaConfig": "./karma.conf.js", 77 | "polyfills": "src/polyfills.ts", 78 | "tsConfig": "src/tsconfig.spec.json", 79 | "scripts": [], 80 | "styles": [ 81 | "src/styles.css" 82 | ], 83 | "assets": [ 84 | { 85 | "glob": "**/*", 86 | "input": "src/assets", 87 | "output": "/assets" 88 | }, 89 | { 90 | "glob": "favicon.ico", 91 | "input": "src", 92 | "output": "/" 93 | } 94 | ] 95 | } 96 | }, 97 | "lint": { 98 | "builder": "@angular-devkit/build-angular:tslint", 99 | "options": { 100 | "tsConfig": [ 101 | "src/tsconfig.app.json", 102 | "src/tsconfig.spec.json" 103 | ], 104 | "exclude": [] 105 | } 106 | } 107 | } 108 | }, 109 | "dev-e2e": { 110 | "root": "", 111 | "projectType": "application", 112 | "cli": {}, 113 | "schematics": {}, 114 | "architect": { 115 | "e2e": { 116 | "builder": "@angular-devkit/build-angular:protractor", 117 | "options": { 118 | "protractorConfig": "./protractor.conf.js", 119 | "devServerTarget": "dev:serve" 120 | } 121 | }, 122 | "lint": { 123 | "builder": "@angular-devkit/build-angular:tslint", 124 | "options": { 125 | "tsConfig": [ 126 | "e2e/tsconfig.e2e.json" 127 | ], 128 | "exclude": [] 129 | } 130 | } 131 | } 132 | }, 133 | "packages": { 134 | "root": "", 135 | "projectType": "application", 136 | "architect": { 137 | "build": { 138 | "builder": "@angular-devkit/build-angular:browser", 139 | "options": { 140 | "outputPath": "dist/app", 141 | "index": "src/index.html", 142 | "main": "src/main.ts", 143 | "tsConfig": "src/tsconfig.packages.json", 144 | "polyfills": "src/polyfills.ts", 145 | "assets": [ 146 | { 147 | "glob": "**/*", 148 | "input": "src/assets", 149 | "output": "/assets" 150 | }, 151 | { 152 | "glob": "favicon.ico", 153 | "input": "src", 154 | "output": "/" 155 | } 156 | ], 157 | "styles": [ 158 | "src/styles.css" 159 | ], 160 | "scripts": [] 161 | }, 162 | "configurations": { 163 | "production": { 164 | "optimization": true, 165 | "outputHashing": "all", 166 | "sourceMap": false, 167 | "extractCss": true, 168 | "namedChunks": false, 169 | "aot": true, 170 | "extractLicenses": true, 171 | "vendorChunk": false, 172 | "buildOptimizer": true, 173 | "fileReplacements": [ 174 | { 175 | "replace": "src/environments/environment.ts", 176 | "with": "src/environments/environment.prod.ts" 177 | } 178 | ] 179 | } 180 | } 181 | }, 182 | "serve": { 183 | "builder": "@angular-devkit/build-angular:dev-server", 184 | "options": { 185 | "browserTarget": "packages:build" 186 | }, 187 | "configurations": { 188 | "production": { 189 | "browserTarget": "packages:build:production" 190 | } 191 | } 192 | }, 193 | "extract-i18n": { 194 | "builder": "@angular-devkit/build-angular:extract-i18n", 195 | "options": { 196 | "browserTarget": "packages:build" 197 | } 198 | }, 199 | "test": { 200 | "builder": "@angular-devkit/build-angular:karma", 201 | "options": { 202 | "main": "src/test.ts", 203 | "karmaConfig": "./karma.conf.js", 204 | "polyfills": "src/polyfills.ts", 205 | "tsConfig": "src/tsconfig.spec.json", 206 | "scripts": [], 207 | "styles": [ 208 | "src/styles.css" 209 | ], 210 | "assets": [ 211 | { 212 | "glob": "**/*", 213 | "input": "src/assets", 214 | "output": "/assets" 215 | }, 216 | { 217 | "glob": "favicon.ico", 218 | "input": "src", 219 | "output": "/" 220 | } 221 | ] 222 | } 223 | }, 224 | "lint": { 225 | "builder": "@angular-devkit/build-angular:tslint", 226 | "options": { 227 | "tsConfig": [ 228 | "src/tsconfig.app.json", 229 | "src/tsconfig.spec.json" 230 | ], 231 | "exclude": [] 232 | } 233 | } 234 | } 235 | }, 236 | "packages-e2e": { 237 | "root": "", 238 | "projectType": "application", 239 | "cli": {}, 240 | "schematics": {}, 241 | "architect": { 242 | "e2e": { 243 | "builder": "@angular-devkit/build-angular:protractor", 244 | "options": { 245 | "protractorConfig": "./protractor.conf.js", 246 | "devServerTarget": "packages:serve" 247 | } 248 | }, 249 | "lint": { 250 | "builder": "@angular-devkit/build-angular:tslint", 251 | "options": { 252 | "tsConfig": [ 253 | "e2e/tsconfig.e2e.json" 254 | ], 255 | "exclude": [] 256 | } 257 | } 258 | } 259 | } 260 | }, 261 | "cli": { 262 | "packageManager": "yarn" 263 | }, 264 | "schematics": { 265 | "@schematics/angular:component": { 266 | "prefix": "app", 267 | "styleext": "css" 268 | }, 269 | "@schematics/angular:directive": { 270 | "prefix": "app" 271 | } 272 | } 273 | } -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar-body.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { 10 | ChangeDetectionStrategy, 11 | Component, 12 | ElementRef, 13 | EventEmitter, 14 | Input, 15 | Output, 16 | ViewEncapsulation, 17 | NgZone, 18 | OnChanges, 19 | SimpleChanges, 20 | } from '@angular/core'; 21 | import {take} from 'rxjs/operators'; 22 | 23 | /** 24 | * Extra CSS classes that can be associated with a calendar cell. 25 | */ 26 | export type SatCalendarCellCssClasses = string | string[] | Set | {[key: string]: any}; 27 | 28 | /** 29 | * An internal class that represents the data corresponding to a single calendar cell. 30 | * @docs-private 31 | */ 32 | export class SatCalendarCell { 33 | constructor(public value: number, 34 | public displayValue: string, 35 | public ariaLabel: string, 36 | public enabled: boolean, 37 | public cssClasses?: SatCalendarCellCssClasses) {} 38 | } 39 | 40 | 41 | /** 42 | * An internal component used to display calendar data in a table. 43 | * @docs-private 44 | */ 45 | @Component({ 46 | selector: '[sat-calendar-body]', 47 | templateUrl: 'calendar-body.html', 48 | styleUrls: ['calendar-body.css'], 49 | host: { 50 | 'class': 'mat-calendar-body', 51 | 'role': 'grid', 52 | 'aria-readonly': 'true' 53 | }, 54 | exportAs: 'matCalendarBody', 55 | encapsulation: ViewEncapsulation.None, 56 | changeDetection: ChangeDetectionStrategy.OnPush, 57 | }) 58 | export class SatCalendarBody implements OnChanges { 59 | /** The label for the table. (e.g. "Jan 2017"). */ 60 | @Input() label: string; 61 | 62 | /** Enables datepicker MouseOver effect on range mode */ 63 | @Input() rangeHoverEffect = true; 64 | 65 | /** The cells to display in the table. */ 66 | @Input() rows: SatCalendarCell[][]; 67 | 68 | /** The value in the table that corresponds to today. */ 69 | @Input() todayValue: number; 70 | 71 | /** The value in the table that is currently selected. */ 72 | @Input() selectedValue: number; 73 | 74 | /** The value in the table since range of dates started. 75 | * Null means no interval or interval doesn't start in this month 76 | */ 77 | @Input() begin: number|null; 78 | 79 | /** The value in the table representing end of dates range. 80 | * Null means no interval or interval doesn't end in this month 81 | */ 82 | @Input() end: number|null; 83 | 84 | /** Whenever user already selected start of dates interval. */ 85 | @Input() beginSelected: boolean; 86 | 87 | /** Whenever the current month is before the date already selected */ 88 | @Input() isBeforeSelected: boolean; 89 | 90 | /** Whether to mark all dates as semi-selected. */ 91 | @Input() rangeFull: boolean; 92 | 93 | /** Whether to use date range selection behaviour.*/ 94 | @Input() rangeMode = false; 95 | 96 | /** The minimum number of free cells needed to fit the label in the first row. */ 97 | @Input() labelMinRequiredCells: number; 98 | 99 | /** The number of columns in the table. */ 100 | @Input() numCols = 7; 101 | 102 | /** The cell number of the active cell in the table. */ 103 | @Input() activeCell = 0; 104 | 105 | /** 106 | * The aspect ratio (width / height) to use for the cells in the table. This aspect ratio will be 107 | * maintained even as the table resizes. 108 | */ 109 | @Input() cellAspectRatio = 1; 110 | 111 | /** Emits when a new value is selected. */ 112 | @Output() readonly selectedValueChange: EventEmitter = new EventEmitter(); 113 | 114 | /** The number of blank cells to put at the beginning for the first row. */ 115 | _firstRowOffset: number; 116 | 117 | /** Padding for the individual date cells. */ 118 | _cellPadding: string; 119 | 120 | /** Width of an individual cell. */ 121 | _cellWidth: string; 122 | 123 | /** The cell number of the hovered cell */ 124 | _cellOver: number; 125 | 126 | constructor(private _elementRef: ElementRef, private _ngZone: NgZone) { } 127 | 128 | _cellClicked(cell: SatCalendarCell): void { 129 | if (cell.enabled) { 130 | this.selectedValueChange.emit(cell.value); 131 | } 132 | } 133 | 134 | _mouseOverCell(cell: SatCalendarCell): void { 135 | if (this.rangeHoverEffect) { 136 | this._cellOver = cell.value; 137 | } 138 | } 139 | 140 | ngOnChanges(changes: SimpleChanges) { 141 | const columnChanges = changes['numCols']; 142 | const {rows, numCols} = this; 143 | 144 | if (changes['rows'] || columnChanges) { 145 | this._firstRowOffset = rows && rows.length && rows[0].length ? numCols - rows[0].length : 0; 146 | } 147 | 148 | if (changes['cellAspectRatio'] || columnChanges || !this._cellPadding) { 149 | this._cellPadding = `${50 * this.cellAspectRatio / numCols}%`; 150 | } 151 | 152 | if (columnChanges || !this._cellWidth) { 153 | this._cellWidth = `${100 / numCols}%`; 154 | } 155 | 156 | if (changes.activeCell) { 157 | // Only modify hovered cell variable when rangeHoverEffect is enabled 158 | if (this.rangeHoverEffect) { 159 | this._cellOver = this.activeCell + 1; 160 | } 161 | } 162 | } 163 | 164 | _isActiveCell(rowIndex: number, colIndex: number): boolean { 165 | let cellNumber = rowIndex * this.numCols + colIndex; 166 | 167 | // Account for the fact that the first row may not have as many cells. 168 | if (rowIndex) { 169 | cellNumber -= this._firstRowOffset; 170 | } 171 | 172 | return cellNumber == this.activeCell; 173 | } 174 | 175 | /** Whenever to mark cell as semi-selected (inside dates interval). */ 176 | _isSemiSelected(date: number) { 177 | if (!this.rangeMode) { 178 | return false; 179 | } 180 | if (this.rangeFull) { 181 | return true; 182 | } 183 | /** Do not mark start and end of interval. */ 184 | if (date === this.begin || date === this.end) { 185 | return false; 186 | } 187 | if (this.begin && !this.end) { 188 | return date > this.begin; 189 | } 190 | if (this.end && !this.begin) { 191 | return date < this.end; 192 | } 193 | return date > this.begin && date < this.end; 194 | } 195 | 196 | /** Whenever to mark cell as semi-selected before the second date is selected (between the begin cell and the hovered cell). */ 197 | _isBetweenOverAndBegin(date: number): boolean { 198 | if (!this._cellOver || !this.rangeMode || !this.beginSelected) { 199 | return false; 200 | } 201 | if (this.isBeforeSelected && !this.begin) { 202 | return date > this._cellOver; 203 | } 204 | if (this._cellOver > this.begin) { 205 | return date > this.begin && date < this._cellOver; 206 | } 207 | if (this._cellOver < this.begin) { 208 | return date < this.begin && date > this._cellOver; 209 | } 210 | return false; 211 | } 212 | 213 | /** Whenever to mark cell as begin of the range. */ 214 | _isBegin(date: number): boolean { 215 | if (this.rangeMode && this.beginSelected && this._cellOver) { 216 | if (this.isBeforeSelected && !this.begin) { 217 | return this._cellOver === date; 218 | } else { 219 | return (this.begin === date && !(this._cellOver < this.begin)) || 220 | (this._cellOver === date && this._cellOver < this.begin) 221 | } 222 | } 223 | return this.begin === date; 224 | } 225 | 226 | /** Whenever to mark cell as end of the range. */ 227 | _isEnd(date: number): boolean { 228 | if (this.rangeMode && this.beginSelected && this._cellOver) { 229 | if (this.isBeforeSelected && !this.begin) { 230 | return false; 231 | } else { 232 | return (this.end === date && !(this._cellOver > this.begin)) || 233 | (this._cellOver === date && this._cellOver > this.begin) 234 | } 235 | } 236 | return this.end === date; 237 | } 238 | 239 | /** Focuses the active cell after the microtask queue is empty. */ 240 | _focusActiveCell() { 241 | this._ngZone.runOutsideAngular(() => { 242 | this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => { 243 | const activeCell: HTMLElement | null = 244 | this._elementRef.nativeElement.querySelector('.mat-calendar-body-active'); 245 | 246 | if (activeCell) { 247 | activeCell.focus(); 248 | } 249 | }); 250 | }); 251 | } 252 | 253 | /** Whenever to highlight the target cell when selecting the second date in range mode */ 254 | _previewCellOver(date: number): boolean { 255 | return this._cellOver === date && this.rangeMode && this.beginSelected; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datetime/date-adapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {inject, InjectionToken, LOCALE_ID} from '@angular/core'; 10 | import {Observable, Subject} from 'rxjs'; 11 | 12 | /** InjectionToken for datepicker that can be used to override default locale code. */ 13 | export const MAT_DATE_LOCALE = new InjectionToken('MAT_DATE_LOCALE', { 14 | providedIn: 'root', 15 | factory: MAT_DATE_LOCALE_FACTORY, 16 | }); 17 | 18 | /** @docs-private */ 19 | export function MAT_DATE_LOCALE_FACTORY(): string { 20 | return inject(LOCALE_ID); 21 | } 22 | 23 | /** 24 | * No longer needed since MAT_DATE_LOCALE has been changed to a scoped injectable. 25 | * If you are importing and providing this in your code you can simply remove it. 26 | * @deprecated 27 | * @breaking-change 8.0.0 28 | */ 29 | export const MAT_DATE_LOCALE_PROVIDER = {provide: MAT_DATE_LOCALE, useExisting: LOCALE_ID}; 30 | 31 | /** Adapts type `D` to be usable as a date by cdk-based components that work with dates. */ 32 | export abstract class DateAdapter { 33 | /** The locale to use for all dates. */ 34 | protected locale: any; 35 | 36 | /** A stream that emits when the locale changes. */ 37 | get localeChanges(): Observable { return this._localeChanges; } 38 | protected _localeChanges = new Subject(); 39 | 40 | /** 41 | * Gets the year component of the given date. 42 | * @param date The date to extract the year from. 43 | * @returns The year component. 44 | */ 45 | abstract getYear(date: D): number; 46 | 47 | /** 48 | * Gets the month component of the given date. 49 | * @param date The date to extract the month from. 50 | * @returns The month component (0-indexed, 0 = January). 51 | */ 52 | abstract getMonth(date: D): number; 53 | 54 | /** 55 | * Gets the date of the month component of the given date. 56 | * @param date The date to extract the date of the month from. 57 | * @returns The month component (1-indexed, 1 = first of month). 58 | */ 59 | abstract getDate(date: D): number; 60 | 61 | /** 62 | * Gets the day of the week component of the given date. 63 | * @param date The date to extract the day of the week from. 64 | * @returns The month component (0-indexed, 0 = Sunday). 65 | */ 66 | abstract getDayOfWeek(date: D): number; 67 | 68 | /** 69 | * Gets a list of names for the months. 70 | * @param style The naming style (e.g. long = 'January', short = 'Jan', narrow = 'J'). 71 | * @returns An ordered list of all month names, starting with January. 72 | */ 73 | abstract getMonthNames(style: 'long' | 'short' | 'narrow'): string[]; 74 | 75 | /** 76 | * Gets a list of names for the dates of the month. 77 | * @returns An ordered list of all date of the month names, starting with '1'. 78 | */ 79 | abstract getDateNames(): string[]; 80 | 81 | /** 82 | * Gets a list of names for the days of the week. 83 | * @param style The naming style (e.g. long = 'Sunday', short = 'Sun', narrow = 'S'). 84 | * @returns An ordered list of all weekday names, starting with Sunday. 85 | */ 86 | abstract getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[]; 87 | 88 | /** 89 | * Gets the name for the year of the given date. 90 | * @param date The date to get the year name for. 91 | * @returns The name of the given year (e.g. '2017'). 92 | */ 93 | abstract getYearName(date: D): string; 94 | 95 | /** 96 | * Gets the first day of the week. 97 | * @returns The first day of the week (0-indexed, 0 = Sunday). 98 | */ 99 | abstract getFirstDayOfWeek(): number; 100 | 101 | /** 102 | * Gets the number of days in the month of the given date. 103 | * @param date The date whose month should be checked. 104 | * @returns The number of days in the month of the given date. 105 | */ 106 | abstract getNumDaysInMonth(date: D): number; 107 | 108 | /** 109 | * Clones the given date. 110 | * @param date The date to clone 111 | * @returns A new date equal to the given date. 112 | */ 113 | abstract clone(date: D): D; 114 | 115 | /** 116 | * Creates a date with the given year, month, and date. Does not allow over/under-flow of the 117 | * month and date. 118 | * @param year The full year of the date. (e.g. 89 means the year 89, not the year 1989). 119 | * @param month The month of the date (0-indexed, 0 = January). Must be an integer 0 - 11. 120 | * @param date The date of month of the date. Must be an integer 1 - length of the given month. 121 | * @returns The new date, or null if invalid. 122 | */ 123 | abstract createDate(year: number, month: number, date: number): D; 124 | 125 | /** 126 | * Gets today's date. 127 | * @returns Today's date. 128 | */ 129 | abstract today(): D; 130 | 131 | /** 132 | * Parses a date from a user-provided value. 133 | * @param value The value to parse. 134 | * @param parseFormat The expected format of the value being parsed 135 | * (type is implementation-dependent). 136 | * @returns The parsed date. 137 | */ 138 | abstract parse(value: any, parseFormat: any): D | null; 139 | 140 | /** 141 | * Formats a date as a string according to the given format. 142 | * @param date The value to format. 143 | * @param displayFormat The format to use to display the date as a string. 144 | * @returns The formatted date string. 145 | */ 146 | abstract format(date: D, displayFormat: any): string; 147 | 148 | /** 149 | * Adds the given number of years to the date. Years are counted as if flipping 12 pages on the 150 | * calendar for each year and then finding the closest date in the new month. For example when 151 | * adding 1 year to Feb 29, 2016, the resulting date will be Feb 28, 2017. 152 | * @param date The date to add years to. 153 | * @param years The number of years to add (may be negative). 154 | * @returns A new date equal to the given one with the specified number of years added. 155 | */ 156 | abstract addCalendarYears(date: D, years: number): D; 157 | 158 | /** 159 | * Adds the given number of months to the date. Months are counted as if flipping a page on the 160 | * calendar for each month and then finding the closest date in the new month. For example when 161 | * adding 1 month to Jan 31, 2017, the resulting date will be Feb 28, 2017. 162 | * @param date The date to add months to. 163 | * @param months The number of months to add (may be negative). 164 | * @returns A new date equal to the given one with the specified number of months added. 165 | */ 166 | abstract addCalendarMonths(date: D, months: number): D; 167 | 168 | /** 169 | * Adds the given number of days to the date. Days are counted as if moving one cell on the 170 | * calendar for each day. 171 | * @param date The date to add days to. 172 | * @param days The number of days to add (may be negative). 173 | * @returns A new date equal to the given one with the specified number of days added. 174 | */ 175 | abstract addCalendarDays(date: D, days: number): D; 176 | 177 | /** 178 | * Gets the RFC 3339 compatible string (https://tools.ietf.org/html/rfc3339) for the given date. 179 | * This method is used to generate date strings that are compatible with native HTML attributes 180 | * such as the `min` or `max` attribute of an ``. 181 | * @param date The date to get the ISO date string for. 182 | * @returns The ISO date string date string. 183 | */ 184 | abstract toIso8601(date: D): string; 185 | 186 | /** 187 | * Checks whether the given object is considered a date instance by this DateAdapter. 188 | * @param obj The object to check 189 | * @returns Whether the object is a date instance. 190 | */ 191 | abstract isDateInstance(obj: any): boolean; 192 | 193 | /** 194 | * Checks whether the given date is valid. 195 | * @param date The date to check. 196 | * @returns Whether the date is valid. 197 | */ 198 | abstract isValid(date: D): boolean; 199 | 200 | /** 201 | * Gets date instance that is not valid. 202 | * @returns An invalid date. 203 | */ 204 | abstract invalid(): D; 205 | 206 | /** 207 | * Attempts to deserialize a value to a valid date object. This is different from parsing in that 208 | * deserialize should only accept non-ambiguous, locale-independent formats (e.g. a ISO 8601 209 | * string). The default implementation does not allow any deserialization, it simply checks that 210 | * the given value is already a valid date object or null. The `` will call this 211 | * method on all of its `@Input()` properties that accept dates. It is therefore possible to 212 | * support passing values from your backend directly to these properties by overriding this method 213 | * to also deserialize the format used by your backend. 214 | * @param value The value to be deserialized into a date object. 215 | * @returns The deserialized date object, either a valid date, null if the value can be 216 | * deserialized into a null date (e.g. the empty string), or an invalid date. 217 | */ 218 | deserialize(value: any): D | null { 219 | if (value == null || this.isDateInstance(value) && this.isValid(value)) { 220 | return value; 221 | } 222 | return this.invalid(); 223 | } 224 | 225 | /** 226 | * Sets the locale used for all dates. 227 | * @param locale The new locale. 228 | */ 229 | setLocale(locale: any) { 230 | this.locale = locale; 231 | this._localeChanges.next(); 232 | } 233 | 234 | /** 235 | * Compares two dates. 236 | * @param first The first date to compare. 237 | * @param second The second date to compare. 238 | * @returns 0 if the dates are equal, a number less than 0 if the first date is earlier, 239 | * a number greater than 0 if the first date is later. 240 | */ 241 | compareDate(first: D, second: D): number { 242 | return this.getYear(first) - this.getYear(second) || 243 | this.getMonth(first) - this.getMonth(second) || 244 | this.getDate(first) - this.getDate(second); 245 | } 246 | 247 | /** 248 | * Checks if two dates are equal. 249 | * @param first The first date to check. 250 | * @param second The second date to check. 251 | * @returns Whether the two dates are equal. 252 | * Null dates are considered equal to other null dates. 253 | */ 254 | sameDate(first: D | null, second: D | null): boolean { 255 | if (first && second) { 256 | let firstValid = this.isValid(first); 257 | let secondValid = this.isValid(second); 258 | if (firstValid && secondValid) { 259 | return !this.compareDate(first, second); 260 | } 261 | return firstValid == secondValid; 262 | } 263 | return first == second; 264 | } 265 | 266 | /** 267 | * Clamp the given date between min and max dates. 268 | * @param date The date to clamp. 269 | * @param min The minimum value to allow. If null or omitted no min is enforced. 270 | * @param max The maximum value to allow. If null or omitted no max is enforced. 271 | * @returns `min` if `date` is less than `min`, `max` if date is greater than `max`, 272 | * otherwise `date`. 273 | */ 274 | clampDate(date: D, min?: D | null, max?: D | null): D { 275 | if (min && this.compareDate(date, min) < 0) { 276 | return min; 277 | } 278 | if (max && this.compareDate(date, max) > 0) { 279 | return max; 280 | } 281 | return date; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/year-view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { 10 | DOWN_ARROW, 11 | END, 12 | ENTER, 13 | HOME, 14 | LEFT_ARROW, 15 | PAGE_DOWN, 16 | PAGE_UP, 17 | RIGHT_ARROW, 18 | UP_ARROW, 19 | SPACE, 20 | } from '@angular/cdk/keycodes'; 21 | import { 22 | AfterContentInit, 23 | ChangeDetectionStrategy, 24 | ChangeDetectorRef, 25 | Component, 26 | EventEmitter, 27 | Inject, 28 | Input, 29 | Optional, 30 | Output, 31 | ViewChild, 32 | ViewEncapsulation, 33 | } from '@angular/core'; 34 | import {Directionality} from '@angular/cdk/bidi'; 35 | import {SatCalendarBody, SatCalendarCell} from './calendar-body'; 36 | import {createMissingDateImplError} from './datepicker-errors'; 37 | import {DateAdapter} from '../datetime/date-adapter'; 38 | import {MAT_DATE_FORMATS, MatDateFormats} from '../datetime/date-formats'; 39 | 40 | /** 41 | * An internal component used to display a single year in the datepicker. 42 | * @docs-private 43 | */ 44 | @Component({ 45 | selector: 'sat-year-view', 46 | templateUrl: 'year-view.html', 47 | exportAs: 'matYearView', 48 | encapsulation: ViewEncapsulation.None, 49 | changeDetection: ChangeDetectionStrategy.OnPush 50 | }) 51 | export class SatYearView implements AfterContentInit { 52 | /** The date to display in this year view (everything other than the year is ignored). */ 53 | @Input() 54 | get activeDate(): D { return this._activeDate; } 55 | set activeDate(value: D) { 56 | let oldActiveDate = this._activeDate; 57 | const validDate = 58 | this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); 59 | this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate); 60 | if (this._dateAdapter.getYear(oldActiveDate) !== this._dateAdapter.getYear(this._activeDate)) { 61 | this._init(); 62 | } 63 | } 64 | private _activeDate: D; 65 | 66 | /** The currently selected date. */ 67 | @Input() 68 | get selected(): D | null { return this._selected; } 69 | set selected(value: D | null) { 70 | this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 71 | this._selectedMonth = this._getMonthInCurrentYear(this._selected); 72 | } 73 | private _selected: D | null; 74 | 75 | /** The minimum selectable date. */ 76 | @Input() 77 | get minDate(): D | null { return this._minDate; } 78 | set minDate(value: D | null) { 79 | this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 80 | } 81 | private _minDate: D | null; 82 | 83 | /** The maximum selectable date. */ 84 | @Input() 85 | get maxDate(): D | null { return this._maxDate; } 86 | set maxDate(value: D | null) { 87 | this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 88 | } 89 | private _maxDate: D | null; 90 | 91 | /** A function used to filter which dates are selectable. */ 92 | @Input() dateFilter: (date: D) => boolean; 93 | 94 | /** Emits when a new month is selected. */ 95 | @Output() readonly selectedChange: EventEmitter = new EventEmitter(); 96 | 97 | /** Emits the selected month. This doesn't imply a change on the selected date */ 98 | @Output() readonly monthSelected: EventEmitter = new EventEmitter(); 99 | 100 | /** Emits when any date is activated. */ 101 | @Output() readonly activeDateChange: EventEmitter = new EventEmitter(); 102 | 103 | /** The body of calendar table */ 104 | @ViewChild(SatCalendarBody, {static: false}) _matCalendarBody: SatCalendarBody; 105 | 106 | /** Grid of calendar cells representing the months of the year. */ 107 | _months: SatCalendarCell[][]; 108 | 109 | /** The label for this year (e.g. "2017"). */ 110 | _yearLabel: string; 111 | 112 | /** The month in this year that today falls on. Null if today is in a different year. */ 113 | _todayMonth: number | null; 114 | 115 | /** 116 | * The month in this year that the selected Date falls on. 117 | * Null if the selected Date is in a different year. 118 | */ 119 | _selectedMonth: number | null; 120 | 121 | constructor(private _changeDetectorRef: ChangeDetectorRef, 122 | @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, 123 | @Optional() public _dateAdapter: DateAdapter, 124 | @Optional() private _dir?: Directionality) { 125 | if (!this._dateAdapter) { 126 | throw createMissingDateImplError('DateAdapter'); 127 | } 128 | if (!this._dateFormats) { 129 | throw createMissingDateImplError('MAT_DATE_FORMATS'); 130 | } 131 | 132 | this._activeDate = this._dateAdapter.today(); 133 | } 134 | 135 | ngAfterContentInit() { 136 | this._init(); 137 | } 138 | 139 | /** Handles when a new month is selected. */ 140 | _monthSelected(month: number) { 141 | const normalizedDate = 142 | this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1); 143 | 144 | this.monthSelected.emit(normalizedDate); 145 | 146 | const daysInMonth = this._dateAdapter.getNumDaysInMonth(normalizedDate); 147 | 148 | this.selectedChange.emit(this._dateAdapter.createDate( 149 | this._dateAdapter.getYear(this.activeDate), month, 150 | Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth))); 151 | } 152 | 153 | /** Handles keydown events on the calendar body when calendar is in year view. */ 154 | _handleCalendarBodyKeydown(event: KeyboardEvent): void { 155 | // TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent 156 | // disabled ones from being selected. This may not be ideal, we should look into whether 157 | // navigation should skip over disabled dates, and if so, how to implement that efficiently. 158 | 159 | const oldActiveDate = this._activeDate; 160 | const isRtl = this._isRtl(); 161 | 162 | switch (event.keyCode) { 163 | case LEFT_ARROW: 164 | this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, isRtl ? 1 : -1); 165 | break; 166 | case RIGHT_ARROW: 167 | this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, isRtl ? -1 : 1); 168 | break; 169 | case UP_ARROW: 170 | this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, -4); 171 | break; 172 | case DOWN_ARROW: 173 | this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 4); 174 | break; 175 | case HOME: 176 | this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 177 | -this._dateAdapter.getMonth(this._activeDate)); 178 | break; 179 | case END: 180 | this.activeDate = this._dateAdapter.addCalendarMonths(this._activeDate, 181 | 11 - this._dateAdapter.getMonth(this._activeDate)); 182 | break; 183 | case PAGE_UP: 184 | this.activeDate = 185 | this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? -10 : -1); 186 | break; 187 | case PAGE_DOWN: 188 | this.activeDate = 189 | this._dateAdapter.addCalendarYears(this._activeDate, event.altKey ? 10 : 1); 190 | break; 191 | case ENTER: 192 | case SPACE: 193 | this._monthSelected(this._dateAdapter.getMonth(this._activeDate)); 194 | break; 195 | default: 196 | // Don't prevent default or focus active cell on keys that we don't explicitly handle. 197 | return; 198 | } 199 | 200 | if (this._dateAdapter.compareDate(oldActiveDate, this.activeDate)) { 201 | this.activeDateChange.emit(this.activeDate); 202 | } 203 | 204 | this._focusActiveCell(); 205 | // Prevent unexpected default actions such as form submission. 206 | event.preventDefault(); 207 | } 208 | 209 | /** Initializes this year view. */ 210 | _init() { 211 | this._selectedMonth = this._getMonthInCurrentYear(this.selected); 212 | this._todayMonth = this._getMonthInCurrentYear(this._dateAdapter.today()); 213 | this._yearLabel = this._dateAdapter.getYearName(this.activeDate); 214 | 215 | let monthNames = this._dateAdapter.getMonthNames('short'); 216 | // First row of months only contains 5 elements so we can fit the year label on the same row. 217 | this._months = [[0, 1, 2, 3], [4, 5, 6, 7], [8, 9, 10, 11]].map(row => row.map( 218 | month => this._createCellForMonth(month, monthNames[month]))); 219 | this._changeDetectorRef.markForCheck(); 220 | } 221 | 222 | /** Focuses the active cell after the microtask queue is empty. */ 223 | _focusActiveCell() { 224 | this._matCalendarBody._focusActiveCell(); 225 | } 226 | 227 | /** 228 | * Gets the month in this year that the given Date falls on. 229 | * Returns null if the given Date is in another year. 230 | */ 231 | private _getMonthInCurrentYear(date: D | null) { 232 | return date && this._dateAdapter.getYear(date) == this._dateAdapter.getYear(this.activeDate) ? 233 | this._dateAdapter.getMonth(date) : null; 234 | } 235 | 236 | /** Creates an SatCalendarCell for the given month. */ 237 | private _createCellForMonth(month: number, monthName: string) { 238 | let ariaLabel = this._dateAdapter.format( 239 | this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1), 240 | this._dateFormats.display.monthYearA11yLabel); 241 | return new SatCalendarCell( 242 | month, monthName.toLocaleUpperCase(), ariaLabel, this._shouldEnableMonth(month)); 243 | } 244 | 245 | /** Whether the given month is enabled. */ 246 | private _shouldEnableMonth(month: number) { 247 | 248 | const activeYear = this._dateAdapter.getYear(this.activeDate); 249 | 250 | if (month === undefined || month === null || 251 | this._isYearAndMonthAfterMaxDate(activeYear, month) || 252 | this._isYearAndMonthBeforeMinDate(activeYear, month)) { 253 | return false; 254 | } 255 | 256 | if (!this.dateFilter) { 257 | return true; 258 | } 259 | 260 | const firstOfMonth = this._dateAdapter.createDate(activeYear, month, 1); 261 | 262 | // If any date in the month is enabled count the month as enabled. 263 | for (let date = firstOfMonth; this._dateAdapter.getMonth(date) == month; 264 | date = this._dateAdapter.addCalendarDays(date, 1)) { 265 | if (this.dateFilter(date)) { 266 | return true; 267 | } 268 | } 269 | 270 | return false; 271 | } 272 | 273 | /** 274 | * Tests whether the combination month/year is after this.maxDate, considering 275 | * just the month and year of this.maxDate 276 | */ 277 | private _isYearAndMonthAfterMaxDate(year: number, month: number) { 278 | if (this.maxDate) { 279 | const maxYear = this._dateAdapter.getYear(this.maxDate); 280 | const maxMonth = this._dateAdapter.getMonth(this.maxDate); 281 | 282 | return year > maxYear || (year === maxYear && month > maxMonth); 283 | } 284 | 285 | return false; 286 | } 287 | 288 | /** 289 | * Tests whether the combination month/year is before this.minDate, considering 290 | * just the month and year of this.minDate 291 | */ 292 | private _isYearAndMonthBeforeMinDate(year: number, month: number) { 293 | if (this.minDate) { 294 | const minYear = this._dateAdapter.getYear(this.minDate); 295 | const minMonth = this._dateAdapter.getMonth(this.minDate); 296 | 297 | return year < minYear || (year === minYear && month < minMonth); 298 | } 299 | 300 | return false; 301 | } 302 | 303 | /** 304 | * @param obj The object to check. 305 | * @returns The given object if it is both a date instance and valid, otherwise null. 306 | */ 307 | private _getValidDateOrNull(obj: any): D | null { 308 | return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null; 309 | } 310 | 311 | /** Determines whether the user has the RTL layout direction. */ 312 | private _isRtl() { 313 | return this._dir && this._dir.value === 'rtl'; 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/multi-year-view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { 10 | DOWN_ARROW, 11 | END, 12 | ENTER, 13 | HOME, 14 | LEFT_ARROW, 15 | PAGE_DOWN, 16 | PAGE_UP, 17 | RIGHT_ARROW, 18 | UP_ARROW, 19 | SPACE, 20 | } from '@angular/cdk/keycodes'; 21 | import { 22 | AfterContentInit, 23 | ChangeDetectionStrategy, 24 | ChangeDetectorRef, 25 | Component, 26 | EventEmitter, 27 | Input, 28 | Optional, 29 | Output, 30 | ViewChild, 31 | ViewEncapsulation, 32 | } from '@angular/core'; 33 | import {Directionality} from '@angular/cdk/bidi'; 34 | import {SatCalendarBody, SatCalendarCell} from './calendar-body'; 35 | import {createMissingDateImplError} from './datepicker-errors'; 36 | import {DateAdapter} from '../datetime/date-adapter'; 37 | 38 | export const yearsPerPage = 24; 39 | 40 | export const yearsPerRow = 4; 41 | 42 | /** 43 | * An internal component used to display a year selector in the datepicker. 44 | * @docs-private 45 | */ 46 | @Component({ 47 | selector: 'sat-multi-year-view', 48 | templateUrl: 'multi-year-view.html', 49 | exportAs: 'matMultiYearView', 50 | encapsulation: ViewEncapsulation.None, 51 | changeDetection: ChangeDetectionStrategy.OnPush 52 | }) 53 | export class SatMultiYearView implements AfterContentInit { 54 | /** The date to display in this multi-year view (everything other than the year is ignored). */ 55 | @Input() 56 | get activeDate(): D { return this._activeDate; } 57 | set activeDate(value: D) { 58 | let oldActiveDate = this._activeDate; 59 | const validDate = 60 | this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); 61 | this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate); 62 | 63 | if (!isSameMultiYearView( 64 | this._dateAdapter, oldActiveDate, this._activeDate, this.minDate, this.maxDate)) { 65 | this._init(); 66 | } 67 | } 68 | private _activeDate: D; 69 | 70 | /** The currently selected date. */ 71 | @Input() 72 | get selected(): D | null { return this._selected; } 73 | set selected(value: D | null) { 74 | this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 75 | this._selectedYear = this._selected && this._dateAdapter.getYear(this._selected); 76 | } 77 | private _selected: D | null; 78 | 79 | /** The minimum selectable date. */ 80 | @Input() 81 | get minDate(): D | null { return this._minDate; } 82 | set minDate(value: D | null) { 83 | this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 84 | } 85 | private _minDate: D | null; 86 | 87 | /** The maximum selectable date. */ 88 | @Input() 89 | get maxDate(): D | null { return this._maxDate; } 90 | set maxDate(value: D | null) { 91 | this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 92 | } 93 | private _maxDate: D | null; 94 | 95 | /** A function used to filter which dates are selectable. */ 96 | @Input() dateFilter: (date: D) => boolean; 97 | 98 | /** Emits when a new year is selected. */ 99 | @Output() readonly selectedChange: EventEmitter = new EventEmitter(); 100 | 101 | /** Emits the selected year. This doesn't imply a change on the selected date */ 102 | @Output() readonly yearSelected: EventEmitter = new EventEmitter(); 103 | 104 | /** Emits when any date is activated. */ 105 | @Output() readonly activeDateChange: EventEmitter = new EventEmitter(); 106 | 107 | /** The body of calendar table */ 108 | @ViewChild(SatCalendarBody, {static: false}) _matCalendarBody: SatCalendarBody; 109 | 110 | /** Grid of calendar cells representing the currently displayed years. */ 111 | _years: SatCalendarCell[][]; 112 | 113 | /** The year that today falls on. */ 114 | _todayYear: number; 115 | 116 | /** The year of the selected date. Null if the selected date is null. */ 117 | _selectedYear: number | null; 118 | 119 | constructor(private _changeDetectorRef: ChangeDetectorRef, 120 | @Optional() public _dateAdapter: DateAdapter, 121 | @Optional() private _dir?: Directionality) { 122 | if (!this._dateAdapter) { 123 | throw createMissingDateImplError('DateAdapter'); 124 | } 125 | 126 | this._activeDate = this._dateAdapter.today(); 127 | } 128 | 129 | ngAfterContentInit() { 130 | this._init(); 131 | } 132 | 133 | /** Initializes this multi-year view. */ 134 | _init() { 135 | this._todayYear = this._dateAdapter.getYear(this._dateAdapter.today()); 136 | 137 | // We want a range years such that we maximize the number of 138 | // enabled dates visible at once. This prevents issues where the minimum year 139 | // is the last item of a page OR the maximum year is the first item of a page. 140 | 141 | // The offset from the active year to the "slot" for the starting year is the 142 | // *actual* first rendered year in the multi-year view. 143 | const activeYear = this._dateAdapter.getYear(this._activeDate); 144 | const minYearOfPage = activeYear - getActiveOffset( 145 | this._dateAdapter, this.activeDate, this.minDate, this.maxDate); 146 | 147 | this._years = []; 148 | for (let i = 0, row: number[] = []; i < yearsPerPage; i++) { 149 | row.push(minYearOfPage + i); 150 | if (row.length == yearsPerRow) { 151 | this._years.push(row.map(year => this._createCellForYear(year))); 152 | row = []; 153 | } 154 | } 155 | this._changeDetectorRef.markForCheck(); 156 | } 157 | 158 | /** Handles when a new year is selected. */ 159 | _yearSelected(year: number) { 160 | this.yearSelected.emit(this._dateAdapter.createDate(year, 0, 1)); 161 | let month = this._dateAdapter.getMonth(this.activeDate); 162 | let daysInMonth = 163 | this._dateAdapter.getNumDaysInMonth(this._dateAdapter.createDate(year, month, 1)); 164 | this.selectedChange.emit(this._dateAdapter.createDate(year, month, 165 | Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth))); 166 | } 167 | 168 | /** Handles keydown events on the calendar body when calendar is in multi-year view. */ 169 | _handleCalendarBodyKeydown(event: KeyboardEvent): void { 170 | const oldActiveDate = this._activeDate; 171 | const isRtl = this._isRtl(); 172 | 173 | switch (event.keyCode) { 174 | case LEFT_ARROW: 175 | this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, isRtl ? 1 : -1); 176 | break; 177 | case RIGHT_ARROW: 178 | this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, isRtl ? -1 : 1); 179 | break; 180 | case UP_ARROW: 181 | this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, -yearsPerRow); 182 | break; 183 | case DOWN_ARROW: 184 | this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, yearsPerRow); 185 | break; 186 | case HOME: 187 | this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, 188 | -getActiveOffset(this._dateAdapter, this.activeDate, this.minDate, this.maxDate)); 189 | break; 190 | case END: 191 | this.activeDate = this._dateAdapter.addCalendarYears(this._activeDate, 192 | yearsPerPage - getActiveOffset( 193 | this._dateAdapter, this.activeDate, this.minDate, this.maxDate) - 1); 194 | break; 195 | case PAGE_UP: 196 | this.activeDate = 197 | this._dateAdapter.addCalendarYears( 198 | this._activeDate, event.altKey ? -yearsPerPage * 10 : -yearsPerPage); 199 | break; 200 | case PAGE_DOWN: 201 | this.activeDate = 202 | this._dateAdapter.addCalendarYears( 203 | this._activeDate, event.altKey ? yearsPerPage * 10 : yearsPerPage); 204 | break; 205 | case ENTER: 206 | case SPACE: 207 | this._yearSelected(this._dateAdapter.getYear(this._activeDate)); 208 | break; 209 | default: 210 | // Don't prevent default or focus active cell on keys that we don't explicitly handle. 211 | return; 212 | } 213 | if (this._dateAdapter.compareDate(oldActiveDate, this.activeDate)) { 214 | this.activeDateChange.emit(this.activeDate); 215 | } 216 | 217 | this._focusActiveCell(); 218 | // Prevent unexpected default actions such as form submission. 219 | event.preventDefault(); 220 | } 221 | 222 | _getActiveCell(): number { 223 | return getActiveOffset(this._dateAdapter, this.activeDate, this.minDate, this.maxDate); 224 | } 225 | 226 | /** Focuses the active cell after the microtask queue is empty. */ 227 | _focusActiveCell() { 228 | this._matCalendarBody._focusActiveCell(); 229 | } 230 | 231 | /** Creates an SatCalendarCell for the given year. */ 232 | private _createCellForYear(year: number) { 233 | let yearName = this._dateAdapter.getYearName(this._dateAdapter.createDate(year, 0, 1)); 234 | return new SatCalendarCell(year, yearName, yearName, this._shouldEnableYear(year)); 235 | } 236 | 237 | /** Whether the given year is enabled. */ 238 | private _shouldEnableYear(year: number) { 239 | // disable if the year is greater than maxDate lower than minDate 240 | if (year === undefined || year === null || 241 | (this.maxDate && year > this._dateAdapter.getYear(this.maxDate)) || 242 | (this.minDate && year < this._dateAdapter.getYear(this.minDate))) { 243 | return false; 244 | } 245 | 246 | // enable if it reaches here and there's no filter defined 247 | if (!this.dateFilter) { 248 | return true; 249 | } 250 | 251 | const firstOfYear = this._dateAdapter.createDate(year, 0, 1); 252 | 253 | // If any date in the year is enabled count the year as enabled. 254 | for (let date = firstOfYear; this._dateAdapter.getYear(date) == year; 255 | date = this._dateAdapter.addCalendarDays(date, 1)) { 256 | if (this.dateFilter(date)) { 257 | return true; 258 | } 259 | } 260 | 261 | return false; 262 | } 263 | 264 | /** 265 | * @param obj The object to check. 266 | * @returns The given object if it is both a date instance and valid, otherwise null. 267 | */ 268 | private _getValidDateOrNull(obj: any): D | null { 269 | return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null; 270 | } 271 | 272 | /** Determines whether the user has the RTL layout direction. */ 273 | private _isRtl() { 274 | return this._dir && this._dir.value === 'rtl'; 275 | } 276 | } 277 | 278 | export function isSameMultiYearView( 279 | dateAdapter: DateAdapter, date1: D, date2: D, minDate: D | null, maxDate: D | null): boolean { 280 | const year1 = dateAdapter.getYear(date1); 281 | const year2 = dateAdapter.getYear(date2); 282 | const startingYear = getStartingYear(dateAdapter, minDate, maxDate); 283 | return Math.floor((year1 - startingYear) / yearsPerPage) === 284 | Math.floor((year2 - startingYear) / yearsPerPage); 285 | } 286 | 287 | /** 288 | * When the multi-year view is first opened, the active year will be in view. 289 | * So we compute how many years are between the active year and the *slot* where our 290 | * "startingYear" will render when paged into view. 291 | */ 292 | export function getActiveOffset( 293 | dateAdapter: DateAdapter, activeDate: D, minDate: D | null, maxDate: D | null): number { 294 | const activeYear = dateAdapter.getYear(activeDate); 295 | return euclideanModulo((activeYear - getStartingYear(dateAdapter, minDate, maxDate)), 296 | yearsPerPage); 297 | } 298 | 299 | /** 300 | * We pick a "starting" year such that either the maximum year would be at the end 301 | * or the minimum year would be at the beginning of a page. 302 | */ 303 | function getStartingYear( 304 | dateAdapter: DateAdapter, minDate: D | null, maxDate: D | null): number { 305 | let startingYear = 0; 306 | if (maxDate) { 307 | const maxYear = dateAdapter.getYear(maxDate); 308 | startingYear = maxYear - yearsPerPage + 1; 309 | } else if (minDate) { 310 | startingYear = dateAdapter.getYear(minDate); 311 | } 312 | return startingYear; 313 | } 314 | 315 | /** Gets remainder that is non-negative, even if first number is negative */ 316 | function euclideanModulo (a: number, b: number): number { 317 | return (a % b + b) % b; 318 | } 319 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datetime/native-date-adapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {Platform} from '@angular/cdk/platform'; 10 | import {Inject, Injectable, Optional} from '@angular/core'; 11 | import {DateAdapter, MAT_DATE_LOCALE} from './date-adapter'; 12 | 13 | // TODO(mmalerba): Remove when we no longer support safari 9. 14 | /** Whether the browser supports the Intl API. */ 15 | let SUPPORTS_INTL_API: boolean; 16 | 17 | // We need a try/catch around the reference to `Intl`, because accessing it in some cases can 18 | // cause IE to throw. These cases are tied to particular versions of Windows and can happen if 19 | // the consumer is providing a polyfilled `Map`. See: 20 | // https://github.com/Microsoft/ChakraCore/issues/3189 21 | // https://github.com/angular/components/issues/15687 22 | try { 23 | SUPPORTS_INTL_API = typeof Intl != 'undefined'; 24 | } catch { 25 | SUPPORTS_INTL_API = false; 26 | } 27 | 28 | /** The default month names to use if Intl API is not available. */ 29 | const DEFAULT_MONTH_NAMES = { 30 | 'long': [ 31 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 32 | 'October', 'November', 'December' 33 | ], 34 | 'short': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 35 | 'narrow': ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'] 36 | }; 37 | 38 | 39 | /** The default date names to use if Intl API is not available. */ 40 | const DEFAULT_DATE_NAMES = range(31, i => String(i + 1)); 41 | 42 | 43 | /** The default day of the week names to use if Intl API is not available. */ 44 | const DEFAULT_DAY_OF_WEEK_NAMES = { 45 | 'long': ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 46 | 'short': ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 47 | 'narrow': ['S', 'M', 'T', 'W', 'T', 'F', 'S'] 48 | }; 49 | 50 | /** First day of week according locale. 51 | * Taken form moment.js source code https://github.com/moment/moment/tree/develop/src/locale 52 | */ 53 | const FIRST_DAY_OF_WEEK = { 54 | af:1, ar:6, 'ar-ly':6, 'ar-ma':6, 'ar-tn':1, az:1, be:1, bg:1, bm:1, br:1, bs:1, ca:1, cs:1, cv:1, 55 | cy:1, da:1, de:1, 'de-at':1, 'de-ch':1, el:1, 'en-au':1, 'en-gb':1, 'en-ie':1, 'en-nz':1, eo:1, 56 | es:1, 'es-do':1, et:1, eu:1, fa:6, fi:1, fo:1, fr:1, 'fr-ch':1, fy:1, gd:1, gl:1, 'gom-latn':1, 57 | hr:1, hu:1, 'hy-am':1, id:1, is:1, it:1, jv:1, ka:1, kk:1, km:1, ky:1, lb:1, lt:1, lv:1, me:1, 58 | mi:1, mk:1, ms:1, 'ms-my':1, mt:1, my:1, nb:1, nl:1, 'nl-be':1, nn:1, pl:1, pt:1, 'pt-BR': 0, ro:1, ru:1, 59 | sd:1, se:1, sk:1, sl:1, sq:1, sr:1, 'sr-cyrl':1, ss:1, sv:1, sw:1, 'tet':1, tg:1, 'tl-ph':1, 60 | 'tlh':1, tr:1, 'tzl':1, 'tzm':6, 'tzm-latn':6, 'ug-cn':1, uk:1, ur:1, uz:1, 'uz-latn':1, vi:1, 61 | 'x-pseudo':1, yo:1, 'zh-cn':1, 62 | }; 63 | 64 | /** 65 | * Matches strings that have the form of a valid RFC 3339 string 66 | * (https://tools.ietf.org/html/rfc3339). Note that the string may not actually be a valid date 67 | * because the regex will match strings an with out of bounds month, date, etc. 68 | */ 69 | const ISO_8601_REGEX = 70 | /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|(?:(?:\+|-)\d{2}:\d{2}))?)?$/; 71 | 72 | 73 | /** Creates an array and fills it with values. */ 74 | function range(length: number, valueFunction: (index: number) => T): T[] { 75 | const valuesArray = Array(length); 76 | for (let i = 0; i < length; i++) { 77 | valuesArray[i] = valueFunction(i); 78 | } 79 | return valuesArray; 80 | } 81 | 82 | /** Adapts the native JS Date for use with cdk-based components that work with dates. */ 83 | @Injectable() 84 | export class NativeDateAdapter extends DateAdapter { 85 | /** Whether to clamp the date between 1 and 9999 to avoid IE and Edge errors. */ 86 | private readonly _clampDate: boolean; 87 | 88 | /** 89 | * Whether to use `timeZone: 'utc'` with `Intl.DateTimeFormat` when formatting dates. 90 | * Without this `Intl.DateTimeFormat` sometimes chooses the wrong timeZone, which can throw off 91 | * the result. (e.g. in the en-US locale `new Date(1800, 7, 14).toLocaleDateString()` 92 | * will produce `'8/13/1800'`. 93 | * 94 | * TODO(mmalerba): drop this variable. It's not being used in the code right now. We're now 95 | * getting the string representation of a Date object from it's utc representation. We're keeping 96 | * it here for sometime, just for precaution, in case we decide to revert some of these changes 97 | * though. 98 | */ 99 | useUtcForDisplay: boolean = true; 100 | 101 | constructor(@Optional() @Inject(MAT_DATE_LOCALE) matDateLocale: string, platform: Platform) { 102 | super(); 103 | super.setLocale(matDateLocale); 104 | 105 | // IE does its own time zone correction, so we disable this on IE. 106 | this.useUtcForDisplay = !platform.TRIDENT; 107 | this._clampDate = platform.TRIDENT || platform.EDGE; 108 | } 109 | 110 | getYear(date: Date): number { 111 | return date.getFullYear(); 112 | } 113 | 114 | getMonth(date: Date): number { 115 | return date.getMonth(); 116 | } 117 | 118 | getDate(date: Date): number { 119 | return date.getDate(); 120 | } 121 | 122 | getDayOfWeek(date: Date): number { 123 | return date.getDay(); 124 | } 125 | 126 | getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { 127 | if (SUPPORTS_INTL_API) { 128 | const dtf = new Intl.DateTimeFormat(this.locale, {month: style, timeZone: 'utc'}); 129 | return range(12, i => 130 | this._stripDirectionalityCharacters(this._format(dtf, new Date(2017, i, 1)))); 131 | } 132 | return DEFAULT_MONTH_NAMES[style]; 133 | } 134 | 135 | getDateNames(): string[] { 136 | if (SUPPORTS_INTL_API) { 137 | const dtf = new Intl.DateTimeFormat(this.locale, {day: 'numeric', timeZone: 'utc'}); 138 | return range(31, i => this._stripDirectionalityCharacters( 139 | this._format(dtf, new Date(2017, 0, i + 1)))); 140 | } 141 | return DEFAULT_DATE_NAMES; 142 | } 143 | 144 | getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { 145 | if (SUPPORTS_INTL_API) { 146 | const dtf = new Intl.DateTimeFormat(this.locale, {weekday: style, timeZone: 'utc'}); 147 | return range(7, i => this._stripDirectionalityCharacters( 148 | this._format(dtf, new Date(2017, 0, i + 1)))); 149 | } 150 | return DEFAULT_DAY_OF_WEEK_NAMES[style]; 151 | } 152 | 153 | getYearName(date: Date): string { 154 | if (SUPPORTS_INTL_API) { 155 | const dtf = new Intl.DateTimeFormat(this.locale, {year: 'numeric', timeZone: 'utc'}); 156 | return this._stripDirectionalityCharacters(this._format(dtf, date)); 157 | } 158 | return String(this.getYear(date)); 159 | } 160 | 161 | getFirstDayOfWeek(): number { 162 | // We can't tell using native JS Date what the first day of the week is. 163 | // Sometimes people use excess language definition, e.g. ru-RU, 164 | // so we use fallback to two-letter language code 165 | const locale = this.locale.toLowerCase(); 166 | return FIRST_DAY_OF_WEEK[locale] || FIRST_DAY_OF_WEEK[locale.substr(0, 2)] || 0; 167 | } 168 | 169 | getNumDaysInMonth(date: Date): number { 170 | return this.getDate(this._createDateWithOverflow( 171 | this.getYear(date), this.getMonth(date) + 1, 0)); 172 | } 173 | 174 | clone(date: Date): Date { 175 | return new Date(date.getTime()); 176 | } 177 | 178 | createDate(year: number, month: number, date: number): Date { 179 | // Check for invalid month and date (except upper bound on date which we have to check after 180 | // creating the Date). 181 | if (month < 0 || month > 11) { 182 | throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`); 183 | } 184 | 185 | if (date < 1) { 186 | throw Error(`Invalid date "${date}". Date has to be greater than 0.`); 187 | } 188 | 189 | let result = this._createDateWithOverflow(year, month, date); 190 | // Check that the date wasn't above the upper bound for the month, causing the month to overflow 191 | if (result.getMonth() != month) { 192 | throw Error(`Invalid date "${date}" for month with index "${month}".`); 193 | } 194 | 195 | return result; 196 | } 197 | 198 | today(): Date { 199 | return new Date(); 200 | } 201 | 202 | parse(value: any): Date | null { 203 | // We have no way using the native JS Date to set the parse format or locale, so we ignore these 204 | // parameters. 205 | if (typeof value == 'number') { 206 | return new Date(value); 207 | } 208 | return value ? new Date(Date.parse(value)) : null; 209 | } 210 | 211 | format(date: Date, displayFormat: Object): string { 212 | if (!this.isValid(date)) { 213 | throw Error('NativeDateAdapter: Cannot format invalid date.'); 214 | } 215 | 216 | if (SUPPORTS_INTL_API) { 217 | // On IE and Edge the i18n API will throw a hard error that can crash the entire app 218 | // if we attempt to format a date whose year is less than 1 or greater than 9999. 219 | if (this._clampDate && (date.getFullYear() < 1 || date.getFullYear() > 9999)) { 220 | date = this.clone(date); 221 | date.setFullYear(Math.max(1, Math.min(9999, date.getFullYear()))); 222 | } 223 | 224 | displayFormat = {...displayFormat, timeZone: 'utc'}; 225 | 226 | const dtf = new Intl.DateTimeFormat(this.locale, displayFormat); 227 | return this._stripDirectionalityCharacters(this._format(dtf, date)); 228 | } 229 | return this._stripDirectionalityCharacters(date.toDateString()); 230 | } 231 | 232 | addCalendarYears(date: Date, years: number): Date { 233 | return this.addCalendarMonths(date, years * 12); 234 | } 235 | 236 | addCalendarMonths(date: Date, months: number): Date { 237 | let newDate = this._createDateWithOverflow( 238 | this.getYear(date), this.getMonth(date) + months, this.getDate(date)); 239 | 240 | // It's possible to wind up in the wrong month if the original month has more days than the new 241 | // month. In this case we want to go to the last day of the desired month. 242 | // Note: the additional + 12 % 12 ensures we end up with a positive number, since JS % doesn't 243 | // guarantee this. 244 | if (this.getMonth(newDate) != ((this.getMonth(date) + months) % 12 + 12) % 12) { 245 | newDate = this._createDateWithOverflow(this.getYear(newDate), this.getMonth(newDate), 0); 246 | } 247 | 248 | return newDate; 249 | } 250 | 251 | addCalendarDays(date: Date, days: number): Date { 252 | return this._createDateWithOverflow( 253 | this.getYear(date), this.getMonth(date), this.getDate(date) + days); 254 | } 255 | 256 | toIso8601(date: Date): string { 257 | return [ 258 | date.getUTCFullYear(), 259 | this._2digit(date.getUTCMonth() + 1), 260 | this._2digit(date.getUTCDate()) 261 | ].join('-'); 262 | } 263 | 264 | /** 265 | * Returns the given value if given a valid Date or null. Deserializes valid ISO 8601 strings 266 | * (https://www.ietf.org/rfc/rfc3339.txt) into valid Dates and empty string into null. Returns an 267 | * invalid date for all other values. 268 | */ 269 | deserialize(value: any): Date | null { 270 | if (typeof value === 'string') { 271 | if (!value) { 272 | return null; 273 | } 274 | // The `Date` constructor accepts formats other than ISO 8601, so we need to make sure the 275 | // string is the right format first. 276 | if (ISO_8601_REGEX.test(value)) { 277 | let date = new Date(value); 278 | if (this.isValid(date)) { 279 | return date; 280 | } 281 | } 282 | } 283 | return super.deserialize(value); 284 | } 285 | 286 | isDateInstance(obj: any) { 287 | return obj instanceof Date; 288 | } 289 | 290 | isValid(date: Date) { 291 | return !isNaN(date.getTime()); 292 | } 293 | 294 | invalid(): Date { 295 | return new Date(NaN); 296 | } 297 | 298 | /** Creates a date but allows the month and date to overflow. */ 299 | private _createDateWithOverflow(year: number, month: number, date: number) { 300 | const result = new Date(year, month, date); 301 | 302 | // We need to correct for the fact that JS native Date treats years in range [0, 99] as 303 | // abbreviations for 19xx. 304 | if (year >= 0 && year < 100) { 305 | result.setFullYear(this.getYear(result) - 1900); 306 | } 307 | return result; 308 | } 309 | 310 | /** 311 | * Pads a number to make it two digits. 312 | * @param n The number to pad. 313 | * @returns The padded number. 314 | */ 315 | private _2digit(n: number) { 316 | return ('00' + n).slice(-2); 317 | } 318 | 319 | /** 320 | * Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while 321 | * other browsers do not. We remove them to make output consistent and because they interfere with 322 | * date parsing. 323 | * @param str The string to strip direction characters from. 324 | * @returns The stripped string. 325 | */ 326 | private _stripDirectionalityCharacters(str: string) { 327 | return str.replace(/[\u200e\u200f]/g, ''); 328 | } 329 | 330 | /** 331 | * When converting Date object to string, javascript built-in functions may return wrong 332 | * results because it applies its internal DST rules. The DST rules around the world change 333 | * very frequently, and the current valid rule is not always valid in previous years though. 334 | * We work around this problem building a new Date object which has its internal UTC 335 | * representation with the local date and time. 336 | * @param dtf Intl.DateTimeFormat object, containg the desired string format. It must have 337 | * timeZone set to 'utc' to work fine. 338 | * @param date Date from which we want to get the string representation according to dtf 339 | * @returns A Date object with its UTC representation based on the passed in date info 340 | */ 341 | private _format(dtf: Intl.DateTimeFormat, date: Date) { 342 | const d = new Date(Date.UTC( 343 | date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), 344 | date.getMinutes(), date.getSeconds(), date.getMilliseconds())); 345 | return dtf.format(d); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/month-view.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import { 10 | DOWN_ARROW, 11 | END, 12 | ENTER, 13 | HOME, 14 | LEFT_ARROW, 15 | PAGE_DOWN, 16 | PAGE_UP, 17 | RIGHT_ARROW, 18 | UP_ARROW, 19 | SPACE, 20 | } from '@angular/cdk/keycodes'; 21 | import { 22 | AfterContentInit, 23 | ChangeDetectionStrategy, 24 | ChangeDetectorRef, 25 | Component, 26 | EventEmitter, 27 | Inject, 28 | Input, 29 | Optional, 30 | Output, 31 | ViewEncapsulation, 32 | ViewChild, 33 | } from '@angular/core'; 34 | import {DateAdapter} from '../datetime/date-adapter'; 35 | import {MAT_DATE_FORMATS, MatDateFormats} from '../datetime/date-formats'; 36 | import {Directionality} from '@angular/cdk/bidi'; 37 | import {SatCalendarBody, SatCalendarCell, SatCalendarCellCssClasses} from './calendar-body'; 38 | import {createMissingDateImplError} from './datepicker-errors'; 39 | 40 | 41 | const DAYS_PER_WEEK = 7; 42 | 43 | 44 | /** 45 | * An internal component used to display a single month in the datepicker. 46 | * @docs-private 47 | */ 48 | @Component({ 49 | selector: 'sat-month-view', 50 | templateUrl: 'month-view.html', 51 | exportAs: 'matMonthView', 52 | encapsulation: ViewEncapsulation.None, 53 | changeDetection: ChangeDetectionStrategy.OnPush 54 | }) 55 | export class SatMonthView implements AfterContentInit { 56 | 57 | /** Current start of interval. */ 58 | @Input() 59 | get beginDate(): D | null { return this._beginDate; } 60 | set beginDate(value: D | null) { 61 | this._beginDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 62 | this.updateRangeSpecificValues(); 63 | } 64 | private _beginDate: D | null; 65 | 66 | /** Current end of interval. */ 67 | @Input() 68 | get endDate(): D | null { return this._endDate; } 69 | set endDate(value: D | null) { 70 | this._endDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 71 | this.updateRangeSpecificValues(); 72 | } 73 | private _endDate: D | null; 74 | 75 | /** Allow selecting range of dates. */ 76 | @Input() rangeMode = false; 77 | 78 | /** Enables datepicker MouseOver effect on range mode */ 79 | @Input() rangeHoverEffect = true; 80 | 81 | /** Enables datepicker closing after selection */ 82 | @Input() closeAfterSelection = true; 83 | 84 | /** First day of interval. */ 85 | _beginDateNumber: number | null; 86 | 87 | /* Last day of interval. */ 88 | _endDateNumber: number | null; 89 | 90 | /** Whenever full month is inside dates interval. */ 91 | _rangeFull: boolean | null = false; 92 | 93 | /** Whenever user already selected start of dates interval. */ 94 | @Input() set beginDateSelected(value: D | null) { this._beginDateSelected = value } ; 95 | 96 | /** Whenever user already selected start of dates interval. An inner property that avoid asynchronous problems */ 97 | _beginDateSelected: D | null; 98 | 99 | /** 100 | * The date to display in this month view (everything other than the month and year is ignored). 101 | */ 102 | @Input() 103 | get activeDate(): D { return this._activeDate; } 104 | set activeDate(value: D) { 105 | const oldActiveDate = this._activeDate; 106 | const validDate = 107 | this._getValidDateOrNull(this._dateAdapter.deserialize(value)) || this._dateAdapter.today(); 108 | this._activeDate = this._dateAdapter.clampDate(validDate, this.minDate, this.maxDate); 109 | if (!this._hasSameMonthAndYear(oldActiveDate, this._activeDate)) { 110 | this._init(); 111 | } 112 | } 113 | private _activeDate: D; 114 | 115 | /** The currently selected date. */ 116 | @Input() 117 | get selected(): D | null { return this._selected; } 118 | set selected(value: D | null) { 119 | this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 120 | this._selectedDate = this._getDateInCurrentMonth(this._selected); 121 | } 122 | private _selected: D | null; 123 | 124 | /** The minimum selectable date. */ 125 | @Input() 126 | get minDate(): D | null { return this._minDate; } 127 | set minDate(value: D | null) { 128 | this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 129 | } 130 | private _minDate: D | null; 131 | 132 | /** The maximum selectable date. */ 133 | @Input() 134 | get maxDate(): D | null { return this._maxDate; } 135 | set maxDate(value: D | null) { 136 | this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 137 | } 138 | private _maxDate: D | null; 139 | 140 | /** Function used to filter which dates are selectable. */ 141 | @Input() dateFilter: (date: D) => boolean; 142 | 143 | /** Function that can be used to add custom CSS classes to dates. */ 144 | @Input() dateClass: (date: D) => SatCalendarCellCssClasses; 145 | 146 | /** Emits when a new date is selected. */ 147 | @Output() readonly selectedChange: EventEmitter = new EventEmitter(); 148 | 149 | /** Emits when any date is selected. */ 150 | @Output() readonly _userSelection: EventEmitter = new EventEmitter(); 151 | 152 | /** Emits when any date is activated. */ 153 | @Output() readonly activeDateChange: EventEmitter = new EventEmitter(); 154 | 155 | /** The body of calendar table */ 156 | @ViewChild(SatCalendarBody, {static: false}) _matCalendarBody: SatCalendarBody; 157 | 158 | /** The label for this month (e.g. "January 2017"). */ 159 | _monthLabel: string; 160 | 161 | /** Grid of calendar cells representing the dates of the month. */ 162 | _weeks: SatCalendarCell[][]; 163 | 164 | /** The number of blank cells in the first row before the 1st of the month. */ 165 | _firstWeekOffset: number; 166 | 167 | /** 168 | * The date of the month that the currently selected Date falls on. 169 | * Null if the currently selected Date is in another month. 170 | */ 171 | _selectedDate: number | null; 172 | 173 | /** The date of the month that today falls on. Null if today is in another month. */ 174 | _todayDate: number | null; 175 | 176 | /** The names of the weekdays. */ 177 | _weekdays: {long: string, narrow: string}[]; 178 | 179 | constructor(private _changeDetectorRef: ChangeDetectorRef, 180 | @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, 181 | @Optional() public _dateAdapter: DateAdapter, 182 | @Optional() private _dir?: Directionality) { 183 | if (!this._dateAdapter) { 184 | throw createMissingDateImplError('DateAdapter'); 185 | } 186 | if (!this._dateFormats) { 187 | throw createMissingDateImplError('MAT_DATE_FORMATS'); 188 | } 189 | 190 | this._activeDate = this._dateAdapter.today(); 191 | } 192 | 193 | ngAfterContentInit() { 194 | this._init(); 195 | } 196 | 197 | /** Handles when a new date is selected. */ 198 | _dateSelected(date: number) { 199 | 200 | if (this.rangeMode) { 201 | 202 | const selectedYear = this._dateAdapter.getYear(this.activeDate); 203 | const selectedMonth = this._dateAdapter.getMonth(this.activeDate); 204 | const selectedDate = this._dateAdapter.createDate(selectedYear, selectedMonth, date); 205 | if (!this._beginDateSelected) { // At first click emit the same start and end of interval 206 | this._beginDateSelected = selectedDate; 207 | this.selectedChange.emit(selectedDate); 208 | } else { 209 | this._beginDateSelected = null; 210 | this.selectedChange.emit(selectedDate); 211 | this._userSelection.emit(); 212 | } 213 | this._createWeekCells(); 214 | this.activeDate = selectedDate; 215 | this._focusActiveCell(); 216 | } else if (this._selectedDate != date) { 217 | 218 | const selectedYear = this._dateAdapter.getYear(this.activeDate); 219 | const selectedMonth = this._dateAdapter.getMonth(this.activeDate); 220 | const selectedDate = this._dateAdapter.createDate(selectedYear, selectedMonth, date); 221 | 222 | this.selectedChange.emit(selectedDate); 223 | this._userSelection.emit(); 224 | this._createWeekCells(); 225 | } 226 | } 227 | 228 | /** Handles keydown events on the calendar body when calendar is in month view. */ 229 | _handleCalendarBodyKeydown(event: KeyboardEvent): void { 230 | // TODO(mmalerba): We currently allow keyboard navigation to disabled dates, but just prevent 231 | // disabled ones from being selected. This may not be ideal, we should look into whether 232 | // navigation should skip over disabled dates, and if so, how to implement that efficiently. 233 | 234 | const oldActiveDate = this._activeDate; 235 | const isRtl = this._isRtl(); 236 | 237 | switch (event.keyCode) { 238 | case LEFT_ARROW: 239 | this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, isRtl ? 1 : -1); 240 | break; 241 | case RIGHT_ARROW: 242 | this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, isRtl ? -1 : 1); 243 | break; 244 | case UP_ARROW: 245 | this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, -7); 246 | break; 247 | case DOWN_ARROW: 248 | this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 7); 249 | break; 250 | case HOME: 251 | this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 252 | 1 - this._dateAdapter.getDate(this._activeDate)); 253 | break; 254 | case END: 255 | this.activeDate = this._dateAdapter.addCalendarDays(this._activeDate, 256 | (this._dateAdapter.getNumDaysInMonth(this._activeDate) - 257 | this._dateAdapter.getDate(this._activeDate))); 258 | break; 259 | case PAGE_UP: 260 | this.activeDate = event.altKey ? 261 | this._dateAdapter.addCalendarYears(this._activeDate, -1) : 262 | this._dateAdapter.addCalendarMonths(this._activeDate, -1); 263 | break; 264 | case PAGE_DOWN: 265 | this.activeDate = event.altKey ? 266 | this._dateAdapter.addCalendarYears(this._activeDate, 1) : 267 | this._dateAdapter.addCalendarMonths(this._activeDate, 1); 268 | break; 269 | case ENTER: 270 | case SPACE: 271 | if (!this.dateFilter || this.dateFilter(this._activeDate)) { 272 | this._dateSelected(this._dateAdapter.getDate(this._activeDate)); 273 | if (!this._beginDateSelected) { 274 | this._userSelection.emit(); 275 | } 276 | if (this._beginDateSelected || !this.closeAfterSelection) { 277 | this._focusActiveCell(); 278 | } 279 | // Prevent unexpected default actions such as form submission. 280 | event.preventDefault(); 281 | } 282 | return; 283 | default: 284 | // Don't prevent default or focus active cell on keys that we don't explicitly handle. 285 | return; 286 | } 287 | 288 | if (this._dateAdapter.compareDate(oldActiveDate, this.activeDate)) { 289 | this.activeDateChange.emit(this.activeDate); 290 | } 291 | 292 | this._focusActiveCell(); 293 | // Prevent unexpected default actions such as form submission. 294 | event.preventDefault(); 295 | } 296 | 297 | /** Initializes this month view. */ 298 | _init() { 299 | this.updateRangeSpecificValues(); 300 | this._selectedDate = this._getDateInCurrentMonth(this.selected); 301 | this._todayDate = this._getDateInCurrentMonth(this._dateAdapter.today()); 302 | this._monthLabel = 303 | this._dateAdapter.getMonthNames('short')[this._dateAdapter.getMonth(this.activeDate)] 304 | .toLocaleUpperCase(); 305 | 306 | let firstOfMonth = this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), 307 | this._dateAdapter.getMonth(this.activeDate), 1); 308 | this._firstWeekOffset = 309 | (DAYS_PER_WEEK + this._dateAdapter.getDayOfWeek(firstOfMonth) - 310 | this._dateAdapter.getFirstDayOfWeek()) % DAYS_PER_WEEK; 311 | 312 | this._initWeekdays(); 313 | this._createWeekCells(); 314 | this._changeDetectorRef.markForCheck(); 315 | } 316 | 317 | /** Focuses the active cell after the microtask queue is empty. */ 318 | _focusActiveCell() { 319 | this._matCalendarBody._focusActiveCell(); 320 | } 321 | 322 | /** Initializes the weekdays. */ 323 | private _initWeekdays() { 324 | const firstDayOfWeek = this._dateAdapter.getFirstDayOfWeek(); 325 | const narrowWeekdays = this._dateAdapter.getDayOfWeekNames('narrow'); 326 | const longWeekdays = this._dateAdapter.getDayOfWeekNames('long'); 327 | 328 | // Rotate the labels for days of the week based on the configured first day of the week. 329 | let weekdays = longWeekdays.map((long, i) => { 330 | return {long, narrow: narrowWeekdays[i]}; 331 | }); 332 | this._weekdays = weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek)); 333 | } 334 | 335 | /** Creates SatCalendarCells for the dates in this month. */ 336 | private _createWeekCells() { 337 | const daysInMonth = this._dateAdapter.getNumDaysInMonth(this.activeDate); 338 | const dateNames = this._dateAdapter.getDateNames(); 339 | this._weeks = [[]]; 340 | for (let i = 0, cell = this._firstWeekOffset; i < daysInMonth; i++, cell++) { 341 | if (cell == DAYS_PER_WEEK) { 342 | this._weeks.push([]); 343 | cell = 0; 344 | } 345 | const date = this._dateAdapter.createDate( 346 | this._dateAdapter.getYear(this.activeDate), 347 | this._dateAdapter.getMonth(this.activeDate), i + 1); 348 | const enabled = this._shouldEnableDate(date); 349 | const ariaLabel = this._dateAdapter.format(date, this._dateFormats.display.dateA11yLabel); 350 | const cellClasses = this.dateClass ? this.dateClass(date) : undefined; 351 | 352 | this._weeks[this._weeks.length - 1] 353 | .push(new SatCalendarCell(i + 1, dateNames[i], ariaLabel, enabled, cellClasses)); 354 | } 355 | } 356 | 357 | /** Date filter for the month */ 358 | private _shouldEnableDate(date: D): boolean { 359 | return !!date && 360 | (!this.dateFilter || this.dateFilter(date)) && 361 | (!this.minDate || this._dateAdapter.compareDate(date, this.minDate) >= 0) && 362 | (!this.maxDate || this._dateAdapter.compareDate(date, this.maxDate) <= 0); 363 | } 364 | 365 | /** 366 | * Gets the date in this month that the given Date falls on. 367 | * Returns null if the given Date is in another month. 368 | */ 369 | private _getDateInCurrentMonth(date: D | null): number | null { 370 | return date && this._hasSameMonthAndYear(date, this.activeDate) ? 371 | this._dateAdapter.getDate(date) : null; 372 | } 373 | 374 | /** Checks whether the 2 dates are non-null and fall within the same month of the same year. */ 375 | private _hasSameMonthAndYear(d1: D | null, d2: D | null): boolean { 376 | return !!(d1 && d2 && this._dateAdapter.getMonth(d1) == this._dateAdapter.getMonth(d2) && 377 | this._dateAdapter.getYear(d1) == this._dateAdapter.getYear(d2)); 378 | } 379 | 380 | /** 381 | * @param obj The object to check. 382 | * @returns The given object if it is both a date instance and valid, otherwise null. 383 | */ 384 | private _getValidDateOrNull(obj: any): D | null { 385 | return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null; 386 | } 387 | 388 | /** Determines whether the user has the RTL layout direction. */ 389 | private _isRtl() { 390 | return this._dir && this._dir.value === 'rtl'; 391 | } 392 | /** Updates range full parameter on each begin or end of interval update. 393 | * Necessary to display calendar-body correctly 394 | */ 395 | private updateRangeSpecificValues(): void { 396 | if (this.rangeMode) { 397 | this._beginDateNumber = this._getDateInCurrentMonth(this._beginDate); 398 | this._endDateNumber = this._getDateInCurrentMonth(this._endDate); 399 | this._rangeFull = this.beginDate && this.endDate && !this._beginDateNumber && 400 | !this._endDateNumber && 401 | this._dateAdapter.compareDate(this.beginDate, this.activeDate) <= 0 && 402 | this._dateAdapter.compareDate(this.activeDate, this.endDate) <= 0; 403 | } else { 404 | this._beginDateNumber = this._endDateNumber = null; 405 | this._rangeFull = false; 406 | } 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datetime/native-date-adapter.spec.ts: -------------------------------------------------------------------------------- 1 | import {Platform} from '@angular/cdk/platform'; 2 | import {LOCALE_ID} from '@angular/core'; 3 | import {async, inject, TestBed} from '@angular/core/testing'; 4 | import {DEC, FEB, JAN, MAR} from '../testing/month-constants'; 5 | import {DateAdapter, MAT_DATE_LOCALE, NativeDateAdapter, NativeDateModule} from './index'; 6 | 7 | const SUPPORTS_INTL = typeof Intl != 'undefined'; 8 | 9 | 10 | describe('NativeDateAdapter', () => { 11 | const platform = new Platform(); 12 | let adapter: NativeDateAdapter; 13 | let assertValidDate: (d: Date | null, valid: boolean) => void; 14 | 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | imports: [NativeDateModule] 18 | }).compileComponents(); 19 | })); 20 | 21 | beforeEach(inject([DateAdapter], (dateAdapter: NativeDateAdapter) => { 22 | adapter = dateAdapter; 23 | 24 | assertValidDate = (d: Date | null, valid: boolean) => { 25 | expect(adapter.isDateInstance(d)).not.toBeNull(`Expected ${d} to be a date instance`); 26 | expect(adapter.isValid(d!)).toBe(valid, 27 | `Expected ${d} to be ${valid ? 'valid' : 'invalid'},` + 28 | ` but was ${valid ? 'invalid' : 'valid'}`); 29 | }; 30 | })); 31 | 32 | it('should get year', () => { 33 | expect(adapter.getYear(new Date(2017, JAN, 1))).toBe(2017); 34 | }); 35 | 36 | it('should get month', () => { 37 | expect(adapter.getMonth(new Date(2017, JAN, 1))).toBe(0); 38 | }); 39 | 40 | it('should get date', () => { 41 | expect(adapter.getDate(new Date(2017, JAN, 1))).toBe(1); 42 | }); 43 | 44 | it('should get day of week', () => { 45 | expect(adapter.getDayOfWeek(new Date(2017, JAN, 1))).toBe(0); 46 | }); 47 | 48 | it('should get long month names', () => { 49 | expect(adapter.getMonthNames('long')).toEqual([ 50 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 51 | 'October', 'November', 'December' 52 | ]); 53 | }); 54 | 55 | it('should get long month names', () => { 56 | expect(adapter.getMonthNames('short')).toEqual([ 57 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' 58 | ]); 59 | }); 60 | 61 | it('should get narrow month names', () => { 62 | // Edge & IE use same value for short and narrow. 63 | if (platform.EDGE || platform.TRIDENT) { 64 | expect(adapter.getMonthNames('narrow')).toEqual([ 65 | 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' 66 | ]); 67 | } else { 68 | expect(adapter.getMonthNames('narrow')).toEqual([ 69 | 'J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D' 70 | ]); 71 | } 72 | }); 73 | 74 | it('should get month names in a different locale', () => { 75 | adapter.setLocale('ja-JP'); 76 | if (SUPPORTS_INTL) { 77 | expect(adapter.getMonthNames('long')).toEqual([ 78 | '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月' 79 | ]); 80 | } else { 81 | expect(adapter.getMonthNames('long')).toEqual([ 82 | 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 83 | 'October', 'November', 'December' 84 | ]); 85 | } 86 | }); 87 | 88 | it('should get date names', () => { 89 | expect(adapter.getDateNames()).toEqual([ 90 | '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', 91 | '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31' 92 | ]); 93 | }); 94 | 95 | it('should get date names in a different locale', () => { 96 | adapter.setLocale('ja-JP'); 97 | if (SUPPORTS_INTL) { 98 | expect(adapter.getDateNames()).toEqual([ 99 | '1日', '2日', '3日', '4日', '5日', '6日', '7日', '8日', '9日', '10日', '11日', '12日', 100 | '13日', '14日', '15日', '16日', '17日', '18日', '19日', '20日', '21日', '22日', '23日', '24日', 101 | '25日', '26日', '27日', '28日', '29日', '30日', '31日' 102 | ]); 103 | } else { 104 | expect(adapter.getDateNames()).toEqual([ 105 | '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', 106 | '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31' 107 | ]); 108 | } 109 | }); 110 | 111 | it('should get long day of week names', () => { 112 | expect(adapter.getDayOfWeekNames('long')).toEqual([ 113 | 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' 114 | ]); 115 | }); 116 | 117 | it('should get short day of week names', () => { 118 | expect(adapter.getDayOfWeekNames('short')).toEqual([ 119 | 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' 120 | ]); 121 | }); 122 | 123 | it('should get narrow day of week names', () => { 124 | // Edge & IE use two-letter narrow days. 125 | if (platform.EDGE || platform.TRIDENT) { 126 | expect(adapter.getDayOfWeekNames('narrow')).toEqual([ 127 | 'Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa' 128 | ]); 129 | } else { 130 | expect(adapter.getDayOfWeekNames('narrow')).toEqual([ 131 | 'S', 'M', 'T', 'W', 'T', 'F', 'S' 132 | ]); 133 | } 134 | }); 135 | 136 | it('should get day of week names in a different locale', () => { 137 | adapter.setLocale('ja-JP'); 138 | if (SUPPORTS_INTL) { 139 | expect(adapter.getDayOfWeekNames('long')).toEqual([ 140 | '日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日' 141 | ]); 142 | } else { 143 | expect(adapter.getDayOfWeekNames('long')).toEqual([ 144 | 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' 145 | ]); 146 | } 147 | }); 148 | 149 | it('should get year name', () => { 150 | expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017'); 151 | }); 152 | 153 | it('should get year name in a different locale', () => { 154 | adapter.setLocale('ja-JP'); 155 | if (SUPPORTS_INTL) { 156 | expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017年'); 157 | } else { 158 | expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017'); 159 | } 160 | }); 161 | 162 | it('should get first day of week', () => { 163 | expect(adapter.getFirstDayOfWeek()).toBe(0); 164 | }); 165 | 166 | it('should create Date', () => { 167 | expect(adapter.createDate(2017, JAN, 1)).toEqual(new Date(2017, JAN, 1)); 168 | }); 169 | 170 | it('should not create Date with month over/under-flow', () => { 171 | expect(() => adapter.createDate(2017, DEC + 1, 1)).toThrow(); 172 | expect(() => adapter.createDate(2017, JAN - 1, 1)).toThrow(); 173 | }); 174 | 175 | it('should not create Date with date over/under-flow', () => { 176 | expect(() => adapter.createDate(2017, JAN, 32)).toThrow(); 177 | expect(() => adapter.createDate(2017, JAN, 0)).toThrow(); 178 | }); 179 | 180 | it('should create Date with low year number', () => { 181 | expect(adapter.createDate(-1, JAN, 1).getFullYear()).toBe(-1); 182 | expect(adapter.createDate(0, JAN, 1).getFullYear()).toBe(0); 183 | expect(adapter.createDate(50, JAN, 1).getFullYear()).toBe(50); 184 | expect(adapter.createDate(99, JAN, 1).getFullYear()).toBe(99); 185 | expect(adapter.createDate(100, JAN, 1).getFullYear()).toBe(100); 186 | }); 187 | 188 | it("should get today's date", () => { 189 | expect(adapter.sameDate(adapter.today(), new Date())) 190 | .toBe(true, "should be equal to today's date"); 191 | }); 192 | 193 | it('should parse string', () => { 194 | expect(adapter.parse('1/1/2017')).toEqual(new Date(2017, JAN, 1)); 195 | }); 196 | 197 | it('should parse number', () => { 198 | let timestamp = new Date().getTime(); 199 | expect(adapter.parse(timestamp)).toEqual(new Date(timestamp)); 200 | }); 201 | 202 | it ('should parse Date', () => { 203 | let date = new Date(2017, JAN, 1); 204 | expect(adapter.parse(date)).toEqual(date); 205 | expect(adapter.parse(date)).not.toBe(date); 206 | }); 207 | 208 | it('should parse invalid value as invalid', () => { 209 | let d = adapter.parse('hello'); 210 | expect(d).not.toBeNull(); 211 | expect(adapter.isDateInstance(d)) 212 | .toBe(true, 'Expected string to have been fed through Date.parse'); 213 | expect(adapter.isValid(d as Date)) 214 | .toBe(false, 'Expected to parse as "invalid date" object'); 215 | }); 216 | 217 | it('should format', () => { 218 | if (SUPPORTS_INTL) { 219 | expect(adapter.format(new Date(2017, JAN, 1), {})).toEqual('1/1/2017'); 220 | } else { 221 | expect(adapter.format(new Date(2017, JAN, 1), {})).toEqual('Sun Jan 01 2017'); 222 | } 223 | }); 224 | 225 | it('should format with custom format', () => { 226 | if (SUPPORTS_INTL) { 227 | expect(adapter.format(new Date(2017, JAN, 1), { 228 | year: 'numeric', 229 | month: 'long', 230 | day: 'numeric' 231 | })).toEqual('January 1, 2017'); 232 | } else { 233 | expect(adapter.format(new Date(2017, JAN, 1), { 234 | year: 'numeric', 235 | month: 'long', 236 | day: 'numeric' 237 | })).toEqual('Sun Jan 01 2017'); 238 | } 239 | }); 240 | 241 | it('should format with a different locale', () => { 242 | adapter.setLocale('ja-JP'); 243 | if (SUPPORTS_INTL) { 244 | // Edge & IE use a different format in Japanese. 245 | if (platform.EDGE || platform.TRIDENT) { 246 | expect(adapter.format(new Date(2017, JAN, 1), {})).toEqual('2017年1月1日'); 247 | } else { 248 | expect(adapter.format(new Date(2017, JAN, 1), {})).toEqual('2017/1/1'); 249 | } 250 | } else { 251 | expect(adapter.format(new Date(2017, JAN, 1), {})).toEqual('Sun Jan 01 2017'); 252 | } 253 | }); 254 | 255 | it('should throw when attempting to format invalid date', () => { 256 | expect(() => adapter.format(new Date(NaN), {})) 257 | .toThrowError(/NativeDateAdapter: Cannot format invalid date\./); 258 | }); 259 | 260 | it('should add years', () => { 261 | expect(adapter.addCalendarYears(new Date(2017, JAN, 1), 1)).toEqual(new Date(2018, JAN, 1)); 262 | expect(adapter.addCalendarYears(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, JAN, 1)); 263 | }); 264 | 265 | it('should respect leap years when adding years', () => { 266 | expect(adapter.addCalendarYears(new Date(2016, FEB, 29), 1)).toEqual(new Date(2017, FEB, 28)); 267 | expect(adapter.addCalendarYears(new Date(2016, FEB, 29), -1)).toEqual(new Date(2015, FEB, 28)); 268 | }); 269 | 270 | it('should add months', () => { 271 | expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, FEB, 1)); 272 | expect(adapter.addCalendarMonths(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 1)); 273 | }); 274 | 275 | it('should respect month length differences when adding months', () => { 276 | expect(adapter.addCalendarMonths(new Date(2017, JAN, 31), 1)).toEqual(new Date(2017, FEB, 28)); 277 | expect(adapter.addCalendarMonths(new Date(2017, MAR, 31), -1)).toEqual(new Date(2017, FEB, 28)); 278 | }); 279 | 280 | it('should add days', () => { 281 | expect(adapter.addCalendarDays(new Date(2017, JAN, 1), 1)).toEqual(new Date(2017, JAN, 2)); 282 | expect(adapter.addCalendarDays(new Date(2017, JAN, 1), -1)).toEqual(new Date(2016, DEC, 31)); 283 | }); 284 | 285 | it('should clone', () => { 286 | let date = new Date(2017, JAN, 1); 287 | expect(adapter.clone(date)).toEqual(date); 288 | expect(adapter.clone(date)).not.toBe(date); 289 | }); 290 | 291 | it('should compare dates', () => { 292 | expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 2))).toBeLessThan(0); 293 | expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, FEB, 1))).toBeLessThan(0); 294 | expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2018, JAN, 1))).toBeLessThan(0); 295 | expect(adapter.compareDate(new Date(2017, JAN, 1), new Date(2017, JAN, 1))).toBe(0); 296 | expect(adapter.compareDate(new Date(2018, JAN, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0); 297 | expect(adapter.compareDate(new Date(2017, FEB, 1), new Date(2017, JAN, 1))).toBeGreaterThan(0); 298 | expect(adapter.compareDate(new Date(2017, JAN, 2), new Date(2017, JAN, 1))).toBeGreaterThan(0); 299 | }); 300 | 301 | it('should clamp date at lower bound', () => { 302 | expect(adapter.clampDate( 303 | new Date(2017, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1))) 304 | .toEqual(new Date(2018, JAN, 1)); 305 | }); 306 | 307 | it('should clamp date at upper bound', () => { 308 | expect(adapter.clampDate( 309 | new Date(2020, JAN, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1))) 310 | .toEqual(new Date(2019, JAN, 1)); 311 | }); 312 | 313 | it('should clamp date already within bounds', () => { 314 | expect(adapter.clampDate( 315 | new Date(2018, FEB, 1), new Date(2018, JAN, 1), new Date(2019, JAN, 1))) 316 | .toEqual(new Date(2018, FEB, 1)); 317 | }); 318 | 319 | it('should use UTC for formatting by default', () => { 320 | if (SUPPORTS_INTL) { 321 | expect(adapter.format(new Date(1800, 7, 14), {day: 'numeric'})).toBe('14'); 322 | } else { 323 | expect(adapter.format(new Date(1800, 7, 14), {day: 'numeric'})).toBe('Thu Aug 14 1800'); 324 | } 325 | }); 326 | 327 | it('should count today as a valid date instance', () => { 328 | let d = new Date(); 329 | expect(adapter.isValid(d)).toBe(true); 330 | expect(adapter.isDateInstance(d)).toBe(true); 331 | }); 332 | 333 | it('should count an invalid date as an invalid date instance', () => { 334 | let d = new Date(NaN); 335 | expect(adapter.isValid(d)).toBe(false); 336 | expect(adapter.isDateInstance(d)).toBe(true); 337 | }); 338 | 339 | it('should count a string as not a date instance', () => { 340 | let d = '1/1/2017'; 341 | expect(adapter.isDateInstance(d)).toBe(false); 342 | }); 343 | 344 | it('should create dates from valid ISO strings', () => { 345 | assertValidDate(adapter.deserialize('1985-04-12T23:20:50.52Z'), true); 346 | assertValidDate(adapter.deserialize('1996-12-19T16:39:57-08:00'), true); 347 | assertValidDate(adapter.deserialize('1937-01-01T12:00:27.87+00:20'), true); 348 | assertValidDate(adapter.deserialize('2017-01-01'), true); 349 | assertValidDate(adapter.deserialize('2017-01-01T00:00:00'), true); 350 | assertValidDate(adapter.deserialize('1990-13-31T23:59:00Z'), false); 351 | assertValidDate(adapter.deserialize('1/1/2017'), false); 352 | assertValidDate(adapter.deserialize('2017-01-01T'), false); 353 | expect(adapter.deserialize('')).toBeNull(); 354 | expect(adapter.deserialize(null)).toBeNull(); 355 | assertValidDate(adapter.deserialize(new Date()), true); 356 | assertValidDate(adapter.deserialize(new Date(NaN)), false); 357 | }); 358 | 359 | it('should create an invalid date', () => { 360 | assertValidDate(adapter.invalid(), false); 361 | }); 362 | 363 | it('should not throw when attempting to format a date with a year less than 1', () => { 364 | expect(() => adapter.format(new Date(-1, 1, 1), {})).not.toThrow(); 365 | }); 366 | 367 | it('should not throw when attempting to format a date with a year greater than 9999', () => { 368 | expect(() => adapter.format(new Date(10000, 1, 1), {})).not.toThrow(); 369 | }); 370 | }); 371 | 372 | 373 | describe('NativeDateAdapter with MAT_DATE_LOCALE override', () => { 374 | let adapter: NativeDateAdapter; 375 | 376 | beforeEach(async(() => { 377 | TestBed.configureTestingModule({ 378 | imports: [NativeDateModule], 379 | providers: [{provide: MAT_DATE_LOCALE, useValue: 'da-DK'}] 380 | }).compileComponents(); 381 | })); 382 | 383 | beforeEach(inject([DateAdapter], (d: NativeDateAdapter) => { 384 | adapter = d; 385 | })); 386 | 387 | it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => { 388 | const expectedValue = SUPPORTS_INTL ? 389 | ['søndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag'] : 390 | ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 391 | 392 | expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue); 393 | }); 394 | 395 | }); 396 | 397 | describe('NativeDateAdapter with LOCALE_ID override', () => { 398 | let adapter: NativeDateAdapter; 399 | 400 | beforeEach(async(() => { 401 | TestBed.configureTestingModule({ 402 | imports: [NativeDateModule], 403 | providers: [{provide: LOCALE_ID, useValue: 'da-DK'}] 404 | }).compileComponents(); 405 | })); 406 | 407 | beforeEach(inject([DateAdapter], (d: NativeDateAdapter) => { 408 | adapter = d; 409 | })); 410 | 411 | it('should cascade locale id from the LOCALE_ID injection token to MAT_DATE_LOCALE', () => { 412 | const expectedValue = SUPPORTS_INTL ? 413 | ['søndag', 'mandag', 'tirsdag', 'onsdag', 'torsdag', 'fredag', 'lørdag'] : 414 | ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; 415 | 416 | expect(adapter.getDayOfWeekNames('long')).toEqual(expectedValue); 417 | }); 418 | }); 419 | -------------------------------------------------------------------------------- /saturn-datepicker/src/datepicker/calendar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google LLC All Rights Reserved. 4 | * 5 | * Use of this source code is governed by an MIT-style license that can be 6 | * found in the LICENSE file at https://angular.io/license 7 | */ 8 | 9 | import {ComponentPortal, ComponentType, Portal} from '@angular/cdk/portal'; 10 | import { 11 | AfterContentInit, 12 | AfterViewChecked, 13 | ChangeDetectionStrategy, 14 | ChangeDetectorRef, 15 | Component, 16 | EventEmitter, 17 | forwardRef, 18 | Inject, 19 | Input, 20 | OnChanges, 21 | OnDestroy, 22 | Optional, 23 | Output, 24 | SimpleChanges, 25 | ViewChild, 26 | ViewEncapsulation, 27 | } from '@angular/core'; 28 | import {Subject, Subscription} from 'rxjs'; 29 | import {SatCalendarCellCssClasses} from './calendar-body'; 30 | import {createMissingDateImplError} from './datepicker-errors'; 31 | import {SatDatepickerIntl} from './datepicker-intl'; 32 | import {SatMonthView} from './month-view'; 33 | import { 34 | getActiveOffset, 35 | isSameMultiYearView, 36 | SatMultiYearView, 37 | yearsPerPage 38 | } from './multi-year-view'; 39 | import {SatYearView} from './year-view'; 40 | 41 | import {SatDatepickerRangeValue} from './datepicker-input'; 42 | import {DateAdapter} from '../datetime/date-adapter'; 43 | import {MAT_DATE_FORMATS, MatDateFormats} from '../datetime/date-formats'; 44 | 45 | /** 46 | * Possible views for the calendar. 47 | * @docs-private 48 | */ 49 | export type SatCalendarView = 'month' | 'year' | 'multi-year'; 50 | 51 | /** Default header for SatCalendar */ 52 | @Component({ 53 | selector: 'sat-calendar-header', 54 | templateUrl: 'calendar-header.html', 55 | exportAs: 'matCalendarHeader', 56 | encapsulation: ViewEncapsulation.None, 57 | changeDetection: ChangeDetectionStrategy.OnPush, 58 | }) 59 | export class SatCalendarHeader { 60 | constructor(private _intl: SatDatepickerIntl, 61 | @Inject(forwardRef(() => SatCalendar)) public calendar: SatCalendar, 62 | @Optional() private _dateAdapter: DateAdapter, 63 | @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, 64 | changeDetectorRef: ChangeDetectorRef) { 65 | 66 | this.calendar.stateChanges.subscribe(() => changeDetectorRef.markForCheck()); 67 | } 68 | 69 | /** The label for the current calendar view. */ 70 | get periodButtonText(): string { 71 | if (this.calendar.currentView == 'month') { 72 | return this._dateAdapter 73 | .format(this.calendar.activeDate, this._dateFormats.display.monthYearLabel) 74 | .toLocaleUpperCase(); 75 | } 76 | if (this.calendar.currentView == 'year') { 77 | return this._dateAdapter.getYearName(this.calendar.activeDate); 78 | } 79 | 80 | // The offset from the active year to the "slot" for the starting year is the 81 | // *actual* first rendered year in the multi-year view, and the last year is 82 | // just yearsPerPage - 1 away. 83 | const activeYear = this._dateAdapter.getYear(this.calendar.activeDate); 84 | const minYearOfPage = activeYear - getActiveOffset( 85 | this._dateAdapter, this.calendar.activeDate, this.calendar.minDate, this.calendar.maxDate); 86 | const maxYearOfPage = minYearOfPage + yearsPerPage - 1; 87 | return `${this._dateAdapter.getYearName(this._dateAdapter.createDate(minYearOfPage, 0, 1))} 88 | \u2013 ${this._dateAdapter.getYearName(this._dateAdapter.createDate(maxYearOfPage, 0, 1))}`; 89 | } 90 | 91 | get periodButtonLabel(): string { 92 | return this.calendar.currentView == 'month' ? 93 | this._intl.switchToMultiYearViewLabel : this._intl.switchToMonthViewLabel; 94 | } 95 | 96 | /** The label for the previous button. */ 97 | get prevButtonLabel(): string { 98 | return { 99 | 'month': this._intl.prevMonthLabel, 100 | 'year': this._intl.prevYearLabel, 101 | 'multi-year': this._intl.prevMultiYearLabel 102 | }[this.calendar.currentView]; 103 | } 104 | 105 | /** The label for the next button. */ 106 | get nextButtonLabel(): string { 107 | return { 108 | 'month': this._intl.nextMonthLabel, 109 | 'year': this._intl.nextYearLabel, 110 | 'multi-year': this._intl.nextMultiYearLabel 111 | }[this.calendar.currentView]; 112 | } 113 | 114 | /** Handles user clicks on the period label. 115 | * Option`calendar.orderPeriodLabel` sort the label period views. 116 | * - Default [multi-year]: multi-year then back to month 117 | * - Month [month]: month > year > multi-year 118 | */ 119 | currentPeriodClicked(): void { 120 | const mouthFirstOrder: SatCalendarView[] = ['month', 'year', 'multi-year']; 121 | const defaultOrder: SatCalendarView[] = ['month', 'multi-year', 'month']; 122 | const orderPeriod = this.calendar.orderPeriodLabel === 'month' ? mouthFirstOrder : defaultOrder; 123 | switch (this.calendar.currentView) { 124 | case 'month': 125 | this.calendar.currentView = orderPeriod[1]; 126 | break; 127 | case 'year': 128 | this.calendar.currentView = orderPeriod[2] 129 | break; 130 | default: 131 | this.calendar.currentView = orderPeriod[0] 132 | break; 133 | } 134 | } 135 | 136 | /** Handles user clicks on the previous button. */ 137 | previousClicked(): void { 138 | this.calendar.activeDate = this.calendar.currentView == 'month' ? 139 | this._dateAdapter.addCalendarMonths(this.calendar.activeDate, -1) : 140 | this._dateAdapter.addCalendarYears( 141 | this.calendar.activeDate, this.calendar.currentView == 'year' ? -1 : -yearsPerPage 142 | ); 143 | } 144 | 145 | /** Handles user clicks on the next button. */ 146 | nextClicked(): void { 147 | this.calendar.activeDate = this.calendar.currentView == 'month' ? 148 | this._dateAdapter.addCalendarMonths(this.calendar.activeDate, 1) : 149 | this._dateAdapter.addCalendarYears( 150 | this.calendar.activeDate, 151 | this.calendar.currentView == 'year' ? 1 : yearsPerPage 152 | ); 153 | } 154 | 155 | /** Whether the previous period button is enabled. */ 156 | previousEnabled(): boolean { 157 | if (!this.calendar.minDate) { 158 | return true; 159 | } 160 | return !this.calendar.minDate || 161 | !this._isSameView(this.calendar.activeDate, this.calendar.minDate); 162 | } 163 | 164 | /** Whether the next period button is enabled. */ 165 | nextEnabled(): boolean { 166 | return !this.calendar.maxDate || 167 | !this._isSameView(this.calendar.activeDate, this.calendar.maxDate); 168 | } 169 | 170 | /** Whether the two dates represent the same view in the current view mode (month or year). */ 171 | private _isSameView(date1: D, date2: D): boolean { 172 | if (this.calendar.currentView == 'month') { 173 | return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2) && 174 | this._dateAdapter.getMonth(date1) == this._dateAdapter.getMonth(date2); 175 | } 176 | if (this.calendar.currentView == 'year') { 177 | return this._dateAdapter.getYear(date1) == this._dateAdapter.getYear(date2); 178 | } 179 | // Otherwise we are in 'multi-year' view. 180 | return isSameMultiYearView( 181 | this._dateAdapter, date1, date2, this.calendar.minDate, this.calendar.maxDate); 182 | } 183 | } 184 | 185 | /** Default footer for SatCalendar */ 186 | @Component({ 187 | moduleId: module.id, 188 | selector: 'sat-calendar-footer', 189 | templateUrl: 'calendar-footer.html', 190 | exportAs: 'matCalendarFooter', 191 | encapsulation: ViewEncapsulation.None, 192 | changeDetection: ChangeDetectionStrategy.OnPush, 193 | }) 194 | export class SatCalendarFooter { 195 | } 196 | 197 | /** 198 | * A calendar that is used as part of the datepicker. 199 | * @docs-private 200 | */ 201 | @Component({ 202 | selector: 'sat-calendar', 203 | templateUrl: 'calendar.html', 204 | styleUrls: ['calendar.css'], 205 | host: { 206 | 'class': 'mat-calendar', 207 | }, 208 | exportAs: 'matCalendar', 209 | encapsulation: ViewEncapsulation.None, 210 | changeDetection: ChangeDetectionStrategy.OnPush, 211 | }) 212 | export class SatCalendar implements AfterContentInit, AfterViewChecked, OnDestroy, OnChanges { 213 | 214 | /** Beginning of date range. */ 215 | @Input() 216 | get beginDate(): D | null { return this._beginDate; } 217 | set beginDate(value: D | null) { 218 | this._beginDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 219 | } 220 | private _beginDate: D | null; 221 | 222 | /** Date range end. */ 223 | @Input() 224 | get endDate(): D | null { return this._endDate; } 225 | set endDate(value: D | null) { 226 | this._endDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 227 | } 228 | private _endDate: D | null; 229 | 230 | /** Whenever datepicker is for selecting range of dates. */ 231 | @Input() rangeMode = false; 232 | 233 | /** Enables datepicker MouseOver effect on range mode */ 234 | @Input() rangeHoverEffect = true; 235 | 236 | /** Enables datepicker closing after selection */ 237 | @Input() closeAfterSelection = true; 238 | 239 | /** Emits when new pair of dates selected. */ 240 | @Output() dateRangesChange = new EventEmitter>(); 241 | 242 | /** Whenever user already selected start of dates interval. */ 243 | beginDateSelected: D | boolean = false; 244 | 245 | /** Emits when a new start date has been selected in range mode. */ 246 | @Output() beginDateSelectedChange = new EventEmitter(); 247 | 248 | /** An input indicating the type of the header component, if set. */ 249 | @Input() headerComponent: ComponentType; 250 | 251 | /** A portal containing the header component type for this calendar. */ 252 | _calendarHeaderPortal: Portal; 253 | 254 | /** An input indicating the type of the footer component, if set. */ 255 | @Input() footerComponent: ComponentType; 256 | 257 | /** A portal containing the footer component type for this calendar. */ 258 | _calendarFooterPortal: Portal; 259 | 260 | private _intlChanges: Subscription; 261 | 262 | /** 263 | * Used for scheduling that focus should be moved to the active cell on the next tick. 264 | * We need to schedule it, rather than do it immediately, because we have to wait 265 | * for Angular to re-evaluate the view children. 266 | */ 267 | private _moveFocusOnNextTick = false; 268 | 269 | /** A date representing the period (month or year) to start the calendar in. */ 270 | @Input() 271 | get startAt(): D | null { return this._startAt; } 272 | set startAt(value: D | null) { 273 | this._startAt = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 274 | } 275 | private _startAt: D | null; 276 | 277 | /** Whether the calendar should be started in month or year view. */ 278 | @Input() startView: SatCalendarView = 'month'; 279 | 280 | /** The currently selected date. */ 281 | @Input() 282 | get selected(): D | null { return this._selected; } 283 | set selected(value: D | null) { 284 | this._selected = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 285 | } 286 | private _selected: D | null; 287 | 288 | /** The minimum selectable date. */ 289 | @Input() 290 | get minDate(): D | null { return this._minDate; } 291 | set minDate(value: D | null) { 292 | this._minDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 293 | } 294 | private _minDate: D | null; 295 | 296 | /** The maximum selectable date. */ 297 | @Input() 298 | get maxDate(): D | null { return this._maxDate; } 299 | set maxDate(value: D | null) { 300 | this._maxDate = this._getValidDateOrNull(this._dateAdapter.deserialize(value)); 301 | } 302 | private _maxDate: D | null; 303 | 304 | /** Function used to filter which dates are selectable. */ 305 | @Input() dateFilter: (date: D) => boolean; 306 | 307 | /** Function that can be used to add custom CSS classes to dates. */ 308 | @Input() dateClass: (date: D) => SatCalendarCellCssClasses; 309 | 310 | /** Order the views when clicking on period label button */ 311 | @Input() orderPeriodLabel: 'multi-year' | 'month' = 'multi-year'; 312 | 313 | /** Emits when the currently selected date changes. */ 314 | @Output() readonly selectedChange: EventEmitter = new EventEmitter(); 315 | 316 | /** 317 | * Emits the year chosen in multiyear view. 318 | * This doesn't imply a change on the selected date. 319 | */ 320 | @Output() readonly yearSelected: EventEmitter = new EventEmitter(); 321 | 322 | /** 323 | * Emits the month chosen in year view. 324 | * This doesn't imply a change on the selected date. 325 | */ 326 | @Output() readonly monthSelected: EventEmitter = new EventEmitter(); 327 | 328 | /** Emits when any date is selected. */ 329 | @Output() readonly _userSelection: EventEmitter = new EventEmitter(); 330 | 331 | /** Reference to the current month view component. */ 332 | @ViewChild(SatMonthView, {static: false}) monthView: SatMonthView; 333 | 334 | /** Reference to the current year view component. */ 335 | @ViewChild(SatYearView, {static: false}) yearView: SatYearView; 336 | 337 | /** Reference to the current multi-year view component. */ 338 | @ViewChild(SatMultiYearView, {static: false}) multiYearView: SatMultiYearView; 339 | 340 | /** 341 | * The current active date. This determines which time period is shown and which date is 342 | * highlighted when using keyboard navigation. 343 | */ 344 | get activeDate(): D { return this._clampedActiveDate; } 345 | set activeDate(value: D) { 346 | this._clampedActiveDate = this._dateAdapter.clampDate(value, this.minDate, this.maxDate); 347 | this.stateChanges.next(); 348 | this._changeDetectorRef.markForCheck(); 349 | } 350 | private _clampedActiveDate: D; 351 | 352 | /** Whether the calendar is in month view. */ 353 | get currentView(): SatCalendarView { return this._currentView; } 354 | set currentView(value: SatCalendarView) { 355 | this._currentView = value; 356 | this._moveFocusOnNextTick = true; 357 | this._changeDetectorRef.markForCheck(); 358 | } 359 | private _currentView: SatCalendarView; 360 | 361 | /** 362 | * Emits whenever there is a state change that the header may need to respond to. 363 | */ 364 | stateChanges = new Subject(); 365 | 366 | constructor(_intl: SatDatepickerIntl, 367 | @Optional() private _dateAdapter: DateAdapter, 368 | @Optional() @Inject(MAT_DATE_FORMATS) private _dateFormats: MatDateFormats, 369 | private _changeDetectorRef: ChangeDetectorRef) { 370 | 371 | if (!this._dateAdapter) { 372 | throw createMissingDateImplError('DateAdapter'); 373 | } 374 | 375 | if (!this._dateFormats) { 376 | throw createMissingDateImplError('MAT_DATE_FORMATS'); 377 | } 378 | 379 | this._intlChanges = _intl.changes.subscribe(() => { 380 | _changeDetectorRef.markForCheck(); 381 | this.stateChanges.next(); 382 | }); 383 | } 384 | 385 | ngAfterContentInit() { 386 | this._calendarHeaderPortal = new ComponentPortal(this.headerComponent || SatCalendarHeader); 387 | this._calendarFooterPortal = new ComponentPortal(this.footerComponent || SatCalendarFooter); 388 | this.activeDate = this.startAt || this._dateAdapter.today(); 389 | 390 | // Assign to the private property since we don't want to move focus on init. 391 | this._currentView = this.startView; 392 | } 393 | 394 | ngAfterViewChecked() { 395 | if (this._moveFocusOnNextTick) { 396 | this._moveFocusOnNextTick = false; 397 | this.focusActiveCell(); 398 | } 399 | } 400 | 401 | ngOnDestroy() { 402 | this._intlChanges.unsubscribe(); 403 | this.stateChanges.complete(); 404 | } 405 | 406 | ngOnChanges(changes: SimpleChanges) { 407 | const change = 408 | changes['minDate'] || changes['maxDate'] || changes['dateFilter']; 409 | 410 | if (change && !change.firstChange) { 411 | const view = this._getCurrentViewComponent(); 412 | 413 | if (view) { 414 | // We need to `detectChanges` manually here, because the `minDate`, `maxDate` etc. are 415 | // passed down to the view via data bindings which won't be up-to-date when we call `_init`. 416 | this._changeDetectorRef.detectChanges(); 417 | view._init(); 418 | } 419 | } 420 | 421 | this.stateChanges.next(); 422 | } 423 | 424 | focusActiveCell() { 425 | this._getCurrentViewComponent()._focusActiveCell(); 426 | } 427 | 428 | /** Updates today's date after an update of the active date */ 429 | updateTodaysDate() { 430 | let view = this.currentView == 'month' ? this.monthView : 431 | (this.currentView == 'year' ? this.yearView : this.multiYearView); 432 | 433 | view.ngAfterContentInit(); 434 | } 435 | 436 | /** Handles date selection in the month view. */ 437 | _dateSelected(date: D): void { 438 | if (this.rangeMode) { 439 | if (!this.beginDateSelected) { 440 | this.beginDateSelected = date; 441 | this.beginDate = date; 442 | this.endDate = date; 443 | this.beginDateSelectedChange.emit(date); 444 | } else { 445 | this.beginDateSelected = false; 446 | if (this._dateAdapter.compareDate(this.beginDate, date) <= 0) { 447 | this.endDate = date; 448 | } else { 449 | this.endDate = this.beginDate; 450 | this.beginDate = date; 451 | } 452 | this.dateRangesChange.emit({begin: this.beginDate, end: this.endDate}); 453 | } 454 | } else if (!this._dateAdapter.sameDate(date, this.selected)) { 455 | this.selected = date; 456 | this.selectedChange.emit(date); 457 | } 458 | } 459 | 460 | /** Handles year selection in the multiyear view. */ 461 | _yearSelectedInMultiYearView(normalizedYear: D) { 462 | this.yearSelected.emit(normalizedYear); 463 | } 464 | 465 | /** Handles month selection in the year view. */ 466 | _monthSelectedInYearView(normalizedMonth: D) { 467 | this.monthSelected.emit(normalizedMonth); 468 | } 469 | 470 | _userSelected(): void { 471 | this._userSelection.emit(); 472 | } 473 | 474 | /** Handles year/month selection in the multi-year/year views. */ 475 | _goToDateInView(date: D, view: 'month' | 'year' | 'multi-year'): void { 476 | this.activeDate = date; 477 | this.currentView = view; 478 | } 479 | 480 | /** 481 | * @param obj The object to check. 482 | * @returns The given object if it is both a date instance and valid, otherwise null. 483 | */ 484 | private _getValidDateOrNull(obj: any): D | null { 485 | return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null; 486 | } 487 | 488 | /** Returns the component instance that corresponds to the current calendar view. */ 489 | private _getCurrentViewComponent() { 490 | return this.monthView || this.yearView || this.multiYearView; 491 | } 492 | 493 | /** Reset inserted values */ 494 | public _reset() { 495 | if (!this.rangeMode) { 496 | this._selected = null; 497 | return this.selectedChange.emit(null); 498 | } 499 | this._beginDate = null; 500 | this._endDate = null; 501 | this.beginDateSelected = null; 502 | this.dateRangesChange.emit(null); 503 | } 504 | } 505 | --------------------------------------------------------------------------------