├── src ├── assets │ └── .gitkeep ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── tsconfig.spec.json ├── index.html ├── tsconfig.app.json ├── tslint.json ├── main.ts ├── app │ ├── app.component.spec.ts │ ├── app.component.scss │ ├── app.component.ts │ └── app.component.html ├── test.ts ├── karma.conf.js └── polyfills.ts ├── .prettierrc ├── projects └── picker │ ├── src │ ├── lib │ │ ├── date-time │ │ │ ├── calendar.component.scss │ │ │ ├── timer-box.component.scss │ │ │ ├── timer.component.scss │ │ │ ├── calendar-body.component.scss │ │ │ ├── calendar-year-view.component.scss │ │ │ ├── date-time-picker.component.html │ │ │ ├── date-time-picker.component.scss │ │ │ ├── calendar-multi-year-view.component.scss │ │ │ ├── date-time-picker-container.component.scss │ │ │ ├── date-time-inline.component.html │ │ │ ├── date-time-inline.component.scss │ │ │ ├── date-time-picker-animation-event.ts │ │ │ ├── calendar-month-view.component.scss │ │ │ ├── adapter │ │ │ │ ├── date-time-format.class.ts │ │ │ │ ├── native-date-time-format.class.ts │ │ │ │ ├── unix-timestamp-adapter │ │ │ │ │ ├── unix-timestamp-date-time-format.class.ts │ │ │ │ │ ├── unix-timestamp-date-time.module.ts │ │ │ │ │ └── unix-timestamp-date-time-adapter.class.ts │ │ │ │ ├── native-date-time.module.ts │ │ │ │ └── date-time-adapter.class.ts │ │ │ ├── calendar-year-view.component.html │ │ │ ├── numberedFixLen.pipe.ts │ │ │ ├── options-provider.ts │ │ │ ├── calendar-month-view.component.html │ │ │ ├── calendar-body.component.html │ │ │ ├── timer.component.html │ │ │ ├── date-time-picker-trigger.directive.ts │ │ │ ├── date-time.module.ts │ │ │ ├── date-time-picker-intl.service.ts │ │ │ ├── calendar-multi-year-view.component.html │ │ │ ├── timer-box.component.html │ │ │ ├── timer-box.component.ts │ │ │ ├── calendar-body.component.spec.ts │ │ │ ├── date-time-picker-container.component.html │ │ │ ├── calendar-body.component.ts │ │ │ ├── date-time.class.ts │ │ │ ├── calendar.component.html │ │ │ ├── date-time-inline.component.ts │ │ │ ├── timer.component.ts │ │ │ └── calendar-multi-year-view.component.spec.ts │ │ ├── dialog │ │ │ ├── dialog-container.component.scss │ │ │ ├── dialog-container.component.html │ │ │ ├── dialog.module.ts │ │ │ ├── dialog-config.class.ts │ │ │ ├── dialog-ref.class.ts │ │ │ ├── dialog-container.component.ts │ │ │ └── dialog.service.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ ├── array.utils.ts │ │ │ ├── object.utils.ts │ │ │ ├── constants.ts │ │ │ └── date.utils.ts │ ├── test.ts │ ├── public_api.ts │ └── test-helpers.ts │ ├── tsconfig.lib.prod.json │ ├── ng-package.json │ ├── tslint.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ └── karma.conf.js ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── .editorconfig ├── .browserslistrc ├── .travis.yml ├── tsconfig.base.json ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ └── ci_test.yml ├── LICENSE ├── tslint.json ├── package.json ├── angular.json └── .eslintrc.js /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | tabWidth: 4 2 | singleQuote: true 3 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/timer-box.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/timer.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-body.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog-container.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-year-view.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-multi-year-view.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker-container.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielmoncada/date-time-picker/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/picker/src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * index 3 | */ 4 | 5 | export * from './object.utils'; 6 | -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog-container.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-inline.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @use '../projects/picker/src/sass/picker.scss' as *; 2 | 3 | html, 4 | body { 5 | margin: 0; 6 | background-color: #f0f0f0; 7 | } 8 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-inline.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | --container-enter-animation-duration: 0; 3 | --container-leave-animation-duration: 0; 4 | } 5 | -------------------------------------------------------------------------------- /projects/picker/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "angularCompilerOptions": { 4 | "compilationMode": "partial", 5 | "enableIvy": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker-animation-event.ts: -------------------------------------------------------------------------------- 1 | export interface IDateTimePickerAnimationEvent { 2 | phaseName: 'start' | 'done'; 3 | toState: 'enter' | 'leave'; 4 | } 5 | -------------------------------------------------------------------------------- /projects/picker/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/picker", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /projects/picker/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [true, "attribute", "owl", "camelCase"], 5 | "component-selector": [true, "element", "owl", "kebab-case"], 6 | "no-host-metadata-property": false 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/picker/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Default Angular browser support 2 | # See: https://angular.dev/tools/cli/build#configuring-browser-compatibility 3 | # And: https://angular.dev/reference/versions#browser-support 4 | last 2 Chrome versions 5 | last 1 Firefox version 6 | last 2 Edge major versions 7 | last 2 Safari major versions 8 | last 2 iOS major versions 9 | Firefox ESR 10 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | cache: 5 | directories: 6 | - ./node_modules 7 | addons: 8 | chrome: stable 9 | before_script: 10 | - npm install -g @angular/cli@19 11 | - npm install 12 | script: 13 | - npm run test-with-coverage 14 | after_success: 15 | - ./node_modules/.bin/codecov -f coverage-final.json 16 | sudo: required 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DateTimePickerApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to date-time-picker-app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /projects/picker/src/lib/utils/array.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * array.utils 3 | */ 4 | 5 | /** Creates an array and fills it with values. */ 6 | export function range(length: number, valueFunction: (index: number) => T): T[] { 7 | const valuesArray = Array(length); 8 | for (let i = 0; i < length; i++) { 9 | valuesArray[i] = valueFunction(i); 10 | } 11 | return valuesArray; 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | }, 6 | "angularCompilerOptions": { 7 | "fullTemplateTypeCheck": true, 8 | "strictInjectionParameters": true, 9 | "strictTemplates": true, 10 | "enableIvy": true 11 | }, 12 | "files": [ 13 | "main.ts", 14 | "polyfills.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-month-view.component.scss: -------------------------------------------------------------------------------- 1 | .week-number { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | margin: 0; 6 | padding: 0; 7 | list-style: none; 8 | margin-bottom: 14px; 9 | margin-top: 46px; 10 | border-right: 1px solid rgba(0, 0, 0, .12); 11 | width: 8%; 12 | font-weight: bold; 13 | li { 14 | font-size: .8em; 15 | } 16 | } -------------------------------------------------------------------------------- /projects/picker/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | }, 6 | "angularCompilerOptions": { 7 | "skipTemplateCodegen": true, 8 | "strictMetadataEmit": true, 9 | "fullTemplateTypeCheck": true, 10 | "strictInjectionParameters": true, 11 | "enableResourceInlining": true, 12 | "enableIvy": true 13 | }, 14 | "exclude": [ 15 | "src/test.ts", 16 | "**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { OWL_DATE_TIME_LOCALE, OptionsTokens } from '../projects/picker/src/public_api'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, { 6 | providers: [ 7 | { 8 | provide: OWL_DATE_TIME_LOCALE, 9 | useValue: 'en-US' 10 | }, 11 | { 12 | provide: OptionsTokens.multiYear, 13 | useFactory: () => ({ yearRows: 5, yearsPerRow: 3, }), 14 | }, 15 | ] 16 | }) 17 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(() => { 6 | TestBed 7 | .configureTestingModule({ imports: [AppComponent] }) 8 | .compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | import 'zone.js'; 3 | import 'zone.js/testing'; 4 | 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | // First, initialize the Angular testing environment. 12 | getTestBed().initTestEnvironment( 13 | BrowserDynamicTestingModule, 14 | platformBrowserDynamicTesting() 15 | ); 16 | -------------------------------------------------------------------------------- /projects/picker/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es/reflect'; 4 | import 'zone.js'; 5 | import 'zone.js/testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/date-time-format.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time-format.class 3 | */ 4 | 5 | import { InjectionToken } from '@angular/core'; 6 | 7 | export interface OwlDateTimeFormats { 8 | parseInput: any; 9 | fullPickerInput: any; 10 | datePickerInput: any; 11 | timePickerInput: any; 12 | monthYearLabel: any; 13 | dateA11yLabel: any; 14 | monthYearA11yLabel: any; 15 | } 16 | 17 | /** InjectionToken for date time picker that can be used to override default format. */ 18 | export const OWL_DATE_TIME_FORMATS = new InjectionToken('OWL_DATE_TIME_FORMATS'); 19 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .button-row { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | gap: 5px; 6 | padding: 0.5rem; 7 | background-color: white; 8 | 9 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.5); 10 | 11 | > button { 12 | border: none; 13 | padding: 0.5rem 1rem; 14 | cursor: pointer; 15 | 16 | background: white; 17 | border: 1px solid #aaa; 18 | border-radius: 4px; 19 | 20 | &:hover { 21 | background: #f0f0f0; 22 | } 23 | &.active { 24 | background: #155799; 25 | color: white; 26 | } 27 | } 28 | } 29 | 30 | main { 31 | padding: 1rem; 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "declaration": false, 9 | "experimentalDecorators": true, 10 | "moduleResolution": "bundler", 11 | "importHelpers": true, 12 | "target": "ES2022", 13 | "module": "ES2022", 14 | "useDefineForClassFields": false, 15 | "lib": [ 16 | "ES2022", 17 | "dom" 18 | ], 19 | "paths": { 20 | "picker": [ 21 | "dist/picker" 22 | ], 23 | "picker/*": [ 24 | "dist/picker/*" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/native-date-time-format.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * native-date-time-format.class 3 | */ 4 | import { OwlDateTimeFormats } from './date-time-format.class'; 5 | 6 | export const OWL_NATIVE_DATE_TIME_FORMATS: OwlDateTimeFormats = { 7 | parseInput: null, 8 | fullPickerInput: {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'}, 9 | datePickerInput: {year: 'numeric', month: 'numeric', day: 'numeric'}, 10 | timePickerInput: {hour: 'numeric', minute: 'numeric'}, 11 | monthYearLabel: {year: 'numeric', month: 'short'}, 12 | dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'}, 13 | monthYearA11yLabel: {year: 'numeric', month: 'long'}, 14 | }; 15 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-year-view.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 15 | 16 |
17 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /.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 | /projects/picker/node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | /.vscode 42 | 43 | .angular 44 | .cache 45 | coverage -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/numberedFixLen.pipe.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * numberFixedLen.pipe 3 | */ 4 | 5 | import { Pipe, PipeTransform } from '@angular/core'; 6 | 7 | @Pipe({ 8 | name: 'numberFixedLen', 9 | standalone: false, 10 | }) 11 | export class NumberFixedLenPipe implements PipeTransform { 12 | transform( num: number, len: number ): any { 13 | const number = Math.floor(num); 14 | const length = Math.floor(len); 15 | 16 | if (num === null || isNaN(number) || isNaN(length)) { 17 | return num; 18 | } 19 | 20 | let numString = number.toString(); 21 | 22 | while (numString.length < length) { 23 | numString = '0' + numString; 24 | } 25 | 26 | return numString; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/unix-timestamp-adapter/unix-timestamp-date-time-format.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * unix-timestamp-date-time-format.class 3 | */ 4 | import {OwlDateTimeFormats} from '../date-time-format.class'; 5 | 6 | export const OWL_UNIX_TIMESTAMP_DATE_TIME_FORMATS: OwlDateTimeFormats = { 7 | parseInput: null, 8 | fullPickerInput: {year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric'}, 9 | datePickerInput: {year: 'numeric', month: 'numeric', day: 'numeric'}, 10 | timePickerInput: {hour: 'numeric', minute: 'numeric'}, 11 | monthYearLabel: {year: 'numeric', month: 'short'}, 12 | dateA11yLabel: {year: 'numeric', month: 'long', day: 'numeric'}, 13 | monthYearA11yLabel: {year: 'numeric', month: 'long'}, 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScript’s language server to improve development experience. 3 | It is not intended to be used to perform a compilation. 4 | 5 | To learn more about this file see: https://angular.io/config/solution-tsconfig. 6 | */ 7 | { 8 | "compilerOptions": { 9 | "esModuleInterop": true 10 | }, 11 | "files": [], 12 | "references": [ 13 | { 14 | "path": "./src/tsconfig.app.json" 15 | }, 16 | { 17 | "path": "./src/tsconfig.spec.json" 18 | }, 19 | { 20 | "path": "./e2e/tsconfig.e2e.json" 21 | }, 22 | { 23 | "path": "./projects/picker/tsconfig.lib.json" 24 | }, 25 | { 26 | "path": "./projects/picker/tsconfig.spec.json" 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * dialog.module 3 | */ 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | import { A11yModule } from '@angular/cdk/a11y'; 8 | import { OverlayModule } from '@angular/cdk/overlay'; 9 | import { PortalModule } from '@angular/cdk/portal'; 10 | import { OWL_DIALOG_SCROLL_STRATEGY_PROVIDER, OwlDialogService } from './dialog.service'; 11 | import { OwlDialogContainerComponent } from './dialog-container.component'; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, A11yModule, OverlayModule, PortalModule], 15 | exports: [], 16 | declarations: [ 17 | OwlDialogContainerComponent, 18 | ], 19 | providers: [ 20 | OWL_DIALOG_SCROLL_STRATEGY_PROVIDER, 21 | OwlDialogService, 22 | ] 23 | }) 24 | export class OwlDialogModule { 25 | } 26 | -------------------------------------------------------------------------------- /e2e/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 | './src/**/*.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: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /.github/workflows/ci_test.yml: -------------------------------------------------------------------------------- 1 | name: CI Test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - '*.md' 8 | - '.github/**' 9 | pull_request: 10 | branches: [ master ] 11 | paths-ignore: 12 | - '*.md' 13 | - '.github/**' 14 | 15 | workflow_dispatch: 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Upgrade Chrome browser 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get --only-upgrade install google-chrome-stable 26 | - name: Use Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: lts/* 30 | - name: Install dependencies 31 | run: npm ci --include=optional 32 | - name: Run tests 33 | run: npm run test:ci 34 | env: 35 | CI: true 36 | -------------------------------------------------------------------------------- /projects/picker/src/lib/utils/object.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * object.utils 3 | */ 4 | 5 | 6 | /** 7 | * Extends an object with the *enumerable* and *own* properties of one or more source objects, 8 | * similar to Object.assign. 9 | * 10 | * @param dest The object which will have properties copied to it. 11 | * @param sources The source objects from which properties will be copied. 12 | */ 13 | export function extendObject(dest: any, ...sources: any[]): any { 14 | if (dest == null) { 15 | throw TypeError('Cannot convert undefined or null to object'); 16 | } 17 | 18 | for (const source of sources) { 19 | if (source != null) { 20 | for (const key in source) { 21 | if (source.hasOwnProperty(key)) { 22 | dest[key] = source[key]; 23 | } 24 | } 25 | } 26 | } 27 | 28 | return dest; 29 | } 30 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/native-date-time.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * native-date-time.module 3 | */ 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { PlatformModule } from '@angular/cdk/platform'; 7 | import { DateTimeAdapter } from './date-time-adapter.class'; 8 | import { NativeDateTimeAdapter } from './native-date-time-adapter.class'; 9 | import { OWL_DATE_TIME_FORMATS } from './date-time-format.class'; 10 | import { OWL_NATIVE_DATE_TIME_FORMATS } from './native-date-time-format.class'; 11 | 12 | @NgModule({ 13 | imports: [PlatformModule], 14 | providers: [ 15 | {provide: DateTimeAdapter, useClass: NativeDateTimeAdapter}, 16 | ], 17 | }) 18 | export class NativeDateTimeModule { 19 | } 20 | 21 | @NgModule({ 22 | imports: [NativeDateTimeModule], 23 | providers: [{provide: OWL_DATE_TIME_FORMATS, useValue: OWL_NATIVE_DATE_TIME_FORMATS}], 24 | }) 25 | export class OwlNativeDateTimeModule { 26 | } 27 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/unix-timestamp-adapter/unix-timestamp-date-time.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * unix-timestamp-date-time.module 3 | */ 4 | 5 | import {NgModule} from '@angular/core'; 6 | import {PlatformModule} from '@angular/cdk/platform'; 7 | import {DateTimeAdapter} from '../date-time-adapter.class'; 8 | import {OWL_DATE_TIME_FORMATS} from '../date-time-format.class'; 9 | import {UnixTimestampDateTimeAdapter} from './unix-timestamp-date-time-adapter.class'; 10 | import {OWL_UNIX_TIMESTAMP_DATE_TIME_FORMATS} from './unix-timestamp-date-time-format.class'; 11 | 12 | @NgModule({ 13 | imports: [PlatformModule], 14 | providers: [ 15 | {provide: DateTimeAdapter, useClass: UnixTimestampDateTimeAdapter}, 16 | ], 17 | }) 18 | export class UnixTimestampDateTimeModule { 19 | } 20 | 21 | @NgModule({ 22 | imports: [UnixTimestampDateTimeModule], 23 | providers: [{provide: OWL_DATE_TIME_FORMATS, useValue: OWL_UNIX_TIMESTAMP_DATE_TIME_FORMATS}], 24 | }) 25 | export class OwlUnixTimestampDateTimeModule { 26 | } 27 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'coverage'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016 Daniel YK Pan. 4 | Copyright (c) 2020 Daniel Moncada. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { OwlDateTimeModule, OwlNativeDateTimeModule } from '../../projects/picker/src/public_api'; 4 | 5 | /** One day in milliseconds */ 6 | const ONE_DAY = 24 * 60 * 60 * 1000; 7 | 8 | @Component({ 9 | standalone: true, 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | styleUrl: './app.component.scss', 13 | changeDetection: ChangeDetectionStrategy.OnPush, 14 | imports: [ 15 | FormsModule, 16 | OwlDateTimeModule, 17 | OwlNativeDateTimeModule 18 | ] 19 | }) 20 | export class AppComponent { 21 | protected readonly currentTab = signal<'date-range' | 'date-range-dialog' | 'date-time-inline'>('date-range'); 22 | 23 | protected selectedDates: [Date, Date] = [ 24 | new Date(Date.now() - ONE_DAY), 25 | new Date(Date.now() + ONE_DAY) 26 | ]; 27 | 28 | protected currentValue: Date = new Date(this.selectedDates[0]); 29 | 30 | protected endValue: Date = new Date(this.selectedDates[1]); 31 | 32 | protected selectedTrigger(date: Date): void { 33 | console.log(date); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /projects/picker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@danielmoncada/angular-datetime-picker", 3 | "version": "20.0.1", 4 | "description": "Angular Date Time Picker", 5 | "keywords": [ 6 | "Angular", 7 | "datepicker", 8 | "date picker", 9 | "timepicker", 10 | "time picker", 11 | "datetime picker", 12 | "date time picker", 13 | "material", 14 | "ngx" 15 | ], 16 | "author": "Maintained and updated by Daniel Moncada, original implementatiom by Daniel Pan", 17 | "license": "MIT", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/danielmoncada/date-time-picker.git" 21 | }, 22 | "homepage": "https://github.com/danielmoncada/date-time-picker", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "peerDependencies": { 27 | "@angular/common": "^19.0.0 || ^20.0.0", 28 | "@angular/core": "^19.0.0 || ^20.0.0", 29 | "@angular/cdk": "^19.0.0 || ^20.0.0" 30 | }, 31 | "dependencies": { 32 | "tslib": "^2.6.2" 33 | }, 34 | "exports": { 35 | "./assets/style/picker.min.css": { 36 | "default": "./assets/style/picker.min.css" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/options-provider.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken, Provider } from '@angular/core'; 2 | 3 | export function defaultOptionsFactory() { 4 | return DefaultOptions.create(); 5 | 6 | } 7 | export function multiYearOptionsFactory(options: Options) { 8 | return options.multiYear; 9 | } 10 | 11 | export interface Options { 12 | multiYear: { 13 | yearsPerRow: number, 14 | yearRows: number 15 | }; 16 | } 17 | export class DefaultOptions { 18 | public static create(): Options { 19 | // Always return new instance 20 | return { 21 | multiYear: { 22 | yearRows: 7, 23 | yearsPerRow: 3 24 | } 25 | }; 26 | } 27 | } 28 | 29 | export abstract class OptionsTokens { 30 | public static all = new InjectionToken('All options token'); 31 | public static multiYear = new InjectionToken('Grid view options token'); 32 | } 33 | 34 | export const optionsProviders: Provider[] = [ 35 | { 36 | provide: OptionsTokens.all, 37 | useFactory: defaultOptionsFactory, 38 | }, 39 | { 40 | provide: OptionsTokens.multiYear, 41 | useFactory: multiYearOptionsFactory, 42 | deps: [OptionsTokens.all], 43 | }, 44 | ]; 45 | -------------------------------------------------------------------------------- /projects/picker/src/lib/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * constants 3 | */ 4 | 5 | import {range} from './array.utils'; 6 | 7 | /** Whether the browser supports the Intl API. */ 8 | export const SUPPORTS_INTL_API = typeof Intl !== 'undefined'; 9 | 10 | /** The default month names to use if Intl API is not available. */ 11 | export const DEFAULT_MONTH_NAMES = { 12 | long: [ 13 | 'January', 14 | 'February', 15 | 'March', 16 | 'April', 17 | 'May', 18 | 'June', 19 | 'July', 20 | 'August', 21 | 'September', 22 | 'October', 23 | 'November', 24 | 'December' 25 | ], 26 | short: [ 27 | 'Jan', 28 | 'Feb', 29 | 'Mar', 30 | 'Apr', 31 | 'May', 32 | 'Jun', 33 | 'Jul', 34 | 'Aug', 35 | 'Sep', 36 | 'Oct', 37 | 'Nov', 38 | 'Dec' 39 | ], 40 | narrow: ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'] 41 | }; 42 | 43 | /** The default day of the week names to use if Intl API is not available. */ 44 | export const DEFAULT_DAY_OF_WEEK_NAMES = { 45 | long: [ 46 | 'Sunday', 47 | 'Monday', 48 | 'Tuesday', 49 | 'Wednesday', 50 | 'Thursday', 51 | 'Friday', 52 | 'Saturday' 53 | ], 54 | short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 55 | narrow: ['S', 'M', 'T', 'W', 'T', 'F', 'S'] 56 | }; 57 | 58 | /** The default date names to use if Intl API is not available. */ 59 | export const DEFAULT_DATE_NAMES = range(31, i => String(i + 1)); 60 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-month-view.component.html: -------------------------------------------------------------------------------- 1 | @if (showCalendarWeeks) { 2 |
    3 | @for (week of weekNumbers; track week) { 4 |
  • 5 | {{ week }} 6 |
  • 7 | } 8 |
9 | } 10 | 15 | 16 | 17 | @for (weekday of weekdays; track weekday.short) { 18 | 25 | } 26 | 27 | 28 | 34 | 35 | 36 | 47 |
23 | {{ weekday.short }} 24 |
48 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-body.component.html: -------------------------------------------------------------------------------- 1 | @for (row of rows; track $index; let rowIndex = $index) { 2 | 3 | @for (item of row; track $index; let colIndex = $index) { 4 | 25 | 33 | {{ item.displayValue }} 34 | 35 | 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /projects/picker/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * picker 3 | */ 4 | 5 | export { OwlDateTimeModule } from './lib/date-time/date-time.module'; 6 | 7 | export { OwlDateTimeIntl } from './lib/date-time/date-time-picker-intl.service'; 8 | 9 | export { OwlNativeDateTimeModule } from './lib/date-time/adapter/native-date-time.module'; 10 | 11 | export { 12 | OWL_DATE_TIME_LOCALE_PROVIDER, 13 | OWL_DATE_TIME_LOCALE, 14 | DateTimeAdapter, 15 | 16 | } from './lib/date-time/adapter/date-time-adapter.class'; 17 | 18 | export { OWL_DATE_TIME_FORMATS, OwlDateTimeFormats } from './lib/date-time/adapter/date-time-format.class'; 19 | 20 | export { 21 | UnixTimestampDateTimeAdapter 22 | } from './lib/date-time/adapter/unix-timestamp-adapter/unix-timestamp-date-time-adapter.class'; 23 | 24 | export { OWL_UNIX_TIMESTAMP_DATE_TIME_FORMATS } from './lib/date-time/adapter/unix-timestamp-adapter/unix-timestamp-date-time-format.class'; 25 | 26 | export { OwlDateTimeInlineComponent } from './lib/date-time/date-time-inline.component'; 27 | 28 | export { OwlDateTimeComponent } from './lib/date-time/date-time-picker.component'; 29 | 30 | export * from './lib/date-time/calendar-body.component'; 31 | 32 | export * from './lib/date-time/timer.component'; 33 | 34 | export * from './lib/date-time/date-time-picker-trigger.directive'; 35 | 36 | export * from './lib/date-time/date-time-picker-input.directive'; 37 | 38 | export * from './lib/date-time/calendar-multi-year-view.component'; 39 | 40 | export * from './lib/date-time/calendar-year-view.component'; 41 | 42 | export * from './lib/date-time/calendar-month-view.component'; 43 | 44 | export * from './lib/date-time/calendar.component'; 45 | 46 | export * from './lib/date-time/timer.component'; 47 | 48 | export { NativeDateTimeAdapter } from './lib/date-time/adapter/native-date-time-adapter.class'; 49 | 50 | export * from './lib/date-time/options-provider'; 51 | 52 | export { PickerType, PickerMode, SelectMode, DateView, DateViewType } from './lib/date-time/date-time.class' 53 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/timer.component.html: -------------------------------------------------------------------------------- 1 | 15 | 29 | @if (showSecondsTimer) { 30 | 44 | } 45 | @if (hour12Timer) { 46 |
47 | 57 |
58 | } 59 | -------------------------------------------------------------------------------- /projects/picker/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly', 'json'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | customLaunchers: { 24 | ChromeHeadlessNoSandbox: { 25 | base: 'ChromeHeadless', 26 | flags: [ 27 | '--no-sandbox', 28 | '--disable-web-security', 29 | '--disable-gpu', 30 | '--disable-dev-shm-usage', 31 | '--disable-extensions', 32 | '--remote-debugging-port=9222', 33 | '--headless' 34 | ] 35 | }, 36 | ChromeHeadlessCI: { 37 | base: 'ChromeHeadless', 38 | flags: [ 39 | '--no-sandbox', 40 | '--disable-web-security', 41 | '--disable-gpu', 42 | '--disable-dev-shm-usage', 43 | '--disable-extensions', 44 | '--disable-background-timer-throttling', 45 | '--disable-backgrounding-occluded-windows', 46 | '--disable-renderer-backgrounding', 47 | '--remote-debugging-port=9222', 48 | '--headless' 49 | ] 50 | } 51 | }, 52 | reporters: ['progress', 'kjhtml'], 53 | port: 9876, 54 | colors: true, 55 | logLevel: config.LOG_INFO, 56 | autoWatch: true, 57 | // Use different browsers based on environment 58 | browsers: process.env.CI ? ['ChromeHeadlessCI'] : ['Chrome'], 59 | singleRun: process.env.CI ? true : false, 60 | restartOnFileChange: false 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | Select demo: 3 | 8 | 13 | 18 |
19 | 20 |
21 | @switch (currentTab()) { 22 | @case ('date-range') { 23 | 26 | 27 | 34 | 35 | 41 | 42 | } 43 | 44 | @case ('date-range-dialog') { 45 | 48 | 49 | 56 | 57 | 64 | 65 | } 66 | 67 | @case ('date-time-inline') { 68 | 74 | 75 | } 76 | } 77 |
78 | -------------------------------------------------------------------------------- /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/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker-trigger.directive.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time-picker-trigger.directive 3 | */ 4 | 5 | 6 | import { 7 | AfterContentInit, 8 | ChangeDetectorRef, 9 | Directive, 10 | Input, 11 | OnChanges, 12 | OnDestroy, 13 | OnInit, 14 | SimpleChanges 15 | } from '@angular/core'; 16 | import { OwlDateTimeComponent } from './date-time-picker.component'; 17 | import { merge, of as observableOf, Subscription } from 'rxjs'; 18 | 19 | @Directive({ 20 | selector: '[owlDateTimeTrigger]', 21 | standalone: false, 22 | host: { 23 | '(click)': 'handleClickOnHost($event)', 24 | '[class.owl-dt-trigger-disabled]': 'owlDTTriggerDisabledClass' 25 | } 26 | }) 27 | export class OwlDateTimeTriggerDirective implements OnInit, OnChanges, AfterContentInit, OnDestroy { 28 | 29 | @Input('owlDateTimeTrigger') dtPicker: OwlDateTimeComponent; 30 | 31 | private _disabled: boolean; 32 | @Input() 33 | get disabled(): boolean { 34 | return this._disabled === undefined ? this.dtPicker.disabled : !!this._disabled; 35 | } 36 | 37 | set disabled( value: boolean ) { 38 | this._disabled = value; 39 | } 40 | 41 | get owlDTTriggerDisabledClass(): boolean { 42 | return this.disabled; 43 | } 44 | 45 | private stateChanges = Subscription.EMPTY; 46 | 47 | constructor( protected changeDetector: ChangeDetectorRef ) { 48 | } 49 | 50 | public ngOnInit(): void { 51 | } 52 | 53 | public ngOnChanges( changes: SimpleChanges ) { 54 | if (changes.datepicker) { 55 | this.watchStateChanges(); 56 | } 57 | } 58 | 59 | public ngAfterContentInit() { 60 | this.watchStateChanges(); 61 | } 62 | 63 | public ngOnDestroy(): void { 64 | this.stateChanges.unsubscribe(); 65 | } 66 | 67 | public handleClickOnHost( event: Event ): void { 68 | if (this.dtPicker) { 69 | this.dtPicker.open(); 70 | event.stopPropagation(); 71 | } 72 | } 73 | 74 | private watchStateChanges(): void { 75 | this.stateChanges.unsubscribe(); 76 | 77 | const inputDisabled = this.dtPicker && this.dtPicker.dtInput ? 78 | this.dtPicker.dtInput.disabledChange : observableOf(); 79 | 80 | const pickerDisabled = this.dtPicker ? 81 | this.dtPicker.disabledChange : observableOf(); 82 | 83 | this.stateChanges = merge([pickerDisabled, inputDisabled]) 84 | .subscribe(() => { 85 | this.changeDetector.markForCheck(); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time.module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time.module 3 | */ 4 | 5 | import { NgModule } from '@angular/core'; 6 | import { CommonModule } from '@angular/common'; 7 | import { A11yModule } from '@angular/cdk/a11y'; 8 | import { OverlayModule } from '@angular/cdk/overlay'; 9 | import { OwlDateTimeTriggerDirective } from './date-time-picker-trigger.directive'; 10 | import { OWL_DTPICKER_SCROLL_STRATEGY_PROVIDER, OwlDateTimeComponent } from './date-time-picker.component'; 11 | import { OwlDateTimeContainerComponent } from './date-time-picker-container.component'; 12 | import { OwlDateTimeInputDirective } from './date-time-picker-input.directive'; 13 | import { OwlDateTimeIntl } from './date-time-picker-intl.service'; 14 | import { OwlMonthViewComponent } from './calendar-month-view.component'; 15 | import { OwlCalendarBodyComponent } from './calendar-body.component'; 16 | import { OwlYearViewComponent } from './calendar-year-view.component'; 17 | import { OwlMultiYearViewComponent } from './calendar-multi-year-view.component'; 18 | import { OwlTimerBoxComponent } from './timer-box.component'; 19 | import { OwlTimerComponent } from './timer.component'; 20 | import { NumberFixedLenPipe } from './numberedFixLen.pipe'; 21 | import { OwlCalendarComponent } from './calendar.component'; 22 | import { OwlDateTimeInlineComponent } from './date-time-inline.component'; 23 | import { OwlDialogModule } from '../dialog/dialog.module'; 24 | import { optionsProviders } from './options-provider'; 25 | 26 | @NgModule({ 27 | imports: [CommonModule, OverlayModule, OwlDialogModule, A11yModule], 28 | exports: [ 29 | OwlCalendarComponent, 30 | OwlTimerComponent, 31 | OwlDateTimeTriggerDirective, 32 | OwlDateTimeInputDirective, 33 | OwlDateTimeComponent, 34 | OwlDateTimeInlineComponent, 35 | OwlMultiYearViewComponent, 36 | OwlYearViewComponent, 37 | OwlMonthViewComponent, 38 | ], 39 | declarations: [ 40 | OwlDateTimeTriggerDirective, 41 | OwlDateTimeInputDirective, 42 | OwlDateTimeComponent, 43 | OwlDateTimeContainerComponent, 44 | OwlMultiYearViewComponent, 45 | OwlYearViewComponent, 46 | OwlMonthViewComponent, 47 | OwlTimerComponent, 48 | OwlTimerBoxComponent, 49 | OwlCalendarComponent, 50 | OwlCalendarBodyComponent, 51 | NumberFixedLenPipe, 52 | OwlDateTimeInlineComponent, 53 | ], 54 | providers: [ 55 | OwlDateTimeIntl, 56 | OWL_DTPICKER_SCROLL_STRATEGY_PROVIDER, 57 | ...optionsProviders, 58 | ] 59 | }) 60 | export class OwlDateTimeModule { 61 | } 62 | -------------------------------------------------------------------------------- /projects/picker/src/lib/utils/date.utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date.utils 3 | */ 4 | 5 | /** 6 | * Creates a date with the given year, month, date, hour, minute and second. Does not allow over/under-flow of the 7 | * month and date. 8 | */ 9 | export function createDate( 10 | year: number, 11 | month: number, 12 | date: number, 13 | hours: number = 0, 14 | minutes: number = 0, 15 | seconds: number = 0 16 | ): Date { 17 | if (month < 0 || month > 11) { 18 | throw Error( 19 | `Invalid month index "${month}". Month index has to be between 0 and 11.` 20 | ); 21 | } 22 | 23 | if (date < 1) { 24 | throw Error( 25 | `Invalid date "${date}". Date has to be greater than 0.` 26 | ); 27 | } 28 | 29 | if (hours < 0 || hours > 23) { 30 | throw Error( 31 | `Invalid hours "${hours}". Hours has to be between 0 and 23.` 32 | ); 33 | } 34 | 35 | if (minutes < 0 || minutes > 59) { 36 | throw Error( 37 | `Invalid minutes "${minutes}". Minutes has to between 0 and 59.` 38 | ); 39 | } 40 | 41 | if (seconds < 0 || seconds > 59) { 42 | throw Error( 43 | `Invalid seconds "${seconds}". Seconds has to be between 0 and 59.` 44 | ); 45 | } 46 | 47 | const result = createDateWithOverflow( 48 | year, 49 | month, 50 | date, 51 | hours, 52 | minutes, 53 | seconds 54 | ); 55 | 56 | // Check that the date wasn't above the upper bound for the month, causing the month to overflow 57 | // For example, createDate(2017, 1, 31) would try to create a date 2017/02/31 which is invalid 58 | if (result.getMonth() !== month) { 59 | throw Error( 60 | `Invalid date "${date}" for month with index "${month}".` 61 | ); 62 | } 63 | 64 | return result; 65 | } 66 | 67 | /** 68 | * Gets the number of days in the month of the given date. 69 | */ 70 | export function getNumDaysInMonth(date: Date): number { 71 | const lastDateOfMonth = createDateWithOverflow( 72 | date.getFullYear(), 73 | date.getMonth() + 1, 74 | 0 75 | ); 76 | 77 | return lastDateOfMonth.getDate(); 78 | } 79 | 80 | /** 81 | * Creates a date but allows the month and date to overflow. 82 | */ 83 | function createDateWithOverflow( 84 | year: number, 85 | month: number, 86 | date: number, 87 | hours: number = 0, 88 | minutes: number = 0, 89 | seconds: number = 0 90 | ): Date { 91 | const result = new Date(year, month, date, hours, minutes, seconds); 92 | 93 | if (year >= 0 && year < 100) { 94 | result.setFullYear(result.getFullYear() - 1900); 95 | } 96 | return result; 97 | } 98 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker-intl.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time-picker-intl.service 3 | */ 4 | 5 | import { Injectable } from '@angular/core'; 6 | import { Subject } from 'rxjs'; 7 | 8 | @Injectable({providedIn: 'root'}) 9 | export class OwlDateTimeIntl { 10 | 11 | /** 12 | * Stream that emits whenever the labels here are changed. Use this to notify 13 | * components if the labels have changed after initialization. 14 | */ 15 | readonly changes: Subject = new Subject(); 16 | 17 | /** A label for the up second button (used by screen readers). */ 18 | upSecondLabel = 'Add a second'; 19 | 20 | /** A label for the down second button (used by screen readers). */ 21 | downSecondLabel = 'Minus a second'; 22 | 23 | /** A label for the up minute button (used by screen readers). */ 24 | upMinuteLabel = 'Add a minute'; 25 | 26 | /** A label for the down minute button (used by screen readers). */ 27 | downMinuteLabel = 'Minus a minute'; 28 | 29 | /** A label for the up hour button (used by screen readers). */ 30 | upHourLabel = 'Add a hour'; 31 | 32 | /** A label for the down hour button (used by screen readers). */ 33 | downHourLabel = 'Minus a hour'; 34 | 35 | /** A label for the previous month button (used by screen readers). */ 36 | prevMonthLabel = 'Previous month'; 37 | 38 | /** A label for the next month button (used by screen readers). */ 39 | nextMonthLabel = 'Next month'; 40 | 41 | /** A label for the previous year button (used by screen readers). */ 42 | prevYearLabel = 'Previous year'; 43 | 44 | /** A label for the next year button (used by screen readers). */ 45 | nextYearLabel = 'Next year'; 46 | 47 | /** A label for the previous multi-year button (used by screen readers). */ 48 | prevMultiYearLabel = 'Previous 21 years'; 49 | 50 | /** A label for the next multi-year button (used by screen readers). */ 51 | nextMultiYearLabel = 'Next 21 years'; 52 | 53 | /** A label for the 'switch to month view' button (used by screen readers). */ 54 | switchToMonthViewLabel = 'Change to month view'; 55 | 56 | /** A label for the 'switch to year view' button (used by screen readers). */ 57 | switchToMultiYearViewLabel = 'Choose month and year'; 58 | 59 | /** A label for the cancel button */ 60 | cancelBtnLabel = 'Cancel'; 61 | 62 | /** A label for the set button */ 63 | setBtnLabel = 'Set'; 64 | 65 | /** A label for the range 'from' in picker info */ 66 | rangeFromLabel = 'From'; 67 | 68 | /** A label for the range 'to' in picker info */ 69 | rangeToLabel = 'To'; 70 | 71 | /** A label for the hour12 button (AM) */ 72 | hour12AMLabel = 'AM'; 73 | 74 | /** A label for the hour12 button (PM) */ 75 | hour12PMLabel = 'PM'; 76 | } 77 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-multi-year-view.component.html: -------------------------------------------------------------------------------- 1 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 |
{{tableHeader}}
30 | 45 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/timer-box.component.html: -------------------------------------------------------------------------------- 1 | @if (showDivider) { 2 | 3 | } 4 | 35 | 49 | 80 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "no-output-on-prefix": true, 120 | "no-inputs-metadata-property": true, 121 | "no-outputs-metadata-property": true, 122 | "no-host-metadata-property": true, 123 | "no-input-rename": true, 124 | "no-output-rename": true, 125 | "use-lifecycle-interface": true, 126 | "use-pipe-transform-interface": true, 127 | "component-class-suffix": true, 128 | "directive-class-suffix": true 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@danielmoncada/angular-datetime-picker", 3 | "version": "20.0.1", 4 | "description": "Angular Date Time Picker", 5 | "keywords": [ 6 | "Angular", 7 | "datepicker", 8 | "date picker", 9 | "timepicker", 10 | "time picker", 11 | "datetime picker", 12 | "date time picker", 13 | "material", 14 | "ngx" 15 | ], 16 | "author": "Maintained and updated by Daniel Moncada, original implementatiom by Daniel Pan", 17 | "license": "MIT", 18 | "scripts": { 19 | "ng": "ng", 20 | "start": "ng serve", 21 | "build": "ng build picker", 22 | "test": "ng test picker", 23 | "test:ci": "ng test --watch=false --no-progress --browsers=ChromeHeadlessCI --code-coverage=true picker", 24 | "lint": "ng lint", 25 | "e2e": "ng e2e", 26 | "build_lib_onWindows": "ng build picker --configuration production && copy \"README.md\" \"dist/picker\"", 27 | "build_lib_onLinux": "ng build picker --configuration production && cp README.md dist/picker", 28 | "build_css_onWindows": "mkdir \"dist/picker/assets/style\" && sass --style=compressed projects/picker/src/sass/picker.scss > dist/picker/assets/style/picker.min.css", 29 | "build_css_onLinux": "mkdir -p dist/picker/assets/style && sass --style=compressed projects/picker/src/sass/picker.scss > dist/picker/assets/style/picker.min.css", 30 | "npm_pack": "cd dist/picker && npm pack", 31 | "package_windows": "npm run build_lib_onWindows && npm run build_css_onWindows && npm run npm_pack", 32 | "package_linux": "npm run build_lib_onLinux && npm run build_css_onLinux && npm run npm_pack" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/danielmoncada/date-time-picker.git" 37 | }, 38 | "dependencies": { 39 | "@angular/cdk": "^20.2.4", 40 | "@angular/common": "^20.3.1", 41 | "@angular/compiler": "^20.3.1", 42 | "@angular/core": "^20.3.1", 43 | "@angular/forms": "^20.3.1", 44 | "@angular/platform-browser": "^20.3.1", 45 | "@angular/platform-browser-dynamic": "^20.3.1", 46 | "@angular/router": "^20.3.1", 47 | "core-js": "^3.45.1", 48 | "moment": "^2.30.1", 49 | "moment-timezone": "^0.6.0", 50 | "np": "^10.2.0", 51 | "rxjs": "^7.8.2", 52 | "tslib": "^2.8.1", 53 | "uuid": "^13.0.0", 54 | "zone.js": "^0.15.1" 55 | }, 56 | "devDependencies": { 57 | "@angular-devkit/build-angular": "^20.3.2", 58 | "@angular-eslint/eslint-plugin": "^20.3.0", 59 | "@angular-eslint/eslint-plugin-template": "^20.3.0", 60 | "@angular-eslint/schematics": "^20.3.0", 61 | "@angular-eslint/template-parser": "^20.3.0", 62 | "@angular/cli": "^20.3.2", 63 | "@angular/compiler-cli": "^20.3.1", 64 | "@angular/language-service": "^20.3.1", 65 | "@types/jasmine": "^5.1.9", 66 | "@types/jasminewd2": "^2.0.13", 67 | "@types/node": "^24.5.2", 68 | "@typescript-eslint/eslint-plugin": "^8.44.1", 69 | "@typescript-eslint/parser": "^8.44.1", 70 | "codecov": "^3.8.3", 71 | "codelyzer": "^6.0.2", 72 | "eslint": "^9.36.0", 73 | "jasmine-core": "~5.1.2", 74 | "jasmine-spec-reporter": "~7.0.0", 75 | "karma": "~6.4.3", 76 | "karma-chrome-launcher": "^3.2.0", 77 | "karma-coverage": "^2.2.1", 78 | "karma-jasmine": "~5.1.0", 79 | "karma-jasmine-html-reporter": "^2.1.0", 80 | "ng-packagr": "^20.3.0", 81 | "protractor": "~7.0.0", 82 | "sass": "^1.93.2", 83 | "ts-node": "^10.9.2", 84 | "typescript": "~5.9.2" 85 | }, 86 | "overrides": { 87 | "@lmdb/lmdb-darwin-arm64": "3.0.12" 88 | }, 89 | "optionalDependencies": { 90 | "@nx/nx-darwin-arm64": "21.5.3", 91 | "@nx/nx-darwin-x64": "21.5.3", 92 | "@nx/nx-linux-x64-gnu": "21.5.3", 93 | "@nx/nx-win32-x64-msvc": "21.5.3" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /projects/picker/src/test-helpers.ts: -------------------------------------------------------------------------------- 1 | // Based on @angular/cdk/testing 2 | import { EventEmitter, NgZone } from '@angular/core'; 3 | 4 | export function dispatchEvent(node: Node | Window, event: Event): Event { 5 | node.dispatchEvent(event); 6 | return event; 7 | } 8 | 9 | export function dispatchFakeEvent( 10 | node: Node | Window, 11 | type: string, 12 | canBubble?: boolean 13 | ): Event { 14 | return dispatchEvent(node, createFakeEvent(type, canBubble)); 15 | } 16 | 17 | export function createFakeEvent( 18 | type: string, 19 | canBubble = false, 20 | cancelable = true 21 | ) { 22 | const event = document.createEvent('Event'); 23 | event.initEvent(type, canBubble, cancelable); 24 | return event; 25 | } 26 | 27 | export function dispatchKeyboardEvent( 28 | node: Node, 29 | type: string, 30 | keyCode: number, 31 | target?: Element 32 | ): KeyboardEvent { 33 | return dispatchEvent( 34 | node, 35 | createKeyboardEvent(type, keyCode, target) 36 | ) as KeyboardEvent; 37 | } 38 | 39 | export function createKeyboardEvent( 40 | type: string, 41 | keyCode: number, 42 | target?: Element, 43 | key?: string 44 | ) { 45 | const event = document.createEvent('KeyboardEvent') as any; 46 | const originalPreventDefault = event.preventDefault; 47 | 48 | // Firefox does not support `initKeyboardEvent`, but supports `initKeyEvent`. 49 | if (event.initKeyEvent) { 50 | event.initKeyEvent(type, true, true, window, 0, 0, 0, 0, 0, keyCode); 51 | } else { 52 | event.initKeyboardEvent(type, true, true, window, 0, key, 0, '', false); 53 | } 54 | 55 | // Webkit Browsers don't set the keyCode when calling the init function. 56 | // See related bug https://bugs.webkit.org/show_bug.cgi?id=16735 57 | Object.defineProperties(event, { 58 | keyCode: { get: () => keyCode }, 59 | key: { get: () => key }, 60 | target: { get: () => target } 61 | }); 62 | 63 | // IE won't set `defaultPrevented` on synthetic events so we need to do it manually. 64 | event.preventDefault = function() { 65 | Object.defineProperty(event, 'defaultPrevented', { get: () => true }); 66 | return originalPreventDefault.apply(this, arguments); 67 | }; 68 | 69 | return event; 70 | } 71 | 72 | export function dispatchMouseEvent( 73 | node: Node, 74 | type: string, 75 | x = 0, 76 | y = 0, 77 | event = createMouseEvent(type, x, y) 78 | ): MouseEvent { 79 | return dispatchEvent(node, event) as MouseEvent; 80 | } 81 | 82 | /** Creates a browser MouseEvent with the specified options. */ 83 | export function createMouseEvent(type: string, x = 0, y = 0, button = 0) { 84 | const event = document.createEvent('MouseEvent'); 85 | 86 | event.initMouseEvent( 87 | type, 88 | true /* canBubble */, 89 | false /* cancelable */, 90 | window /* view */, 91 | 0 /* detail */, 92 | x /* screenX */, 93 | y /* screenY */, 94 | x /* clientX */, 95 | y /* clientY */, 96 | false /* ctrlKey */, 97 | false /* altKey */, 98 | false /* shiftKey */, 99 | false /* metaKey */, 100 | button /* button */, 101 | null /* relatedTarget */ 102 | ); 103 | 104 | // `initMouseEvent` doesn't allow us to pass the `buttons` and 105 | // defaults it to 0 which looks like a fake event. 106 | Object.defineProperty(event, 'buttons', { get: () => 1 }); 107 | 108 | return event; 109 | } 110 | 111 | export class MockNgZone extends NgZone { 112 | onStable: EventEmitter = new EventEmitter(false); 113 | constructor() { 114 | super({ enableLongStackTrace: false }); 115 | } 116 | run(fn: Function): any { 117 | return fn(); 118 | } 119 | runOutsideAngular(fn: Function): any { 120 | return fn(); 121 | } 122 | simulateZoneExit(): void { 123 | this.onStable.emit(null); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/timer-box.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * timer-box.component 3 | */ 4 | 5 | import { 6 | ChangeDetectionStrategy, 7 | Component, 8 | EventEmitter, 9 | ElementRef, 10 | ViewChild, 11 | Input, 12 | OnDestroy, 13 | OnInit, 14 | Output 15 | } from '@angular/core'; 16 | import { coerceNumberProperty } from '@angular/cdk/coercion'; 17 | import { Subject, Subscription } from 'rxjs'; 18 | import { debounceTime } from 'rxjs/operators'; 19 | 20 | @Component({ 21 | exportAs: 'owlDateTimeTimerBox', 22 | selector: 'owl-date-time-timer-box', 23 | templateUrl: './timer-box.component.html', 24 | styleUrls: ['./timer-box.component.scss'], 25 | standalone: false, 26 | preserveWhitespaces: false, 27 | changeDetection: ChangeDetectionStrategy.OnPush, 28 | host: { 29 | '[class.owl-dt-timer-box]': 'owlDTTimerBoxClass' 30 | } 31 | }) 32 | 33 | export class OwlTimerBoxComponent implements OnInit, OnDestroy { 34 | 35 | @Input() showDivider = false; 36 | 37 | @Input() upBtnAriaLabel: string; 38 | 39 | @Input() upBtnDisabled: boolean; 40 | 41 | @Input() downBtnAriaLabel: string; 42 | 43 | @Input() downBtnDisabled: boolean; 44 | 45 | /** 46 | * Value would be displayed in the box 47 | * If it is null, the box would display [value] 48 | * */ 49 | @Input() boxValue: number; 50 | 51 | @Input() value: number; 52 | 53 | @Input() min: number; 54 | 55 | @Input() max: number; 56 | 57 | @Input() step = 1; 58 | 59 | @Input() inputLabel: string; 60 | 61 | @Output() valueChange = new EventEmitter(); 62 | 63 | @Output() inputChange = new EventEmitter(); 64 | 65 | private inputStream = new Subject(); 66 | 67 | private inputStreamSub = Subscription.EMPTY; 68 | 69 | private hasFocus = false; 70 | 71 | get displayValue(): string { 72 | if (this.hasFocus) { 73 | // Don't try to reformat the value that user is currently editing 74 | return this.valueInput.nativeElement.value; 75 | } 76 | 77 | const value = this.boxValue || this.value; 78 | 79 | if (value === null || isNaN(value)) { 80 | return ''; 81 | } 82 | 83 | return value < 10 ? '0' + value.toString() : value.toString(); 84 | } 85 | 86 | get owlDTTimerBoxClass(): boolean { 87 | return true; 88 | } 89 | 90 | @ViewChild('valueInput', { static: true }) 91 | private valueInput: ElementRef; 92 | private onValueInputMouseWheelBind = this.onValueInputMouseWheel.bind(this); 93 | 94 | constructor() { 95 | } 96 | 97 | public ngOnInit() { 98 | this.inputStreamSub = this.inputStream.pipe(debounceTime(750)).subscribe(( val: string ) => { 99 | if (val) { 100 | const inputValue = coerceNumberProperty(val, 0); 101 | this.updateValueViaInput(inputValue); 102 | } 103 | }); 104 | this.bindValueInputMouseWheel(); 105 | } 106 | 107 | public ngOnDestroy(): void { 108 | this.unbindValueInputMouseWheel(); 109 | this.inputStreamSub.unsubscribe(); 110 | } 111 | 112 | public upBtnClicked(): void { 113 | this.updateValue(this.value + this.step); 114 | } 115 | 116 | public downBtnClicked(): void { 117 | this.updateValue(this.value - this.step); 118 | } 119 | 120 | public handleInputChange(val: string ): void { 121 | this.inputStream.next(val); 122 | } 123 | 124 | public focusIn(): void { 125 | this.hasFocus = true; 126 | } 127 | 128 | public focusOut(value: string): void { 129 | this.hasFocus = false; 130 | if (value) { 131 | const inputValue = coerceNumberProperty(value, 0); 132 | this.updateValueViaInput(inputValue); 133 | } 134 | } 135 | 136 | private updateValue( value: number ): void { 137 | this.valueChange.emit(value); 138 | } 139 | 140 | private updateValueViaInput( value: number ): void { 141 | if (value > this.max || value < this.min) { 142 | return; 143 | } 144 | this.inputChange.emit(value); 145 | } 146 | 147 | private onValueInputMouseWheel( event: any ): void { 148 | event = event || window.event; 149 | const delta = event.wheelDelta || -event.deltaY || -event.detail; 150 | 151 | if (delta > 0) { 152 | if (!this.upBtnDisabled) { 153 | this.upBtnClicked(); 154 | } 155 | } else if (delta < 0) { 156 | if (!this.downBtnDisabled) { 157 | this.downBtnClicked(); 158 | } 159 | } 160 | 161 | event.preventDefault ? event.preventDefault() : (event.returnValue = false); 162 | } 163 | 164 | private bindValueInputMouseWheel(): void { 165 | this.valueInput.nativeElement.addEventListener( 166 | 'onwheel' in document ? 'wheel' : 'mousewheel', 167 | this.onValueInputMouseWheelBind); 168 | } 169 | 170 | private unbindValueInputMouseWheel(): void { 171 | this.valueInput.nativeElement.removeEventListener( 172 | 'onwheel' in document ? 'wheel' : 'mousewheel', 173 | this.onValueInputMouseWheelBind); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-body.component.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * calendar-body.component.spec 3 | */ 4 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 5 | import { CalendarCell, OwlCalendarBodyComponent } from './calendar-body.component'; 6 | import { Component } from '@angular/core'; 7 | import { By } from '@angular/platform-browser'; 8 | 9 | describe('OwlCalendarBodyComponent', () => { 10 | beforeEach((() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [ 13 | OwlCalendarBodyComponent, 14 | 15 | // Test components 16 | StandardCalendarBodyComponent, 17 | ], 18 | }).compileComponents(); 19 | })); 20 | 21 | describe('standard CalendarBodyComponent', () => { 22 | let fixture: ComponentFixture; 23 | let testComponent: StandardCalendarBodyComponent; 24 | let calendarBodyNativeElement: Element; 25 | let rowEls: NodeListOf; 26 | let cellEls: NodeListOf; 27 | 28 | const refreshElementLists = () => { 29 | rowEls = calendarBodyNativeElement.querySelectorAll('tr'); 30 | cellEls = calendarBodyNativeElement.querySelectorAll('.owl-dt-calendar-cell'); 31 | }; 32 | 33 | beforeEach(() => { 34 | fixture = TestBed.createComponent(StandardCalendarBodyComponent); 35 | fixture.detectChanges(); 36 | 37 | const calendarBodyDebugElement = fixture.debugElement.query(By.directive(OwlCalendarBodyComponent)); 38 | calendarBodyNativeElement = calendarBodyDebugElement.nativeElement; 39 | testComponent = fixture.componentInstance; 40 | 41 | refreshElementLists(); 42 | }); 43 | 44 | it('should create body', () => { 45 | expect(rowEls.length).toBe(2); 46 | expect(cellEls.length).toBe(14); 47 | }); 48 | 49 | it('should highlight today', () => { 50 | const todayCell = calendarBodyNativeElement.querySelector('.owl-dt-calendar-cell-today'); 51 | expect(todayCell).not.toBeNull(); 52 | expect(todayCell.innerHTML.trim()).toBe('3'); 53 | }); 54 | 55 | it('should highlight selected', () => { 56 | const selectedCell = calendarBodyNativeElement.querySelector('.owl-dt-calendar-cell-selected'); 57 | expect(selectedCell).not.toBeNull(); 58 | expect(selectedCell.innerHTML.trim()).toBe('4'); 59 | }); 60 | 61 | it('cell should be selected on click', () => { 62 | spyOn(testComponent, 'handleSelect'); 63 | expect(testComponent.handleSelect).not.toHaveBeenCalled(); 64 | const todayElement = 65 | calendarBodyNativeElement.querySelector('.owl-dt-calendar-cell-today') as HTMLElement; 66 | todayElement.click(); 67 | fixture.detectChanges(); 68 | 69 | expect(testComponent.handleSelect).toHaveBeenCalled(); 70 | }); 71 | 72 | it('should mark active date', () => { 73 | expect((cellEls[10] as HTMLElement).innerText.trim()).toBe('11'); 74 | expect(cellEls[10].classList).toContain('owl-dt-calendar-cell-active'); 75 | }); 76 | 77 | it('should have aria-current set for today', () => { 78 | const currentCells = calendarBodyNativeElement.querySelectorAll('.owl-dt-calendar-cell[aria-current]'); 79 | expect(currentCells.length).toBe(1); 80 | const todayCell = calendarBodyNativeElement.querySelector('.owl-dt-calendar-cell-today'); 81 | expect(currentCells[0].getAttribute('aria-current')).toBe('date'); 82 | expect(currentCells[0].firstChild).toBe(todayCell); 83 | }); 84 | 85 | it('should have aria-selected set on selected cells', () => { 86 | const calendarCells = calendarBodyNativeElement.querySelectorAll('.owl-dt-calendar-cell'); 87 | const selectedCells = calendarBodyNativeElement.querySelectorAll('.owl-dt-calendar-cell[aria-selected=true]'); 88 | const nonSelectedCells = calendarBodyNativeElement.querySelectorAll('.owl-dt-calendar-cell[aria-selected=false]'); 89 | expect(nonSelectedCells.length).toBe(calendarCells.length - selectedCells.length); 90 | const selectedCell = calendarBodyNativeElement.querySelector('.owl-dt-calendar-cell-selected'); 91 | expect(selectedCells[0].firstChild).toBe(selectedCell); 92 | }); 93 | }); 94 | }); 95 | 96 | @Component({ 97 | standalone: false, 98 | template: ` 99 | 106 |
`, 107 | }) 108 | class StandardCalendarBodyComponent { 109 | rows = [[1, 2, 3, 4, 5, 6, 7], [8, 9, 10, 11, 12, 13, 14]].map(r => r.map(createCell)); 110 | todayValue = 3; 111 | selectedValues = [4]; 112 | activeCell = 10; 113 | 114 | handleSelect() { 115 | } 116 | } 117 | 118 | function createCell( value: number ) { 119 | return new CalendarCell(value, `${value}`, `${value}-label`, true); 120 | } 121 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-picker-container.component.html: -------------------------------------------------------------------------------- 1 |
5 | @if (pickerType === 'both' || pickerType === 'calendar') { 6 | 26 | } 27 | @if (pickerType === 'both' || pickerType === 'timer') { 28 | 40 | } 41 | @if (picker.isInRangeMode) { 42 |
46 | 70 | 94 |
95 | } 96 | @if (showControlButtons) { 97 |
98 | 111 | 124 |
125 | } 126 |
127 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-body.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * calendar-body.component 3 | */ 4 | 5 | import { 6 | ChangeDetectionStrategy, 7 | Component, 8 | ElementRef, 9 | EventEmitter, 10 | Input, 11 | NgZone, 12 | OnInit, 13 | Output 14 | } from '@angular/core'; 15 | import { SelectMode } from './date-time.class'; 16 | import { take } from 'rxjs/operators'; 17 | 18 | export class CalendarCell { 19 | constructor( 20 | public value: number, 21 | public displayValue: string, 22 | public ariaLabel: string, 23 | public enabled: boolean, 24 | public out: boolean = false, 25 | public cellClass: string = '' 26 | ) {} 27 | } 28 | 29 | @Component({ 30 | selector: '[owl-date-time-calendar-body]', 31 | exportAs: 'owlDateTimeCalendarBody', 32 | templateUrl: './calendar-body.component.html', 33 | styleUrls: ['./calendar-body.component.scss'], 34 | host: { 35 | '[class.owl-dt-calendar-body]': 'owlDTCalendarBodyClass' 36 | }, 37 | preserveWhitespaces: false, 38 | standalone: false, 39 | changeDetection: ChangeDetectionStrategy.OnPush 40 | }) 41 | export class OwlCalendarBodyComponent implements OnInit { 42 | /** 43 | * The cell number of the active cell in the table. 44 | */ 45 | @Input() 46 | activeCell = 0; 47 | 48 | /** 49 | * The cells to display in the table. 50 | * */ 51 | @Input() 52 | rows: CalendarCell[][]; 53 | 54 | /** 55 | * The number of columns in the table. 56 | * */ 57 | @Input() 58 | numCols = 7; 59 | 60 | /** 61 | * The ratio (width / height) to use for the cells in the table. 62 | */ 63 | @Input() 64 | cellRatio = 1; 65 | 66 | /** 67 | * The value in the table that corresponds to today. 68 | * */ 69 | @Input() 70 | todayValue: number; 71 | 72 | /** 73 | * The value in the table that is currently selected. 74 | * */ 75 | @Input() 76 | selectedValues: number[]; 77 | 78 | /** 79 | * Current picker select mode 80 | */ 81 | @Input() 82 | selectMode: SelectMode; 83 | 84 | /** 85 | * Emit when a calendar cell is selected 86 | * */ 87 | @Output() 88 | public readonly select = new EventEmitter(); 89 | 90 | get owlDTCalendarBodyClass(): boolean { 91 | return true; 92 | } 93 | 94 | get isInSingleMode(): boolean { 95 | return this.selectMode === 'single'; 96 | } 97 | 98 | get isInRangeMode(): boolean { 99 | return ( 100 | this.selectMode === 'range' || 101 | this.selectMode === 'rangeFrom' || 102 | this.selectMode === 'rangeTo' 103 | ); 104 | } 105 | 106 | constructor(private elmRef: ElementRef, private ngZone: NgZone) {} 107 | 108 | public ngOnInit() {} 109 | 110 | public selectCell(cell: CalendarCell): void { 111 | this.select.emit(cell); 112 | } 113 | 114 | public isActiveCell(rowIndex: number, colIndex: number): boolean { 115 | const cellNumber = rowIndex * this.numCols + colIndex; 116 | return cellNumber === this.activeCell; 117 | } 118 | 119 | /** 120 | * Check if the cell is selected 121 | */ 122 | public isSelected(value: number): boolean { 123 | if (!this.selectedValues || this.selectedValues.length === 0) { 124 | return false; 125 | } 126 | 127 | if (this.isInSingleMode) { 128 | return value === this.selectedValues[0]; 129 | } 130 | 131 | if (this.isInRangeMode) { 132 | const fromValue = this.selectedValues[0]; 133 | const toValue = this.selectedValues[1]; 134 | 135 | return value === fromValue || value === toValue; 136 | } 137 | } 138 | 139 | /** 140 | * Check if the cell in the range 141 | * */ 142 | public isInRange(value: number): boolean { 143 | if (this.isInRangeMode) { 144 | const fromValue = this.selectedValues[0]; 145 | const toValue = this.selectedValues[1]; 146 | 147 | if (fromValue !== null && toValue !== null) { 148 | return value >= fromValue && value <= toValue; 149 | } else { 150 | return value === fromValue || value === toValue; 151 | } 152 | } 153 | } 154 | 155 | /** 156 | * Check if the cell is the range from 157 | * */ 158 | public isRangeFrom(value: number): boolean { 159 | if (this.isInRangeMode) { 160 | const fromValue = this.selectedValues[0]; 161 | return fromValue !== null && value === fromValue; 162 | } 163 | } 164 | 165 | /** 166 | * Check if the cell is the range to 167 | * */ 168 | public isRangeTo(value: number): boolean { 169 | if (this.isInRangeMode) { 170 | const toValue = this.selectedValues[1]; 171 | return toValue !== null && value === toValue; 172 | } 173 | } 174 | 175 | /** 176 | * Focus to a active cell 177 | * */ 178 | public focusActiveCell(): void { 179 | this.ngZone.runOutsideAngular(() => { 180 | this.ngZone.onStable 181 | .asObservable() 182 | .pipe(take(1)) 183 | .subscribe(() => { 184 | this.elmRef.nativeElement 185 | .querySelector('.owl-dt-calendar-cell-active') 186 | .focus(); 187 | }); 188 | }); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog-config.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * dialog-config.class 3 | */ 4 | import { ViewContainerRef } from '@angular/core'; 5 | import { NoopScrollStrategy, ScrollStrategy } from '@angular/cdk/overlay'; 6 | 7 | let uniqueId = 0; 8 | 9 | /** Possible overrides for a dialog's position. */ 10 | export interface DialogPosition { 11 | /** Override for the dialog's top position. */ 12 | top?: string; 13 | 14 | /** Override for the dialog's bottom position. */ 15 | bottom?: string; 16 | 17 | /** Override for the dialog's left position. */ 18 | left?: string; 19 | 20 | /** Override for the dialog's right position. */ 21 | right?: string; 22 | } 23 | 24 | export interface OwlDialogConfigInterface { 25 | /** 26 | * ID of the element that describes the dialog. 27 | */ 28 | ariaDescribedBy?: string | null; 29 | 30 | /** 31 | * Whether to focus the dialog when the dialog is opened 32 | */ 33 | autoFocus?: boolean; 34 | 35 | /** Whether the dialog has a backdrop. */ 36 | hasBackdrop?: boolean; 37 | 38 | /** 39 | * Custom style for the backdrop 40 | * */ 41 | backdropStyle?: any; 42 | 43 | /** Data being injected into the child component. */ 44 | data?: any ; 45 | 46 | /** Whether the user can use escape or clicking outside to close a modal. */ 47 | disableClose?: boolean; 48 | 49 | /** 50 | * ID for the modal. If omitted, a unique one will be generated. 51 | */ 52 | id?: string; 53 | 54 | /** 55 | * The ARIA role of the dialog element. 56 | */ 57 | role?: 'dialog' | 'alertdialog'; 58 | 59 | /** 60 | * Custom class for the pane 61 | * */ 62 | paneClass?: string | string[]; 63 | 64 | /** 65 | * Mouse Event 66 | * */ 67 | event?: MouseEvent; 68 | 69 | /** 70 | * Custom class for the backdrop 71 | * */ 72 | backdropClass?: string | string[]; 73 | 74 | /** 75 | * Whether the dialog should close when the user goes backwards/forwards in history. 76 | * */ 77 | closeOnNavigation?: boolean; 78 | 79 | /** Width of the dialog. */ 80 | width?: string ; 81 | 82 | /** Height of the dialog. */ 83 | height?: string; 84 | 85 | /** 86 | * The min-width of the overlay panel. 87 | * If a number is provided, pixel units are assumed. 88 | * */ 89 | minWidth?: number | string; 90 | 91 | /** 92 | * The min-height of the overlay panel. 93 | * If a number is provided, pixel units are assumed. 94 | * */ 95 | minHeight?: number | string; 96 | 97 | /** 98 | * The max-width of the overlay panel. 99 | * If a number is provided, pixel units are assumed. 100 | * */ 101 | maxWidth?: number | string; 102 | 103 | /** 104 | * The max-height of the overlay panel. 105 | * If a number is provided, pixel units are assumed. 106 | * */ 107 | maxHeight?: number | string; 108 | 109 | /** Position overrides. */ 110 | position?: DialogPosition; 111 | 112 | /** 113 | * The scroll strategy when the dialog is open 114 | * Learn more this from https://material.angular.io/cdk/overlay/overview#scroll-strategies 115 | * */ 116 | scrollStrategy?: ScrollStrategy; 117 | 118 | viewContainerRef?: ViewContainerRef; 119 | } 120 | 121 | export class OwlDialogConfig implements OwlDialogConfigInterface { 122 | /** 123 | * ID of the element that describes the dialog. 124 | */ 125 | public ariaDescribedBy: string | null = null; 126 | 127 | /** 128 | * Whether to focus the dialog when the dialog is opened 129 | */ 130 | public autoFocus = true; 131 | 132 | /** Whether the dialog has a backdrop. */ 133 | public hasBackdrop = true; 134 | 135 | /** 136 | * Custom style for the backdrop 137 | * */ 138 | public backdropStyle: any; 139 | 140 | /** Data being injected into the child component. */ 141 | public data: any = null; 142 | 143 | /** Whether the user can use escape or clicking outside to close a modal. */ 144 | public disableClose = false; 145 | 146 | /** 147 | * ID for the modal. If omitted, a unique one will be generated. 148 | */ 149 | public id: string; 150 | 151 | /** 152 | * The ARIA role of the dialog element. 153 | */ 154 | public role: 'dialog' | 'alertdialog' = 'dialog'; 155 | 156 | /** 157 | * Custom class for the pane 158 | * */ 159 | public paneClass: string | string[] = ''; 160 | 161 | /** 162 | * Mouse Event 163 | * */ 164 | public event: MouseEvent = null; 165 | 166 | /** 167 | * Custom class for the backdrop 168 | * */ 169 | public backdropClass: string | string[] = ''; 170 | 171 | /** 172 | * Whether the dialog should close when the user goes backwards/forwards in history. 173 | * */ 174 | public closeOnNavigation = true; 175 | 176 | /** Width of the dialog. */ 177 | public width = ''; 178 | 179 | /** Height of the dialog. */ 180 | public height = ''; 181 | 182 | /** 183 | * The min-width of the overlay panel. 184 | * If a number is provided, pixel units are assumed. 185 | * */ 186 | public minWidth: number | string; 187 | 188 | /** 189 | * The min-height of the overlay panel. 190 | * If a number is provided, pixel units are assumed. 191 | * */ 192 | public minHeight: number | string; 193 | 194 | /** 195 | * The max-width of the overlay panel. 196 | * If a number is provided, pixel units are assumed. 197 | * */ 198 | public maxWidth: number | string = '85vw'; 199 | 200 | /** 201 | * The max-height of the overlay panel. 202 | * If a number is provided, pixel units are assumed. 203 | * */ 204 | public maxHeight: number | string; 205 | 206 | /** Position overrides. */ 207 | public position: DialogPosition; 208 | 209 | /** 210 | * The scroll strategy when the dialog is open 211 | * Learn more this from https://material.angular.io/cdk/overlay/overview#scroll-strategies 212 | * */ 213 | public scrollStrategy: ScrollStrategy = new NoopScrollStrategy(); 214 | 215 | public viewContainerRef: ViewContainerRef; 216 | 217 | constructor() { 218 | this.id = `owl-dialog-${uniqueId++}`; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog-ref.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * dialog-ref.class 3 | */ 4 | import { ESCAPE } from '@angular/cdk/keycodes'; 5 | import { GlobalPositionStrategy, OverlayRef } from '@angular/cdk/overlay'; 6 | import { Location } from '@angular/common'; 7 | import { Signal, signal } from '@angular/core'; 8 | import { Observable, Subject, Subscription, SubscriptionLike, filter, take } from 'rxjs'; 9 | import { IDateTimePickerAnimationEvent } from '../date-time/date-time-picker-animation-event'; 10 | import { DialogPosition } from './dialog-config.class'; 11 | import { OwlDialogContainerComponent } from './dialog-container.component'; 12 | 13 | export class OwlDialogRef { 14 | 15 | private result: any; 16 | 17 | private _beforeClose$ = new Subject(); 18 | 19 | private _beforeOpen$ = new Subject(); 20 | 21 | private _afterOpen$ = new Subject(); 22 | 23 | private _afterClosed$ = new Subject(); 24 | 25 | /** Subscription to changes in the user's location. */ 26 | private locationChanged: SubscriptionLike = Subscription.EMPTY; 27 | 28 | /** 29 | * The instance of component opened into modal 30 | * */ 31 | public componentInstance = signal(undefined); 32 | 33 | /** Whether the user is allowed to close the dialog. */ 34 | public disableClose = true; 35 | 36 | constructor(private overlayRef: OverlayRef, 37 | private container: OwlDialogContainerComponent, 38 | public readonly id: string, 39 | location?: Location) { 40 | this.disableClose = this.container.config.disableClose; 41 | 42 | this.container.animationStateChanged 43 | .pipe( 44 | filter(( event: IDateTimePickerAnimationEvent ) => event.phaseName === 'start' && event.toState === 'enter'), 45 | take(1) 46 | ) 47 | .subscribe(() => { 48 | this._beforeOpen$.next(null); 49 | this._beforeOpen$.complete(); 50 | }); 51 | 52 | this.container.animationStateChanged 53 | .pipe( 54 | filter(( event: IDateTimePickerAnimationEvent ) => event.phaseName === 'done' && event.toState === 'enter'), 55 | take(1) 56 | ) 57 | .subscribe(() => { 58 | this._afterOpen$.next(null); 59 | this._afterOpen$.complete(); 60 | }); 61 | 62 | this.container.animationStateChanged 63 | .pipe( 64 | filter((event: IDateTimePickerAnimationEvent) => event.phaseName === 'done' && event.toState === 'leave'), 65 | take(1) 66 | ) 67 | .subscribe(() => { 68 | this.overlayRef.dispose(); 69 | this.locationChanged.unsubscribe(); 70 | this._afterClosed$.next(this.result); 71 | this._afterClosed$.complete(); 72 | this.componentInstance.set(undefined); 73 | }); 74 | 75 | this.overlayRef.keydownEvents() 76 | .pipe(filter(event => event.keyCode === ESCAPE && !this.disableClose)) 77 | .subscribe(() => this.close()); 78 | 79 | if (location) { 80 | this.locationChanged = location.subscribe(() => { 81 | if (this.container.config.closeOnNavigation) { 82 | this.close(); 83 | } 84 | }); 85 | } 86 | } 87 | 88 | public close(dialogResult?: any) { 89 | this.result = dialogResult; 90 | 91 | this.container.animationStateChanged 92 | .pipe( 93 | filter(event => event.phaseName === 'start'), 94 | take(1) 95 | ) 96 | .subscribe(() => { 97 | this._beforeClose$.next(dialogResult); 98 | this._beforeClose$.complete(); 99 | this.overlayRef.detachBackdrop(); 100 | }); 101 | 102 | this.container.startExitAnimation(); 103 | } 104 | 105 | /** 106 | * Gets an observable that emits when the overlay's backdrop has been clicked. 107 | */ 108 | public backdropClick(): Observable { 109 | return this.overlayRef.backdropClick(); 110 | } 111 | 112 | /** 113 | * Gets an observable that emits when keydown events are targeted on the overlay. 114 | */ 115 | public keydownEvents(): Observable { 116 | return this.overlayRef.keydownEvents(); 117 | } 118 | 119 | /** 120 | * Updates the dialog's position. 121 | * @param position New dialog position. 122 | */ 123 | public updatePosition(position?: DialogPosition): this { 124 | const strategy = this.getPositionStrategy(); 125 | 126 | if (position && (position.left || position.right)) { 127 | position.left ? strategy.left(position.left) : strategy.right(position.right); 128 | } else { 129 | strategy.centerHorizontally(); 130 | } 131 | 132 | if (position && (position.top || position.bottom)) { 133 | position.top ? strategy.top(position.top) : strategy.bottom(position.bottom); 134 | } else { 135 | strategy.centerVertically(); 136 | } 137 | 138 | this.overlayRef.updatePosition(); 139 | 140 | return this; 141 | } 142 | 143 | /** 144 | * Updates the dialog's width and height. 145 | * @param width New width of the dialog. 146 | * @param height New height of the dialog. 147 | */ 148 | updateSize(width: string = 'auto', height: string = 'auto'): this { 149 | this.getPositionStrategy().width(width).height(height); 150 | this.overlayRef.updatePosition(); 151 | return this; 152 | } 153 | 154 | public isAnimating(): Signal { 155 | return this.container.isAnimating; 156 | } 157 | 158 | public beforeOpen(): Observable { 159 | return this._beforeOpen$.asObservable(); 160 | } 161 | 162 | public afterOpen(): Observable { 163 | return this._afterOpen$.asObservable(); 164 | } 165 | 166 | public beforeClose(): Observable { 167 | return this._beforeClose$.asObservable(); 168 | } 169 | 170 | public afterClosed(): Observable { 171 | return this._afterClosed$.asObservable(); 172 | } 173 | 174 | /** Fetches the position strategy object from the overlay ref. */ 175 | private getPositionStrategy(): GlobalPositionStrategy { 176 | return this.overlayRef.getConfig().positionStrategy as GlobalPositionStrategy; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "cache": { 6 | "enabled": true, 7 | "path": ".cache", 8 | "environment": "all" 9 | }, 10 | "analytics": false 11 | }, 12 | "newProjectRoot": "projects", 13 | "projects": { 14 | "date-time-picker-app": { 15 | "root": "", 16 | "sourceRoot": "src", 17 | "projectType": "application", 18 | "prefix": "owl", 19 | "schematics": { 20 | "@schematics/angular:component": { 21 | "style": "scss" 22 | } 23 | }, 24 | "architect": { 25 | "build": { 26 | "builder": "@angular/build:application", 27 | "options": { 28 | "allowedCommonJsDependencies": [ 29 | "moment-timezone" 30 | ], 31 | "aot": true, 32 | "outputPath": { 33 | "base": "dist/date-time-picker-app" 34 | }, 35 | "index": "src/index.html", 36 | "polyfills": [ 37 | "src/polyfills.ts" 38 | ], 39 | "tsConfig": "src/tsconfig.app.json", 40 | "assets": [ 41 | "src/favicon.ico", 42 | "src/assets" 43 | ], 44 | "styles": [ 45 | "src/styles.scss" 46 | ], 47 | "scripts": [], 48 | "browser": "src/main.ts" 49 | }, 50 | "configurations": { 51 | "production": { 52 | "fileReplacements": [ 53 | { 54 | "replace": "src/environments/environment.ts", 55 | "with": "src/environments/environment.prod.ts" 56 | } 57 | ], 58 | "optimization": true, 59 | "outputHashing": "all", 60 | "sourceMap": false, 61 | "namedChunks": false, 62 | "aot": true, 63 | "extractLicenses": true, 64 | "budgets": [ 65 | { 66 | "type": "initial", 67 | "maximumWarning": "2mb", 68 | "maximumError": "5mb" 69 | }, 70 | { 71 | "type": "anyComponentStyle", 72 | "maximumWarning": "6kb" 73 | } 74 | ] 75 | } 76 | } 77 | }, 78 | "serve": { 79 | "builder": "@angular/build:dev-server", 80 | "options": { 81 | "buildTarget": "date-time-picker-app:build" 82 | }, 83 | "configurations": { 84 | "production": { 85 | "buildTarget": "date-time-picker-app:build:production" 86 | } 87 | } 88 | }, 89 | "extract-i18n": { 90 | "builder": "@angular-devkit/build-angular:extract-i18n", 91 | "options": { 92 | "buildTarget": "date-time-picker-app:build" 93 | } 94 | }, 95 | "test": { 96 | "builder": "@angular-devkit/build-angular:karma", 97 | "options": { 98 | "main": "src/test.ts", 99 | "polyfills": "src/polyfills.ts", 100 | "tsConfig": "src/tsconfig.spec.json", 101 | "karmaConfig": "src/karma.conf.js", 102 | "styles": [ 103 | "src/styles.scss" 104 | ], 105 | "scripts": [], 106 | "assets": [ 107 | "src/favicon.ico", 108 | "src/assets" 109 | ] 110 | } 111 | }, 112 | "lint": { 113 | "builder": "@angular-devkit/build-angular:tslint", 114 | "options": { 115 | "tsConfig": [ 116 | "src/tsconfig.app.json", 117 | "src/tsconfig.spec.json" 118 | ], 119 | "exclude": [ 120 | "**/node_modules/**" 121 | ] 122 | } 123 | } 124 | } 125 | }, 126 | "date-time-picker-app-e2e": { 127 | "root": "e2e/", 128 | "projectType": "application", 129 | "prefix": "", 130 | "architect": { 131 | "e2e": { 132 | "builder": "@angular-devkit/build-angular:protractor", 133 | "options": { 134 | "protractorConfig": "e2e/protractor.conf.js", 135 | "devServerTarget": "date-time-picker-app:serve" 136 | }, 137 | "configurations": { 138 | "production": { 139 | "devServerTarget": "date-time-picker-app:serve:production" 140 | } 141 | } 142 | }, 143 | "lint": { 144 | "builder": "@angular-devkit/build-angular:tslint", 145 | "options": { 146 | "tsConfig": "e2e/tsconfig.e2e.json", 147 | "exclude": [ 148 | "**/node_modules/**" 149 | ] 150 | } 151 | } 152 | } 153 | }, 154 | "picker": { 155 | "root": "projects/picker", 156 | "sourceRoot": "projects/picker/src", 157 | "projectType": "library", 158 | "prefix": "owl", 159 | "architect": { 160 | "build": { 161 | "builder": "@angular-devkit/build-angular:ng-packagr", 162 | "options": { 163 | "tsConfig": "projects/picker/tsconfig.lib.json", 164 | "project": "projects/picker/ng-package.json" 165 | }, 166 | "configurations": { 167 | "production": { 168 | "tsConfig": "projects/picker/tsconfig.lib.prod.json" 169 | } 170 | } 171 | }, 172 | "test": { 173 | "builder": "@angular-devkit/build-angular:karma", 174 | "options": { 175 | "main": "projects/picker/src/test.ts", 176 | "tsConfig": "projects/picker/tsconfig.spec.json", 177 | "karmaConfig": "projects/picker/karma.conf.js" 178 | } 179 | }, 180 | "lint": { 181 | "builder": "@angular-devkit/build-angular:tslint", 182 | "options": { 183 | "tsConfig": [ 184 | "projects/picker/tsconfig.lib.json", 185 | "projects/picker/tsconfig.spec.json" 186 | ], 187 | "exclude": [ 188 | "**/node_modules/**" 189 | ] 190 | } 191 | } 192 | } 193 | } 194 | }, 195 | "schematics": { 196 | "@schematics/angular:component": { 197 | "type": "component" 198 | }, 199 | "@schematics/angular:directive": { 200 | "type": "directive" 201 | }, 202 | "@schematics/angular:service": { 203 | "type": "service" 204 | }, 205 | "@schematics/angular:guard": { 206 | "typeSeparator": "." 207 | }, 208 | "@schematics/angular:interceptor": { 209 | "typeSeparator": "." 210 | }, 211 | "@schematics/angular:module": { 212 | "typeSeparator": "." 213 | }, 214 | "@schematics/angular:pipe": { 215 | "typeSeparator": "." 216 | }, 217 | "@schematics/angular:resolver": { 218 | "typeSeparator": "." 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | "env": { 16 | "browser": true, 17 | "node": true 18 | }, 19 | "parser": "@typescript-eslint/parser", 20 | "parserOptions": { 21 | "project": "tsconfig.json", 22 | "sourceType": "module" 23 | }, 24 | "plugins": [ 25 | "eslint-plugin-import", 26 | "eslint-plugin-jsdoc", 27 | "@angular-eslint/eslint-plugin", 28 | "@typescript-eslint", 29 | "@typescript-eslint/tslint" 30 | ], 31 | "root": true, 32 | "rules": { 33 | "@angular-eslint/component-class-suffix": "error", 34 | "@angular-eslint/directive-class-suffix": "error", 35 | "@angular-eslint/no-host-metadata-property": "error", 36 | "@angular-eslint/no-input-rename": "error", 37 | "@angular-eslint/no-inputs-metadata-property": "error", 38 | "@angular-eslint/no-output-on-prefix": "error", 39 | "@angular-eslint/no-output-rename": "error", 40 | "@angular-eslint/no-outputs-metadata-property": "error", 41 | "@angular-eslint/use-lifecycle-interface": "error", 42 | "@angular-eslint/use-pipe-transform-interface": "error", 43 | "@typescript-eslint/consistent-type-definitions": "error", 44 | "@typescript-eslint/dot-notation": "off", 45 | "@typescript-eslint/explicit-member-accessibility": [ 46 | "off", 47 | { 48 | "accessibility": "explicit" 49 | } 50 | ], 51 | "@typescript-eslint/indent": "error", 52 | "@typescript-eslint/member-delimiter-style": [ 53 | "error", 54 | { 55 | "multiline": { 56 | "delimiter": "semi", 57 | "requireLast": true 58 | }, 59 | "singleline": { 60 | "delimiter": "semi", 61 | "requireLast": false 62 | } 63 | } 64 | ], 65 | "@typescript-eslint/member-ordering": "error", 66 | "@typescript-eslint/naming-convention": [ 67 | "error", 68 | { 69 | "selector": "variable", 70 | "format": [ 71 | "camelCase", 72 | "UPPER_CASE" 73 | ], 74 | "leadingUnderscore": "forbid", 75 | "trailingUnderscore": "forbid" 76 | } 77 | ], 78 | "@typescript-eslint/no-empty-function": "off", 79 | "@typescript-eslint/no-empty-interface": "error", 80 | "@typescript-eslint/no-inferrable-types": [ 81 | "error", 82 | { 83 | "ignoreParameters": true 84 | } 85 | ], 86 | "@typescript-eslint/no-misused-new": "error", 87 | "@typescript-eslint/no-non-null-assertion": "error", 88 | "@typescript-eslint/no-shadow": [ 89 | "error", 90 | { 91 | "hoist": "all" 92 | } 93 | ], 94 | "@typescript-eslint/no-unused-expressions": "error", 95 | "@typescript-eslint/prefer-function-type": "error", 96 | "@typescript-eslint/quotes": [ 97 | "error", 98 | "single" 99 | ], 100 | "@typescript-eslint/semi": [ 101 | "error", 102 | "always" 103 | ], 104 | "@typescript-eslint/type-annotation-spacing": "error", 105 | "@typescript-eslint/unified-signatures": "error", 106 | "arrow-body-style": "error", 107 | "brace-style": [ 108 | "error", 109 | "1tbs" 110 | ], 111 | "constructor-super": "error", 112 | "curly": "error", 113 | "dot-notation": "off", 114 | "eol-last": "error", 115 | "eqeqeq": [ 116 | "error", 117 | "smart" 118 | ], 119 | "guard-for-in": "error", 120 | "id-denylist": "off", 121 | "id-match": "off", 122 | "import/no-deprecated": "warn", 123 | "indent": "off", 124 | "jsdoc/no-types": "error", 125 | "max-len": [ 126 | "error", 127 | { 128 | "code": 140 129 | } 130 | ], 131 | "no-bitwise": "error", 132 | "no-caller": "error", 133 | "no-console": [ 134 | "error", 135 | { 136 | "allow": [ 137 | "log", 138 | "warn", 139 | "error", 140 | "dir", 141 | "timeLog", 142 | "assert", 143 | "clear", 144 | "count", 145 | "countReset", 146 | "group", 147 | "groupEnd", 148 | "table", 149 | "dirxml", 150 | "groupCollapsed", 151 | "Console", 152 | "profile", 153 | "profileEnd", 154 | "timeStamp", 155 | "context", 156 | "createTask" 157 | ] 158 | } 159 | ], 160 | "no-debugger": "error", 161 | "no-empty": "off", 162 | "no-empty-function": "off", 163 | "no-eval": "error", 164 | "no-fallthrough": "error", 165 | "no-new-wrappers": "error", 166 | "no-restricted-imports": [ 167 | "error", 168 | "rxjs/Rx" 169 | ], 170 | "no-shadow": "off", 171 | "no-throw-literal": "error", 172 | "no-trailing-spaces": "error", 173 | "no-undef-init": "error", 174 | "no-underscore-dangle": "off", 175 | "no-unused-expressions": "off", 176 | "no-unused-labels": "error", 177 | "no-var": "error", 178 | "prefer-const": "error", 179 | "quotes": "off", 180 | "radix": "error", 181 | "semi": "off", 182 | "spaced-comment": [ 183 | "error", 184 | "always", 185 | { 186 | "markers": [ 187 | "/" 188 | ] 189 | } 190 | ], 191 | "@typescript-eslint/tslint/config": [ 192 | "error", 193 | { 194 | "rules": { 195 | "import-spacing": true, 196 | "whitespace": [ 197 | true, 198 | "check-branch", 199 | "check-decl", 200 | "check-operator", 201 | "check-separator", 202 | "check-type" 203 | ] 204 | } 205 | } 206 | ] 207 | } 208 | }; 209 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time.class 3 | */ 4 | import {EventEmitter, Inject, Input, Optional, Directive} from '@angular/core'; 5 | import { 6 | coerceBooleanProperty, 7 | coerceNumberProperty 8 | } from '@angular/cdk/coercion'; 9 | import {DateTimeAdapter} from './adapter/date-time-adapter.class'; 10 | import { 11 | OWL_DATE_TIME_FORMATS, 12 | OwlDateTimeFormats 13 | } from './adapter/date-time-format.class'; 14 | 15 | let nextUniqueId = 0; 16 | 17 | export type PickerType = 'both' | 'calendar' | 'timer'; 18 | 19 | export type PickerMode = 'popup' | 'dialog' | 'inline'; 20 | 21 | export type SelectMode = 'single' | 'range' | 'rangeFrom' | 'rangeTo'; 22 | 23 | export enum DateView { 24 | MONTH = 'month', 25 | YEAR = 'year', 26 | MULTI_YEARS = 'multi-years' 27 | } 28 | 29 | export type DateViewType = DateView.MONTH | DateView.YEAR | DateView.MULTI_YEARS; 30 | 31 | @Directive() 32 | export abstract class OwlDateTime { 33 | /** 34 | * Whether to show the second's timer 35 | */ 36 | private _showSecondsTimer = false; 37 | @Input() 38 | get showSecondsTimer(): boolean { 39 | return this._showSecondsTimer; 40 | } 41 | 42 | set showSecondsTimer(val: boolean) { 43 | this._showSecondsTimer = coerceBooleanProperty(val); 44 | } 45 | 46 | /** 47 | * Whether the timer is in hour12 format 48 | */ 49 | private _hour12Timer = false; 50 | @Input() 51 | get hour12Timer(): boolean { 52 | return this._hour12Timer; 53 | } 54 | 55 | set hour12Timer(val: boolean) { 56 | this._hour12Timer = coerceBooleanProperty(val); 57 | } 58 | 59 | /** 60 | * The view that the calendar should start in. 61 | */ 62 | @Input() 63 | startView: DateViewType = DateView.MONTH; 64 | 65 | /** 66 | * Whether to show calendar weeks in the calendar 67 | * */ 68 | @Input() 69 | showCalendarWeeks = false; 70 | 71 | /** 72 | * Whether to should only the year and multi-year views. 73 | */ 74 | @Input() 75 | yearOnly = false; 76 | 77 | /** 78 | * Whether to should only the multi-year view. 79 | */ 80 | @Input() 81 | multiyearOnly = false; 82 | 83 | /** 84 | * Hours to change per step 85 | */ 86 | private _stepHour = 1; 87 | @Input() 88 | get stepHour(): number { 89 | return this._stepHour; 90 | } 91 | 92 | set stepHour(val: number) { 93 | this._stepHour = coerceNumberProperty(val, 1); 94 | } 95 | 96 | /** 97 | * Minutes to change per step 98 | */ 99 | private _stepMinute = 1; 100 | @Input() 101 | get stepMinute(): number { 102 | return this._stepMinute; 103 | } 104 | 105 | set stepMinute(val: number) { 106 | this._stepMinute = coerceNumberProperty(val, 1); 107 | } 108 | 109 | /** 110 | * Seconds to change per step 111 | */ 112 | private _stepSecond = 1; 113 | @Input() 114 | get stepSecond(): number { 115 | return this._stepSecond; 116 | } 117 | 118 | set stepSecond(val: number) { 119 | this._stepSecond = coerceNumberProperty(val, 1); 120 | } 121 | 122 | /** 123 | * Set the first day of week 124 | */ 125 | private _firstDayOfWeek: number; 126 | @Input() 127 | get firstDayOfWeek() { 128 | return this._firstDayOfWeek; 129 | } 130 | 131 | set firstDayOfWeek(value: number) { 132 | value = coerceNumberProperty(value); 133 | if (value > 6 || value < 0) { 134 | this._firstDayOfWeek = undefined; 135 | } else { 136 | this._firstDayOfWeek = value; 137 | } 138 | } 139 | 140 | /** 141 | * Whether to hide dates in other months at the start or end of the current month. 142 | */ 143 | private _hideOtherMonths = false; 144 | @Input() 145 | get hideOtherMonths(): boolean { 146 | return this._hideOtherMonths; 147 | } 148 | 149 | set hideOtherMonths(val: boolean) { 150 | this._hideOtherMonths = coerceBooleanProperty(val); 151 | } 152 | 153 | private readonly _id: string; 154 | get id(): string { 155 | return this._id; 156 | } 157 | 158 | abstract get selected(): T | null; 159 | 160 | abstract get selecteds(): T[] | null; 161 | 162 | abstract get dateTimeFilter(): (date: T | null) => boolean; 163 | 164 | abstract get maxDateTime(): T | null; 165 | 166 | abstract get minDateTime(): T | null; 167 | 168 | abstract get selectMode(): SelectMode; 169 | 170 | abstract get startAt(): T | null; 171 | 172 | abstract get endAt(): T | null; 173 | 174 | abstract get opened(): boolean; 175 | 176 | abstract get pickerMode(): PickerMode; 177 | 178 | abstract get pickerType(): PickerType; 179 | 180 | abstract get isInSingleMode(): boolean; 181 | 182 | abstract get isInRangeMode(): boolean; 183 | 184 | abstract select(date: T | T[]): void; 185 | 186 | abstract yearSelected: EventEmitter; 187 | 188 | abstract monthSelected: EventEmitter; 189 | 190 | abstract dateSelected: EventEmitter; 191 | 192 | abstract selectYear(normalizedYear: T): void; 193 | 194 | abstract selectMonth(normalizedMonth: T): void; 195 | 196 | abstract selectDate(normalizedDate: T): void; 197 | 198 | get formatString(): string { 199 | return this.pickerType === 'both' 200 | ? this.dateTimeFormats.fullPickerInput 201 | : this.pickerType === 'calendar' 202 | ? this.dateTimeFormats.datePickerInput 203 | : this.dateTimeFormats.timePickerInput; 204 | } 205 | 206 | /** 207 | * Date Time Checker to check if the give dateTime is selectable 208 | */ 209 | public dateTimeChecker = (dateTime: T) => { 210 | return ( 211 | !!dateTime && 212 | (!this.dateTimeFilter || this.dateTimeFilter(dateTime)) && 213 | (!this.minDateTime || 214 | this.dateTimeAdapter.compare(dateTime, this.minDateTime) >= 215 | 0) && 216 | (!this.maxDateTime || 217 | this.dateTimeAdapter.compare(dateTime, this.maxDateTime) <= 0) 218 | ); 219 | }; 220 | 221 | get disabled(): boolean { 222 | return false; 223 | } 224 | 225 | protected constructor( 226 | @Optional() protected dateTimeAdapter: DateTimeAdapter, 227 | @Optional() 228 | @Inject(OWL_DATE_TIME_FORMATS) 229 | protected dateTimeFormats: OwlDateTimeFormats 230 | ) { 231 | if (!this.dateTimeAdapter) { 232 | throw Error( 233 | `OwlDateTimePicker: No provider found for DateTimeAdapter. You must import one of the following ` + 234 | `modules at your application root: OwlNativeDateTimeModule, OwlMomentDateTimeModule, or provide a ` + 235 | `custom implementation.` 236 | ); 237 | } 238 | 239 | if (!this.dateTimeFormats) { 240 | throw Error( 241 | `OwlDateTimePicker: No provider found for OWL_DATE_TIME_FORMATS. You must import one of the following ` + 242 | `modules at your application root: OwlNativeDateTimeModule, OwlMomentDateTimeModule, or provide a ` + 243 | `custom implementation.` 244 | ); 245 | } 246 | 247 | this._id = `owl-dt-picker-${nextUniqueId++}`; 248 | } 249 | 250 | protected getValidDate(obj: any): T | null { 251 | return this.dateTimeAdapter.isDateInstance(obj) && 252 | this.dateTimeAdapter.isValid(obj) 253 | ? obj 254 | : null; 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog-container.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * dialog-container.component 3 | */ 4 | 5 | import { 6 | Component, 7 | ComponentRef, 8 | ElementRef, 9 | EmbeddedViewRef, 10 | EventEmitter, 11 | Inject, 12 | OnInit, 13 | Optional, 14 | signal, 15 | ViewChild, 16 | } from '@angular/core'; 17 | import { DOCUMENT } from '@angular/common'; 18 | import { FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y'; 19 | import { 20 | BasePortalOutlet, 21 | CdkPortalOutlet, 22 | ComponentPortal, 23 | TemplatePortal, 24 | } from '@angular/cdk/portal'; 25 | import { OwlDateTimeContainerComponent } from '../date-time/date-time-picker-container.component'; 26 | import { IDateTimePickerAnimationEvent } from '../date-time/date-time-picker-animation-event'; 27 | import { OwlDialogConfigInterface } from './dialog-config.class'; 28 | 29 | @Component({ 30 | selector: 'owl-dialog-container', 31 | templateUrl: './dialog-container.component.html', 32 | standalone: false, 33 | host: { 34 | '[class.owl-dialog-container]': 'owlDialogContainerClass', 35 | '[attr.tabindex]': 'owlDialogContainerTabIndex', 36 | '[attr.id]': 'owlDialogContainerId', 37 | '[attr.role]': 'owlDialogContainerRole', 38 | '[attr.aria-labelledby]': 'owlDialogContainerAriaLabelledby', 39 | '[attr.aria-describedby]': 'owlDialogContainerAriaDescribedby' 40 | } 41 | }) 42 | export class OwlDialogContainerComponent extends BasePortalOutlet 43 | implements OnInit { 44 | @ViewChild(CdkPortalOutlet, { static: true }) 45 | portalOutlet: CdkPortalOutlet | null = null; 46 | 47 | /** The class that traps and manages focus within the dialog. */ 48 | private focusTrap: FocusTrap; 49 | 50 | /** ID of the element that should be considered as the dialog's label. */ 51 | public ariaLabelledBy: string | null = null; 52 | 53 | /** Emits when an animation state changes. */ 54 | public animationStateChanged = new EventEmitter(); 55 | 56 | public isAnimating = signal(false); 57 | 58 | private _config: OwlDialogConfigInterface; 59 | private _component: ComponentRef; 60 | get config(): OwlDialogConfigInterface { 61 | return this._config; 62 | } 63 | 64 | // for animation purpose 65 | private params: any = { 66 | x: '0px', 67 | y: '0px', 68 | ox: '50%', 69 | oy: '50%', 70 | scale: 0 71 | }; 72 | 73 | // A variable to hold the focused element before the dialog was open. 74 | // This would help us to refocus back to element when the dialog was closed. 75 | private elementFocusedBeforeDialogWasOpened: HTMLElement | null = null; 76 | 77 | get owlDialogContainerClass(): boolean { 78 | return true; 79 | } 80 | 81 | get owlDialogContainerTabIndex(): number { 82 | return -1; 83 | } 84 | 85 | get owlDialogContainerId(): string { 86 | return this._config.id; 87 | } 88 | 89 | get owlDialogContainerRole(): string { 90 | return this._config.role || null; 91 | } 92 | 93 | get owlDialogContainerAriaLabelledby(): string { 94 | return this.ariaLabelledBy; 95 | } 96 | 97 | get owlDialogContainerAriaDescribedby(): string { 98 | return this._config.ariaDescribedBy || null; 99 | } 100 | 101 | constructor( 102 | private elementRef: ElementRef, 103 | private focusTrapFactory: FocusTrapFactory, 104 | @Optional() 105 | @Inject(DOCUMENT) 106 | private document: any, 107 | ) { 108 | super(); 109 | } 110 | 111 | public ngOnInit() {} 112 | 113 | /** 114 | * Attach a ComponentPortal as content to this dialog container. 115 | */ 116 | public attachComponentPortal( 117 | portal: ComponentPortal, 118 | ): ComponentRef { 119 | if (this.portalOutlet.hasAttached()) { 120 | throw Error( 121 | 'Attempting to attach dialog content after content is already attached', 122 | ); 123 | } 124 | 125 | this.savePreviouslyFocusedElement(); 126 | const component = this.portalOutlet.attachComponentPortal(portal); 127 | const pickerContainer = component.instance as OwlDateTimeContainerComponent; 128 | pickerContainer.animationStateChanged.subscribe(state => this.onAnimationDone(state)); 129 | this._component = component; 130 | return component; 131 | } 132 | 133 | public attachTemplatePortal( 134 | portal: TemplatePortal, 135 | ): EmbeddedViewRef { 136 | throw new Error('Method not implemented.'); 137 | } 138 | 139 | public setConfig(config: OwlDialogConfigInterface): void { 140 | this._config = config; 141 | 142 | if (config.event) { 143 | this.calculateZoomOrigin(event); 144 | } 145 | } 146 | 147 | public onAnimationStart(event: IDateTimePickerAnimationEvent): void { 148 | this.isAnimating.set(true); 149 | this.animationStateChanged.emit(event); 150 | } 151 | 152 | public onAnimationDone(event: IDateTimePickerAnimationEvent): void { 153 | if (event.toState === 'enter') { 154 | this.trapFocus(); 155 | } else if (event.toState === 'leave') { 156 | this.restoreFocus(); 157 | } 158 | 159 | this.animationStateChanged.emit(event); 160 | this.isAnimating.set(false); 161 | } 162 | 163 | public startExitAnimation() { 164 | this._component.destroy(); 165 | } 166 | 167 | /** 168 | * Calculate origin used in the `zoomFadeInFrom()` 169 | * for animation purpose 170 | */ 171 | private calculateZoomOrigin(event: any): void { 172 | if (!event) { 173 | return; 174 | } 175 | 176 | const clientX = event.clientX; 177 | const clientY = event.clientY; 178 | 179 | const wh = window.innerWidth / 2; 180 | const hh = window.innerHeight / 2; 181 | const x = clientX - wh; 182 | const y = clientY - hh; 183 | const ox = clientX / window.innerWidth; 184 | const oy = clientY / window.innerHeight; 185 | 186 | this.params.x = `${x}px`; 187 | this.params.y = `${y}px`; 188 | this.params.ox = `${ox * 100}%`; 189 | this.params.oy = `${oy * 100}%`; 190 | this.params.scale = 0; 191 | 192 | return; 193 | } 194 | 195 | /** 196 | * Save the focused element before dialog was open 197 | */ 198 | private savePreviouslyFocusedElement(): void { 199 | if (this.document) { 200 | this.elementFocusedBeforeDialogWasOpened = this.document 201 | .activeElement as HTMLElement; 202 | 203 | Promise.resolve().then(() => this.elementRef.nativeElement.focus()); 204 | } 205 | } 206 | 207 | private trapFocus(): void { 208 | if (!this.focusTrap) { 209 | this.focusTrap = this.focusTrapFactory.create( 210 | this.elementRef.nativeElement, 211 | ); 212 | } 213 | 214 | if (this._config.autoFocus) { 215 | this.focusTrap.focusInitialElementWhenReady(); 216 | } 217 | } 218 | 219 | private restoreFocus(): void { 220 | const toFocus = this.elementFocusedBeforeDialogWasOpened; 221 | 222 | // We need the extra check, because IE can set the `activeElement` to null in some cases. 223 | if (toFocus && typeof toFocus.focus === 'function') { 224 | toFocus.focus(); 225 | } 226 | 227 | if (this.focusTrap) { 228 | this.focusTrap.destroy(); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 37 |
38 | 82 |
83 | 118 |
119 |
120 | @switch (currentView) { 121 | @case (DateView.MONTH) { 122 | 137 | } 138 | @case (DateView.YEAR) { 139 | 152 | } 153 | @case (DateView.MULTI_YEARS) { 154 | 167 | } 168 | } 169 |
170 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/date-time-adapter.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time-adapter.class 3 | */ 4 | import { Observable, Subject } from 'rxjs'; 5 | import { inject, InjectionToken, LOCALE_ID } from '@angular/core'; 6 | 7 | /** InjectionToken for date time picker that can be used to override default locale code. */ 8 | export const OWL_DATE_TIME_LOCALE = new InjectionToken( 9 | 'OWL_DATE_TIME_LOCALE', 10 | { 11 | providedIn: 'root', 12 | factory: OWL_DATE_TIME_LOCALE_FACTORY 13 | } 14 | ); 15 | 16 | /** @docs-private */ 17 | export function OWL_DATE_TIME_LOCALE_FACTORY(): string { 18 | return inject(LOCALE_ID); 19 | } 20 | 21 | /** Provider for OWL_DATE_TIME_LOCALE injection token. */ 22 | export const OWL_DATE_TIME_LOCALE_PROVIDER = { 23 | provide: OWL_DATE_TIME_LOCALE, 24 | useExisting: LOCALE_ID 25 | }; 26 | 27 | export abstract class DateTimeAdapter { 28 | /** The locale to use for all dates. */ 29 | protected locale: any; 30 | 31 | /** A stream that emits when the locale changes. */ 32 | protected _localeChanges = new Subject(); 33 | get localeChanges(): Observable { 34 | return this._localeChanges; 35 | } 36 | 37 | public firstMonthOfTheYear: number = 0; 38 | public firstDayOfTheWeek: number = 0; 39 | 40 | /** total milliseconds in a day. */ 41 | protected readonly millisecondsInDay = 86400000; 42 | 43 | /** total milliseconds in a minute. */ 44 | protected readonly milliseondsInMinute = 60000; 45 | 46 | /** 47 | * Get the year of the given date 48 | */ 49 | abstract getYear(date: T): number; 50 | 51 | /** 52 | * Get the month of the given date 53 | * 0 -- January 54 | * 11 -- December 55 | * */ 56 | abstract getMonth(date: T): number; 57 | 58 | /** 59 | * Get the day of the week of the given date 60 | * 0 -- Sunday 61 | * 6 -- Saturday 62 | * */ 63 | abstract getDay(date: T): number; 64 | 65 | /** 66 | * Get the day num of the given date 67 | */ 68 | abstract getDate(date: T): number; 69 | 70 | /** 71 | * Get the hours of the given date 72 | */ 73 | abstract getHours(date: T): number; 74 | 75 | /** 76 | * Get the minutes of the given date 77 | */ 78 | abstract getMinutes(date: T): number; 79 | 80 | /** 81 | * Get the seconds of the given date 82 | */ 83 | abstract getSeconds(date: T): number; 84 | 85 | /** 86 | * Get the milliseconds timestamp of the given date 87 | */ 88 | abstract getTime(date: T): number; 89 | 90 | /** 91 | * Gets the number of days in the month of the given date. 92 | */ 93 | abstract getNumDaysInMonth(date: T): number; 94 | 95 | /** 96 | * Get the number of calendar days between the given dates. 97 | * If dateLeft is before dateRight, it would return positive value 98 | * If dateLeft is after dateRight, it would return negative value 99 | */ 100 | abstract differenceInCalendarDays(dateLeft: T, dateRight: T): number; 101 | 102 | /** 103 | * Gets the name for the year of the given date. 104 | */ 105 | abstract getYearName(date: T): string; 106 | 107 | /** 108 | * Get a list of month names 109 | */ 110 | abstract getMonthNames(style: 'long' | 'short' | 'narrow'): string[]; 111 | 112 | /** 113 | * Get a list of week names 114 | */ 115 | abstract getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[]; 116 | 117 | /** 118 | * Gets a list of names for the dates of the month. 119 | */ 120 | abstract getDateNames(): string[]; 121 | 122 | /** 123 | * Return a Date object as a string, using the ISO standard 124 | */ 125 | abstract toIso8601(date: T): string; 126 | 127 | /** 128 | * Check if the give dates are equal 129 | */ 130 | abstract isEqual(dateLeft: T, dateRight: T): boolean; 131 | 132 | /** 133 | * Check if the give dates are the same day 134 | */ 135 | abstract isSameDay(dateLeft: T, dateRight: T): boolean; 136 | 137 | /** 138 | * Checks whether the given date is valid. 139 | */ 140 | abstract isValid(date: T): boolean; 141 | 142 | /** 143 | * Gets date instance that is not valid. 144 | */ 145 | abstract invalid(): T; 146 | 147 | /** 148 | * Checks whether the given object is considered a date instance by this DateTimeAdapter. 149 | */ 150 | abstract isDateInstance(obj: any): boolean; 151 | 152 | /** 153 | * Add the specified number of years to the given date 154 | */ 155 | abstract addCalendarYears(date: T, amount: number): T; 156 | 157 | /** 158 | * Add the specified number of months to the given date 159 | */ 160 | abstract addCalendarMonths(date: T, amount: number): T; 161 | 162 | /** 163 | * Add the specified number of days to the given date 164 | */ 165 | abstract addCalendarDays(date: T, amount: number): T; 166 | 167 | /** 168 | * Set the hours to the given date. 169 | */ 170 | abstract setHours(date: T, amount: number): T; 171 | 172 | /** 173 | * Set the minutes to the given date. 174 | */ 175 | abstract setMinutes(date: T, amount: number): T; 176 | 177 | /** 178 | * Set the seconds to the given date. 179 | */ 180 | abstract setSeconds(date: T, amount: number): T; 181 | 182 | /** 183 | * Creates a date with the given year, month, date, hour, minute and second. Does not allow over/under-flow of the 184 | * month and date. 185 | */ 186 | abstract createDate(year: number, month: number, date: number): T; 187 | abstract createDate( 188 | year: number, 189 | month: number, 190 | date: number, 191 | hours: number, 192 | minutes: number, 193 | seconds: number 194 | ): T; 195 | 196 | /** 197 | * Clone the given date 198 | */ 199 | abstract clone(date: T): T; 200 | 201 | /** 202 | * Get a new moment 203 | * */ 204 | abstract now(): T; 205 | 206 | /** 207 | * Formats a date as a string according to the given format. 208 | */ 209 | abstract format(date: T, displayFormat: any): string; 210 | 211 | /** 212 | * Parse a user-provided value to a Date Object 213 | */ 214 | abstract parse(value: any, parseFormat: any): T | null; 215 | 216 | /** 217 | * Compare two given dates 218 | * 1 if the first date is after the second, 219 | * -1 if the first date is before the second 220 | * 0 if dates are equal. 221 | * */ 222 | compare(first: T, second: T): number { 223 | if (!this.isValid(first) || !this.isValid(second)) { 224 | throw Error('JSNativeDate: Cannot compare invalid dates.'); 225 | } 226 | 227 | const dateFirst = this.clone(first); 228 | const dateSecond = this.clone(second); 229 | 230 | const diff = this.getTime(dateFirst) - this.getTime(dateSecond); 231 | 232 | if (diff < 0) { 233 | return -1; 234 | } else if (diff > 0) { 235 | return 1; 236 | } else { 237 | // Return 0 if diff is 0; return NaN if diff is NaN 238 | return diff; 239 | } 240 | } 241 | 242 | /** 243 | * Check if two given dates are in the same year 244 | * 1 if the first date's year is after the second, 245 | * -1 if the first date's year is before the second 246 | * 0 if two given dates are in the same year 247 | * */ 248 | compareYear(first: T, second: T): number { 249 | if (!this.isValid(first) || !this.isValid(second)) { 250 | throw Error('JSNativeDate: Cannot compare invalid dates.'); 251 | } 252 | 253 | const yearLeft = this.getYear(first); 254 | const yearRight = this.getYear(second); 255 | 256 | const diff = yearLeft - yearRight; 257 | 258 | if (diff < 0) { 259 | return -1; 260 | } else if (diff > 0) { 261 | return 1; 262 | } else { 263 | return 0; 264 | } 265 | } 266 | 267 | /** 268 | * Attempts to deserialize a value to a valid date object. This is different from parsing in that 269 | * deserialize should only accept non-ambiguous, locale-independent formats (e.g. a ISO 8601 270 | * string). The default implementation does not allow any deserialization, it simply checks that 271 | * the given value is already a valid date object or null. The `` will call this 272 | * method on all of it's `@Input()` properties that accept dates. It is therefore possible to 273 | * support passing values from your backend directly to these properties by overriding this method 274 | * to also deserialize the format used by your backend. 275 | */ 276 | deserialize(value: any): T | null { 277 | if ( 278 | value == null || 279 | (this.isDateInstance(value) && this.isValid(value)) 280 | ) { 281 | return value; 282 | } 283 | return this.invalid(); 284 | } 285 | 286 | /** 287 | * Sets the locale used for all dates. 288 | */ 289 | setLocale(locale: string) { 290 | this.locale = locale; 291 | this._localeChanges.next(locale); 292 | } 293 | 294 | /** 295 | * Get the locale used for all dates. 296 | * */ 297 | getLocale() { 298 | return this.locale; 299 | } 300 | 301 | /** 302 | * Clamp the given date between min and max dates. 303 | */ 304 | clampDate(date: T, min?: T | null, max?: T | null): T { 305 | if (min && this.compare(date, min) < 0) { 306 | return min; 307 | } 308 | if (max && this.compare(date, max) > 0) { 309 | return max; 310 | } 311 | return date; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/date-time-inline.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * date-time-inline.component 3 | */ 4 | 5 | import { 6 | ChangeDetectionStrategy, 7 | ChangeDetectorRef, 8 | Component, EventEmitter, 9 | forwardRef, 10 | Inject, 11 | Input, 12 | OnInit, 13 | Optional, 14 | Output, 15 | ViewChild 16 | } from '@angular/core'; 17 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 18 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; 19 | import { 20 | OwlDateTime, 21 | PickerMode, 22 | PickerType, 23 | SelectMode 24 | } from './date-time.class'; 25 | import { DateTimeAdapter } from './adapter/date-time-adapter.class'; 26 | import { 27 | OWL_DATE_TIME_FORMATS, 28 | OwlDateTimeFormats 29 | } from './adapter/date-time-format.class'; 30 | import { OwlDateTimeContainerComponent } from './date-time-picker-container.component'; 31 | 32 | export const OWL_DATETIME_VALUE_ACCESSOR: any = { 33 | provide: NG_VALUE_ACCESSOR, 34 | useExisting: forwardRef(() => OwlDateTimeInlineComponent), 35 | multi: true 36 | }; 37 | 38 | @Component({ 39 | selector: 'owl-date-time-inline', 40 | templateUrl: './date-time-inline.component.html', 41 | styleUrls: ['./date-time-inline.component.scss'], 42 | host: { 43 | '[class.owl-dt-inline]': 'owlDTInlineClass' 44 | }, 45 | standalone: false, 46 | changeDetection: ChangeDetectionStrategy.OnPush, 47 | preserveWhitespaces: false, 48 | providers: [OWL_DATETIME_VALUE_ACCESSOR] 49 | }) 50 | export class OwlDateTimeInlineComponent extends OwlDateTime 51 | implements OnInit, ControlValueAccessor { 52 | @ViewChild(OwlDateTimeContainerComponent, { static: true }) 53 | container: OwlDateTimeContainerComponent; 54 | 55 | /** 56 | * Set the type of the dateTime picker 57 | * 'both' -- show both calendar and timer 58 | * 'calendar' -- show only calendar 59 | * 'timer' -- show only timer 60 | */ 61 | private _pickerType: PickerType = 'both'; 62 | @Input() 63 | get pickerType(): PickerType { 64 | return this._pickerType; 65 | } 66 | 67 | set pickerType(val: PickerType) { 68 | if (val !== this._pickerType) { 69 | this._pickerType = val; 70 | } 71 | } 72 | 73 | private _disabled = false; 74 | @Input() 75 | get disabled(): boolean { 76 | return !!this._disabled; 77 | } 78 | 79 | set disabled(value: boolean) { 80 | this._disabled = coerceBooleanProperty(value); 81 | } 82 | 83 | private _selectMode: SelectMode = 'single'; 84 | @Input() 85 | get selectMode() { 86 | return this._selectMode; 87 | } 88 | 89 | set selectMode(mode: SelectMode) { 90 | if ( 91 | mode !== 'single' && 92 | mode !== 'range' && 93 | mode !== 'rangeFrom' && 94 | mode !== 'rangeTo' 95 | ) { 96 | throw Error('OwlDateTime Error: invalid selectMode value!'); 97 | } 98 | 99 | this._selectMode = mode; 100 | } 101 | 102 | /** The date to open the calendar to initially. */ 103 | private _startAt: T | null; 104 | @Input() 105 | get startAt(): T | null { 106 | if (this._startAt) { 107 | return this._startAt; 108 | } 109 | 110 | if (this.selectMode === 'single') { 111 | return this.value || null; 112 | } else if ( 113 | this.selectMode === 'range' || 114 | this.selectMode === 'rangeFrom' 115 | ) { 116 | return this.values[0] || null; 117 | } else if (this.selectMode === 'rangeTo') { 118 | return this.values[1] || null; 119 | } else { 120 | return null; 121 | } 122 | } 123 | 124 | set startAt(date: T | null) { 125 | this._startAt = this.getValidDate( 126 | this.dateTimeAdapter.deserialize(date) 127 | ); 128 | } 129 | 130 | /** The date to open for range calendar. */ 131 | private _endAt: T | null; 132 | @Input() 133 | get endAt(): T | null { 134 | if (this._endAt) { 135 | return this._endAt; 136 | } 137 | 138 | if (this.selectMode === 'single') { 139 | return this.value || null; 140 | } else if ( 141 | this.selectMode === 'range' || 142 | this.selectMode === 'rangeFrom' 143 | ) { 144 | return this.values[1] || null; 145 | } else { 146 | return null; 147 | } 148 | } 149 | 150 | set endAt(date: T | null) { 151 | this._endAt = this.getValidDate( 152 | this.dateTimeAdapter.deserialize(date) 153 | ); 154 | } 155 | 156 | private _dateTimeFilter: (date: T | null) => boolean; 157 | @Input('owlDateTimeFilter') 158 | get dateTimeFilter() { 159 | return this._dateTimeFilter; 160 | } 161 | 162 | set dateTimeFilter(filter: (date: T | null) => boolean) { 163 | this._dateTimeFilter = filter; 164 | } 165 | 166 | /** The minimum valid date. */ 167 | private _min: T | null; 168 | 169 | get minDateTime(): T | null { 170 | return this._min || null; 171 | } 172 | 173 | @Input('min') 174 | set minDateTime(value: T | null) { 175 | this._min = this.getValidDate(this.dateTimeAdapter.deserialize(value)); 176 | this.changeDetector.markForCheck(); 177 | } 178 | 179 | /** The maximum valid date. */ 180 | private _max: T | null; 181 | 182 | get maxDateTime(): T | null { 183 | return this._max || null; 184 | } 185 | 186 | @Input('max') 187 | set maxDateTime(value: T | null) { 188 | this._max = this.getValidDate(this.dateTimeAdapter.deserialize(value)); 189 | this.changeDetector.markForCheck(); 190 | } 191 | 192 | private _value: T | null; 193 | @Input() 194 | get value() { 195 | return this._value; 196 | } 197 | 198 | set value(value: T | null) { 199 | value = this.dateTimeAdapter.deserialize(value); 200 | value = this.getValidDate(value); 201 | this._value = value; 202 | this.selected = value; 203 | } 204 | 205 | private _values: T[] = []; 206 | @Input() 207 | get values() { 208 | return this._values; 209 | } 210 | 211 | set values(values: T[]) { 212 | if (values && values.length > 0) { 213 | values = values.map(v => { 214 | v = this.dateTimeAdapter.deserialize(v); 215 | v = this.getValidDate(v); 216 | return v ? this.dateTimeAdapter.clone(v) : null; 217 | }); 218 | this._values = [...values]; 219 | this.selecteds = [...values]; 220 | } else { 221 | this._values = []; 222 | this.selecteds = []; 223 | } 224 | } 225 | 226 | /** 227 | * Emits selected year in multi-year view 228 | * This doesn't imply a change on the selected date. 229 | * */ 230 | @Output() 231 | yearSelected = new EventEmitter(); 232 | 233 | /** 234 | * Emits selected month in year view 235 | * This doesn't imply a change on the selected date. 236 | * */ 237 | @Output() 238 | monthSelected = new EventEmitter(); 239 | 240 | /** 241 | * Emits selected date 242 | * */ 243 | @Output() 244 | dateSelected = new EventEmitter(); 245 | 246 | private _selected: T | null; 247 | get selected() { 248 | return this._selected; 249 | } 250 | 251 | set selected(value: T | null) { 252 | this._selected = value; 253 | this.changeDetector.markForCheck(); 254 | } 255 | 256 | private _selecteds: T[] = []; 257 | get selecteds() { 258 | return this._selecteds; 259 | } 260 | 261 | set selecteds(values: T[]) { 262 | this._selecteds = values; 263 | this.changeDetector.markForCheck(); 264 | } 265 | 266 | get opened(): boolean { 267 | return true; 268 | } 269 | 270 | get pickerMode(): PickerMode { 271 | return 'inline'; 272 | } 273 | 274 | get isInSingleMode(): boolean { 275 | return this._selectMode === 'single'; 276 | } 277 | 278 | get isInRangeMode(): boolean { 279 | return ( 280 | this._selectMode === 'range' || 281 | this._selectMode === 'rangeFrom' || 282 | this._selectMode === 'rangeTo' 283 | ); 284 | } 285 | 286 | get owlDTInlineClass(): boolean { 287 | return true; 288 | } 289 | 290 | private onModelChange: Function = () => { }; 291 | private onModelTouched: Function = () => { }; 292 | 293 | constructor( 294 | protected changeDetector: ChangeDetectorRef, 295 | @Optional() protected dateTimeAdapter: DateTimeAdapter, 296 | @Optional() 297 | @Inject(OWL_DATE_TIME_FORMATS) 298 | protected dateTimeFormats: OwlDateTimeFormats 299 | ) { 300 | super(dateTimeAdapter, dateTimeFormats); 301 | } 302 | 303 | public ngOnInit() { 304 | this.container.picker = this; 305 | } 306 | 307 | public writeValue(value: any): void { 308 | if (this.isInSingleMode) { 309 | this.value = value; 310 | this.container.pickerMoment = value; 311 | } else { 312 | this.values = value; 313 | this.container.pickerMoment = this._values[ 314 | this.container.activeSelectedIndex 315 | ]; 316 | } 317 | } 318 | 319 | public registerOnChange(fn: any): void { 320 | this.onModelChange = fn; 321 | } 322 | 323 | public registerOnTouched(fn: any): void { 324 | this.onModelTouched = fn; 325 | } 326 | 327 | public setDisabledState(isDisabled: boolean): void { 328 | this.disabled = isDisabled; 329 | } 330 | 331 | public select(date: T[] | T): void { 332 | if (this.disabled) { 333 | return; 334 | } 335 | 336 | if (Array.isArray(date)) { 337 | this.values = [...date]; 338 | } else { 339 | this.value = date; 340 | } 341 | this.onModelChange(date); 342 | this.onModelTouched(); 343 | } 344 | 345 | /** 346 | * Emits the selected year in multi-year view 347 | * */ 348 | public selectYear(normalizedYear: T): void { 349 | this.yearSelected.emit(normalizedYear); 350 | } 351 | 352 | /** 353 | * Emits selected month in year view 354 | * */ 355 | public selectMonth(normalizedMonth: T): void { 356 | this.monthSelected.emit(normalizedMonth); 357 | } 358 | 359 | /** 360 | * Emits the selected date 361 | * */ 362 | public selectDate(normalizedDate: T): void { 363 | this.dateSelected.emit(normalizedDate); 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/timer.component.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * timer.component 3 | */ 4 | 5 | import { 6 | ChangeDetectionStrategy, 7 | ChangeDetectorRef, 8 | Component, 9 | ElementRef, 10 | EventEmitter, 11 | Input, 12 | NgZone, 13 | OnInit, 14 | Optional, 15 | Output 16 | } from '@angular/core'; 17 | import { OwlDateTimeIntl } from './date-time-picker-intl.service'; 18 | import { DateTimeAdapter } from './adapter/date-time-adapter.class'; 19 | import { take } from 'rxjs/operators'; 20 | 21 | @Component({ 22 | exportAs: 'owlDateTimeTimer', 23 | selector: 'owl-date-time-timer', 24 | templateUrl: './timer.component.html', 25 | styleUrls: ['./timer.component.scss'], 26 | preserveWhitespaces: false, 27 | standalone: false, 28 | changeDetection: ChangeDetectionStrategy.OnPush, 29 | host: { 30 | '[class.owl-dt-timer]': 'owlDTTimerClass', 31 | '[attr.tabindex]': 'owlDTTimeTabIndex' 32 | } 33 | }) 34 | export class OwlTimerComponent implements OnInit { 35 | /** The current picker moment */ 36 | private _pickerMoment: T; 37 | @Input() 38 | get pickerMoment() { 39 | return this._pickerMoment; 40 | } 41 | 42 | set pickerMoment(value: T) { 43 | value = this.dateTimeAdapter.deserialize(value); 44 | this._pickerMoment = 45 | this.getValidDate(value) || this.dateTimeAdapter.now(); 46 | } 47 | 48 | /** The minimum selectable date time. */ 49 | private _minDateTime: T | null; 50 | @Input() 51 | get minDateTime(): T | null { 52 | return this._minDateTime; 53 | } 54 | 55 | set minDateTime(value: T | null) { 56 | value = this.dateTimeAdapter.deserialize(value); 57 | this._minDateTime = this.getValidDate(value); 58 | } 59 | 60 | /** The maximum selectable date time. */ 61 | private _maxDateTime: T | null; 62 | @Input() 63 | get maxDateTime(): T | null { 64 | return this._maxDateTime; 65 | } 66 | 67 | set maxDateTime(value: T | null) { 68 | value = this.dateTimeAdapter.deserialize(value); 69 | this._maxDateTime = this.getValidDate(value); 70 | } 71 | 72 | private isPM = false; // a flag indicates the current timer moment is in PM or AM 73 | 74 | /** 75 | * Whether to show the second's timer 76 | */ 77 | @Input() 78 | showSecondsTimer: boolean; 79 | 80 | /** 81 | * Whether the timer is in hour12 format 82 | */ 83 | @Input() 84 | hour12Timer: boolean; 85 | 86 | /** 87 | * Hours to change per step 88 | */ 89 | @Input() 90 | stepHour = 1; 91 | 92 | /** 93 | * Minutes to change per step 94 | */ 95 | @Input() 96 | stepMinute = 1; 97 | 98 | /** 99 | * Seconds to change per step 100 | */ 101 | @Input() 102 | stepSecond = 1; 103 | 104 | get hourValue(): number { 105 | return this.dateTimeAdapter.getHours(this.pickerMoment); 106 | } 107 | 108 | /** 109 | * The value would be displayed in hourBox. 110 | * We need this because the value displayed in hourBox it not 111 | * the same as the hourValue when the timer is in hour12Timer mode. 112 | * */ 113 | get hourBoxValue(): number { 114 | let hours = this.hourValue; 115 | 116 | if (!this.hour12Timer) { 117 | return hours; 118 | } else { 119 | if (hours === 0) { 120 | hours = 12; 121 | this.isPM = false; 122 | } else if (hours > 0 && hours < 12) { 123 | this.isPM = false; 124 | } else if (hours === 12) { 125 | this.isPM = true; 126 | } else if (hours > 12 && hours < 24) { 127 | hours = hours - 12; 128 | this.isPM = true; 129 | } 130 | 131 | return hours; 132 | } 133 | } 134 | 135 | get minuteValue(): number { 136 | return this.dateTimeAdapter.getMinutes(this.pickerMoment); 137 | } 138 | 139 | get secondValue(): number { 140 | return this.dateTimeAdapter.getSeconds(this.pickerMoment); 141 | } 142 | 143 | get upHourButtonLabel(): string { 144 | return this.pickerIntl.upHourLabel; 145 | } 146 | 147 | get downHourButtonLabel(): string { 148 | return this.pickerIntl.downHourLabel; 149 | } 150 | 151 | get upMinuteButtonLabel(): string { 152 | return this.pickerIntl.upMinuteLabel; 153 | } 154 | 155 | get downMinuteButtonLabel(): string { 156 | return this.pickerIntl.downMinuteLabel; 157 | } 158 | 159 | get upSecondButtonLabel(): string { 160 | return this.pickerIntl.upSecondLabel; 161 | } 162 | 163 | get downSecondButtonLabel(): string { 164 | return this.pickerIntl.downSecondLabel; 165 | } 166 | 167 | get hour12ButtonLabel(): string { 168 | return this.isPM 169 | ? this.pickerIntl.hour12PMLabel 170 | : this.pickerIntl.hour12AMLabel; 171 | } 172 | 173 | @Output() 174 | selectedChange = new EventEmitter(); 175 | 176 | get owlDTTimerClass(): boolean { 177 | return true; 178 | } 179 | 180 | get owlDTTimeTabIndex(): number { 181 | return -1; 182 | } 183 | 184 | constructor( 185 | private ngZone: NgZone, 186 | private elmRef: ElementRef, 187 | private pickerIntl: OwlDateTimeIntl, 188 | private cdRef: ChangeDetectorRef, 189 | @Optional() private dateTimeAdapter: DateTimeAdapter 190 | ) {} 191 | 192 | public ngOnInit() {} 193 | 194 | /** 195 | * Focus to the host element 196 | * */ 197 | public focus() { 198 | this.ngZone.runOutsideAngular(() => { 199 | this.ngZone.onStable 200 | .asObservable() 201 | .pipe(take(1)) 202 | .subscribe(() => { 203 | this.elmRef.nativeElement.focus(); 204 | }); 205 | }); 206 | } 207 | 208 | /** 209 | * Set the hour value via typing into timer box input 210 | * We need this to handle the hour value when the timer is in hour12 mode 211 | * */ 212 | public setHourValueViaInput(hours: number): void { 213 | if (this.hour12Timer && this.isPM && hours >= 1 && hours <= 11) { 214 | hours = hours + 12; 215 | } else if (this.hour12Timer && !this.isPM && hours === 12) { 216 | hours = 0; 217 | } 218 | 219 | this.setHourValue(hours); 220 | } 221 | 222 | public setHourValue(hours: number): void { 223 | const m = this.dateTimeAdapter.setHours(this.pickerMoment, hours); 224 | this.selectedChange.emit(m); 225 | this.cdRef.markForCheck(); 226 | } 227 | 228 | public setMinuteValue(minutes: number): void { 229 | const m = this.dateTimeAdapter.setMinutes(this.pickerMoment, minutes); 230 | this.selectedChange.emit(m); 231 | this.cdRef.markForCheck(); 232 | } 233 | 234 | public setSecondValue(seconds: number): void { 235 | const m = this.dateTimeAdapter.setSeconds(this.pickerMoment, seconds); 236 | this.selectedChange.emit(m); 237 | this.cdRef.markForCheck(); 238 | } 239 | 240 | public setMeridiem(event: any): void { 241 | this.isPM = !this.isPM; 242 | 243 | let hours = this.hourValue; 244 | if (this.isPM) { 245 | hours = hours + 12; 246 | } else { 247 | hours = hours - 12; 248 | } 249 | 250 | if (hours >= 0 && hours <= 23) { 251 | this.setHourValue(hours); 252 | } 253 | 254 | this.cdRef.markForCheck(); 255 | event.preventDefault(); 256 | } 257 | 258 | /** 259 | * Check if the up hour button is enabled 260 | */ 261 | public upHourEnabled(): boolean { 262 | return ( 263 | !this.maxDateTime || 264 | this.compareHours(this.stepHour, this.maxDateTime) < 1 265 | ); 266 | } 267 | 268 | /** 269 | * Check if the down hour button is enabled 270 | */ 271 | public downHourEnabled(): boolean { 272 | return ( 273 | !this.minDateTime || 274 | this.compareHours(-this.stepHour, this.minDateTime) > -1 275 | ); 276 | } 277 | 278 | /** 279 | * Check if the up minute button is enabled 280 | */ 281 | public upMinuteEnabled(): boolean { 282 | return ( 283 | !this.maxDateTime || 284 | this.compareMinutes(this.stepMinute, this.maxDateTime) < 1 285 | ); 286 | } 287 | 288 | /** 289 | * Check if the down minute button is enabled 290 | */ 291 | public downMinuteEnabled(): boolean { 292 | return ( 293 | !this.minDateTime || 294 | this.compareMinutes(-this.stepMinute, this.minDateTime) > -1 295 | ); 296 | } 297 | 298 | /** 299 | * Check if the up second button is enabled 300 | */ 301 | public upSecondEnabled(): boolean { 302 | return ( 303 | !this.maxDateTime || 304 | this.compareSeconds(this.stepSecond, this.maxDateTime) < 1 305 | ); 306 | } 307 | 308 | /** 309 | * Check if the down second button is enabled 310 | */ 311 | public downSecondEnabled(): boolean { 312 | return ( 313 | !this.minDateTime || 314 | this.compareSeconds(-this.stepSecond, this.minDateTime) > -1 315 | ); 316 | } 317 | 318 | /** 319 | * PickerMoment's hour value +/- certain amount and compare it to the give date 320 | * 1 is after the comparedDate 321 | * -1 is before the comparedDate 322 | * 0 is equal the comparedDate 323 | * */ 324 | private compareHours(amount: number, comparedDate: T): number { 325 | const hours = this.dateTimeAdapter.getHours(this.pickerMoment) + amount; 326 | const result = this.dateTimeAdapter.setHours(this.pickerMoment, hours); 327 | return this.dateTimeAdapter.compare(result, comparedDate); 328 | } 329 | 330 | /** 331 | * PickerMoment's minute value +/- certain amount and compare it to the give date 332 | * 1 is after the comparedDate 333 | * -1 is before the comparedDate 334 | * 0 is equal the comparedDate 335 | * */ 336 | private compareMinutes(amount: number, comparedDate: T): number { 337 | const minutes = 338 | this.dateTimeAdapter.getMinutes(this.pickerMoment) + amount; 339 | const result = this.dateTimeAdapter.setMinutes( 340 | this.pickerMoment, 341 | minutes 342 | ); 343 | return this.dateTimeAdapter.compare(result, comparedDate); 344 | } 345 | 346 | /** 347 | * PickerMoment's second value +/- certain amount and compare it to the give date 348 | * 1 is after the comparedDate 349 | * -1 is before the comparedDate 350 | * 0 is equal the comparedDate 351 | * */ 352 | private compareSeconds(amount: number, comparedDate: T): number { 353 | const seconds = 354 | this.dateTimeAdapter.getSeconds(this.pickerMoment) + amount; 355 | const result = this.dateTimeAdapter.setSeconds( 356 | this.pickerMoment, 357 | seconds 358 | ); 359 | return this.dateTimeAdapter.compare(result, comparedDate); 360 | } 361 | 362 | /** 363 | * Get a valid date object 364 | */ 365 | private getValidDate(obj: any): T | null { 366 | return this.dateTimeAdapter.isDateInstance(obj) && 367 | this.dateTimeAdapter.isValid(obj) 368 | ? obj 369 | : null; 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/adapter/unix-timestamp-adapter/unix-timestamp-date-time-adapter.class.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * unix-timestamp-date-time-adapter.class 3 | */ 4 | 5 | import {Inject, Injectable, Optional} from '@angular/core'; 6 | import {DateTimeAdapter, OWL_DATE_TIME_LOCALE} from '../date-time-adapter.class'; 7 | import {Platform} from '@angular/cdk/platform'; 8 | import {range} from '../../../utils/array.utils'; 9 | import {createDate, getNumDaysInMonth} from '../../../utils/date.utils'; 10 | import {DEFAULT_DATE_NAMES, DEFAULT_DAY_OF_WEEK_NAMES, DEFAULT_MONTH_NAMES, SUPPORTS_INTL_API} from '../../../utils/constants'; 11 | 12 | @Injectable() 13 | export class UnixTimestampDateTimeAdapter extends DateTimeAdapter { 14 | public firstMonthOfTheYear: number = 0; 15 | public firstDayOfTheWeek: number = 0; 16 | 17 | constructor( 18 | @Optional() 19 | @Inject(OWL_DATE_TIME_LOCALE) 20 | private owlDateTimeLocale: string, 21 | platform: Platform 22 | ) { 23 | super(); 24 | super.setLocale(owlDateTimeLocale); 25 | 26 | // IE does its own time zone correction, so we disable this on IE. 27 | this.useUtcForDisplay = !platform.TRIDENT; 28 | this._clampDate = platform.TRIDENT || platform.EDGE; 29 | } 30 | 31 | /** Whether to clamp the date between 1 and 9999 to avoid IE and Edge errors. */ 32 | private readonly _clampDate: boolean; 33 | 34 | /** 35 | * Whether to use `timeZone: 'utc'` with `Intl.DateTimeFormat` when formatting dates. 36 | * Without this `Intl.DateTimeFormat` sometimes chooses the wrong timeZone, which can throw off 37 | * the result. (e.g. in the en-US locale `new Date(1800, 7, 14).toLocaleDateString()` 38 | * will produce `'8/13/1800'`. 39 | */ 40 | useUtcForDisplay: boolean; 41 | 42 | /** 43 | * Strip out unicode LTR and RTL characters. Edge and IE insert these into formatted dates while 44 | * other browsers do not. We remove them to make output consistent and because they interfere with 45 | * date parsing. 46 | */ 47 | private static search_ltr_rtl_pattern = '/[\u200e\u200f]/g'; 48 | private static stripDirectionalityCharacters(str: string) { 49 | return str.replace(UnixTimestampDateTimeAdapter.search_ltr_rtl_pattern, ''); 50 | } 51 | 52 | /** 53 | * When converting Date object to string, javascript built-in functions may return wrong 54 | * results because it applies its internal DST rules. The DST rules around the world change 55 | * very frequently, and the current valid rule is not always valid in previous years though. 56 | * We work around this problem building a new Date object which has its internal UTC 57 | * representation with the local date and time. 58 | */ 59 | private static _format(dtf: Intl.DateTimeFormat, date: Date) { 60 | const d = new Date( 61 | Date.UTC( 62 | date.getFullYear(), 63 | date.getMonth(), 64 | date.getDate(), 65 | date.getHours(), 66 | date.getMinutes(), 67 | date.getSeconds(), 68 | date.getMilliseconds() 69 | ) 70 | ); 71 | return dtf.format(d); 72 | } 73 | 74 | addCalendarDays(date: number, amount: number): number { 75 | const result = new Date(date); 76 | amount = Number(amount); 77 | result.setDate(result.getDate() + amount); 78 | return result.getTime(); 79 | } 80 | 81 | addCalendarMonths(date: number, amount: number): number { 82 | const result = new Date(date); 83 | amount = Number(amount); 84 | 85 | const desiredMonth = result.getMonth() + amount; 86 | const dateWithDesiredMonth = new Date(0); 87 | dateWithDesiredMonth.setFullYear(result.getFullYear(), desiredMonth, 1); 88 | dateWithDesiredMonth.setHours(0, 0, 0, 0); 89 | 90 | const daysInMonth = this.getNumDaysInMonth(dateWithDesiredMonth.getTime()); 91 | // Set the last day of the new month 92 | // if the original date was the last day of the longer month 93 | result.setMonth(desiredMonth, Math.min(daysInMonth, result.getDate())); 94 | return result.getTime(); 95 | } 96 | 97 | addCalendarYears(date: number, amount: number): number { 98 | return this.addCalendarMonths(date, amount * 12); 99 | } 100 | 101 | clone(date: number): number { 102 | return date; 103 | } 104 | 105 | public createDate( 106 | year: number, 107 | month: number, 108 | date: number, 109 | hours: number = 0, 110 | minutes: number = 0, 111 | seconds: number = 0 112 | ): number { 113 | return createDate(year, month, date, hours, minutes, seconds).getTime(); 114 | } 115 | 116 | differenceInCalendarDays(dateLeft: number, dateRight: number): number { 117 | if (this.isValid(dateLeft) && this.isValid(dateRight)) { 118 | const dateLeftStartOfDay = this.createDate( 119 | this.getYear(dateLeft), 120 | this.getMonth(dateLeft), 121 | this.getDate(dateLeft) 122 | ); 123 | const dateRightStartOfDay = this.createDate( 124 | this.getYear(dateRight), 125 | this.getMonth(dateRight), 126 | this.getDate(dateRight) 127 | ); 128 | 129 | const timeStampLeft = 130 | this.getTime(dateLeftStartOfDay) - 131 | new Date(dateLeftStartOfDay).getTimezoneOffset() * 132 | this.milliseondsInMinute; 133 | const timeStampRight = 134 | this.getTime(dateRightStartOfDay) - 135 | new Date(dateRightStartOfDay).getTimezoneOffset() * 136 | this.milliseondsInMinute; 137 | return Math.round( 138 | (timeStampLeft - timeStampRight) / this.millisecondsInDay 139 | ); 140 | } else { 141 | return null; 142 | } 143 | } 144 | 145 | format(date: number, displayFormat: any): string { 146 | if (!this.isValid(date)) { 147 | throw Error('JSNativeDate: Cannot format invalid date.'); 148 | } 149 | 150 | const jsDate = new Date(date); 151 | 152 | if (SUPPORTS_INTL_API) { 153 | if (this._clampDate && 154 | (jsDate.getFullYear() < 1 || jsDate.getFullYear() > 9999)) { 155 | jsDate.setFullYear( 156 | Math.max(1, Math.min(9999, jsDate.getFullYear())) 157 | ); 158 | } 159 | 160 | displayFormat = {...displayFormat, timeZone: 'utc'}; 161 | const dtf = new Intl.DateTimeFormat(this.locale, displayFormat); 162 | return UnixTimestampDateTimeAdapter.stripDirectionalityCharacters(UnixTimestampDateTimeAdapter._format(dtf, jsDate)); 163 | } 164 | 165 | return UnixTimestampDateTimeAdapter.stripDirectionalityCharacters(jsDate.toDateString()); 166 | } 167 | 168 | getDate(date: number): number { 169 | return new Date(date).getDate(); 170 | } 171 | 172 | getDateNames(): string[] { 173 | if (SUPPORTS_INTL_API) { 174 | const dtf = new Intl.DateTimeFormat(this.locale, { 175 | day: 'numeric', 176 | timeZone: 'utc' 177 | }); 178 | return range(31, i => 179 | UnixTimestampDateTimeAdapter.stripDirectionalityCharacters( 180 | UnixTimestampDateTimeAdapter._format(dtf, new Date(2017, 0, i + 1)) 181 | ) 182 | ); 183 | } 184 | return DEFAULT_DATE_NAMES; 185 | } 186 | 187 | getDay(date: number): number { 188 | return new Date(date).getDay(); 189 | } 190 | 191 | getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { 192 | if (SUPPORTS_INTL_API) { 193 | const dtf = new Intl.DateTimeFormat(this.locale, { 194 | weekday: style, 195 | timeZone: 'utc' 196 | }); 197 | return range(7, i => 198 | UnixTimestampDateTimeAdapter.stripDirectionalityCharacters( 199 | UnixTimestampDateTimeAdapter._format(dtf, new Date(2017, 0, i + 1)) 200 | ) 201 | ); 202 | } 203 | 204 | return DEFAULT_DAY_OF_WEEK_NAMES[style]; 205 | } 206 | 207 | getHours(date: number): number { 208 | return new Date(date).getHours(); 209 | } 210 | 211 | getMinutes(date: number): number { 212 | return new Date(date).getMinutes(); 213 | } 214 | 215 | getMonth(date: number): number { 216 | return new Date(date).getMonth(); 217 | } 218 | 219 | getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { 220 | if (SUPPORTS_INTL_API) { 221 | const dtf = new Intl.DateTimeFormat(this.locale, { 222 | month: style, 223 | timeZone: 'utc' 224 | }); 225 | return range(12, i => 226 | UnixTimestampDateTimeAdapter.stripDirectionalityCharacters( 227 | UnixTimestampDateTimeAdapter._format(dtf, new Date(2017, i, 1)) 228 | ) 229 | ); 230 | } 231 | return DEFAULT_MONTH_NAMES[style]; 232 | } 233 | 234 | getNumDaysInMonth(date: number): number { 235 | return getNumDaysInMonth(new Date(date)); 236 | } 237 | 238 | getSeconds(date: number): number { 239 | return new Date(date).getSeconds(); 240 | } 241 | 242 | getTime(date: number): number { 243 | return date; 244 | } 245 | 246 | getYear(date: number): number { 247 | return new Date(date).getFullYear(); 248 | } 249 | 250 | getYearName(date: number): string { 251 | if (SUPPORTS_INTL_API) { 252 | const dtf = new Intl.DateTimeFormat(this.locale, { 253 | year: 'numeric', 254 | timeZone: 'utc' 255 | }); 256 | return UnixTimestampDateTimeAdapter.stripDirectionalityCharacters(UnixTimestampDateTimeAdapter._format(dtf, new Date(date))); 257 | } 258 | return String(this.getYear(date)); 259 | } 260 | 261 | invalid(): number { 262 | return NaN; 263 | } 264 | 265 | isDateInstance(obj: any): boolean { 266 | return typeof obj === 'number'; 267 | } 268 | 269 | isEqual(dateLeft: number, dateRight: number): boolean { 270 | if (this.isValid(dateLeft) && this.isValid(dateRight)) { 271 | return dateLeft === dateRight; 272 | } else { 273 | return false; 274 | } 275 | } 276 | 277 | isSameDay(dateLeft: number, dateRight: number): boolean { 278 | if (this.isValid(dateLeft) && this.isValid(dateRight)) { 279 | const dateLeftStartOfDay = new Date(dateLeft); 280 | const dateRightStartOfDay = new Date(dateRight); 281 | dateLeftStartOfDay.setHours(0, 0, 0, 0); 282 | dateRightStartOfDay.setHours(0, 0, 0, 0); 283 | return (dateLeftStartOfDay.getTime() === dateRightStartOfDay.getTime()); 284 | } else { 285 | return false; 286 | } 287 | } 288 | 289 | isValid(date: number): boolean { 290 | return (date || date === 0) && !isNaN(date); 291 | } 292 | 293 | now(): number { 294 | return new Date().getTime(); 295 | } 296 | 297 | parse(value: any, parseFormat: any): number | null { 298 | // There is no way using the native JS Date to set the parse format or locale 299 | if (typeof value === 'number') { 300 | return value; 301 | } 302 | return value ? new Date(Date.parse(value)).getTime() : null; 303 | } 304 | 305 | setHours(date: number, amount: number): number { 306 | const result = new Date(date); 307 | result.setHours(amount); 308 | return result.getTime(); 309 | } 310 | 311 | setMinutes(date: number, amount: number): number { 312 | const result = new Date(date); 313 | result.setMinutes(amount); 314 | return result.getTime(); 315 | } 316 | 317 | setSeconds(date: number, amount: number): number { 318 | const result = new Date(date); 319 | result.setSeconds(amount); 320 | return result.getTime(); 321 | } 322 | 323 | toIso8601(date: number): string { 324 | return new Date(date).toISOString(); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /projects/picker/src/lib/dialog/dialog.service.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * dialog.service 3 | */ 4 | 5 | import { 6 | ComponentRef, 7 | Inject, 8 | Injectable, 9 | InjectionToken, 10 | Injector, 11 | Optional, 12 | SkipSelf, 13 | TemplateRef 14 | } from '@angular/core'; 15 | import { Location } from '@angular/common'; 16 | import { OwlDialogConfig, OwlDialogConfigInterface } from './dialog-config.class'; 17 | import { OwlDialogRef } from './dialog-ref.class'; 18 | import { OwlDialogContainerComponent } from './dialog-container.component'; 19 | import { extendObject } from '../utils'; 20 | import { defer, Observable, Subject } from 'rxjs'; 21 | import { startWith } from 'rxjs/operators'; 22 | import { 23 | Overlay, 24 | OverlayConfig, 25 | OverlayContainer, 26 | OverlayRef, 27 | ScrollStrategy 28 | } from '@angular/cdk/overlay'; 29 | import { 30 | ComponentPortal, 31 | ComponentType, 32 | } from '@angular/cdk/portal'; 33 | 34 | export const OWL_DIALOG_DATA = new InjectionToken('OwlDialogData'); 35 | 36 | /** 37 | * Injection token that determines the scroll handling while the dialog is open. 38 | * */ 39 | export const OWL_DIALOG_SCROLL_STRATEGY = new InjectionToken< 40 | () => ScrollStrategy 41 | >('owl-dialog-scroll-strategy'); 42 | 43 | export function OWL_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY( 44 | overlay: Overlay 45 | ): () => ScrollStrategy { 46 | const fn = () => overlay.scrollStrategies.block(); 47 | return fn; 48 | } 49 | 50 | /** @docs-private */ 51 | export const OWL_DIALOG_SCROLL_STRATEGY_PROVIDER = { 52 | provide: OWL_DIALOG_SCROLL_STRATEGY, 53 | deps: [Overlay], 54 | useFactory: OWL_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY 55 | }; 56 | 57 | /** 58 | * Injection token that can be used to specify default dialog options. 59 | * */ 60 | export const OWL_DIALOG_DEFAULT_OPTIONS = new InjectionToken( 61 | 'owl-dialog-default-options' 62 | ); 63 | 64 | @Injectable() 65 | export class OwlDialogService { 66 | private ariaHiddenElements = new Map(); 67 | 68 | private _openDialogsAtThisLevel: OwlDialogRef[] = []; 69 | private _beforeOpenAtThisLevel = new Subject>(); 70 | private _afterOpenAtThisLevel = new Subject>(); 71 | private _afterAllClosedAtThisLevel = new Subject(); 72 | 73 | /** Keeps track of the currently-open dialogs. */ 74 | get openDialogs(): OwlDialogRef[] { 75 | return this.parentDialog 76 | ? this.parentDialog.openDialogs 77 | : this._openDialogsAtThisLevel; 78 | } 79 | 80 | /** Stream that emits when a dialog has been opened. */ 81 | get beforeOpen(): Subject> { 82 | return this.parentDialog 83 | ? this.parentDialog.beforeOpen 84 | : this._beforeOpenAtThisLevel; 85 | } 86 | 87 | /** Stream that emits when a dialog has been opened. */ 88 | get afterOpen(): Subject> { 89 | return this.parentDialog 90 | ? this.parentDialog.afterOpen 91 | : this._afterOpenAtThisLevel; 92 | } 93 | 94 | get _afterAllClosed(): any { 95 | const parent = this.parentDialog; 96 | return parent 97 | ? parent._afterAllClosed 98 | : this._afterAllClosedAtThisLevel; 99 | } 100 | 101 | /** 102 | * Stream that emits when all open dialog have finished closing. 103 | * Will emit on subscribe if there are no open dialogs to begin with. 104 | */ 105 | 106 | afterAllClosed: Observable<{}> = defer( 107 | () => 108 | this._openDialogsAtThisLevel.length 109 | ? this._afterAllClosed 110 | : this._afterAllClosed.pipe(startWith(undefined)) 111 | ); 112 | 113 | private readonly scrollStrategy: () => ScrollStrategy; 114 | 115 | constructor( 116 | private overlay: Overlay, 117 | private injector: Injector, 118 | @Optional() private location: Location, 119 | @Inject(OWL_DIALOG_SCROLL_STRATEGY) scrollStrategy: any, 120 | @Optional() 121 | @Inject(OWL_DIALOG_DEFAULT_OPTIONS) 122 | private defaultOptions: OwlDialogConfigInterface, 123 | @Optional() 124 | @SkipSelf() 125 | private parentDialog: OwlDialogService, 126 | private overlayContainer: OverlayContainer 127 | ) { 128 | this.scrollStrategy = scrollStrategy; 129 | if (!parentDialog && location) { 130 | location.subscribe(() => this.closeAll()); 131 | } 132 | } 133 | 134 | public open( 135 | componentOrTemplateRef: ComponentType | TemplateRef, 136 | config?: OwlDialogConfigInterface 137 | ): OwlDialogRef { 138 | config = applyConfigDefaults(config, this.defaultOptions); 139 | 140 | if (config.id && this.getDialogById(config.id)) { 141 | throw Error( 142 | `Dialog with id "${ 143 | config.id 144 | }" exists already. The dialog id must be unique.` 145 | ); 146 | } 147 | 148 | const overlayRef = this.createOverlay(config); 149 | const dialogContainer = this.attachDialogContainer(overlayRef, config); 150 | const dialogRef = this.attachDialogContent( 151 | componentOrTemplateRef, 152 | dialogContainer, 153 | overlayRef, 154 | config 155 | ); 156 | 157 | if (!this.openDialogs.length) { 158 | this.hideNonDialogContentFromAssistiveTechnology(); 159 | } 160 | 161 | this.openDialogs.push(dialogRef); 162 | dialogRef 163 | .afterClosed() 164 | .subscribe(() => this.removeOpenDialog(dialogRef)); 165 | this.beforeOpen.next(dialogRef); 166 | this.afterOpen.next(dialogRef); 167 | return dialogRef; 168 | } 169 | 170 | /** 171 | * Closes all of the currently-open dialogs. 172 | */ 173 | public closeAll(): void { 174 | let i = this.openDialogs.length; 175 | 176 | while (i--) { 177 | this.openDialogs[i].close(); 178 | } 179 | } 180 | 181 | /** 182 | * Finds an open dialog by its id. 183 | * @param id ID to use when looking up the dialog. 184 | */ 185 | public getDialogById(id: string): OwlDialogRef | undefined { 186 | return this.openDialogs.find(dialog => dialog.id === id); 187 | } 188 | 189 | private attachDialogContent( 190 | componentOrTemplateRef: ComponentType | TemplateRef, 191 | dialogContainer: OwlDialogContainerComponent, 192 | overlayRef: OverlayRef, 193 | config: OwlDialogConfigInterface 194 | ) { 195 | const dialogRef = new OwlDialogRef( 196 | overlayRef, 197 | dialogContainer, 198 | config.id, 199 | this.location 200 | ); 201 | 202 | if (config.hasBackdrop) { 203 | overlayRef.backdropClick().subscribe(() => { 204 | if (!dialogRef.disableClose) { 205 | dialogRef.close(); 206 | } 207 | }); 208 | } 209 | 210 | if (componentOrTemplateRef instanceof TemplateRef) { 211 | } else { 212 | const injector = this.createInjector( 213 | config, 214 | dialogRef, 215 | dialogContainer 216 | ); 217 | const contentRef = dialogContainer.attachComponentPortal( 218 | new ComponentPortal(componentOrTemplateRef, undefined, injector) 219 | ); 220 | dialogRef.componentInstance.set(contentRef.instance); 221 | } 222 | 223 | dialogRef 224 | .updateSize(config.width, config.height) 225 | .updatePosition(config.position); 226 | 227 | return dialogRef; 228 | } 229 | 230 | private createInjector( 231 | config: OwlDialogConfigInterface, 232 | dialogRef: OwlDialogRef, 233 | dialogContainer: OwlDialogContainerComponent 234 | ) { 235 | const userInjector = 236 | config?.viewContainerRef?.injector; 237 | const providers = [ 238 | { provide: OwlDialogRef, useValue: dialogRef }, 239 | { provide: OwlDialogContainerComponent, useValue: dialogContainer }, 240 | { provide: OWL_DIALOG_DATA, useValue: config?.data }, 241 | ]; 242 | 243 | return Injector.create({ 244 | providers, 245 | parent: userInjector || this.injector, 246 | }); 247 | } 248 | 249 | private createOverlay(config: OwlDialogConfigInterface): OverlayRef { 250 | const overlayConfig = this.getOverlayConfig(config); 251 | return this.overlay.create(overlayConfig); 252 | } 253 | 254 | private attachDialogContainer( 255 | overlayRef: OverlayRef, 256 | config: OwlDialogConfigInterface 257 | ): OwlDialogContainerComponent { 258 | const containerPortal = new ComponentPortal( 259 | OwlDialogContainerComponent, 260 | config.viewContainerRef 261 | ); 262 | const containerRef: ComponentRef< 263 | OwlDialogContainerComponent 264 | > = overlayRef.attach(containerPortal); 265 | containerRef.instance.setConfig(config); 266 | 267 | return containerRef.instance; 268 | } 269 | 270 | private getOverlayConfig(dialogConfig: OwlDialogConfigInterface): OverlayConfig { 271 | const state = new OverlayConfig({ 272 | positionStrategy: this.overlay.position().global(), 273 | scrollStrategy: 274 | dialogConfig.scrollStrategy || this.scrollStrategy(), 275 | panelClass: dialogConfig.paneClass, 276 | hasBackdrop: dialogConfig.hasBackdrop, 277 | minWidth: dialogConfig.minWidth, 278 | minHeight: dialogConfig.minHeight, 279 | maxWidth: dialogConfig.maxWidth, 280 | maxHeight: dialogConfig.maxHeight 281 | }); 282 | 283 | if (dialogConfig.backdropClass) { 284 | state.backdropClass = dialogConfig.backdropClass; 285 | } 286 | 287 | return state; 288 | } 289 | 290 | private removeOpenDialog(dialogRef: OwlDialogRef): void { 291 | const index = this._openDialogsAtThisLevel.indexOf(dialogRef); 292 | 293 | if (index > -1) { 294 | this.openDialogs.splice(index, 1); 295 | // If all the dialogs were closed, remove/restore the `aria-hidden` 296 | // to a the siblings and emit to the `afterAllClosed` stream. 297 | if (!this.openDialogs.length) { 298 | this.ariaHiddenElements.forEach((previousValue, element) => { 299 | if (previousValue) { 300 | element.setAttribute('aria-hidden', previousValue); 301 | } else { 302 | element.removeAttribute('aria-hidden'); 303 | } 304 | }); 305 | 306 | this.ariaHiddenElements.clear(); 307 | this._afterAllClosed.next(); 308 | } 309 | } 310 | } 311 | 312 | /** 313 | * Hides all of the content that isn't an overlay from assistive technology. 314 | */ 315 | private hideNonDialogContentFromAssistiveTechnology() { 316 | const overlayContainer = this.overlayContainer.getContainerElement(); 317 | 318 | // Ensure that the overlay container is attached to the DOM. 319 | if (overlayContainer.parentElement) { 320 | const siblings = overlayContainer.parentElement.children; 321 | 322 | for (let i = siblings.length - 1; i > -1; i--) { 323 | const sibling = siblings[i]; 324 | 325 | if ( 326 | sibling !== overlayContainer && 327 | sibling.nodeName !== 'SCRIPT' && 328 | sibling.nodeName !== 'STYLE' && 329 | !sibling.hasAttribute('aria-live') 330 | ) { 331 | this.ariaHiddenElements.set( 332 | sibling, 333 | sibling.getAttribute('aria-hidden') 334 | ); 335 | sibling.setAttribute('aria-hidden', 'true'); 336 | } 337 | } 338 | } 339 | } 340 | } 341 | 342 | /** 343 | * Applies default options to the dialog config. 344 | * @param config Config to be modified. 345 | * @param defaultOptions Default config setting 346 | * @returns The new configuration object. 347 | */ 348 | function applyConfigDefaults( 349 | config?: OwlDialogConfigInterface, 350 | defaultOptions?: OwlDialogConfigInterface 351 | ): OwlDialogConfig { 352 | return extendObject(new OwlDialogConfig(), config, defaultOptions); 353 | } 354 | -------------------------------------------------------------------------------- /projects/picker/src/lib/date-time/calendar-multi-year-view.component.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * calendar-multi-year-view.component.spec 3 | */ 4 | 5 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 6 | import { OwlDateTimeIntl } from './date-time-picker-intl.service'; 7 | import { OwlNativeDateTimeModule } from './adapter/native-date-time.module'; 8 | import { OwlDateTimeModule } from './date-time.module'; 9 | import { Component, DebugElement } from '@angular/core'; 10 | import { By } from '@angular/platform-browser'; 11 | import { 12 | OwlMultiYearViewComponent, 13 | } from './calendar-multi-year-view.component'; 14 | import { dispatchMouseEvent, dispatchKeyboardEvent } from '../../test-helpers'; 15 | import { 16 | DOWN_ARROW, 17 | END, 18 | HOME, 19 | LEFT_ARROW, 20 | PAGE_DOWN, 21 | PAGE_UP, 22 | RIGHT_ARROW, 23 | UP_ARROW 24 | } from '@angular/cdk/keycodes'; 25 | import { Options, OptionsTokens } from './options-provider'; 26 | 27 | const JAN = 0, 28 | FEB = 1, 29 | MAR = 2, 30 | APR = 3, 31 | MAY = 4, 32 | JUN = 5, 33 | JUL = 6, 34 | AUG = 7, 35 | SEP = 8, 36 | OCT = 9, 37 | NOV = 10, 38 | DEC = 11; 39 | 40 | const YEAR_ROWS = 7; 41 | const YEARS_PER_ROW = 3; 42 | 43 | describe('OwlMultiYearViewComponent', () => { 44 | beforeEach(() => { 45 | TestBed.configureTestingModule({ 46 | imports: [OwlNativeDateTimeModule, OwlDateTimeModule], 47 | declarations: [ 48 | StandardMultiYearViewComponent, 49 | MultiYearViewWithDateFilterComponent 50 | ], 51 | providers: [OwlDateTimeIntl, { 52 | provide: OptionsTokens.multiYear, 53 | useFactory: () => 54 | ({ 55 | yearRows: YEAR_ROWS, 56 | yearsPerRow: YEARS_PER_ROW, 57 | } as Options['multiYear']), 58 | },] 59 | }).compileComponents(); 60 | }); 61 | 62 | describe('standard multi-years view', () => { 63 | let fixture: ComponentFixture; 64 | let testComponent: StandardMultiYearViewComponent; 65 | let multiYearViewDebugElement: DebugElement; 66 | let multiYearViewElement: HTMLElement; 67 | let multiYearViewInstance: OwlMultiYearViewComponent; 68 | 69 | beforeEach(() => { 70 | fixture = TestBed.createComponent(StandardMultiYearViewComponent); 71 | fixture.detectChanges(); 72 | 73 | multiYearViewDebugElement = fixture.debugElement.query( 74 | By.directive(OwlMultiYearViewComponent) 75 | ); 76 | multiYearViewElement = multiYearViewDebugElement.nativeElement; 77 | testComponent = fixture.componentInstance; 78 | multiYearViewInstance = multiYearViewDebugElement.componentInstance; 79 | }); 80 | 81 | it('should have correct number of years', () => { 82 | const cellEls = multiYearViewElement.querySelectorAll( 83 | '.owl-dt-calendar-cell' 84 | ); 85 | expect(cellEls.length).toBe(YEARS_PER_ROW * YEAR_ROWS); 86 | }); 87 | 88 | it('should shows selected year if in same range', () => { 89 | const selectedElContent = multiYearViewElement.querySelector( 90 | '.owl-dt-calendar-cell-selected.owl-dt-calendar-cell-content' 91 | ); 92 | expect(selectedElContent.innerHTML.trim()).toBe('2020'); 93 | }); 94 | 95 | it('should NOT show selected year if in different range', () => { 96 | testComponent.selected = new Date(2040, JAN, 10); 97 | fixture.detectChanges(); 98 | 99 | const selectedElContent = multiYearViewElement.querySelector( 100 | '.owl-calendar-body-selected.owl-dt-calendar-cell-content' 101 | ); 102 | expect(selectedElContent).toBeNull(); 103 | }); 104 | 105 | it('should fire change event on cell clicked', () => { 106 | const cellDecember = multiYearViewElement.querySelector( 107 | '[aria-label="2030"]' 108 | ); 109 | dispatchMouseEvent(cellDecember, 'click'); 110 | fixture.detectChanges(); 111 | 112 | const selectedElContent = multiYearViewElement.querySelector( 113 | '.owl-dt-calendar-cell-active .owl-dt-calendar-cell-content' 114 | ); 115 | expect(selectedElContent.innerHTML.trim()).toBe('2030'); 116 | }); 117 | 118 | it('should mark active date', () => { 119 | const cell2017 = multiYearViewElement.querySelector( 120 | '[aria-label="2018"]' 121 | ); 122 | expect((cell2017 as HTMLElement).innerText.trim()).toBe('2018'); 123 | expect(cell2017.classList).toContain('owl-dt-calendar-cell-active'); 124 | }); 125 | 126 | it('should decrement year on left arrow press', () => { 127 | const calendarBodyEl = multiYearViewElement.querySelector( 128 | '.owl-dt-calendar-body' 129 | ); 130 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); 131 | fixture.detectChanges(); 132 | 133 | expect(multiYearViewInstance.pickerMoment).toEqual( 134 | new Date(2017, JAN, 5) 135 | ); 136 | 137 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', LEFT_ARROW); 138 | fixture.detectChanges(); 139 | 140 | expect(multiYearViewInstance.pickerMoment).toEqual( 141 | new Date(2016, JAN, 5) 142 | ); 143 | }); 144 | 145 | it('should increment year on right arrow press', () => { 146 | const calendarBodyEl = multiYearViewElement.querySelector( 147 | '.owl-dt-calendar-body' 148 | ); 149 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); 150 | fixture.detectChanges(); 151 | 152 | expect(multiYearViewInstance.pickerMoment).toEqual( 153 | new Date(2019, JAN, 5) 154 | ); 155 | 156 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', RIGHT_ARROW); 157 | fixture.detectChanges(); 158 | 159 | expect(multiYearViewInstance.pickerMoment).toEqual( 160 | new Date(2020, JAN, 5) 161 | ); 162 | }); 163 | 164 | it('should go up a row on up arrow press', () => { 165 | const calendarBodyEl = multiYearViewElement.querySelector( 166 | '.owl-dt-calendar-body' 167 | ); 168 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); 169 | fixture.detectChanges(); 170 | 171 | expect(multiYearViewInstance.pickerMoment).toEqual( 172 | new Date(2018 - YEARS_PER_ROW, JAN, 5) 173 | ); 174 | 175 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', UP_ARROW); 176 | fixture.detectChanges(); 177 | 178 | expect(multiYearViewInstance.pickerMoment).toEqual( 179 | new Date(2018 - YEARS_PER_ROW * 2, JAN, 5) 180 | ); 181 | }); 182 | 183 | it('should go down a row on down arrow press', () => { 184 | const calendarBodyEl = multiYearViewElement.querySelector( 185 | '.owl-dt-calendar-body' 186 | ); 187 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); 188 | fixture.detectChanges(); 189 | 190 | expect(multiYearViewInstance.pickerMoment).toEqual( 191 | new Date(2018 + YEARS_PER_ROW, JAN, 5) 192 | ); 193 | 194 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', DOWN_ARROW); 195 | fixture.detectChanges(); 196 | 197 | expect(multiYearViewInstance.pickerMoment).toEqual( 198 | new Date(2018 + YEARS_PER_ROW * 2, JAN, 5) 199 | ); 200 | }); 201 | 202 | it('should go to first year in current range on home press', () => { 203 | const calendarBodyEl = multiYearViewElement.querySelector( 204 | '.owl-dt-calendar-body' 205 | ); 206 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); 207 | fixture.detectChanges(); 208 | 209 | expect(multiYearViewInstance.pickerMoment).toEqual( 210 | new Date(2016, JAN, 5) 211 | ); 212 | 213 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', HOME); 214 | fixture.detectChanges(); 215 | 216 | expect(multiYearViewInstance.pickerMoment).toEqual( 217 | new Date(2016, JAN, 5) 218 | ); 219 | }); 220 | 221 | it('should go to last year in current range on end press', () => { 222 | const calendarBodyEl = multiYearViewElement.querySelector( 223 | '.owl-dt-calendar-body' 224 | ); 225 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); 226 | fixture.detectChanges(); 227 | 228 | expect(multiYearViewInstance.pickerMoment).toEqual( 229 | new Date(2036, JAN, 5) 230 | ); 231 | 232 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', END); 233 | fixture.detectChanges(); 234 | 235 | expect(multiYearViewInstance.pickerMoment).toEqual( 236 | new Date(2036, JAN, 5) 237 | ); 238 | }); 239 | 240 | it('should go to same index in previous year range page up press', () => { 241 | const calendarBodyEl = multiYearViewElement.querySelector( 242 | '.owl-dt-calendar-body' 243 | ); 244 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); 245 | fixture.detectChanges(); 246 | 247 | expect(multiYearViewInstance.pickerMoment).toEqual( 248 | new Date(2018 - YEARS_PER_ROW * YEAR_ROWS, JAN, 5) 249 | ); 250 | 251 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_UP); 252 | fixture.detectChanges(); 253 | 254 | expect(multiYearViewInstance.pickerMoment).toEqual( 255 | new Date(2018 - YEARS_PER_ROW * YEAR_ROWS * 2, JAN, 5) 256 | ); 257 | }); 258 | 259 | it('should go to same index in next year range on page down press', () => { 260 | const calendarBodyEl = multiYearViewElement.querySelector( 261 | '.owl-dt-calendar-body' 262 | ); 263 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); 264 | fixture.detectChanges(); 265 | 266 | expect(multiYearViewInstance.pickerMoment).toEqual( 267 | new Date(2018 + YEARS_PER_ROW * YEAR_ROWS, JAN, 5) 268 | ); 269 | 270 | dispatchKeyboardEvent(calendarBodyEl, 'keydown', PAGE_DOWN); 271 | fixture.detectChanges(); 272 | 273 | expect(multiYearViewInstance.pickerMoment).toEqual( 274 | new Date(2018 + YEARS_PER_ROW * YEAR_ROWS * 2, JAN, 5) 275 | ); 276 | }); 277 | }); 278 | 279 | describe('multi-years view with date filter', () => { 280 | let fixture: ComponentFixture; 281 | let multiYearViewElement: Element; 282 | 283 | beforeEach(() => { 284 | fixture = TestBed.createComponent( 285 | MultiYearViewWithDateFilterComponent 286 | ); 287 | fixture.detectChanges(); 288 | 289 | const multiYearViewDebugElement = fixture.debugElement.query( 290 | By.directive(OwlMultiYearViewComponent) 291 | ); 292 | multiYearViewElement = multiYearViewDebugElement.nativeElement; 293 | }); 294 | 295 | it('should disable filtered years', () => { 296 | const cell2018 = multiYearViewElement.querySelector( 297 | '[aria-label="2018"]' 298 | ); 299 | const cell2019 = multiYearViewElement.querySelector( 300 | '[aria-label="2019"]' 301 | ); 302 | expect(cell2019.classList).not.toContain( 303 | 'owl-dt-calendar-cell-disabled' 304 | ); 305 | expect(cell2018.classList).toContain( 306 | 'owl-dt-calendar-cell-disabled' 307 | ); 308 | }); 309 | }); 310 | }); 311 | 312 | @Component({ 313 | standalone: false, 314 | template: ` 315 | 319 | ` 320 | }) 321 | class StandardMultiYearViewComponent { 322 | selected = new Date(2020, JAN, 10); 323 | pickerMoment = new Date(2018, JAN, 5); 324 | 325 | handleChange(date: Date): void { 326 | this.pickerMoment = new Date(date); 327 | } 328 | } 329 | 330 | @Component({ 331 | standalone: false, 332 | template: ` 333 | 336 | ` 337 | }) 338 | class MultiYearViewWithDateFilterComponent { 339 | pickerMoment = new Date(2018, JAN, 1); 340 | dateFilter(date: Date) { 341 | return date.getFullYear() !== 2018; 342 | } 343 | } 344 | --------------------------------------------------------------------------------