зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1283385 - (1/2) Implement date picker UI; r=mconley
MozReview-Commit-ID: 8uscU75qrkR --HG-- extra : rebase_source : d3907de7978c1e9241c696d5c2c73115bba455f8
This commit is contained in:
Родитель
e50551a66d
Коммит
7b525c00a2
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
|
||||
%htmlDTD;
|
||||
]>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<head>
|
||||
<title>Date Picker</title>
|
||||
<link rel="stylesheet" href="chrome://global/skin/timepicker.css"/>
|
||||
<script type="application/javascript" src="chrome://global/content/bindings/datekeeper.js"></script>
|
||||
<script type="application/javascript" src="chrome://global/content/bindings/spinner.js"></script>
|
||||
<script type="application/javascript" src="chrome://global/content/bindings/calendar.js"></script>
|
||||
<script type="application/javascript" src="chrome://global/content/bindings/datepicker.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="date-picker">
|
||||
<div class="calendar-container">
|
||||
<div class="nav">
|
||||
<button class="left"><</button>
|
||||
<button class="right">></button>
|
||||
</div>
|
||||
<div class="week-header"></div>
|
||||
<div class="days-viewport">
|
||||
<div class="days-view"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="month-year">
|
||||
<div class="month-year-label"></div>
|
||||
<div class="month-year-arrow"></div>
|
||||
</div>
|
||||
<div class="month-year-view"></div>
|
||||
</div>
|
||||
<template id="spinner-template">
|
||||
<div class="spinner-container">
|
||||
<button class="up"/>
|
||||
<div class="spinner"></div>
|
||||
<button class="down"/>
|
||||
</div>
|
||||
</template>
|
||||
<script type="application/javascript">
|
||||
// We need to hide the scroll bar but maintain its scrolling
|
||||
// capability, so using |overflow: hidden| is not an option.
|
||||
// Instead, we are inserting a user agent stylesheet that is
|
||||
// capable of selecting scrollbars, and do |display: none|.
|
||||
var domWinUtls = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
|
||||
getInterface(Components.interfaces.nsIDOMWindowUtils);
|
||||
domWinUtls.loadSheetUsingURIString('data:text/css,@namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"); scrollbar { display: none; }', domWinUtls.AGENT_SHEET);
|
||||
// Create a DatePicker instance and prepare to be
|
||||
// initialized by the "DatePickerInit" event from datetimepopup.xml
|
||||
const root = document.getElementById("date-picker");
|
||||
new DatePicker({
|
||||
monthYear: root.querySelector(".month-year"),
|
||||
monthYearView: root.querySelector(".month-year-view"),
|
||||
buttonLeft: root.querySelector(".left"),
|
||||
buttonRight: root.querySelector(".right"),
|
||||
weekHeader: root.querySelector(".week-header"),
|
||||
daysView: root.querySelector(".days-view")
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -45,6 +45,7 @@ toolkit.jar:
|
|||
content/global/customizeToolbar.js
|
||||
content/global/customizeToolbar.xul
|
||||
#endif
|
||||
content/global/datepicker.xhtml
|
||||
content/global/devicestorage.properties
|
||||
#ifndef MOZ_FENNEC
|
||||
content/global/editMenuOverlay.js
|
||||
|
@ -69,8 +70,11 @@ toolkit.jar:
|
|||
content/global/bindings/autocomplete.xml (widgets/autocomplete.xml)
|
||||
content/global/bindings/browser.xml (widgets/browser.xml)
|
||||
content/global/bindings/button.xml (widgets/button.xml)
|
||||
content/global/bindings/calendar.js (widgets/calendar.js)
|
||||
content/global/bindings/checkbox.xml (widgets/checkbox.xml)
|
||||
content/global/bindings/colorpicker.xml (widgets/colorpicker.xml)
|
||||
content/global/bindings/datekeeper.js (widgets/datekeeper.js)
|
||||
content/global/bindings/datepicker.js (widgets/datepicker.js)
|
||||
content/global/bindings/datetimepicker.xml (widgets/datetimepicker.xml)
|
||||
content/global/bindings/datetimepopup.xml (widgets/datetimepopup.xml)
|
||||
content/global/bindings/datetimebox.xml (widgets/datetimebox.xml)
|
||||
|
|
|
@ -0,0 +1,172 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Initialize the Calendar and generate nodes for week headers and days, and
|
||||
* attach event listeners.
|
||||
*
|
||||
* @param {Object} options
|
||||
* {
|
||||
* {Number} calViewSize: Number of days to appear on a calendar view
|
||||
* }
|
||||
* @param {Object} context
|
||||
* {
|
||||
* {DOMElement} weekHeader
|
||||
* {DOMElement} daysView
|
||||
* }
|
||||
*/
|
||||
function Calendar(options, context) {
|
||||
const DAYS_IN_A_WEEK = 7;
|
||||
|
||||
this.context = context;
|
||||
this.state = {
|
||||
days: [],
|
||||
weekHeaders: []
|
||||
};
|
||||
this.props = {};
|
||||
this.elements = {
|
||||
weekHeaders: this._generateNodes(DAYS_IN_A_WEEK, context.weekHeader),
|
||||
daysView: this._generateNodes(options.calViewSize, context.daysView)
|
||||
};
|
||||
|
||||
this._attachEventListeners();
|
||||
}
|
||||
|
||||
{
|
||||
Calendar.prototype = {
|
||||
|
||||
/**
|
||||
* Set new properties and render them.
|
||||
*
|
||||
* @param {Object} props
|
||||
* {
|
||||
* {Boolean} isVisible: Whether or not the calendar is in view
|
||||
* {Array<Object>} days: Data for days
|
||||
* {
|
||||
* {Number} dateValue: Date in milliseconds
|
||||
* {Number} textContent
|
||||
* {Array<String>} classNames
|
||||
* }
|
||||
* {Array<Object>} weekHeaders: Data for weekHeaders
|
||||
* {
|
||||
* {Number} textContent
|
||||
* {Array<String>} classNames
|
||||
* }
|
||||
* {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
|
||||
* }
|
||||
*/
|
||||
setProps(props) {
|
||||
if (props.isVisible) {
|
||||
// Transform the days and weekHeaders array for rendering
|
||||
const days = props.days.map(({ dateValue, textContent, classNames }) => {
|
||||
return {
|
||||
dateValue,
|
||||
textContent: props.getDayString(textContent),
|
||||
className: dateValue == props.selectionValue ?
|
||||
classNames.concat("selection").join(" ") :
|
||||
classNames.join(" ")
|
||||
};
|
||||
});
|
||||
const weekHeaders = props.weekHeaders.map(({ textContent, classNames }) => {
|
||||
return {
|
||||
textContent: props.getWeekHeaderString(textContent),
|
||||
className: classNames.join(" ")
|
||||
};
|
||||
});
|
||||
// Update the DOM nodes states
|
||||
this._render({
|
||||
elements: this.elements.daysView,
|
||||
items: days,
|
||||
prevState: this.state.days
|
||||
});
|
||||
this._render({
|
||||
elements: this.elements.weekHeaders,
|
||||
items: weekHeaders,
|
||||
prevState: this.state.weekHeaders,
|
||||
});
|
||||
// Update the state to current
|
||||
this.state.days = days;
|
||||
this.state.weekHeaders = weekHeaders;
|
||||
}
|
||||
|
||||
this.props = Object.assign(this.props, props);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render the items onto the DOM nodes
|
||||
* @param {Object}
|
||||
* {
|
||||
* {Array<DOMElement>} elements
|
||||
* {Array<Object>} items
|
||||
* {Array<Object>} prevState: state of items from last render
|
||||
* }
|
||||
*/
|
||||
_render({ elements, items, prevState }) {
|
||||
for (let i = 0, l = items.length; i < l; i++) {
|
||||
let el = elements[i];
|
||||
|
||||
// Check if state from last render has changed, if so, update the elements
|
||||
if (!prevState[i] || prevState[i].textContent != items[i].textContent) {
|
||||
el.textContent = items[i].textContent;
|
||||
}
|
||||
if (!prevState[i] || prevState[i].className != items[i].className) {
|
||||
el.className = items[i].className;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate DOM nodes
|
||||
*
|
||||
* @param {Number} size: Number of nodes to generate
|
||||
* @param {DOMElement} context: Element to append the nodes to
|
||||
* @return {Array<DOMElement>}
|
||||
*/
|
||||
_generateNodes(size, context) {
|
||||
let frag = document.createDocumentFragment();
|
||||
let refs = [];
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
let el = document.createElement("div");
|
||||
el.dataset.id = i;
|
||||
refs.push(el);
|
||||
frag.appendChild(el);
|
||||
}
|
||||
context.appendChild(frag);
|
||||
|
||||
return refs;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle events
|
||||
* @param {DOMEvent} event
|
||||
*/
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
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
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach event listener to daysView
|
||||
*/
|
||||
_attachEventListeners() {
|
||||
this.context.daysView.addEventListener("click", this);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* DateKeeper keeps track of the date states.
|
||||
*
|
||||
* @param {Object} date parts
|
||||
* {
|
||||
* {Number} year
|
||||
* {Number} month
|
||||
* {Number} date
|
||||
* }
|
||||
* {Object} options
|
||||
* {
|
||||
* {Number} firstDayOfWeek [optional]
|
||||
* {Array<Number>} weekends [optional]
|
||||
* {Number} calViewSize [optional]
|
||||
* }
|
||||
*/
|
||||
function DateKeeper({ year, month, date }, { 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, date);
|
||||
}
|
||||
|
||||
{
|
||||
const DAYS_IN_A_WEEK = 7,
|
||||
MONTHS_IN_A_YEAR = 12,
|
||||
YEAR_VIEW_SIZE = 200,
|
||||
YEAR_BUFFER_SIZE = 10;
|
||||
|
||||
DateKeeper.prototype = {
|
||||
/**
|
||||
* Set new date
|
||||
* @param {Object} date parts
|
||||
* {
|
||||
* {Number} year [optional]
|
||||
* {Number} month [optional]
|
||||
* {Number} date [optional]
|
||||
* }
|
||||
*/
|
||||
set({ year = this.state.year, month = this.state.month, date = this.state.date }) {
|
||||
this._update(year, month, date);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set date with value
|
||||
* @param {Number} value: Date value
|
||||
*/
|
||||
setValue(value) {
|
||||
const dateObj = new Date(value);
|
||||
this._update(dateObj.getUTCFullYear(), dateObj.getUTCMonth(), dateObj.getUTCDate());
|
||||
},
|
||||
|
||||
/**
|
||||
* Set month. Makes sure the date is <= the last day of the month
|
||||
* @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.date, lastDayOfMonth));
|
||||
},
|
||||
|
||||
/**
|
||||
* Set year. Makes sure the date is <= the last day of the month
|
||||
* @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.date, lastDayOfMonth));
|
||||
},
|
||||
|
||||
/**
|
||||
* Set month by offset. Makes sure the date is <= the last day of the month
|
||||
* @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.date, lastDayOfMonth));
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the states.
|
||||
* @param {Number} year [description]
|
||||
* @param {Number} month [description]
|
||||
* @param {Number} date [description]
|
||||
*/
|
||||
_update(year, month, date) {
|
||||
// Use setUTCFullYear so that year 99 doesn't get parsed as 1999
|
||||
this.state.dateObj.setUTCFullYear(year, month, date);
|
||||
this.state.year = this.state.dateObj.getUTCFullYear();
|
||||
this.state.month = this.state.dateObj.getUTCMonth();
|
||||
this.state.date = this.state.dateObj.getUTCDate();
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the array of months
|
||||
* @return {Array<Object>}
|
||||
* {
|
||||
* {Number} value: Month in int
|
||||
* {Boolean} enabled
|
||||
* }
|
||||
*/
|
||||
getMonths() {
|
||||
// TODO: add min/max and step support
|
||||
let months = [];
|
||||
|
||||
for (let i = 0; i < MONTHS_IN_A_YEAR; i++) {
|
||||
months.push({
|
||||
value: i,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return months;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate the array of years
|
||||
* @return {Array<Object>}
|
||||
* {
|
||||
* {Number} value: Year in int
|
||||
* {Boolean} enabled
|
||||
* }
|
||||
*/
|
||||
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();
|
||||
|
||||
// Generate new years array when the year is outside of the first &
|
||||
// last item range. If not, return the cached result.
|
||||
if (!firstItem || !lastItem ||
|
||||
currentYear <= firstItem.value + YEAR_BUFFER_SIZE ||
|
||||
currentYear >= lastItem.value - YEAR_BUFFER_SIZE) {
|
||||
// The year is set in the middle with items on both directions
|
||||
for (let i = -(YEAR_VIEW_SIZE / 2); i < YEAR_VIEW_SIZE / 2; i++) {
|
||||
years.push({
|
||||
value: currentYear + i,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
this.state.years = years;
|
||||
}
|
||||
return this.state.years;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get days for calendar
|
||||
* @return {Array<Object>}
|
||||
* {
|
||||
* {Number} dateValue
|
||||
* {Number} textContent
|
||||
* {Array<String>} classNames
|
||||
* }
|
||||
*/
|
||||
getDays() {
|
||||
// TODO: add min/max and step support
|
||||
let firstDayOfMonth = this._getFirstCalendarDate(this.state.dateObj, this.state.firstDayOfWeek);
|
||||
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);
|
||||
let classNames = [];
|
||||
if (this.state.weekends.includes(dateObj.getUTCDay())) {
|
||||
classNames.push("weekend");
|
||||
}
|
||||
if (month != dateObj.getUTCMonth()) {
|
||||
classNames.push("outside");
|
||||
}
|
||||
days.push({
|
||||
dateValue: dateObj.getTime(),
|
||||
textContent: dateObj.getUTCDate(),
|
||||
classNames
|
||||
});
|
||||
}
|
||||
return days;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get week headers for calendar
|
||||
* @param {Number} firstDayOfWeek
|
||||
* @return {Array<Object>}
|
||||
* {
|
||||
* {Number} textContent
|
||||
* {Array<String>} classNames
|
||||
* }
|
||||
*/
|
||||
_getWeekHeaders(firstDayOfWeek) {
|
||||
let headers = [];
|
||||
let day = firstDayOfWeek;
|
||||
|
||||
for (let i = 0; i < DAYS_IN_A_WEEK; i++) {
|
||||
headers.push({
|
||||
textContent: day % DAYS_IN_A_WEEK,
|
||||
classNames: this.state.weekends.includes(day % DAYS_IN_A_WEEK) ? ["weekend"] : []
|
||||
});
|
||||
day++;
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the first day on a calendar month
|
||||
* @param {Date} dateObj
|
||||
* @param {Number} firstDayOfWeek
|
||||
* @return {Date}
|
||||
*/
|
||||
_getFirstCalendarDate(dateObj, firstDayOfWeek) {
|
||||
const daysOffset = 1 - DAYS_IN_A_WEEK;
|
||||
let firstDayOfMonth = this._newUTCDate(dateObj.getUTCFullYear(), dateObj.getUTCMonth());
|
||||
let dayOfWeek = firstDayOfMonth.getUTCDay();
|
||||
|
||||
return this._newUTCDate(
|
||||
firstDayOfMonth.getUTCFullYear(),
|
||||
firstDayOfMonth.getUTCMonth(),
|
||||
// When first calendar date is the same as first day of the week, add
|
||||
// another row on top of it.
|
||||
firstDayOfWeek == dayOfWeek ? daysOffset : (firstDayOfWeek - dayOfWeek + daysOffset) % DAYS_IN_A_WEEK);
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper function for creating UTC dates
|
||||
* @param {...[Number]} parts
|
||||
* @return {Date}
|
||||
*/
|
||||
_newUTCDate(...parts) {
|
||||
return new Date(Date.UTC(...parts));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
/* 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";
|
||||
|
||||
function DatePicker(context) {
|
||||
this.context = context;
|
||||
this._attachEventListeners();
|
||||
}
|
||||
|
||||
{
|
||||
const CAL_VIEW_SIZE = 42;
|
||||
|
||||
DatePicker.prototype = {
|
||||
/**
|
||||
* Initializes the date picker. Set the default states and properties.
|
||||
* @param {Object} props
|
||||
* {
|
||||
* {Number} year [optional]
|
||||
* {Number} month [optional]
|
||||
* {Number} date [optional]
|
||||
* {String} locale [optional]: User preferred locale
|
||||
* }
|
||||
*/
|
||||
init(props = {}) {
|
||||
this.props = props;
|
||||
this._setDefaultState();
|
||||
this._createComponents();
|
||||
this._update();
|
||||
},
|
||||
|
||||
/*
|
||||
* Set initial date picker states.
|
||||
*/
|
||||
_setDefaultState() {
|
||||
const now = new Date();
|
||||
const { year = now.getFullYear(),
|
||||
month = now.getMonth(),
|
||||
date = now.getDate(),
|
||||
locale } = this.props;
|
||||
|
||||
// TODO: Use calendar info API to get first day of week & weekends
|
||||
// (Bug 1287503)
|
||||
const dateKeeper = new DateKeeper({
|
||||
year, month, date
|
||||
}, {
|
||||
calViewSize: CAL_VIEW_SIZE,
|
||||
firstDayOfWeek: 0,
|
||||
weekends: [0]
|
||||
});
|
||||
|
||||
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;
|
||||
this._update();
|
||||
this._dispatchState();
|
||||
},
|
||||
setYear: year => {
|
||||
dateKeeper.setYear(year);
|
||||
this.state.isYearSet = true;
|
||||
this._update();
|
||||
this._dispatchState();
|
||||
},
|
||||
setMonth: month => {
|
||||
dateKeeper.setMonth(month);
|
||||
this.state.isMonthSet = true;
|
||||
this._update();
|
||||
this._dispatchState();
|
||||
},
|
||||
toggleMonthPicker: () => {
|
||||
this.state.isMonthPickerVisible = !this.state.isMonthPickerVisible;
|
||||
this._update();
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Initalize the date picker components.
|
||||
*/
|
||||
_createComponents() {
|
||||
this.components = {
|
||||
calendar: new Calendar({
|
||||
calViewSize: CAL_VIEW_SIZE,
|
||||
locale: this.state.locale
|
||||
}, {
|
||||
weekHeader: this.context.weekHeader,
|
||||
daysView: this.context.daysView
|
||||
}),
|
||||
monthYear: new MonthYear({
|
||||
setYear: this.state.setYear,
|
||||
setMonth: this.state.setMonth,
|
||||
locale: this.state.locale
|
||||
}, {
|
||||
monthYear: this.context.monthYear,
|
||||
monthYearView: this.context.monthYearView
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Update date picker and its components.
|
||||
*/
|
||||
_update() {
|
||||
const { dateKeeper, selectionValue, isMonthPickerVisible } = this.state;
|
||||
|
||||
if (isMonthPickerVisible) {
|
||||
this.state.months = dateKeeper.getMonths();
|
||||
this.state.years = dateKeeper.getYears();
|
||||
} else {
|
||||
this.state.days = dateKeeper.getDays();
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
this.components.calendar.setProps({
|
||||
isVisible: !isMonthPickerVisible,
|
||||
days: this.state.days,
|
||||
weekHeaders: dateKeeper.state.weekHeaders,
|
||||
setValue: this.state.setValue,
|
||||
getDayString: this.state.getDayString,
|
||||
getWeekHeaderString: this.state.getWeekHeaderString,
|
||||
selectionValue
|
||||
});
|
||||
|
||||
isMonthPickerVisible ?
|
||||
this.context.monthYearView.classList.remove("hidden") :
|
||||
this.context.monthYearView.classList.add("hidden");
|
||||
},
|
||||
|
||||
/**
|
||||
* Use postMessage to pass the state of picker to the panel.
|
||||
*/
|
||||
_dispatchState() {
|
||||
const { year, month, date } = this.state.dateKeeper.state;
|
||||
const { isYearSet, isMonthSet, isDateSet } = this.state;
|
||||
// The panel is listening to window for postMessage event, so we
|
||||
// do postMessage to itself to send data to input boxes.
|
||||
window.postMessage({
|
||||
name: "DatePickerPopupChanged",
|
||||
detail: {
|
||||
year,
|
||||
month,
|
||||
date,
|
||||
isYearSet,
|
||||
isMonthSet,
|
||||
isDateSet
|
||||
}
|
||||
}, "*");
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach event listeners
|
||||
*/
|
||||
_attachEventListeners() {
|
||||
window.addEventListener("message", this);
|
||||
document.addEventListener("click", this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle events.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "message": {
|
||||
this.handleMessage(event);
|
||||
break;
|
||||
}
|
||||
case "click": {
|
||||
if (event.target == this.context.buttonLeft) {
|
||||
this.state.dateKeeper.setMonthByOffset(-1);
|
||||
this._update();
|
||||
} else if (event.target == this.context.buttonRight) {
|
||||
this.state.dateKeeper.setMonthByOffset(1);
|
||||
this._update();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle postMessage events.
|
||||
*
|
||||
* @param {Event} event
|
||||
*/
|
||||
handleMessage(event) {
|
||||
switch (event.data.name) {
|
||||
case "DatePickerSetValue": {
|
||||
this.set(event.data.detail);
|
||||
break;
|
||||
}
|
||||
case "DatePickerInit": {
|
||||
this.init(event.data.detail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the date state and update the components with the new state.
|
||||
*
|
||||
* @param {Object} dateState
|
||||
* {
|
||||
* {Number} year [optional]
|
||||
* {Number} month [optional]
|
||||
* {Number} date [optional]
|
||||
* }
|
||||
*/
|
||||
set(dateState) {
|
||||
if (dateState.year != undefined) {
|
||||
this.state.isYearSet = true;
|
||||
}
|
||||
if (dateState.month != undefined) {
|
||||
this.state.isMonthSet = true;
|
||||
}
|
||||
if (dateState.date != undefined) {
|
||||
this.state.isDateSet = true;
|
||||
}
|
||||
|
||||
this.state.dateKeeper.set(dateState);
|
||||
this._update();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* MonthYear is a component that handles the month & year spinners
|
||||
*
|
||||
* @param {Object} options
|
||||
* {
|
||||
* {String} locale
|
||||
* {Function} setYear
|
||||
* {Function} setMonth
|
||||
* }
|
||||
* @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;
|
||||
|
||||
this.context = context;
|
||||
this.state = { dateFormat };
|
||||
this.props = {};
|
||||
this.components = {
|
||||
month: new Spinner({
|
||||
setValue: month => {
|
||||
this.state.isMonthSet = true;
|
||||
options.setMonth(month);
|
||||
},
|
||||
getDisplayString: month => monthFormat(new Date(0, month)),
|
||||
viewportSize: spinnerSize
|
||||
}, context.monthYearView),
|
||||
year: new Spinner({
|
||||
setValue: year => {
|
||||
this.state.isYearSet = true;
|
||||
options.setYear(year);
|
||||
},
|
||||
getDisplayString: year => yearFormat(new Date(new Date(0).setFullYear(year))),
|
||||
viewportSize: spinnerSize
|
||||
}, context.monthYearView)
|
||||
};
|
||||
|
||||
this._attachEventListeners();
|
||||
}
|
||||
|
||||
MonthYear.prototype = {
|
||||
|
||||
/**
|
||||
* Set new properties and pass them to components
|
||||
*
|
||||
* @param {Object} props
|
||||
* {
|
||||
* {Boolean} isVisible
|
||||
* {Date} dateObj
|
||||
* {Number} month
|
||||
* {Number} year
|
||||
* {Array<Object>} months
|
||||
* {Array<Object>} years
|
||||
* {Function} toggleMonthPicker
|
||||
* }
|
||||
*/
|
||||
setProps(props) {
|
||||
this.context.monthYear.textContent = this.state.dateFormat(props.dateObj);
|
||||
|
||||
if (props.isVisible) {
|
||||
this.components.month.setState({
|
||||
value: props.month,
|
||||
items: props.months,
|
||||
isInfiniteScroll: true,
|
||||
isValueSet: this.state.isMonthSet,
|
||||
smoothScroll: !this.state.firstOpened
|
||||
});
|
||||
this.components.year.setState({
|
||||
value: props.year,
|
||||
items: props.years,
|
||||
isInfiniteScroll: false,
|
||||
isValueSet: this.state.isYearSet,
|
||||
smoothScroll: !this.state.firstOpened
|
||||
});
|
||||
this.state.firstOpened = false;
|
||||
} else {
|
||||
this.state.isMonthSet = false;
|
||||
this.state.isYearSet = false;
|
||||
this.state.firstOpened = true;
|
||||
}
|
||||
|
||||
this.props = Object.assign(this.props, props);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle events
|
||||
* @param {DOMEvent} event
|
||||
*/
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "click": {
|
||||
this.props.toggleMonthPicker();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach event listener to monthYear button
|
||||
*/
|
||||
_attachEventListeners() {
|
||||
this.context.monthYear.addEventListener("click", this);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -96,7 +96,7 @@ function Spinner(props, context) {
|
|||
*/
|
||||
setState(newState) {
|
||||
const { value, items } = this.state;
|
||||
const { value: newValue, items: newItems, isValueSet, isInvalid } = newState;
|
||||
const { value: newValue, items: newItems, isValueSet, isInvalid, smoothScroll = true } = newState;
|
||||
|
||||
if (this._isArrayDiff(newItems, items)) {
|
||||
this.state = Object.assign(this.state, newState);
|
||||
|
@ -104,15 +104,17 @@ function Spinner(props, context) {
|
|||
this._scrollTo(newValue, true);
|
||||
} else if (newValue != value) {
|
||||
this.state = Object.assign(this.state, newState);
|
||||
this._smoothScrollTo(newValue);
|
||||
if (smoothScroll) {
|
||||
this._smoothScrollTo(newValue, true);
|
||||
} else {
|
||||
this._scrollTo(newValue, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (isValueSet) {
|
||||
if (isInvalid) {
|
||||
this._removeSelection();
|
||||
} else {
|
||||
this._updateSelection();
|
||||
}
|
||||
if (isValueSet && !isInvalid) {
|
||||
this._updateSelection();
|
||||
} else {
|
||||
this._removeSelection();
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
--spinner-button-height: 1.2rem;
|
||||
--colon-width: 2rem;
|
||||
--day-period-spacing-width: 1rem;
|
||||
--calendar-width: 23.1rem;
|
||||
--date-picker-item-height: 2.4rem;
|
||||
|
||||
--border: 0.1rem solid #D6D6D6;
|
||||
--border-radius: 0.3rem;
|
||||
|
@ -25,6 +27,11 @@
|
|||
--button-font-color-hover: #4D4D4D;
|
||||
--button-font-color-active: #191919;
|
||||
|
||||
--weekday-font-color: #6C6C6C;
|
||||
--weekday-outside-font-color: #6C6C6C;
|
||||
--weekend-font-color: #DA4E44;
|
||||
--weekend-outside-font-color: #FF988F;
|
||||
|
||||
--disabled-opacity: 0.2;
|
||||
}
|
||||
|
||||
|
@ -35,17 +42,145 @@ html {
|
|||
body {
|
||||
margin: 0;
|
||||
color: var(--font-color);
|
||||
font: message-box;
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
#time-picker {
|
||||
.nav {
|
||||
display: flex;
|
||||
width: var(--calendar-width);
|
||||
height: 2.4rem;
|
||||
margin-bottom: 0.8rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav button {
|
||||
-moz-appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
width: 3rem;
|
||||
height: var(--date-picker-item-height);
|
||||
}
|
||||
|
||||
.month-year {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 3rem;
|
||||
width: 17.1rem;
|
||||
height: var(--date-picker-item-height);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.month-year-view {
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
padding-top: 3.2rem;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: var(--calendar-width);
|
||||
background: window;
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.month-year-view.hidden {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.month-year-view > .spinner-container {
|
||||
width: 5.5rem;
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
.month-year-view .spinner {
|
||||
transform: scaleY(1);
|
||||
transform-origin: top;
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.month-year-view.hidden .spinner {
|
||||
transform: scaleY(0);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.month-year-view .spinner > div {
|
||||
transform: scaleY(1);
|
||||
transition: transform 0.15s;
|
||||
}
|
||||
|
||||
.month-year-view.hidden .spinner > div {
|
||||
transform: scaleY(2.5);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
cursor: default;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--calendar-width);
|
||||
}
|
||||
|
||||
.week-header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.week-header > div {
|
||||
color: var(--weekday-font-color);
|
||||
}
|
||||
|
||||
.week-header > div.weekend {
|
||||
color: var(--weekend-font-color);
|
||||
}
|
||||
|
||||
.days-viewport {
|
||||
height: 15rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.days-view {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.week-header > div,
|
||||
.days-view > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: var(--date-picker-item-height);
|
||||
margin: 0.05rem 0.15rem;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.days-view > div.outside {
|
||||
color: var(--weekday-outside-font-color);
|
||||
}
|
||||
|
||||
.days-view > div.weekend {
|
||||
color: var(--weekend-font-color);
|
||||
}
|
||||
|
||||
.days-view > div.weekend.outside {
|
||||
color: var(--weekend-outside-font-color);
|
||||
}
|
||||
|
||||
#time-picker,
|
||||
.month-year-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
font-family: sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--spinner-width);
|
||||
|
@ -101,7 +236,8 @@ body {
|
|||
scroll-snap-coordinate: 0 0;
|
||||
}
|
||||
|
||||
.spinner-container > .spinner > div:hover::before {
|
||||
.spinner-container > .spinner > div:hover::before,
|
||||
.calendar-container .days-view > div:hover::before {
|
||||
background: var(--fill-color);
|
||||
border: var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
|
@ -114,11 +250,13 @@ body {
|
|||
z-index: -10;
|
||||
}
|
||||
|
||||
.spinner-container > .spinner:not(.scrolling) > div.selection {
|
||||
.spinner-container > .spinner:not(.scrolling) > div.selection,
|
||||
.calendar-container .days-view > div.selection {
|
||||
color: var(--selected-font-color);
|
||||
}
|
||||
|
||||
.spinner-container > .spinner > div.selection::before {
|
||||
.spinner-container > .spinner > div.selection::before,
|
||||
.calendar-container .days-view > div.selection::before {
|
||||
background: var(--selected-fill-color);
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
|
|
Загрузка…
Ссылка в новой задаче