├── 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 |
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 |
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 |
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 | `,
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 |
58 |
62 | {{ fromLabel }}:
65 | {{
66 | fromFormattedValue
67 | }}
68 |
69 |
70 |
82 |
86 | {{ toLabel }}:
89 | {{
90 | toFormattedValue
91 | }}
92 |
93 |
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 |
--------------------------------------------------------------------------------