├── .babelrc ├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── dist ├── better-dateinput-polyfill.js └── better-dateinput-polyfill.min.js ├── index.html ├── karma.conf.js ├── package.json ├── postcss.config.js ├── rollup.config.js ├── src ├── input.js ├── intl.js ├── picker.css ├── picker.js ├── polyfill.css ├── polyfill.js └── util.js └── test └── util.spec.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", {"loose": true}] 4 | ], 5 | "plugins": [ 6 | "html-tag" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UZ4SLQP8S4UUG&source=url 4 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm install 28 | - run: npm run build 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.sublime-workspace 3 | node_modules 4 | bower_components 5 | build 6 | coverage 7 | *.log -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Maksim Chemerisuk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `input[type=date]` polyfill 2 | 3 | [![NPM version][npm-version]][npm-url] [![NPM downloads][npm-downloads]][npm-url] [![Build Status][status-image]][status-url] [![Coverage Status][coveralls-image]][coveralls-url] [![Twitter][twitter-follow]][twitter-url] 4 | 5 | | [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)][donate-url] | Your help is appreciated. Create a PR, submit a bug or just grab me :beer: | 6 | |-|-| 7 | 8 | Why another date picker? The problem is that most of existing solutions do not follow standards regarding to `value` property format, that should have “a valid full-date as defined in [RFC 3339]”. In other words representation of date can vary, but the string value should have `yyyy-MM-dd` format. It helps to work with such values consistently regarding on the current language. 9 | 10 | [VIEW DEMO](http://chemerisuk.github.io/better-dateinput-polyfill/) 11 | 12 | ## Features 13 | 14 | * lightweight polyfill with no dependencies 15 | * works for initial and dynamic content elements 16 | * normalizes `input[type=date]` presentation for desktop browsers 17 | * submitted value always has standards based `yyyy-MM-dd` [RFC 3339] format 18 | * `placeholder` attribute works as expected 19 | * it's possible to change [displayed date value format](https://github.com/chemerisuk/better-dateinput-polyfill#change-default-date-presentation-format) 20 | * you are able to [control where to apply the polyfill](#forcing-the-polyfill) 21 | * keyboard and accessibility friendly 22 | 23 | ## Installation 24 | ```sh 25 | $ npm install better-dateinput-polyfill 26 | ``` 27 | 28 | Then append the following scripts to your page: 29 | ```html 30 | 31 | ``` 32 | 33 | ## Forcing the polyfill 34 | Sometimes it's useful to override browser implemetation with the consistent control implemented by the polyfill. To suppress feature detection you can add `` into your document ``. Value of `content` attribute is a media query where polyfill will be applied: 35 | 36 | ```html 37 | 38 | 39 | 40 | 41 | ``` 42 | 43 | ## Change default date presentation format 44 | When no spicified polyfill uses browser settings to format displayed date. You can override date presentation globally with `` via `content` attribute or directly on a HTML element with `data-format` attribute. Value should be [options for the Date#toLocaleString](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString) call as a stringified JSON object: 45 | ```html 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ``` 57 | 58 | ## Contributing 59 | Download git repository and install project dependencies: 60 | ```sh 61 | $ npm install 62 | ``` 63 | 64 | The project uses set of ES6 transpilers to compile the output file. Now use command below to start development: 65 | ```sh 66 | $ npm run watch 67 | ``` 68 | 69 | After any change file `build/better-dateinput-polyfill.js` is recompiled automatically. 70 | 71 | ## Browser support 72 | #### Desktop 73 | * Chrome 74 | * Safari 75 | * Firefox 76 | * Opera 77 | * Edge 78 | * Internet Explorer 10+ 79 | 80 | #### Mobile 81 | * iOS Safari 10+ 82 | * Chrome for Android 70+ 83 | 84 | [npm-url]: https://www.npmjs.com/package/better-dateinput-polyfill 85 | [npm-version]: https://img.shields.io/npm/v/better-dateinput-polyfill.svg 86 | [npm-downloads]: https://img.shields.io/npm/dm/better-dateinput-polyfill.svg 87 | 88 | [status-url]: https://github.com/chemerisuk/better-dateinput-polyfill/actions 89 | [status-image]: https://github.com/chemerisuk/better-dateinput-polyfill/workflows/Node.js%20CI/badge.svg?branch=master 90 | 91 | [coveralls-url]: https://coveralls.io/r/chemerisuk/better-dateinput-polyfill 92 | [coveralls-image]: http://img.shields.io/coveralls/chemerisuk/better-dateinput-polyfill/master.svg 93 | 94 | [twitter-url]: https://twitter.com/chemerisuk 95 | [twitter-follow]: https://img.shields.io/twitter/follow/chemerisuk.svg?style=social&label=Follow%20me 96 | 97 | [donate-url]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=UZ4SLQP8S4UUG&source=url -------------------------------------------------------------------------------- /dist/better-dateinput-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * better-dateinput-polyfill: input[type=date] polyfill 3 | * @version 4.0.0-beta.2 Sat, 17 Apr 2021 16:07:26 GMT 4 | * @link https://github.com/chemerisuk/better-dateinput-polyfill 5 | * @copyright 2021 Maksim Chemerisuk 6 | * @license MIT 7 | */ 8 | (function () { 9 | 'use strict'; 10 | 11 | var css_248z$1 = "@keyframes dateinput-polyfill{0%{opacity:.99};to{opacity:1};}input[type=date]{animation:dateinput-polyfill 1ms!important}dateinput-picker{background:#fff;box-shadow:0 8px 24px rgba(0,0,0,.2);height:360px;position:absolute;width:315px;z-index:2147483647}dateinput-picker[aria-hidden=true]{visibility:hidden}"; 12 | 13 | var css_248z = "body{cursor:default;font-family:system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:0}[aria-labelledby]{bottom:0;height:87.5vh;left:0;position:absolute;text-align:center;width:100%}[aria-labelledby][aria-hidden=true]{visibility:hidden}header{display:block;height:12.5vh;line-height:12.5vh;overflow:hidden;text-align:center}[role=button]{text-align:center;transition:transform 75ms ease-in;width:14.28571vw}[role=button][rel=prev]{float:left}[role=button][rel=prev]:active{transform:translateX(-2px)}[role=button][rel=next]{float:right}[role=button][rel=next]:active{transform:translateX(2px)}[role=button] svg{pointer-events:none;width:16px;height:100%}@media (hover:hover){[role=button]:hover{transform:scale(1.2)}}[aria-live=polite]{border:1px dotted transparent;color:#007bff;font-weight:700;margin:auto 0;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap}@media (hover:hover){[aria-live=polite]:hover{border-bottom-color:inherit}}table{border-spacing:0;table-layout:fixed}th{box-sizing:border-box;height:12.5vh;padding-bottom:8px;vertical-align:middle}td{border-radius:var(--border-radius);padding:0}td:not([aria-selected]){color:#ccc}td[aria-current=date]{font-weight:700}td[aria-disabled=true]{background-color:#ececec;border-radius:0;color:#ccc;cursor:not-allowed}#months,#years{box-sizing:border-box;float:left;height:100%;line-height:7.29167vh;list-style:none;margin:0;overflow-x:hidden;overflow-y:scroll;padding:0 4px;width:50%}@media (hover:hover){[data-date]:hover,[data-month]:hover,[data-year]:hover{background-color:#ececec}}[data-date][aria-selected=true],[data-month][aria-selected=true],[data-year][aria-selected=true]{background-color:#007bff;color:#fff}"; 14 | 15 | var WINDOW = window; 16 | var DOCUMENT = document; 17 | var HTML = DOCUMENT.documentElement; 18 | var IE = ("ScriptEngineMajorVersion" in WINDOW); 19 | function $(element, selector) { 20 | return Array.prototype.slice.call(element.querySelectorAll(selector), 0); 21 | } 22 | function repeat(times, fn) { 23 | if (typeof fn === "string") { 24 | return Array(times + 1).join(fn); 25 | } else { 26 | return Array.apply(null, Array(times)).map(fn).join(""); 27 | } 28 | } 29 | function svgIcon(path) { 30 | return ""; 31 | } 32 | function injectStyles(cssText, head) { 33 | var style = DOCUMENT.createElement("style"); 34 | style.type = "text/css"; 35 | style.innerHTML = cssText; 36 | 37 | if (head.firstChild) { 38 | head.insertBefore(style, head.firstChild); 39 | } else { 40 | head.appendChild(style); 41 | } 42 | } 43 | 44 | var INTL_SUPPORTED = function () { 45 | try { 46 | new Date().toLocaleString("_"); 47 | } catch (err) { 48 | return err instanceof RangeError; 49 | } 50 | 51 | return false; 52 | }(); 53 | 54 | function parseLocaleDate(value) { 55 | var _split$map = (value || "?").split(/\D/).map(function (s) { 56 | return parseInt(s); 57 | }), 58 | year = _split$map[0], 59 | month = _split$map[1], 60 | date = _split$map[2]; // set hours to 12 because otherwise Safari doesn't return 61 | // correct result string for toLocaleString calls 62 | 63 | 64 | var dateValue = new Date(year, month - 1, date, 12, 0); 65 | return isNaN(dateValue.getTime()) ? null : dateValue; 66 | } 67 | function formatLocaleDate(date) { 68 | return [date.getFullYear(), ("0" + (date.getMonth() + 1)).slice(-2), ("0" + date.getDate()).slice(-2)].join("-"); 69 | } 70 | function getFormatOptions(locale, formatString) { 71 | if (!INTL_SUPPORTED) return {}; 72 | var dateTimeFormat; 73 | 74 | try { 75 | // We perform severals checks here: 76 | // 1) verify lang attribute is supported by browser 77 | // 2) verify format options are valid 78 | dateTimeFormat = new Intl.DateTimeFormat(locale, JSON.parse(formatString || "{}")); 79 | } catch (err) { 80 | console.warn("Fallback to default date format because of error:", err); // fallback to default date format options 81 | 82 | dateTimeFormat = new Intl.DateTimeFormat(); 83 | } 84 | 85 | return dateTimeFormat.resolvedOptions(); 86 | } 87 | function localeWeekday(value, options) { 88 | var date = new Date(1971, 1, value + (options.hour12 ? 0 : 1)); 89 | /* istanbul ignore else */ 90 | 91 | if (INTL_SUPPORTED) { 92 | return date.toLocaleString(options.locale, { 93 | weekday: "short" 94 | }); 95 | } else { 96 | return date.toUTCString().split(",")[0].slice(0, 2); 97 | } 98 | } 99 | function localeMonth(value, options) { 100 | var date = new Date(25e8 * (value + 1)); 101 | /* istanbul ignore else */ 102 | 103 | if (INTL_SUPPORTED) { 104 | return date.toLocaleString(options.locale, { 105 | month: "short" 106 | }); 107 | } else { 108 | return date.toUTCString().split(" ")[2]; 109 | } 110 | } 111 | function localeDate(value, options) { 112 | if (INTL_SUPPORTED) { 113 | return value.toLocaleString(options.locale, options); 114 | } else { 115 | return value.toUTCString().split(" ").slice(0, 4).join(" "); 116 | } 117 | } 118 | function localeMonthYear(value, options) { 119 | if (INTL_SUPPORTED) { 120 | return value.toLocaleString(options.locale, { 121 | month: "long", 122 | year: "numeric" 123 | }); 124 | } else { 125 | return value.toUTCString().split(" ").slice(2, 4).join(" "); 126 | } 127 | } 128 | 129 | var DatePickerImpl = /*#__PURE__*/function () { 130 | function DatePickerImpl(input, formatOptions) { 131 | this._input = input; 132 | this._formatOptions = formatOptions; 133 | 134 | this._initPicker(); 135 | } 136 | 137 | var _proto = DatePickerImpl.prototype; 138 | 139 | _proto._initPicker = function _initPicker() { 140 | var _this = this; 141 | 142 | this._picker = DOCUMENT.createElement("dateinput-picker"); 143 | 144 | this._picker.setAttribute("aria-hidden", true); 145 | 146 | this._input.parentNode.insertBefore(this._picker, this._input); 147 | 148 | var object = DOCUMENT.createElement("object"); 149 | object.type = "text/html"; 150 | object.width = "100%"; 151 | object.height = "100%"; // non-IE: must be BEFORE the element added to the document 152 | 153 | if (!IE) { 154 | object.data = "about:blank"; 155 | } // load content when is ready 156 | 157 | 158 | object.onload = function (event) { 159 | _this._initContent(event.target.contentDocument); // this is a one time event handler 160 | 161 | 162 | delete object.onload; 163 | }; // add object element to the document 164 | 165 | 166 | this._picker.appendChild(object); // IE: must be AFTER the element added to the document 167 | 168 | 169 | if (IE) { 170 | object.data = "about:blank"; 171 | } 172 | }; 173 | 174 | _proto._initContent = function _initContent(pickerRoot) { 175 | var _this2 = this; 176 | 177 | var defaultYearDelta = 30; 178 | var now = new Date(); 179 | 180 | var minDate = this._getLimitationDate("min"); 181 | 182 | var maxDate = this._getLimitationDate("max"); 183 | 184 | var startYear = minDate ? minDate.getFullYear() : now.getFullYear() - defaultYearDelta; 185 | var endYear = maxDate ? maxDate.getFullYear() : now.getFullYear() + defaultYearDelta; // append picker HTML to shadow dom 186 | 187 | pickerRoot.body.innerHTML = "
" + svgIcon("M11.5 14.06L1 8L11.5 1.94z") + " " + svgIcon("M15 8L4.5 14.06L4.5 1.94z") + "
" + repeat(7, function (_, i) { 188 | return ""; 189 | }) + "" + repeat(6, "" + repeat(7, "") + "
" + localeWeekday(i, _this2._formatOptions) + "
") + "
    " + repeat(12, function (_, i) { 190 | return "
  1. " + localeMonth(i, _this2._formatOptions); 191 | }) + "
    " + repeat(endYear - startYear + 1, function (_, i) { 192 | return "
  1. " + (startYear + i); 193 | }) + "
"; 194 | injectStyles(css_248z, pickerRoot.head); 195 | this._caption = $(pickerRoot, "[aria-live=polite]")[0]; 196 | this._pickers = $(pickerRoot, "[aria-labelledby]"); 197 | pickerRoot.addEventListener("mousedown", this._onMouseDown.bind(this)); 198 | pickerRoot.addEventListener("contextmenu", function (event) { 199 | return event.preventDefault(); 200 | }); 201 | pickerRoot.addEventListener("dblclick", function (event) { 202 | return event.preventDefault(); 203 | }); 204 | this.show(); 205 | }; 206 | 207 | _proto._getLimitationDate = function _getLimitationDate(name) { 208 | if (this._input) { 209 | return parseLocaleDate(this._input.getAttribute(name)); 210 | } else { 211 | return null; 212 | } 213 | }; 214 | 215 | _proto._onMouseDown = function _onMouseDown(event) { 216 | var target = event.target; // disable default behavior so input doesn't loose focus 217 | 218 | event.preventDefault(); // skip right/middle mouse button clicks 219 | 220 | if (event.button) return; 221 | 222 | if (target === this._caption) { 223 | this._togglePickerMode(); 224 | } else if (target.getAttribute("role") === "button") { 225 | this._clickButton(target); 226 | } else if (target.hasAttribute("data-date")) { 227 | this._clickDate(target); 228 | } else if (target.hasAttribute("data-month") || target.hasAttribute("data-year")) { 229 | this._clickMonthYear(target); 230 | } 231 | }; 232 | 233 | _proto._clickButton = function _clickButton(target) { 234 | var captionDate = this.getCaptionDate(); 235 | var sign = target.getAttribute("rel") === "prev" ? -1 : 1; 236 | var advancedMode = this.isAdvancedMode(); 237 | 238 | if (advancedMode) { 239 | captionDate.setFullYear(captionDate.getFullYear() + sign); 240 | } else { 241 | captionDate.setMonth(captionDate.getMonth() + sign); 242 | } 243 | 244 | if (this.isValidValue(captionDate)) { 245 | this.render(captionDate); 246 | 247 | if (advancedMode) { 248 | this._input.valueAsDate = captionDate; 249 | } 250 | } 251 | }; 252 | 253 | _proto._clickDate = function _clickDate(target) { 254 | if (target.getAttribute("aria-disabled") !== "true") { 255 | this._input.value = target.getAttribute("data-date"); 256 | this.hide(); 257 | } 258 | }; 259 | 260 | _proto._clickMonthYear = function _clickMonthYear(target) { 261 | var month = parseInt(target.getAttribute("data-month")); 262 | var year = parseInt(target.getAttribute("data-year")); 263 | 264 | if (month >= 0 || year >= 0) { 265 | var captionDate = this.getCaptionDate(); 266 | 267 | if (!isNaN(month)) { 268 | captionDate.setMonth(month); 269 | } 270 | 271 | if (!isNaN(year)) { 272 | captionDate.setFullYear(year); 273 | } 274 | 275 | if (this.isValidValue(captionDate)) { 276 | this._renderAdvancedPicker(captionDate, false); 277 | 278 | this._input.valueAsDate = captionDate; 279 | } 280 | } 281 | }; 282 | 283 | _proto._togglePickerMode = function _togglePickerMode() { 284 | var _this3 = this; 285 | 286 | this._pickers.forEach(function (element, index) { 287 | var currentDate = _this3._input.valueAsDate || new Date(); 288 | var hidden = element.getAttribute("aria-hidden") === "true"; 289 | 290 | if (index === 0) { 291 | if (hidden) { 292 | _this3._renderCalendarPicker(currentDate); 293 | } 294 | } else { 295 | if (hidden) { 296 | _this3._renderAdvancedPicker(currentDate); 297 | } 298 | } 299 | 300 | element.setAttribute("aria-hidden", !hidden); 301 | }); 302 | }; 303 | 304 | _proto._renderCalendarPicker = function _renderCalendarPicker(captionDate) { 305 | var now = new Date(); 306 | var currentDate = this._input.valueAsDate; 307 | 308 | var minDate = this._getLimitationDate("min"); 309 | 310 | var maxDate = this._getLimitationDate("max"); 311 | 312 | var iterDate = new Date(captionDate.getFullYear(), captionDate.getMonth()); // move to beginning of the first week in current month 313 | 314 | iterDate.setDate((this._formatOptions.hour12 ? 0 : iterDate.getDay() === 0 ? -6 : 1) - iterDate.getDay()); 315 | $(this._pickers[0], "td").forEach(function (cell) { 316 | iterDate.setDate(iterDate.getDate() + 1); 317 | var iterDateStr = formatLocaleDate(iterDate); 318 | 319 | if (iterDate.getMonth() === captionDate.getMonth()) { 320 | if (currentDate && iterDateStr === formatLocaleDate(currentDate)) { 321 | cell.setAttribute("aria-selected", true); 322 | } else { 323 | cell.setAttribute("aria-selected", false); 324 | } 325 | } else { 326 | cell.removeAttribute("aria-selected"); 327 | } 328 | 329 | if (iterDateStr === formatLocaleDate(now)) { 330 | cell.setAttribute("aria-current", "date"); 331 | } else { 332 | cell.removeAttribute("aria-current"); 333 | } 334 | 335 | if (minDate && iterDate < minDate || maxDate && iterDate > maxDate) { 336 | cell.setAttribute("aria-disabled", true); 337 | } else { 338 | cell.removeAttribute("aria-disabled"); 339 | } 340 | 341 | cell.textContent = iterDate.getDate(); 342 | cell.setAttribute("data-date", iterDateStr); 343 | }); // update visible caption value 344 | 345 | this.setCaptionDate(captionDate); 346 | }; 347 | 348 | _proto._renderAdvancedPicker = function _renderAdvancedPicker(captionDate, syncScroll) { 349 | if (syncScroll === void 0) { 350 | syncScroll = true; 351 | } 352 | 353 | $(this._pickers[1], "[aria-selected]").forEach(function (selectedElement) { 354 | selectedElement.removeAttribute("aria-selected"); 355 | }); 356 | 357 | if (captionDate) { 358 | var monthItem = $(this._pickers[1], "[data-month=\"" + captionDate.getMonth() + "\"]")[0]; 359 | var yearItem = $(this._pickers[1], "[data-year=\"" + captionDate.getFullYear() + "\"]")[0]; 360 | monthItem.setAttribute("aria-selected", true); 361 | yearItem.setAttribute("aria-selected", true); 362 | 363 | if (syncScroll) { 364 | monthItem.parentNode.scrollTop = monthItem.offsetTop; 365 | yearItem.parentNode.scrollTop = yearItem.offsetTop; 366 | } // update visible caption value 367 | 368 | 369 | this.setCaptionDate(captionDate); 370 | } 371 | }; 372 | 373 | _proto.isValidValue = function isValidValue(dateValue) { 374 | var minDate = this._getLimitationDate("min"); 375 | 376 | var maxDate = this._getLimitationDate("max"); 377 | 378 | return !(minDate && dateValue < minDate || maxDate && dateValue > maxDate); 379 | }; 380 | 381 | _proto.isAdvancedMode = function isAdvancedMode() { 382 | return this._pickers[0].getAttribute("aria-hidden") === "true"; 383 | }; 384 | 385 | _proto.getCaptionDate = function getCaptionDate() { 386 | return new Date(this._caption.getAttribute("datetime")); 387 | }; 388 | 389 | _proto.setCaptionDate = function setCaptionDate(captionDate) { 390 | this._caption.textContent = localeMonthYear(captionDate, this._formatOptions); 391 | 392 | this._caption.setAttribute("datetime", captionDate.toISOString()); 393 | }; 394 | 395 | _proto.isHidden = function isHidden() { 396 | return this._picker.getAttribute("aria-hidden") === "true"; 397 | }; 398 | 399 | _proto.show = function show() { 400 | if (this.isHidden()) { 401 | var startElement = this._input; 402 | 403 | var pickerOffset = this._picker.getBoundingClientRect(); 404 | 405 | var inputOffset = startElement.getBoundingClientRect(); // set picker position depending on current visible area 406 | 407 | var marginTop = inputOffset.height; 408 | 409 | if (HTML.clientHeight < inputOffset.bottom + pickerOffset.height) { 410 | marginTop = -pickerOffset.height; 411 | } 412 | 413 | this._picker.style.marginTop = marginTop + "px"; 414 | 415 | this._renderCalendarPicker(this._input.valueAsDate || new Date()); // display picker 416 | 417 | 418 | this._picker.removeAttribute("aria-hidden"); 419 | } 420 | }; 421 | 422 | _proto.hide = function hide() { 423 | this._picker.setAttribute("aria-hidden", true); 424 | 425 | this.reset(); 426 | }; 427 | 428 | _proto.reset = function reset() { 429 | this._pickers.forEach(function (element, index) { 430 | element.setAttribute("aria-hidden", !!index); 431 | }); 432 | }; 433 | 434 | _proto.render = function render(captionDate) { 435 | if (this.isAdvancedMode()) { 436 | this._renderAdvancedPicker(captionDate); 437 | } else { 438 | this._renderCalendarPicker(captionDate); 439 | } 440 | }; 441 | 442 | return DatePickerImpl; 443 | }(); 444 | 445 | var formatMeta = $(DOCUMENT, "meta[name=dateinput-polyfill-format]")[0]; 446 | var DateInputPolyfill = /*#__PURE__*/function () { 447 | function DateInputPolyfill(input) { 448 | this._input = input; 449 | this._valueInput = this._createValueInput(input); 450 | this._formatOptions = this._createFormatOptions(); 451 | 452 | this._input.addEventListener("focus", this._showPicker.bind(this)); 453 | 454 | this._input.addEventListener("click", this._showPicker.bind(this)); 455 | 456 | this._input.addEventListener("blur", this._hidePicker.bind(this)); 457 | 458 | this._input.addEventListener("keydown", this._onKeydown.bind(this)); 459 | 460 | this._initInput(); 461 | } 462 | 463 | var _proto = DateInputPolyfill.prototype; 464 | 465 | _proto._initInput = function _initInput() { 466 | var valueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value"); // redefine value property for input 467 | 468 | Object.defineProperty(this._input, "value", { 469 | configurable: false, 470 | enumerable: true, 471 | get: this._getValue.bind(this), 472 | set: this._setValue.bind(this, valueDescriptor.set) 473 | }); // redefine valueAsDate property for input 474 | 475 | Object.defineProperty(this._input, "valueAsDate", { 476 | configurable: false, 477 | enumerable: true, 478 | get: this._getDate.bind(this), 479 | set: this._setDate.bind(this, valueDescriptor.set) 480 | }); // change input type to remove built-in picker 481 | 482 | this._input.type = "text"; // do not popup keyboard on mobile devices 483 | 484 | this._input.setAttribute("inputmode", "none"); // need to set readonly attribute as well to prevent 485 | // visible date modification with the cut feature 486 | 487 | 488 | this._input.readOnly = true; // update visible value in text input 489 | 490 | this._input.value = this._getValue(); // update default visible value to formatted date 491 | 492 | this._input.defaultValue = valueDescriptor.get.call(this._input); 493 | }; 494 | 495 | _proto._getValue = function _getValue() { 496 | return this._valueInput.value; 497 | }; 498 | 499 | _proto._setValue = function _setValue(setter, stringValue) { 500 | this._setDate(setter, parseLocaleDate(stringValue)); 501 | }; 502 | 503 | _proto._getDate = function _getDate() { 504 | return parseLocaleDate(this._getValue()); 505 | }; 506 | 507 | _proto._setDate = function _setDate(setter, dateValue) { 508 | setter.call(this._input, dateValue && localeDate(dateValue, this._formatOptions) || ""); 509 | setter.call(this._valueInput, dateValue && formatLocaleDate(dateValue) || ""); 510 | }; 511 | 512 | _proto._createValueInput = function _createValueInput(input) { 513 | var valueInput = DOCUMENT.createElement("input"); 514 | valueInput.style.display = "none"; 515 | valueInput.setAttribute("hidden", ""); 516 | valueInput.disabled = input.disabled; 517 | 518 | if (input.name) { 519 | valueInput.name = input.name; 520 | input.removeAttribute("name"); 521 | } 522 | 523 | if (input.value) { 524 | valueInput.value = valueInput.defaultValue = input.value; 525 | } 526 | 527 | if (input.hasAttribute("form")) { 528 | valueInput.setAttribute("form", input.getAttribute("form")); 529 | } 530 | 531 | return input.parentNode.insertBefore(valueInput, input.nextSibling); 532 | }; 533 | 534 | _proto._createFormatOptions = function _createFormatOptions() { 535 | var locale = this._input.lang || HTML.lang; 536 | 537 | var formatString = this._input.getAttribute("data-format"); 538 | 539 | if (!formatString && formatMeta) { 540 | formatString = formatMeta.content; 541 | } 542 | 543 | return getFormatOptions(locale, formatString); 544 | }; 545 | 546 | _proto._onKeydown = function _onKeydown(event) { 547 | var key = event.key; 548 | 549 | if (key === "Enter") { 550 | if (!this._pickerApi.isHidden()) { 551 | event.preventDefault(); 552 | 553 | this._hidePicker(); 554 | } 555 | } else if (key === " ") { 556 | // disable scroll change 557 | event.preventDefault(); 558 | 559 | this._showPicker(); 560 | } else if (key === "Backspace") { 561 | // prevent browser back navigation 562 | event.preventDefault(); 563 | this._input.value = ""; 564 | 565 | this._pickerApi.reset(); 566 | 567 | this._pickerApi.render(new Date()); 568 | } else { 569 | var offset = 0; 570 | 571 | if (key === "ArrowDown" || key === "Down") { 572 | offset = 7; 573 | } else if (key === "ArrowUp" || key === "Up") { 574 | offset = -7; 575 | } else if (key === "ArrowLeft" || key === "Left") { 576 | offset = -1; 577 | } else if (key === "ArrowRight" || key === "Right") { 578 | offset = 1; 579 | } 580 | 581 | if (!offset) return; // disable scroll change on arrows 582 | 583 | event.preventDefault(); 584 | 585 | var captionDate = this._pickerApi.getCaptionDate(); 586 | 587 | if (this._pickerApi.isAdvancedMode()) { 588 | if (Math.abs(offset) === 7) { 589 | captionDate.setMonth(captionDate.getMonth() + offset / 7); 590 | } else { 591 | captionDate.setFullYear(captionDate.getFullYear() + offset); 592 | } 593 | } else { 594 | captionDate.setDate(captionDate.getDate() + offset); 595 | } 596 | 597 | if (this._pickerApi.isValidValue(captionDate)) { 598 | this._input.valueAsDate = captionDate; 599 | 600 | this._pickerApi.render(captionDate); 601 | } 602 | } 603 | }; 604 | 605 | _proto._showPicker = function _showPicker() { 606 | if (!this._pickerApi) { 607 | this._pickerApi = new DatePickerImpl(this._input, this._formatOptions); 608 | } else { 609 | this._pickerApi.show(); 610 | } 611 | }; 612 | 613 | _proto._hidePicker = function _hidePicker() { 614 | this._pickerApi.hide(); 615 | }; 616 | 617 | return DateInputPolyfill; 618 | }(); 619 | 620 | var ANIMATION_NAME = "dateinput-polyfill"; 621 | var PROPERTY_NAME = "__" + ANIMATION_NAME + "__"; 622 | 623 | function isDateInputSupported() { 624 | // use a stronger type support detection that handles old WebKit browsers: 625 | // http://www.quirksmode.org/blog/archives/2015/03/better_modern_i.html 626 | var input = DOCUMENT.createElement("input"); 627 | input.type = "date"; 628 | input.value = "_"; 629 | return input.value !== "_"; 630 | } 631 | 632 | var mediaMeta = $(DOCUMENT, "meta[name=dateinput-polyfill-media]")[0]; 633 | 634 | if (mediaMeta ? WINDOW.matchMedia(mediaMeta.content) : IE || !isDateInputSupported()) { 635 | // inject style rules with fake animation 636 | injectStyles(css_248z$1, DOCUMENT.head); // attach listener to catch all fake animation starts 637 | 638 | DOCUMENT.addEventListener("animationstart", function (event) { 639 | if (event.animationName === ANIMATION_NAME) { 640 | var input = event.target; 641 | 642 | if (!input[PROPERTY_NAME]) { 643 | input[PROPERTY_NAME] = new DateInputPolyfill(input); 644 | } 645 | } 646 | }); 647 | } 648 | 649 | }()); 650 | -------------------------------------------------------------------------------- /dist/better-dateinput-polyfill.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * better-dateinput-polyfill: input[type=date] polyfill 3 | * @version 4.0.0-beta.2 Sat, 17 Apr 2021 16:07:26 GMT 4 | * @link https://github.com/chemerisuk/better-dateinput-polyfill 5 | * @copyright 2021 Maksim Chemerisuk 6 | * @license MIT 7 | */ 8 | !function(){"use strict";var t=window,e=document,i=e.documentElement,a="ScriptEngineMajorVersion"in t;function n(t,e){return Array.prototype.slice.call(t.querySelectorAll(e),0)}function r(t,e){return"string"==typeof e?Array(t+1).join(e):Array.apply(null,Array(t)).map(e).join("")}function o(t){return''}function s(t,i){var a=e.createElement("style");a.type="text/css",a.innerHTML=t,i.firstChild?i.insertBefore(a,i.firstChild):i.appendChild(a)}var l=function(){try{(new Date).toLocaleString("_")}catch(t){return t instanceof RangeError}return!1}();function u(t){var e=(t||"?").split(/\D/).map((function(t){return parseInt(t)})),i=e[0],a=e[1],n=e[2],r=new Date(i,a-1,n,12,0);return isNaN(r.getTime())?null:r}function d(t){return[t.getFullYear(),("0"+(t.getMonth()+1)).slice(-2),("0"+t.getDate()).slice(-2)].join("-")}var h=function(){function t(t,e){this._input=t,this._formatOptions=e,this._initPicker()}var h=t.prototype;return h._initPicker=function(){var t=this;this._picker=e.createElement("dateinput-picker"),this._picker.setAttribute("aria-hidden",!0),this._input.parentNode.insertBefore(this._picker,this._input);var i=e.createElement("object");i.type="text/html",i.width="100%",i.height="100%",a||(i.data="about:blank"),i.onload=function(e){t._initContent(e.target.contentDocument),delete i.onload},this._picker.appendChild(i),a&&(i.data="about:blank")},h._initContent=function(t){var e=this,i=new Date,a=this._getLimitationDate("min"),u=this._getLimitationDate("max"),d=a?a.getFullYear():i.getFullYear()-30,h=u?u.getFullYear():i.getFullYear()+30;t.body.innerHTML='
'+r(7,(function(t,i){return"");var a,n,r}))+''+r(6,""+r(7,"")+'
"+(a=i,n=e._formatOptions,r=new Date(1971,1,a+(n.hour12?0:1)),(l?r.toLocaleString(n.locale,{weekday:"short"}):r.toUTCString().split(",")[0].slice(0,2))+"
")+"
",s("body{cursor:default;font-family:system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:0}[aria-labelledby]{bottom:0;height:87.5vh;left:0;position:absolute;text-align:center;width:100%}[aria-labelledby][aria-hidden=true]{visibility:hidden}header{display:block;height:12.5vh;line-height:12.5vh;overflow:hidden;text-align:center}[role=button]{text-align:center;transition:transform 75ms ease-in;width:14.28571vw}[role=button][rel=prev]{float:left}[role=button][rel=prev]:active{transform:translateX(-2px)}[role=button][rel=next]{float:right}[role=button][rel=next]:active{transform:translateX(2px)}[role=button] svg{pointer-events:none;width:16px;height:100%}@media (hover:hover){[role=button]:hover{transform:scale(1.2)}}[aria-live=polite]{border:1px dotted transparent;color:#007bff;font-weight:700;margin:auto 0;overflow:hidden;text-align:center;text-overflow:ellipsis;white-space:nowrap}@media (hover:hover){[aria-live=polite]:hover{border-bottom-color:inherit}}table{border-spacing:0;table-layout:fixed}th{box-sizing:border-box;height:12.5vh;padding-bottom:8px;vertical-align:middle}td{border-radius:var(--border-radius);padding:0}td:not([aria-selected]){color:#ccc}td[aria-current=date]{font-weight:700}td[aria-disabled=true]{background-color:#ececec;border-radius:0;color:#ccc;cursor:not-allowed}#months,#years{box-sizing:border-box;float:left;height:100%;line-height:7.29167vh;list-style:none;margin:0;overflow-x:hidden;overflow-y:scroll;padding:0 4px;width:50%}@media (hover:hover){[data-date]:hover,[data-month]:hover,[data-year]:hover{background-color:#ececec}}[data-date][aria-selected=true],[data-month][aria-selected=true],[data-year][aria-selected=true]{background-color:#007bff;color:#fff}",t.head),this._caption=n(t,"[aria-live=polite]")[0],this._pickers=n(t,"[aria-labelledby]"),t.addEventListener("mousedown",this._onMouseDown.bind(this)),t.addEventListener("contextmenu",(function(t){return t.preventDefault()})),t.addEventListener("dblclick",(function(t){return t.preventDefault()})),this.show()},h._getLimitationDate=function(t){return this._input?u(this._input.getAttribute(t)):null},h._onMouseDown=function(t){var e=t.target;t.preventDefault(),t.button||(e===this._caption?this._togglePickerMode():"button"===e.getAttribute("role")?this._clickButton(e):e.hasAttribute("data-date")?this._clickDate(e):(e.hasAttribute("data-month")||e.hasAttribute("data-year"))&&this._clickMonthYear(e))},h._clickButton=function(t){var e=this.getCaptionDate(),i="prev"===t.getAttribute("rel")?-1:1,a=this.isAdvancedMode();a?e.setFullYear(e.getFullYear()+i):e.setMonth(e.getMonth()+i),this.isValidValue(e)&&(this.render(e),a&&(this._input.valueAsDate=e))},h._clickDate=function(t){"true"!==t.getAttribute("aria-disabled")&&(this._input.value=t.getAttribute("data-date"),this.hide())},h._clickMonthYear=function(t){var e=parseInt(t.getAttribute("data-month")),i=parseInt(t.getAttribute("data-year"));if(e>=0||i>=0){var a=this.getCaptionDate();isNaN(e)||a.setMonth(e),isNaN(i)||a.setFullYear(i),this.isValidValue(a)&&(this._renderAdvancedPicker(a,!1),this._input.valueAsDate=a)}},h._togglePickerMode=function(){var t=this;this._pickers.forEach((function(e,i){var a=t._input.valueAsDate||new Date,n="true"===e.getAttribute("aria-hidden");0===i?n&&t._renderCalendarPicker(a):n&&t._renderAdvancedPicker(a),e.setAttribute("aria-hidden",!n)}))},h._renderCalendarPicker=function(t){var e=new Date,i=this._input.valueAsDate,a=this._getLimitationDate("min"),r=this._getLimitationDate("max"),o=new Date(t.getFullYear(),t.getMonth());o.setDate((this._formatOptions.hour12?0:0===o.getDay()?-6:1)-o.getDay()),n(this._pickers[0],"td").forEach((function(n){o.setDate(o.getDate()+1);var s=d(o);o.getMonth()===t.getMonth()?i&&s===d(i)?n.setAttribute("aria-selected",!0):n.setAttribute("aria-selected",!1):n.removeAttribute("aria-selected"),s===d(e)?n.setAttribute("aria-current","date"):n.removeAttribute("aria-current"),a&&or?n.setAttribute("aria-disabled",!0):n.removeAttribute("aria-disabled"),n.textContent=o.getDate(),n.setAttribute("data-date",s)})),this.setCaptionDate(t)},h._renderAdvancedPicker=function(t,e){if(void 0===e&&(e=!0),n(this._pickers[1],"[aria-selected]").forEach((function(t){t.removeAttribute("aria-selected")})),t){var i=n(this._pickers[1],'[data-month="'+t.getMonth()+'"]')[0],a=n(this._pickers[1],'[data-year="'+t.getFullYear()+'"]')[0];i.setAttribute("aria-selected",!0),a.setAttribute("aria-selected",!0),e&&(i.parentNode.scrollTop=i.offsetTop,a.parentNode.scrollTop=a.offsetTop),this.setCaptionDate(t)}},h.isValidValue=function(t){var e=this._getLimitationDate("min"),i=this._getLimitationDate("max");return!(e&&ti)},h.isAdvancedMode=function(){return"true"===this._pickers[0].getAttribute("aria-hidden")},h.getCaptionDate=function(){return new Date(this._caption.getAttribute("datetime"))},h.setCaptionDate=function(t){var e,i;this._caption.textContent=(e=t,i=this._formatOptions,l?e.toLocaleString(i.locale,{month:"long",year:"numeric"}):e.toUTCString().split(" ").slice(2,4).join(" ")),this._caption.setAttribute("datetime",t.toISOString())},h.isHidden=function(){return"true"===this._picker.getAttribute("aria-hidden")},h.show=function(){if(this.isHidden()){var t=this._input,e=this._picker.getBoundingClientRect(),a=t.getBoundingClientRect(),n=a.height;i.clientHeight 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | better-dateinput-polyfill demo 10 | 30 | 31 | 32 |
33 |

34 | 35 | 36 |

37 |

38 | 39 | 40 |

41 |

42 | 43 | 44 |

45 |

46 | 47 | 48 |

49 |

50 | 51 | 52 |

53 |

54 | 55 | 56 |

57 | 58 | 59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | plugins: ["karma-jasmine", "karma-chrome-launcher"], 4 | frameworks: ["jasmine"], 5 | // list of files / patterns to load in the browser 6 | files: [ 7 | {pattern: 'src/util.js', type: 'module'}, 8 | {pattern: 'test/**/*.spec.js', type: 'module'}, 9 | ], 10 | // preprocess matching files before serving them to the browser 11 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 12 | preprocessors: { 13 | }, 14 | // test results reporter to use 15 | // possible values: 'dots', 'progress' 16 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 17 | reporters: ['progress'], 18 | // web server port 19 | port: 9876, 20 | // enable / disable colors in the output (reporters and logs) 21 | colors: true, 22 | // level of logging 23 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 24 | logLevel: config.LOG_INFO, 25 | // enable / disable watching file and executing tests whenever any file changes 26 | autoWatch: true, 27 | // start these browsers 28 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 29 | browsers: ['ChromeHeadless'], 30 | // Continuous Integration mode 31 | // if true, Karma captures browsers, runs the tests and exits 32 | singleRun: false, 33 | // Concurrency level 34 | // how many browser should be started simultaneous 35 | concurrency: Infinity 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-dateinput-polyfill", 3 | "description": "input[type=date] polyfill", 4 | "version": "4.0.0-beta.2", 5 | "author": "Maksim Chemerisuk", 6 | "license": "MIT", 7 | "homepage": "https://github.com/chemerisuk/better-dateinput-polyfill", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/chemerisuk/better-dateinput-polyfill" 11 | }, 12 | "keywords": [ 13 | "web-components" 14 | ], 15 | "devDependencies": { 16 | "@babel/core": "^7.13.15", 17 | "@babel/preset-env": "^7.13.15", 18 | "@rollup/plugin-babel": "^5.3.0", 19 | "babel-plugin-html-tag": "^2.0.1", 20 | "gh-pages": "^3.1.0", 21 | "jasmine-core": "^3.7.1", 22 | "karma": "^6.3.2", 23 | "karma-chrome-launcher": "^3.1.0", 24 | "karma-jasmine": "^4.0.1", 25 | "postcss": "^8.2.9", 26 | "postcss-font-family-system-ui": "^5.0.0", 27 | "postcss-preset-env": "^6.7.0", 28 | "rollup": "^2.44.0", 29 | "rollup-plugin-postcss": "^4.0.0", 30 | "rollup-plugin-terser": "^7.0.2" 31 | }, 32 | "scripts": { 33 | "build": "rollup --config", 34 | "watch": "rollup --config --watch --silent | karma start", 35 | "test": "karma start --single-run", 36 | "version": "npm run build -- --configDist && git add -A dist", 37 | "postversion": "git push && git push --tags", 38 | "publish": "gh-pages -s '{index.html,README.md,build/*}' -d ." 39 | }, 40 | "browserslist": [ 41 | "ChromeAndroid 70", 42 | "iOS 10", 43 | "IE 10" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | const postcssPresetEnv = require("postcss-preset-env"); 2 | const postcssSystemUiFont = require("postcss-font-family-system-ui"); 3 | 4 | module.exports = { 5 | inject: false, 6 | minimize: true, 7 | plugins: [ 8 | postcssSystemUiFont(), 9 | postcssPresetEnv({ 10 | features: { 11 | "nesting-rules": true, 12 | "custom-properties": {"preserve": false}, 13 | "custom-media-queries": {"preserve": false}, 14 | }, 15 | }), 16 | ], 17 | }; 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "@rollup/plugin-babel"; 2 | import postcss from "rollup-plugin-postcss"; 3 | import {terser} from "rollup-plugin-terser"; 4 | import pkg from "./package.json"; 5 | 6 | const banner = `/** 7 | * ${pkg.name}: ${pkg.description} 8 | * @version ${pkg.version} ${new Date().toUTCString()} 9 | * @link ${pkg.homepage} 10 | * @copyright ${new Date().getFullYear()} ${pkg.author} 11 | * @license ${pkg.license} 12 | */` 13 | 14 | export default async function (commandLineArgs) { 15 | if (commandLineArgs.configDist) { 16 | return [{ 17 | input: "src/polyfill.js", 18 | output: { 19 | file: "dist/better-dateinput-polyfill.js", 20 | format: "iife", 21 | banner, 22 | }, 23 | plugins: [ 24 | babel({babelHelpers: "bundled"}), 25 | postcss({config: true, inject: false, minimize: true}), 26 | ], 27 | }, { 28 | input: "src/polyfill.js", 29 | output: { 30 | file: "dist/better-dateinput-polyfill.min.js", 31 | format: "iife", 32 | banner, 33 | }, 34 | plugins: [ 35 | babel({babelHelpers: "bundled"}), 36 | postcss({config: true, inject: false, minimize: true}), 37 | terser({compress: {ecma: 5}}), 38 | ], 39 | }] 40 | } else { 41 | return { 42 | input: "src/polyfill.js", 43 | output: { 44 | file: "build/better-dateinput-polyfill.js", 45 | format: "iife", 46 | banner, 47 | }, 48 | plugins: [ 49 | babel({babelHelpers: "bundled"}), 50 | postcss({config: true, inject: false, minimize: true}), 51 | ], 52 | watch: { 53 | clearScreen: false, 54 | }, 55 | }; 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /src/input.js: -------------------------------------------------------------------------------- 1 | import {DatePickerImpl} from "./picker.js"; 2 | import {$, DOCUMENT, HTML} from "./util.js"; 3 | import {parseLocaleDate, formatLocaleDate, getFormatOptions, localeDate} from "./intl.js"; 4 | 5 | const formatMeta = $(DOCUMENT, "meta[name=dateinput-polyfill-format]")[0]; 6 | 7 | export class DateInputPolyfill { 8 | constructor(input) { 9 | this._input = input; 10 | this._valueInput = this._createValueInput(input); 11 | this._formatOptions = this._createFormatOptions(); 12 | 13 | this._input.addEventListener("focus", this._showPicker.bind(this)); 14 | this._input.addEventListener("click", this._showPicker.bind(this)); 15 | this._input.addEventListener("blur", this._hidePicker.bind(this)); 16 | this._input.addEventListener("keydown", this._onKeydown.bind(this)); 17 | 18 | this._initInput(); 19 | } 20 | 21 | _initInput() { 22 | const valueDescriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value"); 23 | // redefine value property for input 24 | Object.defineProperty(this._input, "value", { 25 | configurable: false, 26 | enumerable: true, 27 | get: this._getValue.bind(this), 28 | set: this._setValue.bind(this, valueDescriptor.set) 29 | }); 30 | // redefine valueAsDate property for input 31 | Object.defineProperty(this._input, "valueAsDate", { 32 | configurable: false, 33 | enumerable: true, 34 | get: this._getDate.bind(this), 35 | set: this._setDate.bind(this, valueDescriptor.set) 36 | }); 37 | // change input type to remove built-in picker 38 | this._input.type = "text"; 39 | // do not popup keyboard on mobile devices 40 | this._input.setAttribute("inputmode", "none"); 41 | // need to set readonly attribute as well to prevent 42 | // visible date modification with the cut feature 43 | this._input.readOnly = true; 44 | // update visible value in text input 45 | this._input.value = this._getValue(); 46 | // update default visible value to formatted date 47 | this._input.defaultValue = valueDescriptor.get.call(this._input); 48 | } 49 | 50 | _getValue() { 51 | return this._valueInput.value; 52 | } 53 | 54 | _setValue(setter, stringValue) { 55 | this._setDate(setter, parseLocaleDate(stringValue)); 56 | } 57 | 58 | _getDate() { 59 | return parseLocaleDate(this._getValue()); 60 | } 61 | 62 | _setDate(setter, dateValue) { 63 | setter.call(this._input, dateValue && localeDate(dateValue, this._formatOptions) || ""); 64 | setter.call(this._valueInput, dateValue && formatLocaleDate(dateValue) || ""); 65 | } 66 | 67 | _createValueInput(input) { 68 | const valueInput = DOCUMENT.createElement("input"); 69 | valueInput.style.display = "none"; 70 | valueInput.setAttribute("hidden", ""); 71 | valueInput.disabled = input.disabled; 72 | if (input.name) { 73 | valueInput.name = input.name; 74 | input.removeAttribute("name"); 75 | } 76 | if (input.value) { 77 | valueInput.value = valueInput.defaultValue = input.value; 78 | } 79 | if (input.hasAttribute("form")) { 80 | valueInput.setAttribute("form", input.getAttribute("form")); 81 | } 82 | return input.parentNode.insertBefore(valueInput, input.nextSibling); 83 | } 84 | 85 | _createFormatOptions() { 86 | const locale = this._input.lang || HTML.lang; 87 | let formatString = this._input.getAttribute("data-format"); 88 | if (!formatString && formatMeta) { 89 | formatString = formatMeta.content; 90 | } 91 | return getFormatOptions(locale, formatString); 92 | } 93 | 94 | _onKeydown(event) { 95 | const key = event.key; 96 | if (key === "Enter") { 97 | if (!this._pickerApi.isHidden()) { 98 | event.preventDefault(); 99 | this._hidePicker(); 100 | } 101 | } else if (key === " ") { 102 | // disable scroll change 103 | event.preventDefault(); 104 | 105 | this._showPicker(); 106 | } else if (key === "Backspace") { 107 | // prevent browser back navigation 108 | event.preventDefault(); 109 | 110 | this._input.value = ""; 111 | this._pickerApi.reset(); 112 | this._pickerApi.render(new Date()); 113 | } else { 114 | let offset = 0; 115 | if (key === "ArrowDown" || key === "Down") { 116 | offset = 7; 117 | } else if (key === "ArrowUp" || key === "Up") { 118 | offset = -7; 119 | } else if (key === "ArrowLeft" || key === "Left") { 120 | offset = -1; 121 | } else if (key === "ArrowRight" || key === "Right") { 122 | offset = 1; 123 | } 124 | if (!offset) return; 125 | // disable scroll change on arrows 126 | event.preventDefault(); 127 | 128 | const captionDate = this._pickerApi.getCaptionDate(); 129 | if (this._pickerApi.isAdvancedMode()) { 130 | if (Math.abs(offset) === 7) { 131 | captionDate.setMonth(captionDate.getMonth() + offset / 7); 132 | } else { 133 | captionDate.setFullYear(captionDate.getFullYear() + offset); 134 | } 135 | } else { 136 | captionDate.setDate(captionDate.getDate() + offset); 137 | } 138 | if (this._pickerApi.isValidValue(captionDate)) { 139 | this._input.valueAsDate = captionDate; 140 | this._pickerApi.render(captionDate); 141 | } 142 | } 143 | } 144 | 145 | _showPicker() { 146 | if (!this._pickerApi) { 147 | this._pickerApi = new DatePickerImpl(this._input, this._formatOptions); 148 | } else { 149 | this._pickerApi.show(); 150 | } 151 | } 152 | 153 | _hidePicker() { 154 | this._pickerApi.hide(); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/intl.js: -------------------------------------------------------------------------------- 1 | const INTL_SUPPORTED = (function() { 2 | try { 3 | new Date().toLocaleString("_"); 4 | } catch (err) { 5 | return err instanceof RangeError; 6 | } 7 | return false; 8 | }()); 9 | 10 | export function parseLocaleDate(value) { 11 | const [year, month, date] = (value || "?").split(/\D/).map((s) => parseInt(s)); 12 | // set hours to 12 because otherwise Safari doesn't return 13 | // correct result string for toLocaleString calls 14 | const dateValue = new Date(year, month - 1, date, 12, 0); 15 | return isNaN(dateValue.getTime()) ? null : dateValue; 16 | } 17 | 18 | export function formatLocaleDate(date) { 19 | return [ 20 | date.getFullYear(), 21 | ("0" + (date.getMonth() + 1)).slice(-2), 22 | ("0" + date.getDate()).slice(-2) 23 | ].join("-"); 24 | } 25 | 26 | export function getFormatOptions(locale, formatString) { 27 | if (!INTL_SUPPORTED) return {}; 28 | 29 | let dateTimeFormat; 30 | try { 31 | // We perform severals checks here: 32 | // 1) verify lang attribute is supported by browser 33 | // 2) verify format options are valid 34 | dateTimeFormat = new Intl.DateTimeFormat(locale, JSON.parse(formatString || "{}")); 35 | } catch (err) { 36 | console.warn("Fallback to default date format because of error:", err); 37 | // fallback to default date format options 38 | dateTimeFormat = new Intl.DateTimeFormat(); 39 | } 40 | return dateTimeFormat.resolvedOptions(); 41 | } 42 | 43 | export function localeWeekday(value, options) { 44 | const date = new Date(1971, 1, value + (options.hour12 ? 0 : 1)); 45 | /* istanbul ignore else */ 46 | if (INTL_SUPPORTED) { 47 | return date.toLocaleString(options.locale, {weekday: "short"}); 48 | } else { 49 | return date.toUTCString().split(",")[0].slice(0, 2); 50 | } 51 | } 52 | 53 | export function localeMonth(value, options) { 54 | const date = new Date(25e8 * (value + 1)); 55 | /* istanbul ignore else */ 56 | if (INTL_SUPPORTED) { 57 | return date.toLocaleString(options.locale, {month: "short"}); 58 | } else { 59 | return date.toUTCString().split(" ")[2]; 60 | } 61 | } 62 | 63 | export function localeDate(value, options) { 64 | if (INTL_SUPPORTED) { 65 | return value.toLocaleString(options.locale, options); 66 | } else { 67 | return value.toUTCString().split(" ").slice(0, 4).join(" "); 68 | } 69 | } 70 | 71 | export function localeMonthYear(value, options) { 72 | if (INTL_SUPPORTED) { 73 | return value.toLocaleString(options.locale, {month: "long", year: "numeric"}); 74 | } else { 75 | return value.toUTCString().split(" ").slice(2, 4).join(" "); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/picker.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background: #fff; 3 | --color-graytext: #ccc; 4 | --color-hover: #ececec; 5 | --color-selected: #007bff; 6 | 7 | --calendar-day-height: calc(100vh / 8); 8 | --calendar-day-width: calc(100vw / 7); 9 | --calendar-inner-padding: 4px; 10 | } 11 | 12 | body { 13 | cursor: default; 14 | font-family: system-ui; 15 | margin: 0; 16 | } 17 | 18 | [aria-labelledby] { 19 | bottom: 0; 20 | height: calc(100vh - var(--calendar-day-height)); 21 | left: 0; 22 | position: absolute; 23 | text-align: center; 24 | width: 100%; 25 | 26 | &[aria-hidden="true"] { 27 | visibility: hidden; 28 | } 29 | } 30 | 31 | header { 32 | display: block; 33 | height: var(--calendar-day-height); 34 | line-height: var(--calendar-day-height); 35 | overflow: hidden; 36 | text-align: center; 37 | } 38 | 39 | [role="button"] { 40 | text-align: center; 41 | transition: transform 75ms ease-in; 42 | width: var(--calendar-day-width); 43 | 44 | &[rel="prev"] { 45 | float: left; 46 | 47 | &:active { 48 | transform: translateX(-2px); 49 | } 50 | } 51 | 52 | &[rel="next"] { 53 | float: right; 54 | 55 | &:active { 56 | transform: translateX(2px); 57 | } 58 | } 59 | 60 | & svg { 61 | pointer-events: none; 62 | width: 16px; 63 | height: 100%; 64 | } 65 | 66 | @media (hover: hover) { 67 | &:hover { 68 | transform: scale(1.2); 69 | } 70 | } 71 | } 72 | 73 | [aria-live="polite"] { 74 | border: 1px dotted transparent; 75 | color: var(--color-selected); 76 | font-weight: bold; 77 | margin: auto 0; 78 | overflow: hidden; 79 | text-align: center; 80 | text-overflow: ellipsis; 81 | white-space: nowrap; 82 | 83 | @media (hover: hover) { 84 | &:hover { 85 | border-bottom-color: inherit; 86 | } 87 | } 88 | } 89 | 90 | table { 91 | border-spacing: 0; 92 | table-layout: fixed; 93 | } 94 | 95 | th { 96 | box-sizing: border-box; 97 | height: var(--calendar-day-height); 98 | padding-bottom: calc(2 * var(--calendar-inner-padding)); 99 | vertical-align: middle; 100 | } 101 | 102 | td { 103 | border-radius: var(--border-radius); 104 | padding: 0; 105 | 106 | &:not([aria-selected]) { 107 | color: var(--color-graytext); 108 | } 109 | 110 | &[aria-current="date"] { 111 | font-weight: bold; 112 | } 113 | 114 | &[aria-disabled="true"] { 115 | background-color: var(--color-hover); 116 | border-radius: 0; 117 | color: var(--color-graytext); 118 | cursor: not-allowed; 119 | } 120 | } 121 | 122 | #months, #years { 123 | box-sizing: border-box; 124 | float: left; 125 | height: 100%; 126 | line-height: calc((100vh - var(--calendar-day-height)) / 12); 127 | list-style: none; 128 | margin: 0; 129 | overflow-x: hidden; 130 | overflow-y: scroll; 131 | padding: 0 var(--calendar-inner-padding); 132 | width: 50%; 133 | } 134 | 135 | [data-date], [data-month], [data-year] { 136 | @media (hover: hover) { 137 | &:hover { 138 | background-color: var(--color-hover); 139 | } 140 | } 141 | 142 | &[aria-selected="true"] { 143 | background-color: var(--color-selected); 144 | color: var(--color-background); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/picker.js: -------------------------------------------------------------------------------- 1 | import PICKER_CSS from "./picker.css"; 2 | import {$, DOCUMENT, HTML, IE, repeat, svgIcon, injectStyles} from "./util.js"; 3 | import {parseLocaleDate, formatLocaleDate, localeWeekday, localeMonth, localeMonthYear} from "./intl.js"; 4 | 5 | export class DatePickerImpl { 6 | constructor(input, formatOptions) { 7 | this._input = input; 8 | this._formatOptions = formatOptions; 9 | 10 | this._initPicker(); 11 | } 12 | 13 | _initPicker() { 14 | this._picker = DOCUMENT.createElement("dateinput-picker"); 15 | this._picker.setAttribute("aria-hidden", true); 16 | this._input.parentNode.insertBefore(this._picker, this._input); 17 | 18 | const object = DOCUMENT.createElement("object"); 19 | object.type = "text/html"; 20 | object.width = "100%"; 21 | object.height = "100%"; 22 | // non-IE: must be BEFORE the element added to the document 23 | if (!IE) { 24 | object.data = "about:blank"; 25 | } 26 | // load content when is ready 27 | object.onload = event => { 28 | this._initContent(event.target.contentDocument); 29 | // this is a one time event handler 30 | delete object.onload; 31 | }; 32 | // add object element to the document 33 | this._picker.appendChild(object); 34 | // IE: must be AFTER the element added to the document 35 | if (IE) { 36 | object.data = "about:blank"; 37 | } 38 | } 39 | 40 | _initContent(pickerRoot) { 41 | const defaultYearDelta = 30; 42 | const now = new Date(); 43 | const minDate = this._getLimitationDate("min"); 44 | const maxDate = this._getLimitationDate("max"); 45 | let startYear = minDate ? minDate.getFullYear() : now.getFullYear() - defaultYearDelta; 46 | let endYear = maxDate ? maxDate.getFullYear() : now.getFullYear() + defaultYearDelta; 47 | // append picker HTML to shadow dom 48 | pickerRoot.body.innerHTML = html` 49 |
50 | 51 | 52 | 53 |
54 | 55 | ${repeat(7, (_, i) => ``)} 56 | ${repeat(6, `${repeat(7, "`)} 57 |
${localeWeekday(i, this._formatOptions)}
")}
58 | 66 | `; 67 | 68 | injectStyles(PICKER_CSS, pickerRoot.head); 69 | 70 | this._caption = $(pickerRoot, "[aria-live=polite]")[0]; 71 | this._pickers = $(pickerRoot, "[aria-labelledby]"); 72 | 73 | pickerRoot.addEventListener("mousedown", this._onMouseDown.bind(this)); 74 | pickerRoot.addEventListener("contextmenu", (event) => event.preventDefault()); 75 | pickerRoot.addEventListener("dblclick", (event) => event.preventDefault()); 76 | 77 | this.show(); 78 | } 79 | 80 | _getLimitationDate(name) { 81 | if (this._input) { 82 | return parseLocaleDate(this._input.getAttribute(name)); 83 | } else { 84 | return null; 85 | } 86 | } 87 | 88 | _onMouseDown(event) { 89 | const target = event.target; 90 | // disable default behavior so input doesn't loose focus 91 | event.preventDefault(); 92 | // skip right/middle mouse button clicks 93 | if (event.button) return; 94 | 95 | if (target === this._caption) { 96 | this._togglePickerMode(); 97 | } else if (target.getAttribute("role") === "button") { 98 | this._clickButton(target); 99 | } else if (target.hasAttribute("data-date")) { 100 | this._clickDate(target); 101 | } else if (target.hasAttribute("data-month") || target.hasAttribute("data-year")) { 102 | this._clickMonthYear(target); 103 | } 104 | } 105 | 106 | _clickButton(target) { 107 | const captionDate = this.getCaptionDate(); 108 | const sign = target.getAttribute("rel") === "prev" ? -1 : 1; 109 | const advancedMode = this.isAdvancedMode(); 110 | if (advancedMode) { 111 | captionDate.setFullYear(captionDate.getFullYear() + sign); 112 | } else { 113 | captionDate.setMonth(captionDate.getMonth() + sign); 114 | } 115 | if (this.isValidValue(captionDate)) { 116 | this.render(captionDate); 117 | if (advancedMode) { 118 | this._input.valueAsDate = captionDate; 119 | } 120 | } 121 | } 122 | 123 | _clickDate(target) { 124 | if (target.getAttribute("aria-disabled") !== "true") { 125 | this._input.value = target.getAttribute("data-date"); 126 | this.hide(); 127 | } 128 | } 129 | 130 | _clickMonthYear(target) { 131 | const month = parseInt(target.getAttribute("data-month")); 132 | const year = parseInt(target.getAttribute("data-year")); 133 | if (month >= 0 || year >= 0) { 134 | const captionDate = this.getCaptionDate(); 135 | if (!isNaN(month)) { 136 | captionDate.setMonth(month); 137 | } 138 | if (!isNaN(year)) { 139 | captionDate.setFullYear(year); 140 | } 141 | if (this.isValidValue(captionDate)) { 142 | this._renderAdvancedPicker(captionDate, false); 143 | this._input.valueAsDate = captionDate; 144 | } 145 | } 146 | } 147 | 148 | _togglePickerMode() { 149 | this._pickers.forEach((element, index) => { 150 | const currentDate = this._input.valueAsDate || new Date(); 151 | const hidden = element.getAttribute("aria-hidden") === "true"; 152 | if (index === 0) { 153 | if (hidden) { 154 | this._renderCalendarPicker(currentDate); 155 | } 156 | } else { 157 | if (hidden) { 158 | this._renderAdvancedPicker(currentDate); 159 | } 160 | } 161 | element.setAttribute("aria-hidden", !hidden); 162 | }); 163 | } 164 | 165 | _renderCalendarPicker(captionDate) { 166 | const now = new Date(); 167 | const currentDate = this._input.valueAsDate; 168 | const minDate = this._getLimitationDate("min"); 169 | const maxDate = this._getLimitationDate("max"); 170 | const iterDate = new Date(captionDate.getFullYear(), captionDate.getMonth()); 171 | // move to beginning of the first week in current month 172 | iterDate.setDate((this._formatOptions.hour12 ? 0 : iterDate.getDay() === 0 ? -6 : 1) - iterDate.getDay()); 173 | 174 | $(this._pickers[0], "td").forEach((cell) => { 175 | iterDate.setDate(iterDate.getDate() + 1); 176 | 177 | const iterDateStr = formatLocaleDate(iterDate); 178 | 179 | if (iterDate.getMonth() === captionDate.getMonth()) { 180 | if (currentDate && iterDateStr === formatLocaleDate(currentDate)) { 181 | cell.setAttribute("aria-selected", true); 182 | } else { 183 | cell.setAttribute("aria-selected", false); 184 | } 185 | } else { 186 | cell.removeAttribute("aria-selected"); 187 | } 188 | 189 | if (iterDateStr === formatLocaleDate(now)) { 190 | cell.setAttribute("aria-current", "date"); 191 | } else { 192 | cell.removeAttribute("aria-current"); 193 | } 194 | 195 | if ((minDate && iterDate < minDate) || (maxDate && iterDate > maxDate)) { 196 | cell.setAttribute("aria-disabled", true); 197 | } else { 198 | cell.removeAttribute("aria-disabled"); 199 | } 200 | 201 | cell.textContent = iterDate.getDate(); 202 | cell.setAttribute("data-date", iterDateStr); 203 | }); 204 | // update visible caption value 205 | this.setCaptionDate(captionDate); 206 | } 207 | 208 | _renderAdvancedPicker(captionDate, syncScroll = true) { 209 | $(this._pickers[1], "[aria-selected]").forEach((selectedElement) => { 210 | selectedElement.removeAttribute("aria-selected"); 211 | }); 212 | 213 | if (captionDate) { 214 | const monthItem = $(this._pickers[1], `[data-month="${captionDate.getMonth()}"]`)[0]; 215 | const yearItem = $(this._pickers[1], `[data-year="${captionDate.getFullYear()}"]`)[0]; 216 | monthItem.setAttribute("aria-selected", true); 217 | yearItem.setAttribute("aria-selected", true); 218 | if (syncScroll) { 219 | monthItem.parentNode.scrollTop = monthItem.offsetTop; 220 | yearItem.parentNode.scrollTop = yearItem.offsetTop; 221 | } 222 | // update visible caption value 223 | this.setCaptionDate(captionDate); 224 | } 225 | } 226 | 227 | isValidValue(dateValue) { 228 | const minDate = this._getLimitationDate("min"); 229 | const maxDate = this._getLimitationDate("max"); 230 | return !((minDate && dateValue < minDate) || (maxDate && dateValue > maxDate)); 231 | } 232 | 233 | isAdvancedMode() { 234 | return this._pickers[0].getAttribute("aria-hidden") === "true"; 235 | } 236 | 237 | getCaptionDate() { 238 | return new Date(this._caption.getAttribute("datetime")); 239 | } 240 | 241 | setCaptionDate(captionDate) { 242 | this._caption.textContent = localeMonthYear(captionDate, this._formatOptions); 243 | this._caption.setAttribute("datetime", captionDate.toISOString()); 244 | } 245 | 246 | isHidden() { 247 | return this._picker.getAttribute("aria-hidden") === "true"; 248 | } 249 | 250 | show() { 251 | if (this.isHidden()) { 252 | const startElement = this._input; 253 | const pickerOffset = this._picker.getBoundingClientRect(); 254 | const inputOffset = startElement.getBoundingClientRect(); 255 | // set picker position depending on current visible area 256 | let marginTop = inputOffset.height; 257 | if (HTML.clientHeight < inputOffset.bottom + pickerOffset.height) { 258 | marginTop = -pickerOffset.height; 259 | } 260 | this._picker.style.marginTop = marginTop + "px"; 261 | 262 | this._renderCalendarPicker(this._input.valueAsDate || new Date()); 263 | // display picker 264 | this._picker.removeAttribute("aria-hidden"); 265 | } 266 | } 267 | 268 | hide() { 269 | this._picker.setAttribute("aria-hidden", true); 270 | this.reset(); 271 | } 272 | 273 | reset() { 274 | this._pickers.forEach((element, index) => { 275 | element.setAttribute("aria-hidden", !!index); 276 | }); 277 | } 278 | 279 | render(captionDate) { 280 | if (this.isAdvancedMode()) { 281 | this._renderAdvancedPicker(captionDate); 282 | } else { 283 | this._renderCalendarPicker(captionDate); 284 | } 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/polyfill.css: -------------------------------------------------------------------------------- 1 | @keyframes dateinput-polyfill { 2 | from {opacity: .99}; 3 | to {opacity: 1}; 4 | } 5 | 6 | input[type="date"] { 7 | /* we need this fake animation to init polyfill */ 8 | animation: dateinput-polyfill 1ms !important; 9 | } 10 | 11 | :root { 12 | --calendar-day-size: 45px; 13 | } 14 | 15 | dateinput-picker { 16 | background: #fff; 17 | box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2); 18 | height: calc(var(--calendar-day-size) * 8); 19 | position: absolute; 20 | width: calc(var(--calendar-day-size) * 7); 21 | z-index: 2147483647; 22 | 23 | &[aria-hidden=true] { 24 | visibility: hidden; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | import POLYFILL_CSS from "./polyfill.css"; 2 | import {DateInputPolyfill} from "./input.js"; 3 | import {$, WINDOW, DOCUMENT, IE, injectStyles} from "./util.js"; 4 | 5 | const ANIMATION_NAME = "dateinput-polyfill"; 6 | const PROPERTY_NAME = `__${ANIMATION_NAME}__`; 7 | 8 | function isDateInputSupported() { 9 | // use a stronger type support detection that handles old WebKit browsers: 10 | // http://www.quirksmode.org/blog/archives/2015/03/better_modern_i.html 11 | const input = DOCUMENT.createElement("input"); 12 | input.type = "date"; 13 | input.value = "_"; 14 | return input.value !== "_"; 15 | } 16 | 17 | const mediaMeta = $(DOCUMENT, "meta[name=dateinput-polyfill-media]")[0]; 18 | if (mediaMeta ? WINDOW.matchMedia(mediaMeta.content) : (IE || !isDateInputSupported())) { 19 | // inject style rules with fake animation 20 | injectStyles(POLYFILL_CSS, DOCUMENT.head); 21 | // attach listener to catch all fake animation starts 22 | DOCUMENT.addEventListener("animationstart", event => { 23 | if (event.animationName === ANIMATION_NAME) { 24 | const input = event.target; 25 | if (!input[PROPERTY_NAME]) { 26 | input[PROPERTY_NAME] = new DateInputPolyfill(input); 27 | } 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export const WINDOW = window; 2 | export const DOCUMENT = document; 3 | export const HTML = DOCUMENT.documentElement; 4 | export const IE = "ScriptEngineMajorVersion" in WINDOW; 5 | 6 | export function $(element, selector) { 7 | return Array.prototype.slice.call(element.querySelectorAll(selector), 0); 8 | } 9 | 10 | export function repeat(times, fn) { 11 | if (typeof fn === "string") { 12 | return Array(times + 1).join(fn); 13 | } else { 14 | return Array.apply(null, Array(times)).map(fn).join(""); 15 | } 16 | } 17 | 18 | export function svgIcon(path, size) { 19 | return html` 20 | 21 | 22 | 23 | `; 24 | } 25 | 26 | export function injectStyles(cssText, head) { 27 | const style = DOCUMENT.createElement("style"); 28 | style.type = "text/css"; 29 | style.innerHTML = cssText; 30 | if (head.firstChild) { 31 | head.insertBefore(style, head.firstChild); 32 | } else { 33 | head.appendChild(style); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/util.spec.js: -------------------------------------------------------------------------------- 1 | import {repeat} from "../src/util.js"; 2 | 3 | describe("util", function() { 4 | describe("repeat", () => { 5 | it("repeats a string", function() { 6 | expect(repeat(3, "ab")).toBe("ababab"); 7 | expect(repeat(4, "xyz")).toBe("xyzxyzxyzxyz"); 8 | }); 9 | }); 10 | }); 11 | --------------------------------------------------------------------------------