forked from mirrors/gecko-dev
This moves callers that are using getLocaleInfo to determine the current locale to render widgets to use the new isAppLocaleRTL method. This will allow us to remove pref overrides from getLocaleInfo. Differential Revision: https://phabricator.services.mozilla.com/D96234
1760 lines
48 KiB
JavaScript
1760 lines
48 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
// This is a UA widget. It runs in per-origin UA widget scope,
|
|
// to be loaded by UAWidgetsChild.jsm.
|
|
|
|
/*
|
|
* This is the class of entry. It will construct the actual implementation
|
|
* according to the value of the "type" property.
|
|
*/
|
|
this.DateTimeBoxWidget = class {
|
|
constructor(shadowRoot) {
|
|
this.shadowRoot = shadowRoot;
|
|
this.element = shadowRoot.host;
|
|
this.document = this.element.ownerDocument;
|
|
this.window = this.document.defaultView;
|
|
}
|
|
|
|
/*
|
|
* Callback called by UAWidgets right after constructor.
|
|
*/
|
|
onsetup() {
|
|
this.switchImpl();
|
|
}
|
|
|
|
/*
|
|
* Callback called by UAWidgets when the "type" property changes.
|
|
*/
|
|
onchange() {
|
|
this.switchImpl();
|
|
}
|
|
|
|
/*
|
|
* Actually switch the implementation.
|
|
* - With type equal to "date", DateInputImplWidget should load.
|
|
* - With type equal to "time", TimeInputImplWidget should load.
|
|
* - Otherwise, nothing should load and loaded impl should be unloaded.
|
|
*/
|
|
switchImpl() {
|
|
let newImpl;
|
|
if (this.element.type == "date") {
|
|
newImpl = DateInputImplWidget;
|
|
} else if (this.element.type == "time") {
|
|
newImpl = TimeInputImplWidget;
|
|
}
|
|
// Skip if we are asked to load the same implementation.
|
|
// This can happen if the property is set again w/o value change.
|
|
if (this.impl && this.impl.constructor == newImpl) {
|
|
return;
|
|
}
|
|
if (this.impl) {
|
|
this.impl.destructor();
|
|
this.shadowRoot.firstChild.remove();
|
|
}
|
|
if (newImpl) {
|
|
this.impl = new newImpl(this.shadowRoot);
|
|
this.impl.onsetup();
|
|
} else {
|
|
this.impl = undefined;
|
|
}
|
|
}
|
|
|
|
destructor() {
|
|
if (!this.impl) {
|
|
return;
|
|
}
|
|
this.impl.destructor();
|
|
this.shadowRoot.firstChild.remove();
|
|
delete this.impl;
|
|
}
|
|
};
|
|
|
|
this.DateTimeInputBaseImplWidget = class {
|
|
constructor(shadowRoot) {
|
|
this.shadowRoot = shadowRoot;
|
|
this.element = shadowRoot.host;
|
|
this.document = this.element.ownerDocument;
|
|
this.window = this.document.defaultView;
|
|
}
|
|
|
|
onsetup() {
|
|
this.generateContent();
|
|
|
|
this.DEBUG = false;
|
|
this.mDateTimeBoxElement = this.shadowRoot.firstChild;
|
|
this.mInputElement = this.element;
|
|
this.mLocales = this.window.getWebExposedLocales();
|
|
|
|
this.mIsRTL = false;
|
|
let intlUtils = this.window.intlUtils;
|
|
if (intlUtils) {
|
|
this.mIsRTL = intlUtils.isAppLocaleRTL();
|
|
}
|
|
|
|
if (this.mIsRTL) {
|
|
let inputBoxWrapper = this.shadowRoot.getElementById("input-box-wrapper");
|
|
inputBoxWrapper.dir = "rtl";
|
|
}
|
|
|
|
this.mMin = this.mInputElement.min;
|
|
this.mMax = this.mInputElement.max;
|
|
this.mStep = this.mInputElement.step;
|
|
this.mIsPickerOpen = false;
|
|
|
|
this.mResetButton = this.shadowRoot.getElementById("reset-button");
|
|
this.mResetButton.style.visibility = "hidden";
|
|
this.mResetButton.addEventListener("mousedown", this, {
|
|
mozSystemGroup: true,
|
|
});
|
|
|
|
this.mInputElement.addEventListener(
|
|
"keypress",
|
|
this,
|
|
{
|
|
capture: true,
|
|
mozSystemGroup: true,
|
|
},
|
|
false
|
|
);
|
|
// This is to open the picker when input element is clicked (this
|
|
// includes padding area).
|
|
this.mInputElement.addEventListener(
|
|
"click",
|
|
this,
|
|
{ mozSystemGroup: true },
|
|
false
|
|
);
|
|
|
|
// Those events are dispatched to <div class="datetimebox"> with bubble set
|
|
// to false. They are trapped inside UA Widget Shadow DOM and are not
|
|
// dispatched to the document.
|
|
this.CONTROL_EVENTS.forEach(eventName => {
|
|
this.mDateTimeBoxElement.addEventListener(eventName, this, {}, false);
|
|
});
|
|
}
|
|
|
|
generateContent() {
|
|
/*
|
|
* Pass the markup through XML parser purely for the reason of loading the localization DTD.
|
|
* Remove it when migrate to Fluent (bug 1504363).
|
|
*/
|
|
const parser = new this.window.DOMParser();
|
|
parser.forceEnableDTD();
|
|
let parserDoc = parser.parseFromString(
|
|
`<!DOCTYPE bindings [
|
|
<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd">
|
|
%datetimeboxDTD;
|
|
]>
|
|
<div class="datetimebox" xmlns="http://www.w3.org/1999/xhtml" role="none">
|
|
<link rel="stylesheet" type="text/css" href="chrome://global/content/bindings/datetimebox.css" />
|
|
<div class="datetime-input-box-wrapper" id="input-box-wrapper" role="presentation">
|
|
<span class="datetime-input-edit-wrapper"
|
|
id="edit-wrapper">
|
|
<!-- Each of the date/time input types will append their input child
|
|
- elements here -->
|
|
</span>
|
|
|
|
<button class="datetime-reset-button" id="reset-button" tabindex="-1" aria-label="&datetime.reset.label;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="datetime-reset-button-svg" width="12" height="12">
|
|
<path d="M 3.9,3 3,3.9 5.1,6 3,8.1 3.9,9 6,6.9 8.1,9 9,8.1 6.9,6 9,3.9 8.1,3 6,5.1 Z M 12,6 A 6,6 0 0 1 6,12 6,6 0 0 1 0,6 6,6 0 0 1 6,0 6,6 0 0 1 12,6 Z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="strings"
|
|
data-m-year-place-holder="&date.year.placeholder;"
|
|
data-m-year-label="&date.year.label;"
|
|
data-m-month-place-holder="&date.month.placeholder;"
|
|
data-m-month-label="&date.month.label;"
|
|
data-m-day-place-holder="&date.day.placeholder;"
|
|
data-m-day-label="&date.day.label;"
|
|
|
|
data-m-hour-place-holder="&time.hour.placeholder;"
|
|
data-m-hour-label="&time.hour.label;"
|
|
data-m-minute-place-holder="&time.minute.placeholder;"
|
|
data-m-minute-label="&time.minute.label;"
|
|
data-m-second-place-holder="&time.second.placeholder;"
|
|
data-m-second-label="&time.second.label;"
|
|
data-m-millisecond-place-holder="&time.millisecond.placeholder;"
|
|
data-m-millisecond-label="&time.millisecond.label;"
|
|
data-m-day-period-place-holder="&time.dayperiod.placeholder;"
|
|
data-m-day-period-label="&time.dayperiod.label;"
|
|
></div>
|
|
</div>`,
|
|
"application/xml"
|
|
);
|
|
|
|
/*
|
|
* The <div id="strings"> is also parsed in the document so that there is no
|
|
* need to create another XML document just to get the strings.
|
|
*/
|
|
let stringsElement = parserDoc.getElementById("strings");
|
|
stringsElement.remove();
|
|
for (let key in stringsElement.dataset) {
|
|
// key will be camelCase version of the attribute key above,
|
|
// like mYearPlaceHolder.
|
|
this[key] = stringsElement.dataset[key];
|
|
}
|
|
|
|
this.shadowRoot.importNodeAndAppendChildAt(
|
|
this.shadowRoot,
|
|
parserDoc.documentElement,
|
|
true
|
|
);
|
|
}
|
|
|
|
destructor() {
|
|
this.mResetButton.addEventListener("mousedown", this, {
|
|
mozSystemGroup: true,
|
|
});
|
|
|
|
this.mInputElement.removeEventListener("keypress", this, {
|
|
capture: true,
|
|
mozSystemGroup: true,
|
|
});
|
|
this.mInputElement.removeEventListener("click", this, {
|
|
mozSystemGroup: true,
|
|
});
|
|
|
|
this.CONTROL_EVENTS.forEach(eventName => {
|
|
this.mDateTimeBoxElement.removeEventListener(eventName, this);
|
|
});
|
|
this.mInputElement = null;
|
|
}
|
|
|
|
get FIELD_EVENTS() {
|
|
return ["focus", "blur", "copy", "cut", "paste"];
|
|
}
|
|
|
|
get CONTROL_EVENTS() {
|
|
return [
|
|
"MozDateTimeValueChanged",
|
|
"MozNotifyMinMaxStepAttrChanged",
|
|
"MozFocusInnerTextBox",
|
|
"MozBlurInnerTextBox",
|
|
"MozDateTimeAttributeChanged",
|
|
"MozPickerValueChanged",
|
|
"MozSetDateTimePickerState",
|
|
];
|
|
}
|
|
|
|
addEventListenersToField(aElement) {
|
|
// These events don't bubble out of the Shadow DOM, so we'll have to add
|
|
// event listeners specifically on each of the fields, not just
|
|
// on the <input>
|
|
this.FIELD_EVENTS.forEach(eventName => {
|
|
aElement.addEventListener(
|
|
eventName,
|
|
this,
|
|
{ mozSystemGroup: true },
|
|
false
|
|
);
|
|
});
|
|
}
|
|
|
|
removeEventListenersToField(aElement) {
|
|
if (!aElement) {
|
|
return;
|
|
}
|
|
|
|
this.FIELD_EVENTS.forEach(eventName => {
|
|
aElement.removeEventListener(eventName, this, { mozSystemGroup: true });
|
|
});
|
|
}
|
|
|
|
log(aMsg) {
|
|
if (this.DEBUG) {
|
|
this.window.dump("[DateTimeBox] " + aMsg + "\n");
|
|
}
|
|
}
|
|
|
|
createEditFieldAndAppend(
|
|
aPlaceHolder,
|
|
aLabel,
|
|
aIsNumeric,
|
|
aMinDigits,
|
|
aMaxLength,
|
|
aMinValue,
|
|
aMaxValue,
|
|
aPageUpDownInterval
|
|
) {
|
|
let root = this.shadowRoot.getElementById("edit-wrapper");
|
|
let field = this.shadowRoot.createElementAndAppendChildAt(root, "span");
|
|
field.classList.add("datetime-edit-field");
|
|
field.textContent = aPlaceHolder;
|
|
field.placeholder = aPlaceHolder;
|
|
field.tabIndex = this.mInputElement.tabIndex;
|
|
|
|
field.setAttribute("readonly", this.mInputElement.readOnly);
|
|
field.setAttribute("disabled", this.mInputElement.disabled);
|
|
// Set property as well for convenience.
|
|
field.disabled = this.mInputElement.disabled;
|
|
field.readOnly = this.mInputElement.readOnly;
|
|
field.setAttribute("aria-label", aLabel);
|
|
|
|
// Used to store the non-formatted value, cleared when value is
|
|
// cleared.
|
|
// DateTimeInputTypeBase::HasBadInput() will read this to decide
|
|
// if the input has value.
|
|
field.setAttribute("value", "");
|
|
|
|
if (aIsNumeric) {
|
|
field.classList.add("numeric");
|
|
// Maximum value allowed.
|
|
field.setAttribute("min", aMinValue);
|
|
// Minumim value allowed.
|
|
field.setAttribute("max", aMaxValue);
|
|
// Interval when pressing pageUp/pageDown key.
|
|
field.setAttribute("pginterval", aPageUpDownInterval);
|
|
// Used to store what the user has already typed in the field,
|
|
// cleared when value is cleared and when field is blurred.
|
|
field.setAttribute("typeBuffer", "");
|
|
// Minimum digits to display, padded with leading 0s.
|
|
field.setAttribute("mindigits", aMinDigits);
|
|
// Maximum length for the field, will be advance to the next field
|
|
// automatically if exceeded.
|
|
field.setAttribute("maxlength", aMaxLength);
|
|
// Set spinbutton ARIA role
|
|
field.setAttribute("role", "spinbutton");
|
|
|
|
if (this.mIsRTL) {
|
|
// Force the direction to be "ltr", so that the field stays in the
|
|
// same order even when it's empty (with placeholder). By using
|
|
// "embed", the text inside the element is still displayed based
|
|
// on its directionality.
|
|
field.style.unicodeBidi = "embed";
|
|
field.style.direction = "ltr";
|
|
}
|
|
} else {
|
|
// Set generic textbox ARIA role
|
|
field.setAttribute("role", "textbox");
|
|
}
|
|
|
|
return field;
|
|
}
|
|
|
|
updateResetButtonVisibility() {
|
|
if (this.isAnyFieldAvailable(false) && !this.isRequired()) {
|
|
this.mResetButton.style.visibility = "";
|
|
} else {
|
|
this.mResetButton.style.visibility = "hidden";
|
|
}
|
|
}
|
|
|
|
focusInnerTextBox() {
|
|
this.log("Focus inner editable field.");
|
|
|
|
let editRoot = this.shadowRoot.getElementById("edit-wrapper");
|
|
for (let child of editRoot.querySelectorAll(
|
|
":scope > span.datetime-edit-field"
|
|
)) {
|
|
this.mLastFocusedField = child;
|
|
child.focus();
|
|
this.log("focused");
|
|
break;
|
|
}
|
|
}
|
|
|
|
blurInnerTextBox() {
|
|
this.log("Blur inner editable field.");
|
|
|
|
if (this.mLastFocusedField) {
|
|
this.mLastFocusedField.blur();
|
|
} else {
|
|
// If .mLastFocusedField hasn't been set, blur all editable fields,
|
|
// so that the bound element will actually be blurred. Note that
|
|
// blurring on a element that has no focus won't have any effect.
|
|
let editRoot = this.shadowRoot.getElementById("edit-wrapper");
|
|
for (let child of editRoot.querySelectorAll(
|
|
":scope > span.datetime-edit-field"
|
|
)) {
|
|
child.blur();
|
|
}
|
|
}
|
|
}
|
|
|
|
notifyInputElementValueChanged() {
|
|
this.log("inputElementValueChanged");
|
|
this.setFieldsFromInputValue();
|
|
}
|
|
|
|
notifyMinMaxStepAttrChanged() {
|
|
// No operation by default
|
|
}
|
|
|
|
setValueFromPicker(aValue) {
|
|
this.setFieldsFromPicker(aValue);
|
|
}
|
|
|
|
advanceToNextField(aReverse) {
|
|
this.log("advanceToNextField");
|
|
|
|
let focusedInput = this.mLastFocusedField;
|
|
let next = aReverse
|
|
? focusedInput.previousElementSibling
|
|
: focusedInput.nextElementSibling;
|
|
if (!next && !aReverse) {
|
|
this.setInputValueFromFields();
|
|
return;
|
|
}
|
|
|
|
while (next) {
|
|
if (next.matches("span.datetime-edit-field")) {
|
|
next.focus();
|
|
break;
|
|
}
|
|
next = aReverse ? next.previousElementSibling : next.nextElementSibling;
|
|
}
|
|
}
|
|
|
|
setPickerState(aIsOpen) {
|
|
this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
|
|
this.mIsPickerOpen = aIsOpen;
|
|
}
|
|
|
|
updateEditAttributes() {
|
|
this.log("updateEditAttributes");
|
|
|
|
let editRoot = this.shadowRoot.getElementById("edit-wrapper");
|
|
|
|
for (let child of editRoot.querySelectorAll(
|
|
":scope > span.datetime-edit-field"
|
|
)) {
|
|
// "disabled" and "readonly" must be set as attributes because they
|
|
// are not defined properties of HTMLSpanElement, and the stylesheet
|
|
// checks the literal string attribute values.
|
|
child.setAttribute("disabled", this.mInputElement.disabled);
|
|
child.setAttribute("readonly", this.mInputElement.readOnly);
|
|
|
|
// Set property as well for convenience.
|
|
child.disabled = this.mInputElement.disabled;
|
|
child.readOnly = this.mInputElement.readOnly;
|
|
|
|
// tabIndex works on all elements
|
|
child.tabIndex = this.mInputElement.tabIndex;
|
|
}
|
|
|
|
this.mResetButton.disabled =
|
|
this.mInputElement.disabled || this.mInputElement.readOnly;
|
|
this.updateResetButtonVisibility();
|
|
}
|
|
|
|
isEmpty(aValue) {
|
|
return aValue == undefined || 0 === aValue.length;
|
|
}
|
|
|
|
getFieldValue(aField) {
|
|
if (!aField || !aField.classList.contains("numeric")) {
|
|
return undefined;
|
|
}
|
|
|
|
let value = aField.getAttribute("value");
|
|
// Avoid returning 0 when field is empty.
|
|
return this.isEmpty(value) ? undefined : Number(value);
|
|
}
|
|
|
|
clearFieldValue(aField) {
|
|
aField.textContent = aField.placeholder;
|
|
aField.setAttribute("value", "");
|
|
if (aField.classList.contains("numeric")) {
|
|
aField.setAttribute("typeBuffer", "");
|
|
}
|
|
this.updateResetButtonVisibility();
|
|
}
|
|
|
|
setFieldValue() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
clearInputFields() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
setFieldsFromInputValue() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
setInputValueFromFields() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
setFieldsFromPicker() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
handleKeypress() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
handleKeyboardNav() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
getCurrentValue() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
isAnyFieldAvailable() {
|
|
throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
|
|
}
|
|
|
|
notifyPicker() {
|
|
if (this.mIsPickerOpen && this.isAnyFieldAvailable(true)) {
|
|
this.mInputElement.updateDateTimePicker(this.getCurrentValue());
|
|
}
|
|
}
|
|
|
|
isDisabled() {
|
|
return this.mInputElement.hasAttribute("disabled");
|
|
}
|
|
|
|
isReadonly() {
|
|
return this.mInputElement.hasAttribute("readonly");
|
|
}
|
|
|
|
isEditable() {
|
|
return !this.isDisabled() && !this.isReadonly();
|
|
}
|
|
|
|
isRequired() {
|
|
return this.mInputElement.hasAttribute("required");
|
|
}
|
|
|
|
containingTree() {
|
|
return this.mInputElement.containingShadowRoot || this.document;
|
|
}
|
|
|
|
handleEvent(aEvent) {
|
|
this.log("handleEvent: " + aEvent.type);
|
|
|
|
if (!aEvent.isTrusted) {
|
|
return;
|
|
}
|
|
|
|
switch (aEvent.type) {
|
|
case "MozDateTimeValueChanged": {
|
|
this.notifyInputElementValueChanged();
|
|
break;
|
|
}
|
|
case "MozNotifyMinMaxStepAttrChanged": {
|
|
this.notifyMinMaxStepAttrChanged();
|
|
break;
|
|
}
|
|
case "MozFocusInnerTextBox": {
|
|
this.focusInnerTextBox();
|
|
break;
|
|
}
|
|
case "MozBlurInnerTextBox": {
|
|
this.blurInnerTextBox();
|
|
break;
|
|
}
|
|
case "MozDateTimeAttributeChanged": {
|
|
this.updateEditAttributes();
|
|
break;
|
|
}
|
|
case "MozPickerValueChanged": {
|
|
this.setValueFromPicker(aEvent.detail);
|
|
break;
|
|
}
|
|
case "MozSetDateTimePickerState": {
|
|
this.setPickerState(aEvent.detail);
|
|
break;
|
|
}
|
|
case "keypress": {
|
|
this.onKeyPress(aEvent);
|
|
break;
|
|
}
|
|
case "click": {
|
|
this.onClick(aEvent);
|
|
break;
|
|
}
|
|
case "focus": {
|
|
this.onFocus(aEvent);
|
|
break;
|
|
}
|
|
case "blur": {
|
|
this.onBlur(aEvent);
|
|
break;
|
|
}
|
|
case "mousedown":
|
|
case "copy":
|
|
case "cut":
|
|
case "paste": {
|
|
aEvent.preventDefault();
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
onFocus(aEvent) {
|
|
this.log("onFocus originalTarget: " + aEvent.originalTarget);
|
|
if (this.containingTree().activeElement != this.mInputElement) {
|
|
return;
|
|
}
|
|
|
|
let target = aEvent.originalTarget;
|
|
if (target.matches("span.datetime-edit-field")) {
|
|
if (target.disabled) {
|
|
return;
|
|
}
|
|
this.mLastFocusedField = target;
|
|
this.mInputElement.setFocusState(true);
|
|
}
|
|
}
|
|
|
|
onBlur(aEvent) {
|
|
this.log(
|
|
"onBlur originalTarget: " +
|
|
aEvent.originalTarget +
|
|
" target: " +
|
|
aEvent.target +
|
|
" rt: " +
|
|
aEvent.relatedTarget
|
|
);
|
|
|
|
let target = aEvent.originalTarget;
|
|
target.setAttribute("typeBuffer", "");
|
|
this.setInputValueFromFields();
|
|
// No need to set and unset the focus state if the focus is staying within
|
|
// our input. Same about closing the picker.
|
|
if (aEvent.relatedTarget != this.mInputElement) {
|
|
this.mInputElement.setFocusState(false);
|
|
if (this.mIsPickerOpen) {
|
|
this.mInputElement.closeDateTimePicker();
|
|
}
|
|
}
|
|
}
|
|
|
|
onKeyPress(aEvent) {
|
|
this.log("onKeyPress key: " + aEvent.key);
|
|
|
|
switch (aEvent.key) {
|
|
// Toggle the picker on space/enter, close on Escape.
|
|
case "Enter":
|
|
case "Escape":
|
|
case " ": {
|
|
if (this.mIsPickerOpen) {
|
|
this.mInputElement.closeDateTimePicker();
|
|
} else if (aEvent.key != "Escape") {
|
|
this.mInputElement.openDateTimePicker(this.getCurrentValue());
|
|
} else {
|
|
// Don't preventDefault();
|
|
break;
|
|
}
|
|
aEvent.preventDefault();
|
|
break;
|
|
}
|
|
case "Backspace": {
|
|
// TODO(emilio, bug 1571533): These functions should look at
|
|
// defaultPrevented.
|
|
if (this.isEditable()) {
|
|
let targetField = aEvent.originalTarget;
|
|
this.clearFieldValue(targetField);
|
|
this.setInputValueFromFields();
|
|
aEvent.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
case "ArrowRight":
|
|
case "ArrowLeft": {
|
|
this.advanceToNextField(!(aEvent.key == "ArrowRight"));
|
|
aEvent.preventDefault();
|
|
break;
|
|
}
|
|
case "ArrowUp":
|
|
case "ArrowDown":
|
|
case "PageUp":
|
|
case "PageDown":
|
|
case "Home":
|
|
case "End": {
|
|
this.handleKeyboardNav(aEvent);
|
|
aEvent.preventDefault();
|
|
break;
|
|
}
|
|
default: {
|
|
// printable characters
|
|
if (
|
|
aEvent.keyCode == 0 &&
|
|
!(aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
|
|
) {
|
|
this.handleKeypress(aEvent);
|
|
aEvent.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
onClick(aEvent) {
|
|
this.log(
|
|
"onClick originalTarget: " +
|
|
aEvent.originalTarget +
|
|
" target: " +
|
|
aEvent.target
|
|
);
|
|
|
|
if (aEvent.defaultPrevented || !this.isEditable()) {
|
|
return;
|
|
}
|
|
|
|
if (aEvent.originalTarget == this.mResetButton) {
|
|
this.clearInputFields(false);
|
|
} else if (!this.mIsPickerOpen) {
|
|
this.mInputElement.openDateTimePicker(this.getCurrentValue());
|
|
}
|
|
}
|
|
};
|
|
|
|
this.DateInputImplWidget = class extends DateTimeInputBaseImplWidget {
|
|
constructor(shadowRoot) {
|
|
super(shadowRoot);
|
|
}
|
|
|
|
onsetup() {
|
|
super.onsetup();
|
|
|
|
this.mMinMonth = 1;
|
|
this.mMaxMonth = 12;
|
|
this.mMinDay = 1;
|
|
this.mMaxDay = 31;
|
|
this.mMinYear = 1;
|
|
// Maximum year limited by ECMAScript date object range, year <= 275760.
|
|
this.mMaxYear = 275760;
|
|
this.mMonthDayLength = 2;
|
|
this.mYearLength = 4;
|
|
this.mMonthPageUpDownInterval = 3;
|
|
this.mDayPageUpDownInterval = 7;
|
|
this.mYearPageUpDownInterval = 10;
|
|
|
|
this.buildEditFields();
|
|
this.updateEditAttributes();
|
|
|
|
if (this.mInputElement.value) {
|
|
this.setFieldsFromInputValue();
|
|
}
|
|
}
|
|
|
|
destructor() {
|
|
this.removeEventListenersToField(this.mYearField);
|
|
this.removeEventListenersToField(this.mMonthField);
|
|
this.removeEventListenersToField(this.mDayField);
|
|
super.destructor();
|
|
}
|
|
|
|
buildEditFields() {
|
|
let root = this.shadowRoot.getElementById("edit-wrapper");
|
|
|
|
let yearMaxLength = this.mMaxYear.toString().length;
|
|
|
|
let formatter = Intl.DateTimeFormat(this.mLocales, {
|
|
year: "numeric",
|
|
month: "numeric",
|
|
day: "numeric",
|
|
});
|
|
formatter.formatToParts(Date.now()).map(part => {
|
|
switch (part.type) {
|
|
case "year":
|
|
this.mYearField = this.createEditFieldAndAppend(
|
|
this.mYearPlaceHolder,
|
|
this.mYearLabel,
|
|
true,
|
|
this.mYearLength,
|
|
yearMaxLength,
|
|
this.mMinYear,
|
|
this.mMaxYear,
|
|
this.mYearPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mYearField);
|
|
break;
|
|
case "month":
|
|
this.mMonthField = this.createEditFieldAndAppend(
|
|
this.mMonthPlaceHolder,
|
|
this.mMonthLabel,
|
|
true,
|
|
this.mMonthDayLength,
|
|
this.mMonthDayLength,
|
|
this.mMinMonth,
|
|
this.mMaxMonth,
|
|
this.mMonthPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mMonthField);
|
|
break;
|
|
case "day":
|
|
this.mDayField = this.createEditFieldAndAppend(
|
|
this.mDayPlaceHolder,
|
|
this.mDayLabel,
|
|
true,
|
|
this.mMonthDayLength,
|
|
this.mMonthDayLength,
|
|
this.mMinDay,
|
|
this.mMaxDay,
|
|
this.mDayPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mDayField);
|
|
break;
|
|
default:
|
|
let span = this.shadowRoot.createElementAndAppendChildAt(
|
|
root,
|
|
"span"
|
|
);
|
|
span.textContent = part.value;
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
clearInputFields(aFromInputElement) {
|
|
this.log("clearInputFields");
|
|
|
|
if (this.mMonthField) {
|
|
this.clearFieldValue(this.mMonthField);
|
|
}
|
|
|
|
if (this.mDayField) {
|
|
this.clearFieldValue(this.mDayField);
|
|
}
|
|
|
|
if (this.mYearField) {
|
|
this.clearFieldValue(this.mYearField);
|
|
}
|
|
|
|
if (!aFromInputElement) {
|
|
if (this.mInputElement.value) {
|
|
this.mInputElement.setUserInput("");
|
|
} else {
|
|
this.mInputElement.updateValidityState();
|
|
}
|
|
}
|
|
}
|
|
|
|
setFieldsFromInputValue() {
|
|
let value = this.mInputElement.value;
|
|
if (!value) {
|
|
this.clearInputFields(true);
|
|
return;
|
|
}
|
|
|
|
this.log("setFieldsFromInputValue: " + value);
|
|
let [year, month, day] = value.split("-");
|
|
|
|
this.setFieldValue(this.mYearField, year);
|
|
this.setFieldValue(this.mMonthField, month);
|
|
this.setFieldValue(this.mDayField, day);
|
|
|
|
this.notifyPicker();
|
|
}
|
|
|
|
setInputValueFromFields() {
|
|
if (this.isAnyFieldEmpty()) {
|
|
// Clear input element's value if any of the field has been cleared,
|
|
// otherwise update the validity state, since it may become "not"
|
|
// invalid if fields are not complete.
|
|
if (this.mInputElement.value) {
|
|
this.mInputElement.setUserInput("");
|
|
} else {
|
|
this.mInputElement.updateValidityState();
|
|
}
|
|
// We still need to notify picker in case any of the field has
|
|
// changed.
|
|
this.notifyPicker();
|
|
return;
|
|
}
|
|
|
|
let { year, month, day } = this.getCurrentValue();
|
|
|
|
// Convert to a valid date string according to:
|
|
// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-date-string
|
|
year = year.toString().padStart(this.mYearLength, "0");
|
|
month = month < 10 ? "0" + month : month;
|
|
day = day < 10 ? "0" + day : day;
|
|
|
|
let date = [year, month, day].join("-");
|
|
|
|
if (date == this.mInputElement.value) {
|
|
return;
|
|
}
|
|
|
|
this.log("setInputValueFromFields: " + date);
|
|
this.notifyPicker();
|
|
this.mInputElement.setUserInput(date);
|
|
}
|
|
|
|
setFieldsFromPicker(aValue) {
|
|
let year = aValue.year;
|
|
let month = aValue.month;
|
|
let day = aValue.day;
|
|
|
|
if (!this.isEmpty(year)) {
|
|
this.setFieldValue(this.mYearField, year);
|
|
}
|
|
|
|
if (!this.isEmpty(month)) {
|
|
this.setFieldValue(this.mMonthField, month);
|
|
}
|
|
|
|
if (!this.isEmpty(day)) {
|
|
this.setFieldValue(this.mDayField, day);
|
|
}
|
|
|
|
// Update input element's .value if needed.
|
|
this.setInputValueFromFields();
|
|
}
|
|
|
|
handleKeypress(aEvent) {
|
|
if (!this.isEditable()) {
|
|
return;
|
|
}
|
|
|
|
let targetField = aEvent.originalTarget;
|
|
let key = aEvent.key;
|
|
|
|
if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
|
|
let buffer = targetField.getAttribute("typeBuffer") || "";
|
|
|
|
buffer = buffer.concat(key);
|
|
this.setFieldValue(targetField, buffer);
|
|
|
|
let n = Number(buffer);
|
|
let max = targetField.getAttribute("max");
|
|
let maxLength = targetField.getAttribute("maxlength");
|
|
if (buffer.length >= maxLength || n * 10 > max) {
|
|
buffer = "";
|
|
this.advanceToNextField();
|
|
}
|
|
targetField.setAttribute("typeBuffer", buffer);
|
|
if (!this.isAnyFieldEmpty()) {
|
|
this.setInputValueFromFields();
|
|
}
|
|
}
|
|
}
|
|
|
|
incrementFieldValue(aTargetField, aTimes) {
|
|
let value = this.getFieldValue(aTargetField);
|
|
|
|
// Use current date if field is empty.
|
|
if (this.isEmpty(value)) {
|
|
let now = new Date();
|
|
|
|
if (aTargetField == this.mYearField) {
|
|
value = now.getFullYear();
|
|
} else if (aTargetField == this.mMonthField) {
|
|
value = now.getMonth() + 1;
|
|
} else if (aTargetField == this.mDayField) {
|
|
value = now.getDate();
|
|
} else {
|
|
this.log("Field not supported in incrementFieldValue.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
let min = Number(aTargetField.getAttribute("min"));
|
|
let max = Number(aTargetField.getAttribute("max"));
|
|
|
|
value += Number(aTimes);
|
|
if (value > max) {
|
|
value -= max - min + 1;
|
|
} else if (value < min) {
|
|
value += max - min + 1;
|
|
}
|
|
|
|
this.setFieldValue(aTargetField, value);
|
|
}
|
|
|
|
handleKeyboardNav(aEvent) {
|
|
if (!this.isEditable()) {
|
|
return;
|
|
}
|
|
|
|
let targetField = aEvent.originalTarget;
|
|
let key = aEvent.key;
|
|
|
|
// Home/End key does nothing on year field.
|
|
if (targetField == this.mYearField && (key == "Home" || key == "End")) {
|
|
return;
|
|
}
|
|
|
|
switch (key) {
|
|
case "ArrowUp":
|
|
this.incrementFieldValue(targetField, 1);
|
|
break;
|
|
case "ArrowDown":
|
|
this.incrementFieldValue(targetField, -1);
|
|
break;
|
|
case "PageUp": {
|
|
let interval = targetField.getAttribute("pginterval");
|
|
this.incrementFieldValue(targetField, interval);
|
|
break;
|
|
}
|
|
case "PageDown": {
|
|
let interval = targetField.getAttribute("pginterval");
|
|
this.incrementFieldValue(targetField, 0 - interval);
|
|
break;
|
|
}
|
|
case "Home":
|
|
let min = targetField.getAttribute("min");
|
|
this.setFieldValue(targetField, min);
|
|
break;
|
|
case "End":
|
|
let max = targetField.getAttribute("max");
|
|
this.setFieldValue(targetField, max);
|
|
break;
|
|
}
|
|
this.setInputValueFromFields();
|
|
}
|
|
|
|
getCurrentValue() {
|
|
let year = this.getFieldValue(this.mYearField);
|
|
let month = this.getFieldValue(this.mMonthField);
|
|
let day = this.getFieldValue(this.mDayField);
|
|
|
|
let date = { year, month, day };
|
|
|
|
this.log("getCurrentValue: " + JSON.stringify(date));
|
|
return date;
|
|
}
|
|
|
|
setFieldValue(aField, aValue) {
|
|
if (!aField || !aField.classList.contains("numeric")) {
|
|
return;
|
|
}
|
|
|
|
let value = Number(aValue);
|
|
if (isNaN(value)) {
|
|
this.log("NaN on setFieldValue!");
|
|
return;
|
|
}
|
|
|
|
let maxLength = aField.getAttribute("maxlength");
|
|
if (aValue.length == maxLength) {
|
|
let min = Number(aField.getAttribute("min"));
|
|
let max = Number(aField.getAttribute("max"));
|
|
|
|
if (value < min) {
|
|
value = min;
|
|
} else if (value > max) {
|
|
value = max;
|
|
}
|
|
}
|
|
|
|
aField.setAttribute("value", value);
|
|
|
|
// Display formatted value based on locale.
|
|
let minDigits = aField.getAttribute("mindigits");
|
|
let formatted = value.toLocaleString(this.mLocales, {
|
|
minimumIntegerDigits: minDigits,
|
|
useGrouping: false,
|
|
});
|
|
|
|
aField.textContent = formatted;
|
|
aField.setAttribute("aria-valuetext", formatted);
|
|
this.updateResetButtonVisibility();
|
|
}
|
|
|
|
isAnyFieldAvailable(aForPicker) {
|
|
let { year, month, day } = this.getCurrentValue();
|
|
|
|
return !this.isEmpty(year) || !this.isEmpty(month) || !this.isEmpty(day);
|
|
}
|
|
|
|
isAnyFieldEmpty() {
|
|
let { year, month, day } = this.getCurrentValue();
|
|
|
|
return this.isEmpty(year) || this.isEmpty(month) || this.isEmpty(day);
|
|
}
|
|
};
|
|
|
|
this.TimeInputImplWidget = class extends DateTimeInputBaseImplWidget {
|
|
constructor(shadowRoot) {
|
|
super(shadowRoot);
|
|
}
|
|
|
|
onsetup() {
|
|
super.onsetup();
|
|
|
|
const kDefaultAMString = "AM";
|
|
const kDefaultPMString = "PM";
|
|
|
|
let { amString, pmString } = this.getStringsForLocale(this.mLocales);
|
|
|
|
this.mAMIndicator = amString || kDefaultAMString;
|
|
this.mPMIndicator = pmString || kDefaultPMString;
|
|
|
|
this.mHour12 = this.is12HourTime(this.mLocales);
|
|
this.mMillisecSeparatorText = ".";
|
|
this.mMaxLength = 2;
|
|
this.mMillisecMaxLength = 3;
|
|
this.mDefaultStep = 60 * 1000; // in milliseconds
|
|
|
|
this.mMinHour = this.mHour12 ? 1 : 0;
|
|
this.mMaxHour = this.mHour12 ? 12 : 23;
|
|
this.mMinMinute = 0;
|
|
this.mMaxMinute = 59;
|
|
this.mMinSecond = 0;
|
|
this.mMaxSecond = 59;
|
|
this.mMinMillisecond = 0;
|
|
this.mMaxMillisecond = 999;
|
|
|
|
this.mHourPageUpDownInterval = 3;
|
|
this.mMinSecPageUpDownInterval = 10;
|
|
|
|
this.buildEditFields();
|
|
this.updateEditAttributes();
|
|
|
|
if (this.mInputElement.value) {
|
|
this.setFieldsFromInputValue();
|
|
}
|
|
}
|
|
|
|
destructor() {
|
|
this.removeEventListenersToField(this.mHourField);
|
|
this.removeEventListenersToField(this.mMinuteField);
|
|
this.removeEventListenersToField(this.mSecondField);
|
|
this.removeEventListenersToField(this.mMillisecField);
|
|
this.removeEventListenersToField(this.mDayPeriodField);
|
|
super.destructor();
|
|
}
|
|
|
|
get kMsPerSecond() {
|
|
return 1000;
|
|
}
|
|
|
|
get kMsPerMinute() {
|
|
return 60 * 1000;
|
|
}
|
|
|
|
getInputElementValues() {
|
|
let value = this.mInputElement.value;
|
|
if (value.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
let hour, minute, second, millisecond;
|
|
[hour, minute, second] = value.split(":");
|
|
if (second) {
|
|
[second, millisecond] = second.split(".");
|
|
|
|
// Convert fraction of second to milliseconds.
|
|
if (millisecond && millisecond.length === 1) {
|
|
millisecond *= 100;
|
|
} else if (millisecond && millisecond.length === 2) {
|
|
millisecond *= 10;
|
|
}
|
|
}
|
|
|
|
return { hour, minute, second, millisecond };
|
|
}
|
|
|
|
hasSecondField() {
|
|
return !!this.mSecondField;
|
|
}
|
|
|
|
hasMillisecField() {
|
|
return !!this.mMillisecField;
|
|
}
|
|
|
|
hasDayPeriodField() {
|
|
return !!this.mDayPeriodField;
|
|
}
|
|
|
|
shouldShowSecondField() {
|
|
let { second } = this.getInputElementValues();
|
|
if (second != undefined) {
|
|
return true;
|
|
}
|
|
|
|
let stepBase = this.mInputElement.getStepBase();
|
|
if (stepBase % this.kMsPerMinute != 0) {
|
|
return true;
|
|
}
|
|
|
|
let step = this.mInputElement.getStep();
|
|
if (step % this.kMsPerMinute != 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
shouldShowMillisecField() {
|
|
let { millisecond } = this.getInputElementValues();
|
|
if (millisecond != undefined) {
|
|
return true;
|
|
}
|
|
|
|
let stepBase = this.mInputElement.getStepBase();
|
|
if (stepBase % this.kMsPerSecond != 0) {
|
|
return true;
|
|
}
|
|
|
|
let step = this.mInputElement.getStep();
|
|
if (step % this.kMsPerSecond != 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
rebuildEditFieldsIfNeeded() {
|
|
if (
|
|
this.shouldShowSecondField() == this.hasSecondField() &&
|
|
this.shouldShowMillisecField() == this.hasMillisecField()
|
|
) {
|
|
return;
|
|
}
|
|
|
|
let focused = this.mInputElement.matches(":focus");
|
|
|
|
let root = this.shadowRoot.getElementById("edit-wrapper");
|
|
while (root.firstChild) {
|
|
root.firstChild.remove();
|
|
}
|
|
|
|
this.removeEventListenersToField(this.mHourField);
|
|
this.removeEventListenersToField(this.mMinuteField);
|
|
this.removeEventListenersToField(this.mSecondField);
|
|
this.removeEventListenersToField(this.mMillisecField);
|
|
this.removeEventListenersToField(this.mDayPeriodField);
|
|
|
|
this.mHourField = null;
|
|
this.mMinuteField = null;
|
|
this.mSecondField = null;
|
|
this.mMillisecField = null;
|
|
this.mDayPeriodField = null;
|
|
|
|
this.buildEditFields();
|
|
if (focused) {
|
|
this.focusInnerTextBox();
|
|
}
|
|
}
|
|
|
|
buildEditFields() {
|
|
let root = this.shadowRoot.getElementById("edit-wrapper");
|
|
|
|
let options = {
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
hour12: this.mHour12,
|
|
};
|
|
|
|
if (this.shouldShowSecondField()) {
|
|
options.second = "numeric";
|
|
}
|
|
|
|
let formatter = Intl.DateTimeFormat(this.mLocales, options);
|
|
formatter.formatToParts(Date.now()).map(part => {
|
|
switch (part.type) {
|
|
case "hour":
|
|
this.mHourField = this.createEditFieldAndAppend(
|
|
this.mHourPlaceHolder,
|
|
this.mHourLabel,
|
|
true,
|
|
this.mMaxLength,
|
|
this.mMaxLength,
|
|
this.mMinHour,
|
|
this.mMaxHour,
|
|
this.mHourPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mHourField);
|
|
break;
|
|
case "minute":
|
|
this.mMinuteField = this.createEditFieldAndAppend(
|
|
this.mMinutePlaceHolder,
|
|
this.mMinuteLabel,
|
|
true,
|
|
this.mMaxLength,
|
|
this.mMaxLength,
|
|
this.mMinMinute,
|
|
this.mMaxMinute,
|
|
this.mMinSecPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mMinuteField);
|
|
break;
|
|
case "second":
|
|
this.mSecondField = this.createEditFieldAndAppend(
|
|
this.mSecondPlaceHolder,
|
|
this.mSecondLabel,
|
|
true,
|
|
this.mMaxLength,
|
|
this.mMaxLength,
|
|
this.mMinSecond,
|
|
this.mMaxSecond,
|
|
this.mMinSecPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mSecondField);
|
|
if (this.shouldShowMillisecField()) {
|
|
// Intl.DateTimeFormat does not support millisecond, so we
|
|
// need to handle this on our own.
|
|
let span = this.shadowRoot.createElementAndAppendChildAt(
|
|
root,
|
|
"span"
|
|
);
|
|
span.textContent = this.mMillisecSeparatorText;
|
|
this.mMillisecField = this.createEditFieldAndAppend(
|
|
this.mMillisecPlaceHolder,
|
|
this.mMillisecLabel,
|
|
true,
|
|
this.mMillisecMaxLength,
|
|
this.mMillisecMaxLength,
|
|
this.mMinMillisecond,
|
|
this.mMaxMillisecond,
|
|
this.mMinSecPageUpDownInterval
|
|
);
|
|
this.addEventListenersToField(this.mMillisecField);
|
|
}
|
|
break;
|
|
case "dayPeriod":
|
|
this.mDayPeriodField = this.createEditFieldAndAppend(
|
|
this.mDayPeriodPlaceHolder,
|
|
this.mDayPeriodLabel,
|
|
false
|
|
);
|
|
this.addEventListenersToField(this.mDayPeriodField);
|
|
|
|
// Give aria autocomplete hint for am/pm
|
|
this.mDayPeriodField.setAttribute("aria-autocomplete", "inline");
|
|
break;
|
|
default:
|
|
let span = this.shadowRoot.createElementAndAppendChildAt(
|
|
root,
|
|
"span"
|
|
);
|
|
span.textContent = part.value;
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
getStringsForLocale(aLocales) {
|
|
this.log("getStringsForLocale: " + aLocales);
|
|
|
|
let intlUtils = this.window.intlUtils;
|
|
if (!intlUtils) {
|
|
return {};
|
|
}
|
|
|
|
let amString, pmString;
|
|
let keys = [
|
|
"dates/gregorian/dayperiods/am",
|
|
"dates/gregorian/dayperiods/pm",
|
|
];
|
|
|
|
let result = intlUtils.getDisplayNames(this.mLocales, {
|
|
style: "short",
|
|
keys,
|
|
});
|
|
|
|
[amString, pmString] = keys.map(key => result.values[key]);
|
|
|
|
return { amString, pmString };
|
|
}
|
|
|
|
is12HourTime(aLocales) {
|
|
let options = new Intl.DateTimeFormat(aLocales, {
|
|
hour: "numeric",
|
|
}).resolvedOptions();
|
|
|
|
return options.hour12;
|
|
}
|
|
|
|
setFieldsFromInputValue() {
|
|
let { hour, minute, second, millisecond } = this.getInputElementValues();
|
|
|
|
if (this.isEmpty(hour) && this.isEmpty(minute)) {
|
|
this.clearInputFields(true);
|
|
return;
|
|
}
|
|
|
|
// Second and millisecond part are optional, rebuild edit fields if
|
|
// needed.
|
|
this.rebuildEditFieldsIfNeeded();
|
|
|
|
this.setFieldValue(this.mHourField, hour);
|
|
this.setFieldValue(this.mMinuteField, minute);
|
|
if (this.mHour12) {
|
|
this.setDayPeriodValue(
|
|
hour >= this.mMaxHour ? this.mPMIndicator : this.mAMIndicator
|
|
);
|
|
}
|
|
|
|
if (this.hasSecondField()) {
|
|
this.setFieldValue(this.mSecondField, second != undefined ? second : 0);
|
|
}
|
|
|
|
if (this.hasMillisecField()) {
|
|
this.setFieldValue(
|
|
this.mMillisecField,
|
|
millisecond != undefined ? millisecond : 0
|
|
);
|
|
}
|
|
|
|
this.notifyPicker();
|
|
}
|
|
|
|
setInputValueFromFields() {
|
|
if (this.isAnyFieldEmpty()) {
|
|
// Clear input element's value if any of the field has been cleared,
|
|
// otherwise update the validity state, since it may become "not"
|
|
// invalid if fields are not complete.
|
|
if (this.mInputElement.value) {
|
|
this.mInputElement.setUserInput("");
|
|
} else {
|
|
this.mInputElement.updateValidityState();
|
|
}
|
|
// We still need to notify picker in case any of the field has
|
|
// changed.
|
|
this.notifyPicker();
|
|
return;
|
|
}
|
|
|
|
let { hour, minute, second, millisecond } = this.getCurrentValue();
|
|
let dayPeriod = this.getDayPeriodValue();
|
|
|
|
// Convert to a valid time string according to:
|
|
// https://html.spec.whatwg.org/multipage/infrastructure.html#valid-time-string
|
|
if (this.mHour12) {
|
|
if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
|
|
hour += this.mMaxHour;
|
|
} else if (dayPeriod == this.mAMIndicator && hour == this.mMaxHour) {
|
|
hour = 0;
|
|
}
|
|
}
|
|
|
|
hour = hour < 10 ? "0" + hour : hour;
|
|
minute = minute < 10 ? "0" + minute : minute;
|
|
|
|
let time = hour + ":" + minute;
|
|
if (second != undefined) {
|
|
second = second < 10 ? "0" + second : second;
|
|
time += ":" + second;
|
|
}
|
|
|
|
if (millisecond != undefined) {
|
|
// Convert milliseconds to fraction of second.
|
|
millisecond = millisecond
|
|
.toString()
|
|
.padStart(this.mMillisecMaxLength, "0");
|
|
time += "." + millisecond;
|
|
}
|
|
|
|
if (time == this.mInputElement.value) {
|
|
return;
|
|
}
|
|
|
|
this.log("setInputValueFromFields: " + time);
|
|
this.notifyPicker();
|
|
this.mInputElement.setUserInput(time);
|
|
}
|
|
|
|
setFieldsFromPicker(aValue) {
|
|
let hour = aValue.hour;
|
|
let minute = aValue.minute;
|
|
this.log("setFieldsFromPicker: " + hour + ":" + minute);
|
|
|
|
if (!this.isEmpty(hour)) {
|
|
this.setFieldValue(this.mHourField, hour);
|
|
if (this.mHour12) {
|
|
this.setDayPeriodValue(
|
|
hour >= this.mMaxHour ? this.mPMIndicator : this.mAMIndicator
|
|
);
|
|
}
|
|
}
|
|
|
|
if (!this.isEmpty(minute)) {
|
|
this.setFieldValue(this.mMinuteField, minute);
|
|
}
|
|
|
|
// Update input element's .value if needed.
|
|
this.setInputValueFromFields();
|
|
}
|
|
|
|
clearInputFields(aFromInputElement) {
|
|
this.log("clearInputFields");
|
|
|
|
if (this.mHourField) {
|
|
this.clearFieldValue(this.mHourField);
|
|
}
|
|
|
|
if (this.mMinuteField) {
|
|
this.clearFieldValue(this.mMinuteField);
|
|
}
|
|
|
|
if (this.hasSecondField()) {
|
|
this.clearFieldValue(this.mSecondField);
|
|
}
|
|
|
|
if (this.hasMillisecField()) {
|
|
this.clearFieldValue(this.mMillisecField);
|
|
}
|
|
|
|
if (this.hasDayPeriodField()) {
|
|
this.clearFieldValue(this.mDayPeriodField);
|
|
}
|
|
|
|
if (!aFromInputElement) {
|
|
if (this.mInputElement.value) {
|
|
this.mInputElement.setUserInput("");
|
|
} else {
|
|
this.mInputElement.updateValidityState();
|
|
}
|
|
}
|
|
}
|
|
|
|
notifyMinMaxStepAttrChanged() {
|
|
// Second and millisecond part are optional, rebuild edit fields if
|
|
// needed.
|
|
this.rebuildEditFieldsIfNeeded();
|
|
// Fill in values again.
|
|
this.setFieldsFromInputValue();
|
|
}
|
|
|
|
incrementFieldValue(aTargetField, aTimes) {
|
|
let value = this.getFieldValue(aTargetField);
|
|
|
|
// Use current time if field is empty.
|
|
if (this.isEmpty(value)) {
|
|
let now = new Date();
|
|
|
|
if (aTargetField == this.mHourField) {
|
|
value = now.getHours();
|
|
if (this.mHour12) {
|
|
value = value % this.mMaxHour || this.mMaxHour;
|
|
}
|
|
} else if (aTargetField == this.mMinuteField) {
|
|
value = now.getMinutes();
|
|
} else if (aTargetField == this.mSecondField) {
|
|
value = now.getSeconds();
|
|
} else if (aTargetField == this.mMillisecField) {
|
|
value = now.getMilliseconds();
|
|
} else {
|
|
this.log("Field not supported in incrementFieldValue.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
let min = aTargetField.getAttribute("min");
|
|
let max = aTargetField.getAttribute("max");
|
|
|
|
value += Number(aTimes);
|
|
if (value > max) {
|
|
value -= max - min + 1;
|
|
} else if (value < min) {
|
|
value += max - min + 1;
|
|
}
|
|
|
|
this.setFieldValue(aTargetField, value);
|
|
}
|
|
|
|
handleKeyboardNav(aEvent) {
|
|
if (!this.isEditable()) {
|
|
return;
|
|
}
|
|
|
|
let targetField = aEvent.originalTarget;
|
|
let key = aEvent.key;
|
|
|
|
if (this.hasDayPeriodField() && targetField == this.mDayPeriodField) {
|
|
// Home/End key does nothing on AM/PM field.
|
|
if (key == "Home" || key == "End") {
|
|
return;
|
|
}
|
|
|
|
this.setDayPeriodValue(
|
|
this.getDayPeriodValue() == this.mAMIndicator
|
|
? this.mPMIndicator
|
|
: this.mAMIndicator
|
|
);
|
|
this.setInputValueFromFields();
|
|
return;
|
|
}
|
|
|
|
switch (key) {
|
|
case "ArrowUp":
|
|
this.incrementFieldValue(targetField, 1);
|
|
break;
|
|
case "ArrowDown":
|
|
this.incrementFieldValue(targetField, -1);
|
|
break;
|
|
case "PageUp": {
|
|
let interval = targetField.getAttribute("pginterval");
|
|
this.incrementFieldValue(targetField, interval);
|
|
break;
|
|
}
|
|
case "PageDown": {
|
|
let interval = targetField.getAttribute("pginterval");
|
|
this.incrementFieldValue(targetField, 0 - interval);
|
|
break;
|
|
}
|
|
case "Home":
|
|
let min = targetField.getAttribute("min");
|
|
this.setFieldValue(targetField, min);
|
|
break;
|
|
case "End":
|
|
let max = targetField.getAttribute("max");
|
|
this.setFieldValue(targetField, max);
|
|
break;
|
|
}
|
|
this.setInputValueFromFields();
|
|
}
|
|
|
|
handleKeypress(aEvent) {
|
|
if (!this.isEditable()) {
|
|
return;
|
|
}
|
|
|
|
let targetField = aEvent.originalTarget;
|
|
let key = aEvent.key;
|
|
|
|
if (this.hasDayPeriodField() && targetField == this.mDayPeriodField) {
|
|
if (key == "a" || key == "A") {
|
|
this.setDayPeriodValue(this.mAMIndicator);
|
|
} else if (key == "p" || key == "P") {
|
|
this.setDayPeriodValue(this.mPMIndicator);
|
|
}
|
|
if (!this.isAnyFieldEmpty()) {
|
|
this.setInputValueFromFields();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (targetField.classList.contains("numeric") && key.match(/[0-9]/)) {
|
|
let buffer = targetField.getAttribute("typeBuffer") || "";
|
|
|
|
buffer = buffer.concat(key);
|
|
this.setFieldValue(targetField, buffer);
|
|
|
|
let n = Number(buffer);
|
|
let max = targetField.getAttribute("max");
|
|
let maxLength = targetField.getAttribute("maxlength");
|
|
if (buffer.length >= maxLength || n * 10 > max) {
|
|
buffer = "";
|
|
this.advanceToNextField();
|
|
}
|
|
targetField.setAttribute("typeBuffer", buffer);
|
|
if (!this.isAnyFieldEmpty()) {
|
|
this.setInputValueFromFields();
|
|
}
|
|
}
|
|
}
|
|
|
|
setFieldValue(aField, aValue) {
|
|
if (!aField || !aField.classList.contains("numeric")) {
|
|
return;
|
|
}
|
|
|
|
let value = Number(aValue);
|
|
if (isNaN(value)) {
|
|
this.log("NaN on setFieldValue!");
|
|
return;
|
|
}
|
|
|
|
if (aField == this.mHourField) {
|
|
if (this.mHour12) {
|
|
// Try to change to 12hr format if user input is 0 or greater
|
|
// than 12.
|
|
let maxLength = aField.getAttribute("maxlength");
|
|
if (value == 0 && aValue.length == maxLength) {
|
|
value = this.mMaxHour;
|
|
} else {
|
|
value = value > this.mMaxHour ? value % this.mMaxHour : value;
|
|
}
|
|
} else if (value > this.mMaxHour) {
|
|
value = this.mMaxHour;
|
|
}
|
|
}
|
|
|
|
aField.setAttribute("value", value);
|
|
|
|
let minDigits = aField.getAttribute("mindigits");
|
|
let formatted = value.toLocaleString(this.mLocales, {
|
|
minimumIntegerDigits: minDigits,
|
|
useGrouping: false,
|
|
});
|
|
|
|
aField.textContent = formatted;
|
|
aField.setAttribute("aria-valuetext", formatted);
|
|
this.updateResetButtonVisibility();
|
|
}
|
|
|
|
getDayPeriodValue(aValue) {
|
|
if (!this.hasDayPeriodField()) {
|
|
return "";
|
|
}
|
|
|
|
let placeholder = this.mDayPeriodField.placeholder;
|
|
let value = this.mDayPeriodField.textContent;
|
|
|
|
return value == placeholder ? "" : value;
|
|
}
|
|
|
|
setDayPeriodValue(aValue) {
|
|
if (!this.hasDayPeriodField()) {
|
|
return;
|
|
}
|
|
|
|
this.mDayPeriodField.textContent = aValue;
|
|
this.mDayPeriodField.setAttribute("value", aValue);
|
|
this.updateResetButtonVisibility();
|
|
}
|
|
|
|
isAnyFieldAvailable(aForPicker) {
|
|
let { hour, minute, second, millisecond } = this.getCurrentValue();
|
|
let dayPeriod = this.getDayPeriodValue();
|
|
|
|
let available = !this.isEmpty(hour) || !this.isEmpty(minute);
|
|
if (available) {
|
|
return true;
|
|
}
|
|
|
|
// Picker only cares about hour:minute.
|
|
if (aForPicker) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
(this.hasDayPeriodField() && !this.isEmpty(dayPeriod)) ||
|
|
(this.hasSecondField() && !this.isEmpty(second)) ||
|
|
(this.hasMillisecField() && !this.isEmpty(millisecond))
|
|
);
|
|
}
|
|
|
|
isAnyFieldEmpty() {
|
|
let { hour, minute, second, millisecond } = this.getCurrentValue();
|
|
let dayPeriod = this.getDayPeriodValue();
|
|
|
|
return (
|
|
this.isEmpty(hour) ||
|
|
this.isEmpty(minute) ||
|
|
(this.hasDayPeriodField() && this.isEmpty(dayPeriod)) ||
|
|
(this.hasSecondField() && this.isEmpty(second)) ||
|
|
(this.hasMillisecField() && this.isEmpty(millisecond))
|
|
);
|
|
}
|
|
|
|
getCurrentValue() {
|
|
let hour = this.getFieldValue(this.mHourField);
|
|
if (!this.isEmpty(hour)) {
|
|
if (this.mHour12) {
|
|
let dayPeriod = this.getDayPeriodValue();
|
|
if (dayPeriod == this.mPMIndicator && hour < this.mMaxHour) {
|
|
hour += this.mMaxHour;
|
|
} else if (dayPeriod == this.mAMIndicator && hour == this.mMaxHour) {
|
|
hour = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
let minute = this.getFieldValue(this.mMinuteField);
|
|
let second = this.getFieldValue(this.mSecondField);
|
|
let millisecond = this.getFieldValue(this.mMillisecField);
|
|
|
|
let time = { hour, minute, second, millisecond };
|
|
|
|
this.log("getCurrentValue: " + JSON.stringify(time));
|
|
return time;
|
|
}
|
|
};
|