mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-13 15:34:01 +00:00
8a5430a780
Currently, we use 'noautohide' to avoid closing the picker when the anchored input box is clicked. However, 'noautohide' does not work well on some Linux distributions and noautohide panels behave differently to regular panels when mousing over another window. So, 'nolluponanchor' is what we want here. MozReview-Commit-ID: CfkufnbUw4v
1820 lines
55 KiB
XML
1820 lines
55 KiB
XML
<?xml version="1.0"?>
|
|
|
|
<!-- 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/. -->
|
|
|
|
<!DOCTYPE bindings [
|
|
<!ENTITY % datetimeboxDTD SYSTEM "chrome://global/locale/datetimebox.dtd">
|
|
%datetimeboxDTD;
|
|
]>
|
|
|
|
<bindings id="datetimeboxBindings"
|
|
xmlns="http://www.mozilla.org/xbl"
|
|
xmlns:html="http://www.w3.org/1999/xhtml"
|
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
|
xmlns:xbl="http://www.mozilla.org/xbl">
|
|
|
|
<binding id="date-input"
|
|
extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
|
|
<resources>
|
|
<stylesheet src="chrome://global/content/textbox.css"/>
|
|
<stylesheet src="chrome://global/skin/textbox.css"/>
|
|
<stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
|
|
</resources>
|
|
|
|
<implementation>
|
|
<constructor>
|
|
<![CDATA[
|
|
/* eslint-disable no-multi-spaces */
|
|
this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[;
|
|
this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[;
|
|
this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[;
|
|
/* eslint-enable no-multi-spaces */
|
|
|
|
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();
|
|
|
|
if (this.mInputElement.value) {
|
|
this.setFieldsFromInputValue();
|
|
}
|
|
]]>
|
|
</constructor>
|
|
|
|
<method name="buildEditFields">
|
|
<body>
|
|
<![CDATA[
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
let root =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
|
|
let yearMaxLength = this.mMaxYear.toString().length
|
|
this.mYearField = this.createEditField(this.mYearPlaceHolder,
|
|
true, this.mYearLength, yearMaxLength, this.mMinYear, this.mMaxYear,
|
|
this.mYearPageUpDownInterval);
|
|
this.mMonthField = this.createEditField(this.mMonthPlaceHolder,
|
|
true, this.mMonthDayLength, this.mMonthDayLength, this.mMinMonth,
|
|
this.mMaxMonth, this.mMonthPageUpDownInterval);
|
|
this.mDayField = this.createEditField(this.mDayPlaceHolder,
|
|
true, this.mMonthDayLength, this.mMonthDayLength, this.mMinDay,
|
|
this.mMaxDay, this.mDayPageUpDownInterval);
|
|
|
|
let fragment = document.createDocumentFragment();
|
|
let formatter = Intl.DateTimeFormat(this.mLocales, {
|
|
year: "numeric",
|
|
month: "numeric",
|
|
day: "numeric"
|
|
});
|
|
formatter.formatToParts(Date.now()).map(part => {
|
|
switch (part.type) {
|
|
case "year":
|
|
fragment.appendChild(this.mYearField);
|
|
break;
|
|
case "month":
|
|
fragment.appendChild(this.mMonthField);
|
|
break;
|
|
case "day":
|
|
fragment.appendChild(this.mDayField);
|
|
break;
|
|
default:
|
|
let span = document.createElementNS(HTML_NS, "span");
|
|
span.textContent = part.value;
|
|
fragment.appendChild(span);
|
|
break;
|
|
}
|
|
});
|
|
|
|
root.appendChild(fragment);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="clearInputFields">
|
|
<parameter name="aFromInputElement"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("clearInputFields");
|
|
|
|
if (this.isDisabled() || this.isReadonly()) {
|
|
return;
|
|
}
|
|
|
|
if (this.mMonthField && !this.mMonthField.disabled &&
|
|
!this.mMonthField.readOnly) {
|
|
this.clearFieldValue(this.mMonthField);
|
|
}
|
|
|
|
if (this.mDayField && !this.mDayField.disabled &&
|
|
!this.mDayField.readOnly) {
|
|
this.clearFieldValue(this.mDayField);
|
|
}
|
|
|
|
if (this.mYearField && !this.mYearField.disabled &&
|
|
!this.mYearField.readOnly) {
|
|
this.clearFieldValue(this.mYearField);
|
|
}
|
|
|
|
if (!aFromInputElement) {
|
|
if (this.mInputElement.value) {
|
|
this.mInputElement.setUserInput("");
|
|
} else {
|
|
this.mInputElement.updateValidityState();
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldsFromInputValue">
|
|
<body>
|
|
<![CDATA[
|
|
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();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setInputValueFromFields">
|
|
<body>
|
|
<![CDATA[
|
|
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);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldsFromPicker">
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
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();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleKeypress">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.isDisabled() || this.isReadonly()) {
|
|
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);
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="incrementFieldValue">
|
|
<parameter name="aTargetField"/>
|
|
<parameter name="aTimes"/>
|
|
<body>
|
|
<![CDATA[
|
|
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);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleKeyboardNav">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.isDisabled() || this.isReadonly()) {
|
|
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();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getCurrentValue">
|
|
<body>
|
|
<![CDATA[
|
|
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;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldValue">
|
|
<parameter name="aField"/>
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
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("rawValue", 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;
|
|
this.updateResetButtonVisibility();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isAnyFieldAvailable">
|
|
<parameter name="aForPicker"/>
|
|
<body>
|
|
<![CDATA[
|
|
let { year, month, day } = this.getCurrentValue();
|
|
|
|
return !this.isEmpty(year) || !this.isEmpty(month) ||
|
|
!this.isEmpty(day);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isAnyFieldEmpty">
|
|
<body>
|
|
<![CDATA[
|
|
let { year, month, day } = this.getCurrentValue();
|
|
|
|
return (this.isEmpty(year) || this.isEmpty(month) ||
|
|
this.isEmpty(day));
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
</implementation>
|
|
</binding>
|
|
|
|
<binding id="time-input"
|
|
extends="chrome://global/content/bindings/datetimebox.xml#datetime-input-base">
|
|
<resources>
|
|
<stylesheet src="chrome://global/content/textbox.css"/>
|
|
<stylesheet src="chrome://global/skin/textbox.css"/>
|
|
<stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
|
|
</resources>
|
|
|
|
<implementation>
|
|
<property name="kMsPerSecond" readonly="true" onget="return 1000;" />
|
|
<property name="kMsPerMinute" readonly="true" onget="return (60 * 1000);" />
|
|
|
|
<constructor>
|
|
<![CDATA[
|
|
const kDefaultAMString = "AM";
|
|
const kDefaultPMString = "PM";
|
|
|
|
let { amString, pmString } =
|
|
this.getStringsForLocale(this.mLocales);
|
|
|
|
this.mAMIndicator = amString || kDefaultAMString;
|
|
this.mPMIndicator = pmString || kDefaultPMString;
|
|
|
|
/* eslint-disable no-multi-spaces */
|
|
this.mHourPlaceHolder = ]]>"&time.hour.placeholder;"<![CDATA[;
|
|
this.mMinutePlaceHolder = ]]>"&time.minute.placeholder;"<![CDATA[;
|
|
this.mSecondPlaceHolder = ]]>"&time.second.placeholder;"<![CDATA[;
|
|
this.mMillisecPlaceHolder = ]]>"&time.millisecond.placeholder;"<![CDATA[;
|
|
this.mDayPeriodPlaceHolder = ]]>"&time.dayperiod.placeholder;"<![CDATA[;
|
|
/* eslint-enable no-multi-spaces */
|
|
|
|
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();
|
|
|
|
if (this.mInputElement.value) {
|
|
this.setFieldsFromInputValue();
|
|
}
|
|
]]>
|
|
</constructor>
|
|
|
|
<method name="getInputElementValues">
|
|
<body>
|
|
<![CDATA[
|
|
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 };
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="hasSecondField">
|
|
<body>
|
|
<![CDATA[
|
|
return !!this.mSecondField;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="hasMillisecField">
|
|
<body>
|
|
<![CDATA[
|
|
return !!this.mMillisecField;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="hasDayPeriodField">
|
|
<body>
|
|
<![CDATA[
|
|
return !!this.mDayPeriodField;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="shouldShowSecondField">
|
|
<body>
|
|
<![CDATA[
|
|
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;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="shouldShowMillisecField">
|
|
<body>
|
|
<![CDATA[
|
|
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;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="rebuildEditFieldsIfNeeded">
|
|
<body>
|
|
<![CDATA[
|
|
if ((this.shouldShowSecondField() == this.hasSecondField()) &&
|
|
(this.shouldShowMillisecField() == this.hasMillisecField())) {
|
|
return;
|
|
}
|
|
|
|
let root =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
while (root.firstChild) {
|
|
root.firstChild.remove();
|
|
}
|
|
|
|
this.mHourField = null;
|
|
this.mMinuteField = null;
|
|
this.mSecondField = null;
|
|
this.mMillisecField = null;
|
|
|
|
this.buildEditFields();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="buildEditFields">
|
|
<body>
|
|
<![CDATA[
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
let root =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
|
|
let options = {
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
hour12: this.mHour12
|
|
};
|
|
|
|
this.mHourField = this.createEditField(this.mHourPlaceHolder,
|
|
true, this.mMaxLength, this.mMaxLength, this.mMinHour,
|
|
this.mMaxHour, this.mHourPageUpDownInterval);
|
|
this.mMinuteField = this.createEditField(this.mMinutePlaceHolder,
|
|
true, this.mMaxLength, this.mMaxLength, this.mMinMinute,
|
|
this.mMaxMinute, this.mMinSecPageUpDownInterval);
|
|
|
|
if (this.mHour12) {
|
|
this.mDayPeriodField = this.createEditField(
|
|
this.mDayPeriodPlaceHolder, false);
|
|
}
|
|
|
|
if (this.shouldShowSecondField()) {
|
|
options.second = "numeric";
|
|
this.mSecondField = this.createEditField(this.mSecondPlaceHolder,
|
|
true, this.mMaxLength, this.mMaxLength, this.mMinSecond,
|
|
this.mMaxSecond, this.mMinSecPageUpDownInterval);
|
|
|
|
if (this.shouldShowMillisecField()) {
|
|
this.mMillisecField = this.createEditField(
|
|
this.mMillisecPlaceHolder, true, this.mMillisecMaxLength,
|
|
this.mMillisecMaxLength, this.mMinMillisecond,
|
|
this.mMaxMillisecond, this.mMinSecPageUpDownInterval);
|
|
}
|
|
}
|
|
|
|
let fragment = document.createDocumentFragment();
|
|
let formatter = Intl.DateTimeFormat(this.mLocales, options);
|
|
formatter.formatToParts(Date.now()).map(part => {
|
|
switch (part.type) {
|
|
case "hour":
|
|
fragment.appendChild(this.mHourField);
|
|
break;
|
|
case "minute":
|
|
fragment.appendChild(this.mMinuteField);
|
|
break;
|
|
case "second":
|
|
fragment.appendChild(this.mSecondField);
|
|
if (this.shouldShowMillisecField()) {
|
|
// Intl.DateTimeFormat does not support millisecond, so we
|
|
// need to handle this on our own.
|
|
let span = document.createElementNS(HTML_NS, "span");
|
|
span.textContent = this.mMillisecSeparatorText;
|
|
fragment.appendChild(span);
|
|
fragment.appendChild(this.mMillisecField);
|
|
}
|
|
break;
|
|
case "dayPeriod":
|
|
fragment.appendChild(this.mDayPeriodField);
|
|
break;
|
|
default:
|
|
let span = document.createElementNS(HTML_NS, "span");
|
|
span.textContent = part.value;
|
|
fragment.appendChild(span);
|
|
break;
|
|
}
|
|
});
|
|
|
|
root.appendChild(fragment);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getStringsForLocale">
|
|
<parameter name="aLocales"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("getStringsForLocale: " + aLocales);
|
|
|
|
let intlUtils = 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 };
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="is12HourTime">
|
|
<parameter name="aLocales"/>
|
|
<body>
|
|
<![CDATA[
|
|
let options = (new Intl.DateTimeFormat(aLocales, {
|
|
hour: "numeric"
|
|
})).resolvedOptions();
|
|
|
|
return options.hour12;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldsFromInputValue">
|
|
<body>
|
|
<![CDATA[
|
|
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();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setInputValueFromFields">
|
|
<body>
|
|
<![CDATA[
|
|
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);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldsFromPicker">
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
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();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="clearInputFields">
|
|
<parameter name="aFromInputElement"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("clearInputFields");
|
|
|
|
if (this.isDisabled() || this.isReadonly()) {
|
|
return;
|
|
}
|
|
|
|
if (this.mHourField && !this.mHourField.disabled &&
|
|
!this.mHourField.readOnly) {
|
|
this.clearFieldValue(this.mHourField);
|
|
}
|
|
|
|
if (this.mMinuteField && !this.mMinuteField.disabled &&
|
|
!this.mMinuteField.readOnly) {
|
|
this.clearFieldValue(this.mMinuteField);
|
|
}
|
|
|
|
if (this.hasSecondField() && !this.mSecondField.disabled &&
|
|
!this.mSecondField.readOnly) {
|
|
this.clearFieldValue(this.mSecondField);
|
|
}
|
|
|
|
if (this.hasMillisecField() && !this.mMillisecField.disabled &&
|
|
!this.mMillisecField.readOnly) {
|
|
this.clearFieldValue(this.mMillisecField);
|
|
}
|
|
|
|
if (this.hasDayPeriodField() && !this.mDayPeriodField.disabled &&
|
|
!this.mDayPeriodField.readOnly) {
|
|
this.clearFieldValue(this.mDayPeriodField);
|
|
}
|
|
|
|
if (!aFromInputElement) {
|
|
if (this.mInputElement.value) {
|
|
this.mInputElement.setUserInput("");
|
|
} else {
|
|
this.mInputElement.updateValidityState();
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="notifyMinMaxStepAttrChanged">
|
|
<body>
|
|
<![CDATA[
|
|
// Second and millisecond part are optional, rebuild edit fields if
|
|
// needed.
|
|
this.rebuildEditFieldsIfNeeded();
|
|
// Fill in values again.
|
|
this.setFieldsFromInputValue();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="incrementFieldValue">
|
|
<parameter name="aTargetField"/>
|
|
<parameter name="aTimes"/>
|
|
<body>
|
|
<![CDATA[
|
|
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);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleKeyboardNav">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.isDisabled() || this.isReadonly()) {
|
|
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();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleKeypress">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.isDisabled() || this.isReadonly()) {
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldValue">
|
|
<parameter name="aField"/>
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
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("rawValue", value);
|
|
|
|
let minDigits = aField.getAttribute("mindigits");
|
|
let formatted = value.toLocaleString(this.mLocales, {
|
|
minimumIntegerDigits: minDigits,
|
|
useGrouping: false
|
|
});
|
|
|
|
aField.textContent = formatted;
|
|
this.updateResetButtonVisibility();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getDayPeriodValue">
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!this.hasDayPeriodField()) {
|
|
return "";
|
|
}
|
|
|
|
let placeholder = this.mDayPeriodField.placeholder;
|
|
let value = this.mDayPeriodField.textContent;
|
|
|
|
return (value == placeholder ? "" : value);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setDayPeriodValue">
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!this.hasDayPeriodField()) {
|
|
return;
|
|
}
|
|
|
|
this.mDayPeriodField.textContent = aValue;
|
|
this.updateResetButtonVisibility();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isAnyFieldAvailable">
|
|
<parameter name="aForPicker"/>
|
|
<body>
|
|
<![CDATA[
|
|
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));
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isAnyFieldEmpty">
|
|
<body>
|
|
<![CDATA[
|
|
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)));
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getCurrentValue">
|
|
<body>
|
|
<![CDATA[
|
|
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;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
</implementation>
|
|
</binding>
|
|
|
|
<binding id="datetime-input-base">
|
|
<resources>
|
|
<stylesheet src="chrome://global/content/textbox.css"/>
|
|
<stylesheet src="chrome://global/skin/textbox.css"/>
|
|
<stylesheet src="chrome://global/content/bindings/datetimebox.css"/>
|
|
</resources>
|
|
|
|
<content>
|
|
<html:div class="datetime-input-box-wrapper" anonid="input-box-wrapper"
|
|
xbl:inherits="context,disabled,readonly">
|
|
<html:span class="datetime-input-edit-wrapper"
|
|
anonid="edit-wrapper">
|
|
<!-- Each of the date/time input types will append their input child
|
|
- elements here -->
|
|
</html:span>
|
|
|
|
<html:button class="datetime-reset-button" anonid="reset-button"
|
|
tabindex="-1" xbl:inherits="disabled"/>
|
|
</html:div>
|
|
</content>
|
|
|
|
<implementation implements="nsIDateTimeInputArea">
|
|
<constructor>
|
|
<![CDATA[
|
|
this.DEBUG = false;
|
|
this.mInputElement = this.parentNode;
|
|
this.mLocales = window.getRegionalPrefsLocales();
|
|
|
|
this.mIsRTL = false;
|
|
let intlUtils = window.intlUtils;
|
|
if (intlUtils) {
|
|
this.mIsRTL =
|
|
intlUtils.getLocaleInfo(this.mLocales).direction === "rtl";
|
|
}
|
|
|
|
if (this.mIsRTL) {
|
|
let inputBoxWrapper =
|
|
document.getAnonymousElementByAttribute(this, "anonid",
|
|
"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 =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "reset-button");
|
|
this.mResetButton.style.visibility = "hidden";
|
|
|
|
this.EVENTS.forEach((eventName) => {
|
|
this.addEventListener(eventName, this, { mozSystemGroup: true });
|
|
});
|
|
// Handle keypress separately since we need to catch it on capturing.
|
|
this.addEventListener("keypress", this, {
|
|
capture: true,
|
|
mozSystemGroup: true
|
|
});
|
|
// This is to open the picker when input element is clicked (this
|
|
// includes padding area).
|
|
this.mInputElement.addEventListener("click", this,
|
|
{ mozSystemGroup: true });
|
|
]]>
|
|
</constructor>
|
|
|
|
<destructor>
|
|
<![CDATA[
|
|
this.mInputElement = null;
|
|
|
|
this.EVENTS.forEach((eventName) => {
|
|
this.removeEventListener(eventName, this, { mozSystemGroup: true });
|
|
});
|
|
this.removeEventListener("keypress", this, {
|
|
capture: true,
|
|
mozSystemGroup: true
|
|
});
|
|
this.mInputElement.removeEventListener("click", this,
|
|
{ mozSystemGroup: true });
|
|
]]>
|
|
</destructor>
|
|
|
|
<property name="EVENTS" readonly="true">
|
|
<getter>
|
|
<![CDATA[
|
|
return ["focus", "blur", "copy", "cut", "paste", "mousedown"];
|
|
]]>
|
|
</getter>
|
|
</property>
|
|
|
|
<method name="log">
|
|
<parameter name="aMsg"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (this.DEBUG) {
|
|
dump("[DateTimeBox] " + aMsg + "\n");
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="createEditField">
|
|
<parameter name="aPlaceHolder"/>
|
|
<parameter name="aIsNumeric"/>
|
|
<parameter name="aMinDigits"/>
|
|
<parameter name="aMaxLength"/>
|
|
<parameter name="aMinValue"/>
|
|
<parameter name="aMaxValue"/>
|
|
<parameter name="aPageUpDownInterval"/>
|
|
<body>
|
|
<![CDATA[
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
let field = document.createElementNS(HTML_NS, "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;
|
|
|
|
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", "");
|
|
// Used to store the non-formatted number, clered when value is
|
|
// cleared.
|
|
field.setAttribute("rawValue", "");
|
|
// 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);
|
|
|
|
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";
|
|
}
|
|
}
|
|
|
|
return field;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="updateResetButtonVisibility">
|
|
<body>
|
|
<![CDATA[
|
|
if (this.isAnyFieldAvailable(false)) {
|
|
this.mResetButton.style.visibility = "visible";
|
|
} else {
|
|
this.mResetButton.style.visibility = "hidden";
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="focusInnerTextBox">
|
|
<body>
|
|
<![CDATA[
|
|
this.log("Focus inner editable field.");
|
|
|
|
let editRoot =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
|
|
for (let child = editRoot.firstChild; child; child = child.nextSibling) {
|
|
if ((child instanceof HTMLSpanElement) &&
|
|
child.classList.contains("datetime-edit-field")) {
|
|
this.mLastFocusedField = child;
|
|
child.focus();
|
|
break;
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="blurInnerTextBox">
|
|
<body>
|
|
<![CDATA[
|
|
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 =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
for (let child = editRoot.firstChild; child; child = child.nextSibling) {
|
|
if ((child instanceof HTMLSpanElement) &&
|
|
child.classList.contains("datetime-edit-field")) {
|
|
child.blur();
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="notifyInputElementValueChanged">
|
|
<body>
|
|
<![CDATA[
|
|
this.log("inputElementValueChanged");
|
|
this.setFieldsFromInputValue();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="notifyMinMaxStepAttrChanged">
|
|
<body>
|
|
<!-- No operation by default -->
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setValueFromPicker">
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.setFieldsFromPicker(aValue);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="hasBadInput">
|
|
<body>
|
|
<![CDATA[
|
|
// Incomplete field does not imply bad input.
|
|
if (this.isAnyFieldEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
// All fields are available but input element's value is empty implies
|
|
// it has been sanitized.
|
|
if (!this.mInputElement.value) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="advanceToNextField">
|
|
<parameter name="aReverse"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("advanceToNextField");
|
|
|
|
let focusedInput = this.mLastFocusedField;
|
|
let next = aReverse ? focusedInput.previousElementSibling
|
|
: focusedInput.nextElementSibling;
|
|
if (!next && !aReverse) {
|
|
this.setInputValueFromFields();
|
|
return;
|
|
}
|
|
|
|
while (next) {
|
|
if ((next instanceof HTMLSpanElement) &&
|
|
next.classList.contains("datetime-edit-field")) {
|
|
next.focus();
|
|
break;
|
|
}
|
|
next = aReverse ? next.previousElementSibling
|
|
: next.nextElementSibling;
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setPickerState">
|
|
<parameter name="aIsOpen"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("picker is now " + (aIsOpen ? "opened" : "closed"));
|
|
this.mIsPickerOpen = aIsOpen;
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setEditAttribute">
|
|
<parameter name="aName"/>
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("setAttribute: " + aName + "=" + aValue);
|
|
|
|
if (aName != "tabindex" && aName != "disabled" &&
|
|
aName != "readonly") {
|
|
return;
|
|
}
|
|
|
|
let editRoot =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
|
|
for (let child = editRoot.firstChild; child; child = child.nextSibling) {
|
|
if ((child instanceof HTMLSpanElement) &&
|
|
child.classList.contains("datetime-edit-field")) {
|
|
|
|
switch (aName) {
|
|
case "tabindex":
|
|
child.setAttribute(aName, aValue);
|
|
break;
|
|
case "disabled": {
|
|
let value = this.mInputElement.disabled;
|
|
child.setAttribute("disabled", value);
|
|
child.disabled = value;
|
|
break;
|
|
}
|
|
case "readonly": {
|
|
let value = this.mInputElement.readOnly;
|
|
child.setAttribute("readonly", value);
|
|
child.readOnly = value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="removeEditAttribute">
|
|
<parameter name="aName"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("removeAttribute: " + aName);
|
|
|
|
if (aName != "tabindex" && aName != "disabled" &&
|
|
aName != "readonly") {
|
|
return;
|
|
}
|
|
|
|
let editRoot =
|
|
document.getAnonymousElementByAttribute(this, "anonid", "edit-wrapper");
|
|
|
|
for (let child = editRoot.firstChild; child; child = child.nextSibling) {
|
|
if ((child instanceof HTMLSpanElement) &&
|
|
child.classList.contains("datetime-edit-field")) {
|
|
child.removeAttribute(aName);
|
|
// Update property as well.
|
|
if (aName == "readonly") {
|
|
child.readOnly = false;
|
|
} else if (aName == "disabled") {
|
|
child.disabled = false;
|
|
}
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isEmpty">
|
|
<parameter name="aValue"/>
|
|
<body>
|
|
return (aValue == undefined || 0 === aValue.length);
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getFieldValue">
|
|
<parameter name="aField"/>
|
|
<body>
|
|
<![CDATA[
|
|
if (!aField || !aField.classList.contains("numeric")) {
|
|
return undefined;
|
|
}
|
|
|
|
let value = aField.getAttribute("rawValue");
|
|
// Avoid returning 0 when field is empty.
|
|
return (this.isEmpty(value) ? undefined : Number(value));
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="clearFieldValue">
|
|
<parameter name="aField"/>
|
|
<body>
|
|
<![CDATA[
|
|
aField.textContent = aField.placeholder;
|
|
if (aField.classList.contains("numeric")) {
|
|
aField.setAttribute("typeBuffer", "");
|
|
aField.setAttribute("rawValue", "");
|
|
}
|
|
this.updateResetButtonVisibility();
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldValue">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="clearInputFields">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldsFromInputValue">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setInputValueFromFields">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="setFieldsFromPicker">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleKeypress">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleKeyboardNav">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="getCurrentValue">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isAnyFieldAvailable">
|
|
<body>
|
|
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
|
</body>
|
|
</method>
|
|
|
|
<method name="notifyPicker">
|
|
<body>
|
|
<![CDATA[
|
|
if (this.mIsPickerOpen && this.isAnyFieldAvailable(true)) {
|
|
this.mInputElement.updateDateTimePicker(this.getCurrentValue());
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isDisabled">
|
|
<body>
|
|
<![CDATA[
|
|
return this.mInputElement.hasAttribute("disabled");
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="isReadonly">
|
|
<body>
|
|
<![CDATA[
|
|
return this.mInputElement.hasAttribute("readonly");
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="handleEvent">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("handleEvent: " + aEvent.type);
|
|
|
|
switch (aEvent.type) {
|
|
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": {
|
|
if (aEvent.originalTarget == this.mResetButton) {
|
|
aEvent.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
case "copy":
|
|
case "cut":
|
|
case "paste": {
|
|
aEvent.preventDefault();
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="onFocus">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("onFocus originalTarget: " + aEvent.originalTarget);
|
|
|
|
if (document.activeElement != this.mInputElement) {
|
|
return;
|
|
}
|
|
|
|
let target = aEvent.originalTarget;
|
|
if ((target instanceof HTMLSpanElement) &&
|
|
target.classList.contains("datetime-edit-field")) {
|
|
if (target.disabled) {
|
|
return;
|
|
}
|
|
this.mLastFocusedField = target;
|
|
this.mInputElement.setFocusState(true);
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="onBlur">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("onBlur originalTarget: " + aEvent.originalTarget +
|
|
" target: " + aEvent.target);
|
|
|
|
let target = aEvent.originalTarget;
|
|
target.setAttribute("typeBuffer", "");
|
|
this.setInputValueFromFields();
|
|
this.mInputElement.setFocusState(false);
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="onKeyPress">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("onKeyPress key: " + aEvent.key);
|
|
|
|
switch (aEvent.key) {
|
|
// Close picker on Enter, Escape or Space key.
|
|
case "Enter":
|
|
case "Escape":
|
|
case " ": {
|
|
if (this.mIsPickerOpen) {
|
|
this.mInputElement.closeDateTimePicker();
|
|
aEvent.preventDefault();
|
|
}
|
|
break;
|
|
}
|
|
case "Backspace": {
|
|
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;
|
|
}
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
|
|
<method name="onClick">
|
|
<parameter name="aEvent"/>
|
|
<body>
|
|
<![CDATA[
|
|
this.log("onClick originalTarget: " + aEvent.originalTarget +
|
|
" target: " + aEvent.target);
|
|
|
|
if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) {
|
|
return;
|
|
}
|
|
|
|
if (aEvent.originalTarget == this.mResetButton) {
|
|
this.clearInputFields(false);
|
|
} else if (!this.mIsPickerOpen) {
|
|
this.mInputElement.openDateTimePicker(this.getCurrentValue());
|
|
}
|
|
]]>
|
|
</body>
|
|
</method>
|
|
</implementation>
|
|
</binding>
|
|
|
|
</bindings>
|