@vaadin/time-picker 22.0.0-alpha7
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 +190 -0
- package/README.md +61 -0
- package/package.json +50 -0
- package/src/interfaces.d.ts +31 -0
- package/src/vaadin-time-picker-combo-box.js +105 -0
- package/src/vaadin-time-picker-dropdown.js +38 -0
- package/src/vaadin-time-picker-item.js +37 -0
- package/src/vaadin-time-picker-overlay.js +31 -0
- package/src/vaadin-time-picker-scroller.js +20 -0
- package/src/vaadin-time-picker.d.ts +186 -0
- package/src/vaadin-time-picker.js +684 -0
- package/theme/lumo/vaadin-time-picker-styles.js +24 -0
- package/theme/lumo/vaadin-time-picker.js +9 -0
- package/theme/material/vaadin-time-picker-styles.js +16 -0
- package/theme/material/vaadin-time-picker.js +9 -0
- package/vaadin-time-picker.d.ts +2 -0
- package/vaadin-time-picker.js +2 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { PolymerElement, html } from '@polymer/polymer/polymer-element.js';
|
|
7
|
+
import { ElementMixin } from '@vaadin/component-base/src/element-mixin.js';
|
|
8
|
+
import { AriaLabelController } from '@vaadin/field-base/src/aria-label-controller.js';
|
|
9
|
+
import { InputController } from '@vaadin/field-base/src/input-controller.js';
|
|
10
|
+
import { InputControlMixin } from '@vaadin/field-base/src/input-control-mixin.js';
|
|
11
|
+
import { PatternMixin } from '@vaadin/field-base/src/pattern-mixin.js';
|
|
12
|
+
import { inputFieldShared } from '@vaadin/field-base/src/styles/input-field-shared-styles.js';
|
|
13
|
+
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin.js';
|
|
14
|
+
import { registerStyles } from '@vaadin/vaadin-themable-mixin/register-styles.js';
|
|
15
|
+
import '@vaadin/input-container/src/vaadin-input-container.js';
|
|
16
|
+
import './vaadin-time-picker-combo-box.js';
|
|
17
|
+
|
|
18
|
+
registerStyles('vaadin-time-picker', inputFieldShared, { moduleId: 'vaadin-time-picker-styles' });
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `<vaadin-time-picker>` is a Web Component providing a time-selection field.
|
|
22
|
+
*
|
|
23
|
+
* ```html
|
|
24
|
+
* <vaadin-time-picker></vaadin-time-picker>
|
|
25
|
+
* ```
|
|
26
|
+
* ```js
|
|
27
|
+
* timePicker.value = '14:30';
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* When the selected `value` is changed, a `value-changed` event is triggered.
|
|
31
|
+
*
|
|
32
|
+
* ### Styling
|
|
33
|
+
*
|
|
34
|
+
* The following custom properties are available for styling:
|
|
35
|
+
*
|
|
36
|
+
* Custom property | Description | Default
|
|
37
|
+
* ------------------------------------------|----------------------------|---------
|
|
38
|
+
* `--vaadin-field-default-width` | Default width of the field | `12em`
|
|
39
|
+
* `--vaadin-time-picker-overlay-max-height` | Max height of the overlay | `65vh`
|
|
40
|
+
*
|
|
41
|
+
* The following shadow DOM parts are available for styling:
|
|
42
|
+
*
|
|
43
|
+
* Part name | Description
|
|
44
|
+
* ----------------|----------------
|
|
45
|
+
* `clear-button` | The clear button
|
|
46
|
+
* `input-field` | Input element wrapper
|
|
47
|
+
* `toggle-button` | The toggle button
|
|
48
|
+
* `label` | The label element
|
|
49
|
+
* `error-message` | The error message element
|
|
50
|
+
* `helper-text` | The helper text element wrapper
|
|
51
|
+
*
|
|
52
|
+
* See [Styling Components](https://vaadin.com/docs/latest/ds/customization/styling-components) documentation.
|
|
53
|
+
*
|
|
54
|
+
* The following state attributes are available for styling:
|
|
55
|
+
*
|
|
56
|
+
* Attribute | Description | Part name
|
|
57
|
+
* -------------|------------------------------------------|------------
|
|
58
|
+
* `disabled` | Set to a disabled time picker | :host
|
|
59
|
+
* `readonly` | Set to a read only time picker | :host
|
|
60
|
+
* `invalid` | Set when the element is invalid | :host
|
|
61
|
+
* `focused` | Set when the element is focused | :host
|
|
62
|
+
* `focus-ring` | Set when the element is keyboard focused | :host
|
|
63
|
+
*
|
|
64
|
+
* ### Internal components
|
|
65
|
+
*
|
|
66
|
+
* In addition to `<vaadin-time-picker>` itself, the following internal
|
|
67
|
+
* components are themable:
|
|
68
|
+
*
|
|
69
|
+
* - `<vaadin-time-picker-combo-box>` - has the same API as [`<vaadin-combo-box-light>`](#/elements/vaadin-combo-box-light).
|
|
70
|
+
*
|
|
71
|
+
* Note: the `theme` attribute value set on `<vaadin-time-picker>` is
|
|
72
|
+
* propagated to the internal components listed above.
|
|
73
|
+
*
|
|
74
|
+
* @fires {Event} change - Fired when the user commits a value change.
|
|
75
|
+
* @fires {CustomEvent} invalid-changed - Fired when the `invalid` property changes.
|
|
76
|
+
* @fires {CustomEvent} value-changed - Fired when the `value` property changes.
|
|
77
|
+
*
|
|
78
|
+
* @extends HTMLElement
|
|
79
|
+
* @mixes ElementMixin
|
|
80
|
+
* @mixes ThemableMixin
|
|
81
|
+
* @mixes InputControlMixin
|
|
82
|
+
* @mixes PatternMixin
|
|
83
|
+
*/
|
|
84
|
+
class TimePicker extends PatternMixin(InputControlMixin(ThemableMixin(ElementMixin(PolymerElement)))) {
|
|
85
|
+
static get is() {
|
|
86
|
+
return 'vaadin-time-picker';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static get template() {
|
|
90
|
+
return html`
|
|
91
|
+
<style>
|
|
92
|
+
/* See https://github.com/vaadin/vaadin-time-picker/issues/145 */
|
|
93
|
+
:host([dir='rtl']) [part='input-field'] {
|
|
94
|
+
direction: ltr;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:host([dir='rtl']) [part='input-field'] ::slotted(input)::placeholder {
|
|
98
|
+
direction: rtl;
|
|
99
|
+
text-align: left;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[part~='toggle-button'] {
|
|
103
|
+
cursor: pointer;
|
|
104
|
+
}
|
|
105
|
+
</style>
|
|
106
|
+
|
|
107
|
+
<div class="vaadin-time-picker-container">
|
|
108
|
+
<div part="label" on-click="focus">
|
|
109
|
+
<slot name="label"></slot>
|
|
110
|
+
<span part="required-indicator" aria-hidden="true"></span>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<vaadin-time-picker-combo-box
|
|
114
|
+
id="comboBox"
|
|
115
|
+
filtered-items="[[__dropdownItems]]"
|
|
116
|
+
value="{{_comboBoxValue}}"
|
|
117
|
+
disabled="[[disabled]]"
|
|
118
|
+
readonly="[[readonly]]"
|
|
119
|
+
auto-open-disabled="[[autoOpenDisabled]]"
|
|
120
|
+
position-target="[[_inputContainer]]"
|
|
121
|
+
theme$="[[theme]]"
|
|
122
|
+
on-change="__onChange"
|
|
123
|
+
>
|
|
124
|
+
<vaadin-input-container
|
|
125
|
+
part="input-field"
|
|
126
|
+
readonly="[[readonly]]"
|
|
127
|
+
disabled="[[disabled]]"
|
|
128
|
+
invalid="[[invalid]]"
|
|
129
|
+
theme$="[[theme]]"
|
|
130
|
+
>
|
|
131
|
+
<slot name="prefix" slot="prefix"></slot>
|
|
132
|
+
<slot name="input"></slot>
|
|
133
|
+
<div id="clearButton" part="clear-button" slot="suffix"></div>
|
|
134
|
+
<div id="toggleButton" class="toggle-button" part="toggle-button" slot="suffix"></div>
|
|
135
|
+
</vaadin-input-container>
|
|
136
|
+
</vaadin-time-picker-combo-box>
|
|
137
|
+
|
|
138
|
+
<div part="helper-text">
|
|
139
|
+
<slot name="helper"></slot>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<div part="error-message">
|
|
143
|
+
<slot name="error-message"></slot>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
static get properties() {
|
|
150
|
+
return {
|
|
151
|
+
/**
|
|
152
|
+
* The time value for this element.
|
|
153
|
+
*
|
|
154
|
+
* Supported time formats are in ISO 8601:
|
|
155
|
+
* - `hh:mm` (default)
|
|
156
|
+
* - `hh:mm:ss`
|
|
157
|
+
* - `hh:mm:ss.fff`
|
|
158
|
+
* @type {string}
|
|
159
|
+
*/
|
|
160
|
+
value: {
|
|
161
|
+
type: String,
|
|
162
|
+
notify: true,
|
|
163
|
+
value: ''
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Minimum time allowed.
|
|
168
|
+
*
|
|
169
|
+
* Supported time formats are in ISO 8601:
|
|
170
|
+
* - `hh:mm`
|
|
171
|
+
* - `hh:mm:ss`
|
|
172
|
+
* - `hh:mm:ss.fff`
|
|
173
|
+
* @type {string}
|
|
174
|
+
*/
|
|
175
|
+
min: {
|
|
176
|
+
type: String,
|
|
177
|
+
value: '00:00:00.000'
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Maximum time allowed.
|
|
182
|
+
*
|
|
183
|
+
* Supported time formats are in ISO 8601:
|
|
184
|
+
* - `hh:mm`
|
|
185
|
+
* - `hh:mm:ss`
|
|
186
|
+
* - `hh:mm:ss.fff`
|
|
187
|
+
* @type {string}
|
|
188
|
+
*/
|
|
189
|
+
max: {
|
|
190
|
+
type: String,
|
|
191
|
+
value: '23:59:59.999'
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Defines the time interval (in seconds) between the items displayed
|
|
196
|
+
* in the time selection box. The default is 1 hour (i.e. `3600`).
|
|
197
|
+
*
|
|
198
|
+
* It also configures the precision of the value string. By default
|
|
199
|
+
* the component formats values as `hh:mm` but setting a step value
|
|
200
|
+
* lower than one minute or one second, format resolution changes to
|
|
201
|
+
* `hh:mm:ss` and `hh:mm:ss.fff` respectively.
|
|
202
|
+
*
|
|
203
|
+
* Unit must be set in seconds, and for correctly configuring intervals
|
|
204
|
+
* in the dropdown, it need to evenly divide a day.
|
|
205
|
+
*
|
|
206
|
+
* Note: it is possible to define step that is dividing an hour in inexact
|
|
207
|
+
* fragments (i.e. 5760 seconds which equals 1 hour 36 minutes), but it is
|
|
208
|
+
* not recommended to use it for better UX experience.
|
|
209
|
+
*/
|
|
210
|
+
step: {
|
|
211
|
+
type: Number
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Set true to prevent the overlay from opening automatically.
|
|
216
|
+
* @attr {boolean} auto-open-disabled
|
|
217
|
+
*/
|
|
218
|
+
autoOpenDisabled: Boolean,
|
|
219
|
+
|
|
220
|
+
/** @private */
|
|
221
|
+
__dropdownItems: {
|
|
222
|
+
type: Array
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The object used to localize this component.
|
|
227
|
+
* To change the default localization, replace the entire
|
|
228
|
+
* _i18n_ object or just the property you want to modify.
|
|
229
|
+
*
|
|
230
|
+
* The object has the following JSON structure:
|
|
231
|
+
*
|
|
232
|
+
* ```
|
|
233
|
+
* {
|
|
234
|
+
* // A function to format given `Object` as
|
|
235
|
+
* // time string. Object is in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`
|
|
236
|
+
* formatTime: (time) => {
|
|
237
|
+
* // returns a string representation of the given
|
|
238
|
+
* // object in `hh` / 'hh:mm' / 'hh:mm:ss' / 'hh:mm:ss.fff' - formats
|
|
239
|
+
* },
|
|
240
|
+
*
|
|
241
|
+
* // A function to parse the given text to an `Object` in the format
|
|
242
|
+
* // `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`.
|
|
243
|
+
* // Must properly parse (at least) text
|
|
244
|
+
* // formatted by `formatTime`.
|
|
245
|
+
* parseTime: text => {
|
|
246
|
+
* // Parses a string in object/string that can be formatted by`formatTime`.
|
|
247
|
+
* }
|
|
248
|
+
* }
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* Both `formatTime` and `parseTime` need to be implemented
|
|
252
|
+
* to ensure the component works properly.
|
|
253
|
+
*
|
|
254
|
+
* @type {!TimePickerI18n}
|
|
255
|
+
*/
|
|
256
|
+
i18n: {
|
|
257
|
+
type: Object,
|
|
258
|
+
value: () => {
|
|
259
|
+
return {
|
|
260
|
+
formatTime: (time) => {
|
|
261
|
+
if (!time) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const pad = (num = 0, fmt = '00') => (fmt + num).substr((fmt + num).length - fmt.length);
|
|
266
|
+
// Always display hour and minute
|
|
267
|
+
let timeString = `${pad(time.hours)}:${pad(time.minutes)}`;
|
|
268
|
+
// Adding second and millisecond depends on resolution
|
|
269
|
+
time.seconds !== undefined && (timeString += `:${pad(time.seconds)}`);
|
|
270
|
+
time.milliseconds !== undefined && (timeString += `.${pad(time.milliseconds, '000')}`);
|
|
271
|
+
return timeString;
|
|
272
|
+
},
|
|
273
|
+
parseTime: (text) => {
|
|
274
|
+
// Parsing with RegExp to ensure correct format
|
|
275
|
+
const MATCH_HOURS = '(\\d|[0-1]\\d|2[0-3])';
|
|
276
|
+
const MATCH_MINUTES = '(\\d|[0-5]\\d)';
|
|
277
|
+
const MATCH_SECONDS = MATCH_MINUTES;
|
|
278
|
+
const MATCH_MILLISECONDS = '(\\d{1,3})';
|
|
279
|
+
const re = new RegExp(
|
|
280
|
+
`^${MATCH_HOURS}(?::${MATCH_MINUTES}(?::${MATCH_SECONDS}(?:\\.${MATCH_MILLISECONDS})?)?)?$`
|
|
281
|
+
);
|
|
282
|
+
const parts = re.exec(text);
|
|
283
|
+
if (parts) {
|
|
284
|
+
// Allows setting the milliseconds with hundreds and tens precision
|
|
285
|
+
if (parts[4]) {
|
|
286
|
+
while (parts[4].length < 3) {
|
|
287
|
+
parts[4] += '0';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { hours: parts[1], minutes: parts[2], seconds: parts[3], milliseconds: parts[4] };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/** @private */
|
|
298
|
+
_comboBoxValue: {
|
|
299
|
+
type: String,
|
|
300
|
+
observer: '__comboBoxValueChanged'
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/** @private */
|
|
304
|
+
_inputContainer: Object
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
static get observers() {
|
|
309
|
+
return ['__updateDropdownItems(i18n.*, min, max, step)'];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Used by `ClearButtonMixin` as a reference to the clear button element.
|
|
314
|
+
* @protected
|
|
315
|
+
* @return {!HTMLElement}
|
|
316
|
+
*/
|
|
317
|
+
get clearElement() {
|
|
318
|
+
return this.$.clearButton;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @protected */
|
|
322
|
+
ready() {
|
|
323
|
+
super.ready();
|
|
324
|
+
|
|
325
|
+
this.addController(
|
|
326
|
+
new InputController(this, (input) => {
|
|
327
|
+
this._setInputElement(input);
|
|
328
|
+
this._setFocusElement(input);
|
|
329
|
+
this.stateTarget = input;
|
|
330
|
+
this.ariaTarget = input;
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
this.addController(new AriaLabelController(this.inputElement, this._labelNode));
|
|
334
|
+
this._inputContainer = this.shadowRoot.querySelector('[part~="input-field"]');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Override method inherited from `InputMixin` to forward the input to combo-box.
|
|
339
|
+
* @protected
|
|
340
|
+
* @override
|
|
341
|
+
*/
|
|
342
|
+
_inputElementChanged(input) {
|
|
343
|
+
super._inputElementChanged(input);
|
|
344
|
+
|
|
345
|
+
if (input) {
|
|
346
|
+
this.$.comboBox._setInputElement(input);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Returns true if the current input value satisfies all constraints (if any).
|
|
352
|
+
* You can override this method for custom validations.
|
|
353
|
+
*
|
|
354
|
+
* @return {boolean} True if the value is valid
|
|
355
|
+
*/
|
|
356
|
+
checkValidity() {
|
|
357
|
+
return !!(
|
|
358
|
+
this.inputElement.checkValidity() &&
|
|
359
|
+
(!this.value || this._timeAllowed(this.i18n.parseTime(this.value))) &&
|
|
360
|
+
(!this._comboBoxValue || this.i18n.parseTime(this._comboBoxValue))
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Override method inherited from `FocusMixin` to validate on blur.
|
|
366
|
+
* @param {boolean} focused
|
|
367
|
+
* @protected
|
|
368
|
+
*/
|
|
369
|
+
_setFocused(focused) {
|
|
370
|
+
super._setFocused(focused);
|
|
371
|
+
|
|
372
|
+
if (!focused) {
|
|
373
|
+
this.validate();
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** @private */
|
|
378
|
+
__validDayDivisor(step) {
|
|
379
|
+
// valid if undefined, or exact divides a day, or has millisecond resolution
|
|
380
|
+
return !step || (24 * 3600) % step === 0 || (step < 1 && ((step % 1) * 1000) % 1 === 0);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Override an event listener from `ClearButtonMixin`
|
|
385
|
+
* to prevent clearing the input value on Esc key.
|
|
386
|
+
* @param {Event} event
|
|
387
|
+
* @protected
|
|
388
|
+
*/
|
|
389
|
+
_onKeyDown(e) {
|
|
390
|
+
if (this.readonly || this.disabled || this.__dropdownItems.length) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const stepResolution = (this.__validDayDivisor(this.step) && this.step) || 60;
|
|
395
|
+
|
|
396
|
+
if (e.keyCode === 40) {
|
|
397
|
+
this.__onArrowPressWithStep(-stepResolution);
|
|
398
|
+
} else if (e.keyCode === 38) {
|
|
399
|
+
this.__onArrowPressWithStep(stepResolution);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** @private */
|
|
404
|
+
__onArrowPressWithStep(step) {
|
|
405
|
+
const objWithStep = this.__addStep(this.__getMsec(this.__memoValue), step, true);
|
|
406
|
+
this.__memoValue = objWithStep;
|
|
407
|
+
this.inputElement.value = this.i18n.formatTime(this.__validateTime(objWithStep));
|
|
408
|
+
this.__dispatchChange();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** @private */
|
|
412
|
+
__dispatchChange() {
|
|
413
|
+
this.dispatchEvent(new CustomEvent('change', { bubbles: true }));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Returning milliseconds from Object in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`
|
|
418
|
+
* @private
|
|
419
|
+
*/
|
|
420
|
+
__getMsec(obj) {
|
|
421
|
+
let result = ((obj && obj.hours) || 0) * 60 * 60 * 1000;
|
|
422
|
+
result += ((obj && obj.minutes) || 0) * 60 * 1000;
|
|
423
|
+
result += ((obj && obj.seconds) || 0) * 1000;
|
|
424
|
+
result += (obj && parseInt(obj.milliseconds)) || 0;
|
|
425
|
+
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Returning seconds from Object in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`
|
|
431
|
+
* @private
|
|
432
|
+
*/
|
|
433
|
+
__getSec(obj) {
|
|
434
|
+
let result = ((obj && obj.hours) || 0) * 60 * 60;
|
|
435
|
+
result += ((obj && obj.minutes) || 0) * 60;
|
|
436
|
+
result += (obj && obj.seconds) || 0;
|
|
437
|
+
result += (obj && obj.milliseconds / 1000) || 0;
|
|
438
|
+
|
|
439
|
+
return result;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Returning Object in the format `{ hours: ..., minutes: ..., seconds: ..., milliseconds: ... }`
|
|
444
|
+
* from the result of adding step value in milliseconds to the milliseconds amount.
|
|
445
|
+
* With `precision` parameter rounding the value to the closest step valid interval.
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
__addStep(msec, step, precision) {
|
|
449
|
+
// If the time is `00:00` and step changes value downwards, it should be considered as `24:00`
|
|
450
|
+
if (msec === 0 && step < 0) {
|
|
451
|
+
msec = 24 * 60 * 60 * 1000;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const stepMsec = step * 1000;
|
|
455
|
+
const diffToNext = msec % stepMsec;
|
|
456
|
+
if (stepMsec < 0 && diffToNext && precision) {
|
|
457
|
+
msec -= diffToNext;
|
|
458
|
+
} else if (stepMsec > 0 && diffToNext && precision) {
|
|
459
|
+
msec -= diffToNext - stepMsec;
|
|
460
|
+
} else {
|
|
461
|
+
msec += stepMsec;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var hh = Math.floor(msec / 1000 / 60 / 60);
|
|
465
|
+
msec -= hh * 1000 * 60 * 60;
|
|
466
|
+
var mm = Math.floor(msec / 1000 / 60);
|
|
467
|
+
msec -= mm * 1000 * 60;
|
|
468
|
+
var ss = Math.floor(msec / 1000);
|
|
469
|
+
msec -= ss * 1000;
|
|
470
|
+
|
|
471
|
+
return { hours: hh < 24 ? hh : 0, minutes: mm, seconds: ss, milliseconds: msec };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** @private */
|
|
475
|
+
__updateDropdownItems(i8n, min, max, step) {
|
|
476
|
+
const minTimeObj = this.__validateTime(this.__parseISO(min));
|
|
477
|
+
const minSec = this.__getSec(minTimeObj);
|
|
478
|
+
|
|
479
|
+
const maxTimeObj = this.__validateTime(this.__parseISO(max));
|
|
480
|
+
const maxSec = this.__getSec(maxTimeObj);
|
|
481
|
+
|
|
482
|
+
this.__adjustValue(minSec, maxSec, minTimeObj, maxTimeObj);
|
|
483
|
+
|
|
484
|
+
this.__dropdownItems = this.__generateDropdownList(minSec, maxSec, step);
|
|
485
|
+
|
|
486
|
+
if (step !== this.__oldStep) {
|
|
487
|
+
this.__oldStep = step;
|
|
488
|
+
const parsedObj = this.__validateTime(this.__parseISO(this.value));
|
|
489
|
+
this.__updateValue(parsedObj);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (this.value) {
|
|
493
|
+
this._comboBoxValue = this.i18n.formatTime(this.i18n.parseTime(this.value));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** @private */
|
|
498
|
+
__generateDropdownList(minSec, maxSec, step) {
|
|
499
|
+
if (step < 15 * 60 || !this.__validDayDivisor(step)) {
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const generatedList = [];
|
|
504
|
+
|
|
505
|
+
// Default step in overlay items is 1 hour
|
|
506
|
+
step = step || 3600;
|
|
507
|
+
|
|
508
|
+
let time = -step + minSec;
|
|
509
|
+
while (time + step >= minSec && time + step <= maxSec) {
|
|
510
|
+
const timeObj = this.__validateTime(this.__addStep(time * 1000, step));
|
|
511
|
+
time += step;
|
|
512
|
+
const formatted = this.i18n.formatTime(timeObj);
|
|
513
|
+
generatedList.push({ label: formatted, value: formatted });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return generatedList;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** @private */
|
|
520
|
+
__adjustValue(minSec, maxSec, minTimeObj, maxTimeObj) {
|
|
521
|
+
// Do not change the value if it is empty
|
|
522
|
+
if (!this.__memoValue) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const valSec = this.__getSec(this.__memoValue);
|
|
527
|
+
|
|
528
|
+
if (valSec < minSec) {
|
|
529
|
+
this.__updateValue(minTimeObj);
|
|
530
|
+
} else if (valSec > maxSec) {
|
|
531
|
+
this.__updateValue(maxTimeObj);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Override an observer from `InputMixin`.
|
|
537
|
+
* @protected
|
|
538
|
+
* @override
|
|
539
|
+
*/
|
|
540
|
+
_valueChanged(value, oldValue) {
|
|
541
|
+
const parsedObj = (this.__memoValue = this.__parseISO(value));
|
|
542
|
+
const newValue = this.__formatISO(parsedObj) || '';
|
|
543
|
+
|
|
544
|
+
if (this.value !== '' && this.value !== null && !parsedObj) {
|
|
545
|
+
this.value = oldValue;
|
|
546
|
+
} else if (this.value !== newValue) {
|
|
547
|
+
this.value = newValue;
|
|
548
|
+
} else {
|
|
549
|
+
this.__updateInputValue(parsedObj);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
this._toggleHasValue(!!this.value);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** @private */
|
|
556
|
+
__comboBoxValueChanged(value, oldValue) {
|
|
557
|
+
if (value === '' && oldValue === undefined) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const parsedObj = this.i18n.parseTime(value);
|
|
562
|
+
const newValue = this.i18n.formatTime(parsedObj) || '';
|
|
563
|
+
|
|
564
|
+
if (parsedObj) {
|
|
565
|
+
if (value !== newValue) {
|
|
566
|
+
this._comboBoxValue = newValue;
|
|
567
|
+
} else {
|
|
568
|
+
this.__updateValue(parsedObj);
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
this.value = '';
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** @private */
|
|
576
|
+
__onChange(event) {
|
|
577
|
+
event.stopPropagation();
|
|
578
|
+
|
|
579
|
+
this.validate();
|
|
580
|
+
|
|
581
|
+
this.__dispatchChange();
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** @private */
|
|
585
|
+
__updateValue(obj) {
|
|
586
|
+
const timeString = this.__formatISO(this.__validateTime(obj)) || '';
|
|
587
|
+
this.value = timeString;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** @private */
|
|
591
|
+
__updateInputValue(obj) {
|
|
592
|
+
const timeString = this.i18n.formatTime(this.__validateTime(obj)) || '';
|
|
593
|
+
this._comboBoxValue = timeString;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** @private */
|
|
597
|
+
__validateTime(timeObject) {
|
|
598
|
+
if (timeObject) {
|
|
599
|
+
timeObject.hours = parseInt(timeObject.hours);
|
|
600
|
+
timeObject.minutes = parseInt(timeObject.minutes || 0);
|
|
601
|
+
timeObject.seconds = this.__stepSegment < 3 ? undefined : parseInt(timeObject.seconds || 0);
|
|
602
|
+
timeObject.milliseconds = this.__stepSegment < 4 ? undefined : parseInt(timeObject.milliseconds || 0);
|
|
603
|
+
}
|
|
604
|
+
return timeObject;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** @private */
|
|
608
|
+
get __stepSegment() {
|
|
609
|
+
if (this.step % 3600 === 0) {
|
|
610
|
+
// Accept hours
|
|
611
|
+
return 1;
|
|
612
|
+
} else if (this.step % 60 === 0 || !this.step) {
|
|
613
|
+
// Accept minutes
|
|
614
|
+
return 2;
|
|
615
|
+
} else if (this.step % 1 === 0) {
|
|
616
|
+
// Accept seconds
|
|
617
|
+
return 3;
|
|
618
|
+
} else if (this.step < 1) {
|
|
619
|
+
// Accept milliseconds
|
|
620
|
+
return 4;
|
|
621
|
+
}
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** @private */
|
|
626
|
+
__formatISO(time) {
|
|
627
|
+
// The default i18n formatter implementation is ISO 8601 compliant
|
|
628
|
+
return TimePicker.properties.i18n.value().formatTime(time);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** @private */
|
|
632
|
+
__parseISO(text) {
|
|
633
|
+
// The default i18n parser implementation is ISO 8601 compliant
|
|
634
|
+
return TimePicker.properties.i18n.value().parseTime(text);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Returns true if `time` satisfies the `min` and `max` constraints (if any).
|
|
639
|
+
*
|
|
640
|
+
* @param {!TimePickerTime} time Value to check against constraints
|
|
641
|
+
* @return {boolean} True if `time` satisfies the constraints
|
|
642
|
+
* @protected
|
|
643
|
+
*/
|
|
644
|
+
_timeAllowed(time) {
|
|
645
|
+
const parsedMin = this.i18n.parseTime(this.min);
|
|
646
|
+
const parsedMax = this.i18n.parseTime(this.max);
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
(!this.__getMsec(parsedMin) || this.__getMsec(time) >= this.__getMsec(parsedMin)) &&
|
|
650
|
+
(!this.__getMsec(parsedMax) || this.__getMsec(time) <= this.__getMsec(parsedMax))
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Override method inherited from `ClearButtonMixin`.
|
|
656
|
+
* @protected
|
|
657
|
+
*/
|
|
658
|
+
_onClearButtonClick() {}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Override method inherited from `InputConstraintsMixin`.
|
|
662
|
+
* @protected
|
|
663
|
+
*/
|
|
664
|
+
_onChange() {}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Override method inherited from `InputMixin`.
|
|
668
|
+
* @protected
|
|
669
|
+
*/
|
|
670
|
+
_onInput() {
|
|
671
|
+
// Need to invoke _checkInputValue from PatternMixin to prevent invalid input
|
|
672
|
+
this._checkInputValue();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Fired when the user commits a value change.
|
|
677
|
+
*
|
|
678
|
+
* @event change
|
|
679
|
+
*/
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
customElements.define(TimePicker.is, TimePicker);
|
|
683
|
+
|
|
684
|
+
export { TimePicker };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright (c) 2021 Vaadin Ltd.
|
|
4
|
+
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
|
|
5
|
+
*/
|
|
6
|
+
import { registerStyles, css } from '@vaadin/vaadin-themable-mixin/register-styles.js';
|
|
7
|
+
import '@vaadin/vaadin-lumo-styles/font-icons.js';
|
|
8
|
+
import { inputFieldShared } from '@vaadin/vaadin-lumo-styles/mixins/input-field-shared.js';
|
|
9
|
+
|
|
10
|
+
const timePicker = css`
|
|
11
|
+
[part~='toggle-button']::before {
|
|
12
|
+
content: var(--lumo-icons-clock);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
:host([dir='rtl']) [part='input-field'] ::slotted(input:placeholder-shown) {
|
|
16
|
+
--_lumo-text-field-overflow-mask-image: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:host([dir='rtl']) [part='input-field'] ::slotted(input) {
|
|
20
|
+
--_lumo-text-field-overflow-mask-image: linear-gradient(to left, transparent, #000 1.25em);
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
registerStyles('vaadin-time-picker', [inputFieldShared, timePicker], { moduleId: 'lumo-time-picker' });
|