├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .gitlab-ci.yml ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── ionic.config.json ├── ng-package.json ├── package-lock.json ├── package.json ├── public_api.ts ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ └── home │ │ ├── home.module.ts │ │ ├── home.page.html │ │ ├── home.page.scss │ │ ├── home.page.spec.ts │ │ └── home.page.ts ├── assets │ └── icon │ │ └── favicon.png ├── demos │ ├── component-basic.ts │ ├── demo-modal-basic.ts │ ├── demo-multi.ts │ ├── demo-options.ts │ ├── demos.module.ts │ └── sub-header-calendar-modal.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── global.scss ├── index.html ├── karma.conf.js ├── main.ts ├── moots-picker │ ├── calendar.controller.ts │ ├── calendar.model.ts │ ├── components │ │ ├── calendar-week.component.scss │ │ ├── calendar-week.component.ts │ │ ├── calendar.component.html │ │ ├── calendar.component.scss │ │ ├── calendar.component.ts │ │ ├── calendar.modal.html │ │ ├── calendar.modal.scss │ │ ├── calendar.modal.ts │ │ ├── clock-picker.component.html │ │ ├── clock-picker.component.scss │ │ ├── clock-picker.component.ts │ │ ├── index.ts │ │ ├── month-picker.component.scss │ │ ├── month-picker.component.ts │ │ ├── month.component.html │ │ ├── month.component.scss │ │ └── month.component.ts │ ├── config.ts │ ├── index.scss │ ├── index.ts │ ├── mixins.scss │ ├── moots-picker.module.ts │ └── services │ │ └── calendar.service.ts ├── polyfills.ts ├── test.ts ├── tests │ ├── modal-basic.spec.ts │ ├── modal-basic.ts │ └── test-mocks.ts ├── theme │ └── variables.scss ├── tsconfig.app.json └── tsconfig.spec.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | indent_style = space 4 | indent_size = 4 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "parserOptions": { 12 | "project": [ 13 | "tsconfig.json", 14 | "tsconfig.spec.json", 15 | "e2e/tsconfig.e2e.json" 16 | ], 17 | "createDefaultProgram": true 18 | }, 19 | "extends": [ 20 | "plugin:@angular-eslint/recommended", 21 | "plugin:@angular-eslint/template/process-inline-templates", 22 | "plugin:import/typescript" 23 | ], 24 | "plugins": [ 25 | "import", 26 | "eslint-plugin-no-null" 27 | ], 28 | "rules": { 29 | "no-unused-vars": "off", 30 | "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 31 | "@typescript-eslint/array-type": [ 32 | "error", 33 | { 34 | "default": "array" 35 | } 36 | ], 37 | "@typescript-eslint/await-thenable": "error", 38 | "@typescript-eslint/dot-notation": "off", 39 | "@typescript-eslint/explicit-member-accessibility": [ 40 | "error", 41 | { 42 | "accessibility": "no-public" 43 | } 44 | ], 45 | "@typescript-eslint/member-delimiter-style": [ 46 | "error", 47 | { 48 | "multiline": { 49 | "delimiter": "semi", 50 | "requireLast": true 51 | }, 52 | "singleline": { 53 | "delimiter": "semi", 54 | "requireLast": false 55 | } 56 | } 57 | ], 58 | "@typescript-eslint/semi": "error", 59 | "@typescript-eslint/member-ordering": "off", 60 | "@typescript-eslint/no-empty-function": "error", 61 | "@typescript-eslint/no-floating-promises": "off", 62 | "@typescript-eslint/no-inferrable-types": "error", 63 | "@typescript-eslint/no-namespace": "off", 64 | "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error", 65 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 66 | "@typescript-eslint/no-var-requires": "error", 67 | "@typescript-eslint/unbound-method": "error", 68 | "@typescript-eslint/unified-signatures": "off", 69 | "@angular-eslint/component-selector": [ 70 | "error", 71 | { 72 | "prefix": "app", 73 | "style": "kebab-case", 74 | "type": "element" 75 | } 76 | ], 77 | "@angular-eslint/directive-selector": [ 78 | "error", 79 | { 80 | "prefix": "app", 81 | "style": "camelCase", 82 | "type": "attribute" 83 | } 84 | ], 85 | "@angular-eslint/component-class-suffix" : "off", 86 | "@typescript-eslint/consistent-type-assertions": "error", 87 | "@typescript-eslint/prefer-for-of": "error", 88 | "@typescript-eslint/prefer-optional-chain": "error", 89 | "@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }], 90 | "import/first": "error", 91 | "import/order": ["error", { "alphabetize": { "order": "asc", "caseInsensitive": false }, "groups": [["builtin", "external"], "parent", ["sibling", "index"]], "newlines-between": "always" }], 92 | "import/newline-after-import": "error", 93 | "import/no-duplicates": "error", 94 | "import/no-mutable-exports": "error", 95 | "object-shorthand": ["error", "always"], 96 | "quotes": ["error", "single"], 97 | "prefer-const": "error", 98 | "no-var": "error", 99 | "linebreak-style": "off", 100 | "max-len": "off", 101 | "no-bitwise": "off", 102 | "no-constant-condition": "error", 103 | "no-control-regex": "error", 104 | "no-duplicate-case": "error", 105 | "no-duplicate-imports": "error", 106 | "no-empty": "error", 107 | "no-fallthrough": "off", 108 | "no-invalid-regexp": "error", 109 | "no-irregular-whitespace": "error", 110 | "no-multiple-empty-lines": "error", 111 | "no-null/no-null": "error", 112 | "no-redeclare": "error", 113 | "no-regex-spaces": "error", 114 | "no-return-await": "error", 115 | "no-sequences": "error", 116 | "no-shadow": "off", 117 | "@typescript-eslint/no-shadow": ["error"], 118 | "no-sparse-arrays": "error", 119 | "no-template-curly-in-string": "error", 120 | "prefer-object-spread": "error", 121 | "quote-props": "off", 122 | "space-in-parens": ["error", "never"] 123 | } 124 | }, 125 | { 126 | "files": [ 127 | "*.html" 128 | ], 129 | "extends": [ 130 | "plugin:@angular-eslint/template/recommended" 131 | ], 132 | "rules": {} 133 | } 134 | ] 135 | } 136 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm install -g npm@^7.0.0 30 | - run: npm i 31 | - run: npm run build 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | *.log 7 | *.tmp 8 | *.tmp.* 9 | log.txt 10 | *.sublime-project 11 | *.sublime-workspace 12 | .vscode/ 13 | npm-debug.log* 14 | 15 | .idea/ 16 | .sourcemaps/ 17 | .sass-cache/ 18 | .tmp/ 19 | .versions/ 20 | coverage/ 21 | dist/ 22 | node_modules/ 23 | tmp/ 24 | temp/ 25 | hooks/ 26 | platforms/ 27 | plugins/ 28 | plugins/android.json 29 | plugins/ios.json 30 | www/ 31 | $RECYCLE.BIN/ 32 | 33 | .DS_Store 34 | Thumbs.db 35 | UserInterfaceState.xcuserstate 36 | .angulardoc.json 37 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: gitlab.moots.io:55550/docker-images/rune-ionic-chrome:master 2 | 3 | variables: 4 | MAVEN_OPTS: "-Dmaven.repo.local=.m2" 5 | 6 | stages: 7 | - build 8 | 9 | cache: 10 | paths: 11 | - .m2/ 12 | 13 | before_script: 14 | - npm install npm@latest -g 15 | - npm install @angular/cli -g 16 | - npm i 17 | 18 | build: 19 | stage: build 20 | script: 21 | - ionic build 22 | 23 | test: 24 | stage: build 25 | script: 26 | - npm run lint 27 | - npm run test-headless 28 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Autogenerated code 2 | /coverage 3 | /dist 4 | /node_modules 5 | /test-reports 6 | /.idea 7 | /amplify 8 | 9 | # Cypress 10 | /cypress/screenshots 11 | /cypress/videos 12 | /cypress/downloads 13 | /cypress/fixtures/example.json 14 | 15 | # Extra files 16 | *.md 17 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "singleQuote": true, 4 | "printWidth": 140, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Run-e 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 | ![Build](https://github.com/moots-technology/moots-datetime-picker/actions/workflows/node.js.yml/badge.svg) 2 | 3 | # Moots Datetime Picker 4 | 5 | The most intuitive and beautiful date & time picker for ionic. Moots datetime picker allows you to set single dates or date ranges in an easy and intuitive way. It features a calendar and clock picker with support for 12 and 24 hour mode. Choose your desired date with the help of our beautiful and easy-to-use interface. Dark mode supported. 6 | 7 | ![140523162-9b537321-d76a-442b-bd1a-edd5a6bf411c](https://user-images.githubusercontent.com/59689061/154686013-09a8e47b-414b-4854-8ab2-f43cc1822c0e.png)![140523171-c4ac6d2a-de5d-4b81-a48a-c1d07a23b6fe](https://user-images.githubusercontent.com/59689061/154686022-b1ed9517-836d-494c-b864-3cde4a185fa3.png) 8 | 9 | ![140523186-0554ae75-cad9-4c84-8b05-9629cfec4ac9](https://user-images.githubusercontent.com/59689061/154686041-189e4ec8-fd54-4677-bdc7-180a7d5c2915.png)![140523196-fdb91465-3052-4fd9-8e23-65563e2aa978](https://user-images.githubusercontent.com/59689061/154686051-7674b32d-11bf-4e8b-a128-14f0b65cb65f.png) 10 | 11 | 12 | # Versions 13 | 14 | | Datetime Picker-Version | Angular-Version | 15 | |---|---| 16 | | <=0.2.9 | Angular 8 | 17 | | >=0.3.0 | Angular 12 | 18 | 19 | 20 | # Live Demo 21 | 22 | Please find a live demo on [Stackblitz](https://moots-picker-demo.stackblitz.io) 23 | 24 | Notes: 25 | - Certain features might not work properly on stackblitz - but work in a real project 26 | - The clock picker is optimised for touch control, thus set your view to a mobile device 27 | 28 | # Install 29 | 30 | Dependencies: 31 | 32 | `npm i luxon @angular/cdk @angular/flex-layout @angular/animations` 33 | 34 | The picker: 35 | 36 | `npm i moots-datetime-picker` 37 | 38 | # Note about time zones 39 | 40 | The picker is time zone agnostic. All input is expected to be in UTC, all calculations are done without regard to user time zone and locale, and all output is in UTC. When you select a certain date and time on the picker, you will get that displayed date and time in UTC format. Any locale specific transformations must happen outside of the picker. 41 | 42 | # Usage 43 | 44 | Import the `MootsPickerModule` and dependencies in your `AppModule`: 45 | 46 | ```ts 47 | @NgModule({ 48 | ... 49 | imports: [ 50 | IonicModule.forRoot(MyApp), 51 | FlexLayoutModule, 52 | BrowserAnimationsModule, 53 | MootsPickerModule 54 | ], 55 | ... 56 | }) 57 | export class AppModule {} 58 | ``` 59 | 60 | Please find below an example as a quick start guide to get the picker running. 61 | 62 | ## Modal 63 | 64 | ```ts 65 | export class DemoModalBasicComponent { 66 | date = new DateTime(); 67 | dateRange = { 68 | from: this.date.valueOf(), 69 | to: this.date.valueOf() 70 | }; 71 | 72 | myCalendar; 73 | 74 | constructor(public modalCtrl: ModalController) {} 75 | 76 | async openCalendar() { 77 | const options: PickerModalOptions = { 78 | pickMode: PickMode.RANGE, 79 | title: 'RANGE', 80 | defaultDateRange: this.dateRange, 81 | weekStart: 1, 82 | step: 4, 83 | locale: 'de' 84 | }; 85 | 86 | this.myCalendar = await this.modalCtrl.create({ 87 | component: PickerModal, 88 | componentProps: { options } 89 | }); 90 | 91 | this.myCalendar.present(); 92 | 93 | const event: any = await this.myCalendar.onDidDismiss(); 94 | const { data: date, role } = event; 95 | 96 | if (role === 'done') { 97 | const startDate = DateTime.fromMillis(event.data.from, { zone: 'Etc/UTC' }); 98 | const endDate = DateTime.fromMillis(event.data.to, { zone: 'Etc/UTC' }); 99 | console.log(startDate); 100 | console.log(endDate); 101 | } 102 | 103 | console.log('role', role); 104 | } 105 | } 106 | ``` 107 | 108 | ## Component 109 | 110 | Coming soon! 111 | 112 | # Development Notes 113 | 114 | To release a new version, commit all your changes and run: 115 | - `npm version patch` to increment the version 116 | - `npm run packagr` to build the library package 117 | - `npm publish dist` to pubish it to npmjs 118 | 119 | # About 120 | 121 | [moots technology](https://mootstech.com.au) is an Adelaide, South Australia based consultancy and software development company with a huge expertise in usage requirements analysis and cloud architecture frameworks for creating modern software solutions. Hereby we prioritise high usability and amazing UX over adding further features. 122 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "defaultProject": "app", 5 | "projects": { 6 | "app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "progress": false, 17 | "outputPath": "www", 18 | "index": "src/index.html", 19 | "main": "src/main.ts", 20 | "polyfills": "src/polyfills.ts", 21 | "tsConfig": "src/tsconfig.app.json", 22 | "assets": [ 23 | { 24 | "glob": "**/*", 25 | "input": "src/assets", 26 | "output": "assets" 27 | }, 28 | { 29 | "glob": "**/*.svg", 30 | "input": "node_modules/ionicons/dist/ionicons/svg", 31 | "output": "./svg" 32 | } 33 | ], 34 | "styles": ["src/theme/variables.scss", "src/global.scss"], 35 | "scripts": [], 36 | "aot": false, 37 | "vendorChunk": true, 38 | "extractLicenses": false, 39 | "buildOptimizer": false, 40 | "sourceMap": true, 41 | "optimization": false, 42 | "namedChunks": true 43 | }, 44 | "configurations": { 45 | "production": { 46 | "fileReplacements": [ 47 | { 48 | "replace": "src/environments/environment.ts", 49 | "with": "src/environments/environment.prod.ts" 50 | } 51 | ], 52 | "optimization": true, 53 | "outputHashing": "all", 54 | "sourceMap": false, 55 | "namedChunks": false, 56 | "aot": true, 57 | "extractLicenses": true, 58 | "vendorChunk": false, 59 | "buildOptimizer": true, 60 | "budgets": [ 61 | { 62 | "type": "initial", 63 | "maximumWarning": "2mb", 64 | "maximumError": "5mb" 65 | } 66 | ] 67 | } 68 | }, 69 | "defaultConfiguration": "" 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "options": { 74 | "browserTarget": "app:build" 75 | }, 76 | "configurations": { 77 | "production": { 78 | "browserTarget": "app:build:production" 79 | } 80 | } 81 | }, 82 | "extract-i18n": { 83 | "builder": "@angular-devkit/build-angular:extract-i18n", 84 | "options": { 85 | "browserTarget": "app:build" 86 | } 87 | }, 88 | "test": { 89 | "builder": "@angular-devkit/build-angular:karma", 90 | "options": { 91 | "main": "src/test.ts", 92 | "polyfills": "src/polyfills.ts", 93 | "tsConfig": "src/tsconfig.spec.json", 94 | "karmaConfig": "src/karma.conf.js", 95 | "scripts": [], 96 | "assets": [ 97 | { 98 | "glob": "favicon.ico", 99 | "input": "src/", 100 | "output": "/" 101 | }, 102 | { 103 | "glob": "**/*", 104 | "input": "src/assets", 105 | "output": "/assets" 106 | } 107 | ] 108 | } 109 | }, 110 | "lint": { 111 | "builder": "@angular-eslint/builder:lint", 112 | "options": { 113 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 114 | } 115 | }, 116 | "ionic-cordova-build": { 117 | "builder": "@ionic/ng-toolkit:cordova-build", 118 | "options": { 119 | "browserTarget": "app:build" 120 | }, 121 | "configurations": { 122 | "production": { 123 | "browserTarget": "app:build:production" 124 | } 125 | } 126 | }, 127 | "ionic-cordova-serve": { 128 | "builder": "@ionic/ng-toolkit:cordova-serve", 129 | "options": { 130 | "cordovaBuildTarget": "app:ionic-cordova-build", 131 | "devServerTarget": "app:serve" 132 | }, 133 | "configurations": { 134 | "production": { 135 | "cordovaBuildTarget": "app:ionic-cordova-build:production", 136 | "devServerTarget": "app:serve:production" 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | "app-e2e": { 143 | "root": "e2e/", 144 | "projectType": "application", 145 | "architect": { 146 | "e2e": { 147 | "builder": "@angular-devkit/build-angular:protractor", 148 | "options": { 149 | "protractorConfig": "e2e/protractor.conf.js", 150 | "devServerTarget": "app:serve" 151 | } 152 | }, 153 | "lint": { 154 | "builder": "@angular-eslint/builder:lint", 155 | "options": { 156 | "lintFilePatterns": ["e2e/src/**/*.ts", "e2e/src/**/*.html"] 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "cli": { 163 | "defaultCollection": "@ionic/schematics-angular" 164 | }, 165 | "schematics": { 166 | "@ionic/schematics-angular:component": { 167 | "styleext": "scss" 168 | }, 169 | "@ionic/schematics-angular:page": { 170 | "styleext": "scss" 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('new App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toContain('The world is your oyster.'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.deepCss('app-root ion-content')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "integrations": {}, 4 | "type": "angular" 5 | } -------------------------------------------------------------------------------- /ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/ng-packagr/ng-package.schema.json", 3 | "lib": { 4 | "entryFile": "public_api.ts" 5 | }, 6 | "whitelistedNonPeerDependencies": ["."] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moots-datetime-picker", 3 | "version": "0.5.0", 4 | "description": "Combination of a calendar datepicker and clock timepicker into one component for ionic 4.", 5 | "author": "run-e software", 6 | "repository": "github:moots-technology/moots-datetime-picker", 7 | "license": "MIT", 8 | "keywords": [ 9 | "ionic", 10 | "calendar", 11 | "datepicker", 12 | "timepicker", 13 | "datetime", 14 | "picker", 15 | "clock" 16 | ], 17 | "scripts": { 18 | "ng": "ng", 19 | "start": "ng serve", 20 | "build": "ng build", 21 | "test": "ng test", 22 | "test-headless": "ng test --browsers=ChromeHeadlessNoSandbox --watch=false", 23 | "lint": "ng lint", 24 | "e2e": "ng e2e", 25 | "packagr": "ng-packagr -p package.json" 26 | }, 27 | "peerDependencies": { 28 | "@types/luxon": "^2.0.5", 29 | "luxon": "^2.0.2" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "^12.2.5", 33 | "@angular-devkit/core": "^12.2.5", 34 | "@angular-devkit/schematics": "^12.2.5", 35 | "@angular/animations": "^12.2.5", 36 | "@angular/cdk": "^12.2.5", 37 | "@angular/cli": "^12.2.5", 38 | "@angular/common": "^12.2.5", 39 | "@angular/compiler": "^12.2.5", 40 | "@angular/compiler-cli": "^12.2.5", 41 | "@angular/core": "^12.2.5", 42 | "@angular-eslint/builder": "^12.2.5", 43 | "@angular-eslint/eslint-plugin": "^12.2.5", 44 | "@angular-eslint/eslint-plugin-template": "^12.2.5", 45 | "@angular-eslint/schematics": "^12.2.5", 46 | "@angular-eslint/template-parser": "^12.2.5", 47 | "@angular/flex-layout": "^12.0.0-beta.34", 48 | "@angular/forms": "^12.2.5", 49 | "@angular/platform-browser": "^12.2.5", 50 | "@angular/platform-browser-dynamic": "^12.2.5", 51 | "@angular/router": "^12.2.5", 52 | "@ionic/angular": "^5.5.2", 53 | "@ionic/angular-toolkit": "^4.0.0", 54 | "@types/jasmine": "~3.6.0", 55 | "@types/jasminewd2": "~2.0.3", 56 | "@types/node": "^12.11.1", 57 | "@typescript-eslint/eslint-plugin": "^5.2.0", 58 | "@typescript-eslint/parser": "^5.2.0", 59 | "codelyzer": "^6.0.0", 60 | "core-js": "^3.6.4", 61 | "eslint": "^8.1.0", 62 | "jasmine-core": "~3.6.0", 63 | "jasmine-spec-reporter": "~5.0.0", 64 | "karma": "~6.3.4", 65 | "karma-chrome-launcher": "~3.1.0", 66 | "karma-coverage-istanbul-reporter": "~3.0.2", 67 | "karma-jasmine": "~4.0.0", 68 | "karma-jasmine-html-reporter": "~1.5.0", 69 | "ng-packagr": "^12.2.1", 70 | "protractor": "~7.0.0", 71 | "puppeteer": "^2.0.0", 72 | "ts-node": "~8.4.1", 73 | "tslib": "^2.0.0", 74 | "typescript": "4.3.5", 75 | "zone.js": "~0.11.4", 76 | "eslint-plugin-import": "^2.25.2", 77 | "eslint-plugin-no-null": "^1.0.2" 78 | }, 79 | "$schema": "./node_modules/ng-packagr/package.schema.json", 80 | "ngPackage": { 81 | "lib": { 82 | "entryFile": "src/moots-picker/index.ts", 83 | "umdModuleIds": { 84 | "@angular/cdk": "ng.cdk", 85 | "@angular/flex-layout": "ng.flex-layout", 86 | "@ionic/angular": "ionic.ng" 87 | }, 88 | "styleIncludePaths": [ 89 | "" 90 | ] 91 | }, 92 | "allowedNonPeerDependencies": [ 93 | "@angular/cdk", 94 | "@angular/flex-layout" 95 | ] 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /public_api.ts: -------------------------------------------------------------------------------- 1 | export * from './src/moots-picker/index'; 2 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FlexLayoutModule } from '@angular/flex-layout'; 3 | import { RouterModule, Routes } from '@angular/router'; 4 | 5 | const routes: Routes = [ 6 | { path: '', redirectTo: 'home', pathMatch: 'full' }, 7 | { path: 'home', loadChildren: () => import('./home/home.module').then(m => m.HomePageModule) }, 8 | ]; 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' }), FlexLayoutModule], 12 | exports: [RouterModule] 13 | }) 14 | export class AppRoutingModule { } 15 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { TestBed, waitForAsync } from '@angular/core/testing'; 3 | import { Platform } from '@ionic/angular'; 4 | 5 | import { AppComponent } from './app.component'; 6 | 7 | describe('AppComponent', () => { 8 | 9 | let platformReadySpy: Promise; 10 | let platformSpy: { ready: any; }; 11 | 12 | beforeEach(waitForAsync(() => { 13 | platformReadySpy = Promise.resolve(); 14 | platformSpy = jasmine.createSpyObj('Platform', { ready: platformReadySpy }); 15 | 16 | TestBed.configureTestingModule({ 17 | declarations: [AppComponent], 18 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 19 | providers: [ 20 | { provide: Platform, useValue: platformSpy }, 21 | ], 22 | }).compileComponents(); 23 | })); 24 | 25 | it('should create the app', () => { 26 | const fixture = TestBed.createComponent(AppComponent); 27 | const app = fixture.debugElement.componentInstance; 28 | expect(app).toBeTruthy(); 29 | }); 30 | 31 | it('should initialize the app', async () => { 32 | TestBed.createComponent(AppComponent); 33 | expect(platformSpy.ready).toHaveBeenCalled(); 34 | await platformReadySpy; 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Platform } from '@ionic/angular'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: 'app.component.html' 7 | }) 8 | export class AppComponent { 9 | constructor( 10 | private platform: Platform 11 | ) { 12 | this.initializeApp(); 13 | } 14 | 15 | initializeApp() { 16 | this.platform.ready().then(() => { 17 | // 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FlexLayoutModule } from '@angular/flex-layout'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { RouteReuseStrategy } from '@angular/router'; 6 | import { IonicModule, IonicRouteStrategy } from '@ionic/angular'; 7 | 8 | import { DemosModule } from '../demos/demos.module'; 9 | import { MootsPickerModule } from '../moots-picker'; 10 | 11 | import { AppRoutingModule } from './app-routing.module'; 12 | import { AppComponent } from './app.component'; 13 | 14 | @NgModule({ 15 | declarations: [AppComponent], 16 | entryComponents: [], 17 | imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, MootsPickerModule, DemosModule, BrowserAnimationsModule, FlexLayoutModule], 18 | providers: [{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }], 19 | bootstrap: [AppComponent], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { RouterModule } from '@angular/router'; 6 | import { IonicModule } from '@ionic/angular'; 7 | 8 | import { DemosModule } from '../../demos/demos.module'; 9 | import { MootsPickerModule } from '../../moots-picker'; 10 | 11 | import { HomePage } from './home.page'; 12 | 13 | @NgModule({ 14 | imports: [ 15 | CommonModule, 16 | FormsModule, 17 | IonicModule, 18 | MootsPickerModule, 19 | DemosModule, 20 | RouterModule.forChild([ 21 | { 22 | path: '', 23 | component: HomePage, 24 | }, 25 | ]), 26 | FlexLayoutModule 27 | ], 28 | declarations: [HomePage], 29 | }) 30 | export class HomePageModule {} 31 | -------------------------------------------------------------------------------- /src/app/home/home.page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Picker Demo 5 | 6 | 7 | 8 | 9 | 10 |

modal mode

11 | 12 | 13 |

component mode

14 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/home/home.page.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moots-technology/moots-datetime-picker/17e57f553e8f97b19ff607960ec5dbefeddf504d/src/app/home/home.page.scss -------------------------------------------------------------------------------- /src/app/home/home.page.spec.ts: -------------------------------------------------------------------------------- 1 | import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 3 | 4 | import { HomePage } from './home.page'; 5 | 6 | describe('HomePage', () => { 7 | let component: HomePage; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(waitForAsync(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [HomePage], 13 | schemas: [CUSTOM_ELEMENTS_SCHEMA] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(HomePage); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | 27 | it('should create modal', () => { 28 | component.init(); 29 | expect(component.modal).toBeTruthy(); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/home/home.page.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { DemoModalBasicComponent } from '../../demos/demo-modal-basic'; 4 | 5 | function createSpyObj(_baseName: string, methodNames: string[]) { 6 | const obj: any = {}; 7 | for (let i = 0; i < methodNames.length; i++) { 8 | obj[methodNames[i]] = () => {}; 9 | } 10 | return obj; 11 | } 12 | 13 | class ModalControllerMock { 14 | public static instance(modalMock?: ModalMock): any { 15 | const instance = createSpyObj("ModalController", ["create"]); 16 | instance.create.and.returnValue(modalMock || ModalMock.instance()); 17 | 18 | return instance; 19 | } 20 | } 21 | 22 | class ModalMock { 23 | public static instance(): any { 24 | let _dismissCallback: Function; 25 | const instance = createSpyObj("Modal", [ 26 | "present", 27 | "dismiss", 28 | "onDidDismiss", 29 | ]); 30 | instance.present.and.returnValue(Promise.resolve()); 31 | 32 | instance.dismiss.and.callFake(() => { 33 | _dismissCallback(); 34 | return Promise.resolve(); 35 | }); 36 | 37 | instance.onDidDismiss.and.callFake((callback: Function) => { 38 | _dismissCallback = callback; 39 | }); 40 | 41 | return instance; 42 | } 43 | } 44 | 45 | @Component({ 46 | selector: 'app-home', 47 | templateUrl: 'home.page.html', 48 | styleUrls: ['home.page.scss'], 49 | }) 50 | export class HomePage { 51 | modal: DemoModalBasicComponent; 52 | 53 | init() { 54 | this.modal = new DemoModalBasicComponent(ModalControllerMock.instance()); 55 | this.modal.openCalendar(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moots-technology/moots-datetime-picker/17e57f553e8f97b19ff607960ec5dbefeddf504d/src/assets/icon/favicon.png -------------------------------------------------------------------------------- /src/demos/component-basic.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { DateTime } from 'luxon'; 3 | 4 | import { CalendarComponentOptions } from '../moots-picker'; 5 | 6 | @Component({ 7 | selector: 'demo-component-basic', 8 | template: ` 9 |
10 |

basic

11 | 12 | 13 | ` 14 | }) 15 | export class DemoComponentBasicComponent { 16 | date: DateTime = DateTime.utc(); 17 | options: CalendarComponentOptions = { 18 | from: this.date.toMillis() 19 | }; 20 | 21 | constructor() { 22 | /**/ 23 | } 24 | 25 | onChange($event: any) { 26 | console.log($event); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/demos/demo-modal-basic.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { DateTime } from 'luxon'; 4 | 5 | import { PickMode, PickerModal, PickerModalOptions } from '../moots-picker'; 6 | 7 | @Component({ 8 | selector: 'demo-modal-basic', 9 | template: ` basic ` 10 | }) 11 | export class DemoModalBasicComponent { 12 | date = DateTime.utc(); 13 | dateRange = { 14 | from: this.date.plus({ hours: 1 }).toMillis(), 15 | to: this.date.plus({ hours: 1 }).toMillis() 16 | }; 17 | 18 | myCalendar: HTMLIonModalElement; 19 | 20 | constructor(public modalCtrl: ModalController) {} 21 | 22 | async openCalendar() { 23 | const options: PickerModalOptions = { 24 | pickMode: PickMode.RANGE, 25 | title: 'RANGE', 26 | defaultDateRange: this.dateRange, 27 | weekStart: 1, 28 | step: 4, 29 | locale: 'en-GB' 30 | }; 31 | this.myCalendar = await this.modalCtrl.create({ 32 | component: PickerModal, 33 | componentProps: { options } 34 | }); 35 | 36 | this.myCalendar.present(); 37 | 38 | const event: any = await this.myCalendar.onDidDismiss(); 39 | const { data: date, role } = event; 40 | 41 | if (role === 'done') { 42 | const from = DateTime.fromMillis(date.from).toUTC(); 43 | const to = DateTime.fromMillis(date.to).toUTC(); 44 | 45 | console.log(from.toString(), to.toString()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/demos/demo-multi.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { DateTime } from 'luxon'; 4 | 5 | import { CalendarComponentOptions, PickMode } from '../moots-picker'; 6 | 7 | @Component({ 8 | selector: 'demo-multi', 9 | template: ` 10 |
11 |

multi

12 | 13 | 14 | ` 15 | }) 16 | export class DemoMultiComponent { 17 | date: string[] = ['2018-01-01', '2018-01-02', '2018-01-05']; 18 | options: CalendarComponentOptions = { 19 | from: DateTime.fromJSDate(new Date(2000, 0, 1)).toMillis(), 20 | pickMode: PickMode.MULTI 21 | }; 22 | 23 | constructor(public modalCtrl: ModalController) {} 24 | 25 | onChange($event: any) { 26 | console.log($event); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/demos/demo-options.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { DateTime } from 'luxon'; 4 | 5 | import { CalendarComponentOptions } from '../moots-picker'; 6 | 7 | @Component({ 8 | selector: 'demo-options', 9 | template: ` 10 |
11 |

options

12 | 13 | 14 | colors 15 | 16 | primary 17 | secondary 18 | danger 19 | light 20 | dark 21 | 22 | 23 | 24 | disableWeeks 25 | 26 | 0 27 | 1 28 | 2 29 | 3 30 | 4 31 | 5 32 | 6 33 | 34 | 35 | 36 | weekStart 37 | 38 | 0 39 | 1 40 | 41 | 42 | 43 | showToggleButtons 44 | 45 | 46 | 47 | showMonthPicker 48 | 49 | 50 | 51 | 52 | 53 | 54 | ` 55 | }) 56 | export class DemoOptionsComponent { 57 | _color = 'primary'; 58 | _showToggleButtons = true; 59 | _showMonthPicker = true; 60 | _disableWeeks: number[] = [0, 6]; 61 | _weekStart = 0; 62 | date = '2018-01-01'; 63 | options: CalendarComponentOptions = { 64 | from: DateTime.fromJSDate(new Date(2000, 0, 1)).toMillis(), 65 | disableWeeks: [...this._disableWeeks] 66 | }; 67 | 68 | constructor(public modalCtrl: ModalController) {} 69 | 70 | onChange($event: any) { 71 | console.log($event); 72 | } 73 | 74 | _changeColors(color: string) { 75 | this.options = { 76 | ...this.options, 77 | color 78 | }; 79 | } 80 | 81 | _changeShowToggleButtons(showToggleButtons: boolean) { 82 | this.options = { 83 | ...this.options, 84 | showToggleButtons 85 | }; 86 | } 87 | 88 | _changeShowMonthPicker(showMonthPicker: boolean) { 89 | this.options = { 90 | ...this.options, 91 | showMonthPicker 92 | }; 93 | } 94 | 95 | _changeDisableWeeks(disableWeeks: string[]) { 96 | this.options = { 97 | ...this.options, 98 | disableWeeks: disableWeeks.map((e) => parseInt(e, 10)) 99 | }; 100 | } 101 | 102 | _changeWeekStart(weekStart: string) { 103 | this.options = { 104 | ...this.options, 105 | weekStart: parseInt(weekStart, 10) 106 | }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/demos/demos.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { IonicModule } from '@ionic/angular'; 5 | 6 | import { MootsPickerModule } from '../moots-picker'; 7 | 8 | import { DemoComponentBasicComponent } from './component-basic'; 9 | import { DemoModalBasicComponent } from './demo-modal-basic'; 10 | import { DemoMultiComponent } from './demo-multi'; 11 | import { DemoOptionsComponent } from './demo-options'; 12 | import { SubHeaderCalendarModal } from './sub-header-calendar-modal'; 13 | 14 | const COMPONENTS = [DemoModalBasicComponent, SubHeaderCalendarModal, DemoMultiComponent, DemoOptionsComponent, DemoComponentBasicComponent]; 15 | 16 | @NgModule({ 17 | declarations: [...COMPONENTS], 18 | imports: [CommonModule, IonicModule, FormsModule, MootsPickerModule], 19 | exports: [...COMPONENTS], 20 | entryComponents: [...COMPONENTS] 21 | }) 22 | export class DemosModule {} 23 | -------------------------------------------------------------------------------- /src/demos/sub-header-calendar-modal.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 | 6 |
7 | 8 | 9 | {{ 10 | d.time | date: 'dd/MM/yyyy' 11 | }} 12 | 13 |
14 |
15 | ` 16 | }) 17 | export class SubHeaderCalendarModal { 18 | toDate(p: any) { 19 | console.log(p); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * App Global CSS 3 | * ---------------------------------------------------------------------------- 4 | * Put style rules here that you want to apply globally. These styles are for 5 | * the entire app and not just one component. Additionally, this file can be 6 | * used as an entry point to import other CSS/Sass files to be included in the 7 | * output CSS. 8 | * For more information on global stylesheets, visit the documentation: 9 | * https://ionicframework.com/docs/layout/global-stylesheets 10 | */ 11 | 12 | /* Core CSS required for Ionic components to work properly */ 13 | @import '~@ionic/angular/css/core.css'; 14 | 15 | /* Basic CSS for apps built with Ionic */ 16 | @import '~@ionic/angular/css/normalize.css'; 17 | @import '~@ionic/angular/css/structure.css'; 18 | @import '~@ionic/angular/css/typography.css'; 19 | @import '~@ionic/angular/css/display.css'; 20 | 21 | /* Optional CSS utils that can be commented out */ 22 | @import '~@ionic/angular/css/padding.css'; 23 | @import '~@ionic/angular/css/float-elements.css'; 24 | @import '~@ionic/angular/css/text-alignment.css'; 25 | @import '~@ionic/angular/css/text-transformation.css'; 26 | @import '~@ionic/angular/css/flex-utils.css'; 27 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ionic App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | process.env.CHROME_BIN = require('puppeteer').executablePath() 5 | 6 | module.exports = function (config) { 7 | config.set({ 8 | basePath: '', 9 | browsers: ['Chrome', 'ChromeHeadlessNoSandbox'], 10 | customLaunchers: { 11 | ChromeHeadlessNoSandbox: { 12 | base: 'ChromeHeadless', 13 | flags: ['--no-sandbox'] 14 | } 15 | }, 16 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 17 | plugins: [ 18 | require('karma-jasmine'), 19 | require('karma-chrome-launcher'), 20 | require('karma-jasmine-html-reporter'), 21 | require('karma-coverage-istanbul-reporter'), 22 | require('@angular-devkit/build-angular/plugins/karma') 23 | ], 24 | client: { 25 | clearContext: false // leave Jasmine Spec Runner output visible in browser 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, 'coverage'), 29 | reports: ['html', 'lcovonly'], 30 | fixWebpackSourcePaths: true 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | singleRun: false 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /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.log(err)); 13 | -------------------------------------------------------------------------------- /src/moots-picker/calendar.controller.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { OverlayEventDetail } from '@ionic/core'; 4 | 5 | import { ModalOptions, PickerModalOptions } from './calendar.model'; 6 | import { PickerModal } from './components/calendar.modal'; 7 | import { CalendarService } from './services/calendar.service'; 8 | 9 | @Injectable() 10 | export class CalendarController { 11 | constructor(public modalCtrl: ModalController, public calSvc: CalendarService) {} 12 | 13 | /** 14 | * @deprecated 15 | */ 16 | openCalendar(calendarOptions: PickerModalOptions, modalOptions: ModalOptions = {}): Promise<{}> { 17 | const options = this.calSvc.safeOpt(calendarOptions); 18 | 19 | return this.modalCtrl 20 | .create({ 21 | component: PickerModal, 22 | componentProps: { 23 | options, 24 | }, 25 | ...modalOptions, 26 | }) 27 | .then((pickerModal: HTMLIonModalElement) => { 28 | pickerModal.present(); 29 | 30 | return pickerModal.onDidDismiss().then((event: OverlayEventDetail) => { 31 | return event.data ? Promise.resolve(event.data) : Promise.reject('cancelled'); 32 | }); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/moots-picker/calendar.model.ts: -------------------------------------------------------------------------------- 1 | import { AnimationBuilder } from '@ionic/core'; 2 | import { DateTime } from 'luxon'; 3 | 4 | export enum GlobalPickState { 5 | BEGIN_DATE, 6 | BEGIN_HOUR, 7 | BEGIN_MINUTE, 8 | END_DATE, 9 | END_HOUR, 10 | END_MINUTE 11 | } 12 | 13 | export enum PickMode { 14 | SINGLE, 15 | MULTI, 16 | RANGE 17 | } 18 | 19 | export interface CalendarOriginal { 20 | date: DateTime; 21 | year: number; 22 | month: number; 23 | firstWeek: number; 24 | howManyDays: number; 25 | } 26 | 27 | export interface CalendarDay { 28 | time: DateTime; 29 | isToday: boolean; 30 | selected: boolean; 31 | disable: boolean; 32 | cssClass: string; 33 | isLastMonth?: boolean; 34 | isNextMonth?: boolean; 35 | title?: string; 36 | subTitle?: string; 37 | marked?: boolean; 38 | style?: { 39 | title?: string; 40 | subTitle?: string; 41 | }; 42 | isFirst?: boolean; 43 | isLast?: boolean; 44 | } 45 | 46 | export class CalendarMonth { 47 | original: CalendarOriginal; 48 | days: CalendarDay[]; 49 | } 50 | 51 | export interface DayConfig { 52 | date: DateTime; 53 | marked?: boolean; 54 | title?: string; 55 | subTitle?: string; 56 | cssClass?: string; 57 | } 58 | 59 | export interface ModalOptions { 60 | showBackdrop?: boolean; 61 | backdropDismiss?: boolean; 62 | enterAnimation?: AnimationBuilder; 63 | leaveAnimation?: AnimationBuilder; 64 | } 65 | 66 | export interface PickerModalOptions extends CalendarOptions { 67 | autoDone?: boolean; 68 | format?: string; 69 | cssClass?: string; 70 | id?: string; 71 | isSaveHistory?: boolean; 72 | closeLabel?: string; 73 | doneLabel?: string; 74 | closeIcon?: boolean; 75 | doneIcon?: boolean; 76 | canBackwardsSelected?: boolean; 77 | title?: string; 78 | defaultScrollTo?: CalendarComponentPayloadTypes; 79 | defaultDate?: CalendarComponentPayloadTypes; 80 | defaultDates?: CalendarComponentPayloadTypes[]; 81 | defaultDateRange?: { from: CalendarComponentPayloadTypes; to?: CalendarComponentPayloadTypes } | undefined; 82 | step?: number; 83 | changeListener?: (data: any) => any; 84 | locale?: string; 85 | startLabel?: string; 86 | endLabel?: string; 87 | fulldayLabel?: string; 88 | fullday?: boolean; 89 | tapticConf?: TapticConfig; 90 | uses24Hours?: boolean; 91 | } 92 | 93 | export interface PickerModalOptionsSafe extends CalendarOptionsSafe { 94 | autoDone?: boolean; 95 | format?: string; 96 | cssClass?: string; 97 | id?: string; 98 | isSaveHistory?: boolean; 99 | closeLabel?: string; 100 | doneLabel?: string; 101 | closeIcon?: boolean; 102 | doneIcon?: boolean; 103 | canBackwardsSelected?: boolean; 104 | title?: string; 105 | defaultScrollTo?: DateTime; 106 | defaultDate?: DateTime; 107 | defaultDates?: DateTime[]; 108 | defaultDateRange?: { from: DateTime; to?: DateTime } | undefined; 109 | step?: number; 110 | changeListener?: (data: any) => any; 111 | locale?: string; 112 | startLabel?: string; 113 | endLabel?: string; 114 | fulldayLabel?: string; 115 | fullday?: boolean; 116 | tapticConf?: TapticConfig; 117 | uses24Hours?: boolean; 118 | } 119 | 120 | export interface TapticConfig { 121 | onClockHover?: () => void; 122 | onClockSelect?: () => void; 123 | onCalendarSelect?: () => void; 124 | } 125 | 126 | export interface CalendarOptions { 127 | from?: CalendarComponentPayloadTypes; 128 | to?: CalendarComponentPayloadTypes; 129 | pickMode?: PickMode; 130 | weekStart?: number; 131 | disableWeeks?: number[]; 132 | weekdays?: string[]; 133 | monthFormat?: string; 134 | color?: string; 135 | defaultTitle?: string; 136 | defaultSubtitle?: string; 137 | daysConfig?: DayConfig[]; 138 | /** 139 | * show last month & next month days fill six weeks 140 | */ 141 | showAdjacentMonthDay?: boolean; 142 | pickState?: GlobalPickState; 143 | } 144 | 145 | export interface CalendarOptionsSafe { 146 | from?: DateTime; 147 | to?: DateTime; 148 | pickMode?: PickMode; 149 | weekStart?: number; 150 | disableWeeks?: number[]; 151 | weekdays?: string[]; 152 | monthFormat?: string; 153 | color?: string; 154 | defaultTitle?: string; 155 | defaultSubtitle?: string; 156 | daysConfig?: DayConfig[]; 157 | /** 158 | * show last month & next month days fill six weeks 159 | */ 160 | showAdjacentMonthDay?: boolean; 161 | pickState?: GlobalPickState; 162 | } 163 | 164 | export interface CalendarComponentOptions extends CalendarOptions { 165 | showToggleButtons?: boolean; 166 | showMonthPicker?: boolean; 167 | monthPickerFormat?: string[]; 168 | } 169 | 170 | export class CalendarResult { 171 | time: number; 172 | unix: number; 173 | dateObj: Date; 174 | string: string; 175 | years: number; 176 | months: number; 177 | date: number; 178 | } 179 | 180 | export class CalendarComponentMonthChange { 181 | oldMonth: number; 182 | newMonth: number; 183 | } 184 | 185 | export type Colors = 'primary' | 'secondary' | 'danger' | 'light' | 'dark' | string; 186 | 187 | export type CalendarComponentPayloadTypes = Date | number; 188 | 189 | export function payloadToDateTime(payload: CalendarComponentPayloadTypes): DateTime { 190 | return payload instanceof Date ? DateTime.fromJSDate(payload, { zone: 'Etc/UTC' }) : DateTime.fromMillis(payload, { zone: 'Etc/UTC' }); 191 | } 192 | 193 | export function payloadsToDateTime(payloads: CalendarComponentPayloadTypes[]): DateTime[] { 194 | var result: DateTime[] = []; 195 | payloads.forEach((payload) => result.push(payloadToDateTime(payload))); 196 | return result; 197 | } 198 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar-week.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | ion-toolbar { 3 | --background: transparent; 4 | --min-height: 40px; 5 | } 6 | 7 | .toolbar-background-md, 8 | .toolbar-background-ios { 9 | background: transparent; 10 | } 11 | 12 | .week-toolbar { 13 | --padding-start: 0; 14 | --padding-end: 0; 15 | --padding-bottom: 0; 16 | --padding-top: 0; 17 | 18 | &.toolbar-md { 19 | min-height: 44px; 20 | } 21 | } 22 | 23 | .week-title { 24 | margin: 0; 25 | height: 44px; 26 | width: 100%; 27 | font-size: 0.9em; 28 | padding: 15px 3%; 29 | background-color: var(--ion-color-light-tint); 30 | border-bottom: solid 1px rgba(0, 0, 0, 0.3); 31 | 32 | &.light, 33 | &.transparent { 34 | color: #9e9e9e; 35 | } 36 | 37 | li { 38 | list-style-type: none; 39 | display: block; 40 | float: left; 41 | width: 14%; 42 | text-align: center; 43 | } 44 | 45 | li:nth-of-type(7n), 46 | li:nth-of-type(7n + 1) { 47 | width: 15%; 48 | } 49 | } 50 | 51 | .last-item { 52 | color: var(--ion-color-danger); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar-week.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { defaults } from '../config'; 4 | 5 | @Component({ 6 | selector: 'moots-calendar-week', 7 | styleUrls: ['./calendar-week.component.scss'], 8 | template: ` 9 | 10 |
    11 |
  • 15 | {{ w }} 16 |
  • 17 |
18 |
19 | ` 20 | }) 21 | export class CalendarWeekComponent { 22 | _weekArray: string[] = defaults.WEEKS_FORMAT; 23 | _displayWeekArray: string[] = this._weekArray; 24 | _weekStart = 0; 25 | @Input() 26 | color: string = defaults.COLOR; 27 | 28 | @Input() 29 | set weekArray(value: string[]) { 30 | if (value && value.length === 7) { 31 | this._weekArray = [...value]; 32 | this.adjustSort(); 33 | } 34 | } 35 | 36 | @Input() 37 | set weekStart(value: number) { 38 | if (value === 0 || value === 1) { 39 | this._weekStart = value; 40 | this.adjustSort(); 41 | } 42 | } 43 | 44 | adjustSort(): void { 45 | if (this._weekStart === 1) { 46 | const cacheWeekArray = [...this._weekArray]; 47 | cacheWeekArray.push(cacheWeekArray.shift()); 48 | this._displayWeekArray = [...cacheWeekArray]; 49 | } else if (this._weekStart === 0) { 50 | this._displayWeekArray = [...this._weekArray]; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {{ _monthFormat(monthOpt.original.date) }} 5 | 6 | 7 | 8 | 9 |
10 | {{ _monthFormat(monthOpt.original.date) }} 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 38 | 39 | 40 | 41 | 42 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | $btn-width: 40px; 3 | 4 | padding: 10px 20px; 5 | box-sizing: border-box; 6 | display: inline-block; 7 | background-color: var(--ion-color-light); 8 | width: 100%; 9 | 10 | .title { 11 | padding: 0 $btn-width 0 $btn-width; 12 | overflow: hidden; 13 | 14 | .back, 15 | .forward, 16 | .switch-btn { 17 | display: block; 18 | position: relative; 19 | float: left; 20 | min-height: 32px; 21 | margin: 0; 22 | padding: 0; 23 | --padding-start: 0; 24 | --padding-end: 0; 25 | font-size: 15px; 26 | } 27 | 28 | .back, 29 | .forward { 30 | color: #757575; 31 | } 32 | 33 | .back { 34 | margin-left: -100%; 35 | left: -$btn-width; 36 | width: $btn-width; 37 | } 38 | 39 | .forward { 40 | margin-left: -$btn-width; 41 | right: -$btn-width; 42 | width: $btn-width; 43 | } 44 | 45 | .switch-btn { 46 | --margin-top: 0; 47 | --margin-bottom: 0; 48 | --margin-start: auto; 49 | --margin-end: auto; 50 | 51 | width: 100%; 52 | text-align: center; 53 | line-height: 32px; 54 | color: #757575; 55 | 56 | .arrow-dropdown { 57 | margin-left: 5px; 58 | } 59 | } 60 | } 61 | 62 | .days.between { 63 | .days-btn.is-last, 64 | .days-btn.is-first { 65 | border-radius: 0; 66 | } 67 | } 68 | 69 | .component-mode { 70 | .days.startSelection.is-last-wrap { 71 | &::after { 72 | border-radius: 0; 73 | } 74 | } 75 | 76 | .days.endSelection.is-first-wrap { 77 | &::after { 78 | border-radius: 0; 79 | } 80 | } 81 | } 82 | .no-scroll { 83 | .scroll-content { 84 | overflow: hidden !important; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output, Provider, forwardRef } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | import { DateTime } from 'luxon'; 4 | import { PickerModalOptionsSafe } from '..'; 5 | 6 | import { 7 | CalendarComponentMonthChange, 8 | CalendarComponentOptions, 9 | CalendarComponentPayloadTypes, 10 | CalendarDay, 11 | CalendarMonth, 12 | PickMode 13 | } from '../calendar.model'; 14 | import { defaults } from '../config'; 15 | import { CalendarService } from '../services/calendar.service'; 16 | 17 | export const ION_CAL_VALUE_ACCESSOR: Provider = { 18 | provide: NG_VALUE_ACCESSOR, 19 | useExisting: forwardRef(() => CalendarComponent), 20 | multi: true 21 | }; 22 | 23 | @Component({ 24 | selector: 'moots-picker-calendar', 25 | providers: [ION_CAL_VALUE_ACCESSOR], 26 | styleUrls: ['./calendar.component.scss'], 27 | templateUrl: 'calendar.component.html' 28 | }) 29 | export class CalendarComponent implements ControlValueAccessor, OnInit { 30 | modalOptions: PickerModalOptionsSafe; 31 | _options: CalendarComponentOptions; 32 | _view: 'month' | 'days' = 'days'; 33 | _calendarMonthValue: CalendarDay[] = [undefined, undefined]; 34 | 35 | _showToggleButtons = true; 36 | get showToggleButtons(): boolean { 37 | return this._showToggleButtons; 38 | } 39 | 40 | set showToggleButtons(value: boolean) { 41 | this._showToggleButtons = value; 42 | } 43 | 44 | _showMonthPicker = true; 45 | get showMonthPicker(): boolean { 46 | return this._showMonthPicker; 47 | } 48 | 49 | set showMonthPicker(value: boolean) { 50 | this._showMonthPicker = value; 51 | } 52 | 53 | monthOpt: CalendarMonth; 54 | 55 | @Input() 56 | format: string = defaults.DATE_FORMAT; 57 | @Input() 58 | readonly = false; 59 | @Output() 60 | change: EventEmitter = new EventEmitter(); 61 | @Output() 62 | monthChange: EventEmitter = new EventEmitter(); 63 | @Output() 64 | select: EventEmitter = new EventEmitter(); 65 | @Output() 66 | selectStart: EventEmitter = new EventEmitter(); 67 | @Output() 68 | selectEnd: EventEmitter = new EventEmitter(); 69 | 70 | @Input() 71 | set options(value: CalendarComponentOptions) { 72 | this._options = value; 73 | this.initOpt(); 74 | if (this.monthOpt && this.monthOpt.original) { 75 | this.monthOpt = this.createMonth(this.monthOpt.original.date); 76 | } 77 | } 78 | 79 | get options(): CalendarComponentOptions { 80 | return this._options; 81 | } 82 | 83 | constructor(public calSvc: CalendarService) {} 84 | 85 | ngOnInit(): void { 86 | this.initOpt(); 87 | this.monthOpt = this.createMonth(DateTime.utc()); 88 | } 89 | 90 | getViewDate() { 91 | return this.monthOpt.original.date; 92 | } 93 | 94 | setViewDate(value: CalendarComponentPayloadTypes) { 95 | this.monthOpt = this.createMonth(this._payloadToTimeNumber(value)); 96 | } 97 | 98 | switchView(): void { 99 | this._view = this._view === 'days' ? 'month' : 'days'; 100 | } 101 | 102 | prev(): void { 103 | if (this._view === 'days') { 104 | this.backMonth(); 105 | } else { 106 | this.prevYear(); 107 | } 108 | } 109 | 110 | next(): void { 111 | if (this._view === 'days') { 112 | this.nextMonth(); 113 | } else { 114 | this.nextYear(); 115 | } 116 | } 117 | 118 | prevYear(): void { 119 | if (this.monthOpt.original.year === 1970) { 120 | return; 121 | } 122 | const backTime = this.monthOpt.original.date.minus({ years: 1 }); 123 | this.monthOpt = this.createMonth(backTime); 124 | } 125 | 126 | nextYear(): void { 127 | const nextTime = this.monthOpt.original.date.plus({ years: 1 }); 128 | this.monthOpt = this.createMonth(nextTime); 129 | } 130 | 131 | nextMonth(): void { 132 | const nextTime = this.monthOpt.original.date.plus({ months: 1 }); 133 | this.monthChange.emit({ 134 | oldMonth: this.calSvc.multiFormat(this.monthOpt.original.date.valueOf()), 135 | newMonth: this.calSvc.multiFormat(nextTime.valueOf()) 136 | }); 137 | this.monthOpt = this.createMonth(nextTime); 138 | } 139 | 140 | canNext(): boolean { 141 | if (!this.modalOptions.to || this._view !== 'days') { 142 | return true; 143 | } 144 | return this.monthOpt.original.date < this.modalOptions.to; 145 | } 146 | 147 | backMonth(): void { 148 | const backTime = this.monthOpt.original.date.minus({ months: 1 }); 149 | this.monthChange.emit({ 150 | oldMonth: this.calSvc.multiFormat(this.monthOpt.original.date.valueOf()), 151 | newMonth: this.calSvc.multiFormat(backTime.valueOf()) 152 | }); 153 | this.monthOpt = this.createMonth(backTime); 154 | } 155 | 156 | canBack(): boolean { 157 | if (!this.modalOptions.from || this._view !== 'days') { 158 | return true; 159 | } 160 | return this.monthOpt.original.date > this.modalOptions.from; 161 | } 162 | 163 | monthOnSelect(month: number): void { 164 | this._view = 'days'; 165 | const newMonth = this.monthOpt.original.date.set({ month: month }); 166 | this.monthChange.emit({ 167 | oldMonth: this.calSvc.multiFormat(this.monthOpt.original.date.valueOf()), 168 | newMonth: this.calSvc.multiFormat(newMonth.valueOf()) 169 | }); 170 | this.monthOpt = this.createMonth(newMonth); 171 | } 172 | 173 | onChanged($event: CalendarDay[]): void { 174 | switch (this.modalOptions.pickMode) { 175 | case PickMode.SINGLE: 176 | const date = $event[0].time; 177 | this._onChanged(date); 178 | this.change.emit(date.toMillis()); 179 | break; 180 | 181 | case PickMode.RANGE: 182 | if ($event[0] && $event[1]) { 183 | const rangeDate = { 184 | from: $event[0].time, 185 | to: $event[1].time 186 | }; 187 | this._onChanged(rangeDate); 188 | this.change.emit(rangeDate); 189 | } 190 | break; 191 | 192 | case PickMode.MULTI: 193 | const dates = []; 194 | 195 | for (const evnt of $event) { 196 | if (evnt && evnt.time) { 197 | dates.push(evnt.time); 198 | } 199 | } 200 | 201 | this._onChanged(dates); 202 | this.change.emit(dates); 203 | break; 204 | 205 | default: 206 | } 207 | } 208 | 209 | swipeEvent($event: any): void { 210 | const isNext = $event.deltaX < 0; 211 | if (isNext && this.canNext()) { 212 | this.nextMonth(); 213 | } else if (!isNext && this.canBack()) { 214 | this.backMonth(); 215 | } 216 | } 217 | 218 | _onChanged = (_date: any) => { 219 | /**/ 220 | }; 221 | 222 | _onTouched = () => { 223 | /**/ 224 | }; 225 | 226 | _payloadToTimeNumber(value: CalendarComponentPayloadTypes): DateTime { 227 | const date = DateTime.fromMillis(value as number, { zone: 'Etc/UTC' }); 228 | return date; 229 | } 230 | 231 | _monthFormat(date: DateTime): string { 232 | return date?.toFormat(this.modalOptions.monthFormat.replace('yyyy', '')); 233 | } 234 | 235 | private initOpt(): void { 236 | if (this._options && typeof this._options.showToggleButtons === 'boolean') { 237 | this.showToggleButtons = this._options.showToggleButtons; 238 | } 239 | if (this._options && typeof this._options.showMonthPicker === 'boolean') { 240 | this.showMonthPicker = this._options.showMonthPicker; 241 | if (this._view !== 'days' && !this.showMonthPicker) { 242 | this._view = 'days'; 243 | } 244 | } 245 | this.modalOptions = this.calSvc.safeOpt(this._options || {}); 246 | } 247 | 248 | createMonth(date: DateTime): CalendarMonth { 249 | return this.calSvc.createMonthsByPeriod(date, 1, this.modalOptions)[0]; 250 | } 251 | 252 | _createCalendarDay(_value: string): CalendarDay { 253 | return this.calSvc.createCalendarDay(this._payloadToTimeNumber(12), this.modalOptions); 254 | } 255 | 256 | writeValue(obj: any): void { 257 | this._writeValue(obj); 258 | if (obj) { 259 | if (this._calendarMonthValue[0]) { 260 | this.monthOpt = this.createMonth(this._calendarMonthValue[0].time); 261 | } else { 262 | this.monthOpt = this.createMonth(DateTime.utc()); 263 | } 264 | } 265 | } 266 | 267 | registerOnChange(fn: () => {}): void { 268 | this._onChanged = fn; 269 | } 270 | 271 | registerOnTouched(fn: () => {}): void { 272 | this._onTouched = fn; 273 | } 274 | 275 | _writeValue(value: any): void { 276 | if (!value) { 277 | this._calendarMonthValue = [undefined, undefined]; 278 | return; 279 | } 280 | 281 | switch (this.modalOptions.pickMode) { 282 | case PickMode.SINGLE: 283 | this._calendarMonthValue[0] = this._createCalendarDay(value); 284 | break; 285 | 286 | case PickMode.RANGE: 287 | if (value.from) { 288 | this._calendarMonthValue[0] = value.from ? this._createCalendarDay(value.from) : undefined; 289 | } 290 | if (value.to) { 291 | this._calendarMonthValue[1] = value.to ? this._createCalendarDay(value.to) : undefined; 292 | } 293 | break; 294 | 295 | case PickMode.MULTI: 296 | if (Array.isArray(value)) { 297 | this._calendarMonthValue = value.map((e) => { 298 | return this._createCalendarDay(e); 299 | }); 300 | } else { 301 | this._calendarMonthValue = [undefined, undefined]; 302 | } 303 | break; 304 | 305 | default: 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar.modal.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
13 |
{{ this.modalOptions.startLabel }}
14 |
{{ getDateString(0) }}
15 |
16 |
{{ getTimeHours(0) }}
17 |
:
18 |
{{ getTimeMinutes(0) }}
19 |
20 | {{ getAmPm(0) }} 21 |
22 |
23 |
24 | 25 |
34 |
{{ this.modalOptions.endLabel }}
35 |
{{ getDateString(1) }}
36 |
37 |
{{ getTimeHours(1) }}
38 |
:
39 |
{{ getTimeMinutes(1) }}
40 |
41 | {{ getAmPm(1) }} 42 |
43 |
44 |
45 |
46 |
47 | 48 | 49 |
50 | 51 |
52 |
53 | 62 |
63 |
64 | 65 | 72 | 73 | 74 | 82 |
83 | 84 |
85 |

{{ _monthFormat(month.original.date) }}

86 | 96 | 97 |
98 |
99 |
100 | 101 | 102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 | 110 | {{ modalOptions.closeLabel }} 111 | 112 | 113 | 114 | 115 | 116 | 117 | {{ modalOptions.doneLabel }} 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar.modal.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | moots-picker-modal { 3 | font-family: Roboto, "Helvetica Neue", sans-serif; 4 | } 5 | 6 | ion-select { 7 | max-width: unset; 8 | .select-icon > .select-icon-inner, 9 | .select-text { 10 | color: #fff !important; 11 | } 12 | &.select-ios { 13 | max-width: unset; 14 | } 15 | } 16 | 17 | .calendar-page { 18 | background-color: #fbfbfb; 19 | } 20 | 21 | .month-box { 22 | display: inline-block; 23 | width: 100%; 24 | padding-bottom: 1em; 25 | border-bottom: 1px solid var(--ion-color-light-shade); 26 | background-color: var(--ion-color-light); 27 | } 28 | 29 | .month-title { 30 | font-size: 1.0rem; 31 | font-weight: 500; 32 | color: var(--ion-color-dark-shade); 33 | font-weight: bold; 34 | } 35 | 36 | h4 { 37 | font-weight: 400; 38 | font-size: 1.1rem; 39 | display: block; 40 | text-align: center; 41 | margin: 1rem 0 0; 42 | color: #929292; 43 | } 44 | 45 | .header-grid { 46 | text-align: center; 47 | vertical-align: middle; 48 | width: 100%; 49 | overflow: hidden; 50 | padding: 0; 51 | } 52 | 53 | .date-col { 54 | color: rgba(255, 255, 255, 0.8); 55 | font-weight: 500; 56 | width: 30%; 57 | margin-top: 0; 58 | } 59 | 60 | h2 { 61 | margin-top: 4px; 62 | } 63 | 64 | h3 { 65 | margin-top: 8px; 66 | } 67 | 68 | .highlight-part { 69 | opacity: 1!important; 70 | color: #FFF; 71 | text-shadow: 72 | 0 0 1vw #0af, 73 | 0 0 3vw #0af, 74 | 0 0 10vw #0af, 75 | 0 0 10vw #8cf, 76 | 0 0 .4vw #FFF; 77 | } 78 | 79 | .selected-am-pm { 80 | color: green; 81 | font-size: 2rem; 82 | } 83 | 84 | .selected-time { 85 | font-size: 4rem; 86 | font-weight: bold; 87 | } 88 | 89 | .active-time-unit { 90 | color: green; 91 | } 92 | 93 | .date-string { 94 | font-size: 17px; 95 | font-weight: bold; 96 | line-height: 1.35; 97 | text-align: center; 98 | opacity: 0.8; 99 | padding: 6px 2px 6px 2px; 100 | } 101 | 102 | .time-string { 103 | font-size: 32px; 104 | font-weight: bold; 105 | font-style: normal; 106 | font-stretch: normal; 107 | line-height: 0.94; 108 | letter-spacing: normal; 109 | text-align: center; 110 | padding: 8px 2px 8px 2px; 111 | } 112 | 113 | .title-label { 114 | font-size: 21px; 115 | font-weight: 900; 116 | font-stretch: normal; 117 | font-style: normal; 118 | line-height: 1.24; 119 | letter-spacing: 0.36px; 120 | color: var(--ion-color-dark-shade); 121 | } 122 | 123 | .date-label-active { 124 | font-size: 15px; 125 | font-weight: normal; 126 | font-stretch: normal; 127 | font-style: normal; 128 | line-height: 1.33; 129 | letter-spacing: -0.24px; 130 | color: var(--ion-color-dark-shade); 131 | } 132 | 133 | .date-label { 134 | font-size: 15px; 135 | font-weight: normal; 136 | font-stretch: normal; 137 | font-style: normal; 138 | line-height: 1.33; 139 | letter-spacing: -0.24px; 140 | color: var(--ion-color-medium); 141 | } 142 | 143 | .date-label.selected { 144 | color: var(--ion-color-dark-shade); 145 | } 146 | 147 | .time-label { 148 | font-size: 32px; 149 | font-weight: 900; 150 | font-stretch: normal; 151 | font-style: normal; 152 | line-height: 0.81; 153 | letter-spacing: 0.55px; 154 | color: var(--ion-color-medium); 155 | 156 | .selected { 157 | color: var(--ion-color-dark-shade); 158 | } 159 | 160 | div { 161 | height: 25px; 162 | } 163 | 164 | } 165 | 166 | .begin-container { 167 | padding: 1em; 168 | height: 148px; 169 | border-top-left-radius: 25px; 170 | border-bottom-left-radius: 25px; 171 | background-color: var(--ion-color-light-tint); 172 | border: solid 2px transparent; 173 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.5); 174 | cursor: pointer; 175 | } 176 | 177 | .end-container { 178 | padding: 1em; 179 | height: 148px; 180 | border-top-right-radius: 25px; 181 | border-bottom-right-radius: 25px; 182 | background-color: var(--ion-color-light-tint); 183 | border: solid 2px transparent; 184 | box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.5); 185 | cursor: pointer; 186 | } 187 | 188 | .header-container { 189 | background: linear-gradient(0deg, var(--ion-color-light-tint) 50%, #1490e1 50%); 190 | padding-top: 1rem; 191 | padding-bottom: 1.25rem; 192 | } 193 | 194 | .root-container { 195 | padding-top: constant(safe-area-inset-top); //for iOS 11.2 196 | padding-top: env(safe-area-inset-top); //for iOS 11.1 197 | } 198 | 199 | .ampm-indicator { 200 | font-size: 1.5rem; 201 | margin-left: 0.25rem; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/moots-picker/components/calendar.modal.ts: -------------------------------------------------------------------------------- 1 | import { animate, state, style, transition, trigger } from '@angular/animations'; 2 | import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, HostBinding, Input, OnInit, Renderer2, ViewChild } from '@angular/core'; 3 | import { IonContent, ModalController, NavParams } from '@ionic/angular'; 4 | import { DateTime } from 'luxon'; 5 | import { CalendarDay, CalendarMonth, GlobalPickState, PickMode, PickerModalOptions, PickerModalOptionsSafe } from '../calendar.model'; 6 | import { CalendarService } from '../services/calendar.service'; 7 | 8 | import { ClockPickState, ClockPickerComponent } from './clock-picker.component'; 9 | 10 | const NUM_OF_MONTHS_TO_CREATE = 2; 11 | 12 | @Component({ 13 | selector: 'moots-picker-modal', 14 | animations: [ 15 | trigger('openClose', [ 16 | state( 17 | 'open', 18 | style({ 19 | opacity: 1 20 | }) 21 | ), 22 | state( 23 | 'closed', 24 | style({ 25 | opacity: 0 26 | }) 27 | ), 28 | transition('open => closed', [animate('0.4s')]), 29 | transition('closed => open', [animate('0.5s')]) 30 | ]), 31 | trigger('enterAnimation', [ 32 | transition(':enter', [style({ opacity: 0 }), animate('500ms', style({ opacity: 1 }))]), 33 | transition(':leave', [style({ opacity: 1 }), animate('400ms', style({ opacity: 0 }))]) 34 | ]), 35 | trigger('highlight', [ 36 | state( 37 | 'active', 38 | style({ 39 | 'box-shadow': '0 5px 15px 0 rgba(0, 0, 0, 0.5)', 40 | border: 'solid 2px #f8e71c' 41 | }) 42 | ), 43 | state( 44 | 'inactive', 45 | style({ 46 | 'box-shadow': '0 1px 3px 0 rgba(0, 0, 0, 0.5)', 47 | border: 'solid 2px transparent' 48 | }) 49 | ), 50 | transition('* => *', [animate('0.2s')]) 51 | ]) 52 | ], 53 | styleUrls: ['./calendar.modal.scss'], 54 | templateUrl: './calendar.modal.html' 55 | }) 56 | export class PickerModal implements OnInit, AfterViewInit { 57 | GlobalPickState = GlobalPickState; 58 | PickMode = PickMode; 59 | 60 | @ViewChild(IonContent, { static: true }) 61 | content: IonContent; 62 | @ViewChild('months', { static: true }) 63 | monthsEle: ElementRef; 64 | @ViewChild('clockPicker') 65 | clockPicker: ClockPickerComponent; 66 | 67 | @HostBinding('class.ion-page') 68 | ionPage = true; 69 | 70 | @Input() 71 | options: PickerModalOptions; 72 | 73 | datesTemp: CalendarDay[] = [undefined, undefined]; 74 | timesTemp: DateTime[] = [undefined, undefined]; 75 | calendarMonths: CalendarMonth[]; 76 | step: number; 77 | showYearPicker: boolean; 78 | year: number; 79 | years: number[]; 80 | _scrollLock = true; 81 | modalOptions: PickerModalOptionsSafe; 82 | actualFirstTime: DateTime; 83 | 84 | pickState = GlobalPickState.BEGIN_DATE; 85 | clockPickState = ClockPickState.HOUR; 86 | 87 | constructor( 88 | private _renderer: Renderer2, 89 | public _elementRef: ElementRef, 90 | public params: NavParams, 91 | public modalCtrl: ModalController, 92 | public ref: ChangeDetectorRef, 93 | public calSvc: CalendarService 94 | ) {} 95 | 96 | is24Hours() { 97 | return this.modalOptions.locale && this.modalOptions.uses24Hours; 98 | } 99 | 100 | onSelectChange(cstate: ClockPickState) { 101 | this.clockPickState = cstate; 102 | switch (this.pickState) { 103 | case GlobalPickState.BEGIN_HOUR: 104 | this.setPickState(GlobalPickState.BEGIN_MINUTE); 105 | break; 106 | case GlobalPickState.BEGIN_MINUTE: 107 | this.setPickState(GlobalPickState.END_DATE); 108 | break; 109 | case GlobalPickState.END_HOUR: 110 | this.setPickState(GlobalPickState.END_MINUTE); 111 | break; 112 | } 113 | } 114 | 115 | onClockValue(time: DateTime) { 116 | if (this.isBegin(this.pickState)) { 117 | this.timesTemp[0] = time; 118 | } else { 119 | this.timesTemp[1] = time; 120 | } 121 | 122 | if (this.clockPickState == ClockPickState.HOUR) { 123 | return; 124 | } 125 | 126 | this.preventInvalidRange(); 127 | 128 | switch (this.pickState) { 129 | case GlobalPickState.BEGIN_HOUR: 130 | this.setPickState(GlobalPickState.BEGIN_MINUTE); 131 | break; 132 | case GlobalPickState.BEGIN_MINUTE: 133 | this.setPickState(GlobalPickState.END_DATE); 134 | break; 135 | case GlobalPickState.END_HOUR: 136 | this.setPickState(GlobalPickState.END_MINUTE); 137 | break; 138 | } 139 | } 140 | 141 | preventInvalidRange() { 142 | if (!this.datesTemp[1] || this.datesTemp[0].time.day === this.datesTemp[1].time.day) { 143 | if (this.timesTemp[0].valueOf() > this.timesTemp[1].valueOf()) { 144 | if (this.isBegin(this.pickState)) { 145 | this.timesTemp[1] = this.timesTemp[0].plus({ minutes: 15 }); 146 | } else { 147 | const ampm = this.getAmPm(1); 148 | if (this.is24Hours() || ampm === 'pm') { 149 | this.timesTemp[0] = this.timesTemp[1].minus({ minutes: 15 }); 150 | } else { 151 | const f = this.timesTemp[1].toFormat('t'); 152 | const temp = DateTime.fromFormat(f.replace(ampm, 'pm'), 't', { zone: 'Etc/UTC' }); 153 | 154 | this.timesTemp[1] = this.timesTemp[1].set({ hour: temp.hour, minute: temp.minute }); 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | getAmPm2(input: DateTime) { 162 | const s = input.toLocaleString({ hour: 'numeric', minute: 'numeric', hour12: !this.options.uses24Hours }); 163 | return s.substring(s.length - 2).toLowerCase(); 164 | } 165 | 166 | getDateString(index: number) { 167 | if (!this.datesTemp[index]) { 168 | index--; 169 | } 170 | return this.datesTemp[index].time.toLocaleString(DateTime.DATE_FULL); 171 | } 172 | 173 | getTimeHours(index: number) { 174 | return this.timesTemp[index].toFormat(this.is24Hours() ? 'HH' : 'hh'); 175 | } 176 | 177 | getTimeMinutes(index: number) { 178 | return this.timesTemp[index].toFormat('mm'); 179 | } 180 | 181 | getAmPm(index: number) { 182 | return this.getAmPm2(this.timesTemp[index]); 183 | } 184 | 185 | setPickState(pstate: GlobalPickState) { 186 | this.pickState = pstate; 187 | if (this.isHour(pstate)) { 188 | this.clockPickState = ClockPickState.HOUR; 189 | } else if (this.isMinute(pstate)) { 190 | this.clockPickState = ClockPickState.MINUTE; 191 | } 192 | } 193 | 194 | onClickStartDate() { 195 | this.setPickState(GlobalPickState.BEGIN_DATE); 196 | this.scrollToDate(this.datesTemp[0].time); 197 | } 198 | 199 | onClickStartHour($event: Event) { 200 | this.setPickState(GlobalPickState.BEGIN_HOUR); 201 | if ($event) { 202 | $event.stopPropagation(); 203 | } 204 | } 205 | 206 | onClickStartMin($event: Event) { 207 | this.setPickState(GlobalPickState.BEGIN_MINUTE); 208 | if ($event) { 209 | $event.stopPropagation(); 210 | } 211 | } 212 | 213 | onClickEndDate() { 214 | this.setPickState(GlobalPickState.END_DATE); 215 | this.scrollToDate(this.datesTemp[0].time); 216 | } 217 | 218 | onClickEndHour($event: Event) { 219 | this.setPickState(GlobalPickState.END_HOUR); 220 | if ($event) { 221 | $event.stopPropagation(); 222 | } 223 | } 224 | 225 | onClickEndMin($event: Event) { 226 | this.setPickState(GlobalPickState.END_MINUTE); 227 | if ($event) { 228 | $event.stopPropagation(); 229 | } 230 | } 231 | 232 | ngOnInit(): void { 233 | this.init(); 234 | this.initDefaultDate(); 235 | } 236 | 237 | ngAfterViewInit(): void { 238 | this.findCssClass(); 239 | if (this.modalOptions.canBackwardsSelected) { 240 | this.backwardsMonth(); 241 | } 242 | } 243 | 244 | init(): void { 245 | this.modalOptions = this.calSvc.safeOpt(this.options); 246 | this.modalOptions.showAdjacentMonthDay = false; 247 | this.step = this.modalOptions.step; 248 | if (this.step > this.calSvc.DEFAULT_STEP) { 249 | this.step = this.calSvc.DEFAULT_STEP; 250 | } 251 | 252 | this.calendarMonths = this.calSvc.createMonthsByPeriod( 253 | this.modalOptions.from.startOf('day'), 254 | this.findInitMonthNumber(this.modalOptions.defaultScrollTo) + this.step, 255 | this.modalOptions 256 | ); 257 | 258 | this.setPickState(this.modalOptions.pickState); 259 | } 260 | 261 | initDefaultDate(): void { 262 | const { 263 | pickMode, 264 | // defaultDate, 265 | defaultDateRange, 266 | defaultDates 267 | } = this.modalOptions; 268 | switch (pickMode) { 269 | case PickMode.SINGLE: 270 | // if (defaultDate) { 271 | // this.datesTemp[0] = this.calSvc.createCalendarDay( 272 | // this._getDayTime(defaultDate), 273 | // this._d 274 | // ); 275 | // this.datesTemp[1] = this.calSvc.createCalendarDay( 276 | // this._getDayTime(defaultDate), 277 | // this._d 278 | // ); 279 | // } 280 | // if ((nowMod.minutes() % 5) > 0) { 281 | // nowMod.minutes(nowMod.minutes() - (nowMod.minutes() % 5)); 282 | // } 283 | // this.timesTemp = [nowMod.format('hh:mm a'), nowMod.format('hh:mm a')]; 284 | break; 285 | case PickMode.RANGE: 286 | if (defaultDateRange) { 287 | if (defaultDateRange.from) { 288 | this.datesTemp[0] = this.calSvc.createCalendarDay(defaultDateRange.from.startOf('day'), this.modalOptions); 289 | this.timesTemp[0] = defaultDateRange.from; 290 | } 291 | if (defaultDateRange.to) { 292 | this.datesTemp[1] = this.calSvc.createCalendarDay(defaultDateRange.to.startOf('day'), this.modalOptions); 293 | if (defaultDateRange.from >= defaultDateRange.to) { 294 | this.datesTemp[1] = undefined; 295 | this.timesTemp[1] = this.timesTemp[0].plus({ 296 | minutes: 30 297 | }); 298 | } else { 299 | this.timesTemp[1] = defaultDateRange.to; 300 | } 301 | } 302 | } 303 | if (this.timesTemp[0].minute % 5 > 0) { 304 | this.timesTemp[0] = this.timesTemp[0].set({ minute: this.timesTemp[0].minute - (this.timesTemp[0].minute % 5) }); 305 | } 306 | if (this.timesTemp[1].minute % 5 > 0) { 307 | this.timesTemp[1] = this.timesTemp[1].set({ minute: this.timesTemp[1].minute - (this.timesTemp[1].minute % 5) }); 308 | } 309 | break; 310 | case PickMode.MULTI: 311 | if (defaultDates && defaultDates.length) { 312 | this.datesTemp = defaultDates.map((e) => this.calSvc.createCalendarDay(e.startOf('day'), this.modalOptions)); 313 | } 314 | break; 315 | default: 316 | this.datesTemp = [undefined, undefined]; 317 | } 318 | } 319 | 320 | findCssClass(): void { 321 | const { cssClass } = this.modalOptions; 322 | if (cssClass) { 323 | cssClass.split(' ').forEach((_class: string) => { 324 | if (_class.trim() !== '') { 325 | this._renderer.addClass(this._elementRef.nativeElement, _class); 326 | } 327 | }); 328 | } 329 | } 330 | 331 | onChange(data: any): void { 332 | // const { pickMode, autoDone } = this._d; 333 | if (this.pickState === GlobalPickState.BEGIN_DATE) { 334 | this.datesTemp[0] = data[0]; 335 | } else if (this.pickState === GlobalPickState.END_DATE) { 336 | this.datesTemp[1] = data[1]; 337 | } 338 | 339 | this.modalOptions.tapticConf.onCalendarSelect(); 340 | this.ref.detectChanges(); 341 | 342 | // if (pickMode !== pickModes.MULTI && autoDone && this.canDone()) { 343 | // this.done(); 344 | // } 345 | 346 | this.repaintDOM(); 347 | if (this.modalOptions.changeListener) { 348 | this.modalOptions.changeListener(data); 349 | } 350 | 351 | if (this.canDone()) { 352 | if (this.pickState === GlobalPickState.END_DATE) { 353 | setTimeout(() => { 354 | if (!this.modalOptions.fullday) { 355 | this.onClickEndHour(undefined); 356 | } 357 | }, 200); 358 | } else { 359 | setTimeout(() => { 360 | if (this.modalOptions.fullday) { 361 | this.onClickEndDate(); 362 | } else { 363 | this.onClickStartHour(undefined); 364 | } 365 | }, 200); 366 | } 367 | } 368 | } 369 | 370 | onCancel(): void { 371 | this.modalCtrl.dismiss(undefined, 'cancel'); 372 | } 373 | 374 | done(): void { 375 | const { pickMode } = this.modalOptions; 376 | 377 | this.preventInvalidRange(); 378 | 379 | this.modalCtrl.dismiss(this.calSvc.wrapResult(this.datesTemp, this.timesTemp, pickMode), 'done'); 380 | } 381 | 382 | canDone(): boolean { 383 | return true; 384 | } 385 | 386 | nextMonth(event: any): void { 387 | const len = this.calendarMonths.length; 388 | const final = this.calendarMonths[len - 1]; 389 | const nextTime = final.original.date.plus({ months: 1 }); 390 | const rangeEnd = this.modalOptions.to ? this.modalOptions.to.minus({ months: 1 }) : 0; 391 | 392 | if (len <= 0 || (rangeEnd !== 0 && final.original.date < rangeEnd)) { 393 | event.target.disabled = true; 394 | return; 395 | } 396 | 397 | this.calendarMonths.push(...this.calSvc.createMonthsByPeriod(nextTime, NUM_OF_MONTHS_TO_CREATE, this.modalOptions)); 398 | event.target.complete(); 399 | this.repaintDOM(); 400 | } 401 | 402 | backwardsMonth(): void { 403 | const first = this.calendarMonths[0]; 404 | 405 | if (first.original.date.valueOf() <= 0) { 406 | this.modalOptions.canBackwardsSelected = false; 407 | return; 408 | } 409 | 410 | const firstTime = (this.actualFirstTime = first.original.date.minus({ 411 | months: NUM_OF_MONTHS_TO_CREATE 412 | })); 413 | 414 | this.calendarMonths.unshift(...this.calSvc.createMonthsByPeriod(firstTime, NUM_OF_MONTHS_TO_CREATE, this.modalOptions)); 415 | this.ref.detectChanges(); 416 | this.repaintDOM(); 417 | } 418 | 419 | scrollToDate(date: DateTime): void { 420 | const defaultDateIndex = this.findInitMonthNumber(date); 421 | const monthElement = this.monthsEle.nativeElement.children[`month-${defaultDateIndex}`]; 422 | const domElemReadyWaitTime = 300; 423 | 424 | setTimeout(() => { 425 | const defaultDateMonth = monthElement ? monthElement.offsetTop : 0; 426 | 427 | if (defaultDateIndex !== -1 && defaultDateMonth !== 0) { 428 | this.content.scrollByPoint(0, defaultDateMonth, 128); 429 | } 430 | }, domElemReadyWaitTime); 431 | } 432 | 433 | scrollToDefaultDate(): void { 434 | this.scrollToDate(this.modalOptions.defaultScrollTo); 435 | } 436 | 437 | onScroll($event: any): void { 438 | if (!this.modalOptions.canBackwardsSelected) { 439 | return; 440 | } 441 | 442 | const { detail } = $event; 443 | 444 | if (detail.scrollTop <= 200 && detail.velocityY < 0 && this._scrollLock) { 445 | this.content.getScrollElement().then((scrollElem) => { 446 | this._scrollLock = !1; 447 | 448 | const heightBeforeMonthPrepend = scrollElem.scrollHeight; 449 | this.backwardsMonth(); 450 | setTimeout(() => { 451 | const heightAfterMonthPrepend = scrollElem.scrollHeight; 452 | 453 | this.content.scrollByPoint(0, heightAfterMonthPrepend - heightBeforeMonthPrepend, 0).then(() => { 454 | this._scrollLock = !0; 455 | }); 456 | }, 180); 457 | }); 458 | } 459 | } 460 | 461 | /** 462 | * In some older Safari versions (observed at Mac's Safari 10.0), there is an issue where style updates to 463 | * shadowRoot descendants don't cause a browser repaint. 464 | * See for more details: https://github.com/Polymer/polymer/issues/4701 465 | */ 466 | repaintDOM() { 467 | return this.content.getScrollElement().then((scrollElem) => { 468 | // Update scrollElem to ensure that height of the container changes as Months are appended/prepended 469 | scrollElem.style.zIndex = '2'; 470 | scrollElem.style.zIndex = 'initial'; 471 | // Update monthsEle to ensure selected state is reflected when tapping on a day 472 | this.monthsEle.nativeElement.style.zIndex = '2'; 473 | this.monthsEle.nativeElement.style.zIndex = 'initial'; 474 | }); 475 | } 476 | 477 | findInitMonthNumber(date: DateTime): number { 478 | let startDate = this.actualFirstTime ? this.actualFirstTime : this.modalOptions.from; 479 | const defaultScrollTo = date; 480 | const isAfter: boolean = defaultScrollTo > startDate; 481 | if (!isAfter) { 482 | return -1; 483 | } 484 | 485 | if (this.showYearPicker) { 486 | startDate = DateTime.fromJSDate(new Date(this.year, 0, 1), { zone: 'Etc/UTC' }); 487 | } 488 | 489 | return defaultScrollTo.diff(startDate, 'months').milliseconds; 490 | } 491 | 492 | _getDayTime(date: DateTime): number { 493 | return DateTime.fromFormat(date.toFormat('yyyy-MM-dd'), 'yyyy-MM-dd', { zone: 'Etc/UTC' }).valueOf(); 494 | } 495 | 496 | _monthFormat(date: DateTime): string { 497 | return date.toLocaleString({ year: 'numeric', month: 'short' }); 498 | } 499 | 500 | trackByIndex(index: number, momentDate: CalendarMonth): number { 501 | return momentDate.original ? momentDate.original.date.valueOf() : index; 502 | } 503 | 504 | isBegin(pstate: GlobalPickState): boolean { 505 | return pstate === GlobalPickState.BEGIN_DATE || pstate === GlobalPickState.BEGIN_HOUR || pstate === GlobalPickState.BEGIN_MINUTE; 506 | } 507 | 508 | isEnd(pstate: GlobalPickState): boolean { 509 | return !this.isBegin(pstate); 510 | } 511 | 512 | isDate(pstate: GlobalPickState): boolean { 513 | return pstate === GlobalPickState.BEGIN_DATE || pstate === GlobalPickState.END_DATE; 514 | } 515 | 516 | isTime(pstate: GlobalPickState): boolean { 517 | return !this.isDate(pstate); 518 | } 519 | 520 | isHour(pstate: GlobalPickState): boolean { 521 | return pstate === GlobalPickState.BEGIN_HOUR || pstate === GlobalPickState.END_HOUR; 522 | } 523 | 524 | isMinute(pstate: GlobalPickState): boolean { 525 | return pstate === GlobalPickState.BEGIN_MINUTE || pstate === GlobalPickState.END_MINUTE; 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /src/moots-picker/components/clock-picker.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
6 | 7 |
8 |
10 | {{ hour }} 11 |
12 |
13 | 14 |
15 |
17 | {{ hour }} 18 |
19 |
20 |
21 |
22 | 23 |
25 | 26 |
27 |
29 | {{ minute }} 30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 | 42 | AM 43 | 44 |
45 |
46 | 48 | PM 49 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /src/moots-picker/components/clock-picker.component.scss: -------------------------------------------------------------------------------- 1 | @import '../mixins.scss'; 2 | 3 | .clock-container { 4 | text-align: center; 5 | user-select: none; 6 | } 7 | 8 | .ampm { 9 | font-weight: 500; 10 | font-size: 1.5rem; 11 | background: none; 12 | margin-top: -1em; 13 | border-radius: 100%; 14 | width: 2.5em; 15 | height: 2.5em; 16 | } 17 | 18 | .highlight { 19 | background-color: #488aff; 20 | border-radius: 100%; 21 | padding: 0.5em; 22 | margin: -0.75rem; 23 | width: 3rem; 24 | height: 3rem; 25 | color: var(--ion-color-primary-contrast) !important; 26 | overflow-wrap: normal; 27 | overflow: visible; 28 | } 29 | 30 | ion-button { 31 | --border-radius: 100%; 32 | } 33 | 34 | div.toolbar-background { 35 | --min-height: 44px; 36 | } 37 | 38 | 39 | .clock-hand { 40 | position: absolute; 41 | width: 1px; 42 | background-color: #488aff; 43 | border: solid 2px #488aff; 44 | height: 36%; 45 | transform-origin: bottom center; 46 | transform: rotate(0deg); 47 | transition: background .15s ease-in-out; 48 | transition: border-width 0.15s ease-in-out; 49 | left: calc(50% - 2px); 50 | top: 14%; 51 | } 52 | 53 | .clock-hand.inner { 54 | top: 28%; 55 | height: 22%; 56 | } 57 | 58 | .clock-center { 59 | position: absolute; 60 | width: 0.5em; 61 | background-color: #488aff; 62 | border-radius: 100%; 63 | height: 0.5em; 64 | left: calc(50% - 0.25em); 65 | top: calc(50% - 0.25em); 66 | } 67 | 68 | .minutes-container { 69 | @include on-circle($item-count: 12, $circle-size: 20em, $item-size: 1.75em); 70 | margin: 1em auto 0; 71 | background-color: var(--ion-color-light-shade); 72 | 73 | .clock-digit { 74 | display: block; 75 | max-width: 100%; 76 | border-radius: 50%; 77 | transition: .15s; 78 | font-size: 28px; 79 | font-weight: 500; 80 | font-stretch: normal; 81 | font-style: normal; 82 | line-height: 1.21; 83 | letter-spacing: 0.34px; 84 | color: var(--ion-color-dark-shade); 85 | z-index: 5; 86 | cursor: pointer; 87 | } 88 | } 89 | 90 | .hours-12-container { 91 | @include on-circle($item-count: 12, $circle-size: 20em, $item-size: 1.75em); 92 | margin: 1em auto 0; 93 | background-color: var(--ion-color-light-shade); 94 | 95 | .clock-digit { 96 | display: block; 97 | max-width: 100%; 98 | border-radius: 50%; 99 | transition: .15s; 100 | font-size: 28px; 101 | font-weight: 500; 102 | font-stretch: normal; 103 | font-style: normal; 104 | line-height: 1.21; 105 | letter-spacing: 0.34px; 106 | color: var(--ion-color-dark-shade); 107 | z-index: 5; 108 | cursor: pointer; 109 | } 110 | } 111 | 112 | .hours-24-container { 113 | @include on-circle($item-count: 12, $circle-size: 20em, $item-size: 2.5em); 114 | margin: 1em auto 0; 115 | margin-top: -20rem; 116 | 117 | .clock-digit { 118 | display: block; 119 | max-width: 100%; 120 | border-radius: 50%; 121 | transition: .15s; 122 | font-size: 16px; 123 | font-weight: 500; 124 | font-stretch: normal; 125 | font-style: normal; 126 | line-height: 1.31; 127 | letter-spacing: -0.32px; 128 | color: #757575; 129 | z-index: 5; 130 | cursor: pointer; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/moots-picker/components/clock-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { animate, keyframes, state, style, transition, trigger } from '@angular/animations'; 2 | import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'; 3 | import { DateTime } from 'luxon'; 4 | 5 | import { TapticConfig } from '../calendar.model'; 6 | 7 | interface Coordinate { 8 | x: number; 9 | y: number; 10 | } 11 | 12 | export enum ClockPickState { 13 | HOUR, 14 | MINUTE 15 | } 16 | 17 | @Component({ 18 | selector: 'moots-clock-picker', 19 | animations: [ 20 | trigger('switch', [ 21 | state( 22 | 'open', 23 | style({ 24 | transform: 'scale(1)', 25 | opacity: 1 26 | }) 27 | ), 28 | state( 29 | 'closed', 30 | style({ 31 | transform: 'scale(1)', 32 | opacity: 1 33 | }) 34 | ), 35 | transition('open <=> closed', [ 36 | animate( 37 | '0.5s ease-in-out', 38 | keyframes([ 39 | style({ 40 | transform: 'scale(1.1)', 41 | opacity: 0.5, 42 | offset: 0.5 43 | }) 44 | ]) 45 | ) 46 | ]) 47 | ]) 48 | ], 49 | styleUrls: ['./clock-picker.component.scss'], 50 | templateUrl: './clock-picker.component.html' 51 | }) 52 | export class ClockPickerComponent { 53 | ClockPickState = ClockPickState; 54 | 55 | @Input() pickState = ClockPickState.HOUR; 56 | @Input() mode24 = true; 57 | 58 | _inputTime: DateTime; 59 | @Input() set inputTime(time: DateTime) { 60 | this._inputTime = time; 61 | let hour = time.toFormat(this.mode24 ? 'HH' : 'hh'); 62 | hour = hour.startsWith('0') ? hour.substr(1) : hour; 63 | this.setClockFromHour(hour); 64 | this.setClockFromMinute(time.toFormat('mm')); 65 | } 66 | 67 | @Input() tapConf: TapticConfig; 68 | 69 | @Output() 70 | selectChange = new EventEmitter(); 71 | @Output() 72 | valueSelected = new EventEmitter(); 73 | @Output() 74 | displayedValue = new EventEmitter(); 75 | 76 | @ViewChild('hourClock') hourClock: any; 77 | @ViewChild('minuteClock') minuteClock: any; 78 | 79 | hourSelected = '3'; 80 | minuteSelected = '00'; 81 | hourHandStyle: { transform: string }; 82 | minuteHandStyle: { transform: string }; 83 | lastClicked: any; 84 | outerHours = ['9', '10', '11', '12', '1', '2', '3', '4', '5', '6', '7', '8']; 85 | innerHours = ['21', '22', '23', '00', '13', '14', '15', '16', '17', '18', '19', '20']; 86 | minutes = ['45', '50', '55', '00', '05', '10', '15', '20', '25', '30', '35', '40']; 87 | 88 | constructor() { 89 | // 90 | } 91 | 92 | ionViewDidLoad() { 93 | this.setClockFromHour('00'); 94 | this.setClockFromMinute('00'); 95 | } 96 | 97 | getAmPm() { 98 | const s = this._inputTime.toLocaleString({ hour: 'numeric', minute: 'numeric', hour12: !this.mode24 }); 99 | return s.substring(s.length - 2).toLowerCase(); 100 | } 101 | 102 | setAmPm(arg: string) { 103 | const f = this._inputTime.toLocaleString({ hour: 'numeric', minute: 'numeric', hour12: !this.mode24 }); 104 | const time = f.replace(this.getAmPm().toLowerCase(), arg.toUpperCase()).replace(this.getAmPm().toUpperCase(), arg.toUpperCase()); 105 | const temp = DateTime.fromFormat(time, 't', { zone: 'Etc/UTC' }); 106 | 107 | this._inputTime = this._inputTime.set({ hour: temp.hour, minute: temp.minute }); 108 | this.valueSelected.emit(this._inputTime); 109 | } 110 | 111 | getHourNumber(): number { 112 | return parseInt(this.hourSelected, 10); 113 | } 114 | 115 | updatedClock(clicked: Coordinate) { 116 | this.lastClicked = clicked; 117 | const clock = this.pickState === ClockPickState.HOUR ? this.hourClock : this.minuteClock; 118 | if (clock) { 119 | const rectangle = clock.nativeElement.getBoundingClientRect(); 120 | const clockCenter = { 121 | x: rectangle.width / 2 + rectangle.left, 122 | y: rectangle.height / 2 + rectangle.top 123 | }; 124 | const angle = (Math.atan2(clockCenter.y - clicked.y, clockCenter.x - clicked.x) * 180) / Math.PI; 125 | if (this.pickState === ClockPickState.HOUR) { 126 | const dist = ClockPickerComponent.calculateDistance(clockCenter.x, clicked.x, clockCenter.y, clicked.y); 127 | this.setHourFromAngle(angle, this.mode24 && dist < rectangle.height / 3); 128 | } else { 129 | this.setMinuteFromAngle(angle); 130 | } 131 | 132 | this.setAmPm; 133 | const time = this.hourSelected.padStart(2, '0') + ' ' + this.minuteSelected + (this.mode24 ? '' : ' ' + this.getAmPm()); 134 | const temp = DateTime.fromFormat(time, this.mode24 ? 'HH mm' : 'hh mm a', { zone: 'Etc/UTC' }); 135 | this._inputTime = this._inputTime.set({ hour: temp.hour, minute: temp.minute }); 136 | this.displayedValue.emit(this._inputTime); 137 | } 138 | } 139 | 140 | draggedClock($event: any) { 141 | this.lastType = 'drag'; 142 | $event.preventDefault(); 143 | const clicked: Coordinate = { 144 | x: $event.changedTouches[0].clientX, 145 | y: $event.changedTouches[0].clientY 146 | }; 147 | this.updatedClock(clicked); 148 | } 149 | 150 | lastType = ''; 151 | tappedClock(event: any) { 152 | const clicked: Coordinate = { 153 | x: event.clientX, 154 | y: event.clientY 155 | }; 156 | let fireEvents = false; 157 | if (event.type === 'touchend') { 158 | this.lastType = 'touchend'; 159 | this.updatedClock(this.lastClicked); 160 | fireEvents = true; 161 | } else { 162 | if (this.lastType !== 'touchend') { 163 | this.updatedClock(clicked); 164 | fireEvents = true; 165 | } 166 | this.lastType = 'clicked'; 167 | this.lastClicked = clicked; 168 | } 169 | if (fireEvents) { 170 | if (this.pickState === ClockPickState.HOUR) { 171 | this.valueSelected.emit(this._inputTime); 172 | this.pickState = ClockPickState.MINUTE; 173 | this.selectChange.emit(this.pickState); 174 | } else if (this.pickState === ClockPickState.MINUTE) { 175 | this.valueSelected.emit(this._inputTime); 176 | } 177 | this.tapConf.onClockSelect(); 178 | } 179 | } 180 | 181 | /** Sets the clock hour handle according to hour parameter */ 182 | setClockFromHour(hour: string) { 183 | const hourN = parseInt(hour, 10); 184 | const hours = hourN <= 12 && hourN > 0 ? this.outerHours : this.innerHours; 185 | hour = hour === '0' ? '00' : hour; 186 | const index = hours.indexOf(hour); 187 | if (index > -1 || index === -6) { 188 | const angle = Math.abs(index) * 30 - 90; 189 | this.hourHandStyle = { 190 | transform: `rotate(${angle}deg)` 191 | }; 192 | } else { 193 | const angle = 12 - Math.abs(index) * 30 - 105; 194 | this.hourHandStyle = { 195 | transform: `rotate(${angle}deg)` 196 | }; 197 | } 198 | this.hourSelected = hour; 199 | } 200 | 201 | /** Sets the clock minute handle according to minute parameter */ 202 | setClockFromMinute(minute: string) { 203 | const index = this.minutes.indexOf(minute); 204 | if (index > -1 || index === -6) { 205 | this.minuteHandStyle = { 206 | transform: `rotate(${Math.abs(index) * 30 - 90}deg)` 207 | }; 208 | } else { 209 | this.minuteHandStyle = { 210 | transform: `rotate(${(12 - Math.abs(index)) * 30 - 105}deg)` 211 | }; 212 | } 213 | this.minuteSelected = minute; 214 | } 215 | 216 | setHourFromAngle(angle: number, inner: boolean) { 217 | const hours = inner ? this.innerHours : this.outerHours; 218 | const index = Math.round(angle / 30); 219 | let toSelect; 220 | if (index > -1 || index === -6) { 221 | toSelect = hours[Math.abs(index)]; 222 | const angleC = Math.abs(index) * 30 - 90; 223 | this.hourHandStyle = { 224 | transform: `rotate(${angleC}deg)` 225 | }; 226 | } else { 227 | toSelect = hours[12 - Math.abs(index)]; 228 | const angleC = (12 - Math.abs(index)) * 30 - 90; 229 | this.hourHandStyle = { 230 | transform: `rotate(${angleC}deg)` 231 | }; 232 | } 233 | if (toSelect !== this.hourSelected) { 234 | this.hourSelected = toSelect; 235 | this.tapConf.onClockHover(); 236 | } 237 | } 238 | 239 | setMinuteFromAngle(angle: number) { 240 | const index = Math.round(angle / 30); 241 | let toSelect; 242 | if (index > -1 || index === -6) { 243 | toSelect = this.minutes[Math.abs(index)]; 244 | this.minuteHandStyle = { 245 | transform: `rotate(${Math.abs(index) * 30 - 90}deg)` 246 | }; 247 | } else { 248 | toSelect = this.minutes[12 - Math.abs(index)]; 249 | this.minuteHandStyle = { 250 | transform: `rotate(${(12 - Math.abs(index)) * 30 - 90}deg)` 251 | }; 252 | } 253 | if (toSelect !== this.minuteSelected) { 254 | this.minuteSelected = toSelect; 255 | this.tapConf.onClockHover(); 256 | } 257 | } 258 | 259 | static calculateDistance(x1: number, x2: number, y1: number, y2: number) { 260 | const dis = Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)); 261 | return Math.abs(dis); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/moots-picker/components/index.ts: -------------------------------------------------------------------------------- 1 | import { CalendarWeekComponent } from './calendar-week.component'; 2 | import { CalendarComponent } from './calendar.component'; 3 | import { PickerModal } from './calendar.modal'; 4 | import { ClockPickerComponent } from './clock-picker.component'; 5 | import { MonthPickerComponent } from './month-picker.component'; 6 | import { MonthComponent } from './month.component'; 7 | 8 | export const CALENDAR_COMPONENTS = [ 9 | PickerModal, 10 | CalendarWeekComponent, 11 | MonthComponent, 12 | CalendarComponent, 13 | MonthPickerComponent, 14 | ClockPickerComponent 15 | ]; 16 | -------------------------------------------------------------------------------- /src/moots-picker/components/month-picker.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .month-picker { 3 | margin: 20px 0; 4 | display: inline-block; 5 | width: 100%; 6 | } 7 | 8 | .month-packer-item { 9 | width: 25%; 10 | box-sizing: border-box; 11 | float: left; 12 | height: 50px; 13 | padding: 5px; 14 | button { 15 | border-radius: 32px; 16 | width: 100%; 17 | height: 100%; 18 | font-size: 0.9em; 19 | background-color: transparent; 20 | } 21 | } 22 | 23 | .month-packer-item { 24 | &.this-month button { 25 | border: 1px solid var(--ion-color-primary); 26 | } 27 | &.active { 28 | button { 29 | background-color: var(--ion-color-primary); 30 | color: var(--ion-color-light-tint); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/moots-picker/components/month-picker.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core'; 2 | import { DateTime } from 'luxon'; 3 | 4 | import { CalendarMonth } from '../calendar.model'; 5 | import { defaults } from '../config'; 6 | 7 | @Component({ 8 | selector: 'moots-calendar-month-picker', 9 | styleUrls: ['./month-picker.component.scss'], 10 | template: ` 11 |
12 |
17 | 18 |
19 |
20 | ` 21 | }) 22 | export class MonthPickerComponent { 23 | @Input() 24 | month: CalendarMonth; 25 | @Input() 26 | color = defaults.COLOR; 27 | @Output() 28 | select: EventEmitter = new EventEmitter(); 29 | _thisMonth = DateTime.utc(); 30 | _monthFormat = defaults.MONTH_FORMAT; 31 | 32 | @Input() 33 | set monthFormat(value: string[]) { 34 | if (Array.isArray(value) && value.length === 12) { 35 | this._monthFormat = value; 36 | } 37 | } 38 | 39 | get monthFormat(): string[] { 40 | return this._monthFormat; 41 | } 42 | 43 | constructor() { 44 | /**/ 45 | } 46 | 47 | _onSelect(month: number): void { 48 | this.select.emit(month); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/moots-picker/components/month.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 | 14 | 15 |
16 |
17 |
18 |
19 | 20 | 21 |
22 | 23 |
26 | 27 | 35 | 36 |
37 |
38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /src/moots-picker/components/month.component.scss: -------------------------------------------------------------------------------- 1 | $disabled-color: var(--ion-color-medium-tint); 2 | 3 | @mixin transition-property($args...) { 4 | -webkit-transition-property: $args; 5 | -moz-transition-property: $args; 6 | -ms-transition-property: $args; 7 | -o-transition-property: $args; 8 | transition-property: $args; 9 | } 10 | 11 | @mixin transition-duration($args...) { 12 | -webkit-transition-duration: $args; 13 | -moz-transition-duration: $args; 14 | -ms-transition-duration: $args; 15 | -o-transition-duration: $args; 16 | transition-duration: $args; 17 | } 18 | 19 | @mixin transition-timing-function($args...) { 20 | -webkit-transition-timing-function: $args; 21 | -moz-transition-timing-function: $args; 22 | -ms-transition-timing-function: $args; 23 | -o-transition-timing-function: $args; 24 | transition-timing-function: $args; 25 | } 26 | 27 | :host { 28 | display: inline-block; 29 | width: 100%; 30 | .days-box { 31 | padding: 0.5rem; 32 | } 33 | .days:nth-of-type(7n), 34 | .days:nth-of-type(7n + 1) { 35 | width: 15%; 36 | } 37 | .days { 38 | width: 14%; 39 | float: left; 40 | text-align: center; 41 | height: 36px; 42 | margin-bottom: 5px; 43 | } 44 | .days .marked p { 45 | font-weight: 500; 46 | } 47 | .days .on-selected { 48 | border: none; 49 | p { 50 | font-size: 1.3em; 51 | } 52 | } 53 | button.days-btn { 54 | border-radius: 36px; 55 | width: 36px; 56 | display: block; 57 | margin: 0 auto; 58 | padding: 0; 59 | height: 36px; 60 | background-color: transparent; 61 | position: relative; 62 | z-index: 2; 63 | outline: 0; 64 | } 65 | button.days-btn p { 66 | margin: 0; 67 | font-size: 1.25em; 68 | color: var(--ion-color-dark-tint); 69 | text-align: center; 70 | } 71 | button.days-btn[disabled] p { 72 | color: $disabled-color; 73 | } 74 | button.days-btn.on-selected small { 75 | transition: bottom 0.3s; 76 | bottom: -14px; 77 | } 78 | button.days-btn small { 79 | overflow: hidden; 80 | display: block; 81 | left: 0; 82 | right: 0; 83 | bottom: -5px; 84 | position: absolute; 85 | z-index: 1; 86 | text-align: center; 87 | font-weight: 200; 88 | } 89 | button.days-btn small, 90 | .days .marked p, 91 | .days .today p { 92 | color: var(--ion-color-primary); 93 | } 94 | .days .today p { 95 | font-weight: 700; 96 | } 97 | .days .last-month-day p, 98 | .days .next-month-day p { 99 | color: $disabled-color; 100 | } 101 | .days .today.on-selected p, 102 | .days .marked.on-selected p { 103 | color: var(--ion-color-primary-contrast); 104 | } 105 | .days .on-selected, 106 | .startSelection button.days-btn, 107 | .endSelection button.days-btn { 108 | background-color: var(--ion-color-primary); 109 | color: var(--ion-color-primary-contrast); 110 | } 111 | .startSelection { 112 | position: relative; 113 | 114 | &:before, 115 | &:after { 116 | height: 36px; 117 | width: 50%; 118 | content: ''; 119 | position: absolute; 120 | top: 0; 121 | right: 0; 122 | display: block; 123 | } 124 | 125 | &:before { 126 | background-color: var(--ion-color-primary); 127 | } 128 | &:after { 129 | background-color: white; 130 | opacity: 0.25; 131 | } 132 | } 133 | .endSelection { 134 | position: relative; 135 | 136 | &:before, 137 | &:after { 138 | height: 36px; 139 | width: 50%; 140 | content: ''; 141 | position: absolute; 142 | top: 0; 143 | left: 0; 144 | display: block; 145 | } 146 | 147 | &:before { 148 | background-color: var(--ion-color-primary); 149 | } 150 | &:after { 151 | background-color: white; 152 | opacity: 0.25; 153 | } 154 | } 155 | .startSelection.endSelection { 156 | &:after { 157 | background-color: transparent; 158 | } 159 | } 160 | .startSelection button.days-btn { 161 | border-radius: 50%; 162 | } 163 | .between button.days-btn { 164 | background-color: var(--ion-color-primary); 165 | width: 100%; 166 | border-radius: 0; 167 | position: relative; 168 | 169 | &:after { 170 | height: 36px; 171 | width: 100%; 172 | content: ''; 173 | position: absolute; 174 | top: 0; 175 | left: 0; 176 | right: 0; 177 | display: block; 178 | background-color: white; 179 | opacity: 0.25; 180 | } 181 | 182 | p { 183 | color: var(--ion-color-light-tint); 184 | } 185 | } 186 | .endSelection button.days-btn { 187 | border-radius: 50%; 188 | p { 189 | color: var(--ion-color-primary-contrast); 190 | } 191 | } 192 | 193 | .days.startSelection:nth-child(7n):before, 194 | .days.between:nth-child(7n) button.days-btn, 195 | button.days-btn.is-last { 196 | border-radius: 0 36px 36px 0; 197 | &.on-selected { 198 | border-radius: 50%; 199 | } 200 | } 201 | 202 | .days.endSelection:nth-child(7n + 1):before, 203 | .days.between:nth-child(7n + 1) button.days-btn, 204 | .days.between.is-first-wrap button.days-btn.is-first, 205 | button.days-btn.is-first { 206 | border-radius: 36px 0 0 36px; 207 | } 208 | 209 | .startSelection button.days-btn.is-first, 210 | .endSelection button.days-btn.is-first, 211 | button.days-btn.is-first.on-selected, 212 | button.days-btn.is-last.on-selected, 213 | .startSelection button.days-btn.is-last, 214 | .endSelection button.days-btn.is-last { 215 | border-radius: 50%; 216 | } 217 | 218 | .startSelection.is-last-wrap { 219 | &::before, 220 | &::after { 221 | border-radius: 0 36px 36px 0; 222 | } 223 | } 224 | 225 | .endSelection.is-first-wrap { 226 | &::before, 227 | &::after { 228 | border-radius: 36px 0 0 36px; 229 | } 230 | } 231 | 232 | .days .on-selected p { 233 | color: var(--ion-color-primary-contrast); 234 | } 235 | .startSelection, 236 | .endSelection, 237 | .between { 238 | button.days-btn { 239 | @include transition-property(background-color); 240 | @include transition-duration(180ms); 241 | @include transition-timing-function(ease-out); 242 | } 243 | } 244 | 245 | .startSelection.endSelection::before { 246 | --ion-color-primary: transparent; 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/moots-picker/components/month.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, ChangeDetectorRef, Component, EventEmitter, Input, Output, forwardRef } from '@angular/core'; 2 | import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; 3 | import { DateTime } from 'luxon'; 4 | 5 | import { CalendarDay, CalendarMonth, CalendarOriginal, PickMode } from '../calendar.model'; 6 | import { defaults } from '../config'; 7 | 8 | export const MONTH_VALUE_ACCESSOR: any = { 9 | provide: NG_VALUE_ACCESSOR, 10 | useExisting: forwardRef(() => MonthComponent), 11 | multi: true 12 | }; 13 | 14 | @Component({ 15 | selector: 'moots-calendar-month', 16 | providers: [MONTH_VALUE_ACCESSOR], 17 | styleUrls: ['./month.component.scss'], 18 | templateUrl: './month.component.html' 19 | }) 20 | export class MonthComponent implements ControlValueAccessor, AfterViewInit { 21 | @Input() 22 | month: CalendarMonth; 23 | @Input() 24 | pickMode: PickMode; 25 | @Input() 26 | isSaveHistory: boolean; 27 | @Input() 28 | id: any; 29 | @Input() 30 | readonly = false; 31 | @Input() 32 | color: string = defaults.COLOR; 33 | @Input() 34 | selectBegin = true; 35 | 36 | @Output() 37 | change: EventEmitter = new EventEmitter(); 38 | @Output() 39 | select: EventEmitter = new EventEmitter(); 40 | @Output() 41 | selectStart: EventEmitter = new EventEmitter(); 42 | @Output() 43 | selectEnd: EventEmitter = new EventEmitter(); 44 | 45 | _date: (CalendarDay | undefined)[] = [undefined, undefined]; 46 | _isInit = false; 47 | _onChanged: () => void; 48 | _onTouched: () => void; 49 | 50 | get _isRange(): boolean { 51 | return this.pickMode === PickMode.RANGE; 52 | } 53 | 54 | constructor(public ref: ChangeDetectorRef) {} 55 | 56 | ngAfterViewInit(): void { 57 | this._isInit = true; 58 | } 59 | 60 | get value() { 61 | return this._date; 62 | } 63 | 64 | writeValue(obj: any): void { 65 | if (Array.isArray(obj)) { 66 | this._date = obj; 67 | } 68 | } 69 | 70 | registerOnChange(fn: any): void { 71 | this._onChanged = fn; 72 | } 73 | 74 | registerOnTouched(fn: any): void { 75 | this._onTouched = fn; 76 | } 77 | 78 | trackByTime(index: number, item: CalendarOriginal): number { 79 | return item?.date ? item.date.valueOf() : index; 80 | } 81 | 82 | isEndSelection(day: CalendarDay): boolean { 83 | if (!day) { 84 | return false; 85 | } 86 | if (this.pickMode !== PickMode.RANGE || !this._isInit || this._date[1] === undefined) { 87 | return false; 88 | } 89 | 90 | return this._date[1].time === day.time; 91 | } 92 | 93 | isBetween(day: CalendarDay): boolean { 94 | if (!day) { 95 | return false; 96 | } 97 | 98 | if (this.pickMode !== PickMode.RANGE || !this._isInit) { 99 | return false; 100 | } 101 | 102 | if (this._date[0] === undefined || this._date[1] === undefined) { 103 | return false; 104 | } 105 | 106 | const start = this._date[0].time; 107 | const end = this._date[1].time; 108 | 109 | return day.time < end && day.time > start; 110 | } 111 | 112 | isStartSelection(day: CalendarDay): boolean { 113 | if (!day) { 114 | return false; 115 | } 116 | if (this.pickMode !== PickMode.RANGE || !this._isInit || this._date[0] === undefined) { 117 | return false; 118 | } 119 | 120 | return this._date[0].time === day.time && this._date[1] !== undefined; 121 | } 122 | 123 | isSelected(time: DateTime): boolean { 124 | if (Array.isArray(this._date)) { 125 | if (this.pickMode !== PickMode.MULTI) { 126 | if (this._date[0] !== undefined) { 127 | return time.hasSame(this._date[0].time, 'day'); 128 | } 129 | 130 | if (this._date[1] !== undefined) { 131 | return time.hasSame(this._date[1].time, 'day'); 132 | } 133 | } else { 134 | return this._date.findIndex((e) => e !== undefined && e.time === time) !== -1; 135 | } 136 | } else { 137 | return false; 138 | } 139 | } 140 | 141 | onSelected(item: CalendarDay): void { 142 | if (this.readonly) { 143 | return; 144 | } 145 | item.selected = true; 146 | this.select.emit(item); 147 | if (this.pickMode === PickMode.SINGLE) { 148 | this._date[0] = item; 149 | this.change.emit(this._date); 150 | return; 151 | } 152 | 153 | if (this.pickMode === PickMode.RANGE) { 154 | if (this._date[0] === undefined || this.selectBegin) { 155 | this._date[0] = item; 156 | if (this._date[1] !== undefined && this._date[0].time >= this._date[1].time) { 157 | this._date[1] = undefined; 158 | } 159 | this.selectStart.emit(item); 160 | } else if (this._date[1] === undefined || !this.selectBegin) { 161 | if (this._date[0].time < item.time) { 162 | this._date[1] = item; 163 | this.selectEnd.emit(item); 164 | } else { 165 | this._date[1] = undefined; 166 | } 167 | } else { 168 | this._date[0] = item; 169 | this.selectStart.emit(item); 170 | this._date[1] = undefined; 171 | } 172 | this.change.emit(this._date); 173 | return; 174 | } 175 | 176 | if (this.pickMode === PickMode.MULTI) { 177 | const index = this._date.findIndex((e) => e !== undefined && e.time === item.time); 178 | 179 | if (index === -1) { 180 | this._date.push(item); 181 | } else { 182 | this._date.splice(index, 1); 183 | } 184 | this.change.emit(this._date.filter((e) => e !== undefined)); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/moots-picker/config.ts: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | DATE_FORMAT: 'yyyy-MM-DD', 3 | COLOR: 'primary', 4 | WEEKS_FORMAT: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], 5 | MONTH_FORMAT: ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'] 6 | }; 7 | -------------------------------------------------------------------------------- /src/moots-picker/index.scss: -------------------------------------------------------------------------------- 1 | @import "components/month.component"; 2 | @import "components/month-picker.component"; 3 | @import "components/calendar-week.component"; 4 | @import "components/calendar.modal"; 5 | @import "components/calendar.component"; 6 | @import "functions"; 7 | -------------------------------------------------------------------------------- /src/moots-picker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './calendar.model'; 2 | export { PickerModal } from './components/calendar.modal'; 3 | export { CalendarWeekComponent } from './components/calendar-week.component'; 4 | export { MonthComponent } from './components/month.component'; 5 | export { CalendarComponent } from './components/calendar.component'; 6 | export { MootsPickerModule } from './moots-picker.module'; 7 | export { CalendarController } from './calendar.controller'; 8 | -------------------------------------------------------------------------------- /src/moots-picker/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin on-circle($item-count, $circle-size, $item-size) { 2 | position: relative; 3 | width: $circle-size; 4 | height: $circle-size; 5 | padding: 0; 6 | border-radius: 50%; 7 | list-style: none; 8 | 9 | > * { 10 | display: block; 11 | position: absolute; 12 | top: 50%; 13 | left: 50%; 14 | width: $item-size; 15 | height: $item-size; 16 | margin: -($item-size / 2); 17 | 18 | $angle: (360 / $item-count); 19 | $rot: 0; 20 | 21 | @for $i from 1 through $item-count { 22 | &:nth-of-type(#{$i}) { 23 | transform: 24 | rotate($rot * 1deg) 25 | translate($circle-size / 4.25) 26 | rotate($rot * -1deg); 27 | } 28 | 29 | $rot: $rot + $angle; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/moots-picker/moots-picker.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { IonicModule, ModalController } from '@ionic/angular'; 6 | 7 | import { CalendarController } from './calendar.controller'; 8 | import { CALENDAR_COMPONENTS } from './components/index'; 9 | import { CalendarService } from './services/calendar.service'; 10 | 11 | export function calendarController(modalCtrl: ModalController, calSvc: CalendarService) { 12 | return new CalendarController(modalCtrl, calSvc); 13 | } 14 | 15 | @NgModule({ 16 | imports: [CommonModule, IonicModule, FormsModule, FlexLayoutModule], 17 | declarations: CALENDAR_COMPONENTS, 18 | exports: CALENDAR_COMPONENTS, 19 | entryComponents: CALENDAR_COMPONENTS, 20 | providers: [ 21 | CalendarService, 22 | { 23 | provide: CalendarController, 24 | useFactory: calendarController, 25 | deps: [ModalController, CalendarService], 26 | }, 27 | ], 28 | schemas: [CUSTOM_ELEMENTS_SCHEMA], 29 | }) 30 | export class MootsPickerModule {} 31 | -------------------------------------------------------------------------------- /src/moots-picker/services/calendar.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { DateTime, Interval } from 'luxon'; 3 | import { payloadsToDateTime, PickerModalOptionsSafe } from '..'; 4 | 5 | import { 6 | CalendarDay, 7 | CalendarMonth, 8 | CalendarOriginal, 9 | DayConfig, 10 | GlobalPickState, 11 | PickMode, 12 | PickerModalOptions, 13 | payloadToDateTime 14 | } from '../calendar.model'; 15 | import { defaults } from '../config'; 16 | 17 | const isBoolean = (input: any) => input === true || input === false; 18 | 19 | @Injectable() 20 | export class CalendarService { 21 | constructor() { 22 | /**/ 23 | } 24 | 25 | get DEFAULT_STEP() { 26 | return 12; 27 | } 28 | 29 | safeOpt(calendarOptions: PickerModalOptions): PickerModalOptionsSafe { 30 | const _disableWeeks: number[] = []; 31 | const _daysConfig: DayConfig[] = []; 32 | const from = calendarOptions.from ? payloadToDateTime(calendarOptions.from) : DateTime.utc(); 33 | const safeOpts: PickerModalOptionsSafe = { 34 | from: from, 35 | to: calendarOptions.to ? payloadToDateTime(calendarOptions.to) : DateTime.utc(), 36 | weekStart: calendarOptions.weekStart || 0, 37 | step: calendarOptions.step || this.DEFAULT_STEP, 38 | id: calendarOptions.id || '', 39 | cssClass: calendarOptions.cssClass || '', 40 | closeLabel: calendarOptions.closeLabel || 'CANCEL', 41 | doneLabel: calendarOptions.doneLabel || 'DONE', 42 | monthFormat: calendarOptions.monthFormat || 'MMM yyyy', 43 | title: calendarOptions.title || 'CALENDAR', 44 | defaultTitle: calendarOptions.defaultTitle || '', 45 | defaultSubtitle: calendarOptions.defaultSubtitle || '', 46 | autoDone: calendarOptions.autoDone || false, 47 | canBackwardsSelected: calendarOptions.canBackwardsSelected || false, 48 | closeIcon: calendarOptions.closeIcon || false, 49 | doneIcon: calendarOptions.doneIcon || false, 50 | // showYearPicker: false, 51 | isSaveHistory: calendarOptions.isSaveHistory || false, 52 | pickMode: calendarOptions.pickMode || PickMode.SINGLE, 53 | color: calendarOptions.closeLabel || defaults.COLOR, 54 | weekdays: calendarOptions.weekdays || defaults.WEEKS_FORMAT, 55 | daysConfig: calendarOptions.daysConfig || _daysConfig, 56 | disableWeeks: calendarOptions.disableWeeks || _disableWeeks, 57 | showAdjacentMonthDay: calendarOptions.showAdjacentMonthDay && true, 58 | locale: calendarOptions.locale || 'en', 59 | startLabel: calendarOptions.startLabel || 'Start', 60 | endLabel: calendarOptions.endLabel || 'End', 61 | uses24Hours: calendarOptions.uses24Hours, 62 | fulldayLabel: calendarOptions.fulldayLabel || 'All Day event', 63 | fullday: calendarOptions.fullday || false, 64 | defaultScrollTo: calendarOptions.defaultScrollTo ? payloadToDateTime(calendarOptions.defaultDate) : from, 65 | defaultDate: calendarOptions.defaultDate ? payloadToDateTime(calendarOptions.defaultDate) : undefined, 66 | defaultDates: calendarOptions.defaultDates ? payloadsToDateTime(calendarOptions.defaultDates) : undefined, 67 | defaultDateRange: calendarOptions.defaultDateRange 68 | ? { from: payloadToDateTime(calendarOptions.defaultDateRange.from), to: payloadToDateTime(calendarOptions.defaultDateRange.to) } 69 | : undefined, 70 | tapticConf: calendarOptions.tapticConf || { 71 | onClockHover: () => { 72 | /**/ 73 | }, 74 | onClockSelect: () => { 75 | /**/ 76 | }, 77 | onCalendarSelect: () => { 78 | /**/ 79 | } 80 | }, 81 | pickState: calendarOptions.pickState || GlobalPickState.BEGIN_DATE 82 | }; 83 | 84 | return safeOpts; 85 | } 86 | 87 | multiFormat(time: number): number { 88 | return time; 89 | } 90 | 91 | createOriginalCalendar(time: number): CalendarOriginal { 92 | const date = DateTime.fromMillis(time, { zone: 'Etc/UTC' }); 93 | const year = date.year; 94 | const month = date.month; 95 | const firstWeek = DateTime.utc(year, month, 1).weekday; 96 | const howManyDays = date.endOf('month').day; 97 | return { 98 | date, 99 | year, 100 | month, 101 | firstWeek, 102 | howManyDays 103 | }; 104 | } 105 | 106 | findDayConfig(day: DateTime, opt: PickerModalOptionsSafe): any { 107 | if (opt.daysConfig.length <= 0) { 108 | return undefined; 109 | } 110 | return opt.daysConfig.find((n) => day.hasSame(n.date, 'day')); 111 | } 112 | 113 | createCalendarDay(time: DateTime, opt: PickerModalOptionsSafe, month?: number): CalendarDay { 114 | const date = time; 115 | const isToday = DateTime.utc().hasSame(date, 'day'); 116 | const isBeforeToday = DateTime.utc().startOf('day') > date; 117 | const dayConfig = this.findDayConfig(date, opt); 118 | const _rangeBeg = opt.from.valueOf(); 119 | const _rangeEnd = opt.to.valueOf(); 120 | let isBetween = true; 121 | const disableWee = opt.disableWeeks.indexOf(date.toJSDate().getDay()) !== -1; 122 | if (_rangeBeg > 0 && _rangeEnd > 0) { 123 | isBetween = opt.canBackwardsSelected 124 | ? date.valueOf() < _rangeBeg 125 | ? false 126 | : isBetween 127 | : Interval.fromDateTimes(opt.from, opt.to).contains(date); 128 | } else if (_rangeBeg > 0 && _rangeEnd === 0) { 129 | if (!opt.canBackwardsSelected) { 130 | const _addTime = date.plus({ days: 1 }); 131 | isBetween = !(_addTime.valueOf() > _rangeBeg); 132 | } else { 133 | isBetween = false; 134 | } 135 | } 136 | 137 | let _disable = false; 138 | _disable = dayConfig && isBoolean(dayConfig.disable) ? dayConfig.disable : disableWee || isBetween; 139 | if (isBeforeToday && !opt.canBackwardsSelected) { 140 | _disable = true; 141 | } 142 | 143 | let title = time.day.toString(); 144 | if (dayConfig && dayConfig.title) { 145 | title = dayConfig.title; 146 | } else if (opt.defaultTitle) { 147 | title = opt.defaultTitle; 148 | } 149 | let subTitle = ''; 150 | if (dayConfig && dayConfig.subTitle) { 151 | subTitle = dayConfig.subTitle; 152 | } else if (opt.defaultSubtitle) { 153 | subTitle = opt.defaultSubtitle; 154 | } 155 | 156 | return { 157 | time, 158 | isToday, 159 | title, 160 | subTitle, 161 | selected: false, 162 | isLastMonth: date.month < month, 163 | isNextMonth: date.month > month, 164 | marked: dayConfig ? dayConfig.marked || false : false, 165 | cssClass: dayConfig ? dayConfig.cssClass || '' : '', 166 | disable: _disable, 167 | isFirst: date.day === 1, 168 | isLast: date.day === date.daysInMonth 169 | }; 170 | } 171 | 172 | createCalendarMonth(original: CalendarOriginal, opt: PickerModalOptionsSafe): CalendarMonth { 173 | const days: CalendarDay[] = new Array(6).fill(undefined); 174 | const len = original.howManyDays; 175 | for (let i = original.firstWeek; i < len + original.firstWeek; i++) { 176 | days[i] = this.createCalendarDay(original.date.plus({ days: i - original.firstWeek }), opt); 177 | } 178 | 179 | const weekStart = opt.weekStart; 180 | 181 | if (weekStart === 1) { 182 | if (days[0] === undefined) { 183 | days.shift(); 184 | } else { 185 | days.unshift(...new Array(6).fill(undefined)); 186 | } 187 | } 188 | 189 | if (opt.showAdjacentMonthDay) { 190 | const _booleanMap = days.map((e) => !!e); 191 | const thisMonth = original.date.month; 192 | let startOffsetIndex = _booleanMap.indexOf(true) - 1; 193 | let endOffsetIndex = _booleanMap.lastIndexOf(true) + 1; 194 | for (startOffsetIndex; startOffsetIndex >= 0; startOffsetIndex--) { 195 | const dayBefore = days[startOffsetIndex + 1].time.minus({ days: 1 }); 196 | days[startOffsetIndex] = this.createCalendarDay(dayBefore, opt, thisMonth); 197 | } 198 | 199 | if (!(_booleanMap.length % 7 === 0 && _booleanMap[_booleanMap.length - 1])) { 200 | for (endOffsetIndex; endOffsetIndex < days.length + (endOffsetIndex % 7); endOffsetIndex++) { 201 | const dayAfter = days[endOffsetIndex - 1].time.plus({ days: 1 }); 202 | days[endOffsetIndex] = this.createCalendarDay(dayAfter, opt, thisMonth); 203 | } 204 | } 205 | } 206 | 207 | return { 208 | days, 209 | original 210 | }; 211 | } 212 | 213 | createMonthsByPeriod(startDate: DateTime, monthsNum: number, opt: PickerModalOptionsSafe): CalendarMonth[] { 214 | const _array: CalendarMonth[] = []; 215 | 216 | const startOfMonth = startDate.startOf('month'); 217 | 218 | for (let i = 0; i < monthsNum; i++) { 219 | const time = startOfMonth.plus({ months: i }).valueOf(); 220 | const originalCalendar = this.createOriginalCalendar(time); 221 | _array.push(this.createCalendarMonth(originalCalendar, opt)); 222 | } 223 | 224 | return _array; 225 | } 226 | 227 | wrapResult(original: CalendarDay[], times: DateTime[], pickMode: PickMode) { 228 | const secondIndex = original[1] ? 1 : 0; 229 | let result: any; 230 | switch (pickMode) { 231 | case PickMode.SINGLE: 232 | result = this.multiFormat(original[0].time.valueOf()); 233 | break; 234 | case PickMode.RANGE: 235 | result = { 236 | from: this.multiFormat( 237 | DateTime.fromMillis(original[0].time.valueOf(), { zone: 'Etc/UTC' }) 238 | .set({ 239 | hour: times[0].hour, 240 | minute: times[0].minute 241 | }) 242 | .startOf('minute') 243 | .valueOf() 244 | ), 245 | to: this.multiFormat( 246 | DateTime.fromMillis(original[secondIndex].time.valueOf(), { zone: 'Etc/UTC' }) 247 | .set({ 248 | hour: times[1].hour, 249 | minute: times[1].minute 250 | }) 251 | .startOf('minute') 252 | .valueOf() 253 | ) 254 | }; 255 | break; 256 | case PickMode.MULTI: 257 | result = original.map((e) => this.multiFormat(e.time.valueOf())); 258 | break; 259 | default: 260 | result = original; 261 | } 262 | return result; 263 | } 264 | } 265 | 266 | /*function detectHourCycle(): boolean { 267 | return ( 268 | new Intl.DateTimeFormat(DateTime.now().toLocal().locale, { 269 | hour: 'numeric' 270 | }) 271 | .formatToParts(new Date(2020, 0, 1, 13)) 272 | .find((part) => part.type === 'hour').value.length === 2 273 | ); 274 | }*/ 275 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /* IE9, IE10 and IE11 requires all of the following polyfills. */ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | /* Evergreen browsers require these. */ 44 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 45 | import 'core-js/proposals/reflect-metadata'; 46 | 47 | /* 48 | * Required to support Web Animations `@angular/platform-browser/animations`. 49 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 50 | */ 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /*************************************************************************************************** 54 | * Zone JS is required by Angular itself. 55 | */ 56 | import 'zone.js'; // Included with Angular CLI. 57 | 58 | /*************************************************************************************************** 59 | * APPLICATION IMPORTS 60 | */ 61 | 62 | /** 63 | * Date, currency, decimal and percent pipes. 64 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 65 | */ 66 | // import 'intl'; // Run `npm install --save intl`. 67 | /** 68 | * Need to import at least one locale-data with intl. 69 | */ 70 | // import 'intl/locale-data/jsonp/en'; 71 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | import 'zone.js/testing'; 3 | import { getTestBed } from '@angular/core/testing'; 4 | import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; 5 | 6 | declare const require: any; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); 10 | // Then we find all the tests. 11 | const context = require.context('./', true, /\.spec\.ts$/); 12 | // And load the modules. 13 | context.keys().map(context); 14 | -------------------------------------------------------------------------------- /src/tests/modal-basic.spec.ts: -------------------------------------------------------------------------------- 1 | import { NO_ERRORS_SCHEMA } from '@angular/core'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { ModalController } from '@ionic/angular'; 4 | import { DateTime } from 'luxon'; 5 | 6 | import { DemoModalBasicComponent } from './modal-basic'; 7 | 8 | describe('DemoModalBasicComponent', () => { 9 | let component: DemoModalBasicComponent; 10 | let fixture: ComponentFixture; 11 | beforeEach(() => { 12 | const modalControllerStub = { create: () => ({}) }; 13 | TestBed.configureTestingModule({ 14 | schemas: [NO_ERRORS_SCHEMA], 15 | declarations: [DemoModalBasicComponent], 16 | providers: [{ provide: ModalController, useValue: modalControllerStub }] 17 | }); 18 | fixture = TestBed.createComponent(DemoModalBasicComponent); 19 | component = fixture.componentInstance; 20 | }); 21 | 22 | // ===================== TESTS ===================== 23 | it('can load instance', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | 27 | it('return undefined on cancel', () => { 28 | const dismissSpy = spyOn(component.modalCtrlMock, 'dismiss'); 29 | 30 | component.openCalendar(); 31 | 32 | component.myPicker.onCancel(); 33 | 34 | expect(dismissSpy).toHaveBeenCalledWith(undefined, 'cancel'); 35 | }); 36 | 37 | it('return input with duration 30 minutes if unchanged and no to-Date', () => { 38 | const testDate = DateTime.utc().plus({ hours: 1 }).startOf('minute'); 39 | component.dateRange = { 40 | from: testDate.toMillis(), 41 | to: testDate.toMillis() 42 | }; 43 | 44 | const dismissSpy = spyOn(component.modalCtrlMock, 'dismiss'); 45 | 46 | component.openCalendar(); 47 | 48 | component.myPicker.done(); 49 | 50 | component.dateRange.to = testDate.plus({ minutes: 30 }).toMillis(); 51 | 52 | expect(dismissSpy.calls.mostRecent().args[1]).toBe('done'); 53 | 54 | expect(dismissSpy.calls.mostRecent().args[0].from.time).toBe(component.dateRange.from.valueOf()); 55 | expect(dismissSpy.calls.mostRecent().args[0].to.time).toBe(component.dateRange.to.valueOf()); 56 | }); 57 | 58 | it('return input if valid to-from supplied', () => { 59 | const testDate = DateTime.utc().plus({ hours: 1 }).startOf('minute'); 60 | component.dateRange = { 61 | from: testDate.toMillis(), 62 | to: DateTime.utc().plus({ hours: 2 }).startOf('minute').toMillis() 63 | }; 64 | 65 | const dismissSpy = spyOn(component.modalCtrlMock, 'dismiss'); 66 | 67 | component.openCalendar(); 68 | 69 | component.myPicker.done(); 70 | 71 | expect(dismissSpy.calls.mostRecent().args[1]).toBe('done'); 72 | 73 | expect(dismissSpy.calls.mostRecent().args[0].from.time).toBe(component.dateRange.from.valueOf()); 74 | expect(dismissSpy.calls.mostRecent().args[0].to.time).toBe(component.dateRange.to.valueOf()); 75 | }); 76 | 77 | it('prevent invalid input dates from being returned', () => { 78 | const testDate = DateTime.utc().plus({ hours: 1 }).startOf('minute'); 79 | const expectedTo = DateTime.utc().plus({ hours: 2 }).startOf('minute'); 80 | component.dateRange = { 81 | from: testDate.toMillis(), 82 | to: testDate.minus({ hours: 2 }).toMillis() 83 | }; 84 | 85 | const dismissSpy = spyOn(component.modalCtrlMock, 'dismiss'); 86 | 87 | component.openCalendar(); 88 | 89 | component.myPicker.done(); 90 | 91 | expect(dismissSpy.calls.mostRecent().args[1]).toBe('done'); 92 | 93 | expect(dismissSpy.calls.mostRecent().args[0].from.time).toBe(component.dateRange.from.valueOf()); 94 | expect(dismissSpy.calls.mostRecent().args[0].to.time).toBe(expectedTo.valueOf()); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /src/tests/modal-basic.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { NavParamsMock } from 'ionic-mocks'; 4 | import { DateTime } from 'luxon'; 5 | 6 | import { PickMode, PickerModal, PickerModalOptions } from '../moots-picker'; 7 | import { CalendarService } from '../moots-picker/services/calendar.service'; 8 | 9 | import { CDRefMock, ModalCtrlMock, RendererMock } from './test-mocks'; 10 | 11 | @Component({ 12 | selector: 'demo-modal-basic', 13 | template: ` basic ` 14 | }) 15 | /** Creates and opens a basic modal picker to be tested */ 16 | export class DemoModalBasicComponent { 17 | currentDate: DateTime = DateTime.utc(); 18 | dateRange = { 19 | from: this.currentDate.valueOf(), 20 | to: this.currentDate.valueOf() 21 | }; 22 | 23 | modalCtrlMock: ModalController = new ModalCtrlMock(); 24 | myPicker: PickerModal; 25 | 26 | constructor() { 27 | /**/ 28 | } 29 | 30 | async openCalendar() { 31 | const options: PickerModalOptions = { 32 | pickMode: PickMode.RANGE, 33 | title: 'RANGE', 34 | defaultDateRange: this.dateRange, 35 | canBackwardsSelected: false, 36 | weekStart: 1, 37 | step: 4, 38 | locale: window.navigator.language 39 | }; 40 | 41 | const rendererMock = new RendererMock(); 42 | const elemRefMock = new ElementRef(undefined); 43 | const cdRefMock = new CDRefMock(); 44 | this.myPicker = new PickerModal( 45 | rendererMock, 46 | elemRefMock, 47 | NavParamsMock.instance(), 48 | this.modalCtrlMock, 49 | cdRefMock, 50 | new CalendarService() 51 | ); 52 | 53 | this.myPicker.options = options; 54 | this.myPicker.ngOnInit(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/tests/test-mocks.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Renderer2, RendererStyleFlags2 } from '@angular/core'; 2 | import { ModalController } from '@ionic/angular'; 3 | import { ModalOptions } from '@ionic/core'; 4 | 5 | export class ModalCtrlMock extends ModalController { 6 | constructor() { 7 | super(undefined, undefined, undefined); 8 | } 9 | 10 | create(_opts: ModalOptions): Promise { 11 | throw new Error('Method not implemented.'); 12 | } 13 | } 14 | 15 | export class RendererMock extends Renderer2 { 16 | data: { [key: string]: any; }; 17 | destroy(): void { 18 | throw new Error('Method not implemented.'); 19 | } 20 | createElement(_name: string, _namespace?: string) { 21 | throw new Error('Method not implemented.'); 22 | } 23 | createComment(_value: string) { 24 | throw new Error('Method not implemented.'); 25 | } 26 | createText(_value: string) { 27 | throw new Error('Method not implemented.'); 28 | } 29 | appendChild(_parent: any, _newChild: any): void { 30 | throw new Error('Method not implemented.'); 31 | } 32 | insertBefore(_parent: any, _newChild: any, _refChild: any): void { 33 | throw new Error('Method not implemented.'); 34 | } 35 | removeChild(_parent: any, _oldChild: any): void { 36 | throw new Error('Method not implemented.'); 37 | } 38 | selectRootElement(_selectorOrNode: any) { 39 | throw new Error('Method not implemented.'); 40 | } 41 | parentNode(_node: any) { 42 | throw new Error('Method not implemented.'); 43 | } 44 | nextSibling(_node: any) { 45 | throw new Error('Method not implemented.'); 46 | } 47 | setAttribute(_el: any, _name: string, _value: string, _namespace?: string): void { 48 | throw new Error('Method not implemented.'); 49 | } 50 | removeAttribute(_el: any, _name: string, _namespace?: string): void { 51 | throw new Error('Method not implemented.'); 52 | } 53 | addClass(_el: any, _name: string): void { 54 | throw new Error('Method not implemented.'); 55 | } 56 | removeClass(_el: any, _name: string): void { 57 | throw new Error('Method not implemented.'); 58 | } 59 | setStyle(_el: any, _style: string, _value: any, _flags?: RendererStyleFlags2): void { 60 | throw new Error('Method not implemented.'); 61 | } 62 | removeStyle(_el: any, _style: string, _flags?: RendererStyleFlags2): void { 63 | throw new Error('Method not implemented.'); 64 | } 65 | setProperty(_el: any, _name: string, _value: any): void { 66 | throw new Error('Method not implemented.'); 67 | } 68 | setValue(_node: any, _value: string): void { 69 | throw new Error('Method not implemented.'); 70 | } 71 | listen(_target: any, _eventName: string, _callback: (event: any) => boolean | void): () => void { 72 | throw new Error('Method not implemented.'); 73 | } 74 | } 75 | 76 | export class CDRefMock extends ChangeDetectorRef { 77 | markForCheck(): void { 78 | throw new Error('Method not implemented.'); 79 | } 80 | detach(): void { 81 | throw new Error('Method not implemented.'); 82 | } 83 | detectChanges(): void { 84 | throw new Error('Method not implemented.'); 85 | } 86 | checkNoChanges(): void { 87 | throw new Error('Method not implemented.'); 88 | } 89 | reattach(): void { 90 | throw new Error('Method not implemented.'); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #3dc2ff; 16 | --ion-color-secondary-rgb: 61, 194, 255; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #36abe0; 20 | --ion-color-secondary-tint: #50c8ff; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #5260ff; 24 | --ion-color-tertiary-rgb: 82, 96, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #4854e0; 28 | --ion-color-tertiary-tint: #6370ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #2dd36f; 32 | --ion-color-success-rgb: 45, 211, 111; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #28ba62; 36 | --ion-color-success-tint: #42d77d; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffc409; 40 | --ion-color-warning-rgb: 255, 196, 9; 41 | --ion-color-warning-contrast: #000000; 42 | --ion-color-warning-contrast-rgb: 0, 0, 0; 43 | --ion-color-warning-shade: #e0ac08; 44 | --ion-color-warning-tint: #ffca22; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #eb445a; 48 | --ion-color-danger-rgb: 235, 68, 90; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #cf3c4f; 52 | --ion-color-danger-tint: #ed576b; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 36, 40; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #92949c; 64 | --ion-color-medium-rgb: 146, 148, 156; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #808289; 68 | --ion-color-medium-tint: #9d9fa6; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 245, 248; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | } 78 | 79 | @media (prefers-color-scheme: dark) { 80 | /* 81 | * Dark Colors 82 | * ------------------------------------------- 83 | */ 84 | 85 | body { 86 | --ion-color-primary: #428cff; 87 | --ion-color-primary-rgb: 66,140,255; 88 | --ion-color-primary-contrast: #ffffff; 89 | --ion-color-primary-contrast-rgb: 255,255,255; 90 | --ion-color-primary-shade: #3a7be0; 91 | --ion-color-primary-tint: #5598ff; 92 | 93 | --ion-color-secondary: #50c8ff; 94 | --ion-color-secondary-rgb: 80,200,255; 95 | --ion-color-secondary-contrast: #ffffff; 96 | --ion-color-secondary-contrast-rgb: 255,255,255; 97 | --ion-color-secondary-shade: #46b0e0; 98 | --ion-color-secondary-tint: #62ceff; 99 | 100 | --ion-color-tertiary: #6a64ff; 101 | --ion-color-tertiary-rgb: 106,100,255; 102 | --ion-color-tertiary-contrast: #ffffff; 103 | --ion-color-tertiary-contrast-rgb: 255,255,255; 104 | --ion-color-tertiary-shade: #5d58e0; 105 | --ion-color-tertiary-tint: #7974ff; 106 | 107 | --ion-color-success: #2fdf75; 108 | --ion-color-success-rgb: 47,223,117; 109 | --ion-color-success-contrast: #000000; 110 | --ion-color-success-contrast-rgb: 0,0,0; 111 | --ion-color-success-shade: #29c467; 112 | --ion-color-success-tint: #44e283; 113 | 114 | --ion-color-warning: #ffd534; 115 | --ion-color-warning-rgb: 255,213,52; 116 | --ion-color-warning-contrast: #000000; 117 | --ion-color-warning-contrast-rgb: 0,0,0; 118 | --ion-color-warning-shade: #e0bb2e; 119 | --ion-color-warning-tint: #ffd948; 120 | 121 | --ion-color-danger: #ff4961; 122 | --ion-color-danger-rgb: 255,73,97; 123 | --ion-color-danger-contrast: #ffffff; 124 | --ion-color-danger-contrast-rgb: 255,255,255; 125 | --ion-color-danger-shade: #e04055; 126 | --ion-color-danger-tint: #ff5b71; 127 | 128 | --ion-color-dark: #f4f5f8; 129 | --ion-color-dark-rgb: 244,245,248; 130 | --ion-color-dark-contrast: #000000; 131 | --ion-color-dark-contrast-rgb: 0,0,0; 132 | --ion-color-dark-shade: #d7d8da; 133 | --ion-color-dark-tint: #f5f6f9; 134 | 135 | --ion-color-medium: #989aa2; 136 | --ion-color-medium-rgb: 152,154,162; 137 | --ion-color-medium-contrast: #000000; 138 | --ion-color-medium-contrast-rgb: 0,0,0; 139 | --ion-color-medium-shade: #86888f; 140 | --ion-color-medium-tint: #a2a4ab; 141 | 142 | --ion-color-light: #222428; 143 | --ion-color-light-rgb: 34,36,40; 144 | --ion-color-light-contrast: #ffffff; 145 | --ion-color-light-contrast-rgb: 255,255,255; 146 | --ion-color-light-shade: #1e2023; 147 | --ion-color-light-tint: #383a3e; 148 | } 149 | 150 | /* 151 | * iOS Dark Theme 152 | * ------------------------------------------- 153 | */ 154 | 155 | .ios body { 156 | --ion-background-color: #000000; 157 | --ion-background-color-rgb: 0,0,0; 158 | 159 | --ion-text-color: #ffffff; 160 | --ion-text-color-rgb: 255,255,255; 161 | 162 | --ion-color-step-50: #0d0d0d; 163 | --ion-color-step-100: #1a1a1a; 164 | --ion-color-step-150: #262626; 165 | --ion-color-step-200: #333333; 166 | --ion-color-step-250: #404040; 167 | --ion-color-step-300: #4d4d4d; 168 | --ion-color-step-350: #595959; 169 | --ion-color-step-400: #666666; 170 | --ion-color-step-450: #737373; 171 | --ion-color-step-500: #808080; 172 | --ion-color-step-550: #8c8c8c; 173 | --ion-color-step-600: #999999; 174 | --ion-color-step-650: #a6a6a6; 175 | --ion-color-step-700: #b3b3b3; 176 | --ion-color-step-750: #bfbfbf; 177 | --ion-color-step-800: #cccccc; 178 | --ion-color-step-850: #d9d9d9; 179 | --ion-color-step-900: #e6e6e6; 180 | --ion-color-step-950: #f2f2f2; 181 | 182 | --ion-item-background: #000000; 183 | 184 | --ion-card-background: #1c1c1d; 185 | } 186 | 187 | .ios ion-modal { 188 | --ion-background-color: var(--ion-color-step-100); 189 | --ion-toolbar-background: var(--ion-color-step-150); 190 | --ion-toolbar-border-color: var(--ion-color-step-250); 191 | } 192 | 193 | 194 | /* 195 | * Material Design Dark Theme 196 | * ------------------------------------------- 197 | */ 198 | 199 | .md body { 200 | --ion-background-color: #121212; 201 | --ion-background-color-rgb: 18,18,18; 202 | 203 | --ion-text-color: #ffffff; 204 | --ion-text-color-rgb: 255,255,255; 205 | 206 | --ion-border-color: #222222; 207 | 208 | --ion-color-step-50: #1e1e1e; 209 | --ion-color-step-100: #2a2a2a; 210 | --ion-color-step-150: #363636; 211 | --ion-color-step-200: #414141; 212 | --ion-color-step-250: #4d4d4d; 213 | --ion-color-step-300: #595959; 214 | --ion-color-step-350: #656565; 215 | --ion-color-step-400: #717171; 216 | --ion-color-step-450: #7d7d7d; 217 | --ion-color-step-500: #898989; 218 | --ion-color-step-550: #949494; 219 | --ion-color-step-600: #a0a0a0; 220 | --ion-color-step-650: #acacac; 221 | --ion-color-step-700: #b8b8b8; 222 | --ion-color-step-750: #c4c4c4; 223 | --ion-color-step-800: #d0d0d0; 224 | --ion-color-step-850: #dbdbdb; 225 | --ion-color-step-900: #e7e7e7; 226 | --ion-color-step-950: #f3f3f3; 227 | 228 | --ion-item-background: #1e1e1e; 229 | 230 | --ion-toolbar-background: #1f1f1f; 231 | 232 | --ion-tab-bar-background: #1f1f1f; 233 | 234 | --ion-card-background: #1e1e1e; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./" 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts" 13 | ], 14 | "include": [ 15 | "polyfills.ts", 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowUnreachableCode": false, 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "noImplicitAny": true, 12 | "noUnusedParameters": true, 13 | "noUnusedLocals": true, 14 | "target": "es2015", 15 | "module": "es2020", 16 | "lib": ["es2018", "dom"], 17 | "types": ["jasmine"] 18 | }, 19 | "angularCompilerOptions": { 20 | "enableI18nLegacyMessageIdFormat": false, 21 | "strictInjectionParameters": true, 22 | "strictInputAccessModifiers": true, 23 | "strictTemplates": true 24 | } 25 | } 26 | --------------------------------------------------------------------------------