├── .github └── workflows │ ├── latest.yml │ └── release.yml ├── LICENSE ├── README.md ├── demo ├── README.md ├── package.json ├── public │ └── index.html ├── rollup.config.js └── src │ ├── App.svelte │ └── main.js ├── dist ├── minify.cmd ├── template_minifier.js ├── wc-datepicker-node.js ├── wc-datepicker-node.min.js ├── wc-datepicker.js └── wc-datepicker.min.js └── package.json /.github/workflows/latest.yml: -------------------------------------------------------------------------------- 1 | name: Latest 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | verify: 7 | name: Verify 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v1 12 | - name: Setup 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 14 16 | - name: Cache 17 | uses: actions/cache@v1 18 | with: 19 | path: node_modules 20 | key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/package.json') }} 21 | restore-keys: | 22 | ${{ runner.OS }}-npm-cache- 23 | - name: Install 24 | run: npm i 25 | - name: Test 26 | run: npm run test --if-present 27 | - name: Lint 28 | run: npm run lint --if-present 29 | - name: Types 30 | run: npm run types --if-present 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@master 14 | - name: Setup 15 | uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | - name: Cache 19 | uses: actions/cache@v1 20 | with: 21 | path: node_modules 22 | key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/package.json') }} 23 | restore-keys: | 24 | ${{ runner.OS }}-npm-cache- 25 | - name: Verify 26 | run: | 27 | npm i 28 | npm run preversion 29 | npm: 30 | runs-on: ubuntu-latest 31 | needs: check 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@master 35 | - name: Publish 36 | run: | 37 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_AUTH_TOKEN }}" > ~/.npmrc 38 | npm publish --access public 39 | gh: 40 | runs-on: ubuntu-latest 41 | needs: check 42 | steps: 43 | - name: Checkout 44 | uses: actions/checkout@master 45 | - name: Publish 46 | run: | 47 | echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> ~/.npmrc 48 | ORG="$(echo '${{ github.repository }}' | cut -d'/' -f1)" 49 | echo "registry=https://npm.pkg.github.com/$ORG" > .npmrc 50 | npm publish 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 VanillaWC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | GitHub Releases 3 | NPM Releases 4 | Bundlephobia 5 | Latest Status 6 | Release Status 7 | 8 | Discord 9 |
10 | 11 | # wc-datepicker 12 | 13 | A web component that wraps a text input element and adds date-picker functionality to it. 14 | 15 | Live demo available [here.](http://135.181.40.67/wc-datepicker/) 16 | 17 | ## Features 18 | 19 | Wc-datepicker is a stand-alone vanilla JS web component that does not use shadow DOM. The component wraps a text input element and adds date-picker functionality to it. The calendar will appear when the input element gets focus. 20 | 21 | Component features include: 22 | 23 | - highly customizable calendar layout 24 | - configurable names of months and week days 25 | - first day of week selection: sunday or monday 26 | - date format customization 27 | - initial date setting 28 | - written date input & validation 29 | - keyboard accessible calendar (tabindex) 30 | - rapid month/year switching with long press 31 | - Angular compatibility (see below) 32 | 33 | ## Including the component to an HTML file 34 | 35 | 1. Import polyfill, this is not needed for modern browsers: 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | 2. Import custom element: 42 | 43 | ```html 44 | 45 | ``` 46 | 47 | 3. Start using it! 48 | 49 | ```html 50 | 51 | 52 | 53 | ``` 54 | ## Including the component from NPM 55 | 56 | 1. Install and import polyfill, this is not needed for modern browsers: 57 | 58 | See https://www.npmjs.com/package/@webcomponents/custom-elements 59 | 60 | 2. Install wc-datepicker NPM package: 61 | 62 | ```console 63 | npm i @vanillawc/wc-datepicker 64 | ``` 65 | 66 | 3. Import custom element: 67 | 68 | ```javascript 69 | import '@vanillawc/wc-datepicker' 70 | ``` 71 | 72 | 4. Start using it: 73 | 74 | ```javascript 75 | var picker = document.createElement('wc-datepicker') 76 | var input = document.createElement('input') 77 | input.setAttribute('type', 'text') 78 | picker.appendChild(input) 79 | document.body.appendChild(picker) 80 | ``` 81 | ## Attributes 82 | 83 | Currently component has only one custom attribute that can be assigned a value in the HTML tag: 84 | 85 | Name | Type | Description | Unit / Values | Default value 86 | -------------- | --------- | ------------------------| -------------------| -------------- 87 | init-date | String | Initial date in the input field | Date in "dd.mm.yyyy" format or
"current" to select current date | None 88 | 89 | 90 | Following component attributes are boolean attributes, also known as valueless attributes. The presence of a boolean attribute in the HTML tag represents the true value, and the absence of the attribute represents the false value: 91 | 92 | Name | Description | if attribute is defined | If attribute is not defined 93 | -------|-------------|-------------------------|---------------------------- 94 | ignore-on-focus | Calendar appearance after the input element gets focus| Calendar won't appear| Calendar appears 95 | sunday-first | First day of the calendar week | Sunday is first | Monday is first 96 | persist-on-select | Calendar visibility after the date has been selected | Calendar won't disappear | Calendar disappears 97 | show-close-icon | Calendar close icon visibility | Icon is visible | Icon is hidden 98 | 99 | Usage examples: 100 | 101 | ```html 102 | 103 | 104 | 105 | ``` 106 | 107 | ```html 108 | 109 | 110 | 111 | ``` 112 | 113 | 114 | Following custom attributes can be specified at build time or dynamically at runtime: 115 | 116 | Name | Type | Description | Unit / Values | Default value 117 | --- | --- | --- | --- | --- 118 | dayNames | Array of strings | Week day names |Week day names from Monday to Sunday | Mon, Tue, Wed, Thu, Fri, Sat, Sun 119 | monthNames | Array of strings | Month names |Month names from January to December | Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec 120 | ignoreOnFocus | Boolean | Calendar appearance after the input element gets focus |true for not appearing
false for appearing | false 121 | sundayFirst | Boolean | Starting day of the week |true for Sunday
false for Monday | false 122 | persistOnSelect | Boolean | Calendar visibility after the date has been selected |true for visible
false for hidden | false 123 | showCloseIcon | Boolean | Calendar close icon visibility |true for visible
false for hidden | false 124 | initDate | String | Initial date in the input field |Date in "dd.mm.yyyy" format or "current" to select current date | None 125 | longPressThreshold | Number | Month/year switch long press threshold |Milliseconds | 500 126 | longPressInterval | Number | Long press month/year switch interval |Milliseconds | 150 127 | 128 | Usage example: 129 | 130 | ```javascript 131 | var picker = document.createElement('wc-datepicker') 132 | picker.dayNames = ['Mo','Tu','We','Th','Fr','Sa','Su'] 133 | picker.initDate = 'current' 134 | var input = document.createElement("input") 135 | input.setAttribute('type', 'text') 136 | picker.appendChild(input) 137 | document.body.appendChild(picker) 138 | ``` 139 | **Regarding dynamic and runtime usage of the component, custom attributes should always be set before the datepicker element is appended to DOM or initialized.** 140 | 141 | Although *init()* method can be called multiple times, not all attributes can be changed after the initial init. 142 | 143 | Regarding *init-date* (*initDate*) attribute, notice that the initial date format is determined by *_returnDateString()* and *_parseAndValidateInputStr()* methods, see chapter **Date format and validation** below. 144 | 145 | ## Methods 146 | 147 | ### init() 148 | Initializes date-picker functionality. This method is called automatically when the datepicker element is appended to DOM. 149 | 150 | This method has no effect, if the element does not have an input element as a child. 151 | 152 | If the datepicker is appended to DOM before the input element is appended to datepicker, *init()* must be called to make datepicker work. 153 | ### setFocusOnCal() 154 | The calendar will appear and get the focus when this method is called. 155 | 156 | On touch UIs this method can be used to prevent the keyboard from appearing, as the text input field won't get the focus. 157 | 158 | This method has no effect, if *init()* has not been called. If *ignoreOnFocus* has been set to true, this method is the only way to make the calendar appear. 159 | ### getDateString() 160 | Returns the date as string. Default format is "mm.dd.yyyy". 161 | 162 | Returns null if the input field does not contain a valid date. 163 | 164 | Date format and validity is determined by *_returnDateString()* and *_parseAndValidateInputStr()* methods. 165 | ### getDateObject() 166 | Returns the date as standard JS date object. 167 | 168 | Returns null if the input field does not contain a valid date. 169 | 170 | **Notice:** 171 | 172 | Since datepicker returns the object date in local time, UTC getter methods should not be used when processing the returned date further. Neither *Date.toJSON()* nor *Date.toISOString()* methods should be used, as they return the date in UTC format too. 173 | ## Date format and validation 174 | Default format is "mm.dd.yyyy". 175 | 176 | Date format can be changed by modifying *_returnDateString()* method. 177 | 178 | When date is written to input field, it is validated automatically if datepicker is initialized. 179 | 180 | If the date format to be used is changed, then the validating method must be modified also. 181 | 182 | The validating method to be modified is *_parseAndValidateInputStr()* 183 | 184 | It must return an object with either 1 or 4 properties: 185 | * valid - a boolean value indicating whether the date string is valid or not 186 | * day - a number value indicating the day of month (1 - 31) of the valid date string 187 | * month - a number value indicating the month (0-11) of the valid date string 188 | * year - a number value indicating the year of the valid date string 189 | 190 | Methods *getDateString()* and *getDateObject()* can also be used for validating the date, see above. 191 | ## Events 192 | If the input element loses focus and the date string is not valid, text input element shall dispatch [invalid](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/invalid_event) event. 193 | 194 | When the date string is edited to be a valid date or a new date is selected from the calendar, text input element shall dispatch *dateselect* event, which is a non-standard custom event. 195 | 196 | *Notice that event dispatching has changed since version 0.0.3. Starting from version 0.0.4, it is the wrapped input element instead of the custom element that shall dispatch events. Custom event name has been changed from datechange to dateselect.* 197 | 198 | ## Style and layout 199 | The style is defined in the HTML template string inside the component's contructor. 200 | Styling can be moved to an external CSS file by cutting and pasting everything that's inside style tags and then removing the void tags. 201 | 202 | Calendar width and height can be adjusted by modifying font-sizes and paddings: 203 | ```css 204 | .calDayName, .calDayStyle, .calAdjacentMonthDay, #calTitle { 205 | padding:5px; 206 | font-size:20px; 207 | text-align:center; 208 | } 209 | .calCtrl { 210 | font-size:20px; 211 | padding:0px 8px; 212 | user-select:none; 213 | } 214 | ``` 215 | 216 | Calendar's adjacent month day numbers can be changed to invisible by replacing the color definition in *.calAdjacentMonthDay* with 217 | ```css 218 | visibility:hidden; 219 | ``` 220 | 221 | ## Angular usage 222 | 223 | Using FLUX dataflow and one-way binding: 224 | 225 | ```html 226 | 227 | 233 | 234 | ``` 235 | 236 | Ignoring component's invalid event and validating date externally: 237 | 238 | ```html 239 | 240 | 246 | 247 | ``` 248 | 249 | Using distinct icon/button to activate the calendar: 250 | 251 | ```html 252 | 253 | 258 | 259 |
260 | 264 |
265 | ``` 266 | ## Building 267 | Unminified scripts in the dist folder can be used and modified as such, there are no build scripts available for them. 268 | 269 | Building is done by executing the minifier script minify.cmd, which is a Linux bash shell script. 270 | 271 | Minify.cmd can be found from dist folder. 272 | 273 | Building (minifying) requires [terser](https://github.com/terser/terser) command line tool to be installed. It can be installed with following command: 274 | ```console 275 | npm install terser -g 276 | ``` 277 | ## Contributing 278 | Questions, suggestions and bug reports are welcome. Safari testing would be nice. 279 | 280 | ## License 281 | Copyright (c) 2019-2020 Jussi Utunen 282 | 283 | Licensed under the MIT License 284 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | *Psst — looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)* 2 | 3 | --- 4 | 5 | # svelte app 6 | 7 | This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. 8 | 9 | To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): 10 | 11 | ```bash 12 | npx degit sveltejs/template svelte-app 13 | cd svelte-app 14 | ``` 15 | 16 | *Note that you will need to have [Node.js](https://nodejs.org) installed.* 17 | 18 | 19 | ## Get started 20 | 21 | Install the dependencies... 22 | 23 | ```bash 24 | cd svelte-app 25 | npm install 26 | ``` 27 | 28 | ...then start [Rollup](https://rollupjs.org): 29 | 30 | ```bash 31 | npm run dev 32 | ``` 33 | 34 | Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. 35 | 36 | By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. 37 | 38 | 39 | ## Building and running in production mode 40 | 41 | To create an optimised version of the app: 42 | 43 | ```bash 44 | npm run build 45 | ``` 46 | 47 | You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). 48 | 49 | 50 | ## Single-page app mode 51 | 52 | By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. 53 | 54 | If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: 55 | 56 | ```js 57 | "start": "sirv public --single" 58 | ``` 59 | 60 | 61 | ## Deploying to the web 62 | 63 | ### With [now](https://zeit.co/now) 64 | 65 | Install `now` if you haven't already: 66 | 67 | ```bash 68 | npm install -g now 69 | ``` 70 | 71 | Then, from within your project folder: 72 | 73 | ```bash 74 | cd public 75 | now deploy --name my-project 76 | ``` 77 | 78 | As an alternative, use the [Now desktop client](https://zeit.co/download) and simply drag the unzipped project folder to the taskbar icon. 79 | 80 | ### With [surge](https://surge.sh/) 81 | 82 | Install `surge` if you haven't already: 83 | 84 | ```bash 85 | npm install -g surge 86 | ``` 87 | 88 | Then, from within your project folder: 89 | 90 | ```bash 91 | npm run build 92 | surge public my-project.surge.sh 93 | ``` 94 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rollup -c", 6 | "dev": "rollup -c -w", 7 | "start": "sirv public" 8 | }, 9 | "devDependencies": { 10 | "rollup": "^1.12.0", 11 | "rollup-plugin-commonjs": "^10.0.0", 12 | "rollup-plugin-livereload": "^1.0.0", 13 | "rollup-plugin-node-resolve": "^5.2.0", 14 | "rollup-plugin-svelte": "^5.0.3", 15 | "rollup-plugin-terser": "^5.1.2", 16 | "svelte": "^3.0.0" 17 | }, 18 | "dependencies": { 19 | "sirv-cli": "^0.4.4", 20 | "svelte-highlight": "^0.3.0", 21 | "@vanillawc/wc-datepicker": "^0.0.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wc-datepicker demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /demo/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | const production = !process.env.ROLLUP_WATCH; 8 | 9 | export default { 10 | input: 'src/main.js', 11 | output: { 12 | sourcemap: true, 13 | format: 'iife', 14 | name: 'app', 15 | file: 'public/build/bundle.js' 16 | }, 17 | plugins: [ 18 | svelte({ 19 | // enable run-time checks when not in production 20 | dev: !production, 21 | // we'll extract any component CSS out into 22 | // a separate file — better for performance 23 | css: css => { 24 | css.write('public/build/bundle.css'); 25 | } 26 | }), 27 | 28 | // If you have external dependencies installed from 29 | // npm, you'll most likely need these plugins. In 30 | // some cases you'll need additional configuration — 31 | // consult the documentation for details: 32 | // https://github.com/rollup/rollup-plugin-commonjs 33 | resolve({ 34 | browser: true, 35 | dedupe: importee => importee === 'svelte' || importee.startsWith('svelte/') 36 | }), 37 | commonjs(), 38 | 39 | // In dev mode, call `npm run start` once 40 | // the bundle has been generated 41 | !production && serve(), 42 | 43 | // Watch the `public` directory and refresh the 44 | // browser on changes when not in production 45 | !production && livereload('public'), 46 | 47 | // If we're building for production (npm run build 48 | // instead of npm run dev), minify 49 | production && terser() 50 | ], 51 | watch: { 52 | clearScreen: false 53 | } 54 | }; 55 | 56 | function serve() { 57 | let started = false; 58 | 59 | return { 60 | writeBundle() { 61 | if (!started) { 62 | started = true; 63 | 64 | require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 65 | stdio: ['ignore', 'inherit', 'inherit'], 66 | shell: true 67 | }); 68 | } 69 | } 70 | }; 71 | } -------------------------------------------------------------------------------- /demo/src/App.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | {@html github} 20 | 21 | 22 |
23 |
24 | wc-datepicker web component is available from GitHub and NPM registry 25 |
26 | 29 |
30 |
31 | Default behaviour, calendar appears when input element gets focus: 32 |
33 | 34 | 35 | 36 |
37 | {html_code} 38 |
39 |
40 | 41 | {` `} 42 |
43 | {` `} 44 |
45 | {`
`} 46 |
47 |
48 |
49 |
50 |
51 | Setting current date as initial date: 52 |
53 | 54 | 55 | 56 |
57 | {html_code} 58 |
59 |
60 | 61 | {` `} 62 |
63 | {` `} 64 |
65 | {`
`} 66 |
67 |
68 |
69 |
70 |
71 | Sunday first: 72 |
73 | 74 | 75 | 76 |
77 | {html_code} 78 |
79 |
80 | 81 | {` `} 82 |
83 | {` `} 84 |
85 | {`
`} 86 |
87 |
88 |
89 |
90 |
91 | Persist on select: 92 |
93 | 94 | 95 | 96 |
97 | {html_code} 98 |
99 |
100 | 101 | {` `} 102 |
103 | {` `} 104 |
105 | {`
`} 106 |
107 |
108 |
109 |
110 |
111 | Show close icon: 112 |
113 | 114 | 115 | 116 |
117 | {html_code} 118 |
119 |
120 | 121 | {` `} 122 |
123 | {` `} 124 |
125 | {`
`} 126 |
127 |
128 |
129 |
130 |
131 | Ignore on focus, call setFocusOnCal() instead: 132 |
133 |
134 | 135 | 136 | 137 |
138 | 📅 139 |
140 |
141 |
142 | {html_code} 143 |
144 |
145 | 146 | {` `} 147 |
148 | {` `} 149 |
150 | {`
`} 151 |
152 | {`
`} 153 |
154 | {` 📅`} 155 |
156 | {`
`} 157 |
158 |
159 |
160 | Javascript code: 161 |
162 |
163 | 164 | {`function toggle_calendar() { 165 | var picker = document.getElementById('picker') 166 | picker.setFocusOnCal() 167 | }`} 168 | 169 |
170 |
171 | 174 |
175 | 176 | 257 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | const app = new App({ 4 | target: document.body 5 | }); 6 | 7 | export default app; 8 | -------------------------------------------------------------------------------- /dist/minify.cmd: -------------------------------------------------------------------------------- 1 | terser wc-datepicker-node.js -c -m --mangle-props reserved=[constructor,observedAttributes,disconnectedCallback,attributeChangedCallback,Datepicker,connectedCallback,enable,disable,setFocusOnCal,getDateString,getDateObject,dayNames,monthNames,sundayFirst,persistOnSelect,ignoreOnFocus,initDate,longPressThreshold,longPressInterval,showCloseIcon] -o _temp-file.js 2 | node template_minifier.js _temp-file.js wc-datepicker-node.min.js 3 | rm _temp-file.js 4 | terser wc-datepicker.js -c -m --mangle-props reserved=[constructor,observedAttributes,disconnectedCallback,attributeChangedCallback,Datepicker,connectedCallback,enable,disable,setFocusOnCal,getDateString,getDateObject,dayNames,monthNames,sundayFirst,persistOnSelect,ignoreOnFocus,initDate,longPressThreshold,longPressInterval,showCloseIcon] -o _temp-file.js 5 | node template_minifier.js _temp-file.js wc-datepicker.min.js 6 | rm _temp-file.js 7 | -------------------------------------------------------------------------------- /dist/template_minifier.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var args = process.argv.slice(2); 3 | var data = fs.readFileSync(args[0]); 4 | var newStr = data.toString().replace(/\\n\s*/g,""); 5 | newStr = newStr.replace(/\s{/g,"{"); 6 | fs.writeFile(args[1], newStr, function (err) { 7 | if (err) throw err; 8 | }); 9 | -------------------------------------------------------------------------------- /dist/wc-datepicker-node.js: -------------------------------------------------------------------------------- 1 | 2 | export class Datepicker extends HTMLElement { 3 | constructor () { 4 | super() 5 | // Regardless of sundayFirst value, set monday as first, sunday as last, always: 6 | this.dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] 7 | this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 8 | this.sundayFirst = false 9 | this.persistOnSelect = false 10 | this.longPressThreshold = 500 11 | this.longPressInterval = 150 12 | this.initDate = null 13 | this.ignoreOnFocus = false 14 | this.showCloseIcon = false 15 | this._inputStrIsValidDate = false 16 | this._longPressIntervalIds = [] 17 | this._longPressTimerIds = [] 18 | this._calTemplate = ` 19 | 66 |
67 |
68 |
◄◄
69 |
70 |
71 |
72 |
►►
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
` 127 | } 128 | 129 | static get observedAttributes () { 130 | return ['init-date', 131 | 'ignore-on-focus', 132 | 'sunday-first', 133 | 'persist-on-select', 134 | 'show-close-icon'] 135 | } 136 | 137 | disconnectedCallback () { 138 | } 139 | 140 | attributeChangedCallback (name, oldValue, newValue) { 141 | if (name === 'init-date') { 142 | this.initDate = newValue 143 | } else if (name === 'ignore-on-focus') { 144 | this.ignoreOnFocus = true 145 | } else if (name === 'sunday-first') { 146 | this.sundayFirst = true 147 | } else if (name === 'persist-on-select') { 148 | this.persistOnSelect = true 149 | } else if (name === 'show-close-icon') { 150 | this.showCloseIcon = true 151 | } 152 | } 153 | 154 | connectedCallback () { 155 | setTimeout(() => { this.init() }, 0) // https://stackoverflow.com/questions/58676021/accessing-custom-elements-child-element-without-using-slots-shadow-dom 156 | } 157 | 158 | init () { 159 | this.textInputElement = this.querySelector('input') 160 | if (this.textInputElement === null) { 161 | return 162 | } 163 | var mainContainer = document.createElement('div') 164 | mainContainer.style.display = 'inline-block' 165 | if (this.container) { 166 | this.container.remove() 167 | } 168 | this.container = this.appendChild(mainContainer) // The returned value is the appended child 169 | 170 | const template = document.createElement('template') 171 | template.innerHTML = this._calTemplate 172 | 173 | this.container.appendChild(this.textInputElement) 174 | this.container.appendChild(template.content) 175 | 176 | this.calTitle = this.querySelector('#calTitle') 177 | this.calContainer = this.querySelector('#calContainer') 178 | this.dateObj = new Date() 179 | var obj 180 | 181 | if (this.initDate !== null) { 182 | obj = this._parseAndValidateInputStr(this.initDate) 183 | if (obj.valid) { 184 | this.dateObj = new Date(obj.year, obj.month, obj.day) 185 | this._inputStrIsValidDate = true 186 | this.textInputElement.value = this._returnDateString(this.dateObj) 187 | } else if (this.initDate === 'current') { 188 | this._inputStrIsValidDate = true 189 | this.textInputElement.value = this._returnDateString(this.dateObj) 190 | } 191 | } else { 192 | obj = this._parseAndValidateInputStr(this.textInputElement.value) 193 | if (obj.valid) { 194 | this.dateObj = new Date(obj.year, obj.month, obj.day) 195 | this._inputStrIsValidDate = true 196 | } else { 197 | this._inputStrIsValidDate = false 198 | } 199 | } 200 | this.initDate = null 201 | 202 | this.displayedMonth = this.dateObj.getMonth() 203 | this.displayedYear = this.dateObj.getFullYear() 204 | 205 | this.calContainer.style.display = 'none' 206 | this._populateDayNames() 207 | this._addHeaderEventHandlers() 208 | this._renderCalendar() 209 | 210 | if (!this.ignoreOnFocus) { 211 | this.textInputElement.onfocus = this._inputOnFocusHandler 212 | this.textInputElement.onfocus = this.textInputElement.onfocus.bind(this) 213 | } 214 | 215 | this.textInputElement.oninput = this._inputOnInputHandler 216 | this.textInputElement.oninput = this.textInputElement.oninput.bind(this) 217 | 218 | this.textInputElement.onblur = this._blurHandler 219 | this.textInputElement.onblur = this.textInputElement.onblur.bind(this) 220 | 221 | this.calContainer.onblur = this._blurHandler 222 | this.calContainer.onblur = this.calContainer.onblur.bind(this) 223 | 224 | if (!this.showCloseIcon) { 225 | this.querySelector('#calCtrlHideCal').style.display = 'none' 226 | } 227 | } 228 | 229 | setFocusOnCal () { 230 | if (this.calContainer) { 231 | this.calContainer.style.display = 'block' 232 | this.calContainer.focus() 233 | } 234 | } 235 | 236 | _dayClickedEventHandler (event) { 237 | this._inputStrIsValidDate = true 238 | this._setNewDateValue(event.target.innerHTML, this.displayedMonth, this.displayedYear) 239 | this.textInputElement.value = this._returnDateString(this.dateObj) 240 | this.textInputElement.dispatchEvent(new CustomEvent('dateselect')) 241 | this._renderCalendar() 242 | if (!this.persistOnSelect) { 243 | this._hideCalendar() 244 | } 245 | } 246 | 247 | _hideCalendar () { 248 | document.activeElement.blur() 249 | } 250 | 251 | _calKeyDownEventHandler (event) { 252 | if (event.key === 'Enter') { 253 | this._dayClickedEventHandler(event) 254 | } 255 | } 256 | 257 | _blurHandler () { 258 | // When the input element loses focus due to click on calContainer, new focus won't be directly set to calContainer, it is set to body. 259 | // After calContainer onclick, focus will be on body unless following delay is introduced: 260 | setTimeout(() => { checkActiveElement(this) }, 0) 261 | function checkActiveElement (ctx) { 262 | if (!(document.activeElement.id === 'calContainer' || document.activeElement.classList.contains('calCtrl') || document.activeElement.classList.contains('calDay') || document.activeElement.isSameNode(ctx.textInputElement))) { 263 | ctx.calContainer.style.display = 'none' 264 | ctx._mouseUpEventHandler() 265 | if (!ctx._inputStrIsValidDate) { 266 | ctx.textInputElement.dispatchEvent(new Event('invalid')) 267 | } 268 | } 269 | } 270 | } 271 | 272 | _addHeaderEventHandlers () { 273 | var entries = this.calContainer.querySelectorAll('.calCtrl').entries() 274 | var entry = entries.next() 275 | while (entry.done === false) { 276 | entry.value[1].tabIndex = 0 277 | entry.value[1].onblur = this._blurHandler 278 | entry.value[1].onblur = entry.value[1].onblur.bind(this) 279 | entry.value[1].onclick = this._controlKeyDownEventHandler 280 | entry.value[1].onclick = entry.value[1].onclick.bind(this) 281 | entry.value[1].onkeydown = this._controlKeyDownEventHandler 282 | entry.value[1].onkeydown = entry.value[1].onkeydown.bind(this) 283 | entry.value[1].onmousedown = this._mouseDownEventHandler 284 | entry.value[1].onmousedown = entry.value[1].onmousedown.bind(this) 285 | entry.value[1].onmouseup = this._mouseUpEventHandler 286 | entry.value[1].onmouseup = entry.value[1].onmouseup.bind(this) 287 | entry.value[1].onmouseleave = this._mouseUpEventHandler 288 | entry.value[1].onmouseleave = entry.value[1].onmouseleave.bind(this) 289 | entry.value[1].ontouchstart = this._mouseDownEventHandler 290 | entry.value[1].ontouchstart = entry.value[1].ontouchstart.bind(this) 291 | entry.value[1].ontouchend = this._mouseUpEventHandler 292 | entry.value[1].ontouchend = entry.value[1].ontouchend.bind(this) 293 | entry.value[1].ontouchcancel = this._mouseUpEventHandler 294 | entry.value[1].ontouchcancel = entry.value[1].ontouchcancel.bind(this) 295 | entry = entries.next() 296 | } 297 | } 298 | 299 | _startLongPressAction (event) { 300 | this._longPressIntervalIds.push(setInterval(() => { this._controlKeyDownEventHandler(event) }, this.longPressInterval)) 301 | this.querySelector('#' + event.target.id).onclick = () => { this._onClickHandlerAfterLongPress(event, this) } 302 | } 303 | 304 | // For better UX, after long press, onclick must be discarded once, 305 | // thus do nothing with the event and set clickhandler back to the real one: 306 | _onClickHandlerAfterLongPress (event, ctx) { 307 | ctx.querySelector('#' + event.target.id).onclick = ctx._controlKeyDownEventHandler 308 | ctx.querySelector('#' + event.target.id).onclick = ctx.querySelector('#' + event.target.id).onclick.bind(ctx) 309 | } 310 | 311 | _mouseDownEventHandler (event) { 312 | this._longPressTimerIds.push(setTimeout(() => { this._startLongPressAction(event) }, this.longPressThreshold)) 313 | } 314 | 315 | _mouseUpEventHandler () { 316 | this._longPressTimerIds.forEach(clearTimeout) 317 | this._longPressTimerIds = [] 318 | this._longPressIntervalIds.forEach(clearInterval) 319 | this._longPressIntervalIds = [] 320 | } 321 | 322 | _parseAndValidateInputStr (str) { 323 | var obj = {} 324 | var day, month, year 325 | var value = str.match(/^\s*(\d{1,2})\.(\d{1,2})\.(\d\d\d\d)\s*$/) 326 | if (value === null) { 327 | obj.valid = false 328 | } else { 329 | day = Number(value[1]) 330 | month = Number(value[2]) 331 | year = Number(value[3]) 332 | if (this._dateIsValid(day, month, year)) { 333 | obj.valid = true 334 | obj.day = day 335 | obj.month = month - 1 336 | obj.year = year 337 | } else { 338 | obj.valid = false 339 | } 340 | } 341 | return obj 342 | } 343 | 344 | _inputOnInputHandler () { 345 | var obj = this._parseAndValidateInputStr(this.textInputElement.value) 346 | if (obj.valid) { 347 | this._inputStrIsValidDate = true 348 | this._setNewDateValue(obj.day, obj.month, obj.year) 349 | this.displayedMonth = obj.month 350 | this.displayedYear = obj.year 351 | this.textInputElement.dispatchEvent(new CustomEvent('dateselect')) 352 | this._renderCalendar() 353 | } else { 354 | this._inputStrIsValidDate = false 355 | } 356 | } 357 | 358 | _dateIsValid (day, month, year) { 359 | if (month < 1 || month > 12) { 360 | return false 361 | } 362 | var last_day_of_month = this._daysInMonth(month, year) 363 | if (day < 1 || day > last_day_of_month) { 364 | return false 365 | } 366 | return true 367 | } 368 | 369 | _controlKeyDownEventHandler (event) { 370 | if (event.key === 'Enter' || event.type !== 'keydown') { 371 | switch (event.target.id) { 372 | case 'calCtrlPrevYear': 373 | this._showPrevYear() 374 | break 375 | case 'calCtrlNextYear': 376 | this._showNextYear() 377 | break 378 | case 'calCtrlPrevMonth': 379 | this._showPrevMonth() 380 | break 381 | case 'calCtrlNextMonth': 382 | this._showNextMonth() 383 | break 384 | case 'calCtrlHideCal': 385 | this._hideCalendar() 386 | break 387 | } 388 | } 389 | } 390 | 391 | _inputOnFocusHandler () { 392 | this._inputOnInputHandler() 393 | this.calContainer.style.display = 'block' 394 | } 395 | 396 | _showNextYear () { 397 | this.displayedYear++ 398 | this._renderCalendar() 399 | } 400 | 401 | _showPrevYear () { 402 | this.displayedYear-- 403 | this._renderCalendar() 404 | } 405 | 406 | _showNextMonth () { 407 | if (this.displayedMonth === 11) { 408 | this.displayedMonth = 0 409 | this.displayedYear++ 410 | } else { 411 | this.displayedMonth++ 412 | } 413 | this._renderCalendar() 414 | } 415 | 416 | _showPrevMonth () { 417 | if (this.displayedMonth === 0) { 418 | this.displayedMonth = 11 419 | this.displayedYear-- 420 | } else { 421 | this.displayedMonth-- 422 | } 423 | this._renderCalendar() 424 | } 425 | 426 | _renderCalendar () { 427 | var tempDate = new Date(this.displayedYear, this.displayedMonth) 428 | tempDate.setDate(1) 429 | this.calTitle.innerHTML = this.monthNames[this.displayedMonth] + ' ' + this.displayedYear 430 | var dayNumbers = [] 431 | var adjacentMonthDays = [] 432 | this._generateDayArray(tempDate, dayNumbers, adjacentMonthDays) 433 | var entries = this.calContainer.querySelectorAll('.calDay').entries() 434 | var entry = entries.next() 435 | while (entry.done === false) { 436 | entry.value[1].classList.remove('calAdjacentMonthDay') 437 | entry.value[1].classList.remove('calSelectedDay') 438 | entry.value[1].classList.remove('calHiddenRow') 439 | entry.value[1].classList.remove('calDayStyle') 440 | entry.value[1].onclick = null 441 | entry.value[1].onblur = null 442 | entry.value[1].onkeydown = null 443 | if (adjacentMonthDays[entry.value[0]]) { 444 | entry.value[1].classList.add('calAdjacentMonthDay') 445 | } else { 446 | entry.value[1].classList.add('calDayStyle') 447 | } 448 | entry.value[1].innerHTML = dayNumbers[entry.value[0]] 449 | if (this.displayedMonth === this.dateObj.getMonth() && this.displayedYear === this.dateObj.getFullYear() && dayNumbers[entry.value[0]] === this.dateObj.getDate() && !adjacentMonthDays[entry.value[0]]) { 450 | entry.value[1].classList.add('calSelectedDay') 451 | } 452 | if (!adjacentMonthDays[entry.value[0]]) { 453 | entry.value[1].onclick = this._dayClickedEventHandler 454 | entry.value[1].onclick = entry.value[1].onclick.bind(this) 455 | entry.value[1].onkeydown = this._calKeyDownEventHandler 456 | entry.value[1].onkeydown = entry.value[1].onkeydown.bind(this) 457 | entry.value[1].tabIndex = 0 458 | entry.value[1].onblur = this._blurHandler 459 | entry.value[1].onblur = entry.value[1].onblur.bind(this) 460 | } else { 461 | entry.value[1].removeAttribute('tabindex') 462 | } 463 | entry = entries.next() 464 | } 465 | 466 | // checking if last (=lowest) row of days are all adjacent month days: 467 | var lastSeven = adjacentMonthDays.slice(35, 42) 468 | if (lastSeven.every(x => x === true)) { 469 | entries = this.calContainer.querySelectorAll('.calDay').entries() 470 | entry = entries.next() 471 | while (entry.done === false) { 472 | if (entry.value[0] > 34) { 473 | entry.value[1].classList.add('calHiddenRow') 474 | } 475 | entry = entries.next() 476 | } 477 | } 478 | } 479 | 480 | getDateString () { 481 | if (this._inputStrIsValidDate) { 482 | return this._returnDateString(this.dateObj) 483 | } 484 | return null 485 | } 486 | 487 | getDateObject () { 488 | if (this._inputStrIsValidDate) { 489 | return this.dateObj 490 | } 491 | return null 492 | } 493 | 494 | _setNewDateValue (day, month, year) { 495 | day = Number(day) 496 | month = Number(month) 497 | year = Number(year) 498 | if (day !== this.dateObj.getDate() || month !== this.dateObj.getMonth() || year !== this.dateObj.getFullYear()) { 499 | // Order is important, always set year first: 500 | this.dateObj.setFullYear(year) 501 | // Do not use setDate here: 502 | // this.dateObj.setDate(day) <-- https://stackoverflow.com/questions/14680396/the-date-getmonth-method-has-bug 503 | // Use setMonth with 2 params instead: 504 | this.dateObj.setMonth(month, day) 505 | } 506 | } 507 | 508 | _returnDateString (date) { 509 | var year = date.getFullYear() 510 | var month = date.getMonth() + 1 511 | var day = date.getDate() 512 | return day + '.' + month + '.' + year 513 | } 514 | 515 | _populateDayNames () { 516 | var dayNameArray = [] 517 | dayNameArray = this.dayNames.slice() 518 | if (this.sundayFirst) { 519 | dayNameArray.pop() 520 | dayNameArray.unshift(this.dayNames[6]) 521 | } 522 | var entries = this.calContainer.querySelectorAll('.calDayName').entries() 523 | var entry = entries.next() 524 | while (entry.done === false) { 525 | entry.value[1].innerHTML = dayNameArray[entry.value[0]] 526 | entry = entries.next() 527 | } 528 | } 529 | 530 | _generateDayArray (date, dayArray, adjacentMonthDaysArray) { 531 | var index 532 | var dateDay = date.getDay() 533 | var dateMonth = date.getMonth() + 1 534 | var dateYear = date.getFullYear() 535 | var daysInMonth = this._daysInMonth(dateMonth, dateYear) 536 | 537 | date.setDate(date.getDate() - 1) 538 | var prevMonth = date.getMonth() + 1 539 | var prevMonthYear = date.getFullYear() 540 | var daysInPrevMonth = this._daysInMonth(prevMonth, prevMonthYear) 541 | 542 | // prev month day filling: 543 | if (this.sundayFirst) { 544 | for (index = 0; index < dateDay; index++) { 545 | dayArray.unshift(daysInPrevMonth) 546 | daysInPrevMonth-- 547 | adjacentMonthDaysArray.push(true) 548 | } 549 | } else { 550 | if (dateDay === 0) { 551 | for (index = 0; index < 6; index++) { 552 | dayArray.unshift(daysInPrevMonth) 553 | daysInPrevMonth-- 554 | adjacentMonthDaysArray.push(true) 555 | } 556 | } else { 557 | for (index = 0; index < dateDay - 1; index++) { 558 | dayArray.unshift(daysInPrevMonth) 559 | daysInPrevMonth-- 560 | adjacentMonthDaysArray.push(true) 561 | } 562 | } 563 | } 564 | 565 | // current month day filling: 566 | for (index = 0; index < daysInMonth; index++) { 567 | dayArray.push(index + 1) 568 | adjacentMonthDaysArray.push(false) 569 | } 570 | 571 | // next month day filling: 572 | var numberOfNextMonthDays = 42 - dayArray.length 573 | for (index = 0; index < numberOfNextMonthDays; index++) { 574 | dayArray.push(index + 1) 575 | adjacentMonthDaysArray.push(true) 576 | } 577 | } 578 | 579 | _isItLeapYear (year) { 580 | return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) 581 | } 582 | 583 | _daysInMonth (month, year) { 584 | if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { 585 | return 31 586 | } else if (month === 4 || month === 6 || month === 9 || month === 11) { 587 | return 30 588 | } else if (month === 2 && this._isItLeapYear(year)) { 589 | return 29 590 | } else if (month === 2 && !(this._isItLeapYear(year))) { 591 | return 28 592 | } 593 | } 594 | } 595 | 596 | customElements.define('wc-datepicker', Datepicker) 597 | -------------------------------------------------------------------------------- /dist/wc-datepicker-node.min.js: -------------------------------------------------------------------------------- 1 | export class Datepicker extends HTMLElement{constructor(){super(),this.dayNames=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],this.monthNames=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],this.sundayFirst=!1,this.persistOnSelect=!1,this.longPressThreshold=500,this.longPressInterval=150,this.initDate=null,this.ignoreOnFocus=!1,this.showCloseIcon=!1,this.i=!1,this.t=[],this.s=[],this.l='
◄◄
►►
'}static get observedAttributes(){return["init-date","ignore-on-focus","sunday-first","persist-on-select","show-close-icon"]}disconnectedCallback(){}attributeChangedCallback(i,t,s){"init-date"===i?this.initDate=s:"ignore-on-focus"===i?this.ignoreOnFocus=!0:"sunday-first"===i?this.sundayFirst=!0:"persist-on-select"===i?this.persistOnSelect=!0:"show-close-icon"===i&&(this.showCloseIcon=!0)}connectedCallback(){setTimeout(()=>{this.init()},0)}init(){if(this.h=this.querySelector("input"),null===this.h)return;var i=document.createElement("div");i.style.display="inline-block",this.v&&this.v.remove(),this.v=this.appendChild(i);const t=document.createElement("template");var s;t.innerHTML=this.l,this.v.appendChild(this.h),this.v.appendChild(t.content),this.o=this.querySelector("#calTitle"),this.u=this.querySelector("#calContainer"),this.D=new Date,null!==this.initDate?(s=this.m(this.initDate)).valid?(this.D=new Date(s.p,s.C,s.g),this.i=!0,this.h.value=this._(this.D)):"current"===this.initDate&&(this.i=!0,this.h.value=this._(this.D)):(s=this.m(this.h.value)).valid?(this.D=new Date(s.p,s.C,s.g),this.i=!0):this.i=!1,this.initDate=null,this.N=this.D.getMonth(),this.k=this.D.getFullYear(),this.u.style.display="none",this.H(),this.M(),this.S(),this.ignoreOnFocus||(this.h.onfocus=this.A,this.h.onfocus=this.h.onfocus.bind(this)),this.h.oninput=this.T,this.h.oninput=this.h.oninput.bind(this),this.h.onblur=this.P,this.h.onblur=this.h.onblur.bind(this),this.u.onblur=this.P,this.u.onblur=this.u.onblur.bind(this),this.showCloseIcon||(this.querySelector("#calCtrlHideCal").style.display="none")}setFocusOnCal(){this.u&&(this.u.style.display="block",this.u.focus())}I(i){this.i=!0,this.Y(i.target.innerHTML,this.N,this.k),this.h.value=this._(this.D),this.h.dispatchEvent(new CustomEvent("dateselect")),this.S(),this.persistOnSelect||this.j()}j(){document.activeElement.blur()}O(i){"Enter"===i.key&&this.I(i)}P(){setTimeout(()=>{var i;i=this,"calContainer"===document.activeElement.id||document.activeElement.classList.contains("calCtrl")||document.activeElement.classList.contains("calDay")||document.activeElement.isSameNode(i.h)||(i.u.style.display="none",i.F(),i.i||i.h.dispatchEvent(new Event("invalid")))},0)}M(){for(var i=this.u.querySelectorAll(".calCtrl").entries(),t=i.next();!1===t.done;)t.value[1].tabIndex=0,t.value[1].onblur=this.P,t.value[1].onblur=t.value[1].onblur.bind(this),t.value[1].onclick=this.L,t.value[1].onclick=t.value[1].onclick.bind(this),t.value[1].onkeydown=this.L,t.value[1].onkeydown=t.value[1].onkeydown.bind(this),t.value[1].onmousedown=this.J,t.value[1].onmousedown=t.value[1].onmousedown.bind(this),t.value[1].onmouseup=this.F,t.value[1].onmouseup=t.value[1].onmouseup.bind(this),t.value[1].onmouseleave=this.F,t.value[1].onmouseleave=t.value[1].onmouseleave.bind(this),t.value[1].ontouchstart=this.J,t.value[1].ontouchstart=t.value[1].ontouchstart.bind(this),t.value[1].ontouchend=this.F,t.value[1].ontouchend=t.value[1].ontouchend.bind(this),t.value[1].ontouchcancel=this.F,t.value[1].ontouchcancel=t.value[1].ontouchcancel.bind(this),t=i.next()}R(i){this.t.push(setInterval(()=>{this.L(i)},this.longPressInterval)),this.querySelector("#"+i.target.id).onclick=()=>{this.V(i,this)}}V(i,t){t.querySelector("#"+i.target.id).onclick=t.L,t.querySelector("#"+i.target.id).onclick=t.querySelector("#"+i.target.id).onclick.bind(t)}J(i){this.s.push(setTimeout(()=>{this.R(i)},this.longPressThreshold))}F(){this.s.forEach(clearTimeout),this.s=[],this.t.forEach(clearInterval),this.t=[]}m(i){var t,s,a,l={},n=i.match(/^\s*(\d{1,2})\.(\d{1,2})\.(\d\d\d\d)\s*$/);return null===n?l.valid=!1:(t=Number(n[1]),s=Number(n[2]),a=Number(n[3]),this.G(t,s,a)?(l.valid=!0,l.g=t,l.C=s-1,l.p=a):l.valid=!1),l}T(){var i=this.m(this.h.value);i.valid?(this.i=!0,this.Y(i.g,i.C,i.p),this.N=i.C,this.k=i.p,this.h.dispatchEvent(new CustomEvent("dateselect")),this.S()):this.i=!1}G(i,t,s){if(t<1||t>12)return!1;var a=this.K(t,s);return!(i<1||i>a)}L(i){if("Enter"===i.key||"keydown"!==i.type)switch(i.target.id){case"calCtrlPrevYear":this.U();break;case"calCtrlNextYear":this.W();break;case"calCtrlPrevMonth":this.$();break;case"calCtrlNextMonth":this.q();break;case"calCtrlHideCal":this.j()}}A(){this.T(),this.u.style.display="block"}W(){this.k++,this.S()}U(){this.k--,this.S()}q(){11===this.N?(this.N=0,this.k++):this.N++,this.S()}$(){0===this.N?(this.N=11,this.k--):this.N--,this.S()}S(){var i=new Date(this.k,this.N);i.setDate(1),this.o.innerHTML=this.monthNames[this.N]+" "+this.k;var t=[],s=[];this.B(i,t,s);for(var a=this.u.querySelectorAll(".calDay").entries(),l=a.next();!1===l.done;)l.value[1].classList.remove("calAdjacentMonthDay"),l.value[1].classList.remove("calSelectedDay"),l.value[1].classList.remove("calHiddenRow"),l.value[1].classList.remove("calDayStyle"),l.value[1].onclick=null,l.value[1].onblur=null,l.value[1].onkeydown=null,s[l.value[0]]?l.value[1].classList.add("calAdjacentMonthDay"):l.value[1].classList.add("calDayStyle"),l.value[1].innerHTML=t[l.value[0]],this.N!==this.D.getMonth()||this.k!==this.D.getFullYear()||t[l.value[0]]!==this.D.getDate()||s[l.value[0]]||l.value[1].classList.add("calSelectedDay"),s[l.value[0]]?l.value[1].removeAttribute("tabindex"):(l.value[1].onclick=this.I,l.value[1].onclick=l.value[1].onclick.bind(this),l.value[1].onkeydown=this.O,l.value[1].onkeydown=l.value[1].onkeydown.bind(this),l.value[1].tabIndex=0,l.value[1].onblur=this.P,l.value[1].onblur=l.value[1].onblur.bind(this)),l=a.next();if(s.slice(35,42).every(i=>!0===i))for(l=(a=this.u.querySelectorAll(".calDay").entries()).next();!1===l.done;)l.value[0]>34&&l.value[1].classList.add("calHiddenRow"),l=a.next()}getDateString(){return this.i?this._(this.D):null}getDateObject(){return this.i?this.D:null}Y(i,t,s){i=Number(i),t=Number(t),s=Number(s),i===this.D.getDate()&&t===this.D.getMonth()&&s===this.D.getFullYear()||(this.D.setFullYear(s),this.D.setMonth(t,i))}_(i){var t=i.getFullYear(),s=i.getMonth()+1;return i.getDate()+"."+s+"."+t}H(){var i=[];i=this.dayNames.slice(),this.sundayFirst&&(i.pop(),i.unshift(this.dayNames[6]));for(var t=this.u.querySelectorAll(".calDayName").entries(),s=t.next();!1===s.done;)s.value[1].innerHTML=i[s.value[0]],s=t.next()}B(i,t,s){var a,l=i.getDay(),n=i.getMonth()+1,e=i.getFullYear(),c=this.K(n,e);i.setDate(i.getDate()-1);var h=i.getMonth()+1,d=i.getFullYear(),r=this.K(h,d);if(this.sundayFirst)for(a=0;a 20 | #calContainer { 21 | border:1px solid black; 22 | position:absolute; 23 | background-color:white; 24 | z-index:1000; 25 | } 26 | #calHeader { 27 | display:flex; 28 | align-items:center; 29 | justify-content:space-around; 30 | margin-top:5px; 31 | } 32 | #calGrid { 33 | display:grid; 34 | grid-template-columns:auto auto auto auto auto auto auto; 35 | padding:10px; 36 | } 37 | .calDayName, .calDayStyle, .calAdjacentMonthDay, #calTitle { 38 | padding:5px; 39 | font-size:20px; 40 | text-align:center; 41 | } 42 | .calDayStyle:hover, .calCtrl:hover { 43 | color:white; 44 | background-color:black; 45 | cursor:default; 46 | } 47 | .calHiddenRow { 48 | display:none; 49 | } 50 | .calAdjacentMonthDay { 51 | color:lightgray; 52 | } 53 | .calSelectedDay { 54 | color:red; 55 | font-weight:bold; 56 | } 57 | #calTitle { 58 | width:110px; 59 | } 60 | .calCtrl { 61 | font-size:20px; 62 | padding:0px 8px; 63 | user-select:none; 64 | } 65 | 66 |
67 |
68 |
◄◄
69 |
70 |
71 |
72 |
►►
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
` 127 | } 128 | 129 | static get observedAttributes () { 130 | return ['init-date', 131 | 'ignore-on-focus', 132 | 'sunday-first', 133 | 'persist-on-select', 134 | 'show-close-icon'] 135 | } 136 | 137 | disconnectedCallback () { 138 | } 139 | 140 | attributeChangedCallback (name, oldValue, newValue) { 141 | if (name === 'init-date') { 142 | this.initDate = newValue 143 | } else if (name === 'ignore-on-focus') { 144 | this.ignoreOnFocus = true 145 | } else if (name === 'sunday-first') { 146 | this.sundayFirst = true 147 | } else if (name === 'persist-on-select') { 148 | this.persistOnSelect = true 149 | } else if (name === 'show-close-icon') { 150 | this.showCloseIcon = true 151 | } 152 | } 153 | 154 | connectedCallback () { 155 | setTimeout(() => { this.init() }, 0) // https://stackoverflow.com/questions/58676021/accessing-custom-elements-child-element-without-using-slots-shadow-dom 156 | } 157 | 158 | init () { 159 | this.textInputElement = this.querySelector('input') 160 | if (this.textInputElement === null) { 161 | return 162 | } 163 | var mainContainer = document.createElement('div') 164 | mainContainer.style.display = 'inline-block' 165 | if (this.container) { 166 | this.container.remove() 167 | } 168 | this.container = this.appendChild(mainContainer) // The returned value is the appended child 169 | 170 | const template = document.createElement('template') 171 | template.innerHTML = this._calTemplate 172 | 173 | this.container.appendChild(this.textInputElement) 174 | this.container.appendChild(template.content) 175 | 176 | this.calTitle = this.querySelector('#calTitle') 177 | this.calContainer = this.querySelector('#calContainer') 178 | this.dateObj = new Date() 179 | var obj 180 | 181 | if (this.initDate !== null) { 182 | obj = this._parseAndValidateInputStr(this.initDate) 183 | if (obj.valid) { 184 | this.dateObj = new Date(obj.year, obj.month, obj.day) 185 | this._inputStrIsValidDate = true 186 | this.textInputElement.value = this._returnDateString(this.dateObj) 187 | } else if (this.initDate === 'current') { 188 | this._inputStrIsValidDate = true 189 | this.textInputElement.value = this._returnDateString(this.dateObj) 190 | } 191 | } else { 192 | obj = this._parseAndValidateInputStr(this.textInputElement.value) 193 | if (obj.valid) { 194 | this.dateObj = new Date(obj.year, obj.month, obj.day) 195 | this._inputStrIsValidDate = true 196 | } else { 197 | this._inputStrIsValidDate = false 198 | } 199 | } 200 | this.initDate = null 201 | 202 | this.displayedMonth = this.dateObj.getMonth() 203 | this.displayedYear = this.dateObj.getFullYear() 204 | 205 | this.calContainer.style.display = 'none' 206 | this._populateDayNames() 207 | this._addHeaderEventHandlers() 208 | this._renderCalendar() 209 | 210 | if (!this.ignoreOnFocus) { 211 | this.textInputElement.onfocus = this._inputOnFocusHandler 212 | this.textInputElement.onfocus = this.textInputElement.onfocus.bind(this) 213 | } 214 | 215 | this.textInputElement.oninput = this._inputOnInputHandler 216 | this.textInputElement.oninput = this.textInputElement.oninput.bind(this) 217 | 218 | this.textInputElement.onblur = this._blurHandler 219 | this.textInputElement.onblur = this.textInputElement.onblur.bind(this) 220 | 221 | this.calContainer.onblur = this._blurHandler 222 | this.calContainer.onblur = this.calContainer.onblur.bind(this) 223 | 224 | if (!this.showCloseIcon) { 225 | this.querySelector('#calCtrlHideCal').style.display = 'none' 226 | } 227 | } 228 | 229 | setFocusOnCal () { 230 | if (this.calContainer) { 231 | this.calContainer.style.display = 'block' 232 | this.calContainer.focus() 233 | } 234 | } 235 | 236 | _dayClickedEventHandler (event) { 237 | this._inputStrIsValidDate = true 238 | this._setNewDateValue(event.target.innerHTML, this.displayedMonth, this.displayedYear) 239 | this.textInputElement.value = this._returnDateString(this.dateObj) 240 | this.textInputElement.dispatchEvent(new CustomEvent('dateselect')) 241 | this._renderCalendar() 242 | if (!this.persistOnSelect) { 243 | this._hideCalendar() 244 | } 245 | } 246 | 247 | _hideCalendar () { 248 | document.activeElement.blur() 249 | } 250 | 251 | _calKeyDownEventHandler (event) { 252 | if (event.key === 'Enter') { 253 | this._dayClickedEventHandler(event) 254 | } 255 | } 256 | 257 | _blurHandler () { 258 | // When the input element loses focus due to click on calContainer, new focus won't be directly set to calContainer, it is set to body. 259 | // After calContainer onclick, focus will be on body unless following delay is introduced: 260 | setTimeout(() => { checkActiveElement(this) }, 0) 261 | function checkActiveElement (ctx) { 262 | if (!(document.activeElement.id === 'calContainer' || document.activeElement.classList.contains('calCtrl') || document.activeElement.classList.contains('calDay') || document.activeElement.isSameNode(ctx.textInputElement))) { 263 | ctx.calContainer.style.display = 'none' 264 | ctx._mouseUpEventHandler() 265 | if (!ctx._inputStrIsValidDate) { 266 | ctx.textInputElement.dispatchEvent(new Event('invalid')) 267 | } 268 | } 269 | } 270 | } 271 | 272 | _addHeaderEventHandlers () { 273 | var entries = this.calContainer.querySelectorAll('.calCtrl').entries() 274 | var entry = entries.next() 275 | while (entry.done === false) { 276 | entry.value[1].tabIndex = 0 277 | entry.value[1].onblur = this._blurHandler 278 | entry.value[1].onblur = entry.value[1].onblur.bind(this) 279 | entry.value[1].onclick = this._controlKeyDownEventHandler 280 | entry.value[1].onclick = entry.value[1].onclick.bind(this) 281 | entry.value[1].onkeydown = this._controlKeyDownEventHandler 282 | entry.value[1].onkeydown = entry.value[1].onkeydown.bind(this) 283 | entry.value[1].onmousedown = this._mouseDownEventHandler 284 | entry.value[1].onmousedown = entry.value[1].onmousedown.bind(this) 285 | entry.value[1].onmouseup = this._mouseUpEventHandler 286 | entry.value[1].onmouseup = entry.value[1].onmouseup.bind(this) 287 | entry.value[1].onmouseleave = this._mouseUpEventHandler 288 | entry.value[1].onmouseleave = entry.value[1].onmouseleave.bind(this) 289 | entry.value[1].ontouchstart = this._mouseDownEventHandler 290 | entry.value[1].ontouchstart = entry.value[1].ontouchstart.bind(this) 291 | entry.value[1].ontouchend = this._mouseUpEventHandler 292 | entry.value[1].ontouchend = entry.value[1].ontouchend.bind(this) 293 | entry.value[1].ontouchcancel = this._mouseUpEventHandler 294 | entry.value[1].ontouchcancel = entry.value[1].ontouchcancel.bind(this) 295 | entry = entries.next() 296 | } 297 | } 298 | 299 | _startLongPressAction (event) { 300 | this._longPressIntervalIds.push(setInterval(() => { this._controlKeyDownEventHandler(event) }, this.longPressInterval)) 301 | this.querySelector('#' + event.target.id).onclick = () => { this._onClickHandlerAfterLongPress(event, this) } 302 | } 303 | 304 | // For better UX, after long press, onclick must be discarded once, 305 | // thus do nothing with the event and set clickhandler back to the real one: 306 | _onClickHandlerAfterLongPress (event, ctx) { 307 | ctx.querySelector('#' + event.target.id).onclick = ctx._controlKeyDownEventHandler 308 | ctx.querySelector('#' + event.target.id).onclick = ctx.querySelector('#' + event.target.id).onclick.bind(ctx) 309 | } 310 | 311 | _mouseDownEventHandler (event) { 312 | this._longPressTimerIds.push(setTimeout(() => { this._startLongPressAction(event) }, this.longPressThreshold)) 313 | } 314 | 315 | _mouseUpEventHandler () { 316 | this._longPressTimerIds.forEach(clearTimeout) 317 | this._longPressTimerIds = [] 318 | this._longPressIntervalIds.forEach(clearInterval) 319 | this._longPressIntervalIds = [] 320 | } 321 | 322 | _parseAndValidateInputStr (str) { 323 | var obj = {} 324 | var day, month, year 325 | var value = str.match(/^\s*(\d{1,2})\.(\d{1,2})\.(\d\d\d\d)\s*$/) 326 | if (value === null) { 327 | obj.valid = false 328 | } else { 329 | day = Number(value[1]) 330 | month = Number(value[2]) 331 | year = Number(value[3]) 332 | if (this._dateIsValid(day, month, year)) { 333 | obj.valid = true 334 | obj.day = day 335 | obj.month = month - 1 336 | obj.year = year 337 | } else { 338 | obj.valid = false 339 | } 340 | } 341 | return obj 342 | } 343 | 344 | _inputOnInputHandler () { 345 | var obj = this._parseAndValidateInputStr(this.textInputElement.value) 346 | if (obj.valid) { 347 | this._inputStrIsValidDate = true 348 | this._setNewDateValue(obj.day, obj.month, obj.year) 349 | this.displayedMonth = obj.month 350 | this.displayedYear = obj.year 351 | this.textInputElement.dispatchEvent(new CustomEvent('dateselect')) 352 | this._renderCalendar() 353 | } else { 354 | this._inputStrIsValidDate = false 355 | } 356 | } 357 | 358 | _dateIsValid (day, month, year) { 359 | if (month < 1 || month > 12) { 360 | return false 361 | } 362 | var last_day_of_month = this._daysInMonth(month, year) 363 | if (day < 1 || day > last_day_of_month) { 364 | return false 365 | } 366 | return true 367 | } 368 | 369 | _controlKeyDownEventHandler (event) { 370 | if (event.key === 'Enter' || event.type !== 'keydown') { 371 | switch (event.target.id) { 372 | case 'calCtrlPrevYear': 373 | this._showPrevYear() 374 | break 375 | case 'calCtrlNextYear': 376 | this._showNextYear() 377 | break 378 | case 'calCtrlPrevMonth': 379 | this._showPrevMonth() 380 | break 381 | case 'calCtrlNextMonth': 382 | this._showNextMonth() 383 | break 384 | case 'calCtrlHideCal': 385 | this._hideCalendar() 386 | break 387 | } 388 | } 389 | } 390 | 391 | _inputOnFocusHandler () { 392 | this._inputOnInputHandler() 393 | this.calContainer.style.display = 'block' 394 | } 395 | 396 | _showNextYear () { 397 | this.displayedYear++ 398 | this._renderCalendar() 399 | } 400 | 401 | _showPrevYear () { 402 | this.displayedYear-- 403 | this._renderCalendar() 404 | } 405 | 406 | _showNextMonth () { 407 | if (this.displayedMonth === 11) { 408 | this.displayedMonth = 0 409 | this.displayedYear++ 410 | } else { 411 | this.displayedMonth++ 412 | } 413 | this._renderCalendar() 414 | } 415 | 416 | _showPrevMonth () { 417 | if (this.displayedMonth === 0) { 418 | this.displayedMonth = 11 419 | this.displayedYear-- 420 | } else { 421 | this.displayedMonth-- 422 | } 423 | this._renderCalendar() 424 | } 425 | 426 | _renderCalendar () { 427 | var tempDate = new Date(this.displayedYear, this.displayedMonth) 428 | tempDate.setDate(1) 429 | this.calTitle.innerHTML = this.monthNames[this.displayedMonth] + ' ' + this.displayedYear 430 | var dayNumbers = [] 431 | var adjacentMonthDays = [] 432 | this._generateDayArray(tempDate, dayNumbers, adjacentMonthDays) 433 | var entries = this.calContainer.querySelectorAll('.calDay').entries() 434 | var entry = entries.next() 435 | while (entry.done === false) { 436 | entry.value[1].classList.remove('calAdjacentMonthDay') 437 | entry.value[1].classList.remove('calSelectedDay') 438 | entry.value[1].classList.remove('calHiddenRow') 439 | entry.value[1].classList.remove('calDayStyle') 440 | entry.value[1].onclick = null 441 | entry.value[1].onblur = null 442 | entry.value[1].onkeydown = null 443 | if (adjacentMonthDays[entry.value[0]]) { 444 | entry.value[1].classList.add('calAdjacentMonthDay') 445 | } else { 446 | entry.value[1].classList.add('calDayStyle') 447 | } 448 | entry.value[1].innerHTML = dayNumbers[entry.value[0]] 449 | if (this.displayedMonth === this.dateObj.getMonth() && this.displayedYear === this.dateObj.getFullYear() && dayNumbers[entry.value[0]] === this.dateObj.getDate() && !adjacentMonthDays[entry.value[0]]) { 450 | entry.value[1].classList.add('calSelectedDay') 451 | } 452 | if (!adjacentMonthDays[entry.value[0]]) { 453 | entry.value[1].onclick = this._dayClickedEventHandler 454 | entry.value[1].onclick = entry.value[1].onclick.bind(this) 455 | entry.value[1].onkeydown = this._calKeyDownEventHandler 456 | entry.value[1].onkeydown = entry.value[1].onkeydown.bind(this) 457 | entry.value[1].tabIndex = 0 458 | entry.value[1].onblur = this._blurHandler 459 | entry.value[1].onblur = entry.value[1].onblur.bind(this) 460 | } else { 461 | entry.value[1].removeAttribute('tabindex') 462 | } 463 | entry = entries.next() 464 | } 465 | 466 | // checking if last (=lowest) row of days are all adjacent month days: 467 | var lastSeven = adjacentMonthDays.slice(35, 42) 468 | if (lastSeven.every(x => x === true)) { 469 | entries = this.calContainer.querySelectorAll('.calDay').entries() 470 | entry = entries.next() 471 | while (entry.done === false) { 472 | if (entry.value[0] > 34) { 473 | entry.value[1].classList.add('calHiddenRow') 474 | } 475 | entry = entries.next() 476 | } 477 | } 478 | } 479 | 480 | getDateString () { 481 | if (this._inputStrIsValidDate) { 482 | return this._returnDateString(this.dateObj) 483 | } 484 | return null 485 | } 486 | 487 | getDateObject () { 488 | if (this._inputStrIsValidDate) { 489 | return this.dateObj 490 | } 491 | return null 492 | } 493 | 494 | _setNewDateValue (day, month, year) { 495 | day = Number(day) 496 | month = Number(month) 497 | year = Number(year) 498 | if (day !== this.dateObj.getDate() || month !== this.dateObj.getMonth() || year !== this.dateObj.getFullYear()) { 499 | // Order is important, always set year first: 500 | this.dateObj.setFullYear(year) 501 | // Do not use setDate here: 502 | // this.dateObj.setDate(day) <-- https://stackoverflow.com/questions/14680396/the-date-getmonth-method-has-bug 503 | // Use setMonth with 2 params instead: 504 | this.dateObj.setMonth(month, day) 505 | } 506 | } 507 | 508 | _returnDateString (date) { 509 | var year = date.getFullYear() 510 | var month = date.getMonth() + 1 511 | var day = date.getDate() 512 | return day + '.' + month + '.' + year 513 | } 514 | 515 | _populateDayNames () { 516 | var dayNameArray = [] 517 | dayNameArray = this.dayNames.slice() 518 | if (this.sundayFirst) { 519 | dayNameArray.pop() 520 | dayNameArray.unshift(this.dayNames[6]) 521 | } 522 | var entries = this.calContainer.querySelectorAll('.calDayName').entries() 523 | var entry = entries.next() 524 | while (entry.done === false) { 525 | entry.value[1].innerHTML = dayNameArray[entry.value[0]] 526 | entry = entries.next() 527 | } 528 | } 529 | 530 | _generateDayArray (date, dayArray, adjacentMonthDaysArray) { 531 | var index 532 | var dateDay = date.getDay() 533 | var dateMonth = date.getMonth() + 1 534 | var dateYear = date.getFullYear() 535 | var daysInMonth = this._daysInMonth(dateMonth, dateYear) 536 | 537 | date.setDate(date.getDate() - 1) 538 | var prevMonth = date.getMonth() + 1 539 | var prevMonthYear = date.getFullYear() 540 | var daysInPrevMonth = this._daysInMonth(prevMonth, prevMonthYear) 541 | 542 | // prev month day filling: 543 | if (this.sundayFirst) { 544 | for (index = 0; index < dateDay; index++) { 545 | dayArray.unshift(daysInPrevMonth) 546 | daysInPrevMonth-- 547 | adjacentMonthDaysArray.push(true) 548 | } 549 | } else { 550 | if (dateDay === 0) { 551 | for (index = 0; index < 6; index++) { 552 | dayArray.unshift(daysInPrevMonth) 553 | daysInPrevMonth-- 554 | adjacentMonthDaysArray.push(true) 555 | } 556 | } else { 557 | for (index = 0; index < dateDay - 1; index++) { 558 | dayArray.unshift(daysInPrevMonth) 559 | daysInPrevMonth-- 560 | adjacentMonthDaysArray.push(true) 561 | } 562 | } 563 | } 564 | 565 | // current month day filling: 566 | for (index = 0; index < daysInMonth; index++) { 567 | dayArray.push(index + 1) 568 | adjacentMonthDaysArray.push(false) 569 | } 570 | 571 | // next month day filling: 572 | var numberOfNextMonthDays = 42 - dayArray.length 573 | for (index = 0; index < numberOfNextMonthDays; index++) { 574 | dayArray.push(index + 1) 575 | adjacentMonthDaysArray.push(true) 576 | } 577 | } 578 | 579 | _isItLeapYear (year) { 580 | return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0) 581 | } 582 | 583 | _daysInMonth (month, year) { 584 | if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { 585 | return 31 586 | } else if (month === 4 || month === 6 || month === 9 || month === 11) { 587 | return 30 588 | } else if (month === 2 && this._isItLeapYear(year)) { 589 | return 29 590 | } else if (month === 2 && !(this._isItLeapYear(year))) { 591 | return 28 592 | } 593 | } 594 | } 595 | 596 | customElements.define('wc-datepicker', Datepicker) 597 | -------------------------------------------------------------------------------- /dist/wc-datepicker.min.js: -------------------------------------------------------------------------------- 1 | class Datepicker extends HTMLElement{constructor(){super(),this.dayNames=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],this.monthNames=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],this.sundayFirst=!1,this.persistOnSelect=!1,this.longPressThreshold=500,this.longPressInterval=150,this.initDate=null,this.ignoreOnFocus=!1,this.showCloseIcon=!1,this.i=!1,this.t=[],this.s=[],this.l='
◄◄
►►
'}static get observedAttributes(){return["init-date","ignore-on-focus","sunday-first","persist-on-select","show-close-icon"]}disconnectedCallback(){}attributeChangedCallback(i,t,s){"init-date"===i?this.initDate=s:"ignore-on-focus"===i?this.ignoreOnFocus=!0:"sunday-first"===i?this.sundayFirst=!0:"persist-on-select"===i?this.persistOnSelect=!0:"show-close-icon"===i&&(this.showCloseIcon=!0)}connectedCallback(){setTimeout(()=>{this.init()},0)}init(){if(this.h=this.querySelector("input"),null===this.h)return;var i=document.createElement("div");i.style.display="inline-block",this.v&&this.v.remove(),this.v=this.appendChild(i);const t=document.createElement("template");var s;t.innerHTML=this.l,this.v.appendChild(this.h),this.v.appendChild(t.content),this.o=this.querySelector("#calTitle"),this.u=this.querySelector("#calContainer"),this.D=new Date,null!==this.initDate?(s=this.m(this.initDate)).valid?(this.D=new Date(s.p,s.C,s.g),this.i=!0,this.h.value=this._(this.D)):"current"===this.initDate&&(this.i=!0,this.h.value=this._(this.D)):(s=this.m(this.h.value)).valid?(this.D=new Date(s.p,s.C,s.g),this.i=!0):this.i=!1,this.initDate=null,this.N=this.D.getMonth(),this.k=this.D.getFullYear(),this.u.style.display="none",this.H(),this.M(),this.S(),this.ignoreOnFocus||(this.h.onfocus=this.A,this.h.onfocus=this.h.onfocus.bind(this)),this.h.oninput=this.T,this.h.oninput=this.h.oninput.bind(this),this.h.onblur=this.P,this.h.onblur=this.h.onblur.bind(this),this.u.onblur=this.P,this.u.onblur=this.u.onblur.bind(this),this.showCloseIcon||(this.querySelector("#calCtrlHideCal").style.display="none")}setFocusOnCal(){this.u&&(this.u.style.display="block",this.u.focus())}I(i){this.i=!0,this.Y(i.target.innerHTML,this.N,this.k),this.h.value=this._(this.D),this.h.dispatchEvent(new CustomEvent("dateselect")),this.S(),this.persistOnSelect||this.j()}j(){document.activeElement.blur()}O(i){"Enter"===i.key&&this.I(i)}P(){setTimeout(()=>{var i;i=this,"calContainer"===document.activeElement.id||document.activeElement.classList.contains("calCtrl")||document.activeElement.classList.contains("calDay")||document.activeElement.isSameNode(i.h)||(i.u.style.display="none",i.F(),i.i||i.h.dispatchEvent(new Event("invalid")))},0)}M(){for(var i=this.u.querySelectorAll(".calCtrl").entries(),t=i.next();!1===t.done;)t.value[1].tabIndex=0,t.value[1].onblur=this.P,t.value[1].onblur=t.value[1].onblur.bind(this),t.value[1].onclick=this.L,t.value[1].onclick=t.value[1].onclick.bind(this),t.value[1].onkeydown=this.L,t.value[1].onkeydown=t.value[1].onkeydown.bind(this),t.value[1].onmousedown=this.J,t.value[1].onmousedown=t.value[1].onmousedown.bind(this),t.value[1].onmouseup=this.F,t.value[1].onmouseup=t.value[1].onmouseup.bind(this),t.value[1].onmouseleave=this.F,t.value[1].onmouseleave=t.value[1].onmouseleave.bind(this),t.value[1].ontouchstart=this.J,t.value[1].ontouchstart=t.value[1].ontouchstart.bind(this),t.value[1].ontouchend=this.F,t.value[1].ontouchend=t.value[1].ontouchend.bind(this),t.value[1].ontouchcancel=this.F,t.value[1].ontouchcancel=t.value[1].ontouchcancel.bind(this),t=i.next()}R(i){this.t.push(setInterval(()=>{this.L(i)},this.longPressInterval)),this.querySelector("#"+i.target.id).onclick=()=>{this.V(i,this)}}V(i,t){t.querySelector("#"+i.target.id).onclick=t.L,t.querySelector("#"+i.target.id).onclick=t.querySelector("#"+i.target.id).onclick.bind(t)}J(i){this.s.push(setTimeout(()=>{this.R(i)},this.longPressThreshold))}F(){this.s.forEach(clearTimeout),this.s=[],this.t.forEach(clearInterval),this.t=[]}m(i){var t,s,a,l={},n=i.match(/^\s*(\d{1,2})\.(\d{1,2})\.(\d\d\d\d)\s*$/);return null===n?l.valid=!1:(t=Number(n[1]),s=Number(n[2]),a=Number(n[3]),this.G(t,s,a)?(l.valid=!0,l.g=t,l.C=s-1,l.p=a):l.valid=!1),l}T(){var i=this.m(this.h.value);i.valid?(this.i=!0,this.Y(i.g,i.C,i.p),this.N=i.C,this.k=i.p,this.h.dispatchEvent(new CustomEvent("dateselect")),this.S()):this.i=!1}G(i,t,s){if(t<1||t>12)return!1;var a=this.K(t,s);return!(i<1||i>a)}L(i){if("Enter"===i.key||"keydown"!==i.type)switch(i.target.id){case"calCtrlPrevYear":this.U();break;case"calCtrlNextYear":this.W();break;case"calCtrlPrevMonth":this.$();break;case"calCtrlNextMonth":this.q();break;case"calCtrlHideCal":this.j()}}A(){this.T(),this.u.style.display="block"}W(){this.k++,this.S()}U(){this.k--,this.S()}q(){11===this.N?(this.N=0,this.k++):this.N++,this.S()}$(){0===this.N?(this.N=11,this.k--):this.N--,this.S()}S(){var i=new Date(this.k,this.N);i.setDate(1),this.o.innerHTML=this.monthNames[this.N]+" "+this.k;var t=[],s=[];this.B(i,t,s);for(var a=this.u.querySelectorAll(".calDay").entries(),l=a.next();!1===l.done;)l.value[1].classList.remove("calAdjacentMonthDay"),l.value[1].classList.remove("calSelectedDay"),l.value[1].classList.remove("calHiddenRow"),l.value[1].classList.remove("calDayStyle"),l.value[1].onclick=null,l.value[1].onblur=null,l.value[1].onkeydown=null,s[l.value[0]]?l.value[1].classList.add("calAdjacentMonthDay"):l.value[1].classList.add("calDayStyle"),l.value[1].innerHTML=t[l.value[0]],this.N!==this.D.getMonth()||this.k!==this.D.getFullYear()||t[l.value[0]]!==this.D.getDate()||s[l.value[0]]||l.value[1].classList.add("calSelectedDay"),s[l.value[0]]?l.value[1].removeAttribute("tabindex"):(l.value[1].onclick=this.I,l.value[1].onclick=l.value[1].onclick.bind(this),l.value[1].onkeydown=this.O,l.value[1].onkeydown=l.value[1].onkeydown.bind(this),l.value[1].tabIndex=0,l.value[1].onblur=this.P,l.value[1].onblur=l.value[1].onblur.bind(this)),l=a.next();if(s.slice(35,42).every(i=>!0===i))for(l=(a=this.u.querySelectorAll(".calDay").entries()).next();!1===l.done;)l.value[0]>34&&l.value[1].classList.add("calHiddenRow"),l=a.next()}getDateString(){return this.i?this._(this.D):null}getDateObject(){return this.i?this.D:null}Y(i,t,s){i=Number(i),t=Number(t),s=Number(s),i===this.D.getDate()&&t===this.D.getMonth()&&s===this.D.getFullYear()||(this.D.setFullYear(s),this.D.setMonth(t,i))}_(i){var t=i.getFullYear(),s=i.getMonth()+1;return i.getDate()+"."+s+"."+t}H(){var i=[];i=this.dayNames.slice(),this.sundayFirst&&(i.pop(),i.unshift(this.dayNames[6]));for(var t=this.u.querySelectorAll(".calDayName").entries(),s=t.next();!1===s.done;)s.value[1].innerHTML=i[s.value[0]],s=t.next()}B(i,t,s){var a,l=i.getDay(),n=i.getMonth()+1,e=i.getFullYear(),c=this.K(n,e);i.setDate(i.getDate()-1);var h=i.getMonth()+1,d=i.getFullYear(),r=this.K(h,d);if(this.sundayFirst)for(a=0;a