mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-25 08:37:30 +09:00
moebius#92: HTML - input - datetime
+ native in moebius: Bug 1317600: https://bugzilla.mozilla.org/show_bug.cgi?id=1317600 A note - not implemented: Bug 1282768: https://bugzilla.mozilla.org/show_bug.cgi?id=1282768 *.css: filter: url("chrome://global/skin/filters.svg#fill");, fill: Bug 1283385: https://bugzilla.mozilla.org/show_bug.cgi?id=1283385 Bug 1323109: https://bugzilla.mozilla.org/show_bug.cgi?id=1323109 Bug 1314544: https://bugzilla.mozilla.org/show_bug.cgi?id=1314544 Bug 1286182: https://bugzilla.mozilla.org/show_bug.cgi?id=1286182 Bug 1325922: https://bugzilla.mozilla.org/show_bug.cgi?id=1325922 A note - not implemented: Bug 1282768: https://bugzilla.mozilla.org/show_bug.cgi?id=1282768 *.css: filter: url("chrome://global/skin/filters.svg#fill");, fill: Bug 1320225: https://bugzilla.mozilla.org/show_bug.cgi?id=1320225 Bug 1341190: https://bugzilla.mozilla.org/show_bug.cgi?id=1341190
This commit is contained in:
parent
a807f7e6c0
commit
cac5c5adc7
34 changed files with 1267 additions and 248 deletions
|
|
@ -26,6 +26,7 @@ skip-if = !e10s
|
|||
[browser_contentTitle.js]
|
||||
[browser_crash_previous_frameloader.js]
|
||||
run-if = e10s && crashreporter
|
||||
[browser_datetime_datepicker.js]
|
||||
[browser_default_image_filename.js]
|
||||
[browser_f7_caret_browsing.js]
|
||||
[browser_findbar.js]
|
||||
|
|
|
|||
222
toolkit/content/tests/browser/browser_datetime_datepicker.js
Normal file
222
toolkit/content/tests/browser/browser_datetime_datepicker.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const MONTH_YEAR = ".month-year",
|
||||
DAYS_VIEW = ".days-view",
|
||||
BTN_PREV_MONTH = ".prev",
|
||||
BTN_NEXT_MONTH = ".next";
|
||||
const DATE_FORMAT = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "long", timeZone: "UTC" }).format;
|
||||
|
||||
// Create a list of abbreviations for calendar class names
|
||||
const W = "weekend",
|
||||
O = "outside",
|
||||
S = "selection",
|
||||
R = "out-of-range",
|
||||
T = "today",
|
||||
P = "off-step";
|
||||
|
||||
// Calendar classlist for 2016-12. Used to verify the classNames are correct.
|
||||
const calendarClasslist_201612 = [
|
||||
[W, O], [O], [O], [O], [], [], [W],
|
||||
[W], [], [], [], [], [], [W],
|
||||
[W], [], [], [], [S], [], [W],
|
||||
[W], [], [], [], [], [], [W],
|
||||
[W], [], [], [], [], [], [W],
|
||||
[W, O], [O], [O], [O], [O], [O], [W, O],
|
||||
];
|
||||
|
||||
function getCalendarText() {
|
||||
return helper.getChildren(DAYS_VIEW).map(child => child.textContent);
|
||||
}
|
||||
|
||||
function getCalendarClassList() {
|
||||
return helper.getChildren(DAYS_VIEW).map(child => Array.from(child.classList));
|
||||
}
|
||||
|
||||
function mergeArrays(a, b) {
|
||||
return a.map((classlist, index) => classlist.concat(b[index]));
|
||||
}
|
||||
|
||||
let helper = new DateTimeTestHelper();
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
helper.cleanup();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that date picker opens to today's date when input field is blank
|
||||
*/
|
||||
add_task(async function test_datepicker_today() {
|
||||
const date = new Date();
|
||||
|
||||
await helper.openPicker("data:text/html, <input type='date'>");
|
||||
|
||||
Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(date));
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
||||
/**
|
||||
* Test that date picker opens to the correct month, with calendar days
|
||||
* displayed correctly, given a date value is set.
|
||||
*/
|
||||
add_task(async function test_datepicker_open() {
|
||||
const inputValue = "2016-12-15";
|
||||
|
||||
await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
|
||||
|
||||
Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(inputValue)));
|
||||
Assert.deepEqual(
|
||||
getCalendarText(),
|
||||
[
|
||||
"27", "28", "29", "30", "1", "2", "3",
|
||||
"4", "5", "6", "7", "8", "9", "10",
|
||||
"11", "12", "13", "14", "15", "16", "17",
|
||||
"18", "19", "20", "21", "22", "23", "24",
|
||||
"25", "26", "27", "28", "29", "30", "31",
|
||||
"1", "2", "3", "4", "5", "6", "7",
|
||||
],
|
||||
"2016-12",
|
||||
);
|
||||
Assert.deepEqual(
|
||||
getCalendarClassList(),
|
||||
calendarClasslist_201612,
|
||||
"2016-12 classNames"
|
||||
);
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
||||
/**
|
||||
* When the prev month button is clicked, calendar should display the dates for
|
||||
* the previous month.
|
||||
*/
|
||||
add_task(async function test_datepicker_prev_month_btn() {
|
||||
const inputValue = "2016-12-15";
|
||||
const prevMonth = "2016-11-01";
|
||||
|
||||
await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
|
||||
helper.click(helper.getElement(BTN_PREV_MONTH));
|
||||
|
||||
Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(prevMonth)));
|
||||
Assert.deepEqual(
|
||||
getCalendarText(),
|
||||
[
|
||||
"30", "31", "1", "2", "3", "4", "5",
|
||||
"6", "7", "8", "9", "10", "11", "12",
|
||||
"13", "14", "15", "16", "17", "18", "19",
|
||||
"20", "21", "22", "23", "24", "25", "26",
|
||||
"27", "28", "29", "30", "1", "2", "3",
|
||||
"4", "5", "6", "7", "8", "9", "10",
|
||||
],
|
||||
"2016-11",
|
||||
);
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
||||
/**
|
||||
* When the next month button is clicked, calendar should display the dates for
|
||||
* the next month.
|
||||
*/
|
||||
add_task(async function test_datepicker_next_month_btn() {
|
||||
const inputValue = "2016-12-15";
|
||||
const nextMonth = "2017-01-01";
|
||||
|
||||
await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
|
||||
helper.click(helper.getElement(BTN_NEXT_MONTH));
|
||||
|
||||
Assert.equal(helper.getElement(MONTH_YEAR).textContent, DATE_FORMAT(new Date(nextMonth)));
|
||||
Assert.deepEqual(
|
||||
getCalendarText(),
|
||||
[
|
||||
"25", "26", "27", "28", "29", "30", "31",
|
||||
"1", "2", "3", "4", "5", "6", "7",
|
||||
"8", "9", "10", "11", "12", "13", "14",
|
||||
"15", "16", "17", "18", "19", "20", "21",
|
||||
"22", "23", "24", "25", "26", "27", "28",
|
||||
"29", "30", "31", "1", "2", "3", "4",
|
||||
],
|
||||
"2017-01",
|
||||
);
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
||||
/**
|
||||
* When a date on the calendar is clicked, date picker should close and set
|
||||
* value to the input box.
|
||||
*/
|
||||
add_task(async function test_datepicker_clicked() {
|
||||
const inputValue = "2016-12-15";
|
||||
const firstDayOnCalendar = "2016-11-27";
|
||||
|
||||
await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}">`);
|
||||
// Click the first item (top-left corner) of the calendar
|
||||
helper.click(helper.getElement(DAYS_VIEW).children[0]);
|
||||
await ContentTask.spawn(helper.tab.linkedBrowser, {}, async function() {
|
||||
let inputEl = content.document.querySelector("input");
|
||||
await ContentTaskUtils.waitForEvent(inputEl, "input");
|
||||
});
|
||||
|
||||
Assert.equal(content.document.querySelector("input").value, firstDayOnCalendar);
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
||||
/**
|
||||
* When min and max attributes are set, calendar should show some dates as
|
||||
* out-of-range.
|
||||
*/
|
||||
add_task(async function test_datepicker_min_max() {
|
||||
const inputValue = "2016-12-15";
|
||||
const inputMin = "2016-12-05";
|
||||
const inputMax = "2016-12-25";
|
||||
|
||||
await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" min="${inputMin}" max="${inputMax}">`);
|
||||
|
||||
Assert.deepEqual(
|
||||
getCalendarClassList(),
|
||||
mergeArrays(calendarClasslist_201612, [
|
||||
// R denotes out-of-range
|
||||
[R], [R], [R], [R], [R], [R], [R],
|
||||
[R], [], [], [], [], [], [],
|
||||
[], [], [], [], [], [], [],
|
||||
[], [], [], [], [], [], [],
|
||||
[], [R], [R], [R], [R], [R], [R],
|
||||
[R], [R], [R], [R], [R], [R], [R],
|
||||
]),
|
||||
"2016-12 with min & max",
|
||||
);
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
||||
/**
|
||||
* When step attribute is set, calendar should show some dates as off-step.
|
||||
*/
|
||||
add_task(async function test_datepicker_step() {
|
||||
const inputValue = "2016-12-15";
|
||||
const inputStep = "5";
|
||||
|
||||
await helper.openPicker(`data:text/html, <input type="date" value="${inputValue}" step="${inputStep}">`);
|
||||
|
||||
Assert.deepEqual(
|
||||
getCalendarClassList(),
|
||||
mergeArrays(calendarClasslist_201612, [
|
||||
// P denotes off-step
|
||||
[P], [P], [P], [], [P], [P], [P],
|
||||
[P], [], [P], [P], [P], [P], [],
|
||||
[P], [P], [P], [P], [], [P], [P],
|
||||
[P], [P], [], [P], [P], [P], [P],
|
||||
[], [P], [P], [P], [P], [], [P],
|
||||
[P], [P], [P], [], [P], [P], [P],
|
||||
]),
|
||||
"2016-12 with step",
|
||||
);
|
||||
|
||||
await helper.tearDown();
|
||||
});
|
||||
|
|
@ -31,3 +31,88 @@ function pushPrefs(...aPrefs) {
|
|||
SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for testing datetime input picker widget
|
||||
*/
|
||||
class DateTimeTestHelper {
|
||||
constructor() {
|
||||
this.panel = document.getElementById("DateTimePickerPanel");
|
||||
this.panel.setAttribute("animate", false);
|
||||
this.tab = null;
|
||||
this.frame = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a new tab with the URL of the test page, and make sure the picker is
|
||||
* ready for testing.
|
||||
*
|
||||
* @param {String} pageUrl
|
||||
*/
|
||||
async openPicker(pageUrl) {
|
||||
this.tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter("input", {}, gBrowser.selectedBrowser);
|
||||
// If dateTimePopupFrame doesn't exist yet, wait for the binding to be attached
|
||||
if (!this.panel.dateTimePopupFrame) {
|
||||
await BrowserTestUtils.waitForEvent(this.panel, "DateTimePickerBindingReady")
|
||||
}
|
||||
this.frame = this.panel.dateTimePopupFrame;
|
||||
await BrowserTestUtils.waitForEvent(this.frame, "load", true);
|
||||
// Wait for picker elements to be ready and open panel transition to end
|
||||
await BrowserTestUtils.waitForEvent(this.frame.contentDocument, "PickerReady");
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an element on the picker.
|
||||
*
|
||||
* @param {String} selector
|
||||
* @return {DOMElement}
|
||||
*/
|
||||
getElement(selector) {
|
||||
return this.frame.contentDocument.querySelector(selector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the children of an element on the picker.
|
||||
*
|
||||
* @param {String} selector
|
||||
* @return {Array<DOMElement>}
|
||||
*/
|
||||
getChildren(selector) {
|
||||
return Array.from(this.getElement(selector).children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Click on an element
|
||||
*
|
||||
* @param {DOMElement} element
|
||||
*/
|
||||
click(element) {
|
||||
EventUtils.synthesizeMouseAtCenter(element, {}, this.frame.contentWindow);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the panel and the tab
|
||||
*/
|
||||
async tearDown() {
|
||||
if (!this.panel.hidden) {
|
||||
let pickerClosePromise = new Promise(resolve => {
|
||||
this.panel.addEventListener("popuphidden", resolve, {once: true});
|
||||
});
|
||||
this.panel.closePicker();
|
||||
await pickerClosePromise;
|
||||
}
|
||||
await BrowserTestUtils.removeTab(this.tab);
|
||||
this.tab = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up after tests. Remove the frame to prevent leak.
|
||||
*/
|
||||
cleanup() {
|
||||
this.frame.remove();
|
||||
this.frame = null;
|
||||
this.panel.removeAttribute("animate");
|
||||
this.panel = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,23 +54,21 @@ function Calendar(options, context) {
|
|||
* {
|
||||
* {Number} textContent
|
||||
* {Array<String>} classNames
|
||||
* {Boolean} enabled
|
||||
* }
|
||||
* {Function} getDayString: Transform day number to string
|
||||
* {Function} getWeekHeaderString: Transform day of week number to string
|
||||
* {Function} setValue: Set value for dateKeeper
|
||||
* {Number} selectionValue: The selection date value
|
||||
* {Function} setSelection: Set selection for dateKeeper
|
||||
* }
|
||||
*/
|
||||
setProps(props) {
|
||||
if (props.isVisible) {
|
||||
// Transform the days and weekHeaders array for rendering
|
||||
const days = props.days.map(({ dateValue, textContent, classNames }) => {
|
||||
const days = props.days.map(({ dateObj, classNames, enabled }) => {
|
||||
return {
|
||||
dateValue,
|
||||
textContent: props.getDayString(textContent),
|
||||
className: dateValue == props.selectionValue ?
|
||||
classNames.concat("selection").join(" ") :
|
||||
classNames.join(" ")
|
||||
textContent: props.getDayString(dateObj.getUTCDate()),
|
||||
className: classNames.join(" "),
|
||||
enabled
|
||||
};
|
||||
});
|
||||
const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => {
|
||||
|
|
@ -152,10 +150,10 @@ function Calendar(options, context) {
|
|||
case "click": {
|
||||
if (event.target.parentNode == this.context.daysView) {
|
||||
let targetId = event.target.dataset.id;
|
||||
this.props.setValue({
|
||||
selectionValue: this.props.days[targetId].dateValue,
|
||||
dateValue: this.props.days[targetId].dateValue
|
||||
});
|
||||
let targetObj = this.props.days[targetId];
|
||||
if (targetObj.enabled) {
|
||||
this.props.setSelection(targetObj.dateObj);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,41 +6,72 @@
|
|||
|
||||
/**
|
||||
* DateKeeper keeps track of the date states.
|
||||
*
|
||||
* @param {Object} date parts
|
||||
* {
|
||||
* {Number} year
|
||||
* {Number} month
|
||||
* {Number} day
|
||||
* }
|
||||
* {Object} options
|
||||
* {
|
||||
* {Number} firstDayOfWeek [optional]
|
||||
* {Array<Number>} weekends [optional]
|
||||
* {Number} calViewSize [optional]
|
||||
* }
|
||||
*/
|
||||
function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
|
||||
this.state = {
|
||||
firstDayOfWeek, weekends, calViewSize,
|
||||
dateObj: new Date(0),
|
||||
years: [],
|
||||
months: [],
|
||||
days: []
|
||||
};
|
||||
this.state.weekHeaders = this._getWeekHeaders(firstDayOfWeek);
|
||||
this._update(year, month, day);
|
||||
function DateKeeper(props) {
|
||||
this.init(props);
|
||||
}
|
||||
|
||||
{
|
||||
const DAYS_IN_A_WEEK = 7,
|
||||
MONTHS_IN_A_YEAR = 12,
|
||||
YEAR_VIEW_SIZE = 200,
|
||||
YEAR_BUFFER_SIZE = 10;
|
||||
YEAR_BUFFER_SIZE = 10,
|
||||
// The min and max values are derived from the ECMAScript spec:
|
||||
// http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
|
||||
MIN_DATE = -8640000000000000,
|
||||
MAX_DATE = 8640000000000000;
|
||||
|
||||
DateKeeper.prototype = {
|
||||
get year() {
|
||||
return this.state.dateObj.getUTCFullYear();
|
||||
},
|
||||
|
||||
get month() {
|
||||
return this.state.dateObj.getUTCMonth();
|
||||
},
|
||||
|
||||
get day() {
|
||||
return this.state.dateObj.getUTCDate();
|
||||
},
|
||||
|
||||
get selection() {
|
||||
return this.state.selection;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set new date
|
||||
* Initialize DateKeeper
|
||||
* @param {Number} year
|
||||
* @param {Number} month
|
||||
* @param {Number} day
|
||||
* @param {String} min
|
||||
* @param {String} max
|
||||
* @param {Number} firstDayOfWeek
|
||||
* @param {Array<Number>} weekends
|
||||
* @param {Number} calViewSize
|
||||
*/
|
||||
init({ year, month, day, min, max, firstDayOfWeek = 0, weekends = [0], calViewSize = 42 }) {
|
||||
const today = new Date();
|
||||
const isDateSet = year != undefined && month != undefined && day != undefined;
|
||||
|
||||
this.state = {
|
||||
firstDayOfWeek, weekends, calViewSize,
|
||||
min: new Date(min != undefined ? min : MIN_DATE),
|
||||
max: new Date(max != undefined ? max : MAX_DATE),
|
||||
today: this._newUTCDate(today.getFullYear(), today.getMonth(), today.getDate()),
|
||||
weekHeaders: this._getWeekHeaders(firstDayOfWeek, weekends),
|
||||
years: [],
|
||||
months: [],
|
||||
days: [],
|
||||
selection: { year, month, day },
|
||||
};
|
||||
|
||||
this.state.dateObj = isDateSet ?
|
||||
this._newUTCDate(year, month, day) :
|
||||
new Date(this.state.today);
|
||||
},
|
||||
/**
|
||||
* Set new date. The year is always treated as full year, so the short-form
|
||||
* is not supported.
|
||||
* @param {Object} date parts
|
||||
* {
|
||||
* {Number} year [optional]
|
||||
|
|
@ -48,17 +79,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* {Number} date [optional]
|
||||
* }
|
||||
*/
|
||||
set({ year = this.state.year, month = this.state.month, day = this.state.day }) {
|
||||
this._update(year, month, day);
|
||||
set({ year = this.year, month = this.month, day = this.day }) {
|
||||
// Use setUTCFullYear so that year 99 doesn't get parsed as 1999
|
||||
this.state.dateObj.setUTCFullYear(year, month, day);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set date with value
|
||||
* @param {Number} value: Date value
|
||||
* Set selection date
|
||||
* @param {Number} year
|
||||
* @param {Number} month
|
||||
* @param {Number} day
|
||||
*/
|
||||
setValue(value) {
|
||||
const dateObj = new Date(value);
|
||||
this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
|
||||
setSelection({ year, month, day }) {
|
||||
this.state.selection.year = year;
|
||||
this.state.selection.month = month;
|
||||
this.state.selection.day = day;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -66,8 +101,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* @param {Number} month
|
||||
*/
|
||||
setMonth(month) {
|
||||
const lastDayOfMonth = this._newUTCDate(this.state.year, month + 1, 0).getUTCDate();
|
||||
this._update(this.state.year, month, Math.min(this.state.day, lastDayOfMonth));
|
||||
const lastDayOfMonth = this._newUTCDate(this.year, month + 1, 0).getUTCDate();
|
||||
this.set({ year: this.year,
|
||||
month,
|
||||
day: Math.min(this.day, lastDayOfMonth) });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -75,8 +112,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* @param {Number} year
|
||||
*/
|
||||
setYear(year) {
|
||||
const lastDayOfMonth = this._newUTCDate(year, this.state.month + 1, 0).getUTCDate();
|
||||
this._update(year, this.state.month, Math.min(this.state.day, lastDayOfMonth));
|
||||
const lastDayOfMonth = this._newUTCDate(year, this.month + 1, 0).getUTCDate();
|
||||
this.set({ year,
|
||||
month: this.month,
|
||||
day: Math.min(this.day, lastDayOfMonth) });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -84,22 +123,10 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* @param {Number} offset
|
||||
*/
|
||||
setMonthByOffset(offset) {
|
||||
const lastDayOfMonth = this._newUTCDate(this.state.year, this.state.month + offset + 1, 0).getUTCDate();
|
||||
this._update(this.state.year, this.state.month + offset, Math.min(this.state.day, lastDayOfMonth));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the states.
|
||||
* @param {Number} year [description]
|
||||
* @param {Number} month [description]
|
||||
* @param {Number} day [description]
|
||||
*/
|
||||
_update(year, month, day) {
|
||||
// Use setUTCFullYear so that year 99 doesn't get parsed as 1999
|
||||
this.state.dateObj.setUTCFullYear(year, month, day);
|
||||
this.state.year = this.state.dateObj.getUTCFullYear();
|
||||
this.state.month = this.state.dateObj.getUTCMonth();
|
||||
this.state.day = this.state.dateObj.getUTCDate();
|
||||
const lastDayOfMonth = this._newUTCDate(this.year, this.month + offset + 1, 0).getUTCDate();
|
||||
this.set({ year: this.year,
|
||||
month: this.month + offset,
|
||||
day: Math.min(this.day, lastDayOfMonth) });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -111,7 +138,6 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* }
|
||||
*/
|
||||
getMonths() {
|
||||
// TODO: add min/max and step support
|
||||
let months = [];
|
||||
|
||||
for (let i = 0; i < MONTHS_IN_A_YEAR; i++) {
|
||||
|
|
@ -133,12 +159,11 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* }
|
||||
*/
|
||||
getYears() {
|
||||
// TODO: add min/max and step support
|
||||
let years = [];
|
||||
|
||||
const firstItem = this.state.years[0];
|
||||
const lastItem = this.state.years[this.state.years.length - 1];
|
||||
const currentYear = this.state.dateObj.getUTCFullYear();
|
||||
const currentYear = this.year;
|
||||
|
||||
// Generate new years array when the year is outside of the first &
|
||||
// last item range. If not, return the cached result.
|
||||
|
|
@ -161,30 +186,43 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
* Get days for calendar
|
||||
* @return {Array<Object>}
|
||||
* {
|
||||
* {Number} dateValue
|
||||
* {Number} textContent
|
||||
* {Date} dateObj
|
||||
* {Array<String>} classNames
|
||||
* {Boolean} enabled
|
||||
* }
|
||||
*/
|
||||
getDays() {
|
||||
// TODO: add min/max and step support
|
||||
let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
|
||||
// TODO: add step support
|
||||
const firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
|
||||
const month = this.month;
|
||||
let days = [];
|
||||
let month = this.state.dateObj.getUTCMonth();
|
||||
|
||||
for (let i = 0; i < this.state.calViewSize; i++) {
|
||||
let dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i);
|
||||
const dateObj = this._newUTCDate(firstDayOfMonth.getUTCFullYear(), firstDayOfMonth.getUTCMonth(), firstDayOfMonth.getUTCDate() + i);
|
||||
let classNames = [];
|
||||
let enabled = true;
|
||||
if (this.state.weekends.includes(dateObj.getUTCDay())) {
|
||||
classNames.push("weekend");
|
||||
}
|
||||
if (month != dateObj.getUTCMonth()) {
|
||||
classNames.push("outside");
|
||||
}
|
||||
if (this.state.selection.year == dateObj.getUTCFullYear() &&
|
||||
this.state.selection.month == dateObj.getUTCMonth() &&
|
||||
this.state.selection.day == dateObj.getUTCDate()) {
|
||||
classNames.push("selection");
|
||||
}
|
||||
if (dateObj.getTime() < this.state.min.getTime() || dateObj.getTime() > this.state.max.getTime()) {
|
||||
classNames.push("out-of-range");
|
||||
enabled = false;
|
||||
}
|
||||
if (this.state.today.getTime() == dateObj.getTime()) {
|
||||
classNames.push("today");
|
||||
}
|
||||
days.push({
|
||||
dateValue: dateObj.getTime(),
|
||||
textContent: dateObj.getUTCDate(),
|
||||
classNames
|
||||
dateObj,
|
||||
classNames,
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
return days;
|
||||
|
|
@ -193,20 +231,21 @@ function DateKeeper({ year, month, day }, { firstDayOfWeek = 0, weekends = [0],
|
|||
/**
|
||||
* Get week headers for calendar
|
||||
* @param {Number} firstDayOfWeek
|
||||
* @param {Array<Number>} weekends
|
||||
* @return {Array<Object>}
|
||||
* {
|
||||
* {Number} textContent
|
||||
* {Array<String>} classNames
|
||||
* }
|
||||
*/
|
||||
_getWeekHeaders(firstDayOfWeek) {
|
||||
_getWeekHeaders(firstDayOfWeek, weekends) {
|
||||
let headers = [];
|
||||
let dayOfWeek = firstDayOfWeek;
|
||||
|
||||
for (let i = 0; i < DAYS_IN_A_WEEK; i++) {
|
||||
headers.push({
|
||||
textContent: dayOfWeek % DAYS_IN_A_WEEK,
|
||||
classNames: this.state.weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : []
|
||||
classNames: weekends.includes(dayOfWeek % DAYS_IN_A_WEEK) ? ["weekend"] : []
|
||||
});
|
||||
dayOfWeek++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,12 @@ function DatePicker(context) {
|
|||
* {Number} year [optional]
|
||||
* {Number} month [optional]
|
||||
* {Number} date [optional]
|
||||
* {String} min
|
||||
* {String} max
|
||||
* {Number} firstDayOfWeek
|
||||
* {Array<Number>} weekends
|
||||
* {Array<String>} monthStrings
|
||||
* {Array<String>} weekdayStrings
|
||||
* {String} locale [optional]: User preferred locale
|
||||
* }
|
||||
*/
|
||||
|
|
@ -28,57 +34,54 @@ function DatePicker(context) {
|
|||
this._setDefaultState();
|
||||
this._createComponents();
|
||||
this._update();
|
||||
document.dispatchEvent(new CustomEvent("PickerReady"));
|
||||
},
|
||||
|
||||
/*
|
||||
* Set initial date picker states.
|
||||
*/
|
||||
_setDefaultState() {
|
||||
const now = new Date();
|
||||
const { year = now.getFullYear(),
|
||||
month = now.getMonth(),
|
||||
day = now.getDate(),
|
||||
locale } = this.props;
|
||||
|
||||
// TODO: Use calendar info API to get first day of week & weekends
|
||||
// (Bug 1287503)
|
||||
const { year, month, day, min, max, firstDayOfWeek, weekends,
|
||||
monthStrings, weekdayStrings, locale } = this.props;
|
||||
const dateKeeper = new DateKeeper({
|
||||
year, month, day
|
||||
}, {
|
||||
calViewSize: CAL_VIEW_SIZE,
|
||||
firstDayOfWeek: 0,
|
||||
weekends: [0]
|
||||
year, month, day, min, max, firstDayOfWeek, weekends,
|
||||
calViewSize: CAL_VIEW_SIZE
|
||||
});
|
||||
|
||||
this.state = {
|
||||
dateKeeper,
|
||||
locale,
|
||||
isMonthPickerVisible: false,
|
||||
isYearSet: false,
|
||||
isMonthSet: false,
|
||||
isDateSet: false,
|
||||
getDayString: new Intl.NumberFormat(locale).format,
|
||||
// TODO: use calendar terms when available (Bug 1287677)
|
||||
getWeekHeaderString: weekday => ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][weekday],
|
||||
setValue: ({ dateValue, selectionValue }) => {
|
||||
dateKeeper.setValue(dateValue);
|
||||
this.state.selectionValue = selectionValue;
|
||||
this.state.isYearSet = true;
|
||||
this.state.isMonthSet = true;
|
||||
this.state.isDateSet = true;
|
||||
getWeekHeaderString: weekday => weekdayStrings[weekday],
|
||||
getMonthString: month => monthStrings[month],
|
||||
setSelection: date => {
|
||||
dateKeeper.setSelection({
|
||||
year: date.getUTCFullYear(),
|
||||
month: date.getUTCMonth(),
|
||||
day: date.getUTCDate(),
|
||||
});
|
||||
this._update();
|
||||
this._dispatchState();
|
||||
this._closePopup();
|
||||
},
|
||||
setYear: year => {
|
||||
dateKeeper.setYear(year);
|
||||
this.state.isYearSet = true;
|
||||
dateKeeper.setSelection({
|
||||
year,
|
||||
month: dateKeeper.selection.month,
|
||||
day: dateKeeper.selection.day,
|
||||
});
|
||||
this._update();
|
||||
this._dispatchState();
|
||||
},
|
||||
setMonth: month => {
|
||||
dateKeeper.setMonth(month);
|
||||
this.state.isMonthSet = true;
|
||||
dateKeeper.setSelection({
|
||||
year: dateKeeper.selection.year,
|
||||
month,
|
||||
day: dateKeeper.selection.day,
|
||||
});
|
||||
this._update();
|
||||
this._dispatchState();
|
||||
},
|
||||
|
|
@ -104,6 +107,7 @@ function DatePicker(context) {
|
|||
monthYear: new MonthYear({
|
||||
setYear: this.state.setYear,
|
||||
setMonth: this.state.setMonth,
|
||||
getMonthString: this.state.getMonthString,
|
||||
locale: this.state.locale
|
||||
}, {
|
||||
monthYear: this.context.monthYear,
|
||||
|
|
@ -116,7 +120,7 @@ function DatePicker(context) {
|
|||
* Update date picker and its components.
|
||||
*/
|
||||
_update() {
|
||||
const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state;
|
||||
const { dateKeeper, isMonthPickerVisible } = this.state;
|
||||
|
||||
if (isMonthPickerVisible) {
|
||||
this.state.months = dateKeeper.getMonths();
|
||||
|
|
@ -128,9 +132,7 @@ function DatePicker(context) {
|
|||
this.components.monthYear.setProps({
|
||||
isVisible: isMonthPickerVisible,
|
||||
dateObj: dateKeeper.state.dateObj,
|
||||
month: dateKeeper.state.month,
|
||||
months: this.state.months,
|
||||
year: dateKeeper.state.year,
|
||||
years: this.state.years,
|
||||
toggleMonthPicker: this.state.toggleMonthPicker
|
||||
});
|
||||
|
|
@ -138,10 +140,9 @@ function DatePicker(context) {
|
|||
isVisible: !isMonthPickerVisible,
|
||||
days: this.state.days,
|
||||
weekHeaders: dateKeeper.state.weekHeaders,
|
||||
setValue: this.state.setValue,
|
||||
setSelection: this.state.setSelection,
|
||||
getDayString: this.state.getDayString,
|
||||
getWeekHeaderString: this.state.getWeekHeaderString,
|
||||
selectionValue
|
||||
getWeekHeaderString: this.state.getWeekHeaderString
|
||||
});
|
||||
|
||||
isMonthPickerVisible ?
|
||||
|
|
@ -162,8 +163,7 @@ function DatePicker(context) {
|
|||
* Use postMessage to pass the state of picker to the panel.
|
||||
*/
|
||||
_dispatchState() {
|
||||
const { year, month, day } = this.state.dateKeeper.state;
|
||||
const { isYearSet, isMonthSet, isDaySet } = this.state;
|
||||
const { year, month, day } = this.state.dateKeeper.selection;
|
||||
// The panel is listening to window for postMessage event, so we
|
||||
// do postMessage to itself to send data to input boxes.
|
||||
window.postMessage({
|
||||
|
|
@ -172,9 +172,6 @@ function DatePicker(context) {
|
|||
year,
|
||||
month,
|
||||
day,
|
||||
isYearSet,
|
||||
isMonthSet,
|
||||
isDaySet
|
||||
}
|
||||
}, "*");
|
||||
},
|
||||
|
|
@ -255,19 +252,12 @@ function DatePicker(context) {
|
|||
set({ year, month, day }) {
|
||||
const { dateKeeper } = this.state;
|
||||
|
||||
if (year != undefined) {
|
||||
this.state.isYearSet = true;
|
||||
}
|
||||
if (month != undefined) {
|
||||
this.state.isMonthSet = true;
|
||||
}
|
||||
if (day != undefined) {
|
||||
this.state.isDaySet = true;
|
||||
}
|
||||
|
||||
dateKeeper.set({
|
||||
year, month, day
|
||||
});
|
||||
dateKeeper.setSelection({
|
||||
year, month, day
|
||||
});
|
||||
this._update();
|
||||
}
|
||||
};
|
||||
|
|
@ -280,12 +270,12 @@ function DatePicker(context) {
|
|||
* {String} locale
|
||||
* {Function} setYear
|
||||
* {Function} setMonth
|
||||
* {Function} getMonthString
|
||||
* }
|
||||
* @param {DOMElement} context
|
||||
*/
|
||||
function MonthYear(options, context) {
|
||||
const spinnerSize = 5;
|
||||
const monthFormat = new Intl.DateTimeFormat(options.locale, { month: "short" }).format;
|
||||
const yearFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric" }).format;
|
||||
const dateFormat = new Intl.DateTimeFormat(options.locale, { year: "numeric", month: "long" }).format;
|
||||
|
||||
|
|
@ -298,7 +288,7 @@ function DatePicker(context) {
|
|||
this.state.isMonthSet = true;
|
||||
options.setMonth(month);
|
||||
},
|
||||
getDisplayString: month => monthFormat(new Date(0, month)),
|
||||
getDisplayString: options.getMonthString,
|
||||
viewportSize: spinnerSize
|
||||
}, context.monthYearView),
|
||||
year: new Spinner({
|
||||
|
|
@ -323,8 +313,6 @@ function DatePicker(context) {
|
|||
* {
|
||||
* {Boolean} isVisible
|
||||
* {Date} dateObj
|
||||
* {Number} month
|
||||
* {Number} year
|
||||
* {Array<Object>} months
|
||||
* {Array<Object>} years
|
||||
* {Function} toggleMonthPicker
|
||||
|
|
@ -336,14 +324,14 @@ function DatePicker(context) {
|
|||
if (props.isVisible) {
|
||||
this.context.monthYear.classList.add("active");
|
||||
this.components.month.setState({
|
||||
value: props.month,
|
||||
value: props.dateObj.getUTCMonth(),
|
||||
items: props.months,
|
||||
isInfiniteScroll: true,
|
||||
isValueSet: this.state.isMonthSet,
|
||||
smoothScroll: !this.state.firstOpened
|
||||
});
|
||||
this.components.year.setState({
|
||||
value: props.year,
|
||||
value: props.dateObj.getUTCFullYear(),
|
||||
items: props.years,
|
||||
isInfiniteScroll: false,
|
||||
isValueSet: this.state.isYearSet,
|
||||
|
|
|
|||
|
|
@ -8,9 +8,17 @@
|
|||
.datetime-input-box-wrapper {
|
||||
-moz-appearance: none;
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
cursor: default;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
min-width: 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.datetime-input-edit-wrapper {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.datetime-input {
|
||||
|
|
@ -43,5 +51,5 @@
|
|||
height: 12px;
|
||||
width: 12px;
|
||||
align-self: center;
|
||||
justify-content: flex-end;
|
||||
flex: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
- 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"
|
||||
|
|
@ -21,12 +26,13 @@
|
|||
<implementation>
|
||||
<constructor>
|
||||
<![CDATA[
|
||||
// TODO: Bug 1320227 - [DateTimeInput] localization for
|
||||
// <input type=date> input box
|
||||
this.mMonthPlaceHolder = "mm";
|
||||
this.mDayPlaceHolder = "dd";
|
||||
this.mYearPlaceHolder = "yyyy";
|
||||
/* eslint-disable no-multi-spaces */
|
||||
this.mYearPlaceHolder = ]]>"&date.year.placeholder;"<![CDATA[;
|
||||
this.mMonthPlaceHolder = ]]>"&date.month.placeholder;"<![CDATA[;
|
||||
this.mDayPlaceHolder = ]]>"&date.day.placeholder;"<![CDATA[;
|
||||
this.mSeparatorText = "/";
|
||||
/* eslint-enable no-multi-spaces */
|
||||
|
||||
this.mMinMonth = 1;
|
||||
this.mMaxMonth = 12;
|
||||
this.mMinDay = 1;
|
||||
|
|
@ -76,6 +82,7 @@
|
|||
if (this.mInputElement.value) {
|
||||
this.setFieldsFromInputValue();
|
||||
}
|
||||
this.updateResetButtonVisibility();
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
|
|
@ -107,9 +114,11 @@
|
|||
this.mYearField.setAttribute("typeBuffer", "");
|
||||
}
|
||||
|
||||
if (!aFromInputElement) {
|
||||
if (!aFromInputElement && this.mInputElement.value) {
|
||||
this.mInputElement.setUserInput("");
|
||||
}
|
||||
|
||||
this.updateResetButtonVisibility();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
|
@ -170,6 +179,13 @@
|
|||
<method name="setInputValueFromFields">
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (!this.isAnyValueAvailable(false) && this.mInputElement.value) {
|
||||
// Values in the input box was cleared, clear the input element's
|
||||
// value if not empty.
|
||||
this.mInputElement.setUserInput("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isFieldInvalid(this.mYearField) ||
|
||||
this.isFieldInvalid(this.mMonthField) ||
|
||||
this.isFieldInvalid(this.mDayField)) {
|
||||
|
|
@ -192,6 +208,10 @@
|
|||
|
||||
let date = [year, month, day].join("-");
|
||||
|
||||
if (date == this.mInputElement.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("setInputValueFromFields: " + date);
|
||||
this.mInputElement.setUserInput(date);
|
||||
]]>
|
||||
|
|
@ -217,6 +237,9 @@
|
|||
if (!this.isEmpty(day)) {
|
||||
this.setFieldValue(this.mDayField, day);
|
||||
}
|
||||
|
||||
// Update input element's .value if needed.
|
||||
this.setInputValueFromFields();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
|
@ -302,6 +325,12 @@
|
|||
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);
|
||||
|
|
@ -406,11 +435,13 @@
|
|||
}
|
||||
|
||||
aField.value = value;
|
||||
this.updateResetButtonVisibility();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="isValueAvailable">
|
||||
<method name="isAnyValueAvailable">
|
||||
<parameter name="aForPicker"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
return !this.isEmpty(this.mMonthField.value) ||
|
||||
|
|
@ -492,6 +523,7 @@
|
|||
if (this.mInputElement.value) {
|
||||
this.setFieldsFromInputValue();
|
||||
}
|
||||
this.updateResetButtonVisibility();
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
|
|
@ -551,7 +583,7 @@
|
|||
}
|
||||
|
||||
this.log("setFieldsFromInputValue: " + value);
|
||||
let [hour, minute, second] = value.split(':');
|
||||
let [hour, minute, second] = value.split(":");
|
||||
|
||||
this.setFieldValue(this.mHourField, hour);
|
||||
this.setFieldValue(this.mMinuteField, minute);
|
||||
|
|
@ -617,6 +649,13 @@
|
|||
<method name="setInputValueFromFields">
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (!this.isAnyValueAvailable(false) && this.mInputElement.value) {
|
||||
// Values in the input box was cleared, clear the input element's
|
||||
// value if not empty.
|
||||
this.mInputElement.setUserInput("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isEmpty(this.mHourField.value) ||
|
||||
this.isEmpty(this.mMinuteField.value) ||
|
||||
(this.mDayPeriodField && this.isEmpty(this.mDayPeriodField.value)) ||
|
||||
|
|
@ -652,6 +691,10 @@
|
|||
time += "." + this.mMillisecField.value;
|
||||
}
|
||||
|
||||
if (time == this.mInputElement.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("setInputValueFromFields: " + time);
|
||||
this.mInputElement.setUserInput(time);
|
||||
]]>
|
||||
|
|
@ -678,6 +721,9 @@
|
|||
if (!this.isEmpty(minute)) {
|
||||
this.setFieldValue(this.mMinuteField, minute);
|
||||
}
|
||||
|
||||
// Update input element's .value if needed.
|
||||
this.setInputValueFromFields();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
|
@ -721,9 +767,11 @@
|
|||
this.mDayPeriodField.value = "";
|
||||
}
|
||||
|
||||
if (!aFromInputElement) {
|
||||
if (!aFromInputElement && this.mInputElement.value) {
|
||||
this.mInputElement.setUserInput("");
|
||||
}
|
||||
|
||||
this.updateResetButtonVisibility();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
|
@ -793,6 +841,7 @@
|
|||
this.mDayPeriodField.value == this.mAMIndicator ?
|
||||
this.mPMIndicator : this.mAMIndicator;
|
||||
this.mDayPeriodField.select();
|
||||
this.updateResetButtonVisibility();
|
||||
this.setInputValueFromFields();
|
||||
return;
|
||||
}
|
||||
|
|
@ -850,6 +899,7 @@
|
|||
this.mDayPeriodField.value = this.mPMIndicator;
|
||||
this.mDayPeriodField.select();
|
||||
}
|
||||
this.updateResetButtonVisibility();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -905,16 +955,30 @@
|
|||
}
|
||||
|
||||
aField.value = value;
|
||||
this.updateResetButtonVisibility();
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="isValueAvailable">
|
||||
<method name="isAnyValueAvailable">
|
||||
<parameter name="aForPicker"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
let available = !this.isEmpty(this.mHourField.value) ||
|
||||
!this.isEmpty(this.mMinuteField.value);
|
||||
|
||||
if (available) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Picker only cares about hour:minute.
|
||||
return !this.isEmpty(this.mHourField.value) ||
|
||||
!this.isEmpty(this.mMinuteField.value);
|
||||
if (aForPicker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (this.mDayPeriodField && !this.isEmpty(this.mDayPeriodField.value)) ||
|
||||
(this.mSecondField && !this.isEmpty(this.mSecondField.value)) ||
|
||||
(this.mMillisecField && !this.isEmpty(this.mMillisecField.value));
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
|
@ -963,7 +1027,8 @@
|
|||
<content>
|
||||
<html:div class="datetime-input-box-wrapper"
|
||||
xbl:inherits="context,disabled,readonly">
|
||||
<html:span>
|
||||
<html:span class="datetime-input-edit-wrapper"
|
||||
anonid="edit-wrapper">
|
||||
<html:input anonid="input-one"
|
||||
class="textbox-input datetime-input numeric"
|
||||
size="2" maxlength="2"
|
||||
|
|
@ -980,9 +1045,8 @@
|
|||
xbl:inherits="disabled,readonly,tabindex"/>
|
||||
</html:span>
|
||||
|
||||
<html:button class="datetime-reset-button" anoid="reset-button"
|
||||
tabindex="-1" xbl:inherits="disabled"
|
||||
onclick="document.getBindingParent(this).clearInputFields(false);"/>
|
||||
<html:button class="datetime-reset-button" anonid="reset-button"
|
||||
tabindex="-1" xbl:inherits="disabled"/>
|
||||
</html:div>
|
||||
</content>
|
||||
|
||||
|
|
@ -997,6 +1061,9 @@
|
|||
this.mStep = this.mInputElement.step;
|
||||
this.mIsPickerOpen = false;
|
||||
|
||||
this.mResetButton =
|
||||
document.getAnonymousElementByAttribute(this, "anonid", "reset-button");
|
||||
|
||||
this.EVENTS.forEach((eventName) => {
|
||||
this.addEventListener(eventName, this, { mozSystemGroup: true });
|
||||
});
|
||||
|
|
@ -1005,6 +1072,9 @@
|
|||
capture: true,
|
||||
mozSystemGroup: true
|
||||
});
|
||||
// This is to close the picker when input element blurs.
|
||||
this.mInputElement.addEventListener("blur", this,
|
||||
{ mozSystemGroup: true });
|
||||
]]>
|
||||
</constructor>
|
||||
|
||||
|
|
@ -1025,7 +1095,7 @@
|
|||
<property name="EVENTS" readonly="true">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
return ["click", "focus", "blur", "copy", "cut", "paste"];
|
||||
return ["click", "focus", "blur", "copy", "cut", "paste", "mousedown"];
|
||||
]]>
|
||||
</getter>
|
||||
</property>
|
||||
|
|
@ -1041,6 +1111,18 @@
|
|||
</body>
|
||||
</method>
|
||||
|
||||
<method name="updateResetButtonVisibility">
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (this.isAnyValueAvailable(false)) {
|
||||
this.mResetButton.style.visibility = "visible";
|
||||
} else {
|
||||
this.mResetButton.style.visibility = "hidden";
|
||||
}
|
||||
]]>
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="focusInnerTextBox">
|
||||
<body>
|
||||
<![CDATA[
|
||||
|
|
@ -1164,10 +1246,16 @@
|
|||
</body>
|
||||
</method>
|
||||
|
||||
<method name="isAnyValueAvailable">
|
||||
<body>
|
||||
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
|
||||
</body>
|
||||
</method>
|
||||
|
||||
<method name="notifyPicker">
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (this.mIsPickerOpen && this.isValueAvailable()) {
|
||||
if (this.mIsPickerOpen && this.isAnyValueAvailable(true)) {
|
||||
this.mInputElement.updateDateTimePicker(this.getCurrentValue());
|
||||
}
|
||||
]]>
|
||||
|
|
@ -1213,6 +1301,12 @@
|
|||
this.onBlur(aEvent);
|
||||
break;
|
||||
}
|
||||
case "mousedown": {
|
||||
if (aEvent.originalTarget == this.mResetButton) {
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "copy":
|
||||
case "cut":
|
||||
case "paste": {
|
||||
|
|
@ -1245,7 +1339,12 @@
|
|||
<parameter name="aEvent"/>
|
||||
<body>
|
||||
<![CDATA[
|
||||
this.log("onBlur originalTarget: " + aEvent.originalTarget);
|
||||
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", "");
|
||||
|
|
@ -1261,16 +1360,23 @@
|
|||
this.log("onKeyPress key: " + aEvent.key);
|
||||
|
||||
switch (aEvent.key) {
|
||||
// Close picker on Enter or Space key.
|
||||
// Close picker on Enter, Escape or Space key.
|
||||
case "Enter":
|
||||
case "Escape":
|
||||
case " ": {
|
||||
this.mInputElement.closeDateTimePicker();
|
||||
aEvent.preventDefault();
|
||||
if (this.mIsPickerOpen) {
|
||||
this.mInputElement.closeDateTimePicker();
|
||||
aEvent.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Backspace": {
|
||||
let targetField = aEvent.originalTarget;
|
||||
targetField.value = "";
|
||||
targetField.setAttribute("typeBuffer", "");
|
||||
this.updateResetButtonVisibility();
|
||||
this.setInputValueFromFields();
|
||||
aEvent.preventDefault();
|
||||
break;
|
||||
}
|
||||
case "ArrowRight":
|
||||
|
|
@ -1312,12 +1418,14 @@
|
|||
// XXX: .originalTarget is not expected.
|
||||
// When clicking on one of the inner text boxes, the .originalTarget is
|
||||
// a HTMLDivElement and when clicking on the reset button, it's a
|
||||
// HTMLButtonElement but it's not equal to our reset-button.
|
||||
// HTMLButtonElement.
|
||||
if (aEvent.defaultPrevented || this.isDisabled() || this.isReadonly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(aEvent.originalTarget instanceof HTMLButtonElement)) {
|
||||
if (aEvent.originalTarget == this.mResetButton) {
|
||||
this.clearInputFields(false);
|
||||
} else if (!this.mIsPickerOpen) {
|
||||
this.mInputElement.openDateTimePicker(this.getCurrentValue());
|
||||
}
|
||||
]]>
|
||||
|
|
|
|||
|
|
@ -999,13 +999,13 @@
|
|||
<body>
|
||||
<![CDATA[
|
||||
var locale = Intl.DateTimeFormat().resolvedOptions().locale + "-u-ca-gregory";
|
||||
var dtfMonth = Intl.DateTimeFormat(locale, {month: "long"});
|
||||
var dtfMonth = Intl.DateTimeFormat(locale, {month: "long", timeZone: "UTC"});
|
||||
var dtfWeekday = Intl.DateTimeFormat(locale, {weekday: "narrow"});
|
||||
|
||||
var monthLabel = this.monthField.firstChild;
|
||||
var tempDate = new Date(2005, 0, 1);
|
||||
var tempDate = new Date(Date.UTC(2005, 0, 1));
|
||||
for (var month = 0; month < 12; month++) {
|
||||
tempDate.setMonth(month);
|
||||
tempDate.setUTCMonth(month);
|
||||
monthLabel.setAttribute("value", dtfMonth.format(tempDate));
|
||||
monthLabel = monthLabel.nextSibling;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,11 +22,20 @@
|
|||
<field name="TIME_PICKER_HEIGHT" readonly="true">"21em"</field>
|
||||
<field name="DATE_PICKER_WIDTH" readonly="true">"23.1em"</field>
|
||||
<field name="DATE_PICKER_HEIGHT" readonly="true">"20.7em"</field>
|
||||
<method name="loadPicker">
|
||||
<constructor><![CDATA[
|
||||
this.l10n = {};
|
||||
const mozIntl = Components.classes["@mozilla.org/mozintl;1"]
|
||||
.getService(Components.interfaces.mozIMozIntl);
|
||||
mozIntl.addGetCalendarInfo(l10n);
|
||||
mozIntl.addGetDisplayNames(l10n);
|
||||
// Notify DateTimePickerHelper.jsm that binding is ready.
|
||||
this.dispatchEvent(new CustomEvent("DateTimePickerBindingReady"));
|
||||
]]></constructor>
|
||||
<method name="openPicker">
|
||||
<parameter name="type"/>
|
||||
<parameter name="anchor"/>
|
||||
<parameter name="detail"/>
|
||||
<body><![CDATA[
|
||||
this.hidden = false;
|
||||
this.type = type;
|
||||
this.pickerState = {};
|
||||
// TODO: Resize picker according to content zoom level
|
||||
|
|
@ -49,17 +58,20 @@
|
|||
break;
|
||||
}
|
||||
}
|
||||
this.hidden = false;
|
||||
this.openPopup(anchor, "after_start", 0, 0);
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="closePicker">
|
||||
<body><![CDATA[
|
||||
this.hidden = true;
|
||||
this.setInputBoxValue(true);
|
||||
this.pickerState = {};
|
||||
this.type = undefined;
|
||||
this.dateTimePopupFrame.removeEventListener("load", this, true);
|
||||
this.dateTimePopupFrame.contentDocument.removeEventListener("message", this, false);
|
||||
this.dateTimePopupFrame.setAttribute("src", "");
|
||||
this.hidePopup();
|
||||
this.hidden = true;
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="setPopupValue">
|
||||
|
|
@ -115,6 +127,34 @@
|
|||
}
|
||||
case "date": {
|
||||
const { year, month, day } = detail.value;
|
||||
const { firstDayOfWeek, weekends } =
|
||||
this.getCalendarInfo(locale);
|
||||
const monthStrings = this.getDisplayNames(
|
||||
locale, [
|
||||
"dates/gregorian/months/january",
|
||||
"dates/gregorian/months/february",
|
||||
"dates/gregorian/months/march",
|
||||
"dates/gregorian/months/april",
|
||||
"dates/gregorian/months/may",
|
||||
"dates/gregorian/months/june",
|
||||
"dates/gregorian/months/july",
|
||||
"dates/gregorian/months/august",
|
||||
"dates/gregorian/months/september",
|
||||
"dates/gregorian/months/october",
|
||||
"dates/gregorian/months/november",
|
||||
"dates/gregorian/months/december",
|
||||
], "short");
|
||||
const weekdayStrings = this.getDisplayNames(
|
||||
locale, [
|
||||
"dates/gregorian/weekdays/sunday",
|
||||
"dates/gregorian/weekdays/monday",
|
||||
"dates/gregorian/weekdays/tuesday",
|
||||
"dates/gregorian/weekdays/wednesday",
|
||||
"dates/gregorian/weekdays/thursday",
|
||||
"dates/gregorian/weekdays/friday",
|
||||
"dates/gregorian/weekdays/saturday",
|
||||
], "short");
|
||||
|
||||
this.postMessageToPicker({
|
||||
name: "PickerInit",
|
||||
detail: {
|
||||
|
|
@ -122,7 +162,13 @@
|
|||
// Month value from input box starts from 1 instead of 0
|
||||
month: month == undefined ? undefined : month - 1,
|
||||
day,
|
||||
locale
|
||||
firstDayOfWeek,
|
||||
weekends,
|
||||
monthStrings,
|
||||
weekdayStrings,
|
||||
locale,
|
||||
min: detail.min,
|
||||
max: detail.max,
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
|
@ -184,6 +230,46 @@
|
|||
}
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="getCalendarInfo">
|
||||
<parameter name="locale"/>
|
||||
<body><![CDATA[
|
||||
const calendarInfo = this.l10n.getCalendarInfo(locale);
|
||||
|
||||
// Day of week from calendarInfo starts from 1 as Sunday to 7 as Saturday,
|
||||
// so they need to be mapped to JavaScript convention with 0 as Sunday
|
||||
// and 6 as Saturday
|
||||
let firstDayOfWeek = calendarInfo.firstDayOfWeek - 1,
|
||||
weekendStart = calendarInfo.weekendStart - 1,
|
||||
weekendEnd = calendarInfo.weekendEnd - 1;
|
||||
|
||||
let weekends = [];
|
||||
|
||||
// Make sure weekendEnd is greater than weekendStart
|
||||
if (weekendEnd < weekendStart) {
|
||||
weekendEnd += 7;
|
||||
}
|
||||
|
||||
// We get the weekends by incrementing weekendStart up to weekendEnd.
|
||||
// If the start and end is the same day, then weekends only has one day.
|
||||
for (let day = weekendStart; day <= weekendEnd; day++) {
|
||||
weekends.push(day % 7);
|
||||
}
|
||||
|
||||
return {
|
||||
firstDayOfWeek,
|
||||
weekends
|
||||
}
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="getDisplayNames">
|
||||
<parameter name="locale"/>
|
||||
<parameter name="keys"/>
|
||||
<parameter name="style"/>
|
||||
<body><![CDATA[
|
||||
const displayNames = this.l10n.getDisplayNames(locale, {keys, style});
|
||||
return keys.map(key => displayNames.values[key]);
|
||||
]]></body>
|
||||
</method>
|
||||
<method name="handleEvent">
|
||||
<parameter name="aEvent"/>
|
||||
<body><![CDATA[
|
||||
|
|
@ -230,12 +316,5 @@
|
|||
</method>
|
||||
|
||||
</implementation>
|
||||
<handlers>
|
||||
<handler event="popuphiding">
|
||||
<![CDATA[
|
||||
this.closePicker();
|
||||
]]>
|
||||
</handler>
|
||||
</handlers>
|
||||
</binding>
|
||||
</bindings>
|
||||
|
|
|
|||
|
|
@ -299,11 +299,11 @@ function Spinner(props, context) {
|
|||
// An "active" class is needed to simulate :active pseudo-class
|
||||
// because element is not focused.
|
||||
event.target.classList.add("active");
|
||||
this._smoothScrollToIndex(index + 1);
|
||||
this._smoothScrollToIndex(index - 1);
|
||||
}
|
||||
if (event.target == down) {
|
||||
event.target.classList.add("active");
|
||||
this._smoothScrollToIndex(index - 1);
|
||||
this._smoothScrollToIndex(index + 1);
|
||||
}
|
||||
if (event.target.parentNode == spinner) {
|
||||
// Listen to dragging events
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue