Bug 1283385 - (1/2) Implement date picker UI; r=mconley

MozReview-Commit-ID: 8uscU75qrkR

--HG--
extra : rebase_source : d3907de7978c1e9241c696d5c2c73115bba455f8
This commit is contained in:
Scott Wu 2016-11-09 23:38:11 +08:00
parent e50551a66d
commit 7b525c00a2
7 changed files with 989 additions and 14 deletions

View File

@ -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">&lt;</button>
<button class="right">&gt;</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>

View File

@ -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)

View File

@ -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);
}
};
}

View File

@ -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));
}
};
}

View File

@ -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);
}
};
}

View File

@ -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();
}
},

View File

@ -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);