diff --git a/.editorconfig b/.editorconfig index 259cf3d4..efb33674 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,4 +17,4 @@ trim_trailing_whitespace = true insert_final_newline = true [*.md] -trim_trailing_whitespace = false +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.gitattributes b/.gitattributes index 0ab1bfae..90b98ce7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,15 +6,17 @@ # checkin and prevent conversion to CRLF when they are checked out # (this is required in order to prevent newline related issues like, # for example, after the build script is run) -*.* text eol=lf .* text eol=lf *.css text eol=lf *.html text eol=lf *.js text eol=lf *.json text eol=lf -*.scss text eol=lf *.md text eol=lf +*.scss text eol=lf *.sh text eol=lf +*.ts text eol=lf +*.tsx text eol=lf *.txt text eol=lf *.xml text eol=lf +*.yaml text eol=lf *.yml text eol=lf diff --git a/.gitignore b/.gitignore index 5ac47b3d..884f21bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,19 @@ -bower_components* -bower-*.json +# .gitignore 101 - https://www.quora.com/Does-gitignore-file-only-work-on-current-folder + +# dot files +.*cache +.build/ .DS_Store +.tmp/ +.vscode +*.env + +# debug +*.log* + +# folders +coverage*/ +dist*/ +node_modules/ + +# Do not ignore diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..b93f74c0 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# User config +package-lock=true +progress=true +quiet=true +update-binary=true diff --git a/.travis.yml b/.travis.yml index 208c37dc..8c1a0c2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,42 @@ language: node_js sudo: required -before_script: - - npm install -g polymer-cli - - polymer install --variants -node_js: stable +dist: trusty +node_js: + - '8' + - '10' addons: firefox: latest - apt: - sources: - - google-chrome - packages: - - google-chrome-stable + chrome: stable +cache: + directories: + - node_modules +before_install: + - npm i -g npm@latest polymer-cli@latest +install: + - npm ci --quiet +before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start + - npm run ts script: - - xvfb-run polymer test -dist: trusty + - npm run test:ci + - if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then + npm run test:ci -- + -s 'windows 7/internet explorer@11' + -s 'windows 10/microsoftedge@18' + -s 'windows 10/microsoftedge@17' + -s 'windows 10/microsoftedge@13' + -s 'os x 10.11/safari@9' + -s 'macos 10.12/safari@10.1' + -s 'macos 10.13/safari@11.1' + -s 'macos 10.13/safari@12' + -s 'Linux/chrome@41' + -s 'windows 10/chrome@70' + -s 'windows 10/firefox@62' + -s 'windows 10/firefox@63'; + fi + +# Should you require setup for TravisCI with Saucelabs? +# Watch https://www.youtube.com/watch?v=afy_EEq_4Go + +# Platform configuration: https://wiki.saucelabs.com/display/DOCS/Platform+Configurator#/ diff --git a/.yo-rc.json b/.yo-rc.json deleted file mode 100644 index cf932709..00000000 --- a/.yo-rc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "generator-polymer": { - "ghUser": "motss" - } -} \ No newline at end of file diff --git a/LICENSE b/LICENSE index b7da17b5..8b34d4cc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ The MIT License (MIT) -Copyright © 2017 Rong Sen Ng +Copyright © 2018 Rong Sen Ng Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 39ffd347..ca4df289 100644 --- a/README.md +++ b/README.md @@ -1,127 +1,31 @@ -[![GitHub version](https://badge.fury.io/gh/motss%2Fapp-datepicker.svg)](http://badge.fury.io/gh/motss%2Fapp-datepicker) -[![Bower version](https://badge.fury.io/bo/app-datepicker.svg)](http://badge.fury.io/bo/app-datepicker) -[![Build Status](https://travis-ci.org/motss/app-datepicker.svg?branch=master)](https://travis-ci.org/motss/app-datepicker) +
+

app-datepicker

-# app-datepicker (formerly `jv-datepicker`) -![img-app-datepicker](https://cloud.githubusercontent.com/assets/10607759/26274668/48b75cce-3d81-11e7-81aa-b79ab9b90d36.png) +

Datepicker element built with lit-element and Material Design 2

+
- - - - +
+> A different way of `datepicker`-ing on the web. +Stay tuned for more update on imminent release! 🎉 -See the [component page](http://motss.github.io/app-datepicker/components/app-datepicker/) for more information. +Meantime, feel free to check the older version out at: -An custom Polymer element built from scratch to provide a datepicker based on Google's Material Design that is more compelling and rich with features. + 1. [`2.x` branch][2-x-url] - Built with Material Design and [Polymer 2][polymer-2-url], published at Bower. + 2. [`3.x` branch][3-x-url] - Built with Material Design and [Polymer 3][polymer-3-url], published at NPM. +![screen shot 2019-01-30 at 12 49 30](https://user-images.githubusercontent.com/10607759/51959002-857c1100-248d-11e9-8d1a-9abbafdb2385.png) -## Update (v2.11.0) -- **Pleased to announce that `app-datepicker` is now compatible with both Polymer 1.x and Polymer 2.0 stable.** -- **Now Intl polyfill will not load (previously it does) if the browser does not natively support it and it is recommended for users to load the polyfill at the top-level document by some feature detections.** -- `alwaysResetSelectedDateOnDialogClose` - proposed by [#74](https://github.com/motss/app-datepicker/issues/74) to allow datepicker to reset the selected date to today's date once the datepicker closes and the demo has this included as well. -- As of `v2.11`, all dates will no longer include users' local system's timezone offset and all will be default to GMT/ UTC timezone. For more info, please see [#89](https://github.com/motss/app-datepicker/pull/89). - -Example: - - - - - - - - -`app-datepicker` provides a regular datepicker element. -While `app-datepicker-dialog` has a `app-datepicker` being wrapped inside a dialog. - - -~~## ( Coming soon!) Generating your own boilerplate code of the compounds~~ -~~At the end of the demo, there is a section where user can play around with to generate your own boilerplate code with the attributes provided.~~ - - -## Styling -Now with mixins, head over to the [component page](http://motss.github.io/app-datepicker/components/app-datepicker/) for more details. - - -## Getting Started -1. Install with bower. -`bower install --save app-datepicker` - -2. Load the dependencies and the Intl polyfill if needed. - - Load [`Intl Polyfill`](https://github.com/andyearnshaw/Intl.js) for unsupported browsers via feature detection, - - ```js - if (window.Intl) { - var intlPolyfill = document.createElement('script'); - intlPolyfill.src = 'path_to_intl_polyfill'; - document.head.appendChild(intlPolyfill); - } - ``` - - For `app-datepicker`, - - ```html - - ``` - For `app-datepicker-dialog`, - - ```html - - ``` - -3. Markup with `` or ``. - -4. Done. - -## Browser Support - -### `app-datepicker` and `app-datepicker-dialog`: - -#### Microsoft Windows x64 -| ![Internet Explorer](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/internet-explorer/internet-explorer_48x48.png) | ![Microsoft Edge](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/edge/edge_48x48.png) | ![Mozilla Firefox](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox/firefox_48x48.png) ![Mozilla Firefox Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox-developer-edition/firefox-developer-edition_48x48.png) | ![Google Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_48x48.png) ![Google Chrome Canary](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/archive/chrome-canary_19-48/chrome-canary_19-48_48x48.png) | ![Opera](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera/opera_48x48.png) ![Opera Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera-developer/opera-developer_48x48.png) -| --- | --- | --- | --- | --- -| 11 | 12+ | latest | latest | latest - - -#### Linux x64 -| ![Mozilla Firefox](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox/firefox_48x48.png) ![Mozilla Firefox Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox-developer-edition/firefox-developer-edition_48x48.png) | ![Google Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_48x48.png) ![Google Chrome Canary](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/archive/chrome-canary_19-48/chrome-canary_19-48_48x48.png) | ![Opera](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera/opera_48x48.png) ![Opera Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera-developer/opera-developer_48x48.png) -| --- | --- | --- -| latest | latest | latest - - -#### Mac OS X -| ![Mozilla Firefox](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox/firefox_48x48.png) ![Mozilla Firefox Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox-developer-edition/firefox-developer-edition_48x48.png) | ![Google Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_48x48.png) ![Google Chrome Canary](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/archive/chrome-canary_19-48/chrome-canary_19-48_48x48.png) | ![Opera](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera/opera_48x48.png) ![Opera Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera-developer/opera-developer_48x48.png) | ![Safari](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/safari/safari_48x48.png) ![Safari Technology Preview](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/safari-technology-preview/safari-technology-preview_48x48.png) -| --- | --- | --- | --- -| latest | latest | latest | 7+ - - -#### Android 4.4.4 and above - -| ![Mozilla Firefox](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox/firefox_48x48.png) | ![Google Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_48x48.png) ![Google Chrome Dev](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome-dev/chrome-dev_48x48.png) | ![Opera](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera/opera_48x48.png) | ![Android WebView](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/android-webview-beta/android-webview-beta_48x48.png) -| --- | --- | --- | --- -| latest | latest | latest | latest - -#### iOS 7.1 and above -| ![Mozilla Firefox](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox/firefox_48x48.png) | ![Google Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_48x48.png) | ![Opera](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera/opera_48x48.png) | ![Safari for iOS](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/safari-ios/safari-ios_48x48.png) -| --- | --- | --- | --- -| latest | latest | latest | 7+ - -### [Intl.DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat) - -ECMAScript Internationalization API for `locale`. For more details please visit [Can I use... Intl?](http://caniuse.com/#search=intl): - - -| ![Internet Explorer](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/internet-explorer/internet-explorer_48x48.png) | ![Microsoft Edge](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/edge/edge_48x48.png) | ![Mozilla Firefox](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox/firefox_48x48.png) ![Mozilla Firefox Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/firefox-developer-edition/firefox-developer-edition_48x48.png) | ![Google Chrome](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/chrome/chrome_48x48.png) ![Google Chrome Canary](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/archive/chrome-canary_19-48/chrome-canary_19-48_48x48.png) | ![Opera](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera/opera_48x48.png) ![Opera Developer Edition](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/opera-developer/opera-developer_48x48.png) | ![Safari](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/safari/safari_48x48.png) ![Safari Technology Preview](https://cdnjs.cloudflare.com/ajax/libs/browser-logos/35.1.0/safari-technology-preview/safari-technology-preview_48x48.png) -| --- | --- | --- | --- | --- | --- | -| 11 | 12+ | latest | latest | latest | 10+ ** +## License -___** [Intl Polyfill for unsupported browsers](https://github.com/andyearnshaw/Intl.js)___ +[MIT License](http://motss.mit-license.org/) © Rong Sen Ng -## Throughput -[![Throughput Graph](https://graphs.waffle.io/motss/app-datepicker/throughput.svg)](https://waffle.io/motss/app-datepicker/metrics/throughput) +[2-x-url]: https://github.com/motss/app-datepicker/tree/2.x +[3-x-url]: https://github.com/motss/app-datepicker/tree/3.x +[polymer-2-url]: https://polymer-library.polymer-project.org/2.0/docs/devguide/feature-overview +[polymer-3-url]: https://polymer-library.polymer-project.org/3.0/docs/devguide/feature-overview -## License -[MIT License](http://motss.mit-license.org/) © Rong Sen Ng +[intl-polyfill-url]: https://github.com/andyearnshaw/Intl.js +[web-animations-js-polyfill-url]: https://www.npmjs.com/package/web-animations-js +[polymer-3-browser-support-url]: https://polymer-library.polymer-project.org/3.0/docs/browsers diff --git a/animations/datepicker-slide-from-bottom-animation.html b/animations/datepicker-slide-from-bottom-animation.html deleted file mode 100644 index 05cbf3da..00000000 --- a/animations/datepicker-slide-from-bottom-animation.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/animations/datepicker-slide-from-left-animation.html b/animations/datepicker-slide-from-left-animation.html deleted file mode 100644 index 6465176a..00000000 --- a/animations/datepicker-slide-from-left-animation.html +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - diff --git a/animations/datepicker-slide-from-right-animation.html b/animations/datepicker-slide-from-right-animation.html deleted file mode 100644 index a9df7c09..00000000 --- a/animations/datepicker-slide-from-right-animation.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/animations/datepicker-slide-from-top-animation.html b/animations/datepicker-slide-from-top-animation.html deleted file mode 100644 index d650cd41..00000000 --- a/animations/datepicker-slide-from-top-animation.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - diff --git a/app-datepicker-animations.html b/app-datepicker-animations.html deleted file mode 100644 index 04c6fa5e..00000000 --- a/app-datepicker-animations.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - diff --git a/app-datepicker-dialog.html b/app-datepicker-dialog.html deleted file mode 100644 index add3074d..00000000 --- a/app-datepicker-dialog.html +++ /dev/null @@ -1,151 +0,0 @@ - - - - - - - - - - - - diff --git a/app-datepicker-icons.html b/app-datepicker-icons.html deleted file mode 100644 index a70f20e0..00000000 --- a/app-datepicker-icons.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app-datepicker.html b/app-datepicker.html deleted file mode 100644 index fa11c0da..00000000 --- a/app-datepicker.html +++ /dev/null @@ -1,1773 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/bower.json b/bower.json deleted file mode 100644 index edc78d8c..00000000 --- a/bower.json +++ /dev/null @@ -1,90 +0,0 @@ -{ - "name": "app-datepicker", - "version": "2.12.0", - "authors": [ - "motss " - ], - "description": "A datepicker element based on Google's Material Design built from scratch with Polymer", - "keywords": [ - "web-component", - "web-components", - "polymer", - "seed", - "calendar", - "date", - "picker", - "cal", - "date-picker", - "datepicker", - "motss" - ], - "main": "app-datepicker.html", - "license": "MIT", - "homepage": "https://github.com/motss/app-datepicker/", - "ignore": [ - "/.*", - "LICENSE", - "README.md" - ], - "dependencies": { - "iron-iconset-svg": "PolymerElements/iron-iconset-svg#1 - 2", - "iron-list": "PolymerElements/iron-list#1 - 2", - "iron-selector": "PolymerElements/iron-selector#1 - 2", - "neon-animation": "PolymerElements/neon-animation#1 - 2", - "paper-dialog-behavior": "PolymerElements/paper-dialog-behavior#1 - 2", - "paper-icon-button": "PolymerElements/paper-icon-button#1 - 2", - "polymer": "Polymer/polymer#1.9 - 2", - "web-animations-js": "web-animations/web-animations-js#^2.2.0", - "paper-button": "PolymerElements/paper-button#1 - 2" - - }, - "devDependencies": { - "iron-component-page": "PolymerElements/iron-component-page#1 - 2", - "iron-demo-helpers": "PolymerElements/iron-demo-helpers#1 - 2", - "iron-validator-behavior": "PolymerElements/iron-validator-behavior#1 - 2", - "paper-button": "PolymerElements/paper-button#1 - 2", - "paper-checkbox": "PolymerElements/paper-checkbox#1 - 2", - "paper-input": "PolymerElements/paper-input#1 - 2", - "paper-dialog": "PolymerElements/paper-dialog#1 - 2", - "web-component-tester": "^6.0.0", - "iron-test-helpers": "polymerelements/iron-test-helpers#1 - 2", - "web-animations-js": "web-animations/web-animations-js#^2.2.0", - "webcomponentsjs": "webcomponents/webcomponentsjs#^1.0.0", - "fetch": "^1.0.0", - "intl": "^1.2.4" - }, - "variants": { - "1.x": { - "dependencies": { - "iron-iconset-svg": "PolymerElements/iron-iconset-svg#^1.1.1", - "iron-list": "PolymerElements/iron-list#^1.4.6", - "iron-selector": "PolymerElements/iron-selector#^1.5.3", - "neon-animation": "PolymerElements/neon-animation#^1.2.5", - "paper-dialog-behavior": "PolymerElements/paper-dialog-behavior#^1.2.8", - "paper-icon-button": "PolymerElements/paper-icon-button#^1.1.6", - "polymer": "Polymer/polymer#^1.9.1" - }, - "devDependencies": { - "iron-component-page": "PolymerElements/iron-component-page#^1.1.9", - "iron-demo-helpers": "PolymerElements/iron-demo-helpers#^1.2.6", - "iron-validator-behavior": "PolymerElements/iron-validator-behavior#^1.0.2", - "paper-button": "PolymerElements/paper-button#^1.0.15", - "paper-checkbox": "PolymerElements/paper-checkbox#^1.4.2", - "paper-input": "PolymerElements/paper-input#^1.1.24", - "paper-dialog": "PolymerElements/paper-dialog#^1.1.0", - "iron-test-helpers": "polymerelements/iron-test-helpers#^1.4.1", - "web-component-tester": "^5.0.0", - "web-animations-js": "web-animations/web-animations-js#^2.2.0", - "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0", - "fetch": "^1.0.0", - "intl": "^1.2.4" - }, - "resolutions": { - "webcomponentsjs": "^0.7" - } - } - }, - "resolutions": { - "webcomponentsjs": "^1.0.0" - } -} diff --git a/clean-directories.sh b/clean-directories.sh deleted file mode 100644 index 250c03c9..00000000 --- a/clean-directories.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -clear; printf "Running %s script...\n" "$(basename "$0" .sh)" - -has_unused_directories=false -directories="dist/ bower_components/ bower_components-1.x/ bower-1.x.json node_modules/ npm-debug.log yarn-error.log" - -for directory in $directories; do - if [ -d "$directory" ] || [ -f "$directory" ]; then - printf "\nRemoving %s...\n" "$directory" - rm -rf "$directory" - has_unused_directories=true - fi -done - -if [ "$has_unused_directories" = true ]; then - printf "\nCleaning done.\n" -else - printf "\nNothing to clean here.\n\n" -fi diff --git a/code-of-conduct.md b/code-of-conduct.md index 38b9bd25..d2f46afd 100644 --- a/code-of-conduct.md +++ b/code-of-conduct.md @@ -68,7 +68,7 @@ members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at [http://contributor-covenant.org/version/1/4][version] +available at [https://contributor-covenant.org/version/1/4][version] -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/demo/bind-date-to-input-date.html b/demo/bind-date-to-input-date.html deleted file mode 100644 index 7857d6ee..00000000 --- a/demo/bind-date-to-input-date.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - diff --git a/demo/demo.html b/demo/demo.html deleted file mode 100644 index a98cf627..00000000 --- a/demo/demo.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - Bind input-date to date - - - - - - - - - - - - - - - diff --git a/demo/fetch.html b/demo/fetch.html deleted file mode 100644 index c158dc12..00000000 --- a/demo/fetch.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/demo/index.html b/demo/index.html deleted file mode 100644 index bb683823..00000000 --- a/demo/index.html +++ /dev/null @@ -1,589 +0,0 @@ - - - - app-datepicker Demo - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/index.html b/index.html index 38fdbb70..246f24bf 100644 --- a/index.html +++ b/index.html @@ -1,21 +1,175 @@ - - - app-datepicker - - - - - - - - - - + + + + + + + + + + + - + + + +

app-datepicker

+ +
+
+
PointerEvent: 
+ + + + + + + + + + + + Select date + +
+ - diff --git a/intl.html b/intl.html deleted file mode 100644 index 6f4a4c7d..00000000 --- a/intl.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..26b8f820 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,766 @@ +{ + "name": "app-datepicker", + "version": "4.0.0-rc.4", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@fimbul/bifrost": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@fimbul/bifrost/-/bifrost-0.17.0.tgz", + "integrity": "sha512-gVTkJAOef5HtN6LPmrtt5fAUmBywwlgmObsU3FBhPoNeXPLaIl2zywXkJEtvvVLQnaFmtff3x+wIj5lHRCDE3Q==", + "dev": true, + "requires": { + "@fimbul/ymir": "^0.17.0", + "get-caller-file": "^2.0.0", + "tslib": "^1.8.1", + "tsutils": "^3.5.0" + }, + "dependencies": { + "tsutils": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.7.0.tgz", + "integrity": "sha512-n+e+3q7Jx2kfZw7tjfI9axEIWBY0sFMOlC+1K70X0SeXpO/UYSB+PN+E9tIJNqViB7oiXQdqD7dNchnvoneZew==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "@fimbul/ymir": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@fimbul/ymir/-/ymir-0.17.0.tgz", + "integrity": "sha512-xMXM9KTXRLHLVS6dnX1JhHNEkmWHcAVCQ/4+DA1KKwC/AFnGHzu/7QfQttEPgw3xplT+ILf9e3i64jrFwB3JtA==", + "dev": true, + "requires": { + "inversify": "^5.0.0", + "reflect-metadata": "^0.1.12", + "tslib": "^1.8.1" + } + }, + "@material/animation": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-0.40.1.tgz", + "integrity": "sha512-HtxFUw04EHg4S6pXfTA3Z0wKxnNDNcDhe1Np2Y2geo+lAk2Hb7m8yCL/GaL9o2I/eRYsgUXC0U7+Mk74GCz3zw==" + }, + "@material/base": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/base/-/base-0.40.1.tgz", + "integrity": "sha512-vrbOK8hONVCYgURQ9h7nkXvMdYnZVVNmAfFFijF8fbWQdwnoPcNTdqV6RoQlhBEqHYHQqLNfdUDlznAPKLclGQ==" + }, + "@material/button": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/button/-/button-0.40.1.tgz", + "integrity": "sha512-xLNjq9zySnpZAP4UynyeXnnlLXf3iIA/6ilecwgF4d2ooUmNXcRdlRa8wGYT36JHsCfsP3AeZOjoTZUcmaiejw==", + "requires": { + "@material/elevation": "^0.40.1", + "@material/ripple": "^0.40.1", + "@material/rtl": "^0.40.1", + "@material/shape": "^0.40.1", + "@material/theme": "^0.40.1", + "@material/typography": "^0.40.1" + } + }, + "@material/elevation": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-0.40.1.tgz", + "integrity": "sha512-VD9ii90WzI+t4df08A9hQIsYLH/N+85a2Mqo10CNVZLZYW5fDOwFH/h7553aNoAuSHKPcGCLdyav9J9oC6TSaQ==", + "requires": { + "@material/animation": "^0.40.1", + "@material/theme": "^0.40.1" + } + }, + "@material/mwc-base": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@material/mwc-base/-/mwc-base-0.3.5.tgz", + "integrity": "sha512-gkP7K7v4wFB9eeGyJ/7DriVPKWXhVwgRHY8QI2669AslasBbP6tuMrZ83IZRIc1ffK4rakL+sc77NwhOPmafHQ==", + "requires": { + "lit-element": "^2.0.0-rc.2", + "lit-html": "^1.0.0-rc.2" + } + }, + "@material/mwc-button": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@material/mwc-button/-/mwc-button-0.3.5.tgz", + "integrity": "sha512-IhpcK7wNV5KQu0/fJIgZPZyvH8HVSxjUvj4oP8mOU5JUetybKkIdPA77MT+x++4pp8Rh5AXy0+YAf3A5dfF7Eg==", + "requires": { + "@material/button": "^0.40.0", + "@material/mwc-base": "^0.3.5", + "@material/mwc-icon": "^0.3.5", + "@material/mwc-ripple": "^0.3.5", + "lit-element": "^2.0.0-rc.2", + "lit-html": "^1.0.0-rc.2" + } + }, + "@material/mwc-icon": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@material/mwc-icon/-/mwc-icon-0.3.5.tgz", + "integrity": "sha512-UnmhMEWICac1LReYQ6Th9tmovuJBd2nU111Vnc1LLEzmaOs8AJclGTdvSFTVQyfsdR2OuOFBWkTXRTiENXMhEA==", + "requires": { + "@material/mwc-base": "^0.3.5", + "lit-element": "^2.0.0-rc.2" + } + }, + "@material/mwc-ripple": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@material/mwc-ripple/-/mwc-ripple-0.3.5.tgz", + "integrity": "sha512-KE6KfyMbjz1yhvvQ22xh03bH+j1UqbOgOpKr0XrIuvbdUtYjf1gGf+HsonM5x4RVsxfUSPkqsyFC3q2Wu0XMsg==", + "requires": { + "@material/mwc-base": "^0.3.5", + "@material/ripple": "^0.40.0", + "lit-element": "^2.0.0-rc.2", + "lit-html": "^1.0.0-rc.2" + } + }, + "@material/ripple": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-0.40.1.tgz", + "integrity": "sha512-sndeTS4VHa0v1UGj7MNcxMCuO9LJ1DjoL1EjE6BH3Lm3M1MnXJHdsBo2CgPbU/FI84tt6+eyHGOYPdPrEDJhCA==", + "requires": { + "@material/animation": "^0.40.1", + "@material/base": "^0.40.1", + "@material/theme": "^0.40.1" + } + }, + "@material/rtl": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-0.40.1.tgz", + "integrity": "sha512-Pk6Iw1/KrhWZoZtkDsPMDUW0bm7Z1zeXb3MTQRCFmjf1wU5cRxgOTtuoZLcJqlcKGppLAzJL/TJV3E7KEiuL0A==" + }, + "@material/shape": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-0.40.1.tgz", + "integrity": "sha512-o1pw5+s/jWqsKbUAkCCaEcB8XLqJ4FlZhYfSvxZ88WRw9zoWOt9iQMMP82wLWhUX1DSzpNRI8BAD7aNLK6yRlA==" + }, + "@material/theme": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-0.40.1.tgz", + "integrity": "sha512-cH1CsGIDisEQ2oroZhLTypV0Ir00x3WIwFXnPo7qv3832tuIDkZY623U3rUax6KNPz4Hh1j0tNpTwgrNZwvwWA==" + }, + "@material/typography": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-0.40.1.tgz", + "integrity": "sha512-LkW2tAsId8zGKxGA5VIFXV/D1h4vCHQIuALRMaDpHbNGffgr2ubtJNvCh2EQkm19MTv4igVLEjn1Svh0dXcTpA==" + }, + "@messageflow/tslint-config": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@messageflow/tslint-config/-/tslint-config-1.3.0.tgz", + "integrity": "sha512-9XyKVj4IRcuxq2P1TK/AmQQIIdQHoiZ0YH7WITZJBsPvLbXPaHKa78zZtVQdV1O7qNM+xkJ0SE6KM7WgPqFpUg==", + "dev": true, + "requires": { + "tslint-config-airbnb": "^5.10.0", + "tslint-immutable": "^4.6.0" + } + }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.5.tgz", + "integrity": "sha512-lAVp+Kj54ui/vLUFxsJTMtWvZraZxum3w3Nwkble2dNuV5VnPA+Mi2oGX9XYJAaIvZi3tn3cbjS/qcJXRb6Bww==", + "dev": true + }, + "@types/node": { + "version": "10.12.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", + "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==", + "dev": true + }, + "@webcomponents/webcomponentsjs": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@webcomponents/webcomponentsjs/-/webcomponentsjs-2.2.4.tgz", + "integrity": "sha512-YkLxK9Mbw6QK5bNZ67Rarb/yj0gN+Ziy5+2sLjM0lDb3XafM36gXKZXaIBz4zvLA/cYYTdg+l1LlqXeHEcmeiA==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "doctrine": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", + "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", + "dev": true, + "requires": { + "esutils": "^1.1.6", + "isarray": "0.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esutils": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", + "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-caller-file": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.1.tgz", + "integrity": "sha512-SpOZHfz845AH0wJYVuZk2jWDqFmu7Xubsx+ldIpwzy5pDUpu7OJHK7QYNSA2NPlDSKQwM1GFaAkciOWjjW92Sg==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "inversify": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/inversify/-/inversify-5.0.1.tgz", + "integrity": "sha512-Ieh06s48WnEYGcqHepdsJUIJUXpwH5o5vodAX+DK2JA/gjy4EbEcQZxw+uFfzysmKjiLXGYwNG3qDZsKVMcINQ==", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "lit-element": { + "version": "2.0.0-rc.5", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-2.0.0-rc.5.tgz", + "integrity": "sha512-cMmWNWSFyYfXAd09bnqFhqDr5kuR/5guImD5ZRTk223EiBJaoo7naZnQngSYAMjgDn1CSbTE1LRtzviMS+g0RA==", + "requires": { + "lit-html": "^1.0.0-rc.2" + } + }, + "lit-html": { + "version": "1.0.0-rc.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-1.0.0-rc.2.tgz", + "integrity": "sha512-4bq34lhVmwWly1zBXicOBJLOwaWfjOVbchEEmFnZLuztxjh5wRd2WqV0URX8Q47MQ7PaIjn/eXyTRKsYhSAeRw==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "resolve": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.9.0.tgz", + "integrity": "sha512-TZNye00tI67lwYvzxCxHGjwTNlUV70io54/Ed4j6PscB8xVfuBJpRenI/o6dVk0cY0PYTY27AgCoGGxRnYuItQ==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tslint": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", + "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" + } + }, + "tslint-config-airbnb": { + "version": "5.11.1", + "resolved": "https://registry.npmjs.org/tslint-config-airbnb/-/tslint-config-airbnb-5.11.1.tgz", + "integrity": "sha512-hkaittm2607vVMe8eotANGN1CimD5tor7uoY3ypg2VTtEcDB/KGWYbJOz58t8LI4cWSyWtgqYQ5F0HwKxxhlkQ==", + "dev": true, + "requires": { + "tslint-consistent-codestyle": "^1.14.1", + "tslint-eslint-rules": "^5.4.0", + "tslint-microsoft-contrib": "~5.2.1" + } + }, + "tslint-consistent-codestyle": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/tslint-consistent-codestyle/-/tslint-consistent-codestyle-1.15.0.tgz", + "integrity": "sha512-6BNDBbZh2K0ibRXe70Mkl9gfVttxQ3t3hqV1BRDfpIcjrUoOgD946iH4SrXp+IggDgeMs3dJORjD5tqL5j4jXg==", + "dev": true, + "requires": { + "@fimbul/bifrost": "^0.17.0", + "tslib": "^1.7.1", + "tsutils": "^2.29.0" + } + }, + "tslint-eslint-rules": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz", + "integrity": "sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w==", + "dev": true, + "requires": { + "doctrine": "0.7.2", + "tslib": "1.9.0", + "tsutils": "^3.0.0" + }, + "dependencies": { + "tslib": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", + "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", + "dev": true + }, + "tsutils": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.7.0.tgz", + "integrity": "sha512-n+e+3q7Jx2kfZw7tjfI9axEIWBY0sFMOlC+1K70X0SeXpO/UYSB+PN+E9tIJNqViB7oiXQdqD7dNchnvoneZew==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tslint-immutable": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/tslint-immutable/-/tslint-immutable-4.9.1.tgz", + "integrity": "sha512-iIFCq08H4YyNIX0bV5N6fGQtAmjc4OQZKQCgBP5WHgQaITyGAHPVmAw+Yf7qe0zbRCvCDZdrdEC/191fLGFiww==", + "dev": true + }, + "tslint-microsoft-contrib": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/tslint-microsoft-contrib/-/tslint-microsoft-contrib-5.2.1.tgz", + "integrity": "sha512-PDYjvpo0gN9IfMULwKk0KpVOPMhU6cNoT9VwCOLeDl/QS8v8W2yspRpFFuUS7/c5EIH/n8ApMi8TxJAz1tfFUA==", + "dev": true, + "requires": { + "tsutils": "^2.27.2 <2.29.0" + }, + "dependencies": { + "tsutils": { + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.28.0.tgz", + "integrity": "sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", + "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", + "dev": true + }, + "wct-mocha": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wct-mocha/-/wct-mocha-1.0.0.tgz", + "integrity": "sha512-rvDjW4kJMV8/lpihKMDHMZwycT5ALtoLni/Q0Ggdg1rPRpIW5pjoslSR/UIl2gWRMYqAs9nFRVYxASwEYG6brA==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..47bf7ec8 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "app-datepicker", + "version": "4.0.0-rc.4", + "description": "A custom datepicker element based on Google's Material Design built from scratch with lit-element", + "keywords": [ + "cal", + "calendar", + "date", + "date-picker", + "datepicker", + "lit-element", + "lit-html", + "picker", + "web-component", + "web-components" + ], + "homepage": "https://github.com/motss/app-datepicker", + "repository": { + "type": "git", + "url": "git@github.com:motss/app-datepicker.git" + }, + "license": "MIT", + "author": { + "name": "Rong Sen Ng", + "email": "wes.ngrongsen@gmail.com", + "url": "https://github.com/motss" + }, + "files": [ + "dist" + ], + "main": "dist/app-datepicker.js", + "module": "dist/app-datepicker.js", + "typings": "dist/app-datepicker.d.ts", + "scripts": { + "build": "npm run ts -- -p tsconfig.prod.json", + "document": "polymer analyze > analysis.json", + "lint": "tslint --project tsconfig.json --config tslint.prod.json --format stylish 'src/**/*.ts*'", + "lint:debug": "tslint --project tsconfig.json --config tslint.json --format stylish 'src/**/*.ts*'", + "prepublishOnly": "npm run lint && npm run build", + "serve": "polymer serve . --port 4343 --npm --module-resolution=node", + "test": "polymer test --skip-selenium-install", + "test:ci": "polymer test --config-file ./wct.config.ci.json", + "testkeep": "polymer test -p --expanded --module-resolution=node --npm", + "ts": "rm -rf dist/ && tsc", + "watch": "npm run ts -- --watch" + }, + "dependencies": { + "@material/mwc-button": "^0.3.5", + "lit-element": "^2.0.0-rc.5", + "lit-html": "^1.0.0-rc.2" + }, + "devDependencies": { + "@messageflow/tslint-config": "^1.3.0", + "@types/chai": "^4.1.7", + "@types/mocha": "^5.2.5", + "@types/node": "^10.12.18", + "@webcomponents/webcomponentsjs": "^2.2.4", + "chai": "^4.2.0", + "mocha": "^5.2.0", + "tslint": "^5.12.1", + "typescript": "^3.2.4", + "wct-mocha": "^1.0.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 6.4.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/paper-dialog-theme.html b/paper-dialog-theme.html deleted file mode 100644 index efc2a511..00000000 --- a/paper-dialog-theme.html +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/src/app-datepicker-dialog.ts b/src/app-datepicker-dialog.ts new file mode 100644 index 00000000..8d63a251 --- /dev/null +++ b/src/app-datepicker-dialog.ts @@ -0,0 +1,316 @@ +import { AppDatepicker } from './app-datepicker.js'; +import { FocusTrap, KEYCODES_MAP } from './datepicker-helpers.js'; + +import { css, customElement, html, LitElement, property, query } from 'lit-element'; + +import '@material/mwc-button/mwc-button.js'; + +import './app-datepicker.js'; +import { datepickerVariables } from './common-styles.js'; +import { + dispatchCustomEvent, + getResolvedLocale, + setFocusTrap, +} from './datepicker-helpers.js'; + +@customElement(AppDatepickerDialog.is) +export class AppDatepickerDialog extends LitElement { + static get is() { + return 'app-datepicker-dialog'; + } + + @property({ type: String }) + public min: string; + + @property({ type: String }) + public max: string = '2100-12-31'; + + @property({ type: Number }) + public firstDayOfWeek: number = 0; + + @property({ type: Boolean }) + public showWeekNumber: boolean = false; + + @property({ type: String }) + public weekNumberType: string = 'first-4-day-week'; + + @property({ type: String }) + public disabledDays: string = '0,6'; + + @property({ type: String }) + public disableDates: string; + + @property({ type: String }) + public format: string = 'yyyy-MM-dd'; + + @property({ type: Boolean }) + public landscape: boolean = false; + + @property({ type: String }) + public locale: string = getResolvedLocale(); + + @property({ type: Number }) + public dragRatio: number = .15; + + @property({ type: String }) + public startView: string = 'calendar'; + + @property({ type: String }) + public value: string; + + @property({ type: String }) + public dismissLabel: string = 'cancel'; + + @property({ type: String }) + public confirmLabel: string = 'ok'; + + @property({ type: Boolean }) + public noFocusTrap: boolean = false; + + @query('.scrim') + private _scrim: HTMLDivElement; + + @query('.content-container') + private _contentContainer: HTMLDivElement; + + @query('.datepicker') + private _datepicker: AppDatepicker; + + @query('mwc-button[dialog-confirm]') + private _dialogConfirm: HTMLElement; + + private _focusable: HTMLElement; + private _focusTrap: FocusTrap; + + public constructor() { + super(); + + this.setAttribute('role', 'dialog'); + this.setAttribute('aria-label', 'datepicker'); + this.setAttribute('aria-modal', 'true'); + } + + public open() { + const scrim = this._scrim; + const contentContainer = this._contentContainer; + + this.removeAttribute('aria-hidden'); + scrim.style.visibility = 'visible'; + contentContainer.style.visibility = 'visible'; + + const keyframes: Keyframe[] = [ + { opacity: '0' }, + { opacity: '1' }, + ]; + const opts: KeyframeAnimationOptions = { + duration: 100, + }; + const fadeInAnimation = contentContainer.animate(keyframes, opts); + + new Promise(yay => (fadeInAnimation.onfinish = yay)).then(() => { + contentContainer.style.opacity = '1'; + + const focusable = this._focusable; + + if (!this.noFocusTrap) { + this._focusTrap = setFocusTrap(this, [ + focusable, + this._dialogConfirm, + ])!; + } + + focusable.focus(); + dispatchCustomEvent(this, 'datepicker-dialog-opened', { opened: true, value: this.value }); + }); + } + + public close() { + const scrim = this._scrim; + const contentContainer = this._contentContainer; + + scrim.style.visibility = ''; + + const keyframes: Keyframe[] = [ + { opacity: '1' }, + { opacity: '0' }, + ]; + const opts: KeyframeAnimationOptions = { + duration: 100, + }; + const fadeOutAnimation = contentContainer.animate(keyframes, opts); + + new Promise(yay => (fadeOutAnimation.onfinish = yay)).then(() => { + contentContainer.style.opacity = ''; + contentContainer.style.visibility = ''; + + this.setAttribute('aria-hidden', 'true'); + if (!this.noFocusTrap) this._focusTrap.disconnect(); + dispatchCustomEvent(this, 'datepicker-dialog-closed', { opened: false, value: this.value }); + }); + } + + protected firstUpdated() { + this._updateValue(); + this.addEventListener('keyup', (ev: KeyboardEvent) => { + if (ev.keyCode === KEYCODES_MAP.ESCAPE) { + this.close(); + } + }); + + dispatchCustomEvent(this, 'datepicker-dialog-first-updated', { value: this.value }); + } + + static get styles() { + return [ + datepickerVariables, + css` + :host { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + z-index: var(--app-datepicker-dialog-z-index, 24); + -webkit-tap-highlight-color: rgba(0,0,0,0); + } + :host([opened]) > .scrim, + :host([opened]) > .content-container { + visibility: visible; + opacity: 1; + } + + .scrim, + .content-container { + pointer-events: auto; + } + + .scrim { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, .25); + visibility: hidden; + z-index: 22; + } + + .content-container { + position: absolute; + top: 50%; + left: 50%; + max-width: 100%; + max-height: 100%; + background-color: #fff; + transform: translate3d(-50%, -50%, 0); + border-radius: var(--app-datepicker-border-radius); + will-change: transform, opacity; + overflow: hidden; + visibility: hidden; + opacity: 0; + z-index: 23; + } + + .datepicker { + --app-datepicker-border-bottom-left-radius: 0; + --app-datepicker-border-bottom-right-radius: 0; + } + + .actions-container { + display: flex; + align-items: center; + justify-content: flex-end; + + margin: 0; + padding: 12px; + background-color: inherit; + color: var(--app-datepicker-primary-color); + } + + mwc-button + mwc-button { + margin: 0 0 0 8px; + } + + /** + * NOTE: IE11-only fix via CSS hack. + * + * Visit https://bit.ly/2DEUNZu|CSS for more relevant browsers' hacks. + */ + @media screen and (-ms-high-contrast: none) { + mwc-button[dialog-dismiss] { + min-width: 10ch; + } + } + `, + ]; + } + + protected render() { + const min = this.min; + const max = this.max; + const firstDayOfWeek = this.firstDayOfWeek; + const showWeekNumber = this.showWeekNumber; + const weekNumberType = this.weekNumberType; + const disabledDays = this.disabledDays; + const disabledDates = this.disableDates; + const format = this.format; + const landscape = this.landscape; + const locale = this.locale; + const dragRatio = this.dragRatio; + const startView = this.startView; + const value = this.value; + const dismissLabel = this.dismissLabel; + const confirmLabel = this.confirmLabel; + + //
+ return html` +
+ +
+ + +
+ ${dismissLabel} + ${confirmLabel} +
+
+ `; + } + + private _update() { + this._updateValue(); + this.close(); + } + + private _updateValue() { + const datepicker = this._datepicker; + this.value = datepicker.value; + } + + private _setFocusable(ev: CustomEvent) { + const { firstFocusableElement } = ev.detail; + this._focusable = firstFocusableElement; + } + +} diff --git a/src/app-datepicker-icons.ts b/src/app-datepicker-icons.ts new file mode 100644 index 00000000..e5267be8 --- /dev/null +++ b/src/app-datepicker-icons.ts @@ -0,0 +1,6 @@ +import { html } from 'lit-element'; + +// tslint:disable:max-line-length +export const iconChevronLeft = html``; +export const iconChevronRight = html``; +// tslint:enable:max-line-length diff --git a/src/app-datepicker.ts b/src/app-datepicker.ts new file mode 100644 index 00000000..724fc096 --- /dev/null +++ b/src/app-datepicker.ts @@ -0,0 +1,1283 @@ +type DateTimeFormatter = (date?: number | Date | undefined) => string; +interface Formatters { + dayFormatter: DateTimeFormatter; + fullDateFormatter: DateTimeFormatter; + longWeekdayFormatter: DateTimeFormatter; + narrowWeekdayFormatter: DateTimeFormatter; + longMonthYearFormatter: DateTimeFormatter; + dateFormatter: DateTimeFormatter; + yearFormatter: DateTimeFormatter; + + locale: string; +} +export const enum START_VIEW { + CALENDAR = 'calendar', + YEAR_LIST = 'yearList', +} +export const enum MONTH_UPDATE_TYPE { + PREVIOUS = 'previous', + NEXT = 'next', +} + +import { + css, + customElement, + html, + LitElement, + property, + query, +} from 'lit-element'; + +import { cache } from 'lit-html/directives/cache.js'; +import { classMap } from 'lit-html/directives/class-map.js'; + +import { iconChevronLeft, iconChevronRight } from './app-datepicker-icons.js'; +import { calendarDays, calendarWeekdays, WEEK_NUMBER_TYPE } from './calendar.js'; +import { datepickerVariables, resetButton } from './common-styles.js'; +import { + arrayFilled, + computeNewFocusedDateWithKeyboard, + computeThreeCalendarsInARow, + dispatchCustomEvent, + findShadowTarget, + getResolvedDate, + getResolvedLocale, + isValidDate, + KEYCODES_MAP, + targetScrollTo, + toFormattedDateString, + stripLTRMark, +} from './datepicker-helpers.js'; +import { Tracker } from './tracker.js'; + +function updateFormatters(locale: string): Formatters { + const dayFormatter = Intl.DateTimeFormat(locale, { day: 'numeric', timeZone: 'UTC' }).format; + const fullDateFormatter = Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format; + const longWeekdayFormatter = Intl.DateTimeFormat(locale, { + weekday: 'long', + timeZone: 'UTC', + }).format; + const narrowWeekdayFormatter = Intl.DateTimeFormat(locale, { + weekday: 'narrow', + timeZone: 'UTC', + }).format; + const longMonthYearFormatter = Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'long', + timeZone: 'UTC', + }).format; + const dateFormatter = Intl.DateTimeFormat(locale, { + weekday: 'short', + month: 'short', + day: 'numeric', + timeZone: 'UTC', + }).format; + const yearFormatter = Intl.DateTimeFormat(locale, { + year: 'numeric', + timeZone: 'UTC', + }).format; + + return { + dayFormatter, + fullDateFormatter, + longMonthYearFormatter, + longWeekdayFormatter, + narrowWeekdayFormatter, + dateFormatter, + yearFormatter, + + locale, + }; +} + +function renderHeaderSelectorButton({ + selectedDate, + focusedDate, + startView, + + updateViewFn, + dateFormatterFn, +}) { + const formattedDate = stripLTRMark(dateFormatterFn(focusedDate)); + const isCalendarView = startView === START_VIEW.CALENDAR; + + return html` + + +
+ +
+ `; +} + +function renderDatepickerYearList({ + yearList, + selectedDate, + + updateYearFn, + yearFormatterFn, +}) { + return html` +
+
${ + yearList.map(n => + html``) + }
+
+ `; +} + +function renderDatepickerCalendar({ + disabledDays, + firstDayOfWeek, + focusedDate, + max, + min, + selectedDate, + showWeekNumber, + todayDate, + weekNumberType, + + updateFocusedDateFn, + updateMonthFn, + updateMonthWithKeyboardFn, + + dayFormatterFn, + fullDateFormatterFn, + longWeekdayFormatterFn, + narrowWeekdayFormatterFn, + longMonthYearFormatterFn, +}) { + let clt = window.performance.now(); + const minTime = +min; + const maxTime = +max; + const weekdays = calendarWeekdays({ + firstDayOfWeek, + showWeekNumber, + + longWeekdayFormatter: longWeekdayFormatterFn, + narrowWeekdayFormatter: narrowWeekdayFormatterFn, + }); + const calendars = computeThreeCalendarsInARow(selectedDate).map((n, idx) => { + const nFy = n.getUTCFullYear(); + const nM = n.getUTCMonth(); + const firstDayOfMonthTime = +new Date(Date.UTC(nFy, nM, 1)); + const lastDayOfMonthTime = +new Date(Date.UTC(nFy, nM + 1, 0)); + + /** + * NOTE: Return `null` when one of the followings fulfills:- + * + * minTime maxTime + * |--------|--------o--------|--------| + * last day | valid dates | first day + * + * - last day of the month < `minTime` - entire month should be disabled + * - first day of the month > `maxTime` - entire month should be disabled + */ + if (lastDayOfMonthTime < minTime || firstDayOfMonthTime > maxTime) { + return null; + } + + return calendarDays({ + firstDayOfWeek, + showWeekNumber, + weekNumberType, + selectedDate: n, + idOffset: idx * 10, + + dayFormatter: dayFormatterFn, + fullDateFormatter: fullDateFormatterFn, + }); + }); + clt = window.performance.now() - clt; + const cltEl = document.body.querySelector('.calendar-render-time'); + if (cltEl) { + cltEl.textContent = `Rendering calendar takes ${clt < 1 ? '< 1' : clt.toFixed(2)} ms`; + } + + let hasMinDate = false; + let hasMaxDate = false; + + const fixedDisabledDays = Array.isArray(disabledDays) && disabledDays.length > 0 + ? disabledDays.map(n => showWeekNumber ? n + 1 : n) + : []; + const weekdaysContent = weekdays.map((o: any) => { + return html`${o.value}`; + }); + const calendarsContent = calendars.map((daysInMonth, i) => { + if (daysInMonth == null) { + /** NOTE: If first and last month is of type null, set the corresponding flag. */ + if (i === 0) hasMinDate = true; + if (i === 2) hasMaxDate = true; + + return html`
`; + } + + let formattedDate: string | null = null; + + const tbodyContent = daysInMonth.map((n) => { + const trContent = n.map((o: any, oi) => { + if (o.fullDate == null && o.value == null) { + return html``; + } + + /** NOTE(motss): Could be week number labeling */ + if (o.fullDate == null && showWeekNumber && oi < 1) { + return html` + ${o.value} + `; + } + + const curTime = +new Date(o.fullDate); + if (formattedDate == null) formattedDate = longMonthYearFormatterFn(curTime); + + const isDisabledDay = fixedDisabledDays.some(fdd => fdd === oi) + || (curTime < minTime || curTime > maxTime); + + return html` + +
${o.value}
+ + `; + }); + + return html`${trContent}`; + }); + + return html` +
+
${formattedDate}
+ + + + ${weekdaysContent} + + + ${tbodyContent} +
+
+ `; + }); + /** + * FIXME(motss): For unknown reason, this has to be moved out of the `html` tagged literal. On + * IE11, this particular element is not parsed by `ShadyCSS` thus losing the original CSS styling + * when it first gets rendered. This workaround resolves the issue temporarily but still good to + * dig into this further to find out the root cause and report it to the Polymer Team. + */ + const calendarViewFullCalendarContent = html` +
${calendarsContent}
`; + + /** + * FIXME(motss): Allow users to customize the aria-label for accessibility and i18n reason. + */ + return html` +
+
+
+ ${hasMinDate + ? null + : html` + + `} +
+ +
+ ${hasMaxDate + ? null + : html` + + `} +
+
+ + ${calendarViewFullCalendarContent} +
+ `; +} + +@customElement(AppDatepicker.is) +export class AppDatepicker extends LitElement { + static get is() { + return 'app-datepicker'; + } + + @property({ type: String, reflect: true }) + public get min() { + return toFormattedDateString(this._min); + } + public set min(val: string) { + const valDate = getResolvedDate(val); + + if (isValidDate(val, valDate)) { + const oldVal = this._min; + + this._min = valDate; + this.requestUpdate('min', oldVal); + } + } + + @property({ type: String, reflect: true }) + public get max() { + return toFormattedDateString(this._max); + } + // public max: string = '2100-12-31'; + public set max(val: string) { + const valDate = getResolvedDate(val); + + if (isValidDate(val, valDate)) { + const oldVal = this._max; + + this._max = valDate; + this.requestUpdate('max', oldVal); + } + } + + @property({ type: String, reflect: true }) + public get value() { + return toFormattedDateString(this._focusedDate); + } + public set value(val: string) { + /** NOTE: Converts all datetime to UTC */ + const minDate = getResolvedDate(this._min); + const maxDate = getResolvedDate(this.max); + const valDate = getResolvedDate(val); + + if (isValidDate(val, valDate)) { + if (+valDate < +minDate || +valDate > +maxDate) return; + + const oldVal = this.value; + this._focusedDate = valDate; + this._selectedDate = valDate; + // this.valueAsDate = newDate; + // this.valueAsNumber = +newDate; + + this.requestUpdate('value', oldVal); + } + } + + @property({ type: String, reflect: true }) + public get startView() { + return this._startView; + } + public set startView(val: string) { + /** + * NOTE: Defaults to `START_VIEW.CALENDAR` when `val` is falsy. + */ + const defaultVal = !val ? START_VIEW.CALENDAR : val; + + /** + * NOTE: No-op when `val` is not falsy and not valid accepted values. + */ + if (defaultVal !== START_VIEW.CALENDAR && defaultVal !== START_VIEW.YEAR_LIST) return; + + const oldVal = this._startView; + + this._startView = defaultVal; + this.requestUpdate('startView', oldVal); + } + + @property({ type: Number, reflect: true }) + public firstDayOfWeek: number = 0; + + @property({ type: Boolean, reflect: true }) + public showWeekNumber: boolean = false; + + @property({ type: String, reflect: true }) + public weekNumberType: string = WEEK_NUMBER_TYPE.FIRST_4_DAY_WEEK; + + @property({ type: Boolean, reflect: true }) + public landscape: boolean = false; + + @property({ type: String }) + public locale: string = getResolvedLocale(); + + @property({ type: Number }) + public dragRatio: number = .15; + + @property({ type: String }) + public disabledDays: string = '0,6'; + + @property({ type: String }) + public disableDates: string; + + // @property({ type: String }) + // public format: string = 'yyyy-MM-dd'; + + @property({ type: Date }) + private _selectedDate: Date; + + @property({ type: Date }) + private _focusedDate: Date; + + @query('.year-list-view__full-list') + private _yearViewFullList: HTMLDivElement; + + @query('.datepicker-body__calendar-view') + private _datepickerBodyCalendarView: HTMLDivElement; + + @query('.calendar-view__full-calendar') + private _calendarViewFullCalendar: HTMLDivElement; + + @query('.btn__selector-year') + private _buttonSelectorYear: HTMLButtonElement; + + @query('.year-list-view__list-item') + private _yearViewListItem: HTMLButtonElement; + + private _min: Date; + private _max: Date; + private _startView: string; + private _todayDate: Date; + private _totalDraggableDistance: number; + private _dragAnimationDuration: number = 150; + private _yearList: number[]; + private _hasCalendarSetup: boolean = false; + private _hasNativeElementAnimate: boolean = + Element.prototype.animate.toString().indexOf('[native code]') >= 0; + private _formatters: Formatters; + + // weekdayFormat: String, + + static get styles() { + // tslint:disable:max-line-length + return [ + datepickerVariables, + resetButton, + css` + :host { + min-width: 300px; + width: 300px; + /** NOTE: Magic number as 16:9 aspect ratio does not look good */ + /* height: calc((var(--app-datepicker-width) / .66) - var(--app-datepicker-footer-height, 56px)); */ + background-color: #fff; + border-top-left-radius: var(--app-datepicker-border-top-left-radius, var(--app-datepicker-border-radius)); + border-top-right-radius: var(--app-datepicker-border-top-right-radius, var(--app-datepicker-border-radius)); + border-bottom-left-radius: var(--app-datepicker-border-bottom-left-radius, var(--app-datepicker-border-radius)); + border-bottom-right-radius: var(--app-datepicker-border-bottom-right-radius, var(--app-datepicker-border-radius)); + overflow: hidden; + } + :host([landscape]) { + display: flex; + + /** - */ + min-width: calc(568px - 16px * 2); + width: calc(568px - 16px * 2); + } + + .datepicker-header + .datepicker-body { + border-top: 1px solid #ddd; + } + :host([landscape]) > .datepicker-header + .datepicker-body { + border-top: none; + border-left: 1px solid #ddd; + } + + .datepicker-header { + display: flex; + flex-direction: column; + align-items: flex-start; + + position: relative; + padding: 16px 24px; + } + :host([landscape]) > .datepicker-header { + /** :this. + :this. */ + min-width: calc(14ch + 24px * 2); + } + + .btn__selector-year, + .btn__selector-calendar { + color: rgba(0, 0, 0, .55); + cursor: pointer; + /* outline: none; */ + } + .btn__selector-year.selected, + .btn__selector-calendar.selected { + color: currentColor; + } + + /** + * NOTE: IE11-only fix. This prevents formatted focused date from overflowing the container. + */ + .datepicker-toolbar { + width: 100%; + } + + .btn__selector-year { + font-size: 16px; + font-weight: 700; + } + .btn__selector-calendar { + font-size: 36px; + font-weight: 700; + line-height: 1; + } + + .datepicker-body { + position: relative; + width: 100%; + overflow: hidden; + } + + .datepicker-body__calendar-view { + min-height: 56px; + } + + .calendar-view__month-selector { + display: flex; + align-items: center; + + position: absolute; + top: 0; + left: 0; + width: 100%; + padding: 0 8px; + z-index: 1; + } + + .month-selector-container { + max-height: 56px; + height: 100%; + } + .month-selector-container + .month-selector-container { + margin: 0 0 0 auto; + } + /* .month-selector-container > paper-icon-button-light { + max-width: 56px; + max-height: 56px; + height: 56px; + width: 56px; + color: rgba(0, 0, 0, .25); + } */ + + .month-selector-button { + padding: calc((56px - 24px) / 2); + /** + * NOTE: button element contains no text, only SVG. + * No extra height will incur with such setting. + */ + line-height: 0; + } + .month-selector-button:hover { + cursor: pointer; + } + + .calendar-view__full-calendar { + display: flex; + justify-content: center; + + position: relative; + width: calc(100% * 3); + padding: 0 0 16px; + will-change: transform; + /** + * NOTE: Required for Pointer Events API to work on touch devices. + * Native \`pan-y\` action will be fired by the browsers since we only care about the + * horizontal direction. This is great as vertical scrolling still works even when touch + * event happens on a datepicker's calendar. + */ + touch-action: pan-y; + /* outline: none; */ + } + + .year-list-view__full-list { + max-height: calc(48px * 7); + overflow-y: auto; + } + + .calendar-weekdays > th, + td.weekday-label { + color: rgba(0, 0, 0, .55); + font-weight: 400; + } + + .calendar-container { + max-width: calc(100% / 3); + width: calc(100% / 3); + } + + .calendar-label, + .calendar-table { + width: calc(100% - 16px * 2); + } + + .calendar-label { + display: flex; + align-items: center; + justify-content: center; + + max-width: calc(100% - 8px * 2); + width: 100%; + height: 56px; + margin: 0 8px; + font-weight: 500; + text-align: center; + } + + .calendar-table { + -moz-user-select: none; + -webkit-user-select: none; + user-select: none; + + margin: 0 16px; + border-collapse: collapse; + text-align: center; + } + + tr > th, + tr > td { + position: relative; + min-height: 40px; + height: 40px; + padding: 8px 0; + } + + /** + * NOTE: Interesting fact! That is ::after will trigger paint when dragging. This will trigger + * layout and paint on **ONLY** affected nodes. This is much cheaper as compared to rendering + * all :::after of all calendar day elements. When dragging the entire calendar container, + * because of all layout and paint trigger on each and every ::after, this becomes a expensive + * task for the browsers especially on low-end devices. Even though animating opacity is much + * cheaper, the technique does not work here. Adding 'will-change' will further reduce overall + * painting at the expense of memory consumption as many cells in a table has been promoted + * a its own layer. + */ + tr > td.full-calendar__day:not(.day--empty):not(.day--disabled):not(.weekday-label) { + will-change: transform; + } + tr > td.full-calendar__day:not(.day--empty):not(.day--disabled):not(.weekday-label).day--focused::after, + tr > td.full-calendar__day:not(.day--empty):not(.day--disabled):not(.day--focused):not(.weekday-label):hover::after { + content: ''; + display: block; + position: absolute; + width: 40px; + height: 40px; + top: 50%; + left: 50%; + background-color: var(--app-datepicker-primary-color); + border-radius: 50%; + transform: translate3d(-50%, -50%, 0); + will-change: transform; + opacity: 0; + pointer-events: none; + } + tr > td.full-calendar__day:not(.day--empty):not(.day--disabled):not(.weekday-label).day--focused::after { + opacity: 1; + } + tr > td.full-calendar__day:not(.day--empty):not(.day--disabled):not(.day--focused):not(.weekday-label):hover::after { + opacity: .15; + } + tr > td.full-calendar__day:not(.day--empty):not(.day--disabled):not(.weekday-label) { + cursor: pointer; + pointer-events: auto; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + } + tr > td.full-calendar__day.day--focused:not(.day--empty):not(.day--disabled):not(.weekday-label)::after, + tr > td.full-calendar__day.day--today.day--focused:not(.day--empty):not(.day--disabled):not(.weekday-label)::after { + opacity: 1; + } + + tr > td.full-calendar__day > .calendar-day { + position: relative; + color: currentColor; + z-index: 1; + pointer-events: none; + } + tr > td.full-calendar__day.day--today { + color: var(--app-datepicker-primary-color); + } + tr > td.full-calendar__day.day--focused, + tr > td.full-calendar__day.day--today.day--focused { + color: #fff; + } + tr > td.full-calendar__day.day--empty, + tr > td.full-calendar__day.weekday-label, + tr > td.full-calendar__day.day--disabled > .calendar-day { + pointer-events: none; + } + tr > td.full-calendar__day.day--disabled, + tr > td.full-calendar__day.day--today.day--focused.day--disabled { + color: rgba(0, 0, 0, .35); + } + + .year-list-view__list-item { + position: relative; + width: 100%; + padding: 12px 16px; + text-align: center; + /** NOTE: Reduce paint when hovering and scrolling, but this increases memory usage */ + /* will-change: opacity; */ + /* outline: none; */ + } + .year-list-view__list-item:hover { + cursor: pointer; + } + .year-list-view__list-item > div { + z-index: 1; + } + .year-list-view__list-item::after { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #000; + opacity: 0; + pointer-events: none; + } + .year-list-view__list-item:focus::after, + .year-list-view__list-item:hover::after { + opacity: .05; + } + .year-list-view__list-item.year--selected { + color: var(--app-datepicker-primary-color); + font-size: 24px; + font-weight: 500; + } + `, + ]; + // tslint:enable:max-line-length + } + + public constructor() { + super(); + + this._updateMonthFn = this._updateMonthFn.bind(this); + this._updateViewFn = this._updateViewFn.bind(this); + this._updateYearFn = this._updateYearFn.bind(this); + this._updateFocusedDateFn = this._updateFocusedDateFn.bind(this); + this._trackingStartFn = this._trackingStartFn.bind(this); + this._trackingMoveFn = this._trackingMoveFn.bind(this); + this._trackingEndFn = this._trackingEndFn.bind(this); + this._updateMonthWithKeyboardFn = this._updateMonthWithKeyboardFn.bind(this); + + const todayDate = getResolvedDate(); + const todayDateFullYear = todayDate.getUTCFullYear(); + const yearList = + arrayFilled(2100 - todayDateFullYear + 1).map((_, i) => todayDateFullYear + i); + // const yearList = + // Array.from(Array(2100 - todayDateFullYear + 1), (_, i) => todayDateFullYear + i); + const allFormatters = updateFormatters(this.locale); + const formattedTodayDate = toFormattedDateString(todayDate); + + this.min = formattedTodayDate; + this.value = formattedTodayDate; + + this._startView = START_VIEW.CALENDAR; + this._yearList = yearList; + this._todayDate = todayDate; + this._selectedDate = todayDate; + this._focusedDate = todayDate; + this._formatters = allFormatters; + } + + protected render() { + const locale = this.locale; + const disabledDays = this.disabledDays; + const firstDayOfWeek = this.firstDayOfWeek; + const min = this._min; + const max = this._max; + const showWeekNumber = this.showWeekNumber; + const weekNumberType = this.weekNumberType; + const yearList = this._yearList; + const formatters = this._formatters; + + const focusedDate = new Date(this._focusedDate); + const selectedDate = new Date(this._selectedDate); + const startView = this._startView; + const todayDate = getResolvedDate(); + const didLocaleChange = formatters.locale !== locale; + const allFormatters = didLocaleChange ? updateFormatters(locale) : formatters; + + /** + * NOTE: Update `_formatters` when `locale` changes. + */ + if (didLocaleChange) this._formatters = allFormatters; + + /** + * NOTE(motss): For perf reason, initialize all formatters for calendar rendering + */ + const datepickerBodyContent = + startView === START_VIEW.CALENDAR + ? renderDatepickerCalendar({ + disabledDays, + firstDayOfWeek, + focusedDate, + max, + min, + selectedDate, + showWeekNumber, + todayDate, + weekNumberType, + + updateFocusedDateFn: this._updateFocusedDateFn, + updateMonthFn: this._updateMonthFn, + updateMonthWithKeyboardFn: this._updateMonthWithKeyboardFn, + + dayFormatterFn: allFormatters.dayFormatter, + fullDateFormatterFn: allFormatters.fullDateFormatter, + longMonthYearFormatterFn: allFormatters.longMonthYearFormatter, + longWeekdayFormatterFn: allFormatters.longWeekdayFormatter, + narrowWeekdayFormatterFn: allFormatters.narrowWeekdayFormatter, + }) + : renderDatepickerYearList({ + selectedDate, + yearList, + + updateYearFn: this._updateYearFn, + yearFormatterFn: allFormatters.yearFormatter, + }); + + // tslint:disable:max-line-length + return html` +
${ + renderHeaderSelectorButton({ + selectedDate, + focusedDate, + startView, + + updateViewFn: this._updateViewFn, + dateFormatterFn: allFormatters.dateFormatter, + }) + }
+ +
${cache(datepickerBodyContent)}
+ `; + // tslint:enable:max-line-length + } + + protected firstUpdated() { + const firstFocusableElement = + this._startView === START_VIEW.CALENDAR + ? this._buttonSelectorYear + : this._yearViewListItem; + + dispatchCustomEvent(this, 'datepicker-first-updated', { firstFocusableElement }); + } + + protected updated(changed) { + const startView = this._startView; + + if (startView === START_VIEW.YEAR_LIST) { + const selectedYearScrollTop = + (this._selectedDate.getUTCFullYear() - this._todayDate.getUTCFullYear() - 2) * 48; + + targetScrollTo(this._yearViewFullList, { top: selectedYearScrollTop, left: 0 }); + return; + } + + const shouldTriggerCalendarLayout = changed.has('landscape') || !this._hasCalendarSetup; + + if (startView === START_VIEW.CALENDAR && shouldTriggerCalendarLayout) { + const dragEl = this._calendarViewFullCalendar; + const totalDraggableDistance = this._datepickerBodyCalendarView.getBoundingClientRect().width; + let started = false; + let dx = 0; + let abortDragIfHasMinDate = false; + let abortDragIfHasMaxDate = false; + + const handlers = { + down: () => { + if (started) return; + this._trackingStartFn(); + started = true; + }, + move: (changedPointer, oldPointer) => { + if (!started) return; + dx += changedPointer.x - oldPointer.x; + abortDragIfHasMinDate = dx > 0 && dragEl.classList.contains('has-min-date'); + abortDragIfHasMaxDate = dx < 0 && dragEl.classList.contains('has-max-date'); + + if (abortDragIfHasMaxDate || abortDragIfHasMinDate) return; + + this._trackingMoveFn(dx); + }, + up: (changedPointer, oldPointer) => { + if (!started) return; + if (abortDragIfHasMaxDate || abortDragIfHasMinDate) { + abortDragIfHasMaxDate = false; + abortDragIfHasMinDate = false; + dx = 0; + return; + } + + dx += changedPointer.x - oldPointer.x; + this._trackingEndFn(dx); + dx = 0; + started = false; + }, + }; + // tslint:disable-next-line:no-unused-expression + new Tracker(dragEl, handlers); + + dragEl.style.transform = `translate3d(${totalDraggableDistance * -1}px, 0, 0)`; + this._totalDraggableDistance = totalDraggableDistance; + this._hasCalendarSetup = true; + } + } + + private _updateViewFn(view: string) { + const oldView = this._startView; + + this._startView = view; + this.requestUpdate('_startView', oldView); + } + + private _updateMonthFn(updateType: string) { + const calendarViewFullCalendar = this._calendarViewFullCalendar; + const totalDraggableDistance = this._totalDraggableDistance; + const dateDate = this._selectedDate; + const fy = dateDate.getUTCFullYear(); + const m = dateDate.getUTCMonth(); + const isPreviousMonth = updateType === MONTH_UPDATE_TYPE.PREVIOUS; + const initialX = totalDraggableDistance * -1; + const newDx = totalDraggableDistance * (isPreviousMonth ? 0 : -2); + + const dragAnimation = calendarViewFullCalendar.animate([ + { transform: `translate3d(${initialX}px, 0, 0)` }, + { transform: `translate3d(${newDx}px, 0, 0)` }, + ], { + duration: this._dragAnimationDuration, + easing: 'cubic-bezier(0, 0, .4, 1)', + fill: this._hasNativeElementAnimate ? 'none' : 'both', + }); + + return new Promise(yay => (dragAnimation.onfinish = yay)) + .then(() => new Promise(yay => window.requestAnimationFrame(yay))) + .then(() => { + const newM = m + (isPreviousMonth ? -1 : 1); + this._selectedDate = new Date(Date.UTC(fy, newM, 1)); + + return this.updateComplete; + }) + .then(() => { + calendarViewFullCalendar.style.transform = `translate3d(${initialX}px, 0, 0)`; + }); + } + + private _updateYearFn(ev: CustomEvent) { + const selectedYearEl = + findShadowTarget(ev, n => n.classList.contains('year-list-view__list-item')); + + if (selectedYearEl == null) return; + + const dateDate = this._selectedDate; + const m = dateDate.getUTCMonth(); + const d = dateDate.getUTCDate(); + /** FIXME(motss): the content might not always be a number for other locale */ + const selectedYear = +((selectedYearEl as HTMLButtonElement).textContent!); + + /** + * 2 things to do here: + * - Update `_selectedDate` with selected year + * - Update `_startView` to `START_VIEW.CALENDAR` + */ + this._selectedDate = new Date(Date.UTC(selectedYear, m, d)); + this._startView = START_VIEW.CALENDAR; + } + + private _updateFocusedDateFn(ev: CustomEvent) { + const selectedDayEl = findShadowTarget(ev, n => n.classList.contains('full-calendar__day')); + + /** NOTE: Required condition check else these will trigger unwanted re-rendering */ + if (selectedDayEl == null + || (selectedDayEl as HTMLTableDataCellElement).classList.contains('day--empty') + || (selectedDayEl as HTMLTableDataCellElement).classList.contains('day--disabled') + || (selectedDayEl as HTMLTableDataCellElement).classList.contains('day--focused') + || (selectedDayEl as HTMLTableDataCellElement).classList.contains('weekday-label')) return; + + const dateDate = new Date(this._selectedDate); + const fy = dateDate.getUTCFullYear(); + const m = dateDate.getUTCMonth(); + /** FIXME(motss): the content might not always be a number for other locale */ + const selectedDate = +((selectedDayEl as HTMLTableDataCellElement).textContent!); + + this._focusedDate = new Date(Date.UTC(fy, m, selectedDate)); + } + + private _trackingStartFn() { + const trackableEl = this._calendarViewFullCalendar; + const trackableElWidth = trackableEl.getBoundingClientRect().width; + const totalDraggableDistance = trackableElWidth / 3; + + /** + * NOTE(motss): Perf tips - By setting fixed width for the following containers, + * it drastically minimizes layout and painting during tracking even on slow + * devices:- + * + * - `.calendar-view__full-calender` + * - `.datepicker-body__calendar-view` + */ + trackableEl.style.width = `${trackableElWidth}px`; + trackableEl.style.minWidth = `${trackableElWidth}px`; + this._totalDraggableDistance = totalDraggableDistance; + } + private _trackingMoveFn(dx: number) { + const totalDraggableDistance = this._totalDraggableDistance; + const clamped = Math.min(totalDraggableDistance, Math.abs(dx)); + const isPositive = dx > 0; + const newX = totalDraggableDistance * -1 + (clamped * (isPositive ? 1 : -1)); + this._calendarViewFullCalendar.style.transform = `translate3d(${newX}px, 0, 0)`; + } + private _trackingEndFn(dx: number) { + const calendarViewFullCalendar = this._calendarViewFullCalendar; + const totalDraggableDistance = this._totalDraggableDistance; + const isPositive = dx > 0; + const absDx = Math.abs(dx); + const clamped = Math.min(totalDraggableDistance, absDx); + const initialX = totalDraggableDistance * -1; + const newX = totalDraggableDistance * -1 + (clamped * (isPositive ? 1 : -1)); + + /** + * NOTE(motss): + * If dragged distance < `dragRatio`, reset calendar position. + */ + if (absDx < totalDraggableDistance * this.dragRatio) { + const restoreDragAnimation = calendarViewFullCalendar.animate([ + { transform: `translate3d(${newX}px, 0, 0)` }, + { transform: `translate3d(${initialX}px, 0, 0)` }, + ], { + duration: this._dragAnimationDuration, + easing: 'cubic-bezier(0, 0, .4, 1)', + fill: this._hasNativeElementAnimate ? 'none' : 'both', + }); + + return new Promise(yay => (restoreDragAnimation.onfinish = yay)) + .then(() => new Promise(yay => window.requestAnimationFrame(yay))) + .then(() => { + calendarViewFullCalendar.style.transform = `translate3d(${initialX}px, 0, 0)`; + return this.updateComplete; + }); + } + + const restDx = totalDraggableDistance * (isPositive ? 0 : -2); + const dragAnimation = calendarViewFullCalendar.animate([ + { transform: `translate3d(${newX}px, 0, 0)` }, + { transform: `translate3d(${restDx}px, 0, 0)` }, + ], { + duration: this._dragAnimationDuration, + easing: 'cubic-bezier(0, 0, .4, 1)', + fill: this._hasNativeElementAnimate ? 'none' : 'both', + }); + + /** NOTE(motss): Drag to next calendar when drag ratio meets threshold value */ + return new Promise(yay => (dragAnimation.onfinish = yay)) + .then(() => new Promise(yay => window.requestAnimationFrame(yay))) + .then(() => { + const dateDate = new Date(this._selectedDate); + const fy = dateDate.getUTCFullYear(); + const m = dateDate.getUTCMonth(); + const d = dateDate.getUTCDate(); + const nm = isPositive ? -1 : 1; + + this._selectedDate = new Date(Date.UTC(fy, m + nm, d)); + + return this.updateComplete; + }) + .then(() => { + calendarViewFullCalendar.style.transform = `translate3d(${initialX}px, 0, 0)`; + return this.updateComplete; + }); + } + + // Left Move focus to the previous day. Will move to the last day of the previous month, + // if the current day is the first day of a month. + // Right Move focus to the next day. Will move to the first day of the following month, + // if the current day is the last day of a month. + // Up Move focus to the same day of the previous week. + // Will wrap to the appropriate day in the previous month. + // Down Move focus to the same day of the following week. + // Will wrap to the appropriate day in the following month. + // PgUp Move focus to the same date of the previous month. If that date does not exist, + // focus is placed on the last day of the month. + // PgDn Move focus to the same date of the following month. If that date does not exist, + // focus is placed on the last day of the month. + // Alt+PgUp Move focus to the same date of the previous year. + // If that date does not exist (e.g leap year), focus is placed on the last day of the month. + // Alt+PgDn Move focus to the same date of the following year. + // If that date does not exist (e.g leap year), focus is placed on the last day of the month. + // Home Move to the first day of the month. + // End Move to the last day of the month + // Tab / Shift+Tab If the datepicker is in modal mode, navigate between calender grid and + // close/previous/next selection buttons, otherwise move to the field following/preceding the + // date textbox associated with the datepicker + // Enter / Space Fill the date textbox with the selected date then close the datepicker widget. + private _updateMonthWithKeyboardFn(ev: KeyboardEvent) { + const keyCode = ev.keyCode; + + /** NOTE: Skip for TAB key and other non-related keys */ + if (keyCode === KEYCODES_MAP.TAB || + (keyCode !== KEYCODES_MAP.ARROW_DOWN + && keyCode !== KEYCODES_MAP.ARROW_LEFT + && keyCode !== KEYCODES_MAP.ARROW_RIGHT + && keyCode !== KEYCODES_MAP.ARROW_UP + && keyCode !== KEYCODES_MAP.END + && keyCode !== KEYCODES_MAP.HOME + && keyCode !== KEYCODES_MAP.PAGE_DOWN + && keyCode !== KEYCODES_MAP.PAGE_UP + && keyCode !== KEYCODES_MAP.ENTER + && keyCode !== KEYCODES_MAP.SPACE)) return; + + const hasAltKey = ev.altKey; + const min = getResolvedDate(this._min); + const max = getResolvedDate(this.max); + const selectedDate = this._selectedDate; + const focusedDate = this._focusedDate; + const fdFy = focusedDate.getUTCFullYear(); + const fdM = focusedDate.getUTCMonth(); + const fdD = focusedDate.getUTCDate(); + + let fy = fdFy; + let m = fdM; + let d = fdD; + let isMonthYearUpdate = false; + + /** + * NOTE: Focus to the 1st day of the current month when:- + * + * - `_selectedDate` has a different value of _full year_ than that of `_focusedDate`. + * - `_selectedDate` has a different value of _month_ than that of `_focusedDate`. + * + * This could simply mean that user changes `_selectedDate` with a new value of `month` or + * `year` but `_focusedDate` remains unchanged and it could be an out-of-bound calendar date. + * When keyboard event is detected, the 1st day of the current visible/ focusing month should be + * focused by updating `_focusedDate` with that value. + */ + if (selectedDate.getUTCMonth() !== fdM || selectedDate.getUTCFullYear() !== fdFy) { + this._focusedDate = new Date(Date.UTC( + selectedDate.getUTCFullYear(), + selectedDate.getUTCMonth(), + 1)); + + return this.updateComplete; + } + + switch (keyCode) { + case KEYCODES_MAP.ARROW_UP: { + d -= 7; + break; + } + case KEYCODES_MAP.ARROW_RIGHT: { + d += 1; + break; + } + case KEYCODES_MAP.ARROW_DOWN: { + d += 7; + break; + } + case KEYCODES_MAP.ARROW_LEFT: { + d -= 1; + break; + } + case KEYCODES_MAP.HOME: { + d = 1; + break; + } + case KEYCODES_MAP.END: { + m += 1; + d = 0; + break; + } + case KEYCODES_MAP.ENTER: { + if (+selectedDate !== +focusedDate) { + this._selectedDate = focusedDate; + } + + return this.updateComplete; + } + case KEYCODES_MAP.PAGE_UP: { + isMonthYearUpdate = true; + hasAltKey ? fy -= 1 : m -= 1; + break; + } + case KEYCODES_MAP.PAGE_DOWN: { + isMonthYearUpdate = true; + hasAltKey ? fy += 1 : m += 1; + break; + } + } + + /** NOTE: Skip calendar update if new focused date remains unchanged. */ + if (fy === fdFy && m === fdM && d === fdD) return; + + const { shouldUpdateDate, date } = computeNewFocusedDateWithKeyboard({ + min, + max, + selectedDate, + fy, + m, + d, + isMonthYearUpdate, + }); + + /** + * NOTE: If `date` returns null or `date` still same as `focusedDate`, + * this can skip updating any dates. This could simply mean the new focused date is + * within the range of * `min` and `max` dates. + */ + if (date == null || +date === +focusedDate) return; + + /** + * NOTE: Update `_selectedDate` if new focused date is no longer in the same month or year. + */ + if (shouldUpdateDate) this._selectedDate = date; + + this._focusedDate = date; + + return this.updateComplete; + } + +} + +// TODO: To look into `passive` event listener option in future. +// TODO: To suppport `valueAsDate` and `valueAsNumber`. +// TODO: To support RTL layout. +// TODO: To reflect value on certain properties according to specs/ browser impl: min, max, value. +// TODO: `disabledDays` and `disabledDates` are not supported +// FIXME: Updating `min` via attribute or property breaks entire UI +// TODO: To add support for labels such week number for better i18n diff --git a/src/calendar.ts b/src/calendar.ts new file mode 100644 index 00000000..d441d3e0 --- /dev/null +++ b/src/calendar.ts @@ -0,0 +1,243 @@ +export const enum WEEK_NUMBER_TYPE { + FIRST_4_DAY_WEEK = 'first-4-day-week', + FIRST_DAY_OF_YEAR = 'first-day-of-year', + FIRST_FULL_WEEK = 'first-full-week', +} + +import { stripLTRMark } from './datepicker-helpers'; + +function normalizeWeekday(weekday: number) { + if (weekday >= 0 && weekday < 7) return weekday; + + const weekdayOffset = weekday < 0 + ? 7 * Math.ceil(Math.abs(weekday / 7)) + : 0; + + return (weekdayOffset + weekday) % 7; +} + +function getFixedDateForWeekNumber(weekNumberType: string, date: Date) { + const wd = date.getUTCDay(); + const fy = date.getUTCFullYear(); + const m = date.getUTCMonth(); + const d = date.getUTCDate(); + + switch (weekNumberType) { + case WEEK_NUMBER_TYPE.FIRST_4_DAY_WEEK: + return new Date(Date.UTC(fy, m, d - wd + 3)); + case WEEK_NUMBER_TYPE.FIRST_DAY_OF_YEAR: + return new Date(Date.UTC(fy, m, d - wd + 6)); + case WEEK_NUMBER_TYPE.FIRST_FULL_WEEK: + return new Date(Date.UTC(fy, m, d - wd)); + default: + return date; + } +} + +/** + * {@link https://bit.ly/2UvEN2y|Compute week number by type} + * @param weekNumberType {string} + * @param date {Date} + * @return {} + */ +function computeWeekNumber(weekNumberType: string, date: Date) { + const fixedNow = getFixedDateForWeekNumber(weekNumberType, date); + const firstDayOfYear = new Date(Date.UTC(fixedNow.getUTCFullYear(), 0, 1)); + const wk = Math.ceil(((+fixedNow - +firstDayOfYear) / 864e5 + 1) / 7); + + return { + originalDate: date, + fixedDate: fixedNow, + weekNumber: wk, + }; +} + +export function calendarWeekdays({ + firstDayOfWeek, + showWeekNumber, + + longWeekdayFormatter, + narrowWeekdayFormatter, +}) { + const fixedFirstDayOfWeek = 1 + ((firstDayOfWeek + (firstDayOfWeek < 0 ? 7 : 0)) % 7); + const weekdays: unknown[] = showWeekNumber ? [{ label: 'Week', value: 'Wk' }] : []; + + for (let i = 0, len = 7; i < len; i += 1) { + const dateDate = new Date(Date.UTC(2017, 0, fixedFirstDayOfWeek + i)); + + weekdays.push({ + /** NOTE: Stripping LTR mark away for x-browser compatibilities and consistency reason */ + label: stripLTRMark(longWeekdayFormatter(dateDate)), + value: stripLTRMark(narrowWeekdayFormatter(dateDate)), + }); + } + + return weekdays; +} + +// Month Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec +// Days 31 28 31 30 31 30 31 31 30 31 30 31 +// 31? 0 2 4 6 7 9 11 +// 30? 3 5 8 10 +// Feb? 1 +// Su Mo Tu We Th Fr Sa startDay - _firstDayOfWeek +// 1 2 5 - 0 < 0 ? 6 : 5 - 0; +// Mo Tu We Th Fr Sa Su +// 1 2 3 5 - 1 < 0 ? 6 : 5 - 1; +// Tu We Th Fr Sa Su Mo +// 1 2 3 4 5 - 2 < 0 ? 6 : 5 - 2; +// We Th Fr Sa Su Mo Tu +// 1 2 3 4 5 5 - 3 < 0 ? 6 : 5 - 3; +// Th Fr Sa Su Mo Tu We +// 1 2 3 4 5 6 5 - 4 < 0 ? 6 : 5 - 4; +// Fr Sa Su Mo Tu We Th +// 1 2 3 4 5 6 7 5 - 5 < 0 ? 6 : 5 - 5; +// Sa Su Mo Tu We Th Fr +// 1 5 - 6 < 0 ? 6 : 5 - 6; +export function calendarDays({ + firstDayOfWeek, + selectedDate, + showWeekNumber, + weekNumberType, + idOffset, + + fullDateFormatter, + dayFormatter, +}) { + const fy = selectedDate.getUTCFullYear(); + const selectedMonth = selectedDate.getUTCMonth(); + const totalDays = new Date(Date.UTC(fy, selectedMonth + 1, 0)).getUTCDate(); + const preFirstWeekday = new Date(Date.UTC(fy, selectedMonth, 1)).getUTCDay() - firstDayOfWeek; + const firstWeekday = normalizeWeekday(preFirstWeekday); + const totalCol = showWeekNumber ? 8 : 7; + const firstWeekdayWithWeekNumberOffset = firstWeekday + (showWeekNumber ? 1 : 0); + + const fullCalendar: unknown[][] = []; + let calendarRow: unknown[] = []; + let day = 1; + let row = 0; + let col = 0; + let calendarFilled = false; + /** + * NOTE(motss): Thinking this is cool to write, + * don't blame me for writing this kind of loop. + * Optimization is totally welcome to make things faster. + * Also, I'd like to learn a better way. PM me and we can talk about that. 😄 + */ + for (let i = 0, len = 6 * totalCol + (showWeekNumber ? 6 : 0); i <= len; i += 1, col += 1) { + if (col >= totalCol) { + col = 0; + row += 1; + fullCalendar.push(calendarRow); + calendarRow = []; + } + + if (i >= len) break; + + const rowVal = col + (row * totalCol); + + if (!calendarFilled && showWeekNumber && col < 1) { + const { weekNumber } = computeWeekNumber( + weekNumberType, + new Date(Date.UTC(fy, selectedMonth, day - (row < 1 ? firstWeekday : 0)))); + const weekLabel = `Week ${weekNumber}`; + + calendarRow.push({ + fullDate: null, + label: weekLabel, + value: weekNumber, + id: weekLabel, + }); + // calendarRow.push(weekNumber); + continue; + } + + if (calendarFilled || rowVal < firstWeekdayWithWeekNumberOffset) { + calendarRow.push({ + fullDate: null, + label: null, + value: null, + id: (day + idOffset), + }); + // calendarRow.push(null); + continue; + } + + const d = new Date(Date.UTC(fy, selectedMonth, day)); + const fullDate = d.toJSON(); + + calendarRow.push({ + fullDate, + /** NOTE: Stripping LTR mark away for x-browser compatibilities and consistency reason */ + label: stripLTRMark(fullDateFormatter(d)), + value: stripLTRMark(dayFormatter(d)), + id: fullDate, + }); + // calendarRow.push(day); + day += 1; + + if (day > totalDays) calendarFilled = true; + } + + return fullCalendar; +} + +export function calendar({ + firstDayOfWeek, + showWeekNumber, + locale, + selectedDate, + weekNumberType, + idOffset, + + longWeekdayFormatterFn, + narrowWeekdayFormatterFn, + dayFormatterFn, + fullDateFormatterFn, +}) { + const longWeekdayFormatter = longWeekdayFormatterFn == null + ? Intl.DateTimeFormat(locale, { weekday: 'long', timeZone: 'UTC' }).format + : longWeekdayFormatterFn; + const narrowWeekdayFormatter = narrowWeekdayFormatterFn == null + ? Intl.DateTimeFormat(locale, { + /** NOTE: Only 'short' or 'narrow' (fallback) is allowed for 'weekdayFormat'. */ + // weekday: /^(short|narrow)/i.test(weekdayFormat) + // ? weekdayFormat + // : 'narrow', + weekday: 'narrow', + timeZone: 'UTC', + }).format + : narrowWeekdayFormatterFn; + const dayFormatter = dayFormatterFn == null + ? Intl.DateTimeFormat(locale, { day: 'numeric', timeZone: 'UTC' }).format + : dayFormatterFn; + const fullDateFormatter = fullDateFormatterFn == null + ? Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'short', + day: 'numeric', + weekday: 'short', + timeZone: 'UTC', + }).format + : fullDateFormatterFn; + + const weekdays = calendarWeekdays({ + firstDayOfWeek, + showWeekNumber, + + longWeekdayFormatter, + narrowWeekdayFormatter, + }); + const daysInMonth = calendarDays({ + dayFormatter, + fullDateFormatter, + + firstDayOfWeek, + selectedDate, + showWeekNumber, + weekNumberType, + idOffset: idOffset == null ? 0 : idOffset, + }); + + return { weekdays, daysInMonth }; +} diff --git a/src/common-styles.ts b/src/common-styles.ts new file mode 100644 index 00000000..ba5db412 --- /dev/null +++ b/src/common-styles.ts @@ -0,0 +1,66 @@ +import { css } from 'lit-element'; + +export const resetButton = css` +button { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + position: relative; + display: block; + margin: 0; + padding: 0; + background: none; /** NOTE: IE11 fix */ + color: inherit; + border: none; + font: inherit; + text-align: left; + text-transform: inherit; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +`; +export const resetAnchor = css` +a { + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + + position: relative; + display: inline-block; + background: initial; + color: inherit; + font: inherit; + text-transform: inherit; + text-decoration: none; + outline: none; +} +a:focus, +a:focus.page-selected { + text-decoration: underline; +} +`; +export const resetSvgIcon = css` +svg { + display: block; + min-width: var(--svg-icon-min-width, 24px); + min-height: var(--svg-icon-min-height, 24px); + fill: var(--svg-icon-fill, currentColor); + pointer-events: none; +} +`; +export const absoluteHidden = css`[hidden] { display: none !important; }`; +export const datepickerVariables = css` +:host { + display: block; + + /* --app-datepicker-width: 300px; */ + /* --app-datepicker-primary-color: #4285f4; */ + --app-datepicker-primary-color: #1a73e8; + --app-datepicker-border-radius: 8px; + --app-datepicker-header-height: 80px; + + --mdc-theme-primary: #1a73e8; +} + +* { + box-sizing: border-box; +} +`; diff --git a/src/datepicker-helpers.ts b/src/datepicker-helpers.ts new file mode 100644 index 00000000..dd91a2b9 --- /dev/null +++ b/src/datepicker-helpers.ts @@ -0,0 +1,260 @@ +export interface FocusTrap { + disconnect: () => void; +} +type AnyEventType = CustomEvent | KeyboardEvent | MouseEvent | PointerEvent; + +export const KEYCODES_MAP = { + // CTRL: 17, + // ALT: 18, + ESCAPE: 27, + SHIFT: 16, + TAB: 9, + ENTER: 13, + SPACE: 32, + PAGE_UP: 33, + PAGE_DOWN: 34, + END: 35, + HOME: 36, + ARROW_LEFT: 37, + ARROW_UP: 38, + ARROW_RIGHT: 39, + ARROW_DOWN: 40, +}; + +export function getResolvedDate(date?: number | Date | string | undefined): Date { + const dateDate = date == null ? new Date() : new Date(date); + const isUTCDateFormat = + typeof date === 'string' && ( + /^\d{4}[^\d\w]\d{2}[^\d\w]\d{2}$/i.test(date) || + /^\d{4}[^\d\w]\d{2}[^\d\w]\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/i.test(date)); + + let fy = dateDate.getFullYear(); + let m = dateDate.getMonth(); + let d = dateDate.getDate(); + + /** + * NOTE: Depends on the input date string, browser will interpret the Date object differently. + * For instance, a simple date string `2020-01-03` will default to UTC timezone. In order to get + * the correct expected date that is `3`, `.getUTCDate` is required as `.getDate` will return a + * date value that is based on the local timezone after the date conversion by the browser. In PST + * timezone, that will return `2`. + * + * ```ts + * // In PST (UTC-08:00) timezone, the following code will output: + * const dateString = '2020-01-03'; + * const dateDate = new Date(dateString); // UTC time is '2020-01-03T00:00:00.000+08:00' + * + * dateDate.getUTCDate(); // 3 + * dateDate.getDate(); // 2 + * ``` + */ + if (isUTCDateFormat) { + fy = dateDate.getUTCFullYear(); + m = dateDate.getUTCMonth(); + d = dateDate.getUTCDate(); + } + + /** + * NOTE: Converts local datetime to UTC by extracting only the values locally using `get*` methods + * instead of the `getUTC*` methods. + * + * FWIW, there could be still cases where `get*` methods returns something different than what is + * expected but that is acceptable since we're relying on browser to tell us the local datetime + * and we just use those values and treated them as if they were datetime to UTC. + */ + return new Date(Date.UTC(fy, m, d)); +} + +export function getResolvedLocale() { + return (Intl + && Intl.DateTimeFormat + && Intl.DateTimeFormat().resolvedOptions + && Intl.DateTimeFormat().resolvedOptions().locale) + || 'en-US'; +} + +export function computeThreeCalendarsInARow(selectedDate: Date) { + const dateDate = new Date(selectedDate); + const fy = dateDate.getUTCFullYear(); + const m = dateDate.getUTCMonth(); + const d = dateDate.getUTCDate(); + + return [ + new Date(Date.UTC(fy, m - 1, d)), + dateDate, + new Date(Date.UTC(fy, m + 1, d)), + ]; +} + +export function toFormattedDateString(date: Date) { + return date instanceof Date ? date.toJSON().replace(/^(.+)T.+/i, '$1') : ''; +} + +export function computeNewFocusedDateWithKeyboard({ + min, + max, + selectedDate, + fy, + m, + d, + isMonthYearUpdate, +}) { + let newFocusedDate = new Date(Date.UTC(fy, m, d)); + let dayInNewFocusedDate = newFocusedDate.getUTCDate(); + + /** + * NOTE: Check if new focused date exists in that new month. e.g. Feb 30. This should fallback to + * last day of the month (Feb), that is, Feb 28. Then if Feb 28 happens to fall in the disabled + * date range, the next step can take care of that. + */ + if (isMonthYearUpdate && d !== dayInNewFocusedDate) { + const newAdjustedDate = new Date(Date.UTC(fy, m + 1, 0)); + + dayInNewFocusedDate = newAdjustedDate.getUTCDate(); + newFocusedDate = newAdjustedDate; + } + + /** + * NOTE: Clipping new focused date to `min` or `max` when it falls into the range of disabled + * dates. + */ + const isLessThanMin = +newFocusedDate < +min; + const isMoreThanMax = +newFocusedDate > +max; + if (isLessThanMin || isMoreThanMax) { + /** Set to `min` date */ + if (isLessThanMin) { + return { shouldUpdateDate: false, date: min }; + } + + /** Set to `max` date */ + if (isMoreThanMax) { + return { shouldUpdateDate: false, date: max }; + } + + /** FIXME: Then when this happen? */ + return { shouldUpdateDate: false, date: null }; + } + + const shouldUpdateDate = newFocusedDate.getUTCMonth() !== selectedDate.getUTCMonth() + || newFocusedDate.getUTCFullYear() !== selectedDate.getUTCFullYear(); + + return { shouldUpdateDate, date: newFocusedDate }; +} + +export function findShadowTarget(ev: AnyEventType, callback: (n: HTMLElement) => boolean) { + return ev.composedPath().find((n: HTMLElement) => { + if (n instanceof HTMLElement) return callback(n); + return false; + }); +} + +export function dispatchCustomEvent( + target: HTMLElement, + eventName: string, + detail: T +) { + return target.dispatchEvent(new CustomEvent(eventName, { + detail, + bubbles: true, + composed: true, + })); +} + +export function setFocusTrap( + target: HTMLElement, + focusableElements: HTMLElement[] +): FocusTrap | null { + if (target == null || focusableElements == null) return null; + + const [firstEl, lastEl] = focusableElements; + const keydownCallback = (ev: KeyboardEvent) => { + const isTabKey = ev.keyCode === KEYCODES_MAP.TAB; + const isShiftTabKey = ev.shiftKey && isTabKey; + + if (!isTabKey && !isShiftTabKey) return; + + // const focusedTarget = ev.target as HTMLElement; + const isFocusingLastEl = findShadowTarget(ev, n => n.isEqualNode(lastEl)) != null; + + if (isFocusingLastEl && !isShiftTabKey) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + + // focusedTarget.blur(); + firstEl.focus(); + return; + } + + const isFocusingFirstEl = findShadowTarget(ev, n => n.isEqualNode(firstEl)) != null; + + if (isFocusingFirstEl && isShiftTabKey) { + ev.preventDefault(); + ev.stopImmediatePropagation(); + + // focusedTarget.blur(); + /** + * NOTE: `.focus()` native `