├── .browserslistrc ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── bg-messages.xlf ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── mat-timepicker │ ├── LICENSE │ ├── README.md │ ├── karma.conf.js │ ├── ng-package.json │ ├── package.json │ ├── src │ ├── lib │ │ ├── clock │ │ │ ├── clock.component.html │ │ │ ├── clock.component.scss │ │ │ ├── clock.component.spec.ts │ │ │ └── clock.component.ts │ │ ├── interfaces-and-types.ts │ │ ├── mat-timepicker.module.ts │ │ ├── timepicker-dialog │ │ │ ├── timepicker-dialog.component.html │ │ │ ├── timepicker-dialog.component.scss │ │ │ ├── timepicker-dialog.component.spec.ts │ │ │ └── timepicker-dialog.component.ts │ │ ├── timepicker.directive.spec.ts │ │ ├── timepicker.directive.ts │ │ └── util.ts │ ├── public-api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tslint.json │ └── yarn.lock ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ └── app.module.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── timepicker-hours.png ├── timepicker-min.png ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.angular/cache 36 | /.sass-cache 37 | /connect.lock 38 | /coverage 39 | /libpeerconnection.log 40 | npm-debug.log 41 | yarn-error.log 42 | testem.log 43 | /typings 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ilia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Material Timepicker 2 | 3 | ## Installation 4 | * `npm install mat-timepicker` || ` yarn add mat-timepicker` 5 | 6 | ## Features: 7 | 8 | * Clock view dialog for selecting hour and minutes and options for dialog toggle customizations. 9 | * Input time editing. 10 | * Validations: minDate / maxDate (options: strict - datetime check / non-strict - time check). 11 | * The timepicker can be used with template and reactive forms. 12 | 13 | ## Configuration and usage 14 | **Keep in mind that the selector for the timepicker directive is *`input[matTimepicker]`*** 15 | 16 | your.module.ts 17 | ```typescript 18 | import { MatTimepickerModule } from 'mat-timepicker'; 19 | 20 | @NgModule({ 21 | declarations: [...], 22 | imports: [ 23 | MatTimepickerModule 24 | ], 25 | ... 26 | }) 27 | export class YourModule { } 28 | ``` 29 | 30 | ## Simple Case 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | ### More Complex Case 37 | ```html 38 | 39 | TIMEPICKER 40 | 41 | 42 | 46 | 47 | 48 | access_time 49 | 50 | 51 | Invalid Date 52 | 53 | ``` 54 | 55 | ### MatTimepicker Directive API 56 | 57 | ```typescript 58 | @Input() required = false; 59 | @Input() disabled = false; 60 | @Input() placeholder = null; 61 | 62 | /* Use a custom template for the ok button */ 63 | @Input() okButtonTemplate: TemplateRef | null = null; 64 | /* Use a custom template for the cancel button */ 65 | @Input() cancelButtonTemplate: TemplateRef | null = null; 66 | /* Where: 67 | export interface MatTimepickerButtonTemplateContext { 68 | $implicit: () => void; <--- The click handler for each the button (either okClickHandler/closeClickHandler) 69 | label: string; <--- The label that was provided to the mat-timepicker directive (either okLabel/cancelLabel) 70 | } 71 | In order to use this check out the bottom of the template driven form inside the example app 72 | */ 73 | 74 | /* Override the label of the ok button. */ 75 | @Input() okLabel = 'Ok'; 76 | 77 | /* Override the label of the cancel button. */ 78 | @Input() cancelLabel = 'Cancel'; 79 | 80 | /** Override the ante meridiem abbreviation. */ 81 | @Input() anteMeridiemAbbreviation = 'am'; 82 | 83 | /** Override the post meridiem abbreviation. */ 84 | @Input() postMeridiemAbbreviation = 'pm'; 85 | 86 | /* Sets the clock mode, 12-hour or 24-hour clocks are supported. */ 87 | @Input() mode: '12h' | '24h' = '24h'; 88 | 89 | /* Set the color of the timepicker control */ 90 | @Input() color: ThemePalette = 'primary'; 91 | 92 | /* Set the value of the timepicker control */ 93 | /* ⚠️(when using template driven forms then you should use [ngModel]="someValue")⚠️ */ 94 | @Input() value: Date = new Date(); 95 | 96 | /* Minimum time to pick from */ 97 | @Input() minDate: Date; 98 | 99 | /* Maximum time to pick from */ 100 | @Input() maxDate: Date; 101 | 102 | /* Disables the dialog open when clicking the input field */ 103 | @Input() disableDialogOpenOnClick = false; 104 | 105 | /* Input that allows you to control when the control is in an errored state */ 106 | /* (check out the demo app) */ 107 | @Input() errorStateMatcher: ErrorStateMatcher; 108 | 109 | /* Strict mode checks the full date (Day/Month/Year Hours:Minutes) when doing the minDate maxDate validation. If you need to check only the Hours:Minutes then you can set it to false */ 110 | @Input() strict = true; 111 | 112 | /* Emits when time has changed */ 113 | @Output() timeChange: EventEmitter = new EventEmitter(); 114 | 115 | /* Emits when the input is invalid */ 116 | @Output() invalidInput: EventEmitter = new EventEmitter(); 117 | ``` 118 | 119 | ### Check out the [**Demo App**](https://stackblitz.com/github/IliaIdakiev/angular-material-timepicker)! (Please note that stackblitz sometimes fails to run Angular applications properly and that doesn't mean that the library is broken) 120 | 121 | --- 122 | 123 | ## i18n (v5.0.0+) 124 | In versions before v5.0.0 putting `import '@angular/localize/init';` inside polyfills.ts was mandatory. From v5.0.0 it is no longer mandatory (which is useful for users that are not using i18n). In order to use i18n you have to use the inputs: okLabel, cancelLabel. 125 | 126 | **Please note that you need to provide both the input attribute and the value (e.g okLabel="Ok") and the i18n attribute (e.g i18n-okLabel="|@@")** for more info check out [this](https://angular.io/guide/i18n#mark-element-attributes-for-translations) 127 | 128 | Example: 129 | ```html 130 |
131 | 132 | 24 TIMEPICKER 133 | 137 | access_time 138 | Invalid Date 139 | 140 |
141 | ``` 142 | 143 | --- 144 | 145 | Dialog View 146 | 147 | Hour Select (24h): 148 | 149 | ![alt text](https://github.com/IliaIdakiev/angular-material-timepicker/blob/master/timepicker-hours.png?raw=true "Hour Select (24h)") 150 | 151 | Minutes Select: 152 | 153 | ![alt text](https://github.com/IliaIdakiev/angular-material-timepicker/blob/master/timepicker-min.png?raw=true "Hour Select (24h)") 154 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-material-timepicker": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "i18n": { 14 | "sourceLocale": "en-US", 15 | "locales": { 16 | "bg": "bg-messages.xlf" 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "app", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/angular-material-timepicker", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": "src/polyfills.ts", 30 | "tsConfig": "tsconfig.app.json", 31 | "localize": false, 32 | "assets": [ 33 | "src/favicon.ico", 34 | "src/assets" 35 | ], 36 | "styles": [ 37 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 38 | "src/styles.scss" 39 | ], 40 | "scripts": [], 41 | "vendorChunk": true, 42 | "extractLicenses": false, 43 | "buildOptimizer": false, 44 | "sourceMap": true, 45 | "optimization": false, 46 | "namedChunks": true 47 | }, 48 | "configurations": { 49 | "bg": { 50 | "localize": [ 51 | "bg" 52 | ] 53 | }, 54 | "production": { 55 | "fileReplacements": [ 56 | { 57 | "replace": "src/environments/environment.ts", 58 | "with": "src/environments/environment.prod.ts" 59 | } 60 | ], 61 | "optimization": true, 62 | "outputHashing": "all", 63 | "sourceMap": false, 64 | "namedChunks": false, 65 | "extractLicenses": true, 66 | "vendorChunk": false, 67 | "buildOptimizer": true, 68 | "budgets": [ 69 | { 70 | "type": "initial", 71 | "maximumWarning": "2mb", 72 | "maximumError": "5mb" 73 | }, 74 | { 75 | "type": "anyComponentStyle", 76 | "maximumWarning": "6kb", 77 | "maximumError": "10kb" 78 | } 79 | ] 80 | } 81 | }, 82 | "defaultConfiguration": "" 83 | }, 84 | "serve": { 85 | "builder": "@angular-devkit/build-angular:dev-server", 86 | "options": { 87 | "browserTarget": "angular-material-timepicker:build" 88 | }, 89 | "configurations": { 90 | "production": { 91 | "browserTarget": "angular-material-timepicker:build:production" 92 | }, 93 | "bg": { 94 | "browserTarget": "angular-material-timepicker:build:bg" 95 | } 96 | } 97 | }, 98 | "extract-i18n": { 99 | "builder": "@angular-devkit/build-angular:extract-i18n", 100 | "options": { 101 | "browserTarget": "angular-material-timepicker:build" 102 | } 103 | }, 104 | "test": { 105 | "builder": "@angular-devkit/build-angular:karma", 106 | "options": { 107 | "main": "src/test.ts", 108 | "polyfills": "src/polyfills.ts", 109 | "tsConfig": "tsconfig.spec.json", 110 | "karmaConfig": "karma.conf.js", 111 | "assets": [ 112 | "src/favicon.ico", 113 | "src/assets" 114 | ], 115 | "styles": [ 116 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 117 | "src/styles.scss" 118 | ], 119 | "scripts": [] 120 | } 121 | }, 122 | "e2e": { 123 | "builder": "@angular-devkit/build-angular:protractor", 124 | "options": { 125 | "protractorConfig": "e2e/protractor.conf.js", 126 | "devServerTarget": "angular-material-timepicker:serve" 127 | }, 128 | "configurations": { 129 | "production": { 130 | "devServerTarget": "angular-material-timepicker:serve:production" 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "mat-timepicker": { 137 | "projectType": "library", 138 | "root": "projects/mat-timepicker", 139 | "sourceRoot": "projects/mat-timepicker/src", 140 | "prefix": "lib", 141 | "architect": { 142 | "build": { 143 | "builder": "@angular-devkit/build-angular:ng-packagr", 144 | "options": { 145 | "tsConfig": "projects/mat-timepicker/tsconfig.lib.json", 146 | "project": "projects/mat-timepicker/ng-package.json" 147 | }, 148 | "configurations": { 149 | "production": { 150 | "tsConfig": "projects/mat-timepicker/tsconfig.lib.prod.json" 151 | } 152 | } 153 | }, 154 | "test": { 155 | "builder": "@angular-devkit/build-angular:karma", 156 | "options": { 157 | "main": "projects/mat-timepicker/src/test.ts", 158 | "tsConfig": "projects/mat-timepicker/tsconfig.spec.json", 159 | "karmaConfig": "projects/mat-timepicker/karma.conf.js" 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /bg-messages.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('angular-material-timepicker app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /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-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/angular-material-timepicker'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-material-timepicker", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build mat-timepicker && ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^14.1.1", 15 | "@angular/cdk": "^14.1.1", 16 | "@angular/common": "^14.1.1", 17 | "@angular/compiler": "^14.1.1", 18 | "@angular/core": "^14.1.1", 19 | "@angular/forms": "^14.1.1", 20 | "@angular/localize": "^14.1.1", 21 | "@angular/material": "^14.1.1", 22 | "@angular/platform-browser": "^14.1.1", 23 | "@angular/platform-browser-dynamic": "^14.1.1", 24 | "@angular/router": "^14.1.1", 25 | "rxjs": "~6.5.4", 26 | "tslib": "^2.4.0", 27 | "zone.js": "~0.11.4" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "14.1.1", 31 | "@angular/cli": "^14.1.1", 32 | "@angular/compiler-cli": "^14.1.1", 33 | "@angular/language-service": "^14.1.1", 34 | "@types/jasmine": "~3.6.0", 35 | "@types/jasminewd2": "~2.0.3", 36 | "@types/node": "^12.11.1", 37 | "codelyzer": "^6.0.0", 38 | "jasmine-core": "~3.6.0", 39 | "jasmine-spec-reporter": "~5.0.0", 40 | "karma": "~6.3.2", 41 | "karma-chrome-launcher": "~3.1.0", 42 | "karma-coverage-istanbul-reporter": "~3.0.2", 43 | "karma-jasmine": "~4.0.0", 44 | "karma-jasmine-html-reporter": "^1.5.0", 45 | "ng-packagr": "^14.1.0", 46 | "protractor": "~7.0.0", 47 | "ts-node": "~8.3.0", 48 | "tslint": "~6.1.0", 49 | "typescript": "~4.6.4" 50 | } 51 | } -------------------------------------------------------------------------------- /projects/mat-timepicker/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 ilia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /projects/mat-timepicker/README.md: -------------------------------------------------------------------------------- 1 | # Angular Material Timepicker 2 | 3 | ## Installation 4 | * `npm install mat-timepicker` || ` yarn add mat-timepicker` 5 | 6 | ## Features: 7 | 8 | * Clock view dialog for selecting hour and minutes and options for dialog toggle customizations. 9 | * Input time editing. 10 | * Validations: minDate / maxDate (options: strict - datetime check / non-strict - time check). 11 | * The timepicker can be used with template and reactive forms. 12 | 13 | ## Configuration and usage 14 | **Keep in mind that the selector for the timepicker directive is *`input[matTimepicker]`*** 15 | 16 | your.module.ts 17 | ```typescript 18 | import { MatTimepickerModule } from 'mat-timepicker'; 19 | 20 | @NgModule({ 21 | declarations: [...], 22 | imports: [ 23 | MatTimepickerModule 24 | ], 25 | ... 26 | }) 27 | export class YourModule { } 28 | ``` 29 | 30 | ## Simple Case 31 | 32 | ```html 33 | 34 | ``` 35 | 36 | ### More Complex Case 37 | ```html 38 | 39 | TIMEPICKER 40 | 41 | 42 | 46 | 47 | 48 | access_time 49 | 50 | 51 | Invalid Date 52 | 53 | ``` 54 | 55 | ### MatTimepicker Directive API 56 | 57 | ```typescript 58 | @Input() required = false; 59 | @Input() disabled = false; 60 | @Input() placeholder = null; 61 | 62 | /* Use a custom template for the ok button */ 63 | @Input() okButtonTemplate: TemplateRef | null = null; 64 | /* Use a custom template for the cancel button */ 65 | @Input() cancelButtonTemplate: TemplateRef | null = null; 66 | /* Where: 67 | export interface MatTimepickerButtonTemplateContext { 68 | $implicit: () => void; <--- The click handler for each the button (either okClickHandler/closeClickHandler) 69 | label: string; <--- The label that was provided to the mat-timepicker directive (either okLabel/cancelLabel) 70 | } 71 | In order to use this check out the bottom of the template driven form inside the example app 72 | */ 73 | 74 | /* Override the label of the ok button. */ 75 | @Input() okLabel = 'Ok'; 76 | 77 | /* Override the label of the cancel button. */ 78 | @Input() cancelLabel = 'Cancel'; 79 | 80 | /** Override the ante meridiem abbreviation. */ 81 | @Input() anteMeridiemAbbreviation = 'am'; 82 | 83 | /** Override the post meridiem abbreviation. */ 84 | @Input() postMeridiemAbbreviation = 'pm'; 85 | 86 | /* Sets the clock mode, 12-hour or 24-hour clocks are supported. */ 87 | @Input() mode: '12h' | '24h' = '24h'; 88 | 89 | /* Set the color of the timepicker control */ 90 | @Input() color: ThemePalette = 'primary'; 91 | 92 | /* Set the value of the timepicker control */ 93 | /* ⚠️(when using template driven forms then you should use [ngModel]="someValue")⚠️ */ 94 | @Input() value: Date = new Date(); 95 | 96 | /* Minimum time to pick from */ 97 | @Input() minDate: Date; 98 | 99 | /* Maximum time to pick from */ 100 | @Input() maxDate: Date; 101 | 102 | /* Disables the dialog open when clicking the input field */ 103 | @Input() disableDialogOpenOnClick = false; 104 | 105 | /* Input that allows you to control when the control is in an errored state */ 106 | /* (check out the demo app) */ 107 | @Input() errorStateMatcher: ErrorStateMatcher; 108 | 109 | /* Strict mode checks the full date (Day/Month/Year Hours:Minutes) when doing the minDate maxDate validation. If you need to check only the Hours:Minutes then you can set it to false */ 110 | @Input() strict = true; 111 | 112 | /* Emits when time has changed */ 113 | @Output() timeChange: EventEmitter = new EventEmitter(); 114 | 115 | /* Emits when the input is invalid */ 116 | @Output() invalidInput: EventEmitter = new EventEmitter(); 117 | ``` 118 | 119 | ### Check out the [**Demo App**](https://stackblitz.com/github/IliaIdakiev/angular-material-timepicker)! (Please note that stackblitz sometimes fails to run Angular applications properly and that doesn't mean that the library is broken) 120 | 121 | --- 122 | 123 | ## i18n (v5.0.0+) 124 | In versions before v5.0.0 putting `import '@angular/localize/init';` inside polyfills.ts was mandatory. From v5.0.0 it is no longer mandatory (which is useful for users that are not using i18n). In order to use i18n you have to use the inputs: okLabel, cancelLabel. 125 | 126 | **Please note that you need to provide both the input attribute and the value (e.g okLabel="Ok") and the i18n attribute (e.g i18n-okLabel="|@@")** for more info check out [this](https://angular.io/guide/i18n#mark-element-attributes-for-translations) 127 | 128 | Example: 129 | ```html 130 |
131 | 132 | 24 TIMEPICKER 133 | 137 | access_time 138 | Invalid Date 139 | 140 |
141 | ``` 142 | 143 | --- 144 | 145 | Dialog View 146 | 147 | Hour Select (24h): 148 | 149 | ![alt text](https://github.com/IliaIdakiev/angular-material-timepicker/blob/master/timepicker-hours.png?raw=true "Hour Select (24h)") 150 | 151 | Minutes Select: 152 | 153 | ![alt text](https://github.com/IliaIdakiev/angular-material-timepicker/blob/master/timepicker-min.png?raw=true "Hour Select (24h)") 154 | -------------------------------------------------------------------------------- /projects/mat-timepicker/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-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/mat-timepicker'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /projects/mat-timepicker/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/mat-timepicker", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } -------------------------------------------------------------------------------- /projects/mat-timepicker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mat-timepicker", 3 | "version": "5.1.8", 4 | "peerDependencies": { 5 | "@angular/animations": "^14.1.0", 6 | "@angular/cdk": "^14.1.3", 7 | "@angular/common": "^14.1.0", 8 | "@angular/compiler": "^14.1.0", 9 | "@angular/core": "^14.1.0", 10 | "@angular/forms": "^14.1.0", 11 | "@angular/material": "14.1.3", 12 | "@angular/platform-browser": "^14.1.0", 13 | "@angular/platform-browser-dynamic": "^14.1.0", 14 | "@angular/router": "^14.1.0", 15 | "rxjs": "~7.5.0", 16 | "zone.js": "~0.11.4" 17 | }, 18 | "author": { 19 | "name": "Iliya Idakiev", 20 | "url": "https://developers.google.com/community/experts/directory/profile/profile-iliya_idakiev" 21 | }, 22 | "homepage": "https://github.com/IliaIdakiev/angular-material-timepicker", 23 | "license": "MIT", 24 | "keywords": [ 25 | "time", 26 | "picker", 27 | "timepicker", 28 | "material timepicker", 29 | "angular material", 30 | "angular", 31 | "angular 2", 32 | "angular 3", 33 | "angular 4", 34 | "angular 5", 35 | "angular 6", 36 | "angular 7", 37 | "angular 8", 38 | "angular 9" 39 | ] 40 | } -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/clock/clock.component.html: -------------------------------------------------------------------------------- 1 |
2 |
5 |
8 | 9 | 10 | 14 | 15 |
16 | 17 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | 39 | 40 | 41 |
-------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/clock/clock.component.scss: -------------------------------------------------------------------------------- 1 | .root { 2 | width: 256px; 3 | height: 256px; 4 | cursor: default; 5 | } 6 | 7 | .circle { 8 | width: 256px; 9 | height: 256px; 10 | border-radius: 50%; 11 | position: relative; 12 | background: #ededed; 13 | cursor: pointer; 14 | } 15 | 16 | .number { 17 | width: 32px; 18 | height: 32px; 19 | border: 0px; 20 | left: calc(50% - 16px); 21 | top: calc(50% - 16px); 22 | position: absolute; 23 | text-align: center; 24 | line-height: 32px; 25 | cursor: pointer; 26 | font-size: 14px; 27 | pointer-events: none; 28 | user-select: none; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | flex-direction: column; 33 | background-color: transparent !important; 34 | background: transparent !important; 35 | box-shadow: 0px -1px 5px -200px rgba(0,0,0,1) !important; 36 | -webkit-box-shadow: 0px -1px 5px -200px rgba(0,0,0,1) !important; 37 | -moz-box-shadow: 0px -1px 5px -200px rgba(0,0,0,1) !important; 38 | 39 | &.disabled { 40 | color: rgba(1,1,1,.1); 41 | } 42 | 43 | &:not(.selected):not(.disabled) { 44 | color: rgba(0,0,0,.87); 45 | } 46 | 47 | &:not(.disabled).minute-dot { 48 | color: rgba(1, 1, 1, 0.7); 49 | &.selected { 50 | color: transparent; 51 | } 52 | } 53 | } 54 | 55 | .small-number { 56 | font-size: 12px; 57 | 58 | &:not(.selected):not(.disabled) { 59 | color: rgba(0,0,0,.67); 60 | } 61 | } 62 | 63 | .pointer-container { 64 | width: calc(50% - 20px); 65 | height: 2; 66 | position: absolute; 67 | left: 50%; 68 | top: calc(50% - 1px); 69 | transform-origin: left center; 70 | pointer-events: none; 71 | 72 | &.disabled { 73 | * { 74 | background-color: transparent; 75 | } 76 | } 77 | } 78 | 79 | .pointer { 80 | height: 1px; 81 | } 82 | 83 | .animated-pointer { 84 | transition: all 200ms ease-out; 85 | } 86 | 87 | .small-pointer { 88 | width: calc(50% - 52px); 89 | } 90 | 91 | .inner-dot { 92 | position: absolute; 93 | top: -3px; 94 | left: -4px; 95 | width: 8px; 96 | height: 8px; 97 | border-radius: 50%; 98 | box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 99 | -webkit-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 100 | -moz-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 101 | 102 | 103 | 104 | } 105 | 106 | .outer-dot { 107 | width: 32px; 108 | height: 32px; 109 | position: absolute; 110 | right: -16px; 111 | border-radius: 50%; 112 | box-sizing: content-box; 113 | box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 114 | -webkit-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 115 | -moz-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 116 | } 117 | 118 | .outer-dot-odd { 119 | width: 32px; 120 | height: 32px; 121 | display: flex; 122 | align-items: center; 123 | justify-content: center; 124 | flex-direction: column; 125 | box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 126 | -webkit-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 127 | -moz-box-shadow: 0px 3px 5px -1px rgb(0 0 0 / 0%), 0px 6px 10px 0px rgb(0 0 0 / 0%), 0px 1px 18px 0px rgb(0 0 0 / 0%) !important; 128 | } -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/clock/clock.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { ClockComponent } from './clock.component'; 4 | 5 | describe('ClockComponent', () => { 6 | let component: ClockComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ClockComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ClockComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/clock/clock.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, ChangeDetectionStrategy } from '@angular/core'; 2 | import { ClockViewType, ClockNumber, ITimeData, ClockMode } from '../interfaces-and-types'; 3 | import { isAllowed, getIsAvailabeFn } from '../util'; 4 | 5 | @Component({ 6 | selector: 'mat-clock', 7 | templateUrl: './clock.component.html', 8 | styleUrls: ['./clock.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class ClockComponent implements OnChanges { 12 | 13 | @Input() mode: ClockMode; 14 | @Input() viewType: ClockViewType; 15 | @Input() color = 'primary'; 16 | @Input() formattedValue: number; 17 | @Input() minDate: Date; 18 | @Input() maxDate: Date; 19 | @Input() isPm: boolean; 20 | @Input() formattedHours: number; 21 | @Input() minutes: number; 22 | @Output() changeEvent: EventEmitter = new EventEmitter(); 23 | @Output() unavailableSelection: EventEmitter = new EventEmitter(); 24 | @Output() invalidMeridiem: EventEmitter = new EventEmitter(); 25 | @Output() invalidSelection: EventEmitter = new EventEmitter(); 26 | @Output() clearInvalidMeridiem: EventEmitter = new EventEmitter(); 27 | 28 | @Input() allowed12HourMap = null; 29 | @Input() allowed24HourMap = null; 30 | 31 | isFormattedValueAllowed = true; 32 | 33 | isAvailableFn: ReturnType; 34 | 35 | meridiem = null; 36 | touching = false; 37 | angle: number; 38 | numbers: ClockNumber[] = []; 39 | secondaryNumbers: ClockNumber[] = []; 40 | minuteDots: ClockNumber[] = []; 41 | invalidMeridiemEmitted = true; 42 | 43 | initIsAllowedFn() { 44 | if (!this.allowed12HourMap && !this.allowed24HourMap) { return; } 45 | this.isAvailableFn = getIsAvailabeFn(this.allowed12HourMap, this.allowed24HourMap, this.mode); 46 | } 47 | 48 | isAvailable(value) { 49 | return this.isAvailableFn ? this.isAvailableFn(value, this.viewType, this.isPm, this.formattedHours) : true; 50 | } 51 | 52 | ngOnChanges(simpleChanges: SimpleChanges) { 53 | 54 | if ( 55 | simpleChanges.allowed12HourMap || 56 | simpleChanges.allowed24HourMap || 57 | (simpleChanges.mode && !simpleChanges.mode.firstChange) 58 | ) { 59 | this.initIsAllowedFn(); 60 | } 61 | 62 | this.calculateAngule(); 63 | this.setNumbers(); 64 | this.meridiem = this.isPm ? 'PM' : 'AM'; 65 | 66 | if (simpleChanges.formattedValue && (this.allowed12HourMap || this.allowed24HourMap)) { 67 | this.isFormattedValueAllowed = this.isAvailable(this.formattedValue); 68 | } 69 | 70 | const isSelectedTimeAvailable = (this.isAvailableFn) ? 71 | // when calling isAvailableFn here we should always set the viewType to minutes because we want to check the hours and the minutes 72 | this.isAvailableFn(this.minutes, 'minutes', this.isPm, this.formattedHours) : true; 73 | 74 | // if (this.mode === '24h' && this.viewType === 'minutes' && this.isAvailableFn) { 75 | // const areMinitesAvailable = this.isAvailableFn(this.minutes, 'minutes', this.isPm, this.formattedHours); 76 | // if (!areMinitesAvailable) { 77 | // if (this.minDate && this.minDate.getMinutes() > this.minutes) { 78 | // setTimeout(() => { this.changeEvent.emit({ value: this.minDate.getMinutes(), type: 'minutes' }); }); 79 | // } else { 80 | // setTimeout(() => { this.changeEvent.emit({ value: this.maxDate.getMinutes(), type: 'minutes' }); }); 81 | // } 82 | // } 83 | // } 84 | 85 | if (isSelectedTimeAvailable && this.invalidMeridiemEmitted) { 86 | this.clearInvalidMeridiem.emit(); 87 | this.invalidMeridiemEmitted = false; 88 | } 89 | 90 | this.invalidSelection.emit(!isSelectedTimeAvailable); 91 | } 92 | 93 | calculateAngule() { 94 | this.angle = this.getPointerAngle(this.formattedValue, this.viewType); 95 | } 96 | 97 | setNumbers() { 98 | if (this.viewType === 'hours') { 99 | if (this.mode === '12h') { 100 | const meridiem = this.isPm ? 'pm' : 'am'; 101 | const isAllowedFn = this.allowed12HourMap ? num => this.allowed12HourMap[meridiem][num + 1][0] : undefined; 102 | this.numbers = this.getNumbers(12, { size: 256 }, isAllowedFn); 103 | this.secondaryNumbers = []; 104 | this.minuteDots = []; 105 | } else if (this.mode === '24h') { 106 | const isAllowedFn = this.allowed24HourMap ? num => this.allowed24HourMap[num][0] : undefined; 107 | this.numbers = this.getNumbers(12, { size: 256 }, isAllowedFn); 108 | this.secondaryNumbers = this.getNumbers(12, { size: 256 - 64, start: 13 }, isAllowedFn); 109 | this.minuteDots = []; 110 | } 111 | } else { 112 | const meridiem = this.isPm ? 'pm' : 'am'; 113 | const isAllowedFn = 114 | !!this.allowed12HourMap ? num => this.allowed12HourMap[meridiem][this.formattedHours][num] : 115 | !!this.allowed24HourMap ? num => this.allowed24HourMap[this.formattedHours][num] : undefined; 116 | 117 | this.numbers = this.getNumbers(12, { size: 256, start: 5, step: 5 }, isAllowedFn); 118 | this.minuteDots = this.getNumbers(60, { size: 256, start: 13 }).map(digit => { 119 | if (digit.display <= 59) { 120 | digit.allowed = isAllowedFn ? isAllowedFn(digit.display) : true; 121 | return digit; 122 | } 123 | digit.display = digit.display - 60; 124 | digit.allowed = isAllowedFn ? isAllowedFn(digit.display) : true; 125 | return digit; 126 | }); 127 | this.secondaryNumbers = []; 128 | } 129 | } 130 | 131 | disableAnimatedPointer() { 132 | this.touching = true; 133 | } 134 | 135 | enableAnimatedPointer() { 136 | this.touching = false; 137 | } 138 | 139 | handleTouchMove = (e: any) => { 140 | e.preventDefault(); // prevent scrolling behind the clock on iOS 141 | const rect = e.target.getBoundingClientRect(); 142 | this.movePointer(e.changedTouches[0].clientX - rect.left, e.changedTouches[0].clientY - rect.top); 143 | } 144 | 145 | handleTouchEnd(e: any) { 146 | this.handleTouchMove(e); 147 | this.enableAnimatedPointer(); 148 | } 149 | 150 | handleMouseMove(e: any) { 151 | // MouseEvent.which is deprecated, but MouseEvent.buttons is not supported in Safari 152 | if ((e.buttons === 1 || e.which === 1) && this.touching) { 153 | const rect = e.target.getBoundingClientRect(); 154 | this.movePointer(e.clientX - rect.left, e.clientY - rect.top); 155 | } 156 | } 157 | 158 | handleClick(e: any) { 159 | const rect = e.target.getBoundingClientRect(); 160 | this.movePointer(e.clientX - rect.left, e.clientY - rect.top); 161 | } 162 | 163 | movePointer(x, y) { 164 | const value = this.getPointerValue(x, y, 256); 165 | if (!this.isAvailable(value)) { 166 | this.unavailableSelection.emit(); 167 | return; 168 | } 169 | if (value !== this.formattedValue) { 170 | this.changeEvent.emit({ value, type: this.viewType }); 171 | if (this.viewType !== 'minutes') { 172 | if (!this.isAvailable(value)) { 173 | if (this.minDate && this.isAvailable(value) 174 | ) { 175 | this.changeEvent.emit({ value: this.minDate.getMinutes(), type: 'minutes' }); 176 | } else if (this.maxDate && this.isAvailable(value)) { 177 | this.changeEvent.emit({ value: this.maxDate.getMinutes(), type: 'minutes' }); 178 | } 179 | } 180 | } 181 | } 182 | } 183 | 184 | getNumbers(count, { size, start = 1, step = 1 }, isAllowedFn?: (num: number) => boolean) { 185 | return Array.apply(null, Array(count)).map((_, i) => ({ 186 | display: i * step + start, 187 | translateX: (size / 2 - 20) * Math.cos(2 * Math.PI * (i - 2) / count), 188 | translateY: (size / 2 - 20) * Math.sin(2 * Math.PI * (i - 2) / count), 189 | allowed: isAllowedFn ? isAllowedFn(i) : true 190 | })); 191 | } 192 | 193 | getPointerAngle(value, mode: ClockViewType) { 194 | if (this.viewType === 'hours') { 195 | return this.mode === '12h' ? 360 / 12 * (value - 3) : 360 / 12 * (value % 12 - 3); 196 | } 197 | return 360 / 60 * (value - 15); 198 | } 199 | 200 | getPointerValue(x, y, size) { 201 | let value; 202 | let angle = Math.atan2(size / 2 - x, size / 2 - y) / Math.PI * 180; 203 | if (angle < 0) { 204 | angle = 360 + angle; 205 | } 206 | 207 | if (this.viewType === 'hours') { 208 | if (this.mode === '12h') { 209 | value = 12 - Math.round(angle * 12 / 360); 210 | return value === 0 ? 12 : value; 211 | } 212 | 213 | const radius = Math.sqrt(Math.pow(size / 2 - x, 2) + Math.pow(size / 2 - y, 2)); 214 | value = 12 - Math.round(angle * 12 / 360); 215 | if (value === 0) { value = 12; } 216 | if (radius < size / 2 - 32) { value = value === 12 ? 0 : value + 12; } 217 | return value; 218 | 219 | } 220 | 221 | value = Math.round(60 - 60 * angle / 360); 222 | return value === 60 ? 0 : value; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/interfaces-and-types.ts: -------------------------------------------------------------------------------- 1 | export type ClockViewType = 'hours' | 'minutes'; 2 | export type ClockMode = '12h' | '24h'; 3 | 4 | export interface ClockNumber { 5 | display: number; 6 | translateX: number; 7 | translateY: number; 8 | allowed: boolean; 9 | } 10 | 11 | export interface ITimeData { 12 | minutes: number; 13 | hours: number; 14 | meridiem: string; 15 | } 16 | 17 | export interface IAllowed24HourMap { [hour: number]: { [minute: number]: boolean; }; } 18 | export interface IAllowed12HourMap { 19 | am: { [hour: number]: { [minute: number]: boolean } }; 20 | pm: { [hour: number]: { [minute: number]: boolean } }; 21 | } 22 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/mat-timepicker.module.ts: -------------------------------------------------------------------------------- 1 | import { MatDialogModule } from '@angular/material/dialog'; 2 | import { MatButtonModule } from '@angular/material/button'; 3 | import { MatToolbarModule } from '@angular/material/toolbar'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | 7 | import { NgModule } from '@angular/core'; 8 | import { CommonModule } from '@angular/common'; 9 | import { ClockComponent } from './clock/clock.component'; 10 | import { MatTimepickerComponentDialogComponent } from './timepicker-dialog/timepicker-dialog.component'; 11 | import { MatTimepickerDirective } from './timepicker.directive'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | ClockComponent, 16 | MatTimepickerDirective, 17 | MatTimepickerComponentDialogComponent 18 | ], 19 | imports: [ 20 | CommonModule, 21 | MatDialogModule, 22 | MatButtonModule, 23 | MatToolbarModule, 24 | MatIconModule, 25 | MatInputModule 26 | ], 27 | exports: [ 28 | MatTimepickerDirective 29 | ] 30 | }) 31 | export class MatTimepickerModule { } 32 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 |
12 |
13 | 15 | {{ twoDigits(formattedHours) }} 16 | 17 | : 18 | 20 | {{ twoDigits(minutes) }} 21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 |
29 | {{postMeridiemAbbreviation | uppercase 30 | }} 31 | {{anteMeridiemAbbreviation | uppercase 32 | }} 33 |
34 |
35 |
36 |
37 | 45 |
46 |
47 |
48 | 49 | 51 | 52 | 54 | 55 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.scss: -------------------------------------------------------------------------------- 1 | mat-dialog-content { 2 | min-height: 395px; 3 | padding: 0px; 4 | margin-top: -24px; 5 | overflow: hidden; 6 | } 7 | 8 | mat-dialog-actions { 9 | justify-content: flex-end; 10 | margin-right: -8px; 11 | margin-left: -8px; 12 | } 13 | 14 | .root { 15 | min-width: 282px; 16 | } 17 | 18 | .header { 19 | border-top-left-radius: 2px; 20 | border-top-right-radius: 2px; 21 | padding: 20px 0; 22 | line-height: 58px; 23 | font-size: 58px; 24 | display: flex; 25 | justify-content: center; 26 | align-items: center; 27 | user-select: none; 28 | height: 98px; 29 | 30 | .fixed-font-size { 31 | font-size: 58px; 32 | } 33 | 34 | .time-frame { 35 | height: 60px; 36 | } 37 | } 38 | 39 | .time { 40 | transition: all 200ms ease-out; 41 | cursor: pointer; 42 | &:not(.select) { 43 | opacity: .6; 44 | } 45 | } 46 | 47 | .placeholder { 48 | flex: 1; 49 | } 50 | 51 | .ampm { 52 | display: flex; 53 | flex-direction: column-reverse; 54 | flex: 1; 55 | font-size: 14px; 56 | line-height: 20px; 57 | margin-left: 16px; 58 | font-weight: 700px; 59 | } 60 | 61 | .select { 62 | color: white; 63 | } 64 | 65 | .body { 66 | padding: 24px 16px; 67 | padding-bottom: 20px; 68 | display: flex; 69 | justify-content: center; 70 | } 71 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { MatTimepickerComponentDialogComponent } from './timepicker-dialog.component'; 4 | 5 | describe('TimePickerComponent', () => { 6 | let component: MatTimepickerComponentDialogComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [MatTimepickerComponentDialogComponent] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MatTimepickerComponentDialogComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/timepicker-dialog/timepicker-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { MAT_DIALOG_DATA } from '@angular/material/dialog'; 2 | import { Component, EventEmitter, Output, Inject, DoCheck, TemplateRef } from '@angular/core'; 3 | import { ClockViewType, ClockMode, IAllowed24HourMap, IAllowed12HourMap } from '../interfaces-and-types'; 4 | import { twoDigits, convertHoursForMode } from '../util'; 5 | import { MatTimepickerButtonTemplateContext } from '../timepicker.directive'; 6 | 7 | @Component({ 8 | selector: 'mat-timepicker-dialog', 9 | templateUrl: './timepicker-dialog.component.html', 10 | styleUrls: ['./timepicker-dialog.component.scss'] 11 | }) 12 | export class MatTimepickerComponentDialogComponent implements DoCheck { 13 | 14 | twoDigits = twoDigits; 15 | 16 | @Output() changeEvent: EventEmitter = new EventEmitter(); 17 | @Output() okClickEvent: EventEmitter = new EventEmitter(); 18 | @Output() cancelClickEvent: EventEmitter = new EventEmitter(); 19 | 20 | allowed24HourMap: IAllowed24HourMap = null; 21 | allowed12HourMap: IAllowed12HourMap = null; 22 | 23 | invalidSelection = false; 24 | 25 | okLabel: string; 26 | cancelLabel: string; 27 | 28 | okButtonTemplate: TemplateRef; 29 | cancelButtonTemplate: TemplateRef; 30 | 31 | anteMeridiemAbbreviation: string; 32 | postMeridiemAbbreviation: string; 33 | 34 | set value(value: any) { 35 | value = value || this.minDate || this.maxDate || new Date(); 36 | this.hours = value.getHours(); 37 | this.minutes = value.getMinutes(); 38 | this._value = value; 39 | } 40 | 41 | get value() { return this._value; } 42 | 43 | mode: ClockMode; 44 | viewType: ClockViewType = 'hours'; 45 | 46 | minutes: any; 47 | color: string; 48 | isPm = false; 49 | skipMinuteAutoSwitch = false; 50 | autoSwitchID = null; 51 | invalidMedianID = null; 52 | hasInvalidMeridiem = false; 53 | editHoursClicked = false; 54 | isClosing = false; 55 | 56 | minDate: Date; 57 | maxDate: Date; 58 | 59 | // tslint:disable-next-line:variable-name 60 | _formattedHour: any; 61 | // tslint:disable-next-line:variable-name 62 | _hours: any; 63 | // tslint:disable-next-line:variable-name 64 | _value: Date; 65 | 66 | set hours(value: any) { 67 | this._hours = value; 68 | this._formattedHour = convertHoursForMode(this.hours, this.mode).hour; 69 | } 70 | get hours() { return this._hours; } 71 | 72 | get formattedHours() { return this._formattedHour; } 73 | 74 | bindData(data: any) { 75 | this.mode = data.mode; 76 | this.okLabel = data.okLabel; 77 | this.cancelLabel = data.cancelLabel; 78 | this.okButtonTemplate = data.okButtonTemplate; 79 | this.cancelButtonTemplate = data.cancelButtonTemplate; 80 | this.anteMeridiemAbbreviation = data.anteMeridiemAbbreviation; 81 | this.postMeridiemAbbreviation = data.postMeridiemAbbreviation; 82 | this.color = data.color; 83 | this.minDate = data.minDate; 84 | this.maxDate = data.maxDate; 85 | this.allowed12HourMap = data.allowed12HourMap; 86 | this.allowed24HourMap = data.allowed24HourMap; 87 | } 88 | 89 | constructor(@Inject(MAT_DIALOG_DATA) public data) { 90 | this.isPm = data.isPm; 91 | this.bindData(data); 92 | // keep this always at the bottom 93 | this.value = data.value; 94 | } 95 | 96 | ngDoCheck() { this.bindData(this.data); } 97 | 98 | handleClockChange({ value, type }: { value: number, type: 'minutes' | 'hours' }) { 99 | const is24hoursAutoMeridiemChange = this.mode === '24h' && type === 'hours' && ( 100 | (this.hours >= 12 && value < 12) || (this.hours < 12 && value >= 12)); 101 | if ((this.hasInvalidMeridiem && this.mode === '12h') || is24hoursAutoMeridiemChange) { 102 | this.isPm = !this.isPm; 103 | this.hasInvalidMeridiem = false; 104 | } 105 | 106 | if ((type && type === 'hours') || (!type && this.viewType === 'hours')) { 107 | this.hours = value; 108 | } else if ((type && type === 'minutes') || (!type && this.viewType === 'minutes')) { 109 | this.minutes = value; 110 | } 111 | 112 | const newValue = new Date(); 113 | const hours = this.isPm ? this.hours < 12 ? this.hours + 12 : this.hours : this.hours === 12 ? 0 : this.hours; 114 | newValue.setHours(hours); 115 | newValue.setMinutes(this.minutes); 116 | newValue.setSeconds(0); 117 | newValue.setMilliseconds(0); 118 | this.value = newValue; 119 | this.changeEvent.emit(newValue); 120 | } 121 | 122 | clearInvalidMeridiem() { 123 | this.hasInvalidMeridiem = false; 124 | } 125 | 126 | handleUnavailableSelection() { 127 | clearTimeout(this.autoSwitchID); 128 | } 129 | 130 | handleClockChangeDone(e) { 131 | e.preventDefault(); // prevent mouseUp after touchEnd 132 | 133 | if (this.viewType === 'hours' && !this.skipMinuteAutoSwitch) { 134 | this.autoSwitchID = setTimeout(() => { 135 | this.editMinutes(); 136 | this.autoSwitchID = null; 137 | }, 300); 138 | } 139 | } 140 | 141 | editHours() { 142 | this.viewType = 'hours'; 143 | this.editHoursClicked = true; 144 | setTimeout(() => { this.editHoursClicked = false; }, 0); 145 | } 146 | 147 | editMinutes() { 148 | if (this.hasInvalidMeridiem) { 149 | this.isPm = !this.isPm; 150 | this.hasInvalidMeridiem = false; 151 | } 152 | this.viewType = 'minutes'; 153 | } 154 | 155 | invalidSelectionHandler(value) { 156 | this.invalidSelection = value; 157 | } 158 | 159 | 160 | invalidMeridiem() { 161 | if (this.viewType !== 'minutes' && this.editHoursClicked) { 162 | if (this.invalidMedianID) { return; } 163 | this.invalidMedianID = setTimeout(() => { 164 | this.isPm = !this.isPm; 165 | this.hasInvalidMeridiem = false; 166 | }, 0); 167 | return; 168 | } 169 | this.hasInvalidMeridiem = true; 170 | } 171 | 172 | meridiemChange(hours) { 173 | const changeData = { 174 | type: this.viewType, 175 | value: this.viewType === 'hours' ? hours : this.value.getMinutes() 176 | }; 177 | this.handleClockChange(changeData); 178 | } 179 | 180 | 181 | setAm() { 182 | if (this.hours >= 12) { 183 | this.hours = this.hours - 12; 184 | } 185 | this.isPm = false; 186 | 187 | this.meridiemChange(this.hours); 188 | } 189 | 190 | setPm() { 191 | if (this.hours < 12) { 192 | this.hours = this.hours + 12; 193 | } 194 | this.isPm = true; 195 | this.meridiemChange(this.hours); 196 | } 197 | 198 | okClickHandler = () => { 199 | if (this.hasInvalidMeridiem) { 200 | this.isPm = !this.isPm; 201 | this.hasInvalidMeridiem = false; 202 | } 203 | this.okClickEvent.emit(this.value); 204 | } 205 | 206 | cancelClickHandler = () => { 207 | this.cancelClickEvent.emit(); 208 | } 209 | 210 | } 211 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/timepicker.directive.spec.ts: -------------------------------------------------------------------------------- 1 | // import { TimepickerDirective } from './timepicker.directive'; 2 | 3 | // describe('TimepickerDirective', () => { 4 | // it('should create an instance', () => { 5 | // const directive = new TimepickerDirective(); 6 | // expect(directive).toBeTruthy(); 7 | // }); 8 | // }); 9 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/timepicker.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ControlValueAccessor, 3 | NgForm, 4 | NgControl, 5 | FormGroupDirective, 6 | FormControl, 7 | FormControlName, 8 | Validators, 9 | FormGroup, 10 | FormControlDirective, 11 | } from '@angular/forms'; 12 | import { 13 | Directive, 14 | OnInit, 15 | EventEmitter, 16 | Input, 17 | ElementRef, 18 | OnChanges, 19 | Renderer2, 20 | AfterViewInit, 21 | OnDestroy, 22 | Optional, 23 | SimpleChanges, 24 | NgZone, 25 | HostBinding, 26 | Self, 27 | Output, 28 | HostListener, 29 | TemplateRef, 30 | } from '@angular/core'; 31 | import { MatDialog, MatDialogRef } from '@angular/material/dialog'; 32 | import { 33 | MatFormFieldControl, 34 | MatFormField, 35 | } from '@angular/material/form-field'; 36 | import { 37 | ClockMode, 38 | IAllowed24HourMap, 39 | IAllowed12HourMap, 40 | } from './interfaces-and-types'; 41 | import { 42 | twoDigits, 43 | convertHoursForMode, 44 | isAllowed, 45 | isDateInRange, 46 | isTimeInRange, 47 | } from './util'; 48 | import { MatTimepickerComponentDialogComponent } from './timepicker-dialog/timepicker-dialog.component'; 49 | import { Subject } from 'rxjs'; 50 | import { takeUntil, first } from 'rxjs/operators'; 51 | import { FocusMonitor } from '@angular/cdk/a11y'; 52 | import { coerceBooleanProperty } from '@angular/cdk/coercion'; 53 | import { ErrorStateMatcher } from '@angular/material/core'; 54 | import { Platform } from '@angular/cdk/platform'; 55 | 56 | export interface MatTimepickerButtonTemplateContext { 57 | $implicit: () => void; 58 | label: string; 59 | } 60 | 61 | @Directive({ 62 | selector: 'input[matTimepicker]', 63 | providers: [ 64 | { provide: MatFormFieldControl, useExisting: MatTimepickerDirective }, 65 | ], 66 | // tslint:disable-next-line:no-host-metadata-property 67 | host: { 68 | /** 69 | * @breaking-change 8.0.0 remove .mat-form-field-autofill-control in favor of AutofillMonitor. 70 | */ 71 | // tslint:disable-next-line:object-literal-key-quotes 72 | class: 'mat-input-element mat-form-field-autofill-control', 73 | '[class.mat-input-server]': '_isServer', 74 | // Native input properties that are overwritten by Angular inputs need to be synced with 75 | // the native input element. Otherwise property bindings for those don't work. 76 | '[attr.id]': 'id', 77 | '[attr.placeholder]': 'placeholder', 78 | '[disabled]': 'disabled', 79 | '[required]': 'required', 80 | '[attr.readonly]': 'readonly || null', 81 | '[attr.aria-invalid]': 'errorState', 82 | '[attr.aria-required]': 'required.toString()', 83 | }, 84 | exportAs: 'matTimepicker', 85 | }) 86 | export class MatTimepickerDirective 87 | implements 88 | OnInit, 89 | OnChanges, 90 | AfterViewInit, 91 | OnDestroy, 92 | ControlValueAccessor, 93 | MatFormFieldControl 94 | { 95 | static nextId = 0; 96 | 97 | /** Whether the component is being rendered on the server. */ 98 | // tslint:disable-next-line:variable-name 99 | readonly _isServer: boolean; 100 | 101 | // tslint:disable-next-line:variable-name 102 | _errorState = false; 103 | get errorState() { 104 | const oldState = this._errorState; 105 | const parent = this._parentFormGroup || this._parentForm; 106 | const control = this.ngControl 107 | ? (this.ngControl.control as FormControl) 108 | : null; 109 | const newState = this.errorStateMatcher 110 | ? this.errorStateMatcher.isErrorState(control, parent) 111 | : oldState; 112 | 113 | if (newState !== oldState) { 114 | this._errorState = newState; 115 | this.stateChanges.next(); 116 | } 117 | 118 | return newState; 119 | } 120 | 121 | @Input() 122 | get disabled(): boolean { 123 | if (this.ngControl && this.ngControl.disabled !== null) { 124 | return this.ngControl.disabled; 125 | } 126 | return this._disabled; 127 | } 128 | set disabled(value: boolean) { 129 | this._disabled = coerceBooleanProperty(value); 130 | 131 | // Browsers may not fire the blur event if the input is disabled too quickly. 132 | // Reset from here to ensure that the element doesn't become stuck. 133 | if (this.focused) { 134 | this.focused = false; 135 | this.stateChanges.next(); 136 | } 137 | } 138 | // tslint:disable-next-line:variable-name 139 | protected _disabled = false; 140 | 141 | @Input() get id(): string { 142 | return this._id; 143 | } 144 | set id(value: string) { 145 | this._id = value || this._uid; 146 | } 147 | // tslint:disable-next-line:variable-name 148 | protected _id: string; 149 | 150 | @Input() get readonly(): boolean { 151 | return this._readonly; 152 | } 153 | set readonly(value: boolean) { 154 | this._readonly = coerceBooleanProperty(value); 155 | } 156 | // tslint:disable-next-line:variable-name 157 | private _readonly = false; 158 | 159 | private isAlive: Subject = new Subject(); 160 | stateChanges = new Subject(); 161 | 162 | // tslint:disable-next-line:variable-name 163 | protected _uid = `mat-time-picker-${MatTimepickerDirective.nextId++}`; 164 | @HostBinding('class.floating') get shouldLabelFloat() { 165 | return this.focused || !this.empty; 166 | } 167 | @HostBinding('attr.aria-describedby') describedBy = ''; 168 | 169 | @Input() errorStateMatcher: ErrorStateMatcher; 170 | 171 | @Input() get required() { 172 | return this._required; 173 | } 174 | 175 | set required(req) { 176 | this._required = coerceBooleanProperty(req); 177 | this.stateChanges.next(); 178 | } 179 | // tslint:disable-next-line:variable-name 180 | private _required = false; 181 | 182 | @Input() get placeholder() { 183 | return this._placeholder; 184 | } 185 | set placeholder(plh) { 186 | this._placeholder = plh; 187 | this.stateChanges.next(); 188 | } 189 | // tslint:disable-next-line:variable-name 190 | private _placeholder: string; 191 | 192 | focused = false; 193 | private pattern: RegExp; 194 | 195 | private allowed24HourMap: IAllowed24HourMap = null; 196 | private allowed12HourMap: IAllowed12HourMap = null; 197 | 198 | private isInputFocused = false; 199 | 200 | /* Use a custom template for the ok button */ 201 | @Input() 202 | okButtonTemplate: TemplateRef | null = null; 203 | /* Use a custom template for the cancel button */ 204 | @Input() 205 | cancelButtonTemplate: TemplateRef | null = 206 | null; 207 | 208 | /** Override the label of the ok button. */ 209 | @Input() okLabel = 'Ok'; 210 | /** Override the label of the cancel button. */ 211 | @Input() cancelLabel = 'Cancel'; 212 | /** Override the ante meridiem abbreviation. */ 213 | @Input() anteMeridiemAbbreviation = 'am'; 214 | /** Override the post meridiem abbreviation. */ 215 | @Input() postMeridiemAbbreviation = 'pm'; 216 | 217 | /** Sets the clock mode, 12-hour or 24-hour clocks are supported. */ 218 | @Input() mode: ClockMode = '24h'; 219 | @Input() color = 'primary'; 220 | @Input() disableDialogOpenOnClick = false; 221 | @Input() strict = true; 222 | 223 | controlType = 'angular-material-timepicker'; 224 | 225 | private listeners: (() => void)[] = []; 226 | 227 | @Input() minDate: Date; 228 | @Input() maxDate: Date; 229 | 230 | // tslint:disable-next-line:variable-name 231 | private _isPm: boolean; 232 | // tslint:disable-next-line:variable-name 233 | private _value: Date; 234 | // tslint:disable-next-line:variable-name 235 | private _formattedValueString: string; 236 | 237 | // tslint:disable-next-line:variable-name 238 | private _skipValueChangeEmission = true; 239 | 240 | @Input() set value(value: Date) { 241 | if (value === this._value) { 242 | return; 243 | } 244 | this._value = value; 245 | if (!value) { 246 | this._formattedValueString = null; 247 | this.setInputElementValue(''); 248 | this.currentValue = value; 249 | return; 250 | } 251 | 252 | const { hour, isPm } = convertHoursForMode(value.getHours(), this.mode); 253 | this._isPm = isPm; 254 | this._formattedValueString = 255 | this.mode === '12h' 256 | ? `${hour}:${twoDigits(value.getMinutes())} ${isPm ? this.postMeridiemAbbreviation : this.anteMeridiemAbbreviation 257 | }` 258 | : `${twoDigits(value.getHours())}:${twoDigits(value.getMinutes())}`; 259 | 260 | if (!this.isInputFocused) { 261 | this.setInputElementValue(this.formattedValueString); 262 | } 263 | this.currentValue = value; 264 | this.stateChanges.next(); 265 | 266 | if (this._skipValueChangeEmission) { 267 | return; 268 | } 269 | this.timeChange.emit(this.currentValue); 270 | } 271 | 272 | get value() { 273 | return this._value; 274 | } 275 | 276 | get isPm() { 277 | return this._isPm; 278 | } 279 | 280 | get empty() { 281 | return !(this.currentValue instanceof Date); 282 | } 283 | 284 | private get formattedValueString() { 285 | return this._formattedValueString; 286 | } 287 | 288 | private currentValue: Date; 289 | private modalRef: MatDialogRef; 290 | 291 | private onChangeFn: any; 292 | private onTouchedFn: any; 293 | private combination: string[] = []; 294 | 295 | @Output() timeChange: EventEmitter = new EventEmitter(); 296 | @Output() invalidInput: EventEmitter = new EventEmitter(); 297 | 298 | @HostListener('input') inputHandler() { 299 | let value = (this.elRef.nativeElement as any).value as string; 300 | const length = value.length; 301 | if (length === 0) { 302 | this.writeValue(null, true); 303 | if (this.onChangeFn) { 304 | this.onChangeFn(null); 305 | } 306 | return; 307 | } 308 | 309 | const meridiemResult = value.match(/am|pm/i); 310 | let meridiem: string | null = null; 311 | if (meridiemResult) { 312 | value = value.replace(meridiemResult[0], ''); 313 | [meridiem] = meridiemResult; 314 | } 315 | const valueHasColumn = value.includes(':'); 316 | let [hours, minutes]: any = 317 | length === 1 318 | ? [value, 0] 319 | : length === 2 && !valueHasColumn 320 | ? [value, 0] 321 | : valueHasColumn 322 | ? value.split(':') 323 | : value.split(/(\d\d)/).filter((v) => v); 324 | 325 | hours = +hours; 326 | 327 | if (/\s/.test(minutes)) { 328 | let other; 329 | [minutes, other] = minutes.split(/\s/); 330 | if (other === 'pm' && !isNaN(hours) && hours < 12) { 331 | hours += 12; 332 | } 333 | } 334 | 335 | minutes = +minutes; 336 | 337 | if (isNaN(hours) || isNaN(minutes)) { 338 | this.writeValue(null, true); 339 | return; 340 | } 341 | 342 | if (hours < 12 && meridiem && meridiem.toLowerCase() === 'pm') { 343 | hours += 12; 344 | } else if (hours >= 12 && meridiem && meridiem.toLowerCase() === 'am') { 345 | hours -= 12; 346 | } 347 | 348 | if (this.mode === '12h' && +hours < 0) { 349 | hours = '0'; 350 | } else { 351 | if (+hours > 24) { 352 | hours = '24'; 353 | } else if (+hours < 0) { 354 | hours = '0'; 355 | } 356 | } 357 | 358 | if (+minutes > 59) { 359 | minutes = '59'; 360 | } else if (+minutes < 0) { 361 | minutes = '0'; 362 | } 363 | 364 | const d = this.value ? new Date(this.value.getTime()) : new Date(); 365 | d.setHours(+hours); 366 | d.setMinutes(+minutes); 367 | d.setSeconds(0); 368 | d.setMilliseconds(0); 369 | 370 | const isValueInRange = isDateInRange(this.minDate, this.maxDate, d); 371 | if (!isValueInRange) { 372 | this.invalidInput.emit(); 373 | } 374 | 375 | this.writeValue(d, true); 376 | if (this.onChangeFn) { 377 | this.onChangeFn(d); 378 | } 379 | } 380 | 381 | @HostListener('keydown', ['$event']) keydownHandler(event: any) { 382 | if (event.metaKey || event.ctrlKey || event.altKey) { 383 | this.combination = this.combination.concat(event.code); 384 | return; 385 | } 386 | if (!/^[0-9a-zA-Z\s]{0,1}$/.test(event.key)) { 387 | return; 388 | } 389 | const target = event.target; 390 | const tValue = target.value; 391 | const value = `${tValue.slice(0, target.selectionStart)}${event.key 392 | }${tValue.slice(target.selectionEnd)}`; 393 | if (value.match(this.pattern) || this.combination.length > 0) { 394 | return true; 395 | } 396 | event.preventDefault(); 397 | event.stopImmediatePropagation(); 398 | } 399 | 400 | @HostListener('keyup', ['$event']) keyupHandler(event: any) { 401 | this.combination = this.combination.filter((v) => v !== event.code); 402 | } 403 | 404 | @HostListener('focus') focusHandler() { 405 | this.isInputFocused = true; 406 | } 407 | 408 | @HostListener('focusout') focusoutHandler() { 409 | this.isInputFocused = false; 410 | this.setInputElementValue(this.formattedValueString); 411 | if (this.onTouchedFn && !this.modalRef) { 412 | this.onTouchedFn(); 413 | } 414 | } 415 | 416 | constructor( 417 | @Optional() @Self() public ngControl: NgControl, 418 | public dialog: MatDialog, 419 | private renderer: Renderer2, 420 | private zone: NgZone, 421 | private fm: FocusMonitor, 422 | private elRef: ElementRef, 423 | // tslint:disable-next-line:variable-name 424 | protected _platform: Platform, 425 | // tslint:disable-next-line:variable-name 426 | @Optional() private _parentForm: NgForm, 427 | // tslint:disable-next-line:variable-name 428 | @Optional() private _matFormFiled: MatFormField, 429 | // tslint:disable-next-line:variable-name 430 | @Optional() private _parentFormGroup: FormGroupDirective, 431 | // tslint:disable-next-line:variable-name 432 | _defaultErrorStateMatcher: ErrorStateMatcher 433 | ) { 434 | this.id = this.id; 435 | 436 | this.errorStateMatcher = _defaultErrorStateMatcher; 437 | if (this.ngControl != null) { 438 | this.ngControl.valueAccessor = this; 439 | } 440 | 441 | 442 | if (_platform.IOS) { 443 | zone.runOutsideAngular(() => { 444 | elRef.nativeElement.addEventListener('keyup', (event: Event) => { 445 | const el = event.target as HTMLInputElement; 446 | if (!el.value && !el.selectionStart && !el.selectionEnd) { 447 | // Note: Just setting `0, 0` doesn't fix the issue. Setting 448 | // `1, 1` fixes it for the first time that you type text and 449 | // then hold delete. Toggling to `1, 1` and then back to 450 | // `0, 0` seems to completely fix it. 451 | el.setSelectionRange(1, 1); 452 | el.setSelectionRange(0, 0); 453 | } 454 | }); 455 | }); 456 | } 457 | 458 | this._isServer = !this._platform.isBrowser; 459 | } 460 | 461 | setDescribedByIds(ids: string[]) { 462 | this.describedBy = ids.join(' '); 463 | } 464 | 465 | onContainerClick(event: MouseEvent) { 466 | if ((event.target as Element).tagName.toLowerCase() !== 'input') { 467 | this.elRef.nativeElement.focus(); 468 | } 469 | } 470 | 471 | setInputElementValue(value: any) { 472 | if (value === null || value === undefined) { 473 | value = ''; 474 | } 475 | Promise.resolve().then(() => { 476 | this.zone.runOutsideAngular(() => { 477 | this.renderer.setProperty(this.elRef.nativeElement, 'value', value); 478 | }); 479 | }); 480 | } 481 | 482 | validate() { 483 | if (this.currentValue === null || this.currentValue === undefined) { 484 | return null; 485 | } 486 | 487 | const isValueInRange = this.strict 488 | ? isDateInRange(this.minDate, this.maxDate, this.currentValue) 489 | : isTimeInRange(this.minDate, this.maxDate, this.currentValue); 490 | 491 | return isValueInRange ? null : { dateRange: true }; 492 | } 493 | 494 | ngAfterViewInit() { 495 | this.listeners.push( 496 | this.renderer.listen( 497 | this._matFormFiled 498 | ? this._matFormFiled._elementRef.nativeElement 499 | : this.elRef.nativeElement, 500 | 'click', 501 | this.clickHandler 502 | ) 503 | ); 504 | } 505 | 506 | clickHandler = (e: FocusEvent) => { 507 | if ( 508 | (this.modalRef && this.modalRef.componentInstance.isClosing) || 509 | this.disabled || 510 | this.disableDialogOpenOnClick 511 | ) { 512 | return; 513 | } 514 | if (!this.modalRef && !this.disableDialogOpenOnClick) { 515 | this.showDialog(); 516 | } 517 | }; 518 | 519 | ngOnInit() { 520 | if (this.ngControl && this.ngControl.control?.parent) { 521 | const [key] = Object.entries(this.ngControl.control.parent.controls).find(([, c]) => c === this.ngControl.control); 522 | const control = this.ngControl.control.parent.get(key); 523 | this.required = !!control?.hasValidator(Validators.required); 524 | } else if (this.ngControl) { 525 | const control = (this.ngControl as FormControlName)?.formDirective?.control?.get(this.ngControl.path) || null; 526 | this.required = !!control?.hasValidator(Validators.required); 527 | } 528 | 529 | if (this._platform.isBrowser) { 530 | this.fm.monitor(this.elRef.nativeElement, true).subscribe((origin) => { 531 | this.focused = !!origin; 532 | this.stateChanges.next(); 533 | }); 534 | } 535 | 536 | const hasMaxDate = !!this.maxDate; 537 | const hasMinDate = !!this.minDate; 538 | 539 | if (hasMinDate || hasMaxDate) { 540 | if (hasMinDate) { 541 | this.minDate.setSeconds(0); 542 | this.minDate.setMilliseconds(0); 543 | } 544 | if (hasMaxDate) { 545 | this.maxDate.setSeconds(0); 546 | this.maxDate.setMilliseconds(0); 547 | } 548 | Promise.resolve().then(() => this.generateAllowedMap()); 549 | 550 | if (!(this.ngControl as any)._rawValidators.find((v) => v === this)) { 551 | this.ngControl.control.setValidators( 552 | ((this.ngControl as any)._rawValidators as any[]).concat(this) 553 | ); 554 | this.ngControl.control.updateValueAndValidity(); 555 | } 556 | } 557 | 558 | this._skipValueChangeEmission = false; 559 | } 560 | 561 | generateAllowedMap() { 562 | const isStrictMode = this.strict && this.value instanceof Date; 563 | if (this.mode === '24h') { 564 | this.allowed24HourMap = {}; 565 | for (let h = 0; h < 24; h++) { 566 | for (let m = 0; m < 60; m++) { 567 | const hourMap = this.allowed24HourMap[h] || {}; 568 | if (isStrictMode) { 569 | const currentDate = new Date(this.value.getTime()); 570 | currentDate.setHours(h); 571 | currentDate.setMinutes(m); 572 | currentDate.setSeconds(0); 573 | currentDate.setMilliseconds(0); 574 | hourMap[m] = isDateInRange(this.minDate, this.maxDate, currentDate); 575 | } else { 576 | hourMap[m] = isAllowed(h, m, this.minDate, this.maxDate, '24h'); 577 | } 578 | this.allowed24HourMap[h] = hourMap; 579 | } 580 | } 581 | } else { 582 | this.allowed12HourMap = { am: {}, pm: {} }; 583 | for (let h = 0; h < 24; h++) { 584 | const meridiem = h < 12 ? 'am' : 'pm'; 585 | for (let m = 0; m < 60; m++) { 586 | const hour = h > 12 ? h - 12 : h === 0 ? 12 : h; 587 | const hourMap = this.allowed12HourMap[meridiem][hour] || {}; 588 | if (isStrictMode) { 589 | const currentDate = new Date(this.value.getTime()); 590 | currentDate.setHours(h); 591 | currentDate.setMinutes(m); 592 | currentDate.setSeconds(0); 593 | currentDate.setMilliseconds(0); 594 | hourMap[m] = isDateInRange(this.minDate, this.maxDate, currentDate); 595 | } else { 596 | hourMap[m] = isAllowed(h, m, this.minDate, this.maxDate, '24h'); 597 | } 598 | this.allowed12HourMap[meridiem][hour] = hourMap; 599 | } 600 | } 601 | } 602 | } 603 | 604 | ngOnChanges(simpleChanges: SimpleChanges) { 605 | this.pattern = 606 | this.mode === '24h' 607 | ? /^[0-9]{1,2}:?([0-9]{1,2})?$/ 608 | : /^[0-9]{1,2}:?([0-9]{1,2})?\s?(a|p)?m?$/; 609 | 610 | if ( 611 | (simpleChanges.minDate && 612 | !simpleChanges.minDate.isFirstChange() && 613 | +simpleChanges.minDate.currentValue !== 614 | simpleChanges.minDate.previousValue) || 615 | (simpleChanges.maxDate && 616 | !simpleChanges.maxDate.isFirstChange() && 617 | +simpleChanges.maxDate.currentValue !== 618 | simpleChanges.maxDate.previousValue) || 619 | (simpleChanges.disableLimitBase && 620 | !simpleChanges.disableLimitBase.isFirstChange() && 621 | +simpleChanges.disableLimitBase.currentValue !== 622 | simpleChanges.disableLimitBase.previousValue) 623 | ) { 624 | this.generateAllowedMap(); 625 | this.ngControl.control.updateValueAndValidity(); 626 | } 627 | 628 | if (!this.modalRef || !this.modalRef.componentInstance) { 629 | return; 630 | } 631 | 632 | this.modalRef.componentInstance.data = { 633 | mode: this.mode, 634 | value: this.currentValue, 635 | okLabel: this.okLabel, 636 | cancelLabel: this.cancelLabel, 637 | okButtonTemplate: this.okButtonTemplate, 638 | cancelButtonTemplate: this.cancelButtonTemplate, 639 | anteMeridiemAbbreviation: this.anteMeridiemAbbreviation, 640 | postMeridiemAbbreviation: this.postMeridiemAbbreviation, 641 | color: this.color, 642 | isPm: this.isPm, 643 | minDate: this.minDate, 644 | maxDate: this.maxDate, 645 | allowed12HourMap: this.allowed12HourMap, 646 | allowed24HourMap: this.allowed24HourMap, 647 | }; 648 | } 649 | 650 | checkValidity(value: Date) { 651 | if (!value) { 652 | return false; 653 | } 654 | const hour = value.getHours(); 655 | const minutes = value.getMinutes(); 656 | const meridiem = this.isPm ? 'PM' : 'AM'; 657 | return isAllowed( 658 | hour, 659 | minutes, 660 | this.minDate, 661 | this.maxDate, 662 | this.mode, 663 | meridiem 664 | ); 665 | } 666 | 667 | writeValue(value: Date, isInnerCall = false): void { 668 | if (!isInnerCall) { 669 | this._skipValueChangeEmission = true; 670 | Promise.resolve().then(() => (this._skipValueChangeEmission = false)); 671 | } 672 | 673 | if (value) { 674 | value.setSeconds(0); 675 | value.setMilliseconds(0); 676 | } 677 | 678 | if (+this.value !== +value) { 679 | this.value = value; 680 | } 681 | } 682 | 683 | registerOnChange(fn: any): void { 684 | this.onChangeFn = fn; 685 | } 686 | 687 | registerOnTouched(fn: any): void { 688 | this.onTouchedFn = fn; 689 | } 690 | 691 | setDisabledState?(isDisabled: boolean): void { 692 | this.disabled = isDisabled; 693 | } 694 | 695 | showDialog() { 696 | if (this.disabled) { 697 | return; 698 | } 699 | this.isInputFocused = false; 700 | this.modalRef = this.dialog.open(MatTimepickerComponentDialogComponent, { 701 | autoFocus: false, 702 | data: { 703 | mode: this.mode, 704 | value: this.currentValue, 705 | okLabel: this.okLabel, 706 | cancelLabel: this.cancelLabel, 707 | okButtonTemplate: this.okButtonTemplate, 708 | cancelButtonTemplate: this.cancelButtonTemplate, 709 | anteMeridiemAbbreviation: this.anteMeridiemAbbreviation, 710 | postMeridiemAbbreviation: this.postMeridiemAbbreviation, 711 | color: this.color, 712 | isPm: this.isPm, 713 | minDate: this.minDate, 714 | maxDate: this.maxDate, 715 | allowed12HourMap: this.allowed12HourMap, 716 | allowed24HourMap: this.allowed24HourMap, 717 | }, 718 | }); 719 | const instance = this.modalRef.componentInstance; 720 | instance.changeEvent 721 | .pipe(takeUntil(this.isAlive)) 722 | .subscribe(this.handleChange); 723 | instance.okClickEvent 724 | .pipe(takeUntil(this.isAlive)) 725 | .subscribe(this.handleOk); 726 | instance.cancelClickEvent 727 | .pipe(takeUntil(this.isAlive)) 728 | .subscribe(this.handleCancel); 729 | this.modalRef 730 | .beforeClosed() 731 | .pipe(first()) 732 | .subscribe(() => (instance.isClosing = true)); 733 | this.modalRef 734 | .afterClosed() 735 | .pipe(first()) 736 | .subscribe(() => { 737 | if (this.onTouchedFn) { 738 | this.onTouchedFn(); 739 | } 740 | this.modalRef = null; 741 | this.elRef.nativeElement.focus(); 742 | }); 743 | 744 | this.currentValue = this.value as Date; 745 | } 746 | 747 | handleChange = (newValue) => { 748 | if (!(newValue instanceof Date)) { 749 | return; 750 | } 751 | const v = 752 | this.value instanceof Date ? new Date(this.value.getTime()) : new Date(); 753 | v.setHours(newValue.getHours()); 754 | v.setMinutes(newValue.getMinutes()); 755 | v.setSeconds(0); 756 | v.setMilliseconds(0); 757 | this.currentValue = v; 758 | }; 759 | 760 | handleOk = (value) => { 761 | if (!this.currentValue && value) { 762 | this.currentValue = value; 763 | } 764 | if (this.onChangeFn) { 765 | this.onChangeFn(this.currentValue); 766 | } 767 | this.value = this.currentValue; 768 | this.modalRef.close(); 769 | }; 770 | 771 | handleCancel = () => { 772 | this.modalRef.close(); 773 | }; 774 | 775 | ngOnDestroy() { 776 | this.isAlive.next(); 777 | this.isAlive.complete(); 778 | this.stateChanges.complete(); 779 | 780 | if (this._platform.isBrowser) { 781 | this.fm.stopMonitoring(this.elRef.nativeElement); 782 | } 783 | 784 | this.listeners.forEach((l) => l()); 785 | } 786 | } 787 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/lib/util.ts: -------------------------------------------------------------------------------- 1 | import { ITimeData, ClockViewType, ClockMode } from './interfaces-and-types'; 2 | 3 | export function twoDigits(n) { 4 | return n < 10 ? `0${n}` : `${n}`; 5 | } 6 | 7 | export function addDays(date: Date, days: number) { 8 | const result = new Date(date); 9 | result.setDate(result.getDate() + days); 10 | return result; 11 | } 12 | 13 | export function convertHoursForMode(hour: number, mode: ClockMode) { 14 | const isPm = hour >= 12; 15 | if (mode === '24h') { 16 | return { hour, isPm }; 17 | } else if (hour === 0 || hour === 12) { 18 | return { hour: 12, isPm }; 19 | } else if (hour < 12) { 20 | return { hour, isPm }; 21 | } 22 | return { hour: hour - 12, isPm }; 23 | } 24 | 25 | function mod(a, b) { 26 | return a - Math.floor(a / b) * b; 27 | } 28 | 29 | export function getShortestAngle(from, to) { 30 | const difference = to - from; 31 | return from + mod(difference + 180, 360) - 180; 32 | } 33 | 34 | export function isDateInRange(minDate: Date, maxDate: Date, current: Date) { 35 | const unixCurrentDate = +current; 36 | return (!minDate || +minDate <= unixCurrentDate) && (!maxDate || unixCurrentDate <= +maxDate); 37 | } 38 | 39 | export function isTimeInRange(minDate: Date, maxDate: Date, current: Date) { 40 | if (minDate instanceof Date) { 41 | const newMinDate = new Date(); 42 | newMinDate.setHours(minDate.getHours()); 43 | newMinDate.setMinutes(minDate.getMinutes()); 44 | newMinDate.setSeconds(0); 45 | newMinDate.setMilliseconds(0); 46 | minDate = newMinDate; 47 | } 48 | if (maxDate instanceof Date) { 49 | const newMaxDate = new Date(); 50 | newMaxDate.setHours(maxDate.getHours()); 51 | newMaxDate.setMinutes(maxDate.getMinutes()); 52 | newMaxDate.setSeconds(0); 53 | newMaxDate.setMilliseconds(0); 54 | maxDate = newMaxDate; 55 | } 56 | if (current instanceof Date) { 57 | const newCurrent = new Date(); 58 | newCurrent.setHours(current.getHours()); 59 | newCurrent.setMinutes(current.getMinutes()); 60 | newCurrent.setSeconds(0); 61 | newCurrent.setMilliseconds(0); 62 | current = newCurrent; 63 | } 64 | const unixCurrentDate = +current; 65 | return (!minDate || +minDate <= unixCurrentDate) && (!maxDate || unixCurrentDate <= +maxDate); 66 | } 67 | 68 | // used when generating the allowed maps 69 | 70 | export function isAllowed( 71 | hour: number, 72 | minutes: number, 73 | minDate: Date, 74 | maxDate: Date, 75 | clockMode: ClockMode, 76 | selectedMeridiem?: 'AM' | 'PM' 77 | ) { 78 | if (hour > 24 || hour < 0 || minutes > 60 || minutes < 0) { return false; } 79 | 80 | if (!minDate && !maxDate) { return true; } 81 | 82 | if (clockMode === '12h') { 83 | if (hour === 12 && selectedMeridiem === 'AM') { hour = 0; } 84 | if (hour > 12) { hour -= 12; } 85 | } 86 | const checkDate = new Date(); 87 | 88 | checkDate.setHours(hour); 89 | checkDate.setMinutes(minutes); 90 | checkDate.setSeconds(0); 91 | checkDate.setMilliseconds(0); 92 | 93 | return isDateInRange(minDate, maxDate, checkDate); 94 | } 95 | 96 | // used by the clock component to visually disable the not allowed values 97 | 98 | export function getIsAvailabeFn(allowed12HourMap, allowed24HourMap, mode: ClockMode) { 99 | return (value: number, viewType: ClockViewType, isPm: boolean, h?: number) => { 100 | const isHourCheck = viewType === 'hours'; 101 | const [hour, minutes] = isHourCheck ? [value, null] : [h, value]; 102 | 103 | if (mode === '12h') { 104 | if (!allowed12HourMap) { return true; } 105 | const meridiem = isPm ? 'pm' : 'am'; 106 | if (isHourCheck) { 107 | return !!Object.values(allowed12HourMap[meridiem][hour]).find(v => v === true); 108 | } 109 | return allowed12HourMap[meridiem][hour][minutes]; 110 | } 111 | 112 | if (!allowed24HourMap) { return true; } 113 | 114 | if (isHourCheck) { 115 | return !!Object.values(allowed24HourMap[hour]).find(v => v === true); 116 | } 117 | return allowed24HourMap[hour][minutes]; 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /projects/mat-timepicker/src/public-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of mat-timepicker 3 | */ 4 | 5 | export * from './lib/timepicker.directive'; 6 | export * from './lib/mat-timepicker.module'; 7 | -------------------------------------------------------------------------------- /projects/mat-timepicker/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/es7/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 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting(), { 18 | teardown: { destroyAfterEach: false } 19 | } 20 | ); 21 | // Then we find all the tests. 22 | const context = require.context('./', true, /\.spec\.ts$/); 23 | // And load the modules. 24 | context.keys().map(context); 25 | -------------------------------------------------------------------------------- /projects/mat-timepicker/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "declarationMap": true, 6 | "target": "es2020", 7 | "declaration": true, 8 | "inlineSources": true, 9 | "types": [], 10 | "lib": [ 11 | "dom", 12 | "es2018" 13 | ] 14 | }, 15 | "angularCompilerOptions": { 16 | "skipTemplateCodegen": true, 17 | "strictMetadataEmit": true, 18 | "enableResourceInlining": true 19 | }, 20 | "exclude": [ 21 | "src/test.ts", 22 | "**/*.spec.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /projects/mat-timepicker/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } -------------------------------------------------------------------------------- /projects/mat-timepicker/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.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 | -------------------------------------------------------------------------------- /projects/mat-timepicker/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "mat", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "mat", 14 | "kebab-case" 15 | ] 16 | } 17 | } -------------------------------------------------------------------------------- /projects/mat-timepicker/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Timepickers with on click dialog 4 |
5 | 6 | 24 TIMEPICKER 7 | 10 | access_time 11 | Invalid Date 12 | 13 |
14 |
15 | 16 | 12 TIMEPICKER 17 | 19 | access_time 20 | Invalid Date 21 | 22 |
23 |
24 |
25 | Timepickers with icon on click dialog 26 |
27 | 28 | 24 TIMEPICKER 29 | 32 | access_time 33 | Invalid Date 34 | 35 |
36 |
37 | 38 | 12 TIMEPICKER 39 | 42 | access_time 43 | Invalid Date 44 | 45 |
46 |
47 |
48 | Timepickers with icon on click dialog and ShowOnDirtyErrorStateMatcher 49 |
50 | 51 | 24 TIMEPICKER 52 | 55 | access_time 56 | Invalid Date 57 | 58 |
59 |
60 | 61 | 12 TIMEPICKER 62 | 65 | access_time 66 | Invalid Date 67 | 68 |
69 |
70 |
71 | Timepickers with default value, icon on click dialog and custom error matcher (look into the source code of 72 | the example 73 | app) 74 | 75 |
76 | 77 | 24 TIMEPICKER 78 | 82 | access_time 83 | Invalid Date 84 | 85 |
86 |
87 | 88 | 12 TIMEPICKER 89 | 93 | access_time 94 | Invalid Date 95 | 96 |

(strict mode by default is set to true so we are checking if the datetime is between the min and max since 97 | the 98 | dates are different we will always be in an error state)

99 |
100 |
101 |
102 | Timepickers with custom ok and cancel buttons 103 | 104 | 105 | 106 | 107 | 108 | 109 |
110 | 111 | 24 TIMEPICKER 112 | 115 | access_time 116 | Invalid Date 117 | 118 |
119 |
120 |
Template Form Value: {{f.value | json}}
121 |
122 |

Reactive Form

123 |
124 | 125 | Time 126 | 127 | 128 |
Reactive Form Value: {{form.value | json}}
129 | {{form.get('time').errors | json}} 130 | 131 |
-------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | #timepicker-example-1 { 2 | max-width: 200px; 3 | } 4 | 5 | #timepicker-example-2 { 6 | max-width: 178px; 7 | } 8 | 9 | #timepicker-example-3 { 10 | max-width: 178px; 11 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(waitForAsync(() => { 6 | TestBed.configureTestingModule({ 7 | declarations: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | it(`should have as title 'angular-material-timepicker'`, () => { 20 | const fixture = TestBed.createComponent(AppComponent); 21 | const app = fixture.debugElement.componentInstance; 22 | expect(app.title).toEqual('angular-material-timepicker'); 23 | }); 24 | 25 | it('should render title in a h1 tag', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | fixture.detectChanges(); 28 | const compiled = fixture.debugElement.nativeElement; 29 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-material-timepicker!'); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { 3 | ShowOnDirtyErrorStateMatcher, 4 | ErrorStateMatcher, 5 | } from '@angular/material/core'; 6 | import { 7 | FormControl, 8 | FormBuilder, 9 | FormGroup, 10 | Validators, 11 | } from '@angular/forms'; 12 | 13 | class CustomErrorStateMatcher implements ErrorStateMatcher { 14 | isErrorState(control: FormControl | null) { 15 | return control.invalid; 16 | } 17 | } 18 | 19 | @Component({ 20 | selector: 'app-root', 21 | templateUrl: './app.component.html', 22 | styleUrls: ['./app.component.scss'], 23 | }) 24 | export class AppComponent { 25 | title = 'angular-material-timepicker'; 26 | minValue: Date; 27 | maxValue: Date; 28 | defaultValue: Date; 29 | 30 | showOnDirtyErrorStateMatcher = new ShowOnDirtyErrorStateMatcher(); 31 | customErrorStateMatcher = new CustomErrorStateMatcher(); 32 | 33 | form: FormGroup; 34 | 35 | constructor(private formBuilder: FormBuilder) { 36 | const minValue = new Date(); 37 | minValue.setHours(6); 38 | minValue.setMinutes(10); 39 | this.minValue = minValue; 40 | 41 | const maxValue = new Date(); 42 | maxValue.setHours(18); 43 | maxValue.setMinutes(10); 44 | this.maxValue = maxValue; 45 | 46 | const d = new Date(); 47 | d.setDate(1); 48 | d.setMonth(2); 49 | d.setHours(7); 50 | d.setMinutes(0); 51 | d.setSeconds(1); 52 | d.setMilliseconds(10); 53 | this.defaultValue = d; 54 | 55 | this.form = this.formBuilder.group({ 56 | time: [this.defaultValue, Validators.required], 57 | }); 58 | } 59 | 60 | timeChangeHandler(data) { 61 | console.log('time changed to', data); 62 | } 63 | 64 | invalidInputHandler() { 65 | console.log('invalid input'); 66 | } 67 | 68 | changeMaxValue() { 69 | const maxValue = new Date(); 70 | maxValue.setHours(20); 71 | maxValue.setMinutes(10); 72 | this.maxValue = maxValue; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | 4 | import { AppComponent } from './app.component'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | 7 | import { MatTimepickerModule } from 'mat-timepicker'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 10 | import { MatFormFieldModule } from '@angular/material/form-field'; 11 | import { MatInputModule } from '@angular/material/input'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent 16 | ], 17 | imports: [ 18 | BrowserModule, 19 | FormsModule, 20 | BrowserAnimationsModule, 21 | MatTimepickerModule, 22 | MatFormFieldModule, 23 | MatIconModule, 24 | MatInputModule, 25 | ReactiveFormsModule 26 | ], 27 | providers: [], 28 | bootstrap: [AppComponent] 29 | }) 30 | export class AppModule { } 31 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularMaterialTimepicker 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** 26 | * By default, zone.js will patch all possible macroTask and DomEvents 27 | * user can disable parts of macroTask/DomEvents patch by setting following flags 28 | * because those flags need to be set before `zone.js` being loaded, and webpack 29 | * will put import in the top of bundle, so user need to create a separate file 30 | * in this directory (for example: zone-flags.ts), and put the following flags 31 | * into that file, and then add the following code before importing zone.js. 32 | * import './zone-flags'; 33 | * 34 | * The flags allowed in zone-flags.ts are listed here. 35 | * 36 | * The following flags will work for all browsers. 37 | * 38 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 39 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 40 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 41 | * 42 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 43 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 44 | * 45 | * (window as any).__Zone_enable_cross_context_check = true; 46 | * 47 | */ 48 | 49 | /*************************************************************************************************** 50 | * Zone JS is required by default for Angular itself. 51 | */ 52 | import 'zone.js'; // Included with Angular CLI. 53 | 54 | 55 | /*************************************************************************************************** 56 | * APPLICATION IMPORTS 57 | */ 58 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @use '@angular/material' as mat; 3 | @include mat.core(); 4 | 5 | 6 | $my-app-primary: mat.define-palette(mat.$blue-grey-palette); 7 | $my-app-accent: mat.define-palette(mat.$pink-palette, 500, 900, A100); 8 | $my-app-warn: mat.define-palette(mat.$deep-orange-palette); 9 | $my-app-theme: mat.define-light-theme($my-app-primary, $my-app-accent, $my-app-warn); 10 | @include mat.all-component-themes($my-app-theme); 11 | .alternate-theme { 12 | $alternate-primary: mat.define-palette(mat.$light-blue-palette); 13 | $alternate-accent: mat.define-palette(mat.$yellow-palette, 400); 14 | $alternate-theme: mat.define-light-theme($alternate-primary, $alternate-accent); 15 | @include mat.all-component-themes($alternate-theme); 16 | } 17 | 18 | html, body { height: 100%; } 19 | body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } 20 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting(), { 21 | teardown: { destroyAfterEach: false } 22 | } 23 | ); 24 | // Then we find all the tests. 25 | const context = require.context('./', true, /\.spec\.ts$/); 26 | // And load the modules. 27 | context.keys().map(context); 28 | -------------------------------------------------------------------------------- /timepicker-hours.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/timepicker-hours.png -------------------------------------------------------------------------------- /timepicker-min.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IliaIdakiev/angular-material-timepicker/3b98106b0effb9db46ed25a1d88bcffad3e75281/timepicker-min.png -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2020", 14 | "lib": [ 15 | "es2018", 16 | "dom" 17 | ], 18 | "paths": { 19 | "mat-timepicker": [ 20 | "dist/mat-timepicker/mat-timepicker", 21 | "dist/mat-timepicker" 22 | ] 23 | } 24 | }, 25 | "angularCompilerOptions": { 26 | "fullTemplateTypeCheck": true, 27 | "strictInjectionParameters": true 28 | } 29 | } -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "align": { 5 | "options": [ 6 | "parameters", 7 | "statements" 8 | ] 9 | }, 10 | "array-type": false, 11 | "arrow-return-shorthand": true, 12 | "curly": true, 13 | "deprecation": { 14 | "severity": "warning" 15 | }, 16 | "component-class-suffix": true, 17 | "contextual-lifecycle": true, 18 | "directive-class-suffix": true, 19 | "directive-selector": [ 20 | true, 21 | "attribute", 22 | "app", 23 | "camelCase" 24 | ], 25 | "component-selector": [ 26 | true, 27 | "element", 28 | "app", 29 | "kebab-case" 30 | ], 31 | "eofline": true, 32 | "import-blacklist": [ 33 | true, 34 | "rxjs/Rx" 35 | ], 36 | "import-spacing": true, 37 | "indent": { 38 | "options": [ 39 | "spaces" 40 | ] 41 | }, 42 | "max-classes-per-file": false, 43 | "max-line-length": [ 44 | true, 45 | 140 46 | ], 47 | "member-ordering": [ 48 | true, 49 | { 50 | "order": [ 51 | "static-field", 52 | "instance-field", 53 | "static-method", 54 | "instance-method" 55 | ] 56 | } 57 | ], 58 | "no-console": [ 59 | true, 60 | "debug", 61 | "info", 62 | "time", 63 | "timeEnd", 64 | "trace" 65 | ], 66 | "no-empty": false, 67 | "no-inferrable-types": [ 68 | true, 69 | "ignore-params" 70 | ], 71 | "no-non-null-assertion": true, 72 | "no-redundant-jsdoc": true, 73 | "no-switch-case-fall-through": true, 74 | "no-var-requires": false, 75 | "object-literal-key-quotes": [ 76 | true, 77 | "as-needed" 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single" 82 | ], 83 | "semicolon": { 84 | "options": [ 85 | "always" 86 | ] 87 | }, 88 | "space-before-function-paren": { 89 | "options": { 90 | "anonymous": "never", 91 | "asyncArrow": "always", 92 | "constructor": "never", 93 | "method": "never", 94 | "named": "never" 95 | } 96 | }, 97 | "typedef-whitespace": { 98 | "options": [ 99 | { 100 | "call-signature": "nospace", 101 | "index-signature": "nospace", 102 | "parameter": "nospace", 103 | "property-declaration": "nospace", 104 | "variable-declaration": "nospace" 105 | }, 106 | { 107 | "call-signature": "onespace", 108 | "index-signature": "onespace", 109 | "parameter": "onespace", 110 | "property-declaration": "onespace", 111 | "variable-declaration": "onespace" 112 | } 113 | ] 114 | }, 115 | "variable-name": { 116 | "options": [ 117 | "ban-keywords", 118 | "check-format", 119 | "allow-pascal-case" 120 | ] 121 | }, 122 | "whitespace": { 123 | "options": [ 124 | "check-branch", 125 | "check-decl", 126 | "check-operator", 127 | "check-separator", 128 | "check-type", 129 | "check-typecast" 130 | ] 131 | }, 132 | "no-conflicting-lifecycle": true, 133 | "no-host-metadata-property": true, 134 | "no-input-rename": true, 135 | "no-inputs-metadata-property": true, 136 | "no-output-native": true, 137 | "no-output-on-prefix": true, 138 | "no-output-rename": true, 139 | "no-outputs-metadata-property": true, 140 | "template-banana-in-box": true, 141 | "template-no-negated-async": true, 142 | "use-lifecycle-interface": true, 143 | "use-pipe-transform-interface": true 144 | }, 145 | "rulesDirectory": [ 146 | "codelyzer" 147 | ] 148 | } --------------------------------------------------------------------------------