зеркало из https://github.com/mozilla/gecko-dev.git
1829 строки
56 KiB
XML
1829 строки
56 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 });
|
|
// This is to close the picker when input element blurs.
|
|
this.mInputElement.addEventListener("blur", 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 });
|
|
this.mInputElement.removeEventListener("blur", 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);
|
|
|
|
if (aEvent.target == this.mInputElement && this.mIsPickerOpen) {
|
|
this.mInputElement.closeDateTimePicker();
|
|
}
|
|
|
|
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>
|