@digitransit-component/digitransit-component-datetimepicker 6.0.10 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-AGPL.txt +661 -0
- package/LICENSE-EUPL.txt +291 -0
- package/lib/index.cjs +1 -1
- package/lib/index.cjs.map +1 -1
- package/lib/index.development.cjs +1594 -2
- package/lib/index.js +1586 -0
- package/lib/index.js.map +1 -0
- package/package.json +18 -7
package/lib/index.js
ADDED
|
@@ -0,0 +1,1586 @@
|
|
|
1
|
+
import PropTypes from 'prop-types';
|
|
2
|
+
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
3
|
+
import { useTranslation, I18nextProvider } from 'react-i18next';
|
|
4
|
+
import { DateTime, Settings } from 'luxon';
|
|
5
|
+
import debounce from 'lodash/debounce.js';
|
|
6
|
+
import uniqueId from 'lodash/uniqueId.js';
|
|
7
|
+
import Icon from '@digitransit-component/digitransit-component-icon';
|
|
8
|
+
import Select from 'react-select';
|
|
9
|
+
import cx from 'classnames';
|
|
10
|
+
import Modal from 'react-modal';
|
|
11
|
+
import { useSelect } from 'downshift';
|
|
12
|
+
import { createInstance } from 'i18next';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle typing time and adding necessary :
|
|
16
|
+
*
|
|
17
|
+
* @param {string} typedValue
|
|
18
|
+
*
|
|
19
|
+
* @return {string}
|
|
20
|
+
*/
|
|
21
|
+
function parseTypedTime(typedValue) {
|
|
22
|
+
let actualValue = typedValue;
|
|
23
|
+
if (actualValue.length === 3 && actualValue.split('').every(n => Number.isInteger(Number(n)))) {
|
|
24
|
+
// add ':' if user types three numbers in a row
|
|
25
|
+
if (Number(actualValue.slice(0, 2)) <= 23) {
|
|
26
|
+
actualValue = `${actualValue.slice(0, 2)}:${actualValue[2]}`;
|
|
27
|
+
} else {
|
|
28
|
+
actualValue = `${actualValue[0]}:${actualValue.slice(1)}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return actualValue;
|
|
32
|
+
}
|
|
33
|
+
const validateClock = (hours, minutes) => {
|
|
34
|
+
const hoursValid = !Number.isNaN(hours) && hours >= 0 && hours <= 23;
|
|
35
|
+
const minutesLen = minutes.length;
|
|
36
|
+
const minutesValid = minutesLen === 2 ? !Number.isNaN(minutes) && Number(minutes) >= 0 && Number(minutes <= 59) : !Number.isNaN(minutes) && Number(minutes) >= 0 && Number(minutes <= 5);
|
|
37
|
+
return hoursValid && minutesValid;
|
|
38
|
+
};
|
|
39
|
+
function validateInput(inputValue) {
|
|
40
|
+
// We don't accept seconds in the input
|
|
41
|
+
if (inputValue.split(':').length > 2) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (inputValue.length <= 2) {
|
|
45
|
+
// Too many options, don't validate
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
if (inputValue.length === 3) {
|
|
49
|
+
let hours;
|
|
50
|
+
let minutes;
|
|
51
|
+
if (inputValue.includes(':')) {
|
|
52
|
+
[hours, minutes] = inputValue.split(':');
|
|
53
|
+
} else if (inputValue.startsWith('0')) {
|
|
54
|
+
hours = inputValue.substring(0, inputValue.length - 1);
|
|
55
|
+
minutes = inputValue.substring(inputValue.length - 1 || 0);
|
|
56
|
+
if (Number(minutes) > 5) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// This is how basically moment handles string formatting.
|
|
61
|
+
// If the first letter of the string is 1, then rest are minutes.
|
|
62
|
+
// if the first letter is 2, then if second letter is 0,1,2 or 3, then second letter is hour, else rest are minutes
|
|
63
|
+
// else, first letter is hour, rest of them are minutes.
|
|
64
|
+
const values = inputValue.split('');
|
|
65
|
+
if (Number(values[0]) === 1) {
|
|
66
|
+
[hours, minutes] = [values[0].concat(values[1]), values[2]];
|
|
67
|
+
} else if (Number(values[0]) === 2) {
|
|
68
|
+
if (Number(values[1] <= 3)) {
|
|
69
|
+
[hours, minutes] = [values[0].concat(values[1]), values[2]];
|
|
70
|
+
} else {
|
|
71
|
+
[hours, minutes] = [values[0], values[1].concat(values[2])];
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
[hours, minutes] = [values[0], values[1].concat(values[2])];
|
|
75
|
+
}
|
|
76
|
+
return validateClock(hours, minutes);
|
|
77
|
+
}
|
|
78
|
+
return validateClock(hours, minutes);
|
|
79
|
+
}
|
|
80
|
+
if (inputValue.length === 5 || inputValue.length === 4) {
|
|
81
|
+
const values = inputValue.split(':');
|
|
82
|
+
const hours = values[0];
|
|
83
|
+
const minutes = values[1];
|
|
84
|
+
if (inputValue.startsWith('0') && minutes.length === 1 && Number(minutes) > 5) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
return validateClock(hours, minutes);
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
const getTs = (inputValue, currentTimestamp) => {
|
|
92
|
+
const trimmed = inputValue.trim();
|
|
93
|
+
if (trimmed.match(/^[0-9]{1,2}(\.|:)[0-9]{2}$/) !== null) {
|
|
94
|
+
const splitter = trimmed.includes('.') ? '.' : ':';
|
|
95
|
+
const values = trimmed.split(splitter);
|
|
96
|
+
const hours = Number(values[0]);
|
|
97
|
+
const hoursValid = !Number.isNaN(hours) && hours >= 0 && hours <= 23;
|
|
98
|
+
const minutes = Number(values[1]);
|
|
99
|
+
const minutesValid = !Number.isNaN(minutes) && minutes >= 0 && minutes <= 59;
|
|
100
|
+
if (!minutesValid || !hoursValid) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const newStamp = DateTime.fromMillis(currentTimestamp).set({
|
|
104
|
+
hour: hours,
|
|
105
|
+
minute: minutes,
|
|
106
|
+
second: 0,
|
|
107
|
+
millisecond: 0
|
|
108
|
+
}).valueOf();
|
|
109
|
+
return newStamp;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
function styleInject(css, ref) {
|
|
115
|
+
if ( ref === void 0 ) ref = {};
|
|
116
|
+
var insertAt = ref.insertAt;
|
|
117
|
+
|
|
118
|
+
if (typeof document === 'undefined') { return; }
|
|
119
|
+
|
|
120
|
+
var head = document.head || document.getElementsByTagName('head')[0];
|
|
121
|
+
var style = document.createElement('style');
|
|
122
|
+
style.type = 'text/css';
|
|
123
|
+
|
|
124
|
+
if (insertAt === 'top') {
|
|
125
|
+
if (head.firstChild) {
|
|
126
|
+
head.insertBefore(style, head.firstChild);
|
|
127
|
+
} else {
|
|
128
|
+
head.appendChild(style);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
head.appendChild(style);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (style.styleSheet) {
|
|
135
|
+
style.styleSheet.cssText = css;
|
|
136
|
+
} else {
|
|
137
|
+
style.appendChild(document.createTextNode(css));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
var css_248z = ".styles_sr-only__D6MJ6:not(:focus, :active) {\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n\n.styles_dt-datetimepicker__DSYbh {\n border: none;\n padding: 0;\n margin: 0;\n font-size: 15px;\n font-weight: var(--font-weight-medium);\n}\n.styles_dt-datetimepicker__DSYbh button,\n.styles_dt-datetimepicker__DSYbh label {\n font-size: 1em;\n font-weight: inherit;\n}\n.styles_dt-datetimepicker__DSYbh label button {\n cursor: pointer;\n font-family: inherit;\n}\n.styles_dt-datetimepicker__DSYbh .styles_hidden__TjnWH {\n display: none;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-open-container__zWBiT {\n background-color: #f2f5f7;\n border-radius: 5px;\n padding: 15px;\n margin: 9px 0 9px 0;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n min-height: 36px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj.styles_closed__QYbCF {\n margin: 4px 0 6px 18px;\n}\n@media screen and (max-width: 319px) {\n .styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj {\n margin-left: 14px;\n }\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_time-icon__ES47Y svg {\n width: 18px;\n height: 19px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_dropdown-icon__O0GrI {\n margin-left: 9px;\n position: relative;\n top: 1px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_dropdown-icon__O0GrI svg {\n width: 14px;\n height: 10px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_close-icon__B5-I5 svg {\n width: 12px;\n height: 12px;\n border-radius: 1px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj .styles_close-icon__B5-I5 svg path {\n stroke-width: 30;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj label {\n cursor: pointer;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj label.styles_label-open__dVDlF {\n align-items: flex-start;\n display: flex;\n}\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj button,\n.styles_dt-datetimepicker__DSYbh .styles_top-row-container__rGdmj label {\n font-size: 1em;\n letter-spacing: -0.5px;\n line-height: 1;\n}\n.styles_dt-datetimepicker__DSYbh .styles_textbutton__sVckw {\n margin-left: 9px;\n background: none;\n border: none;\n color: #666;\n}\n@media screen and (min-width: 768px) {\n .styles_dt-datetimepicker__DSYbh .styles_textbutton__sVckw {\n margin-left: 14px;\n }\n}\n.styles_dt-datetimepicker__DSYbh .styles_textbutton__sVckw.styles_active__9LaDz {\n color: var(--color);\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-left__nwwOo {\n position: relative;\n margin-right: 4px;\n width: 63%;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-right__qlPKD {\n position: relative;\n margin-left: 4px;\n width: 37%;\n}\n.styles_dt-datetimepicker__DSYbh .styles_departure-or-arrival-container__Rj3b9 {\n height: calc(1rem + 20px);\n background-color: #ddd;\n border-radius: 8px;\n display: flex;\n width: calc(5px + 13em);\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o {\n color: #333;\n margin: 2px;\n background: none;\n border-radius: 6px;\n padding: 9px 0;\n width: 50%;\n cursor: pointer;\n height: calc(1rem + 16px);\n text-align: center;\n font-weight: 400;\n font-size: 15px;\n line-height: 18px;\n letter-spacing: -0.03em;\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o.styles_active__9LaDz {\n font-weight: 500;\n background: white;\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o:focus-within {\n /* First apply outline that is used in Edge and Firefox */\n outline: 1px dotted #212121;\n /* Then override outline for webkit browsers like Chrome, this has no effect on other browsers */\n outline: 5px auto -webkit-focus-ring-color; /* stylelint-disable-line declaration-block-no-duplicate-properties */\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton-label__LeV2o.styles_first-radio__F8N7J {\n margin: 0;\n display: flex;\n align-items: flex-start;\n}\n.styles_dt-datetimepicker__DSYbh .styles_radio-textbutton__Ad81W {\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_departure-now-button__8ZlDU {\n font-family: inherit;\n letter-spacing: -0.02em;\n height: calc(1rem + 18px);\n color: var(--color);\n padding: 8px 0;\n width: 6.6875rem;\n background: white;\n border: 1px solid #888;\n border-radius: 30px;\n margin-left: 11px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_open-button__yl4be {\n display: flex;\n margin-bottom: 0;\n margin-top: 1px;\n padding: 2px 0 2px 0;\n}\n.styles_dt-datetimepicker__DSYbh button.styles_close-button__mdfIU {\n transition: transform 0.2s;\n position: relative;\n top: -14px;\n right: -8px;\n border: 0;\n font-weight: normal;\n background: none;\n color: var(--color);\n padding: 0;\n line-height: normal;\n}\n.styles_dt-datetimepicker__DSYbh button.styles_close-button__mdfIU:hover {\n transform: scale(1.2);\n}\n.styles_dt-datetimepicker__DSYbh .styles_right-edge__XbqgK {\n margin-left: auto;\n margin-right: 0;\n position: relative;\n top: -2px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-container__sR9aU {\n cursor: pointer;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4 {\n color: var(--color);\n height: 40px;\n position: absolute;\n z-index: 7;\n pointer-events: none;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_date-input-icon__SFXGx {\n display: flex;\n align-items: center;\n padding: 0;\n margin-left: 14px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_date-input-icon__SFXGx svg {\n width: 20px;\n height: 18px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_time-input-icon__J1qei {\n display: flex;\n align-items: center;\n padding: 0;\n margin-left: 8px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-icon__wsdX4.styles_time-input-icon__J1qei svg {\n margin-top: 1px;\n width: 22px;\n height: 22px;\n}\n.styles_dt-datetimepicker__DSYbh .datetimepicker-invalid-input input {\n color: #f00 !important;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE {\n position: relative;\n height: 40px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__control {\n cursor: inherit;\n height: 40px;\n border: 1px solid #888;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__value-container {\n padding: 0;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__single-value {\n margin: 0 0 0 47px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__input {\n background: inherit;\n position: relative;\n margin-left: 40px;\n margin-top: -4px;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__input input {\n opacity: 1 !important;\n background: inherit;\n box-shadow: none;\n font-stretch: normal;\n font-style: normal;\n line-height: 1.33;\n letter-spacing: -0.5px;\n color: #333;\n font-family: inherit;\n font-size: 15px;\n font-weight: var(--font-weight-medium);\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__input input:focus {\n box-shadow: none;\n outline: none;\n border: none;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu {\n z-index: 19;\n padding: 0;\n background: #fff;\n max-height: 250px;\n border-radius: 5px;\n box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.06);\n border: 1px rgba(0, 0, 0, 0.1) solid;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list {\n max-height: 250px;\n padding: 0;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list .datetimepicker-select__option {\n height: 50px;\n border-top: 1px solid #ddd;\n padding: 13px 0 0 12px;\n letter-spacing: -0.6px;\n color: #333;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list .datetimepicker-select__option--is-selected {\n background: inherit;\n}\n.styles_dt-datetimepicker__DSYbh .styles_datetimepicker-select-container__Y8wgE .datetimepicker-select__menu .datetimepicker-select__menu-list .datetimepicker-select__option--is-focused {\n background: #dfe5e8;\n}\n.styles_dt-datetimepicker__DSYbh .styles_picker-container__wz2Zj {\n margin: 14px 0 0 0;\n display: flex;\n}\n.styles_dt-datetimepicker__DSYbh .styles_combobox-mobile-container__ZzG8g {\n flex-grow: 1;\n}\n\n.styles_mobile-modal-overlay__wq8cS {\n position: fixed;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background-color: rgba(0, 0, 0, 0.6);\n z-index: 10001;\n}\n\n.styles_mobile-modal-content__Gla3M {\n font-family: inherit;\n background: white;\n width: 89%;\n position: absolute;\n left: 5.5%;\n padding: 31px 20px 33px 20px;\n top: 80px;\n border-radius: 5px;\n}\n.styles_mobile-modal-content__Gla3M .styles_top-row__hzke9 {\n display: flex;\n justify-content: space-between;\n margin-bottom: 21px;\n}\n.styles_mobile-modal-content__Gla3M .styles_top-row__hzke9 .styles_modal-title__CztKf {\n font-weight: var(--font-weight-medium);\n font-size: 20px;\n line-height: 18px;\n letter-spacing: -0.5px;\n color: #333;\n padding: 6px 0 0 8px;\n}\n.styles_mobile-modal-content__Gla3M .styles_top-row__hzke9 .styles_departure-now-button__8ZlDU {\n background-color: white;\n font-family: inherit;\n padding: 5px 15px;\n border: 1px solid #888;\n border-radius: 50px;\n font-weight: var(--font-weight-medium);\n font-size: 15px;\n line-height: 18px;\n letter-spacing: -0.02em;\n color: var(--color);\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM {\n display: flex;\n border: 2px solid #ddd;\n border-radius: 5px;\n margin-bottom: 16px;\n background-color: #ddd;\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM .styles_radio-tab-label__-7Fx- {\n padding: 9px 0;\n font-size: 15px;\n line-height: 18px;\n letter-spacing: -0.5px;\n border-radius: 3px;\n font-weight: normal;\n width: 50%;\n text-align: center;\n color: #333;\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM .styles_radio-tab-label__-7Fx-.styles_active__9LaDz {\n font-weight: var(--font-weight-medium);\n background-color: #fff;\n}\n.styles_mobile-modal-content__Gla3M .styles_tab-row__6WYwM .styles_radio-textbutton__Ad81W {\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n height: 1px;\n overflow: hidden;\n position: absolute;\n white-space: nowrap;\n width: 1px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j {\n display: flex;\n margin-bottom: 25px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j label:first-child {\n flex-basis: 60%;\n margin-right: 10px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j label:nth-child(2) {\n flex-basis: 40%;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW {\n border: 1px solid #888;\n border-radius: 5px;\n display: flex;\n align-items: center;\n height: 40px;\n line-height: 1.5;\n font-size: 100%;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW:focus-within {\n background: rgba(0, 122, 201, 0.06);\n border: 2px solid var(--color);\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_combobox-icon__wsdX4 {\n height: 18px;\n font-size: 18px;\n padding: 0 0 0 10px;\n border: none;\n position: relative;\n top: 3px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_combobox-icon__wsdX4.styles_time-input-icon__J1qei > svg {\n height: 20px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_combobox-icon__wsdX4.styles_date-input-icon__SFXGx > svg {\n height: 20px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .mobile-datetimepicker-invalid-input {\n color: #f00 !important;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_time-input-mobile__QGutf {\n font-family: inherit;\n box-shadow: none;\n font-size: 18px;\n letter-spacing: -0.5px;\n line-height: 20px;\n font-weight: var(--font-weight-medium);\n border: none;\n outline: none;\n background: none;\n width: 54px;\n margin-left: 10px;\n color: #333;\n height: 1.5265rem;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-display__Xnjg1 {\n position: relative;\n left: 10px;\n border: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-display__Xnjg1:focus {\n outline: none;\n box-shadow: none;\n background: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-display__Xnjg1 {\n background: inherit;\n box-shadow: none;\n font-family: inherit;\n font-weight: var(--font-weight-medium);\n font-stretch: normal;\n font-style: normal;\n font-size: 16px;\n line-height: 1.33;\n letter-spacing: -0.5px;\n color: #333;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-hidden__tkKxe {\n font-size: 16px;\n margin-bottom: 12px;\n height: 0;\n width: 0;\n background: inherit;\n color: transparent;\n outline: none;\n box-shadow: none;\n border: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_mobile-input-hidden__tkKxe:focus {\n outline: none;\n box-shadow: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 {\n position: absolute;\n width: calc((100% - 50px) * 0.6);\n height: 25px;\n font-size: 15px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestion__3Hx-R {\n list-style: none;\n font-size: 16px;\n font-style: normal;\n font-weight: normal;\n line-height: 49px;\n height: 50px;\n margin: 0;\n padding: 0 0 0 16px;\n color: rgba(0, 0, 0, 0.87);\n background: white;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestionHighlighted__lexDG {\n background: #dfe5e8;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestionsContainerOpen__pzu14 {\n position: absolute;\n z-index: 19;\n max-height: 250px;\n overflow-x: hidden;\n overflow-y: scroll;\n border-radius: 5px;\n box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.06);\n border: 1px rgba(0, 0, 0, 0.1) solid;\n top: 42px;\n left: -2px;\n width: 100%;\n font-size: 16px;\n line-height: 1;\n color: rgba(0, 0, 0, 0.87);\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_suggestionsContainerOpen__pzu14 ul {\n padding: 0;\n margin: 0;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_input__Yk2Tv {\n color: black;\n box-shadow: none;\n border: none;\n background: none;\n font-size: 16px;\n font-weight: var(--font-weight-medium);\n height: 25px;\n display: inline-block;\n font-family: inherit;\n line-height: 1.33;\n padding-left: 46px;\n}\n.styles_mobile-modal-content__Gla3M .styles_input-row__r-26j .styles_input-container__US3eW .styles_container__YXGq1 .styles_input__Yk2Tv:focus {\n outline: none;\n border: none;\n background: none;\n}\n.styles_mobile-modal-content__Gla3M .styles_buttons-row__zrM35 {\n display: flex;\n flex-direction: column;\n font-size: 16px;\n font-weight: var(--font-weight-medium);\n}\n.styles_mobile-modal-content__Gla3M .styles_buttons-row__zrM35 button {\n background-color: white;\n font-family: inherit;\n font-weight: var(--font-weight-medium);\n font-size: 16px;\n letter-spacing: -0.02em;\n width: 100%;\n border-radius: 50px;\n height: 40px;\n color: var(--color);\n border: 1px solid #888;\n}\n.styles_mobile-modal-content__Gla3M .styles_buttons-row__zrM35 button.styles_ready-button__e23mO {\n color: white;\n background-color: var(--color);\n border: none;\n margin-bottom: 10px;\n}";
|
|
142
|
+
var styles = {"sr-only":"styles_sr-only__D6MJ6","dt-datetimepicker":"styles_dt-datetimepicker__DSYbh","hidden":"styles_hidden__TjnWH","datetimepicker-open-container":"styles_datetimepicker-open-container__zWBiT","top-row-container":"styles_top-row-container__rGdmj","closed":"styles_closed__QYbCF","time-icon":"styles_time-icon__ES47Y","dropdown-icon":"styles_dropdown-icon__O0GrI","close-icon":"styles_close-icon__B5-I5","label-open":"styles_label-open__dVDlF","textbutton":"styles_textbutton__sVckw","active":"styles_active__9LaDz","combobox-left":"styles_combobox-left__nwwOo","combobox-right":"styles_combobox-right__qlPKD","departure-or-arrival-container":"styles_departure-or-arrival-container__Rj3b9","radio-textbutton-label":"styles_radio-textbutton-label__LeV2o","first-radio":"styles_first-radio__F8N7J","radio-textbutton":"styles_radio-textbutton__Ad81W","departure-now-button":"styles_departure-now-button__8ZlDU","open-button":"styles_open-button__yl4be","close-button":"styles_close-button__mdfIU","right-edge":"styles_right-edge__XbqgK","combobox-container":"styles_combobox-container__sR9aU","combobox-icon":"styles_combobox-icon__wsdX4","date-input-icon":"styles_date-input-icon__SFXGx","time-input-icon":"styles_time-input-icon__J1qei","datetimepicker-select-container":"styles_datetimepicker-select-container__Y8wgE","picker-container":"styles_picker-container__wz2Zj","combobox-mobile-container":"styles_combobox-mobile-container__ZzG8g","mobile-modal-overlay":"styles_mobile-modal-overlay__wq8cS","mobile-modal-content":"styles_mobile-modal-content__Gla3M","top-row":"styles_top-row__hzke9","modal-title":"styles_modal-title__CztKf","tab-row":"styles_tab-row__6WYwM","radio-tab-label":"styles_radio-tab-label__-7Fx-","input-row":"styles_input-row__r-26j","input-container":"styles_input-container__US3eW","time-input-mobile":"styles_time-input-mobile__QGutf","mobile-input-display":"styles_mobile-input-display__Xnjg1","mobile-input-hidden":"styles_mobile-input-hidden__tkKxe","container":"styles_container__YXGq1","suggestion":"styles_suggestion__3Hx-R","suggestionHighlighted":"styles_suggestionHighlighted__lexDG","suggestionsContainerOpen":"styles_suggestionsContainerOpen__pzu14","input":"styles_input__Yk2Tv","buttons-row":"styles_buttons-row__zrM35","ready-button":"styles_ready-button__e23mO"};
|
|
143
|
+
styleInject(css_248z);
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Component to display a date or time input on desktop.
|
|
147
|
+
*
|
|
148
|
+
* @param {Object} props
|
|
149
|
+
* @param {Number} props.value Currently selected time. Unix timestamp in milliseconds
|
|
150
|
+
* @param {function} props.onChange This is called with new timestamp when input is changed
|
|
151
|
+
* @param {function} props.getDisplay Function to get a string representation from a timestamp
|
|
152
|
+
* @param {array} props.timeChoices Array of timestamps to choose from
|
|
153
|
+
* @param {string} id Id prefix for labels and aria attributes. Should be unique in page
|
|
154
|
+
* @param {string} label Text to show as input label
|
|
155
|
+
* @param {node} icon JSX for icon to show in input
|
|
156
|
+
* @param {boolean} disableTyping Set to true to disable typing in the input
|
|
157
|
+
* @param {boolean} datePicker Is the picker DatePicker or TimePicker
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* <DesktopDatetimepicker
|
|
161
|
+
* value={1590133823000}
|
|
162
|
+
* onChange={timestamp => update(timestamp)}
|
|
163
|
+
* getDisplay={timestamp => formatTime(timestamp)}
|
|
164
|
+
* timeChoices={[1590133823000]}
|
|
165
|
+
* id="timeinput"
|
|
166
|
+
* icon="<Icon />"
|
|
167
|
+
* disableTyping={false} // can be omitted if not true
|
|
168
|
+
* datePicker={true} // can be omitted if not true
|
|
169
|
+
*/
|
|
170
|
+
function DesktopDatetimepicker({
|
|
171
|
+
value,
|
|
172
|
+
onChange,
|
|
173
|
+
getDisplay,
|
|
174
|
+
timeChoices,
|
|
175
|
+
id,
|
|
176
|
+
label,
|
|
177
|
+
icon,
|
|
178
|
+
disableTyping,
|
|
179
|
+
timeZone,
|
|
180
|
+
datePicker,
|
|
181
|
+
translationSettings
|
|
182
|
+
}) {
|
|
183
|
+
Settings.defaultZone = timeZone;
|
|
184
|
+
const [t] = useTranslation();
|
|
185
|
+
const [displayValue, changeDisplayValue] = useState(getDisplay(value));
|
|
186
|
+
const [typing, setTyping] = useState(false);
|
|
187
|
+
const [showAllOptions, setShowAllOptions] = useState(true);
|
|
188
|
+
const [invalidInput, setinvalidInput] = useState(false);
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
changeDisplayValue(getDisplay(value));
|
|
191
|
+
if (!datePicker) {
|
|
192
|
+
setinvalidInput(false);
|
|
193
|
+
}
|
|
194
|
+
}, [value]);
|
|
195
|
+
|
|
196
|
+
// newValue is string
|
|
197
|
+
const handleTimestamp = newValue => {
|
|
198
|
+
const asNumber = Number(newValue);
|
|
199
|
+
try {
|
|
200
|
+
DateTime.fromMillis(asNumber);
|
|
201
|
+
} catch (e) {
|
|
202
|
+
// If the value is not a valid timestamp, we don't change it
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
onChange(asNumber);
|
|
206
|
+
};
|
|
207
|
+
function isinvalidInput(str) {
|
|
208
|
+
const regex = /[a-zA-Z§ÄäÖö.-\s]/g;
|
|
209
|
+
if (str.length < displayValue?.length) {
|
|
210
|
+
const valid = validateInput(str);
|
|
211
|
+
setinvalidInput(!valid);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (regex.test(str)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
const time = str.length > 5 ? str.slice(0, -1) : str;
|
|
218
|
+
const valid = validateInput(time);
|
|
219
|
+
setinvalidInput(!valid);
|
|
220
|
+
return str.length <= 5;
|
|
221
|
+
}
|
|
222
|
+
const onInputChange = (newValue, {
|
|
223
|
+
action
|
|
224
|
+
}) => {
|
|
225
|
+
if (action === 'menu-close') {
|
|
226
|
+
setShowAllOptions(true);
|
|
227
|
+
} else if (showAllOptions) {
|
|
228
|
+
setShowAllOptions(false);
|
|
229
|
+
}
|
|
230
|
+
if (disableTyping || newValue && !isinvalidInput(newValue)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (action === 'input-change') {
|
|
234
|
+
const validated = parseTypedTime(newValue);
|
|
235
|
+
changeDisplayValue(validated);
|
|
236
|
+
setTyping(true);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
const options = timeChoices.map(time => {
|
|
240
|
+
return {
|
|
241
|
+
value: time.toString(),
|
|
242
|
+
label: getDisplay(time)
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Time picker has a list of minutes, instead of 15 minutes. We need to filter those out
|
|
247
|
+
// so closest option will show up in the list.
|
|
248
|
+
const closestOptions = datePicker ? options : options.filter(o => o.label.split(':')[1] % 15 === 0);
|
|
249
|
+
const closestOption = closestOptions.length > 0 ? closestOptions?.reduce((a, b) => Math.abs(value - a.value) < Math.abs(value - b.value) ? a : b) : undefined;
|
|
250
|
+
const inputId = `${id}-input`;
|
|
251
|
+
const labelId = `${id}-label`;
|
|
252
|
+
const filterOptions = (option, input) => {
|
|
253
|
+
const hour = input.length <= 2 ? input : input.split(':')[0];
|
|
254
|
+
const isValidHour = /^([0-1]?[0-9]|2[0-3])$/.test(hour);
|
|
255
|
+
const comp = hour.length === 1 ? '0'.concat(input) : input;
|
|
256
|
+
return isValidHour ? option.label.split(':')[0] === comp.split(':')[0] : true;
|
|
257
|
+
};
|
|
258
|
+
const ariaError = t('invalid-input', translationSettings);
|
|
259
|
+
return /*#__PURE__*/React.createElement("label", {
|
|
260
|
+
className: styles['combobox-container'],
|
|
261
|
+
htmlFor: inputId
|
|
262
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
263
|
+
className: styles['sr-only'],
|
|
264
|
+
id: labelId
|
|
265
|
+
}, label, " ", displayValue), icon, /*#__PURE__*/React.createElement(Select, {
|
|
266
|
+
"aria-labelledby": labelId,
|
|
267
|
+
ariaLiveMessages: {
|
|
268
|
+
guidance: context => {
|
|
269
|
+
// When user types invalid value, isDisabled becomes undefined instead of false
|
|
270
|
+
if (context.isDisabled === undefined && invalidInput) {
|
|
271
|
+
return ariaError;
|
|
272
|
+
}
|
|
273
|
+
return '.'; // this can't be empty for some reason
|
|
274
|
+
},
|
|
275
|
+
onChange: () => {
|
|
276
|
+
return '';
|
|
277
|
+
},
|
|
278
|
+
onFilter: () => {
|
|
279
|
+
return '';
|
|
280
|
+
},
|
|
281
|
+
onFocus: ({
|
|
282
|
+
context,
|
|
283
|
+
label: itemLabel
|
|
284
|
+
}) => {
|
|
285
|
+
if (context === 'menu') {
|
|
286
|
+
return itemLabel;
|
|
287
|
+
}
|
|
288
|
+
return '';
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
options: options,
|
|
292
|
+
inputId: inputId,
|
|
293
|
+
onChange: time => {
|
|
294
|
+
const currentTime = DateTime.fromMillis(value).toFormat('HH:mm');
|
|
295
|
+
const ts = getTs(displayValue, value);
|
|
296
|
+
if (typing) {
|
|
297
|
+
if (ts !== null) {
|
|
298
|
+
if (currentTime !== displayValue && time.value === value) {
|
|
299
|
+
handleTimestamp(ts);
|
|
300
|
+
} else {
|
|
301
|
+
handleTimestamp(time.value);
|
|
302
|
+
}
|
|
303
|
+
setTyping(false);
|
|
304
|
+
} else {
|
|
305
|
+
if (time.value !== value) {
|
|
306
|
+
handleTimestamp(time.value);
|
|
307
|
+
} else {
|
|
308
|
+
// reset value
|
|
309
|
+
changeDisplayValue(getDisplay(value));
|
|
310
|
+
}
|
|
311
|
+
setTyping(false);
|
|
312
|
+
}
|
|
313
|
+
if (!datePicker) {
|
|
314
|
+
setinvalidInput(false);
|
|
315
|
+
}
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (!datePicker) {
|
|
319
|
+
setinvalidInput(false);
|
|
320
|
+
}
|
|
321
|
+
handleTimestamp(time.value);
|
|
322
|
+
},
|
|
323
|
+
components: {
|
|
324
|
+
IndicatorsContainer: () => null
|
|
325
|
+
},
|
|
326
|
+
className: cx(styles['datetimepicker-select-container'], invalidInput && 'datetimepicker-invalid-input'),
|
|
327
|
+
classNamePrefix: "datetimepicker-select",
|
|
328
|
+
onInputChange: onInputChange,
|
|
329
|
+
inputValue: !disableTyping && displayValue,
|
|
330
|
+
value: closestOption,
|
|
331
|
+
noOptionsMessage: () => {
|
|
332
|
+
return '';
|
|
333
|
+
},
|
|
334
|
+
filterOption: (option, input) => {
|
|
335
|
+
if (datePicker) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
const completeInput = input.length === 5 || input.length === 4 && input.split(':')[0].length === 1;
|
|
339
|
+
const isMod15 = option.label.split(':')[1] % 15 === 0;
|
|
340
|
+
const minuteInput = input.split(':')[1]?.length === 1;
|
|
341
|
+
if (showAllOptions && isMod15) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
if (minuteInput) {
|
|
345
|
+
const inputH = input.split(':')[0].length === 1 ? '0'.concat(input.split(':')[0]) : input.split(':')[0];
|
|
346
|
+
const inputM = input.split(':')[1];
|
|
347
|
+
const optH = option.label.split(':')[0];
|
|
348
|
+
const optM = option.label.split(':')[1];
|
|
349
|
+
if (inputH === optH) {
|
|
350
|
+
const time = Number(inputM) * 10;
|
|
351
|
+
const total = Number(optM) - time;
|
|
352
|
+
if (total >= 0 && total <= 9) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (completeInput && !showAllOptions) {
|
|
359
|
+
return input.length === 4 ? '0'.concat(input) === option.label : input === option.label;
|
|
360
|
+
}
|
|
361
|
+
if (isMod15) {
|
|
362
|
+
return filterOptions(option, input);
|
|
363
|
+
}
|
|
364
|
+
return false;
|
|
365
|
+
},
|
|
366
|
+
controlShouldRenderValue: disableTyping,
|
|
367
|
+
tabSelectsValue: false,
|
|
368
|
+
placeholder: "",
|
|
369
|
+
onFocus: e => {
|
|
370
|
+
if (!disableTyping) {
|
|
371
|
+
e.target.select();
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
onBlur: () => {
|
|
375
|
+
// removing focus also locks in value
|
|
376
|
+
if (typing) {
|
|
377
|
+
const validated = getTs(displayValue, value);
|
|
378
|
+
if (validated !== null) {
|
|
379
|
+
handleTimestamp(validated);
|
|
380
|
+
setTyping(false);
|
|
381
|
+
} else {
|
|
382
|
+
changeDisplayValue(getDisplay(value));
|
|
383
|
+
setTyping(false);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (!datePicker) {
|
|
387
|
+
setinvalidInput(false);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
onKeyDown: e => {
|
|
391
|
+
if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
|
|
392
|
+
setTyping(false);
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
isSearchable: !disableTyping
|
|
396
|
+
}));
|
|
397
|
+
}
|
|
398
|
+
process.env.NODE_ENV !== "production" ? DesktopDatetimepicker.propTypes = {
|
|
399
|
+
value: PropTypes.number.isRequired,
|
|
400
|
+
onChange: PropTypes.func.isRequired,
|
|
401
|
+
getDisplay: PropTypes.func.isRequired,
|
|
402
|
+
timeChoices: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
403
|
+
id: PropTypes.string.isRequired,
|
|
404
|
+
label: PropTypes.node.isRequired,
|
|
405
|
+
icon: PropTypes.node.isRequired,
|
|
406
|
+
disableTyping: PropTypes.bool,
|
|
407
|
+
timeZone: PropTypes.string,
|
|
408
|
+
datePicker: PropTypes.bool,
|
|
409
|
+
translationSettings: PropTypes.shape({
|
|
410
|
+
lng: PropTypes.string.isRequired
|
|
411
|
+
})
|
|
412
|
+
} : void 0;
|
|
413
|
+
DesktopDatetimepicker.defaultProps = {
|
|
414
|
+
disableTyping: false,
|
|
415
|
+
timeZone: 'Europe/Helsinki',
|
|
416
|
+
datePicker: false,
|
|
417
|
+
translationSettings: {
|
|
418
|
+
lng: 'fi'
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Detect if user is on a mobile device by parsing userAgent. This is not 100% reliable but close enough.
|
|
424
|
+
*
|
|
425
|
+
* @return {boolean}
|
|
426
|
+
*/
|
|
427
|
+
function isMobile() {
|
|
428
|
+
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
|
429
|
+
const mobile = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(userAgent);
|
|
430
|
+
return mobile;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Detect if user is on an android device. Not 100% reliable.
|
|
435
|
+
*/
|
|
436
|
+
function isAndroid() {
|
|
437
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
438
|
+
return userAgent.indexOf('android') > -1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Detect if input type="date" and type="time" are supported in by the browser
|
|
443
|
+
*
|
|
444
|
+
* @return {boolean}
|
|
445
|
+
*/
|
|
446
|
+
function dateTimeInputIsSupported() {
|
|
447
|
+
if (typeof window === 'undefined' || window === null) {
|
|
448
|
+
return false;
|
|
449
|
+
}
|
|
450
|
+
const elem = document.createElement('input');
|
|
451
|
+
elem.type = 'date';
|
|
452
|
+
if (elem.type !== 'date') {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
elem.type = 'time';
|
|
456
|
+
if (elem.type !== 'time') {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const AndroidSelect = ({
|
|
463
|
+
value,
|
|
464
|
+
dateChoices,
|
|
465
|
+
id,
|
|
466
|
+
getDisplay,
|
|
467
|
+
onChange,
|
|
468
|
+
labelId,
|
|
469
|
+
inputId
|
|
470
|
+
}) => {
|
|
471
|
+
const items = dateChoices.map(date => ({
|
|
472
|
+
value: date,
|
|
473
|
+
label: getDisplay(date)
|
|
474
|
+
}));
|
|
475
|
+
const selectedItem = items.find(option => option.value === value) || items[0] || null;
|
|
476
|
+
const {
|
|
477
|
+
isOpen,
|
|
478
|
+
getToggleButtonProps,
|
|
479
|
+
getMenuProps,
|
|
480
|
+
getItemProps,
|
|
481
|
+
highlightedIndex
|
|
482
|
+
} = useSelect({
|
|
483
|
+
selectedItem,
|
|
484
|
+
items,
|
|
485
|
+
itemToString: item => item?.label ?? '',
|
|
486
|
+
onSelectedItemChange: ({
|
|
487
|
+
selectedItem: nextSelectedItem
|
|
488
|
+
}) => {
|
|
489
|
+
if (nextSelectedItem) {
|
|
490
|
+
onChange(nextSelectedItem.value);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
id
|
|
494
|
+
});
|
|
495
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
496
|
+
className: styles.container
|
|
497
|
+
}, /*#__PURE__*/React.createElement("div", getToggleButtonProps({
|
|
498
|
+
'aria-labelledby': labelId,
|
|
499
|
+
id: inputId
|
|
500
|
+
}), /*#__PURE__*/React.createElement("div", {
|
|
501
|
+
className: styles.input
|
|
502
|
+
}, selectedItem.label)), /*#__PURE__*/React.createElement("div", {
|
|
503
|
+
className: cx(styles.suggestionsContainerOpen, !isOpen && 'hidden')
|
|
504
|
+
}, /*#__PURE__*/React.createElement("ul", getMenuProps(), isOpen && items.map((option, index) => /*#__PURE__*/React.createElement("li", Object.assign({
|
|
505
|
+
key: option.value,
|
|
506
|
+
className: cx(styles.suggestion, index === highlightedIndex && styles.suggestionHighlighted)
|
|
507
|
+
}, getItemProps({
|
|
508
|
+
index
|
|
509
|
+
})), /*#__PURE__*/React.createElement("span", null, option.label))))));
|
|
510
|
+
};
|
|
511
|
+
process.env.NODE_ENV !== "production" ? AndroidSelect.propTypes = {
|
|
512
|
+
value: PropTypes.number.isRequired,
|
|
513
|
+
dateChoices: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
514
|
+
id: PropTypes.string.isRequired,
|
|
515
|
+
getDisplay: PropTypes.func.isRequired,
|
|
516
|
+
onChange: PropTypes.func.isRequired,
|
|
517
|
+
labelId: PropTypes.string.isRequired,
|
|
518
|
+
inputId: PropTypes.string.isRequired
|
|
519
|
+
} : void 0;
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Component to display a date input on mobile
|
|
523
|
+
*/
|
|
524
|
+
function MobileDatepicker({
|
|
525
|
+
value,
|
|
526
|
+
getDisplay,
|
|
527
|
+
onChange,
|
|
528
|
+
itemCount,
|
|
529
|
+
startTime,
|
|
530
|
+
id,
|
|
531
|
+
label,
|
|
532
|
+
icon,
|
|
533
|
+
timeZone
|
|
534
|
+
}) {
|
|
535
|
+
Settings.defaultZone = timeZone;
|
|
536
|
+
const dateChoices = Array(itemCount).fill().map((_, i) => DateTime.fromMillis(startTime).plus({
|
|
537
|
+
days: i
|
|
538
|
+
}).toMillis());
|
|
539
|
+
const nativeInput = !isAndroid();
|
|
540
|
+
const labelId = `${id}-label`;
|
|
541
|
+
const inputId = `${id}-input`;
|
|
542
|
+
return /*#__PURE__*/React.createElement("label", {
|
|
543
|
+
className: styles['input-container'],
|
|
544
|
+
htmlFor: inputId
|
|
545
|
+
}, /*#__PURE__*/React.createElement("span", null, icon), /*#__PURE__*/React.createElement("span", {
|
|
546
|
+
className: styles['sr-only'],
|
|
547
|
+
id: labelId
|
|
548
|
+
}, label), nativeInput ? /*#__PURE__*/React.createElement("select", {
|
|
549
|
+
id: inputId,
|
|
550
|
+
className: styles['mobile-input-display'],
|
|
551
|
+
onChange: e => onChange(Number(e.target.value)),
|
|
552
|
+
value: value
|
|
553
|
+
}, dateChoices.map(date => {
|
|
554
|
+
return /*#__PURE__*/React.createElement("option", {
|
|
555
|
+
key: date,
|
|
556
|
+
value: date
|
|
557
|
+
}, getDisplay(date));
|
|
558
|
+
})) : /*#__PURE__*/React.createElement(AndroidSelect, {
|
|
559
|
+
value,
|
|
560
|
+
dateChoices,
|
|
561
|
+
id,
|
|
562
|
+
getDisplay,
|
|
563
|
+
onChange,
|
|
564
|
+
labelId,
|
|
565
|
+
inputId
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
process.env.NODE_ENV !== "production" ? MobileDatepicker.propTypes = {
|
|
569
|
+
value: PropTypes.number.isRequired,
|
|
570
|
+
getDisplay: PropTypes.func.isRequired,
|
|
571
|
+
onChange: PropTypes.func.isRequired,
|
|
572
|
+
itemCount: PropTypes.number.isRequired,
|
|
573
|
+
startTime: PropTypes.number.isRequired,
|
|
574
|
+
id: PropTypes.string.isRequired,
|
|
575
|
+
label: PropTypes.string.isRequired,
|
|
576
|
+
icon: PropTypes.node,
|
|
577
|
+
timeZone: PropTypes.string
|
|
578
|
+
} : void 0;
|
|
579
|
+
MobileDatepicker.defaultProps = {
|
|
580
|
+
icon: null,
|
|
581
|
+
timeZone: 'Europe/Helsinki'
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Component to display a time input on mobile
|
|
586
|
+
*/
|
|
587
|
+
function MobileTimepicker({
|
|
588
|
+
value,
|
|
589
|
+
getDisplay,
|
|
590
|
+
onChange,
|
|
591
|
+
id,
|
|
592
|
+
label,
|
|
593
|
+
icon,
|
|
594
|
+
timeZone
|
|
595
|
+
}) {
|
|
596
|
+
const [inputValue, changeInputValue] = useState(getDisplay(value));
|
|
597
|
+
const [isValidInput, setValidInput] = useState(true);
|
|
598
|
+
Settings.defaultZone = timeZone;
|
|
599
|
+
const inputId = `${id}-input`;
|
|
600
|
+
const labelId = `${id}-label`;
|
|
601
|
+
const showError = !isValidInput;
|
|
602
|
+
return /*#__PURE__*/React.createElement("label", {
|
|
603
|
+
className: styles['input-container'],
|
|
604
|
+
htmlFor: inputId
|
|
605
|
+
}, /*#__PURE__*/React.createElement("span", null, icon), /*#__PURE__*/React.createElement("span", {
|
|
606
|
+
className: styles['sr-only'],
|
|
607
|
+
id: labelId
|
|
608
|
+
}, label), /*#__PURE__*/React.createElement("input", {
|
|
609
|
+
id: inputId,
|
|
610
|
+
inputMode: "numeric",
|
|
611
|
+
type: "text",
|
|
612
|
+
maxLength: "6",
|
|
613
|
+
className: cx(styles['time-input-mobile'], showError && 'mobile-datetimepicker-invalid-input'),
|
|
614
|
+
value: inputValue,
|
|
615
|
+
onFocus: e => {
|
|
616
|
+
e.target.setSelectionRange(0, 0); // set caret to start of input
|
|
617
|
+
},
|
|
618
|
+
onChange: event => {
|
|
619
|
+
let newValue = event.target.value.replace('.', ':');
|
|
620
|
+
const valid = validateInput(newValue);
|
|
621
|
+
setValidInput(valid);
|
|
622
|
+
if (
|
|
623
|
+
// number typed as first char => clear rest of the input
|
|
624
|
+
newValue.match(/[0-9]{3}:[0-9]{2}/) && newValue.slice(1) === inputValue) {
|
|
625
|
+
newValue = newValue.charAt(0);
|
|
626
|
+
}
|
|
627
|
+
if (newValue.length > 5) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const actual = parseTypedTime(newValue);
|
|
631
|
+
changeInputValue(actual);
|
|
632
|
+
const timestamp = getTs(actual, value);
|
|
633
|
+
if (timestamp) {
|
|
634
|
+
onChange(timestamp);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}));
|
|
638
|
+
}
|
|
639
|
+
process.env.NODE_ENV !== "production" ? MobileTimepicker.propTypes = {
|
|
640
|
+
value: PropTypes.number.isRequired,
|
|
641
|
+
getDisplay: PropTypes.func.isRequired,
|
|
642
|
+
onChange: PropTypes.func.isRequired,
|
|
643
|
+
id: PropTypes.string.isRequired,
|
|
644
|
+
label: PropTypes.string.isRequired,
|
|
645
|
+
icon: PropTypes.node,
|
|
646
|
+
timeZone: PropTypes.string
|
|
647
|
+
} : void 0;
|
|
648
|
+
MobileTimepicker.defaultProps = {
|
|
649
|
+
icon: null,
|
|
650
|
+
timeZone: 'Europe/Helsinki'
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
Settings.defaultLocale = 'en';
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* @param {object} props
|
|
657
|
+
* @param {'arrival'|'departure'} props.departureOrArrival Whether the picker is for arrival or departure time
|
|
658
|
+
* @param {function} props.onNowClick Called when the "Now" button is clicked
|
|
659
|
+
* @param {string} props.lang Language code for translations
|
|
660
|
+
* @param {string} props.color Color for the component, used in styles
|
|
661
|
+
* @param {string} props.timeZone Time zone to use for date and time calculations
|
|
662
|
+
* @param {function} props.onSubmit Called when the user submits the selected date and time
|
|
663
|
+
* @param {function} props.onCancel called when the user cancels the selection
|
|
664
|
+
* @param {number} props.timestamp Initial timestamp to display in the picker
|
|
665
|
+
* @param {function} props.getTimeDisplay Called to get the display string for a given timestamp
|
|
666
|
+
* @param {number} props.dateSelectItemCount Determines how many date options are shown in the date picker
|
|
667
|
+
* @param {function} props.getDateDisplay Called to get the display string for a given date
|
|
668
|
+
* @param {object} props.fontWeights Font weights used in the component styles
|
|
669
|
+
* @returns {JSX.Element}
|
|
670
|
+
*/
|
|
671
|
+
function MobilePickerModal({
|
|
672
|
+
departureOrArrival,
|
|
673
|
+
onNowClick,
|
|
674
|
+
lang,
|
|
675
|
+
color,
|
|
676
|
+
timeZone,
|
|
677
|
+
onSubmit,
|
|
678
|
+
onCancel,
|
|
679
|
+
timestamp,
|
|
680
|
+
getTimeDisplay,
|
|
681
|
+
dateSelectItemCount,
|
|
682
|
+
getDateDisplay,
|
|
683
|
+
fontWeights,
|
|
684
|
+
onAfterClose
|
|
685
|
+
}) {
|
|
686
|
+
Settings.defaultZone = timeZone;
|
|
687
|
+
const [t] = useTranslation();
|
|
688
|
+
const translationSettings = {
|
|
689
|
+
lng: lang
|
|
690
|
+
};
|
|
691
|
+
const [displayTimestamp, changeTimestamp] = useState(timestamp);
|
|
692
|
+
const [departureOrArrivalCurrent, changeDepartureOrArrival] = useState(departureOrArrival);
|
|
693
|
+
function onArrivalClick() {
|
|
694
|
+
changeDepartureOrArrival('arrival');
|
|
695
|
+
}
|
|
696
|
+
function onDepartureClick() {
|
|
697
|
+
changeDepartureOrArrival('departure');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// for input labels
|
|
701
|
+
const [htmlId] = useState(uniqueId('datetimepicker-'));
|
|
702
|
+
const dateSelectStartTime = DateTime.now().startOf('day').set({
|
|
703
|
+
hour: DateTime.fromMillis(displayTimestamp).hour,
|
|
704
|
+
minute: DateTime.fromMillis(displayTimestamp).minute
|
|
705
|
+
}).toMillis();
|
|
706
|
+
return /*#__PURE__*/React.createElement(Modal, {
|
|
707
|
+
appElement: document ? document.querySelector('#app') : undefined,
|
|
708
|
+
isOpen: true,
|
|
709
|
+
className: styles['mobile-modal-content'],
|
|
710
|
+
overlayClassName: styles['mobile-modal-overlay'],
|
|
711
|
+
onAfterClose: onAfterClose
|
|
712
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
713
|
+
id: "digitransit-mobile-datetime",
|
|
714
|
+
style: {
|
|
715
|
+
'--color': `${color}`,
|
|
716
|
+
'--font-weight-medium': fontWeights.medium
|
|
717
|
+
}
|
|
718
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
719
|
+
className: styles['top-row']
|
|
720
|
+
}, /*#__PURE__*/React.createElement("h3", {
|
|
721
|
+
className: styles['modal-title']
|
|
722
|
+
}, t('choose-time', translationSettings)), /*#__PURE__*/React.createElement("button", {
|
|
723
|
+
type: "button",
|
|
724
|
+
className: styles['departure-now-button'],
|
|
725
|
+
onClick: onNowClick
|
|
726
|
+
}, t('departure-now', translationSettings))), /*#__PURE__*/React.createElement("div", {
|
|
727
|
+
className: styles['tab-row']
|
|
728
|
+
}, /*#__PURE__*/React.createElement("label", {
|
|
729
|
+
htmlFor: `${htmlId}-modal-departure`,
|
|
730
|
+
className: `${styles['radio-tab-label']}
|
|
731
|
+
${styles[departureOrArrivalCurrent === 'departure' ? 'active' : undefined]}`
|
|
732
|
+
}, t('departure', translationSettings), /*#__PURE__*/React.createElement("input", {
|
|
733
|
+
id: `${htmlId}-modal-departure`,
|
|
734
|
+
name: "departureOrArrival",
|
|
735
|
+
type: "radio",
|
|
736
|
+
value: "departure",
|
|
737
|
+
className: styles['radio-textbutton'],
|
|
738
|
+
onChange: () => {
|
|
739
|
+
onDepartureClick();
|
|
740
|
+
},
|
|
741
|
+
checked: departureOrArrivalCurrent === 'departure'
|
|
742
|
+
})), /*#__PURE__*/React.createElement("label", {
|
|
743
|
+
htmlFor: `${htmlId}-modal-arrival`,
|
|
744
|
+
className: `${styles['radio-tab-label']}
|
|
745
|
+
${styles[departureOrArrivalCurrent === 'arrival' ? 'active' : undefined]}`
|
|
746
|
+
}, t('arrival', translationSettings), /*#__PURE__*/React.createElement("input", {
|
|
747
|
+
id: `${htmlId}-modal-arrival`,
|
|
748
|
+
name: "departureOrArrival",
|
|
749
|
+
type: "radio",
|
|
750
|
+
value: "arrival",
|
|
751
|
+
className: styles['radio-textbutton'],
|
|
752
|
+
onChange: () => {
|
|
753
|
+
onArrivalClick();
|
|
754
|
+
},
|
|
755
|
+
checked: departureOrArrivalCurrent === 'arrival'
|
|
756
|
+
}))), /*#__PURE__*/React.createElement("div", {
|
|
757
|
+
className: styles['input-row']
|
|
758
|
+
}, /*#__PURE__*/React.createElement(MobileDatepicker, {
|
|
759
|
+
value: displayTimestamp,
|
|
760
|
+
getDisplay: getDateDisplay,
|
|
761
|
+
onChange: changeTimestamp,
|
|
762
|
+
itemCount: dateSelectItemCount,
|
|
763
|
+
startTime: dateSelectStartTime,
|
|
764
|
+
id: `${htmlId}-date`,
|
|
765
|
+
label: t('date', translationSettings),
|
|
766
|
+
icon: /*#__PURE__*/React.createElement("span", {
|
|
767
|
+
className: `${styles['combobox-icon']} ${styles['date-input-icon']}`
|
|
768
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
769
|
+
img: "calendar",
|
|
770
|
+
color: color
|
|
771
|
+
})),
|
|
772
|
+
timeZone: timeZone
|
|
773
|
+
}), /*#__PURE__*/React.createElement(MobileTimepicker, {
|
|
774
|
+
value: displayTimestamp,
|
|
775
|
+
getDisplay: getTimeDisplay,
|
|
776
|
+
onChange: changeTimestamp,
|
|
777
|
+
id: `${htmlId}-time`,
|
|
778
|
+
label: t('time', translationSettings),
|
|
779
|
+
icon: /*#__PURE__*/React.createElement("span", {
|
|
780
|
+
className: `${styles['combobox-icon']} ${styles['time-input-icon']}`
|
|
781
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
782
|
+
img: "time",
|
|
783
|
+
color: color
|
|
784
|
+
})),
|
|
785
|
+
timeZone: timeZone
|
|
786
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
787
|
+
className: styles['buttons-row']
|
|
788
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
789
|
+
type: "button",
|
|
790
|
+
className: styles['ready-button'],
|
|
791
|
+
onClick: () => onSubmit(displayTimestamp, departureOrArrivalCurrent)
|
|
792
|
+
}, t('ready', translationSettings)), /*#__PURE__*/React.createElement("button", {
|
|
793
|
+
type: "button",
|
|
794
|
+
className: styles['cancel-button'],
|
|
795
|
+
onClick: onCancel
|
|
796
|
+
}, t('cancel', translationSettings)))));
|
|
797
|
+
}
|
|
798
|
+
process.env.NODE_ENV !== "production" ? MobilePickerModal.propTypes = {
|
|
799
|
+
timeZone: PropTypes.string,
|
|
800
|
+
departureOrArrival: PropTypes.oneOf(['departure', 'arrival']).isRequired,
|
|
801
|
+
onNowClick: PropTypes.func.isRequired,
|
|
802
|
+
lang: PropTypes.string.isRequired,
|
|
803
|
+
color: PropTypes.string,
|
|
804
|
+
onSubmit: PropTypes.func.isRequired,
|
|
805
|
+
onCancel: PropTypes.func.isRequired,
|
|
806
|
+
onAfterClose: PropTypes.func.isRequired,
|
|
807
|
+
timestamp: PropTypes.number.isRequired,
|
|
808
|
+
getTimeDisplay: PropTypes.func.isRequired,
|
|
809
|
+
dateSelectItemCount: PropTypes.number.isRequired,
|
|
810
|
+
getDateDisplay: PropTypes.func.isRequired,
|
|
811
|
+
fontWeights: PropTypes.shape({
|
|
812
|
+
medium: PropTypes.number.isRequired
|
|
813
|
+
}).isRequired
|
|
814
|
+
} : void 0;
|
|
815
|
+
MobilePickerModal.defaultProps = {
|
|
816
|
+
color: '#007ac9',
|
|
817
|
+
timeZone: 'Europe/Helsinki'
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
Settings.defaultLocale = 'en';
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* This component renders combobox style inputs for selecting date and time. This is a controlled component, timestamp is the current value of both inputs.
|
|
824
|
+
* @param {Object} props
|
|
825
|
+
*
|
|
826
|
+
* @param {Number} props.timestamp Currently selected time as a unix timestamp in milliseconds. Set to null to signify that "now" is selected. Displayed time is updated in realtime when set to null
|
|
827
|
+
* @param {function} props.onTimeChange Called with new timestamp when time input changes
|
|
828
|
+
* @param {function} props.onDateChange Called with new timestamp when date input changes
|
|
829
|
+
* @param {'arrival'|'departure'} props.departureOrArrival Determine if input is set to choose departure or arrival time
|
|
830
|
+
* @param {function} props.onNowClick Called when "depart now" button is clicked
|
|
831
|
+
* @param {function} props.onDepartureClick Called when "departure" button is clicked
|
|
832
|
+
* @param {function} props.onArrivalClick Called when "arrival" button is clicked
|
|
833
|
+
* @param {node} props.embedWhenClosed JSX element to render in the corner when input is closed
|
|
834
|
+
* @param {node} props.embedWhenOpen JSX element to render when input is open
|
|
835
|
+
* @param {string} props.lang Language selection
|
|
836
|
+
* @param {number} props.serviceTimeRange Determine number of days shown in timepicker. Optional. default is 30.
|
|
837
|
+
* @param {function} props.onOpen Determine what to do when timepicker is open. Optional. no default implementation.
|
|
838
|
+
* @param {function} props.onClose Determine what to do when timepicker is closed. Optional. no default implementation.
|
|
839
|
+
* @param {function} props.openPicker Determine if timepicker should be open in intial render. Optional. Default is undefined.
|
|
840
|
+
*
|
|
841
|
+
*
|
|
842
|
+
* @example
|
|
843
|
+
* <Datetimepicker
|
|
844
|
+
* timestamp={1590133823000}
|
|
845
|
+
* onTimeChange={(newTimestamp) => update(newTimestamp)}
|
|
846
|
+
* onDateChange={(newTimestamp) => update(newTimestamp)}
|
|
847
|
+
* departureOrArrival={'departure'}
|
|
848
|
+
* onNowClick={() => setTimestampToNull()}
|
|
849
|
+
* onDepartureClick={() => departureClicked()}
|
|
850
|
+
* onArrivalClick={() => arrivalClicked()}
|
|
851
|
+
* embedWhenClosed={<button />}
|
|
852
|
+
* lang={'en'}
|
|
853
|
+
* serviceTimeRange={15}
|
|
854
|
+
* />
|
|
855
|
+
*/
|
|
856
|
+
function Datetimepicker({
|
|
857
|
+
timestamp,
|
|
858
|
+
onTimeChange,
|
|
859
|
+
onDateChange,
|
|
860
|
+
departureOrArrival,
|
|
861
|
+
onNowClick,
|
|
862
|
+
onDepartureClick,
|
|
863
|
+
onArrivalClick,
|
|
864
|
+
embedWhenClosed,
|
|
865
|
+
embedWhenOpen,
|
|
866
|
+
lang,
|
|
867
|
+
color,
|
|
868
|
+
timeZone,
|
|
869
|
+
onModalSubmit,
|
|
870
|
+
fontWeights,
|
|
871
|
+
serviceTimeRange,
|
|
872
|
+
onOpen,
|
|
873
|
+
onClose,
|
|
874
|
+
openPicker
|
|
875
|
+
}) {
|
|
876
|
+
Settings.defaultZone = timeZone;
|
|
877
|
+
const [t] = useTranslation();
|
|
878
|
+
const [isOpen, changeOpen] = useState(openPicker || false);
|
|
879
|
+
const [displayTimestamp, changeDisplayTimestamp] = useState(timestamp || DateTime.now().toMillis());
|
|
880
|
+
// timer for updating displayTimestamp in real time
|
|
881
|
+
const [timerId, setTimer] = useState(null);
|
|
882
|
+
// for input labels
|
|
883
|
+
const [htmlId] = useState(uniqueId('datetimepicker-'));
|
|
884
|
+
const [useMobileInputs] = useState(isMobile() && dateTimeInputIsSupported());
|
|
885
|
+
const openPickerRef = useRef();
|
|
886
|
+
const inputRef = useRef();
|
|
887
|
+
const alertRef = useRef();
|
|
888
|
+
const translationSettings = {
|
|
889
|
+
lng: lang
|
|
890
|
+
};
|
|
891
|
+
useEffect(() => {
|
|
892
|
+
Settings.defaultLocale = lang;
|
|
893
|
+
}, [lang]);
|
|
894
|
+
const nowSelected = timestamp === null;
|
|
895
|
+
useEffect(() => {
|
|
896
|
+
if (nowSelected) {
|
|
897
|
+
changeDisplayTimestamp(DateTime.now().toMillis());
|
|
898
|
+
} else {
|
|
899
|
+
// clear timer
|
|
900
|
+
if (timerId) {
|
|
901
|
+
clearInterval(timerId);
|
|
902
|
+
setTimer(null);
|
|
903
|
+
}
|
|
904
|
+
changeDisplayTimestamp(timestamp);
|
|
905
|
+
}
|
|
906
|
+
}, [timestamp]);
|
|
907
|
+
|
|
908
|
+
// set timer to update displayTimestamp when minute changes if nowSelected
|
|
909
|
+
useEffect(() => {
|
|
910
|
+
if (!nowSelected) {
|
|
911
|
+
return undefined;
|
|
912
|
+
}
|
|
913
|
+
if (timerId) {
|
|
914
|
+
clearInterval(timerId);
|
|
915
|
+
}
|
|
916
|
+
const newId = setInterval(() => {
|
|
917
|
+
const now = DateTime.now();
|
|
918
|
+
const sameMinute = DateTime.fromMillis(displayTimestamp).hasSame(now, 'minute');
|
|
919
|
+
if (!sameMinute) {
|
|
920
|
+
clearInterval(newId);
|
|
921
|
+
setTimer(null);
|
|
922
|
+
changeDisplayTimestamp(now.toMillis());
|
|
923
|
+
}
|
|
924
|
+
}, 5000);
|
|
925
|
+
setTimer(newId);
|
|
926
|
+
return () => clearInterval(newId);
|
|
927
|
+
}, [displayTimestamp]);
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* @param {number} date Date in milliseconds since unix epoch
|
|
931
|
+
* @returns {string} formatted date
|
|
932
|
+
*/
|
|
933
|
+
const getDateDisplay = date => {
|
|
934
|
+
const time = DateTime.fromMillis(date);
|
|
935
|
+
const now = DateTime.now();
|
|
936
|
+
let formatted;
|
|
937
|
+
if (time.hasSame(now, 'day')) {
|
|
938
|
+
formatted = t('today', translationSettings);
|
|
939
|
+
formatted = `${formatted.charAt(0).toUpperCase()}${formatted.slice(1)}`;
|
|
940
|
+
} else if (time.hasSame(now.plus({
|
|
941
|
+
days: 1
|
|
942
|
+
}), 'day')) {
|
|
943
|
+
formatted = t('tomorrow', translationSettings);
|
|
944
|
+
formatted = `${formatted.charAt(0).toUpperCase()}${formatted.slice(1)}`;
|
|
945
|
+
} else {
|
|
946
|
+
formatted = time.toFormat('ccc d.L.');
|
|
947
|
+
}
|
|
948
|
+
return formatted;
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// param time is timestamp
|
|
952
|
+
const getTimeDisplay = time => {
|
|
953
|
+
return DateTime.fromMillis(time).toFormat('HH:mm');
|
|
954
|
+
};
|
|
955
|
+
|
|
956
|
+
// compute choices only when timestamp changes instead of every render
|
|
957
|
+
const [timeChoices, dateChoices] = useMemo(() => {
|
|
958
|
+
const selectedMoment = DateTime.fromMillis(timestamp || DateTime.now().toMillis());
|
|
959
|
+
const timeSelectStartTime = selectedMoment.startOf('day').toMillis();
|
|
960
|
+
const isMobileCurrently = isMobile();
|
|
961
|
+
let newTimeChoices = [];
|
|
962
|
+
let current = selectedMoment.startOf('day');
|
|
963
|
+
while (current.hasSame(timeSelectStartTime, 'day')) {
|
|
964
|
+
newTimeChoices.push(current.valueOf());
|
|
965
|
+
if (isMobileCurrently) {
|
|
966
|
+
current = current.plus({
|
|
967
|
+
minutes: 15
|
|
968
|
+
});
|
|
969
|
+
} else {
|
|
970
|
+
current = current.plus({
|
|
971
|
+
minutes: 1
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (timestamp === null) {
|
|
976
|
+
// if time is set to now
|
|
977
|
+
// add times in 5 min intervals for next 30 mins
|
|
978
|
+
const fiveMinutes = 1000 * 60 * 5;
|
|
979
|
+
const nextFiveMin = Math.ceil(displayTimestamp / fiveMinutes) * fiveMinutes;
|
|
980
|
+
const timesToAdd = Array(7).fill().map((_, i) => nextFiveMin + i * fiveMinutes);
|
|
981
|
+
const closestIndexAfter = newTimeChoices.map(time => displayTimestamp - time).findIndex(time => time <= 0);
|
|
982
|
+
newTimeChoices.splice(closestIndexAfter, 0, ...timesToAdd);
|
|
983
|
+
newTimeChoices = Array.from(new Set(newTimeChoices)); // remove duplicates
|
|
984
|
+
}
|
|
985
|
+
const dateSelectStartTime = DateTime.now().startOf('day').set({
|
|
986
|
+
hour: selectedMoment.hour,
|
|
987
|
+
minute: selectedMoment.minute
|
|
988
|
+
});
|
|
989
|
+
const newDateChoices = Array(serviceTimeRange).fill().map((_, i) => dateSelectStartTime.plus({
|
|
990
|
+
days: i
|
|
991
|
+
}).toMillis());
|
|
992
|
+
return [newTimeChoices, newDateChoices];
|
|
993
|
+
}, [timestamp]);
|
|
994
|
+
function showScreenreaderCloseAlert() {
|
|
995
|
+
if (alertRef.current) {
|
|
996
|
+
alertRef.current.innerHTML = t('accessible-closed', translationSettings);
|
|
997
|
+
setTimeout(() => {
|
|
998
|
+
// The component may have unmounted (clearing the ref) before this
|
|
999
|
+
// fires - don't crash in that case.
|
|
1000
|
+
if (alertRef.current) {
|
|
1001
|
+
alertRef.current.innerHTML = null;
|
|
1002
|
+
}
|
|
1003
|
+
}, 100);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function showScreenreaderOpenAlert() {
|
|
1007
|
+
if (alertRef.current) {
|
|
1008
|
+
alertRef.current.innerHTML = t('accessible-opened', translationSettings);
|
|
1009
|
+
setTimeout(() => {
|
|
1010
|
+
// The component may have unmounted (clearing the ref) before this
|
|
1011
|
+
// fires - don't crash in that case.
|
|
1012
|
+
if (alertRef.current) {
|
|
1013
|
+
alertRef.current.innerHTML = null;
|
|
1014
|
+
}
|
|
1015
|
+
}, 100);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const handleClose = () => {
|
|
1019
|
+
changeOpen(false);
|
|
1020
|
+
showScreenreaderCloseAlert();
|
|
1021
|
+
if (onClose) {
|
|
1022
|
+
onClose();
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
const handleNowClick = () => {
|
|
1026
|
+
handleClose();
|
|
1027
|
+
onNowClick();
|
|
1028
|
+
};
|
|
1029
|
+
function renderOpen() {
|
|
1030
|
+
if (useMobileInputs) {
|
|
1031
|
+
return isOpen && /*#__PURE__*/React.createElement(MobilePickerModal, {
|
|
1032
|
+
departureOrArrival: departureOrArrival,
|
|
1033
|
+
onNowClick: handleNowClick,
|
|
1034
|
+
lang: lang,
|
|
1035
|
+
color: color,
|
|
1036
|
+
onSubmit: (newTimestamp, newDepartureOrArrival) => {
|
|
1037
|
+
onModalSubmit(newTimestamp, newDepartureOrArrival);
|
|
1038
|
+
changeOpen(false);
|
|
1039
|
+
showScreenreaderCloseAlert();
|
|
1040
|
+
},
|
|
1041
|
+
onCancel: handleClose,
|
|
1042
|
+
onAfterClose: () => requestAnimationFrame(() => {
|
|
1043
|
+
openPickerRef.current?.focus();
|
|
1044
|
+
}),
|
|
1045
|
+
getTimeDisplay: getTimeDisplay,
|
|
1046
|
+
timeZone: timeZone,
|
|
1047
|
+
timestamp: displayTimestamp,
|
|
1048
|
+
getDateDisplay: getDateDisplay,
|
|
1049
|
+
dateSelectItemCount: serviceTimeRange,
|
|
1050
|
+
getDisplay: getTimeDisplay,
|
|
1051
|
+
fontWeights: fontWeights
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
1055
|
+
className: `${styles['datetimepicker-open-container']} ${!isOpen ? styles.hidden : ''}`
|
|
1056
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
1057
|
+
className: isOpen ? `${styles['top-row-container']} datetimepicker-top-row` : `${styles.hidden} datetimepicker-top-row`
|
|
1058
|
+
}, ' ', /*#__PURE__*/React.createElement("span", {
|
|
1059
|
+
role: "alert",
|
|
1060
|
+
className: styles['sr-only'],
|
|
1061
|
+
ref: alertRef
|
|
1062
|
+
}), /*#__PURE__*/React.createElement("span", null), /*#__PURE__*/React.createElement("span", {
|
|
1063
|
+
className: styles['departure-or-arrival-container']
|
|
1064
|
+
}, /*#__PURE__*/React.createElement("label", {
|
|
1065
|
+
htmlFor: `${htmlId}-departure`,
|
|
1066
|
+
className: `${styles['radio-textbutton-label']}
|
|
1067
|
+
${styles[departureOrArrival === 'departure' ? 'active' : undefined]}`
|
|
1068
|
+
}, t('departure', translationSettings), /*#__PURE__*/React.createElement("input", {
|
|
1069
|
+
id: `${htmlId}-departure`,
|
|
1070
|
+
name: "departureOrArrival",
|
|
1071
|
+
type: "radio",
|
|
1072
|
+
value: "departure",
|
|
1073
|
+
className: styles['radio-textbutton'],
|
|
1074
|
+
onChange: () => {
|
|
1075
|
+
onDepartureClick();
|
|
1076
|
+
},
|
|
1077
|
+
checked: departureOrArrival === 'departure'
|
|
1078
|
+
})), /*#__PURE__*/React.createElement("label", {
|
|
1079
|
+
htmlFor: `${htmlId}-arrival`,
|
|
1080
|
+
className: `${styles['radio-textbutton-label']}
|
|
1081
|
+
${styles[departureOrArrival === 'arrival' ? 'active' : undefined]}`
|
|
1082
|
+
}, t('arrival', translationSettings), /*#__PURE__*/React.createElement("input", {
|
|
1083
|
+
id: `${htmlId}-arrival`,
|
|
1084
|
+
name: "departureOrArrival",
|
|
1085
|
+
type: "radio",
|
|
1086
|
+
value: "arrival",
|
|
1087
|
+
className: styles['radio-textbutton'],
|
|
1088
|
+
onChange: () => {
|
|
1089
|
+
onArrivalClick();
|
|
1090
|
+
},
|
|
1091
|
+
checked: departureOrArrival === 'arrival'
|
|
1092
|
+
}))), /*#__PURE__*/React.createElement("button", {
|
|
1093
|
+
id: `${htmlId}-now`,
|
|
1094
|
+
type: "button",
|
|
1095
|
+
className: styles['departure-now-button'],
|
|
1096
|
+
onClick: handleNowClick,
|
|
1097
|
+
ref: inputRef
|
|
1098
|
+
}, t('departure-now', translationSettings)), /*#__PURE__*/React.createElement("span", {
|
|
1099
|
+
className: styles['right-edge']
|
|
1100
|
+
}, /*#__PURE__*/React.createElement("button", {
|
|
1101
|
+
type: "button",
|
|
1102
|
+
className: styles['close-button'],
|
|
1103
|
+
"aria-controls": `${htmlId}-root`,
|
|
1104
|
+
"aria-expanded": "true",
|
|
1105
|
+
onClick: handleClose
|
|
1106
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1107
|
+
className: styles['close-icon']
|
|
1108
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
1109
|
+
img: "close",
|
|
1110
|
+
color: color
|
|
1111
|
+
})), /*#__PURE__*/React.createElement("span", {
|
|
1112
|
+
className: styles['sr-only']
|
|
1113
|
+
}, t('accessible-close', translationSettings))))), /*#__PURE__*/React.createElement("div", {
|
|
1114
|
+
className: isOpen ? `${styles['picker-container']}` : `${styles.hidden} datetimepicker-top-row`
|
|
1115
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1116
|
+
className: styles['combobox-left']
|
|
1117
|
+
}, /*#__PURE__*/React.createElement(DesktopDatetimepicker, {
|
|
1118
|
+
value: displayTimestamp,
|
|
1119
|
+
onChange: newValue => {
|
|
1120
|
+
onDateChange(newValue);
|
|
1121
|
+
},
|
|
1122
|
+
getDisplay: getDateDisplay,
|
|
1123
|
+
timeChoices: dateChoices,
|
|
1124
|
+
validate: () => null,
|
|
1125
|
+
icon: /*#__PURE__*/React.createElement("span", {
|
|
1126
|
+
className: `${styles['combobox-icon']} ${styles['date-input-icon']}`
|
|
1127
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
1128
|
+
img: "calendar",
|
|
1129
|
+
color: color
|
|
1130
|
+
})),
|
|
1131
|
+
id: `${htmlId}-date`,
|
|
1132
|
+
label: t('date', translationSettings),
|
|
1133
|
+
disableTyping: true,
|
|
1134
|
+
timeZone: timeZone,
|
|
1135
|
+
datePicker: true
|
|
1136
|
+
})), /*#__PURE__*/React.createElement("span", {
|
|
1137
|
+
className: styles['combobox-right']
|
|
1138
|
+
}, /*#__PURE__*/React.createElement(DesktopDatetimepicker, {
|
|
1139
|
+
value: displayTimestamp,
|
|
1140
|
+
onChange: newValue => {
|
|
1141
|
+
onTimeChange(newValue);
|
|
1142
|
+
},
|
|
1143
|
+
getDisplay: getTimeDisplay,
|
|
1144
|
+
timeChoices: timeChoices,
|
|
1145
|
+
icon: /*#__PURE__*/React.createElement("span", {
|
|
1146
|
+
className: `${styles['combobox-icon']} ${styles['time-input-icon']}`
|
|
1147
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
1148
|
+
img: "time",
|
|
1149
|
+
color: color
|
|
1150
|
+
})),
|
|
1151
|
+
id: `${htmlId}-time`,
|
|
1152
|
+
label: t('time', translationSettings),
|
|
1153
|
+
timeZone: timeZone,
|
|
1154
|
+
translationSettings: translationSettings
|
|
1155
|
+
})))), /*#__PURE__*/React.createElement("span", {
|
|
1156
|
+
className: isOpen ? '' : styles.hidden
|
|
1157
|
+
}, embedWhenOpen));
|
|
1158
|
+
}
|
|
1159
|
+
const formatToprowSummary = () => {
|
|
1160
|
+
if (nowSelected && departureOrArrival === 'departure') {
|
|
1161
|
+
return /*#__PURE__*/React.createElement("span", null, t('departure-now', translationSettings));
|
|
1162
|
+
}
|
|
1163
|
+
const dateDisplay = getDateDisplay(displayTimestamp);
|
|
1164
|
+
const timeDisplay = getTimeDisplay(displayTimestamp);
|
|
1165
|
+
const summary = t(departureOrArrival === 'departure' ? 'departure' : 'arrival', translationSettings);
|
|
1166
|
+
const isToday = DateTime.now().hasSame(DateTime.fromMillis(displayTimestamp), 'day');
|
|
1167
|
+
const isTomorrow = DateTime.now().plus({
|
|
1168
|
+
days: 1
|
|
1169
|
+
}).hasSame(DateTime.fromMillis(displayTimestamp), 'day');
|
|
1170
|
+
if (isToday) {
|
|
1171
|
+
return /*#__PURE__*/React.createElement("span", null, `${summary} ${timeDisplay}`);
|
|
1172
|
+
}
|
|
1173
|
+
if (isTomorrow) {
|
|
1174
|
+
return /*#__PURE__*/React.createElement("span", null, `${summary} ${dateDisplay.toLowerCase()} ${timeDisplay}`);
|
|
1175
|
+
}
|
|
1176
|
+
return /*#__PURE__*/React.createElement("span", null, `${summary} ${dateDisplay} ${timeDisplay}`);
|
|
1177
|
+
};
|
|
1178
|
+
return /*#__PURE__*/React.createElement("fieldset", {
|
|
1179
|
+
className: styles['dt-datetimepicker'],
|
|
1180
|
+
id: `${htmlId}-root`,
|
|
1181
|
+
style: {
|
|
1182
|
+
'--color': `${color}`,
|
|
1183
|
+
'--font-weight-medium': fontWeights.medium
|
|
1184
|
+
}
|
|
1185
|
+
}, /*#__PURE__*/React.createElement("legend", {
|
|
1186
|
+
className: styles['sr-only']
|
|
1187
|
+
}, t('accessible-title', translationSettings)), /*#__PURE__*/React.createElement("span", {
|
|
1188
|
+
className: styles['sr-only']
|
|
1189
|
+
}, t('accessible-update-instructions', translationSettings)), /*#__PURE__*/React.createElement("div", {
|
|
1190
|
+
className: !isOpen ? `${styles['top-row-container']} ${styles.closed} datetimepicker-top-row` : `${styles.hidden} datetimepicker-top-row`
|
|
1191
|
+
}, /*#__PURE__*/React.createElement("label", {
|
|
1192
|
+
className: styles['label-open'],
|
|
1193
|
+
htmlFor: `${htmlId}-open`
|
|
1194
|
+
}, /*#__PURE__*/React.createElement("span", {
|
|
1195
|
+
className: styles['time-icon']
|
|
1196
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
1197
|
+
img: "time",
|
|
1198
|
+
color: color
|
|
1199
|
+
})), /*#__PURE__*/React.createElement("span", {
|
|
1200
|
+
className: styles['sr-only']
|
|
1201
|
+
}, t('accessible-open', translationSettings)), /*#__PURE__*/React.createElement("span", {
|
|
1202
|
+
role: "alert",
|
|
1203
|
+
className: styles['sr-only'],
|
|
1204
|
+
ref: alertRef
|
|
1205
|
+
}), /*#__PURE__*/React.createElement("button", {
|
|
1206
|
+
id: `${htmlId}-open`,
|
|
1207
|
+
type: "button",
|
|
1208
|
+
className: `${styles.textbutton} ${styles.active} ${styles['open-button']}`,
|
|
1209
|
+
"aria-controls": `${htmlId}-root`,
|
|
1210
|
+
"aria-expanded": "false",
|
|
1211
|
+
onClick: () => {
|
|
1212
|
+
changeOpen(true);
|
|
1213
|
+
showScreenreaderOpenAlert();
|
|
1214
|
+
if (onOpen) {
|
|
1215
|
+
onOpen();
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
ref: openPickerRef
|
|
1219
|
+
}, formatToprowSummary(), /*#__PURE__*/React.createElement("span", {
|
|
1220
|
+
className: styles['dropdown-icon']
|
|
1221
|
+
}, /*#__PURE__*/React.createElement(Icon, {
|
|
1222
|
+
img: "arrow-dropdown",
|
|
1223
|
+
color: color
|
|
1224
|
+
})))), /*#__PURE__*/React.createElement("span", {
|
|
1225
|
+
className: styles['right-edge']
|
|
1226
|
+
}, embedWhenClosed)), /*#__PURE__*/React.createElement("div", null), renderOpen());
|
|
1227
|
+
}
|
|
1228
|
+
process.env.NODE_ENV !== "production" ? Datetimepicker.propTypes = {
|
|
1229
|
+
timestamp: PropTypes.number,
|
|
1230
|
+
timeZone: PropTypes.string,
|
|
1231
|
+
onTimeChange: PropTypes.func.isRequired,
|
|
1232
|
+
onDateChange: PropTypes.func.isRequired,
|
|
1233
|
+
departureOrArrival: PropTypes.oneOf(['departure', 'arrival']).isRequired,
|
|
1234
|
+
onNowClick: PropTypes.func.isRequired,
|
|
1235
|
+
onDepartureClick: PropTypes.func.isRequired,
|
|
1236
|
+
onArrivalClick: PropTypes.func.isRequired,
|
|
1237
|
+
embedWhenClosed: PropTypes.node,
|
|
1238
|
+
embedWhenOpen: PropTypes.node,
|
|
1239
|
+
lang: PropTypes.string.isRequired,
|
|
1240
|
+
color: PropTypes.string,
|
|
1241
|
+
onModalSubmit: PropTypes.func.isRequired,
|
|
1242
|
+
fontWeights: PropTypes.shape({
|
|
1243
|
+
medium: PropTypes.number.isRequired
|
|
1244
|
+
}).isRequired,
|
|
1245
|
+
serviceTimeRange: PropTypes.number.isRequired,
|
|
1246
|
+
onOpen: PropTypes.func,
|
|
1247
|
+
onClose: PropTypes.func,
|
|
1248
|
+
openPicker: PropTypes.bool
|
|
1249
|
+
} : void 0;
|
|
1250
|
+
Datetimepicker.defaultProps = {
|
|
1251
|
+
timestamp: null,
|
|
1252
|
+
embedWhenClosed: null,
|
|
1253
|
+
embedWhenOpen: null,
|
|
1254
|
+
color: '#007ac9',
|
|
1255
|
+
timeZone: 'Europe/Helsinki',
|
|
1256
|
+
onOpen: null,
|
|
1257
|
+
onClose: null,
|
|
1258
|
+
openPicker: undefined
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
/* eslint sort-keys: "error" */
|
|
1262
|
+
const translations = {
|
|
1263
|
+
de: {
|
|
1264
|
+
translation: {
|
|
1265
|
+
'accessible-close': 'Schließe die Auswahl',
|
|
1266
|
+
'accessible-closed': 'Auswahl geschlossen',
|
|
1267
|
+
'accessible-open': 'Öffne die Auswahl',
|
|
1268
|
+
'accessible-opened': 'Auswahl geöffnet',
|
|
1269
|
+
'accessible-title': 'Datum und Uhrzeitauswahl',
|
|
1270
|
+
arrival: 'Ankunft',
|
|
1271
|
+
cancel: 'Abbrechen',
|
|
1272
|
+
'choose-time': 'Zeit wählen',
|
|
1273
|
+
date: 'Datum',
|
|
1274
|
+
departure: 'Abfahrt',
|
|
1275
|
+
'departure-now': 'Abfahrt jetzt',
|
|
1276
|
+
ready: 'Fertig',
|
|
1277
|
+
time: 'Zeit',
|
|
1278
|
+
today: 'Heute',
|
|
1279
|
+
tomorrow: 'Morgen'
|
|
1280
|
+
}
|
|
1281
|
+
},
|
|
1282
|
+
en: {
|
|
1283
|
+
translation: {
|
|
1284
|
+
'accessible-close': 'Close the picker',
|
|
1285
|
+
'accessible-closed': 'Timepicker closed',
|
|
1286
|
+
'accessible-open': 'Open the picker',
|
|
1287
|
+
'accessible-opened': 'Timepicker opened',
|
|
1288
|
+
'accessible-title': 'Date and timepicker',
|
|
1289
|
+
'accessible-update-instructions': 'Changing time parameters will trigger a new search.',
|
|
1290
|
+
arrival: 'Arrival',
|
|
1291
|
+
cancel: 'Cancel',
|
|
1292
|
+
'choose-time': 'Choose time',
|
|
1293
|
+
date: 'Date',
|
|
1294
|
+
departure: 'Departure',
|
|
1295
|
+
'departure-now': 'Leaving now',
|
|
1296
|
+
'invalid-input': 'The time you entered is invalid',
|
|
1297
|
+
ready: 'Ready',
|
|
1298
|
+
time: 'Time',
|
|
1299
|
+
today: 'Today',
|
|
1300
|
+
tomorrow: 'Tomorrow'
|
|
1301
|
+
}
|
|
1302
|
+
},
|
|
1303
|
+
fi: {
|
|
1304
|
+
translation: {
|
|
1305
|
+
'accessible-close': 'Sulje valitsin',
|
|
1306
|
+
'accessible-closed': 'Valitsin suljettu',
|
|
1307
|
+
'accessible-open': 'Avaa valitsin',
|
|
1308
|
+
'accessible-opened': 'Valitsin avattu',
|
|
1309
|
+
'accessible-title': 'Aika- ja päivämäärävalitsin',
|
|
1310
|
+
'accessible-update-instructions': 'Aikavalintojen muuttaminen laukaisee uuden haun.',
|
|
1311
|
+
arrival: 'Perillä',
|
|
1312
|
+
cancel: 'Peruuta',
|
|
1313
|
+
'choose-time': 'Valitse aika',
|
|
1314
|
+
date: 'Päivä',
|
|
1315
|
+
departure: 'Lähtöaika',
|
|
1316
|
+
'departure-now': 'Lähtö nyt',
|
|
1317
|
+
'invalid-input': 'Syöttämäsi aika on virheellinen',
|
|
1318
|
+
ready: 'Valmis',
|
|
1319
|
+
time: 'Aika',
|
|
1320
|
+
today: 'Tänään',
|
|
1321
|
+
tomorrow: 'Huomenna'
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
pl: {
|
|
1325
|
+
translation: {
|
|
1326
|
+
'accessible-close': 'Zamknij wybornik',
|
|
1327
|
+
'accessible-closed': 'Zamknięto wybornik',
|
|
1328
|
+
'accessible-open': 'Otwórz wybornik',
|
|
1329
|
+
'accessible-opened': 'Otwarto wybornik',
|
|
1330
|
+
'accessible-title': 'Wybór daty i godziny',
|
|
1331
|
+
'accessible-update-instructions': 'Zmiana wybranej godziny wywoła nowe wyszukiwanie.',
|
|
1332
|
+
arrival: 'Przyjazd',
|
|
1333
|
+
cancel: 'Anuluj',
|
|
1334
|
+
'choose-time': 'Wybierz godzinę',
|
|
1335
|
+
date: 'Data',
|
|
1336
|
+
departure: 'Wyjazd',
|
|
1337
|
+
'departure-now': 'Wyjeżdżam teraz',
|
|
1338
|
+
'invalid-input': 'Wprowadzona godzina jest nieprawidłowa',
|
|
1339
|
+
ready: 'Gotowe',
|
|
1340
|
+
time: 'Godzina',
|
|
1341
|
+
today: 'Dziś',
|
|
1342
|
+
tomorrow: 'Jutro'
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
sv: {
|
|
1346
|
+
translation: {
|
|
1347
|
+
'accessible-close': 'Stäng väljaren',
|
|
1348
|
+
'accessible-closed': 'Väljaren stängde',
|
|
1349
|
+
'accessible-open': 'Öppna väljaren',
|
|
1350
|
+
'accessible-opened': 'Väljaren öppnade',
|
|
1351
|
+
'accessible-title': 'Tid- och datumväljaren',
|
|
1352
|
+
'accessible-update-instructions': 'Om du ändrar tiderna, startas en ny sökning automatiskt.',
|
|
1353
|
+
arrival: 'Anländer',
|
|
1354
|
+
cancel: 'Avbryt',
|
|
1355
|
+
'choose-time': 'Välj tid',
|
|
1356
|
+
date: 'Datum',
|
|
1357
|
+
departure: 'Avgångstid',
|
|
1358
|
+
'departure-now': 'Avgår nu',
|
|
1359
|
+
'invalid-input': 'Tiden du angav är felaktig',
|
|
1360
|
+
ready: 'Färdig',
|
|
1361
|
+
time: 'Tid',
|
|
1362
|
+
today: 'I dag',
|
|
1363
|
+
tomorrow: 'I morgon'
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
};
|
|
1367
|
+
|
|
1368
|
+
const i18n = createInstance({
|
|
1369
|
+
fallbackLng: 'fi',
|
|
1370
|
+
// debug: true,
|
|
1371
|
+
resources: translations,
|
|
1372
|
+
defaultNS: 'translation',
|
|
1373
|
+
react: {
|
|
1374
|
+
useSuspense: true
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
i18n.init();
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* This component renders an input to choose a date and time. Renders separate input fields for date and time selection. Values for timestamp and arriveBy correspond to Digitransit query params time and arriveBy. This component will display a native date input on mobile and a custom one for desktop. Mobile detection is done by parsing user agent.
|
|
1381
|
+
*
|
|
1382
|
+
* @alias Datetimepicker
|
|
1383
|
+
*
|
|
1384
|
+
* @param {Object} props
|
|
1385
|
+
*
|
|
1386
|
+
* @param {boolean} props.realtime Determine if selected time should be updated in realtime when 'now' is selected.
|
|
1387
|
+
* @param {Number} props.initialTimestamp Initial value for selected time. Unix timestamp in seconds. Updating this will change timepicker value but the correct value is kept in component state even if this is not updated.
|
|
1388
|
+
* @param {boolean} props.initialArriveBy Initial value for arriveBy. Determines if picker is in arrival mode (true) or departure mode (false). Correct value is kept in component state even if this is not updated. Changing this will also trigger change in the component.
|
|
1389
|
+
* @param {function} props.onTimeChange Called with (time, arriveBy) when time input changes. time is number timestamp in seconds, arriveBy is boolean
|
|
1390
|
+
* @param {function} props.onDateChange Called with (time, arriveBy) when date input changes. time is number timestamp in seconds, arriveBy is boolean
|
|
1391
|
+
* @param {function} props.onNowClick Called when "depart now" button is clicked. time is current input value in seconds
|
|
1392
|
+
* @param {function} props.onDepartureClick Called with (time) when "departure" button is clicked. time is current input value in seconds
|
|
1393
|
+
* @param {function} props.onArrivalClick Called with (time) when "arrival" button is clicked. time is current input value in seconds
|
|
1394
|
+
* @param {node} props.embedWhenClosed JSX element to render in the corner when input is closed
|
|
1395
|
+
* @param {node} props.embedWhenOpen JSX element to render when input is open
|
|
1396
|
+
* @param {string} props.lang Language selection. Default 'en'
|
|
1397
|
+
* @param {number} props.serviceTimeRange Determine number of days shown in timepicker. Optional. default is 30.
|
|
1398
|
+
* @param {function} props.onOpen Determine what to do when timepicker is open. Optional. no default implementation.
|
|
1399
|
+
* @param {function} props.onClose Determine what to do when timepicker is closed. Optional. no default implementation.
|
|
1400
|
+
* @param {function} props.openPicker Determine if timepicker should be open in intial render. Optional. Default is undefined.
|
|
1401
|
+
* @example
|
|
1402
|
+
* <Datetimepicker
|
|
1403
|
+
* realtime={true}
|
|
1404
|
+
* initialTimestamp={1590133823}
|
|
1405
|
+
* initialArriveBy={false}
|
|
1406
|
+
* onTimeChange={(time, arriveBy) => changeUrl(time, arriveBy)}
|
|
1407
|
+
* onDateChange={(time, arriveBy) => changeUrl(time, arriveBy)}
|
|
1408
|
+
* onNowClick={(time) => changeUrl(undefined, undefined)}
|
|
1409
|
+
* onDepartureClick={(time) => changeUrl(time, 'true')}
|
|
1410
|
+
* onArrivalClick={(time) => changeUrl(time, undefined)}
|
|
1411
|
+
* embedWhenClosed={<button />}
|
|
1412
|
+
* lang={'en'}
|
|
1413
|
+
* serviceTimeRange={15}
|
|
1414
|
+
* />
|
|
1415
|
+
*/
|
|
1416
|
+
function DatetimepickerStateContainer({
|
|
1417
|
+
realtime,
|
|
1418
|
+
initialArriveBy,
|
|
1419
|
+
initialTimestamp,
|
|
1420
|
+
onDepartureClick,
|
|
1421
|
+
onArrivalClick,
|
|
1422
|
+
onTimeChange,
|
|
1423
|
+
onDateChange,
|
|
1424
|
+
onNowClick,
|
|
1425
|
+
embedWhenClosed,
|
|
1426
|
+
embedWhenOpen,
|
|
1427
|
+
lang,
|
|
1428
|
+
color,
|
|
1429
|
+
timeZone,
|
|
1430
|
+
fontWeights,
|
|
1431
|
+
serviceTimeRange,
|
|
1432
|
+
onOpen,
|
|
1433
|
+
onClose,
|
|
1434
|
+
openPicker
|
|
1435
|
+
}) {
|
|
1436
|
+
Settings.defaultLocale = lang;
|
|
1437
|
+
Settings.defaultZone = timeZone;
|
|
1438
|
+
const initialNow = realtime ? null : DateTime.now().toMillis();
|
|
1439
|
+
const [timestamp, changeTimestampState] = useState(initialTimestamp ? initialTimestamp * 1000 : initialNow);
|
|
1440
|
+
const [departureOrArrival, changeDepartureOrArrival] = useState(initialArriveBy === true ? 'arrival' : 'departure');
|
|
1441
|
+
|
|
1442
|
+
// update state if props change
|
|
1443
|
+
useEffect(() => {
|
|
1444
|
+
const bothNull = timestamp === null && initialTimestamp === undefined;
|
|
1445
|
+
const oneNull = timestamp === null || initialTimestamp === undefined;
|
|
1446
|
+
const sameTime = Math.round(timestamp / 1000) === initialTimestamp;
|
|
1447
|
+
const timestampChanged = !bothNull && (oneNull || !sameTime);
|
|
1448
|
+
if (timestampChanged) {
|
|
1449
|
+
if (initialTimestamp === undefined) {
|
|
1450
|
+
changeTimestampState(null);
|
|
1451
|
+
} else {
|
|
1452
|
+
changeTimestampState(initialTimestamp * 1000);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (!initialArriveBy === (departureOrArrival === 'arrival')) {
|
|
1456
|
+
changeDepartureOrArrival(initialArriveBy === true ? 'arrival' : 'departure');
|
|
1457
|
+
}
|
|
1458
|
+
}, [initialTimestamp, initialArriveBy]);
|
|
1459
|
+
const timeChanged = debounce(newTime => {
|
|
1460
|
+
if (newTime === null) {
|
|
1461
|
+
changeTimestampState(DateTime.now().toMillis());
|
|
1462
|
+
onTimeChange(DateTime.now().toUnixInteger(), departureOrArrival === 'arrival');
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
changeTimestampState(newTime);
|
|
1466
|
+
onTimeChange(Math.round(newTime / 1000), departureOrArrival === 'arrival');
|
|
1467
|
+
}, 10);
|
|
1468
|
+
const dateChanged = debounce(newDate => {
|
|
1469
|
+
if (newDate === null) {
|
|
1470
|
+
changeTimestampState(DateTime.now().toMillis());
|
|
1471
|
+
onDateChange(DateTime.now().toUnixInteger(), departureOrArrival === 'arrival');
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
changeTimestampState(newDate);
|
|
1475
|
+
onDateChange(Math.round(newDate / 1000), departureOrArrival === 'arrival');
|
|
1476
|
+
}, 10);
|
|
1477
|
+
const nowClicked = () => {
|
|
1478
|
+
changeDepartureOrArrival('departure');
|
|
1479
|
+
const newTimestamp = realtime ? null : DateTime.now().toMillis();
|
|
1480
|
+
changeTimestampState(newTimestamp);
|
|
1481
|
+
onNowClick(Math.round(newTimestamp / 1000));
|
|
1482
|
+
};
|
|
1483
|
+
const departureClicked = () => {
|
|
1484
|
+
let changed = false;
|
|
1485
|
+
let newTime = timestamp;
|
|
1486
|
+
if (timestamp === null) {
|
|
1487
|
+
const now = DateTime.now().toMillis();
|
|
1488
|
+
changeTimestampState(now);
|
|
1489
|
+
newTime = now;
|
|
1490
|
+
changed = true;
|
|
1491
|
+
}
|
|
1492
|
+
if (departureOrArrival !== 'departure') {
|
|
1493
|
+
changeDepartureOrArrival('departure');
|
|
1494
|
+
changed = true;
|
|
1495
|
+
}
|
|
1496
|
+
if (changed) {
|
|
1497
|
+
onDepartureClick(Math.round(newTime / 1000));
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
const arrivalClicked = () => {
|
|
1501
|
+
let changed = false;
|
|
1502
|
+
let newTime = timestamp;
|
|
1503
|
+
if (timestamp === null) {
|
|
1504
|
+
const now = DateTime.now().toMillis();
|
|
1505
|
+
changeTimestampState(now);
|
|
1506
|
+
newTime = now;
|
|
1507
|
+
changed = true;
|
|
1508
|
+
}
|
|
1509
|
+
if (departureOrArrival !== 'arrival') {
|
|
1510
|
+
changeDepartureOrArrival('arrival');
|
|
1511
|
+
changed = true;
|
|
1512
|
+
}
|
|
1513
|
+
if (changed) {
|
|
1514
|
+
onArrivalClick(Math.round(newTime / 1000));
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
const onModalSubmit = (time, mode) => {
|
|
1518
|
+
changeTimestampState(time);
|
|
1519
|
+
changeDepartureOrArrival(mode);
|
|
1520
|
+
onTimeChange(Math.round(time / 1000), mode === 'arrival', true);
|
|
1521
|
+
};
|
|
1522
|
+
return /*#__PURE__*/React.createElement(I18nextProvider, {
|
|
1523
|
+
i18n: i18n
|
|
1524
|
+
}, /*#__PURE__*/React.createElement(Datetimepicker, {
|
|
1525
|
+
timestamp: timestamp,
|
|
1526
|
+
onTimeChange: timeChanged,
|
|
1527
|
+
onDateChange: dateChanged,
|
|
1528
|
+
departureOrArrival: departureOrArrival,
|
|
1529
|
+
onNowClick: nowClicked,
|
|
1530
|
+
onDepartureClick: departureClicked,
|
|
1531
|
+
onArrivalClick: arrivalClicked,
|
|
1532
|
+
embedWhenClosed: embedWhenClosed,
|
|
1533
|
+
embedWhenOpen: embedWhenOpen,
|
|
1534
|
+
lang: lang,
|
|
1535
|
+
color: color,
|
|
1536
|
+
timeZone: timeZone,
|
|
1537
|
+
onModalSubmit: onModalSubmit,
|
|
1538
|
+
fontWeights: fontWeights,
|
|
1539
|
+
serviceTimeRange: serviceTimeRange,
|
|
1540
|
+
onOpen: onOpen,
|
|
1541
|
+
onClose: onClose,
|
|
1542
|
+
openPicker: openPicker
|
|
1543
|
+
}));
|
|
1544
|
+
}
|
|
1545
|
+
process.env.NODE_ENV !== "production" ? DatetimepickerStateContainer.propTypes = {
|
|
1546
|
+
realtime: PropTypes.bool,
|
|
1547
|
+
initialTimestamp: PropTypes.number,
|
|
1548
|
+
initialArriveBy: PropTypes.bool,
|
|
1549
|
+
onDepartureClick: PropTypes.func.isRequired,
|
|
1550
|
+
onArrivalClick: PropTypes.func.isRequired,
|
|
1551
|
+
onTimeChange: PropTypes.func.isRequired,
|
|
1552
|
+
onDateChange: PropTypes.func.isRequired,
|
|
1553
|
+
onNowClick: PropTypes.func.isRequired,
|
|
1554
|
+
embedWhenClosed: PropTypes.node,
|
|
1555
|
+
embedWhenOpen: PropTypes.node,
|
|
1556
|
+
lang: PropTypes.string,
|
|
1557
|
+
color: PropTypes.string,
|
|
1558
|
+
timeZone: PropTypes.string,
|
|
1559
|
+
fontWeights: PropTypes.shape({
|
|
1560
|
+
medium: PropTypes.number
|
|
1561
|
+
}),
|
|
1562
|
+
serviceTimeRange: PropTypes.number,
|
|
1563
|
+
onOpen: PropTypes.func,
|
|
1564
|
+
onClose: PropTypes.func,
|
|
1565
|
+
openPicker: PropTypes.bool
|
|
1566
|
+
} : void 0;
|
|
1567
|
+
DatetimepickerStateContainer.defaultProps = {
|
|
1568
|
+
realtime: false,
|
|
1569
|
+
initialArriveBy: undefined,
|
|
1570
|
+
initialTimestamp: undefined,
|
|
1571
|
+
embedWhenClosed: null,
|
|
1572
|
+
embedWhenOpen: null,
|
|
1573
|
+
lang: 'en',
|
|
1574
|
+
color: '#007ac9',
|
|
1575
|
+
timeZone: 'Europe/Helsinki',
|
|
1576
|
+
fontWeights: {
|
|
1577
|
+
medium: 500
|
|
1578
|
+
},
|
|
1579
|
+
onOpen: null,
|
|
1580
|
+
onClose: null,
|
|
1581
|
+
openPicker: undefined,
|
|
1582
|
+
serviceTimeRange: 30
|
|
1583
|
+
};
|
|
1584
|
+
|
|
1585
|
+
export { DatetimepickerStateContainer as default };
|
|
1586
|
+
//# sourceMappingURL=index.js.map
|