├── .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-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 = "" + repeat(7, function (_, i) {
188 | return "" + localeWeekday(i, _this2._formatOptions) + " ";
189 | }) + " " + repeat(6, "" + repeat(7, "") + " ") + "
" + repeat(12, function (_, i) {
190 | return "" + localeMonth(i, _this2._formatOptions);
191 | }) + " " + repeat(endYear - startYear + 1, function (_, i) {
192 | return "" + (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""+(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))+" ");var a,n,r}))+' '+r(6,""+r(7,"")+" ")+'
'+r(12,(function(t,i){return''+(a=i,n=e._formatOptions,r=new Date(25e8*(a+1)),l?r.toLocaleString(n.locale,{month:"short"}):r.toUTCString().split(" ")[2]);var a,n,r}))+' '+r(h-d+1,(function(t,e){return''+(d+e)}))+" ",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 |
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 |
54 |
55 | ${repeat(7, (_, i) => `${localeWeekday(i, this._formatOptions)} `)}
56 | ${repeat(6, `${repeat(7, "")} `)}
57 |
58 |
59 |
${repeat(12, (_, i) => {
60 | return `${localeMonth(i, this._formatOptions)}`;
61 | })}
62 |
${repeat(endYear - startYear + 1, (_, i) => {
63 | return `${startYear + i}`;
64 | })}
65 |
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 |
--------------------------------------------------------------------------------