+import DatePicker from './wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker';
import Devtools from './wcfsetup/install/files/ts/WoltLabSuite/Core/Devtools';
import DomUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/Dom/Util';
import * as ColorUtil from './wcfsetup/install/files/ts/WoltLabSuite/Core/ColorUtil';
WCF: any;
bc_wcfDomUtil: typeof DomUtil;
- __wcf_bc_colorUtil: typeof ColorUtil;
bc_wcfSimpleDropdown: typeof UiDropdownSimple;
+ __wcf_bc_colorUtil: typeof ColorUtil;
+ __wcf_bc_datePicker: typeof DatePicker;
}
interface String {
/**
* Date picker with time support.
*
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module WoltLabSuite/Core/Date/Picker
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Date/Picker
*/
-define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Ui/Alignment', 'WoltLabSuite/Core/Ui/CloseOverlay'], function (DateUtil, DomTraverse, DomUtil, EventHandler, Language, ObjectMap, DomChangeListener, UiAlignment, UiCloseOverlay) {
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+var __importDefault = (this && this.__importDefault) || function (mod) {
+ return (mod && mod.__esModule) ? mod : { "default": mod };
+};
+define(["require", "exports", "../Core", "./Util", "../Dom/Change/Listener", "../Event/Handler", "../Language", "../Ui/Alignment", "../Ui/CloseOverlay", "../Dom/Util"], function (require, exports, Core, DateUtil, Listener_1, EventHandler, Language, UiAlignment, CloseOverlay_1, Util_1) {
"use strict";
- var _didInit = false;
- var _firstDayOfWeek = 0;
- var _wasInsidePicker = false;
- var _data = new ObjectMap();
- var _input = null;
- var _maxDate = 0;
- var _minDate = 0;
- var _dateCells = [];
- var _dateGrid = null;
- var _dateHour = null;
- var _dateMinute = null;
- var _dateMonth = null;
- var _dateMonthNext = null;
- var _dateMonthPrevious = null;
- var _dateTime = null;
- var _dateYear = null;
- var _datePicker = null;
- var _callbackOpen = null;
- var _callbackFocus = null;
+ Core = __importStar(Core);
+ DateUtil = __importStar(DateUtil);
+ Listener_1 = __importDefault(Listener_1);
+ EventHandler = __importStar(EventHandler);
+ Language = __importStar(Language);
+ UiAlignment = __importStar(UiAlignment);
+ CloseOverlay_1 = __importDefault(CloseOverlay_1);
+ Util_1 = __importDefault(Util_1);
+ let _didInit = false;
+ let _firstDayOfWeek = 0;
+ let _wasInsidePicker = false;
+ const _data = new Map();
+ let _input = null;
+ let _maxDate;
+ let _minDate;
+ const _dateCells = [];
+ let _dateGrid;
+ let _dateHour;
+ let _dateMinute;
+ let _dateMonth;
+ let _dateMonthNext;
+ let _dateMonthPrevious;
+ let _dateTime;
+ let _dateYear;
+ let _datePicker = null;
+ /**
+ * Creates the date picker DOM.
+ */
+ function createPicker() {
+ if (_datePicker !== null) {
+ return;
+ }
+ _datePicker = document.createElement('div');
+ _datePicker.className = 'datePicker';
+ _datePicker.addEventListener('click', event => {
+ event.stopPropagation();
+ });
+ const header = document.createElement('header');
+ _datePicker.appendChild(header);
+ _dateMonthPrevious = document.createElement('a');
+ _dateMonthPrevious.className = 'previous jsTooltip';
+ _dateMonthPrevious.href = '#';
+ _dateMonthPrevious.setAttribute('role', 'button');
+ _dateMonthPrevious.tabIndex = 0;
+ _dateMonthPrevious.title = Language.get('wcf.date.datePicker.previousMonth');
+ _dateMonthPrevious.setAttribute('aria-label', Language.get('wcf.date.datePicker.previousMonth'));
+ _dateMonthPrevious.innerHTML = '<span class="icon icon16 fa-arrow-left"></span>';
+ _dateMonthPrevious.addEventListener('click', DatePicker.previousMonth);
+ header.appendChild(_dateMonthPrevious);
+ const monthYearContainer = document.createElement('span');
+ header.appendChild(monthYearContainer);
+ _dateMonth = document.createElement('select');
+ _dateMonth.className = 'month jsTooltip';
+ _dateMonth.title = Language.get('wcf.date.datePicker.month');
+ _dateMonth.setAttribute('aria-label', Language.get('wcf.date.datePicker.month'));
+ _dateMonth.addEventListener('change', changeMonth);
+ monthYearContainer.appendChild(_dateMonth);
+ let months = '';
+ const monthNames = Language.get('__monthsShort');
+ for (let i = 0; i < 12; i++) {
+ months += '<option value="' + i + '">' + monthNames[i] + '</option>';
+ }
+ _dateMonth.innerHTML = months;
+ _dateYear = document.createElement('select');
+ _dateYear.className = 'year jsTooltip';
+ _dateYear.title = Language.get('wcf.date.datePicker.year');
+ _dateYear.setAttribute('aria-label', Language.get('wcf.date.datePicker.year'));
+ _dateYear.addEventListener('change', changeYear);
+ monthYearContainer.appendChild(_dateYear);
+ _dateMonthNext = document.createElement('a');
+ _dateMonthNext.className = 'next jsTooltip';
+ _dateMonthNext.href = '#';
+ _dateMonthNext.setAttribute('role', 'button');
+ _dateMonthNext.tabIndex = 0;
+ _dateMonthNext.title = Language.get('wcf.date.datePicker.nextMonth');
+ _dateMonthNext.setAttribute('aria-label', Language.get('wcf.date.datePicker.nextMonth'));
+ _dateMonthNext.innerHTML = '<span class="icon icon16 fa-arrow-right"></span>';
+ _dateMonthNext.addEventListener('click', DatePicker.nextMonth);
+ header.appendChild(_dateMonthNext);
+ _dateGrid = document.createElement('ul');
+ _datePicker.appendChild(_dateGrid);
+ const item = document.createElement('li');
+ item.className = 'weekdays';
+ _dateGrid.appendChild(item);
+ const weekdays = Language.get('__daysShort');
+ for (let i = 0; i < 7; i++) {
+ let day = i + _firstDayOfWeek;
+ if (day > 6)
+ day -= 7;
+ const span = document.createElement('span');
+ span.textContent = weekdays[day];
+ item.appendChild(span);
+ }
+ // create date grid
+ for (let i = 0; i < 6; i++) {
+ const row = document.createElement('li');
+ _dateGrid.appendChild(row);
+ for (let j = 0; j < 7; j++) {
+ const cell = document.createElement('a');
+ cell.addEventListener('click', click);
+ _dateCells.push(cell);
+ row.appendChild(cell);
+ }
+ }
+ _dateTime = document.createElement('footer');
+ _datePicker.appendChild(_dateTime);
+ _dateHour = document.createElement('select');
+ _dateHour.className = 'hour';
+ _dateHour.title = Language.get('wcf.date.datePicker.hour');
+ _dateHour.setAttribute('aria-label', Language.get('wcf.date.datePicker.hour'));
+ _dateHour.addEventListener('change', formatValue);
+ const date = new Date(2000, 0, 1);
+ const timeFormat = Language.get('wcf.date.timeFormat').replace(/:/, '').replace(/[isu]/g, '');
+ let tmp = '';
+ for (let i = 0; i < 24; i++) {
+ date.setHours(i);
+ tmp += '<option value="' + i + '">' + DateUtil.format(date, timeFormat) + "</option>";
+ }
+ _dateHour.innerHTML = tmp;
+ _dateTime.appendChild(_dateHour);
+ _dateTime.appendChild(document.createTextNode('\u00A0:\u00A0'));
+ _dateMinute = document.createElement('select');
+ _dateMinute.className = 'minute';
+ _dateMinute.title = Language.get('wcf.date.datePicker.minute');
+ _dateMinute.setAttribute('aria-label', Language.get('wcf.date.datePicker.minute'));
+ _dateMinute.addEventListener('change', formatValue);
+ tmp = '';
+ for (let i = 0; i < 60; i++) {
+ tmp += '<option value="' + i + '">' + (i < 10 ? '0' + i.toString() : i) + '</option>';
+ }
+ _dateMinute.innerHTML = tmp;
+ _dateTime.appendChild(_dateMinute);
+ document.body.appendChild(_datePicker);
+ document.body.addEventListener('focus', maintainFocus, { capture: true });
+ }
+ /**
+ * Initializes the minimum/maximum date range.
+ */
+ function initDateRange(element, now, isMinDate) {
+ const name = isMinDate ? 'minDate' : 'maxDate';
+ let value = (element.dataset[name] || '').trim();
+ if (value.match(/^(\d{4})-(\d{2})-(\d{2})$/)) {
+ // YYYY-mm-dd
+ value = new Date(value).getTime().toString();
+ }
+ else if (value === 'now') {
+ value = now.getTime().toString();
+ }
+ else if (value.match(/^\d{1,3}$/)) {
+ // relative time span in years
+ const date = new Date(now.getTime());
+ date.setFullYear(date.getFullYear() + ~~value * (isMinDate ? -1 : 1));
+ value = date.getTime().toString();
+ }
+ else if (value.match(/^datePicker-(.+)$/)) {
+ // element id, e.g. `datePicker-someOtherElement`
+ value = RegExp.$1;
+ if (document.getElementById(value) === null) {
+ throw new Error("Reference date picker identified by '" + value + "' does not exists (element id: '" + element.id + "').");
+ }
+ }
+ else if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
+ value = new Date(value).getTime().toString();
+ }
+ else {
+ value = new Date((isMinDate ? 1902 : 2038), 0, 1).getTime().toString();
+ }
+ element.dataset[name] = value;
+ }
+ /**
+ * Sets up callbacks and event listeners.
+ */
+ function setup() {
+ if (_didInit)
+ return;
+ _didInit = true;
+ _firstDayOfWeek = parseInt(Language.get('wcf.date.firstDayOfTheWeek'), 10);
+ Listener_1.default.add('WoltLabSuite/Core/Date/Picker', DatePicker.init);
+ CloseOverlay_1.default.add('WoltLabSuite/Core/Date/Picker', close);
+ }
+ function getDateValue(attributeName) {
+ let date = _input.dataset[attributeName] || '';
+ if (date.match(/^datePicker-(.+)$/)) {
+ const referenceElement = document.getElementById(RegExp.$1);
+ if (referenceElement === null) {
+ throw new Error(`Unable to find an element with the id '${RegExp.$1}'.`);
+ }
+ date = referenceElement.dataset.value || '';
+ }
+ return new Date(parseInt(date, 10));
+ }
+ /**
+ * Opens the date picker.
+ */
+ function open(event) {
+ event.preventDefault();
+ event.stopPropagation();
+ createPicker();
+ const target = event.currentTarget;
+ const input = (target.nodeName === 'INPUT') ? target : target.previousElementSibling;
+ if (input === _input) {
+ close();
+ return;
+ }
+ const dialogContent = input.closest('.dialogContent');
+ if (dialogContent !== null) {
+ if (!Core.stringToBool(dialogContent.dataset.hasDatepickerScrollListener || '')) {
+ dialogContent.addEventListener('scroll', onDialogScroll);
+ dialogContent.dataset.hasDatepickerScrollListener = '1';
+ }
+ }
+ _input = input;
+ const data = _data.get(_input);
+ const value = _input.dataset.value;
+ let date;
+ if (value) {
+ date = new Date(parseInt(value, 10));
+ if (date.toString() === 'Invalid Date') {
+ date = new Date();
+ }
+ }
+ else {
+ date = new Date();
+ }
+ // set min/max date
+ _minDate = getDateValue('minDate');
+ if (_minDate.getTime() > date.getTime()) {
+ date = _minDate;
+ }
+ _maxDate = getDateValue('maxDate');
+ if (data.isDateTime) {
+ _dateHour.value = date.getHours().toString();
+ _dateMinute.value = date.getMinutes().toString();
+ _datePicker.classList.add('datePickerTime');
+ }
+ else {
+ _datePicker.classList.remove('datePickerTime');
+ }
+ _datePicker.classList[(data.isTimeOnly) ? 'add' : 'remove']('datePickerTimeOnly');
+ renderPicker(date.getDate(), date.getMonth(), date.getFullYear());
+ UiAlignment.set(_datePicker, _input);
+ _input.nextElementSibling.setAttribute('aria-expanded', 'true');
+ _wasInsidePicker = false;
+ }
+ /**
+ * Closes the date picker.
+ */
+ function close() {
+ if (_datePicker === null || !_datePicker.classList.contains('active')) {
+ return;
+ }
+ _datePicker.classList.remove('active');
+ const data = _data.get(_input);
+ if (typeof data.onClose === 'function') {
+ data.onClose();
+ }
+ EventHandler.fire('WoltLabSuite/Core/Date/Picker', 'close', { element: _input });
+ const sibling = _input.nextElementSibling;
+ sibling.setAttribute('aria-expanded', 'false');
+ _input = null;
+ }
+ /**
+ * Updates the position of the date picker in a dialog if the dialog content
+ * is scrolled.
+ */
+ function onDialogScroll(event) {
+ if (_input === null) {
+ return;
+ }
+ const dialogContent = event.currentTarget;
+ const offset = Util_1.default.offset(_input);
+ const dialogOffset = Util_1.default.offset(dialogContent);
+ // check if date picker input field is still (partially) visible
+ if (offset.top + _input.clientHeight <= dialogOffset.top) {
+ // top check
+ close();
+ }
+ else if (offset.top >= dialogOffset.top + dialogContent.offsetHeight) {
+ // bottom check
+ close();
+ }
+ else if (offset.left <= dialogOffset.left) {
+ // left check
+ close();
+ }
+ else if (offset.left >= dialogOffset.left + dialogContent.offsetWidth) {
+ // right check
+ close();
+ }
+ else {
+ UiAlignment.set(_datePicker, _input);
+ }
+ }
+ /**
+ * Renders the full picker on init.
+ */
+ function renderPicker(day, month, year) {
+ renderGrid(day, month, year);
+ // create options for month and year
+ let years = '';
+ for (let i = _minDate.getFullYear(), last = _maxDate.getFullYear(); i <= last; i++) {
+ years += '<option value="' + i + '">' + i + '</option>';
+ }
+ _dateYear.innerHTML = years;
+ _dateYear.value = year.toString();
+ _dateMonth.value = month.toString();
+ _datePicker.classList.add('active');
+ }
+ /**
+ * Updates the date grid.
+ */
+ function renderGrid(day, month, year) {
+ const hasDay = (day !== undefined);
+ const hasMonth = (month !== undefined);
+ if (typeof day !== 'number') {
+ day = parseInt(day || _dateGrid.dataset.day || '0', 10);
+ }
+ if (typeof month !== 'number') {
+ month = parseInt(month || '0', 10);
+ }
+ if (typeof year !== 'number') {
+ year = parseInt(year || '0', 10);
+ }
+ // rebuild cells
+ if (hasMonth || year) {
+ let rebuildMonths = (year !== 0);
+ // rebuild grid
+ const fragment = document.createDocumentFragment();
+ fragment.appendChild(_dateGrid);
+ if (!hasMonth) {
+ month = parseInt(_dateGrid.dataset.month, 10);
+ }
+ if (!year) {
+ year = parseInt(_dateGrid.dataset.year, 10);
+ }
+ // check if current selection exceeds min/max date
+ let date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-' + ('0' + day.toString()).slice(-2));
+ if (date < _minDate) {
+ year = _minDate.getFullYear();
+ month = _minDate.getMonth();
+ day = _minDate.getDate();
+ _dateMonth.value = month.toString();
+ _dateYear.value = year.toString();
+ rebuildMonths = true;
+ }
+ else if (date > _maxDate) {
+ year = _maxDate.getFullYear();
+ month = _maxDate.getMonth();
+ day = _maxDate.getDate();
+ _dateMonth.value = month.toString();
+ _dateYear.value = year.toString();
+ rebuildMonths = true;
+ }
+ date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+ // shift until first displayed day equals first day of week
+ while (date.getDay() !== _firstDayOfWeek) {
+ date.setDate(date.getDate() - 1);
+ }
+ // show the last row
+ Util_1.default.show(_dateCells[35].parentNode);
+ let selectable;
+ const comparableMinDate = new Date(_minDate.getFullYear(), _minDate.getMonth(), _minDate.getDate());
+ for (let i = 0; i < 42; i++) {
+ if (i === 35 && date.getMonth() !== month) {
+ // skip the last row if it only contains the next month
+ Util_1.default.hide(_dateCells[35].parentNode);
+ break;
+ }
+ const cell = _dateCells[i];
+ cell.textContent = date.getDate().toString();
+ selectable = (date.getMonth() === month);
+ if (selectable) {
+ if (date < comparableMinDate)
+ selectable = false;
+ else if (date > _maxDate)
+ selectable = false;
+ }
+ cell.classList[selectable ? 'remove' : 'add']('otherMonth');
+ if (selectable) {
+ cell.href = '#';
+ cell.setAttribute('role', 'button');
+ cell.tabIndex = 0;
+ cell.title = DateUtil.formatDate(date);
+ cell.setAttribute('aria-label', DateUtil.formatDate(date));
+ }
+ date.setDate(date.getDate() + 1);
+ }
+ _dateGrid.dataset.month = month.toString();
+ _dateGrid.dataset.year = year.toString();
+ _datePicker.insertBefore(fragment, _dateTime);
+ if (!hasDay) {
+ // check if date is valid
+ date = new Date(year, month, day);
+ if (date.getDate() !== day) {
+ while (date.getMonth() !== month) {
+ date.setDate(date.getDate() - 1);
+ }
+ day = date.getDate();
+ }
+ }
+ if (rebuildMonths) {
+ for (let i = 0; i < 12; i++) {
+ const currentMonth = _dateMonth.children[i];
+ currentMonth.disabled = (year === _minDate.getFullYear() && +currentMonth.value < _minDate.getMonth()) || (year === _maxDate.getFullYear() && +currentMonth.value > _maxDate.getMonth());
+ }
+ const nextMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+ nextMonth.setMonth(nextMonth.getMonth() + 1);
+ _dateMonthNext.classList[(nextMonth < _maxDate) ? 'add' : 'remove']('active');
+ const previousMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+ previousMonth.setDate(previousMonth.getDate() - 1);
+ _dateMonthPrevious.classList[(previousMonth > _minDate) ? 'add' : 'remove']('active');
+ }
+ }
+ // update active day
+ if (day) {
+ for (let i = 0; i < 35; i++) {
+ const cell = _dateCells[i];
+ cell.classList[(!cell.classList.contains('otherMonth') && +cell.textContent === day) ? 'add' : 'remove']('active');
+ }
+ _dateGrid.dataset.day = day.toString();
+ }
+ formatValue();
+ }
+ /**
+ * Sets the visible and shadow value
+ */
+ function formatValue() {
+ const data = _data.get(_input);
+ let date;
+ if (Core.stringToBool(_input.dataset.empty || '')) {
+ return;
+ }
+ if (data.isDateTime) {
+ date = new Date(+_dateGrid.dataset.year, +_dateGrid.dataset.month, +_dateGrid.dataset.day, +_dateHour.value, +_dateMinute.value);
+ }
+ else {
+ date = new Date(+_dateGrid.dataset.year, +_dateGrid.dataset.month, +_dateGrid.dataset.day);
+ }
+ DatePicker.setDate(_input, date);
+ }
+ /**
+ * Handles changes to the month select element.
+ */
+ function changeMonth(event) {
+ const target = event.currentTarget;
+ renderGrid(undefined, +target.value);
+ }
+ /**
+ * Handles changes to the year select element.
+ */
+ function changeYear(event) {
+ const target = event.currentTarget;
+ renderGrid(undefined, undefined, +target.value);
+ }
+ /**
+ * Handles clicks on an individual day.
+ */
+ function click(event) {
+ event.preventDefault();
+ const target = event.currentTarget;
+ if (target.classList.contains('otherMonth')) {
+ return;
+ }
+ _input.dataset.empty = 'false';
+ renderGrid(+target.textContent);
+ const data = _data.get(_input);
+ if (!data.isDateTime) {
+ close();
+ }
+ }
/**
- * @exports WoltLabSuite/Core/Date/Picker
+ * Validates given element or id if it represents an active date picker.
*/
- var DatePicker = {
+ function getElement(element) {
+ if (typeof element === 'string') {
+ element = document.getElementById(element);
+ }
+ if (!(element instanceof HTMLInputElement) || !element.classList.contains('inputDatePicker') || !_data.has(element)) {
+ throw new Error("Expected a valid date picker input element or id.");
+ }
+ return element;
+ }
+ function maintainFocus(event) {
+ if (_datePicker === null || !_datePicker.classList.contains('active')) {
+ return;
+ }
+ if (!_datePicker.contains(event.target)) {
+ if (_wasInsidePicker) {
+ const sibling = _input.nextElementSibling;
+ sibling.focus();
+ _wasInsidePicker = false;
+ }
+ else {
+ _datePicker.querySelector('.previous').focus();
+ }
+ }
+ else {
+ _wasInsidePicker = true;
+ }
+ }
+ const DatePicker = {
/**
* Initializes all date and datetime input fields.
*/
- init: function () {
- this._setup();
- var elements = elBySelAll('input[type="date"]:not(.inputDatePicker), input[type="datetime"]:not(.inputDatePicker)');
- var now = new Date();
- for (var i = 0, length = elements.length; i < length; i++) {
- var element = elements[i];
+ init() {
+ setup();
+ const now = new Date();
+ document.querySelectorAll('input[type="date"]:not(.inputDatePicker), input[type="datetime"]:not(.inputDatePicker)').forEach(element => {
element.classList.add('inputDatePicker');
element.readOnly = true;
- var isDateTime = (elAttr(element, 'type') === 'datetime');
- var isTimeOnly = (isDateTime && elDataBool(element, 'time-only'));
- var disableClear = elDataBool(element, 'disable-clear');
- var ignoreTimezone = isDateTime && elDataBool(element, 'ignore-timezone');
- var isBirthday = element.classList.contains('birthday');
- elData(element, 'is-date-time', isDateTime);
- elData(element, 'is-time-only', isTimeOnly);
+ const isDateTime = (element.type === 'datetime');
+ const isTimeOnly = isDateTime && Core.stringToBool(element.dataset.timeOnly || '');
+ const disableClear = Core.stringToBool(element.dataset.disableClear || '');
+ const ignoreTimezone = isDateTime && Core.stringToBool(element.dataset.ignoreTimezone || '');
+ const isBirthday = element.classList.contains('birthday');
+ element.dataset.isDateTime = isDateTime ? 'true' : 'false';
+ element.dataset.isTimeOnly = isTimeOnly ? 'true' : 'false';
// convert value
- var date = null, value = elAttr(element, 'value');
+ let date = null;
+ let value = element.value;
// ignore the timezone, if the value is only a date (YYYY-MM-DD)
- var isDateOnly = /^\d+-\d+-\d+$/.test(value);
- if (elAttr(element, 'value')) {
+ const isDateOnly = /^\d+-\d+-\d+$/.test(value);
+ if (value) {
if (isTimeOnly) {
date = new Date();
- var tmp = value.split(':');
- date.setHours(tmp[0], tmp[1]);
+ const tmp = value.split(':');
+ date.setHours(+tmp[0], +tmp[1]);
}
else {
if (ignoreTimezone || isBirthday || isDateOnly) {
- var timezoneOffset = new Date(value).getTimezoneOffset();
- var timezone = (timezoneOffset > 0) ? '-' : '+'; // -120 equals GMT+0200
+ let timezoneOffset = new Date(value).getTimezoneOffset();
+ let timezone = (timezoneOffset > 0) ? '-' : '+'; // -120 equals GMT+0200
timezoneOffset = Math.abs(timezoneOffset);
- var hours = (Math.floor(timezoneOffset / 60)).toString();
- var minutes = (timezoneOffset % 60).toString();
+ const hours = (Math.floor(timezoneOffset / 60)).toString();
+ const minutes = (timezoneOffset % 60).toString();
timezone += (hours.length === 2) ? hours : '0' + hours;
timezone += ':';
timezone += (minutes.length === 2) ? minutes : '0' + minutes;
}
date = new Date(value);
}
- var time = date.getTime();
+ const time = date.getTime();
// check for invalid dates
if (isNaN(time)) {
value = '';
}
else {
- elData(element, 'value', time);
- var format = (isTimeOnly) ? 'formatTime' : ('formatDate' + (isDateTime ? 'Time' : ''));
+ element.dataset.value = time.toString();
+ const format = (isTimeOnly) ? 'formatTime' : ('formatDate' + (isDateTime ? 'Time' : ''));
value = DateUtil[format](date);
}
}
- var isEmpty = (value.length === 0);
+ const isEmpty = (value.length === 0);
// handle birthday input
if (isBirthday) {
- elData(element, 'min-date', '120');
+ element.dataset.minDate = '120';
// do not use 'now' here, all though it makes sense, it causes bad UX
- elData(element, 'max-date', new Date().getFullYear() + '-12-31');
+ element.dataset.maxDate = new Date().getFullYear() + '-12-31';
}
else {
- if (element.min)
- elData(element, 'min-date', element.min);
- if (element.max)
- elData(element, 'max-date', element.max);
+ if (element.min) {
+ element.dataset.minDate = element.min;
+ }
+ if (element.max) {
+ element.dataset.maxDate = element.max;
+ }
}
- this._initDateRange(element, now, true);
- this._initDateRange(element, now, false);
- if (elData(element, 'min-date') === elData(element, 'max-date')) {
+ initDateRange(element, now, true);
+ initDateRange(element, now, false);
+ if ((element.dataset.minDate || '') === (element.dataset.maxDate || '')) {
throw new Error("Minimum and maximum date cannot be the same (element id '" + element.id + "').");
}
// change type to prevent browser's datepicker to trigger
element.type = 'text';
element.value = value;
- elData(element, 'empty', isEmpty);
- if (elData(element, 'placeholder')) {
- elAttr(element, 'placeholder', elData(element, 'placeholder'));
+ element.dataset.empty = isEmpty ? 'true' : 'false';
+ const placeholder = element.dataset.placeholder || '';
+ if (placeholder) {
+ element.placeholder = placeholder;
}
// add a hidden element to hold the actual date
- var shadowElement = elCreate('input');
+ const shadowElement = document.createElement('input');
shadowElement.id = element.id + 'DatePicker';
shadowElement.name = element.name;
shadowElement.type = 'hidden';
}
element.parentNode.insertBefore(shadowElement, element);
element.removeAttribute('name');
- element.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
+ element.addEventListener('click', open);
+ let clearButton = null;
if (!element.disabled) {
// create input addon
- var container = elCreate('div');
+ const container = document.createElement('div');
container.className = 'inputAddon';
- var button = elCreate('a');
- button.className = 'inputSuffix button jsTooltip';
- button.href = '#';
- elAttr(button, 'role', 'button');
- elAttr(button, 'tabindex', '0');
- elAttr(button, 'title', Language.get('wcf.date.datePicker'));
- elAttr(button, 'aria-label', Language.get('wcf.date.datePicker'));
- elAttr(button, 'aria-haspopup', true);
- elAttr(button, 'aria-expanded', false);
- button.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
- container.appendChild(button);
- var icon = elCreate('span');
+ clearButton = document.createElement('a');
+ clearButton.className = 'inputSuffix button jsTooltip';
+ clearButton.href = '#';
+ clearButton.setAttribute('role', 'button');
+ clearButton.tabIndex = 0;
+ clearButton.title = Language.get('wcf.date.datePicker');
+ clearButton.setAttribute('aria-label', Language.get('wcf.date.datePicker'));
+ clearButton.setAttribute('aria-haspopup', 'true');
+ clearButton.setAttribute('aria-expanded', 'false');
+ clearButton.addEventListener('click', open);
+ container.appendChild(clearButton);
+ let icon = document.createElement('span');
icon.className = 'icon icon16 fa-calendar';
- button.appendChild(icon);
+ clearButton.appendChild(icon);
element.parentNode.insertBefore(container, element);
- container.insertBefore(element, button);
+ container.insertBefore(element, clearButton);
if (!disableClear) {
- button = elCreate('a');
+ const button = document.createElement('a');
button.className = 'inputSuffix button';
- button.addEventListener(WCF_CLICK_EVENT, this.clear.bind(this, element));
+ button.addEventListener('click', this.clear.bind(this, element));
if (isEmpty)
button.style.setProperty('visibility', 'hidden', '');
container.appendChild(button);
- icon = elCreate('span');
+ icon = document.createElement('span');
icon.className = 'icon icon16 fa-times';
button.appendChild(icon);
}
}
// check if the date input has one of the following classes set otherwise default to 'short'
- var hasClass = false, knownClasses = ['tiny', 'short', 'medium', 'long'];
- for (var j = 0; j < 4; j++) {
+ const knownClasses = ['tiny', 'short', 'medium', 'long'];
+ let hasClass = false;
+ for (let j = 0; j < 4; j++) {
if (element.classList.contains(knownClasses[j])) {
hasClass = true;
}
element.classList.add('short');
}
_data.set(element, {
- clearButton: button,
+ clearButton,
shadow: shadowElement,
- disableClear: disableClear,
- isDateTime: isDateTime,
- isEmpty: isEmpty,
- isTimeOnly: isTimeOnly,
- ignoreTimezone: ignoreTimezone,
- onClose: null
+ disableClear,
+ isDateTime,
+ isEmpty,
+ isTimeOnly,
+ ignoreTimezone,
+ onClose: null,
});
- }
- },
- /**
- * Initializes the minimum/maximum date range.
- *
- * @param {Element} element input element
- * @param {Date} now current date
- * @param {boolean} isMinDate true for the minimum date
- */
- _initDateRange: function (element, now, isMinDate) {
- var attribute = 'data-' + (isMinDate ? 'min' : 'max') + '-date';
- var value = (element.hasAttribute(attribute)) ? elAttr(element, attribute).trim() : '';
- if (value.match(/^(\d{4})-(\d{2})-(\d{2})$/)) {
- // YYYY-mm-dd
- value = new Date(value).getTime();
- }
- else if (value === 'now') {
- value = now.getTime();
- }
- else if (value.match(/^\d{1,3}$/)) {
- // relative time span in years
- var date = new Date(now.getTime());
- date.setFullYear(date.getFullYear() + ~~value * (isMinDate ? -1 : 1));
- value = date.getTime();
- }
- else if (value.match(/^datePicker-(.+)$/)) {
- // element id, e.g. `datePicker-someOtherElement`
- value = RegExp.$1;
- if (elById(value) === null) {
- throw new Error("Reference date picker identified by '" + value + "' does not exists (element id: '" + element.id + "').");
- }
- }
- else if (/^\d{4}\-\d{2}\-\d{2}T/.test(value)) {
- value = new Date(value).getTime();
- }
- else {
- value = new Date((isMinDate ? 1902 : 2038), 0, 1).getTime();
- }
- elAttr(element, attribute, value);
- },
- /**
- * Sets up callbacks and event listeners.
- */
- _setup: function () {
- if (_didInit)
- return;
- _didInit = true;
- _firstDayOfWeek = ~~Language.get('wcf.date.firstDayOfTheWeek');
- _callbackOpen = this._open.bind(this);
- DomChangeListener.add('WoltLabSuite/Core/Date/Picker', this.init.bind(this));
- UiCloseOverlay.add('WoltLabSuite/Core/Date/Picker', this._close.bind(this));
- },
- /**
- * Opens the date picker.
- *
- * @param {object} event event object
- */
- _open: function (event) {
- event.preventDefault();
- event.stopPropagation();
- this._createPicker();
- if (_callbackFocus === null) {
- _callbackFocus = this._maintainFocus.bind(this);
- document.body.addEventListener('focus', _callbackFocus, { capture: true });
- }
- var input = (event.currentTarget.nodeName === 'INPUT') ? event.currentTarget : event.currentTarget.previousElementSibling;
- if (input === _input) {
- this._close();
- return;
- }
- var dialogContent = DomTraverse.parentByClass(input, 'dialogContent');
- if (dialogContent !== null) {
- if (!elDataBool(dialogContent, 'has-datepicker-scroll-listener')) {
- dialogContent.addEventListener('scroll', this._onDialogScroll.bind(this));
- elData(dialogContent, 'has-datepicker-scroll-listener', 1);
- }
- }
- _input = input;
- var data = _data.get(_input), date, value = elData(_input, 'value');
- if (value) {
- date = new Date(+value);
- if (date.toString() === 'Invalid Date') {
- date = new Date();
- }
- }
- else {
- date = new Date();
- }
- // set min/max date
- _minDate = elData(_input, 'min-date');
- if (_minDate.match(/^datePicker-(.+)$/))
- _minDate = elData(elById(RegExp.$1), 'value');
- _minDate = new Date(+_minDate);
- if (_minDate.getTime() > date.getTime())
- date = _minDate;
- _maxDate = elData(_input, 'max-date');
- if (_maxDate.match(/^datePicker-(.+)$/))
- _maxDate = elData(elById(RegExp.$1), 'value');
- _maxDate = new Date(+_maxDate);
- if (data.isDateTime) {
- _dateHour.value = date.getHours();
- _dateMinute.value = date.getMinutes();
- _datePicker.classList.add('datePickerTime');
- }
- else {
- _datePicker.classList.remove('datePickerTime');
- }
- _datePicker.classList[(data.isTimeOnly) ? 'add' : 'remove']('datePickerTimeOnly');
- this._renderPicker(date.getDate(), date.getMonth(), date.getFullYear());
- UiAlignment.set(_datePicker, _input);
- elAttr(_input.nextElementSibling, 'aria-expanded', true);
- _wasInsidePicker = false;
- },
- /**
- * Closes the date picker.
- */
- _close: function () {
- if (_datePicker !== null && _datePicker.classList.contains('active')) {
- _datePicker.classList.remove('active');
- var data = _data.get(_input);
- if (typeof data.onClose === 'function') {
- data.onClose();
- }
- EventHandler.fire('WoltLabSuite/Core/Date/Picker', 'close', { element: _input });
- elAttr(_input.nextElementSibling, 'aria-expanded', false);
- _input = null;
- _minDate = 0;
- _maxDate = 0;
- }
- },
- /**
- * Updates the position of the date picker in a dialog if the dialog content
- * is scrolled.
- *
- * @param {Event} event scroll event
- */
- _onDialogScroll: function (event) {
- if (_input === null) {
- return;
- }
- var dialogContent = event.currentTarget;
- var offset = DomUtil.offset(_input);
- var dialogOffset = DomUtil.offset(dialogContent);
- // check if date picker input field is still (partially) visible
- if (offset.top + _input.clientHeight <= dialogOffset.top) {
- // top check
- this._close();
- }
- else if (offset.top >= dialogOffset.top + dialogContent.offsetHeight) {
- // bottom check
- this._close();
- }
- else if (offset.left <= dialogOffset.left) {
- // left check
- this._close();
- }
- else if (offset.left >= dialogOffset.left + dialogContent.offsetWidth) {
- // right check
- this._close();
- }
- else {
- UiAlignment.set(_datePicker, _input);
- }
- },
- /**
- * Renders the full picker on init.
- *
- * @param {int} day
- * @param {int} month
- * @param {int} year
- */
- _renderPicker: function (day, month, year) {
- this._renderGrid(day, month, year);
- // create options for month and year
- var years = '';
- for (var i = _minDate.getFullYear(), last = _maxDate.getFullYear(); i <= last; i++) {
- years += '<option value="' + i + '">' + i + '</option>';
- }
- _dateYear.innerHTML = years;
- _dateYear.value = year;
- _dateMonth.value = month;
- _datePicker.classList.add('active');
- },
- /**
- * Updates the date grid.
- *
- * @param {int} day
- * @param {int} month
- * @param {int} year
- */
- _renderGrid: function (day, month, year) {
- var cell, hasDay = (day !== undefined), hasMonth = (month !== undefined), i;
- day = ~~day || ~~elData(_dateGrid, 'day');
- month = ~~month;
- year = ~~year;
- // rebuild cells
- if (hasMonth || year) {
- var rebuildMonths = (year !== 0);
- // rebuild grid
- var fragment = document.createDocumentFragment();
- fragment.appendChild(_dateGrid);
- if (!hasMonth)
- month = ~~elData(_dateGrid, 'month');
- year = year || ~~elData(_dateGrid, 'year');
- // check if current selection exceeds min/max date
- var date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-' + ('0' + day.toString()).slice(-2));
- if (date < _minDate) {
- year = _minDate.getFullYear();
- month = _minDate.getMonth();
- day = _minDate.getDate();
- _dateMonth.value = month;
- _dateYear.value = year;
- rebuildMonths = true;
- }
- else if (date > _maxDate) {
- year = _maxDate.getFullYear();
- month = _maxDate.getMonth();
- day = _maxDate.getDate();
- _dateMonth.value = month;
- _dateYear.value = year;
- rebuildMonths = true;
- }
- date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
- // shift until first displayed day equals first day of week
- while (date.getDay() !== _firstDayOfWeek) {
- date.setDate(date.getDate() - 1);
- }
- // show the last row
- elShow(_dateCells[35].parentNode);
- var selectable;
- var comparableMinDate = new Date(_minDate.getFullYear(), _minDate.getMonth(), _minDate.getDate());
- for (i = 0; i < 42; i++) {
- if (i === 35 && date.getMonth() !== month) {
- // skip the last row if it only contains the next month
- elHide(_dateCells[35].parentNode);
- break;
- }
- cell = _dateCells[i];
- cell.textContent = date.getDate();
- selectable = (date.getMonth() === month);
- if (selectable) {
- if (date < comparableMinDate)
- selectable = false;
- else if (date > _maxDate)
- selectable = false;
- }
- cell.classList[selectable ? 'remove' : 'add']('otherMonth');
- if (selectable) {
- cell.href = '#';
- elAttr(cell, 'role', 'button');
- elAttr(cell, 'tabindex', '0');
- elAttr(cell, 'title', DateUtil.formatDate(date));
- elAttr(cell, 'aria-label', DateUtil.formatDate(date));
- }
- date.setDate(date.getDate() + 1);
- }
- elData(_dateGrid, 'month', month);
- elData(_dateGrid, 'year', year);
- _datePicker.insertBefore(fragment, _dateTime);
- if (!hasDay) {
- // check if date is valid
- date = new Date(year, month, day);
- if (date.getDate() !== day) {
- while (date.getMonth() !== month) {
- date.setDate(date.getDate() - 1);
- }
- day = date.getDate();
- }
- }
- if (rebuildMonths) {
- for (i = 0; i < 12; i++) {
- var currentMonth = _dateMonth.children[i];
- currentMonth.disabled = (year === _minDate.getFullYear() && currentMonth.value < _minDate.getMonth()) || (year === _maxDate.getFullYear() && currentMonth.value > _maxDate.getMonth());
- }
- var nextMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
- nextMonth.setMonth(nextMonth.getMonth() + 1);
- _dateMonthNext.classList[(nextMonth < _maxDate) ? 'add' : 'remove']('active');
- var previousMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
- previousMonth.setDate(previousMonth.getDate() - 1);
- _dateMonthPrevious.classList[(previousMonth > _minDate) ? 'add' : 'remove']('active');
- }
- }
- // update active day
- if (day) {
- for (i = 0; i < 35; i++) {
- cell = _dateCells[i];
- cell.classList[(!cell.classList.contains('otherMonth') && ~~cell.textContent === day) ? 'add' : 'remove']('active');
- }
- elData(_dateGrid, 'day', day);
- }
- this._formatValue();
- },
- /**
- * Sets the visible and shadow value
- */
- _formatValue: function () {
- var data = _data.get(_input), date;
- if (elData(_input, 'empty') === 'true') {
- return;
- }
- if (data.isDateTime) {
- date = new Date(elData(_dateGrid, 'year'), elData(_dateGrid, 'month'), elData(_dateGrid, 'day'), _dateHour.value, _dateMinute.value);
- }
- else {
- date = new Date(elData(_dateGrid, 'year'), elData(_dateGrid, 'month'), elData(_dateGrid, 'day'));
- }
- this.setDate(_input, date);
- },
- /**
- * Creates the date picker DOM.
- */
- _createPicker: function () {
- if (_datePicker !== null) {
- return;
- }
- _datePicker = elCreate('div');
- _datePicker.className = 'datePicker';
- _datePicker.addEventListener(WCF_CLICK_EVENT, function (event) { event.stopPropagation(); });
- var header = elCreate('header');
- _datePicker.appendChild(header);
- _dateMonthPrevious = elCreate('a');
- _dateMonthPrevious.className = 'previous jsTooltip';
- _dateMonthPrevious.href = '#';
- elAttr(_dateMonthPrevious, 'role', 'button');
- elAttr(_dateMonthPrevious, 'tabindex', '0');
- elAttr(_dateMonthPrevious, 'title', Language.get('wcf.date.datePicker.previousMonth'));
- elAttr(_dateMonthPrevious, 'aria-label', Language.get('wcf.date.datePicker.previousMonth'));
- _dateMonthPrevious.innerHTML = '<span class="icon icon16 fa-arrow-left"></span>';
- _dateMonthPrevious.addEventListener(WCF_CLICK_EVENT, this.previousMonth.bind(this));
- header.appendChild(_dateMonthPrevious);
- var monthYearContainer = elCreate('span');
- header.appendChild(monthYearContainer);
- _dateMonth = elCreate('select');
- _dateMonth.className = 'month jsTooltip';
- elAttr(_dateMonth, 'title', Language.get('wcf.date.datePicker.month'));
- elAttr(_dateMonth, 'aria-label', Language.get('wcf.date.datePicker.month'));
- _dateMonth.addEventListener('change', this._changeMonth.bind(this));
- monthYearContainer.appendChild(_dateMonth);
- var i, months = '', monthNames = Language.get('__monthsShort');
- for (i = 0; i < 12; i++) {
- months += '<option value="' + i + '">' + monthNames[i] + '</option>';
- }
- _dateMonth.innerHTML = months;
- _dateYear = elCreate('select');
- _dateYear.className = 'year jsTooltip';
- elAttr(_dateYear, 'title', Language.get('wcf.date.datePicker.year'));
- elAttr(_dateYear, 'aria-label', Language.get('wcf.date.datePicker.year'));
- _dateYear.addEventListener('change', this._changeYear.bind(this));
- monthYearContainer.appendChild(_dateYear);
- _dateMonthNext = elCreate('a');
- _dateMonthNext.className = 'next jsTooltip';
- _dateMonthNext.href = '#';
- elAttr(_dateMonthNext, 'role', 'button');
- elAttr(_dateMonthNext, 'tabindex', '0');
- elAttr(_dateMonthNext, 'title', Language.get('wcf.date.datePicker.nextMonth'));
- elAttr(_dateMonthNext, 'aria-label', Language.get('wcf.date.datePicker.nextMonth'));
- _dateMonthNext.innerHTML = '<span class="icon icon16 fa-arrow-right"></span>';
- _dateMonthNext.addEventListener(WCF_CLICK_EVENT, this.nextMonth.bind(this));
- header.appendChild(_dateMonthNext);
- _dateGrid = elCreate('ul');
- _datePicker.appendChild(_dateGrid);
- var item = elCreate('li');
- item.className = 'weekdays';
- _dateGrid.appendChild(item);
- var span, weekdays = Language.get('__daysShort');
- for (i = 0; i < 7; i++) {
- var day = i + _firstDayOfWeek;
- if (day > 6)
- day -= 7;
- span = elCreate('span');
- span.textContent = weekdays[day];
- item.appendChild(span);
- }
- // create date grid
- var callbackClick = this._click.bind(this), cell, row;
- for (i = 0; i < 6; i++) {
- row = elCreate('li');
- _dateGrid.appendChild(row);
- for (var j = 0; j < 7; j++) {
- cell = elCreate('a');
- cell.addEventListener(WCF_CLICK_EVENT, callbackClick);
- _dateCells.push(cell);
- row.appendChild(cell);
- }
- }
- _dateTime = elCreate('footer');
- _datePicker.appendChild(_dateTime);
- _dateHour = elCreate('select');
- _dateHour.className = 'hour';
- elAttr(_dateHour, 'title', Language.get('wcf.date.datePicker.hour'));
- elAttr(_dateHour, 'aria-label', Language.get('wcf.date.datePicker.hour'));
- _dateHour.addEventListener('change', this._formatValue.bind(this));
- var tmp = '';
- var date = new Date(2000, 0, 1);
- var timeFormat = Language.get('wcf.date.timeFormat').replace(/:/, '').replace(/[isu]/g, '');
- for (i = 0; i < 24; i++) {
- date.setHours(i);
- tmp += '<option value="' + i + '">' + DateUtil.format(date, timeFormat) + "</option>";
- }
- _dateHour.innerHTML = tmp;
- _dateTime.appendChild(_dateHour);
- _dateTime.appendChild(document.createTextNode('\u00A0:\u00A0'));
- _dateMinute = elCreate('select');
- _dateMinute.className = 'minute';
- elAttr(_dateMinute, 'title', Language.get('wcf.date.datePicker.minute'));
- elAttr(_dateMinute, 'aria-label', Language.get('wcf.date.datePicker.minute'));
- _dateMinute.addEventListener('change', this._formatValue.bind(this));
- tmp = '';
- for (i = 0; i < 60; i++) {
- tmp += '<option value="' + i + '">' + (i < 10 ? '0' + i.toString() : i) + '</option>';
- }
- _dateMinute.innerHTML = tmp;
- _dateTime.appendChild(_dateMinute);
- document.body.appendChild(_datePicker);
+ });
},
/**
* Shows the previous month.
*/
- previousMonth: function (event) {
+ previousMonth(event) {
event.preventDefault();
if (_dateMonth.value === '0') {
- _dateMonth.value = 11;
- _dateYear.value = ~~_dateYear.value - 1;
+ _dateMonth.value = '11';
+ _dateYear.value = (+_dateYear.value - 1).toString();
}
else {
- _dateMonth.value = ~~_dateMonth.value - 1;
+ _dateMonth.value = (+_dateMonth.value - 1).toString();
}
- this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
+ renderGrid(undefined, +_dateMonth.value, +_dateYear.value);
},
/**
* Shows the next month.
*/
- nextMonth: function (event) {
+ nextMonth(event) {
event.preventDefault();
if (_dateMonth.value === '11') {
- _dateMonth.value = 0;
- _dateYear.value = ~~_dateYear.value + 1;
+ _dateMonth.value = '0';
+ _dateYear.value = (+_dateYear.value + 1).toString();
}
else {
- _dateMonth.value = ~~_dateMonth.value + 1;
- }
- this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
- },
- /**
- * Handles changes to the month select element.
- *
- * @param {object} event event object
- */
- _changeMonth: function (event) {
- this._renderGrid(undefined, event.currentTarget.value);
- },
- /**
- * Handles changes to the year select element.
- *
- * @param {object} event event object
- */
- _changeYear: function (event) {
- this._renderGrid(undefined, undefined, event.currentTarget.value);
- },
- /**
- * Handles clicks on an individual day.
- *
- * @param {object} event event object
- */
- _click: function (event) {
- event.preventDefault();
- if (event.currentTarget.classList.contains('otherMonth')) {
- return;
- }
- elData(_input, 'empty', false);
- this._renderGrid(event.currentTarget.textContent);
- var data = _data.get(_input);
- if (!data.isDateTime) {
- this._close();
+ _dateMonth.value = (+_dateMonth.value + 1).toString();
}
+ renderGrid(undefined, +_dateMonth.value, +_dateYear.value);
},
/**
* Returns the current Date object or null.
- *
- * @param {(Element|string)} element input element or id
- * @return {?Date} Date object or null
*/
- getDate: function (element) {
- element = this._getElement(element);
- if (element.hasAttribute('data-value')) {
- return new Date(+elData(element, 'value'));
+ getDate(element) {
+ element = getElement(element);
+ const value = element.dataset.value || '';
+ if (value) {
+ return new Date(+value);
}
return null;
},
/**
* Sets the date of given element.
*
- * @param {(HTMLInputElement|string)} element input element or id
- * @param {Date} date Date object
+ * @param {(HTMLInputElement|string)} element input element or id
+ * @param {Date} date Date object
*/
- setDate: function (element, date) {
- element = this._getElement(element);
- var data = _data.get(element);
- elData(element, 'value', date.getTime());
- var format = '', value;
+ setDate(element, date) {
+ element = getElement(element);
+ const data = _data.get(element);
+ element.dataset.value = date.getTime().toString();
+ let format = '';
+ let value;
if (data.isDateTime) {
if (data.isTimeOnly) {
value = DateUtil.formatTime(date);
},
/**
* Returns the current value.
- *
- * @param {(Element|string)} element input element or id
- * @return {string} current date value
*/
- getValue: function (element) {
- element = this._getElement(element);
- var data = _data.get(element);
+ getValue(element) {
+ element = getElement(element);
+ const data = _data.get(element);
if (data) {
return data.shadow.value;
}
},
/**
* Clears the date value of given element.
- *
- * @param {(HTMLInputElement|string)} element input element or id
*/
- clear: function (element) {
- element = this._getElement(element);
- var data = _data.get(element);
+ clear(element) {
+ element = getElement(element);
+ const data = _data.get(element);
element.removeAttribute('data-value');
element.value = '';
- if (!data.disableClear)
+ if (!data.disableClear) {
data.clearButton.style.setProperty('visibility', 'hidden', '');
+ }
data.isEmpty = true;
data.shadow.value = '';
},
/**
* Reverts the date picker into a normal input field.
- *
- * @param {(HTMLInputElement|string)} element input element or id
*/
- destroy: function (element) {
- element = this._getElement(element);
- var data = _data.get(element);
- var container = element.parentNode;
+ destroy(element) {
+ element = getElement(element);
+ const data = _data.get(element);
+ const container = element.parentNode;
container.parentNode.insertBefore(element, container);
- elRemove(container);
- elAttr(element, 'type', 'date' + (data.isDateTime ? 'time' : ''));
+ container.remove();
+ element.type = 'date' + (data.isDateTime ? 'time' : '');
element.name = data.shadow.name;
element.value = data.shadow.value;
element.removeAttribute('data-value');
- element.removeEventListener(WCF_CLICK_EVENT, _callbackOpen);
- elRemove(data.shadow);
+ element.removeEventListener('click', open);
+ data.shadow.remove();
element.classList.remove('inputDatePicker');
element.readOnly = false;
- _data['delete'](element);
+ _data.delete(element);
},
/**
* Sets the callback invoked on picker close.
- *
- * @param {(Element|string)} element input element or id
- * @param {function} callback callback function
*/
- setCloseCallback: function (element, callback) {
- element = this._getElement(element);
+ setCloseCallback(element, callback) {
+ element = getElement(element);
_data.get(element).onClose = callback;
},
- /**
- * Validates given element or id if it represents an active date picker.
- *
- * @param {(Element|string)} element input element or id
- * @return {Element} input element
- */
- _getElement: function (element) {
- if (typeof element === 'string')
- element = elById(element);
- if (!(element instanceof Element) || !element.classList.contains('inputDatePicker') || !_data.has(element)) {
- throw new Error("Expected a valid date picker input element or id.");
- }
- return element;
- },
- /**
- * @param {Event} event
- */
- _maintainFocus: function (event) {
- if (_datePicker !== null && _datePicker.classList.contains('active')) {
- if (!_datePicker.contains(event.target)) {
- if (_wasInsidePicker) {
- _input.nextElementSibling.focus();
- _wasInsidePicker = false;
- }
- else {
- elBySel('.previous', _datePicker).focus();
- }
- }
- else {
- _wasInsidePicker = true;
- }
- }
- }
};
// backward-compatibility for `$.ui.datepicker` shim
window.__wcf_bc_datePicker = DatePicker;
CallbackList_1 = __importDefault(CallbackList_1);
const _callbackList = new CallbackList_1.default();
let _hot = false;
- return {
+ const DomChangeListener = {
/**
* @see CallbackList.add
*/
}
},
};
+ return DomChangeListener;
});
vertical: 'bottom',
// allow flipping over axis, possible values: both, horizontal, vertical and none
allowFlip: 'both',
- }, options);
+ }, options || {});
if (!Array.isArray(options.pointerClassNames) || options.pointerClassNames.length !== (options.pointer ? 1 : 2)) {
options.pointerClassNames = [];
}
+++ /dev/null
-/**
- * Date picker with time support.
- *
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module WoltLabSuite/Core/Date/Picker
- */
-define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'ObjectMap', 'Dom/ChangeListener', 'Ui/Alignment', 'WoltLabSuite/Core/Ui/CloseOverlay'], function(DateUtil, DomTraverse, DomUtil, EventHandler, Language, ObjectMap, DomChangeListener, UiAlignment, UiCloseOverlay) {
- "use strict";
-
- var _didInit = false;
- var _firstDayOfWeek = 0;
- var _wasInsidePicker = false;
-
- var _data = new ObjectMap();
- var _input = null;
- var _maxDate = 0;
- var _minDate = 0;
-
- var _dateCells = [];
- var _dateGrid = null;
- var _dateHour = null;
- var _dateMinute = null;
- var _dateMonth = null;
- var _dateMonthNext = null;
- var _dateMonthPrevious = null;
- var _dateTime = null;
- var _dateYear = null;
- var _datePicker = null;
-
- var _callbackOpen = null;
- var _callbackFocus = null;
-
- /**
- * @exports WoltLabSuite/Core/Date/Picker
- */
- var DatePicker = {
- /**
- * Initializes all date and datetime input fields.
- */
- init: function() {
- this._setup();
-
- var elements = elBySelAll('input[type="date"]:not(.inputDatePicker), input[type="datetime"]:not(.inputDatePicker)');
- var now = new Date();
- for (var i = 0, length = elements.length; i < length; i++) {
- var element = elements[i];
- element.classList.add('inputDatePicker');
- element.readOnly = true;
-
- var isDateTime = (elAttr(element, 'type') === 'datetime');
- var isTimeOnly = (isDateTime && elDataBool(element, 'time-only'));
- var disableClear = elDataBool(element, 'disable-clear');
- var ignoreTimezone = isDateTime && elDataBool(element, 'ignore-timezone');
- var isBirthday = element.classList.contains('birthday');
-
- elData(element, 'is-date-time', isDateTime);
- elData(element, 'is-time-only', isTimeOnly);
-
- // convert value
- var date = null, value = elAttr(element, 'value');
-
- // ignore the timezone, if the value is only a date (YYYY-MM-DD)
- var isDateOnly = /^\d+-\d+-\d+$/.test(value);
-
- if (elAttr(element, 'value')) {
- if (isTimeOnly) {
- date = new Date();
- var tmp = value.split(':');
- date.setHours(tmp[0], tmp[1]);
- }
- else {
- if (ignoreTimezone || isBirthday || isDateOnly) {
- var timezoneOffset = new Date(value).getTimezoneOffset();
- var timezone = (timezoneOffset > 0) ? '-' : '+'; // -120 equals GMT+0200
- timezoneOffset = Math.abs(timezoneOffset);
- var hours = (Math.floor(timezoneOffset / 60)).toString();
- var minutes = (timezoneOffset % 60).toString();
- timezone += (hours.length === 2) ? hours : '0' + hours;
- timezone += ':';
- timezone += (minutes.length === 2) ? minutes : '0' + minutes;
-
- if (isBirthday || isDateOnly) {
- value += 'T00:00:00' + timezone;
- }
- else {
- value = value.replace(/[+-][0-9]{2}:[0-9]{2}$/, timezone);
- }
- }
-
- date = new Date(value);
- }
-
- var time = date.getTime();
-
- // check for invalid dates
- if (isNaN(time)) {
- value = '';
- }
- else {
- elData(element, 'value', time);
- var format = (isTimeOnly) ? 'formatTime' : ('formatDate' + (isDateTime ? 'Time' : ''));
- value = DateUtil[format](date);
- }
- }
-
- var isEmpty = (value.length === 0);
-
- // handle birthday input
- if (isBirthday) {
- elData(element, 'min-date', '120');
-
- // do not use 'now' here, all though it makes sense, it causes bad UX
- elData(element, 'max-date', new Date().getFullYear() + '-12-31');
- }
- else {
- if (element.min) elData(element, 'min-date', element.min);
- if (element.max) elData(element, 'max-date', element.max);
- }
-
- this._initDateRange(element, now, true);
- this._initDateRange(element, now, false);
-
- if (elData(element, 'min-date') === elData(element, 'max-date')) {
- throw new Error("Minimum and maximum date cannot be the same (element id '" + element.id + "').");
- }
-
- // change type to prevent browser's datepicker to trigger
- element.type = 'text';
- element.value = value;
- elData(element, 'empty', isEmpty);
-
- if (elData(element, 'placeholder')) {
- elAttr(element, 'placeholder', elData(element, 'placeholder'));
- }
-
- // add a hidden element to hold the actual date
- var shadowElement = elCreate('input');
- shadowElement.id = element.id + 'DatePicker';
- shadowElement.name = element.name;
- shadowElement.type = 'hidden';
-
- if (date !== null) {
- if (isTimeOnly) {
- shadowElement.value = DateUtil.format(date, 'H:i');
- }
- else if (ignoreTimezone) {
- shadowElement.value = DateUtil.format(date, 'Y-m-dTH:i:s');
- }
- else {
- shadowElement.value = DateUtil.format(date, (isDateTime) ? 'c' : 'Y-m-d');
- }
- }
-
- element.parentNode.insertBefore(shadowElement, element);
- element.removeAttribute('name');
-
- element.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
-
- if (!element.disabled) {
- // create input addon
- var container = elCreate('div');
- container.className = 'inputAddon';
-
- var button = elCreate('a');
-
- button.className = 'inputSuffix button jsTooltip';
- button.href = '#';
- elAttr(button, 'role', 'button');
- elAttr(button, 'tabindex', '0');
- elAttr(button, 'title', Language.get('wcf.date.datePicker'));
- elAttr(button, 'aria-label', Language.get('wcf.date.datePicker'));
- elAttr(button, 'aria-haspopup', true);
- elAttr(button, 'aria-expanded', false);
- button.addEventListener(WCF_CLICK_EVENT, _callbackOpen);
- container.appendChild(button);
-
- var icon = elCreate('span');
- icon.className = 'icon icon16 fa-calendar';
- button.appendChild(icon);
-
- element.parentNode.insertBefore(container, element);
- container.insertBefore(element, button);
-
- if (!disableClear) {
- button = elCreate('a');
- button.className = 'inputSuffix button';
- button.addEventListener(WCF_CLICK_EVENT, this.clear.bind(this, element));
- if (isEmpty) button.style.setProperty('visibility', 'hidden', '');
-
- container.appendChild(button);
-
- icon = elCreate('span');
- icon.className = 'icon icon16 fa-times';
- button.appendChild(icon);
- }
- }
-
- // check if the date input has one of the following classes set otherwise default to 'short'
- var hasClass = false, knownClasses = ['tiny', 'short', 'medium', 'long'];
- for (var j = 0; j < 4; j++) {
- if (element.classList.contains(knownClasses[j])) {
- hasClass = true;
- }
- }
-
- if (!hasClass) {
- element.classList.add('short');
- }
-
- _data.set(element, {
- clearButton: button,
- shadow: shadowElement,
-
- disableClear: disableClear,
- isDateTime: isDateTime,
- isEmpty: isEmpty,
- isTimeOnly: isTimeOnly,
- ignoreTimezone: ignoreTimezone,
-
- onClose: null
- });
- }
- },
-
- /**
- * Initializes the minimum/maximum date range.
- *
- * @param {Element} element input element
- * @param {Date} now current date
- * @param {boolean} isMinDate true for the minimum date
- */
- _initDateRange: function(element, now, isMinDate) {
- var attribute = 'data-' + (isMinDate ? 'min' : 'max') + '-date';
- var value = (element.hasAttribute(attribute)) ? elAttr(element, attribute).trim() : '';
-
- if (value.match(/^(\d{4})-(\d{2})-(\d{2})$/)) {
- // YYYY-mm-dd
- value = new Date(value).getTime();
- }
- else if (value === 'now') {
- value = now.getTime();
- }
- else if (value.match(/^\d{1,3}$/)) {
- // relative time span in years
- var date = new Date(now.getTime());
- date.setFullYear(date.getFullYear() + ~~value * (isMinDate ? -1 : 1));
-
- value = date.getTime();
- }
- else if (value.match(/^datePicker-(.+)$/)) {
- // element id, e.g. `datePicker-someOtherElement`
- value = RegExp.$1;
-
- if (elById(value) === null) {
- throw new Error("Reference date picker identified by '" + value + "' does not exists (element id: '" + element.id + "').");
- }
- }
- else if (/^\d{4}\-\d{2}\-\d{2}T/.test(value)) {
- value = new Date(value).getTime();
- }
- else {
- value = new Date((isMinDate ? 1902 : 2038), 0, 1).getTime();
- }
-
- elAttr(element, attribute, value);
- },
-
- /**
- * Sets up callbacks and event listeners.
- */
- _setup: function() {
- if (_didInit) return;
- _didInit = true;
-
- _firstDayOfWeek = ~~Language.get('wcf.date.firstDayOfTheWeek');
- _callbackOpen = this._open.bind(this);
-
- DomChangeListener.add('WoltLabSuite/Core/Date/Picker', this.init.bind(this));
- UiCloseOverlay.add('WoltLabSuite/Core/Date/Picker', this._close.bind(this));
- },
-
- /**
- * Opens the date picker.
- *
- * @param {object} event event object
- */
- _open: function(event) {
- event.preventDefault();
- event.stopPropagation();
-
- this._createPicker();
-
- if (_callbackFocus === null) {
- _callbackFocus = this._maintainFocus.bind(this);
- document.body.addEventListener('focus', _callbackFocus, { capture: true });
- }
-
- var input = (event.currentTarget.nodeName === 'INPUT') ? event.currentTarget : event.currentTarget.previousElementSibling;
- if (input === _input) {
- this._close();
- return;
- }
-
- var dialogContent = DomTraverse.parentByClass(input, 'dialogContent');
- if (dialogContent !== null) {
- if (!elDataBool(dialogContent, 'has-datepicker-scroll-listener')) {
- dialogContent.addEventListener('scroll', this._onDialogScroll.bind(this));
- elData(dialogContent, 'has-datepicker-scroll-listener', 1);
- }
- }
-
- _input = input;
- var data = _data.get(_input), date, value = elData(_input, 'value');
- if (value) {
- date = new Date(+value);
-
- if (date.toString() === 'Invalid Date') {
- date = new Date();
- }
- }
- else {
- date = new Date();
- }
-
- // set min/max date
- _minDate = elData(_input, 'min-date');
- if (_minDate.match(/^datePicker-(.+)$/)) _minDate = elData(elById(RegExp.$1), 'value');
- _minDate = new Date(+_minDate);
- if (_minDate.getTime() > date.getTime()) date = _minDate;
-
- _maxDate = elData(_input, 'max-date');
- if (_maxDate.match(/^datePicker-(.+)$/)) _maxDate = elData(elById(RegExp.$1), 'value');
- _maxDate = new Date(+_maxDate);
-
- if (data.isDateTime) {
- _dateHour.value = date.getHours();
- _dateMinute.value = date.getMinutes();
-
- _datePicker.classList.add('datePickerTime');
- }
- else {
- _datePicker.classList.remove('datePickerTime');
- }
-
- _datePicker.classList[(data.isTimeOnly) ? 'add' : 'remove']('datePickerTimeOnly');
-
- this._renderPicker(date.getDate(), date.getMonth(), date.getFullYear());
-
- UiAlignment.set(_datePicker, _input);
-
- elAttr(_input.nextElementSibling, 'aria-expanded', true);
-
- _wasInsidePicker = false;
- },
-
- /**
- * Closes the date picker.
- */
- _close: function() {
- if (_datePicker !== null && _datePicker.classList.contains('active')) {
- _datePicker.classList.remove('active');
-
- var data = _data.get(_input);
- if (typeof data.onClose === 'function') {
- data.onClose();
- }
-
- EventHandler.fire('WoltLabSuite/Core/Date/Picker', 'close', {element: _input});
-
- elAttr(_input.nextElementSibling, 'aria-expanded', false);
- _input = null;
- _minDate = 0;
- _maxDate = 0;
- }
- },
-
- /**
- * Updates the position of the date picker in a dialog if the dialog content
- * is scrolled.
- *
- * @param {Event} event scroll event
- */
- _onDialogScroll: function(event) {
- if (_input === null) {
- return;
- }
-
- var dialogContent = event.currentTarget;
-
- var offset = DomUtil.offset(_input);
- var dialogOffset = DomUtil.offset(dialogContent);
-
- // check if date picker input field is still (partially) visible
- if (offset.top + _input.clientHeight <= dialogOffset.top) {
- // top check
- this._close();
- }
- else if (offset.top >= dialogOffset.top + dialogContent.offsetHeight) {
- // bottom check
- this._close();
- }
- else if (offset.left <= dialogOffset.left) {
- // left check
- this._close();
- }
- else if (offset.left >= dialogOffset.left + dialogContent.offsetWidth) {
- // right check
- this._close();
- }
- else {
- UiAlignment.set(_datePicker, _input);
- }
- },
-
- /**
- * Renders the full picker on init.
- *
- * @param {int} day
- * @param {int} month
- * @param {int} year
- */
- _renderPicker: function(day, month, year) {
- this._renderGrid(day, month, year);
-
- // create options for month and year
- var years = '';
- for (var i = _minDate.getFullYear(), last = _maxDate.getFullYear(); i <= last; i++) {
- years += '<option value="' + i + '">' + i + '</option>';
- }
- _dateYear.innerHTML = years;
- _dateYear.value = year;
-
- _dateMonth.value = month;
-
- _datePicker.classList.add('active');
- },
-
- /**
- * Updates the date grid.
- *
- * @param {int} day
- * @param {int} month
- * @param {int} year
- */
- _renderGrid: function(day, month, year) {
- var cell, hasDay = (day !== undefined), hasMonth = (month !== undefined), i;
-
- day = ~~day || ~~elData(_dateGrid, 'day');
- month = ~~month;
- year = ~~year;
-
- // rebuild cells
- if (hasMonth || year) {
- var rebuildMonths = (year !== 0);
-
- // rebuild grid
- var fragment = document.createDocumentFragment();
- fragment.appendChild(_dateGrid);
-
- if (!hasMonth) month = ~~elData(_dateGrid, 'month');
- year = year || ~~elData(_dateGrid, 'year');
-
- // check if current selection exceeds min/max date
- var date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-' + ('0' + day.toString()).slice(-2));
- if (date < _minDate) {
- year = _minDate.getFullYear();
- month = _minDate.getMonth();
- day = _minDate.getDate();
-
- _dateMonth.value = month;
- _dateYear.value = year;
-
- rebuildMonths = true;
- }
- else if (date > _maxDate) {
- year = _maxDate.getFullYear();
- month = _maxDate.getMonth();
- day = _maxDate.getDate();
-
- _dateMonth.value = month;
- _dateYear.value = year;
-
- rebuildMonths = true;
- }
-
- date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
-
- // shift until first displayed day equals first day of week
- while (date.getDay() !== _firstDayOfWeek) {
- date.setDate(date.getDate() - 1);
- }
-
- // show the last row
- elShow(_dateCells[35].parentNode);
-
- var selectable;
- var comparableMinDate = new Date(_minDate.getFullYear(), _minDate.getMonth(), _minDate.getDate());
- for (i = 0; i < 42; i++) {
- if (i === 35 && date.getMonth() !== month) {
- // skip the last row if it only contains the next month
- elHide(_dateCells[35].parentNode);
-
- break;
- }
-
- cell = _dateCells[i];
-
- cell.textContent = date.getDate();
- selectable = (date.getMonth() === month);
- if (selectable) {
- if (date < comparableMinDate) selectable = false;
- else if (date > _maxDate) selectable = false;
- }
-
- cell.classList[selectable ? 'remove' : 'add']('otherMonth');
- if (selectable) {
- cell.href = '#';
- elAttr(cell, 'role', 'button');
- elAttr(cell, 'tabindex', '0');
- elAttr(cell, 'title', DateUtil.formatDate(date));
- elAttr(cell, 'aria-label', DateUtil.formatDate(date));
- }
-
- date.setDate(date.getDate() + 1);
- }
-
- elData(_dateGrid, 'month', month);
- elData(_dateGrid, 'year', year);
-
- _datePicker.insertBefore(fragment, _dateTime);
-
- if (!hasDay) {
- // check if date is valid
- date = new Date(year, month, day);
- if (date.getDate() !== day) {
- while (date.getMonth() !== month) {
- date.setDate(date.getDate() - 1);
- }
-
- day = date.getDate();
- }
- }
-
- if (rebuildMonths) {
- for (i = 0; i < 12; i++) {
- var currentMonth = _dateMonth.children[i];
-
- currentMonth.disabled = (year === _minDate.getFullYear() && currentMonth.value < _minDate.getMonth()) || (year === _maxDate.getFullYear() && currentMonth.value > _maxDate.getMonth());
- }
-
- var nextMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
- nextMonth.setMonth(nextMonth.getMonth() + 1);
-
- _dateMonthNext.classList[(nextMonth < _maxDate) ? 'add' : 'remove']('active');
-
- var previousMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
- previousMonth.setDate(previousMonth.getDate() - 1);
-
- _dateMonthPrevious.classList[(previousMonth > _minDate) ? 'add' : 'remove']('active');
- }
- }
-
- // update active day
- if (day) {
- for (i = 0; i < 35; i++) {
- cell = _dateCells[i];
-
- cell.classList[(!cell.classList.contains('otherMonth') && ~~cell.textContent === day) ? 'add' : 'remove']('active');
- }
-
- elData(_dateGrid, 'day', day);
- }
-
- this._formatValue();
- },
-
- /**
- * Sets the visible and shadow value
- */
- _formatValue: function() {
- var data = _data.get(_input), date;
-
- if (elData(_input, 'empty') === 'true') {
- return;
- }
-
- if (data.isDateTime) {
- date = new Date(
- elData(_dateGrid, 'year'),
- elData(_dateGrid, 'month'),
- elData(_dateGrid, 'day'),
- _dateHour.value,
- _dateMinute.value
- );
- }
- else {
- date = new Date(
- elData(_dateGrid, 'year'),
- elData(_dateGrid, 'month'),
- elData(_dateGrid, 'day')
- );
- }
-
- this.setDate(_input, date);
- },
-
- /**
- * Creates the date picker DOM.
- */
- _createPicker: function() {
- if (_datePicker !== null) {
- return;
- }
-
- _datePicker = elCreate('div');
- _datePicker.className = 'datePicker';
- _datePicker.addEventListener(WCF_CLICK_EVENT, function(event) { event.stopPropagation(); });
-
- var header = elCreate('header');
- _datePicker.appendChild(header);
-
- _dateMonthPrevious = elCreate('a');
- _dateMonthPrevious.className = 'previous jsTooltip';
- _dateMonthPrevious.href = '#';
- elAttr(_dateMonthPrevious, 'role', 'button');
- elAttr(_dateMonthPrevious, 'tabindex', '0');
- elAttr(_dateMonthPrevious, 'title', Language.get('wcf.date.datePicker.previousMonth'));
- elAttr(_dateMonthPrevious, 'aria-label', Language.get('wcf.date.datePicker.previousMonth'));
- _dateMonthPrevious.innerHTML = '<span class="icon icon16 fa-arrow-left"></span>';
- _dateMonthPrevious.addEventListener(WCF_CLICK_EVENT, this.previousMonth.bind(this));
- header.appendChild(_dateMonthPrevious);
-
- var monthYearContainer = elCreate('span');
- header.appendChild(monthYearContainer);
-
- _dateMonth = elCreate('select');
- _dateMonth.className = 'month jsTooltip';
- elAttr(_dateMonth, 'title', Language.get('wcf.date.datePicker.month'));
- elAttr(_dateMonth, 'aria-label', Language.get('wcf.date.datePicker.month'));
- _dateMonth.addEventListener('change', this._changeMonth.bind(this));
- monthYearContainer.appendChild(_dateMonth);
-
- var i, months = '', monthNames = Language.get('__monthsShort');
- for (i = 0; i < 12; i++) {
- months += '<option value="' + i + '">' + monthNames[i] + '</option>';
- }
- _dateMonth.innerHTML = months;
-
- _dateYear = elCreate('select');
- _dateYear.className = 'year jsTooltip';
- elAttr(_dateYear, 'title', Language.get('wcf.date.datePicker.year'));
- elAttr(_dateYear, 'aria-label', Language.get('wcf.date.datePicker.year'));
- _dateYear.addEventListener('change', this._changeYear.bind(this));
- monthYearContainer.appendChild(_dateYear);
-
- _dateMonthNext = elCreate('a');
- _dateMonthNext.className = 'next jsTooltip';
- _dateMonthNext.href = '#';
- elAttr(_dateMonthNext, 'role', 'button');
- elAttr(_dateMonthNext, 'tabindex', '0');
- elAttr(_dateMonthNext, 'title', Language.get('wcf.date.datePicker.nextMonth'));
- elAttr(_dateMonthNext, 'aria-label', Language.get('wcf.date.datePicker.nextMonth'));
- _dateMonthNext.innerHTML = '<span class="icon icon16 fa-arrow-right"></span>';
- _dateMonthNext.addEventListener(WCF_CLICK_EVENT, this.nextMonth.bind(this));
- header.appendChild(_dateMonthNext);
-
- _dateGrid = elCreate('ul');
- _datePicker.appendChild(_dateGrid);
-
- var item = elCreate('li');
- item.className = 'weekdays';
- _dateGrid.appendChild(item);
-
- var span, weekdays = Language.get('__daysShort');
- for (i = 0; i < 7; i++) {
- var day = i + _firstDayOfWeek;
- if (day > 6) day -= 7;
-
- span = elCreate('span');
- span.textContent = weekdays[day];
- item.appendChild(span);
- }
-
- // create date grid
- var callbackClick = this._click.bind(this), cell, row;
- for (i = 0; i < 6; i++) {
- row = elCreate('li');
- _dateGrid.appendChild(row);
-
- for (var j = 0; j < 7; j++) {
- cell = elCreate('a');
- cell.addEventListener(WCF_CLICK_EVENT, callbackClick);
- _dateCells.push(cell);
-
- row.appendChild(cell);
- }
- }
-
- _dateTime = elCreate('footer');
- _datePicker.appendChild(_dateTime);
-
- _dateHour = elCreate('select');
- _dateHour.className = 'hour';
- elAttr(_dateHour, 'title', Language.get('wcf.date.datePicker.hour'));
- elAttr(_dateHour, 'aria-label', Language.get('wcf.date.datePicker.hour'));
- _dateHour.addEventListener('change', this._formatValue.bind(this));
-
- var tmp = '';
- var date = new Date(2000, 0, 1);
- var timeFormat = Language.get('wcf.date.timeFormat').replace(/:/, '').replace(/[isu]/g, '');
- for (i = 0; i < 24; i++) {
- date.setHours(i);
- tmp += '<option value="' + i + '">' + DateUtil.format(date, timeFormat) + "</option>";
- }
- _dateHour.innerHTML = tmp;
-
- _dateTime.appendChild(_dateHour);
-
- _dateTime.appendChild(document.createTextNode('\u00A0:\u00A0'));
-
- _dateMinute = elCreate('select');
- _dateMinute.className = 'minute';
- elAttr(_dateMinute, 'title', Language.get('wcf.date.datePicker.minute'));
- elAttr(_dateMinute, 'aria-label', Language.get('wcf.date.datePicker.minute'));
- _dateMinute.addEventListener('change', this._formatValue.bind(this));
-
- tmp = '';
- for (i = 0; i < 60; i++) {
- tmp += '<option value="' + i + '">' + (i < 10 ? '0' + i.toString() : i) + '</option>';
- }
- _dateMinute.innerHTML = tmp;
-
- _dateTime.appendChild(_dateMinute);
-
- document.body.appendChild(_datePicker);
- },
-
- /**
- * Shows the previous month.
- */
- previousMonth: function(event) {
- event.preventDefault();
-
- if (_dateMonth.value === '0') {
- _dateMonth.value = 11;
- _dateYear.value = ~~_dateYear.value - 1;
- }
- else {
- _dateMonth.value = ~~_dateMonth.value - 1;
- }
-
- this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
- },
-
- /**
- * Shows the next month.
- */
- nextMonth: function(event) {
- event.preventDefault();
-
- if (_dateMonth.value === '11') {
- _dateMonth.value = 0;
- _dateYear.value = ~~_dateYear.value + 1;
- }
- else {
- _dateMonth.value = ~~_dateMonth.value + 1;
- }
-
- this._renderGrid(undefined, _dateMonth.value, _dateYear.value);
- },
-
- /**
- * Handles changes to the month select element.
- *
- * @param {object} event event object
- */
- _changeMonth: function(event) {
- this._renderGrid(undefined, event.currentTarget.value);
- },
-
- /**
- * Handles changes to the year select element.
- *
- * @param {object} event event object
- */
- _changeYear: function(event) {
- this._renderGrid(undefined, undefined, event.currentTarget.value);
- },
-
- /**
- * Handles clicks on an individual day.
- *
- * @param {object} event event object
- */
- _click: function(event) {
- event.preventDefault();
-
- if (event.currentTarget.classList.contains('otherMonth')) {
- return;
- }
-
- elData(_input, 'empty', false);
-
- this._renderGrid(event.currentTarget.textContent);
-
- var data = _data.get(_input);
- if (!data.isDateTime) {
- this._close();
- }
- },
-
- /**
- * Returns the current Date object or null.
- *
- * @param {(Element|string)} element input element or id
- * @return {?Date} Date object or null
- */
- getDate: function(element) {
- element = this._getElement(element);
-
- if (element.hasAttribute('data-value')) {
- return new Date(+elData(element, 'value'));
- }
-
- return null;
- },
-
- /**
- * Sets the date of given element.
- *
- * @param {(HTMLInputElement|string)} element input element or id
- * @param {Date} date Date object
- */
- setDate: function(element, date) {
- element = this._getElement(element);
- var data = _data.get(element);
-
- elData(element, 'value', date.getTime());
-
- var format = '', value;
- if (data.isDateTime) {
- if (data.isTimeOnly) {
- value = DateUtil.formatTime(date);
- format = 'H:i';
- }
- else if (data.ignoreTimezone) {
- value = DateUtil.formatDateTime(date);
- format = 'Y-m-dTH:i:s';
- }
- else {
- value = DateUtil.formatDateTime(date);
- format = 'c';
- }
- }
- else {
- value = DateUtil.formatDate(date);
- format = 'Y-m-d';
- }
-
- element.value = value;
- data.shadow.value = DateUtil.format(date, format);
-
- // show clear button
- if (!data.disableClear) {
- data.clearButton.style.removeProperty('visibility');
- }
- },
-
- /**
- * Returns the current value.
- *
- * @param {(Element|string)} element input element or id
- * @return {string} current date value
- */
- getValue: function (element) {
- element = this._getElement(element);
- var data = _data.get(element);
-
- if (data) {
- return data.shadow.value;
- }
-
- return '';
- },
-
- /**
- * Clears the date value of given element.
- *
- * @param {(HTMLInputElement|string)} element input element or id
- */
- clear: function(element) {
- element = this._getElement(element);
- var data = _data.get(element);
-
- element.removeAttribute('data-value');
- element.value = '';
-
- if (!data.disableClear) data.clearButton.style.setProperty('visibility', 'hidden', '');
- data.isEmpty = true;
- data.shadow.value = '';
- },
-
- /**
- * Reverts the date picker into a normal input field.
- *
- * @param {(HTMLInputElement|string)} element input element or id
- */
- destroy: function(element) {
- element = this._getElement(element);
- var data = _data.get(element);
-
- var container = element.parentNode;
- container.parentNode.insertBefore(element, container);
- elRemove(container);
-
- elAttr(element, 'type', 'date' + (data.isDateTime ? 'time' : ''));
- element.name = data.shadow.name;
- element.value = data.shadow.value;
-
- element.removeAttribute('data-value');
- element.removeEventListener(WCF_CLICK_EVENT, _callbackOpen);
- elRemove(data.shadow);
-
- element.classList.remove('inputDatePicker');
- element.readOnly = false;
- _data['delete'](element);
- },
-
- /**
- * Sets the callback invoked on picker close.
- *
- * @param {(Element|string)} element input element or id
- * @param {function} callback callback function
- */
- setCloseCallback: function(element, callback) {
- element = this._getElement(element);
- _data.get(element).onClose = callback;
- },
-
- /**
- * Validates given element or id if it represents an active date picker.
- *
- * @param {(Element|string)} element input element or id
- * @return {Element} input element
- */
- _getElement: function(element) {
- if (typeof element === 'string') element = elById(element);
-
- if (!(element instanceof Element) || !element.classList.contains('inputDatePicker') || !_data.has(element)) {
- throw new Error("Expected a valid date picker input element or id.");
- }
-
- return element;
- },
-
- /**
- * @param {Event} event
- */
- _maintainFocus: function(event) {
- if (_datePicker !== null && _datePicker.classList.contains('active')) {
- if (!_datePicker.contains(event.target)) {
- if (_wasInsidePicker) {
- _input.nextElementSibling.focus();
- _wasInsidePicker = false;
- }
- else {
- elBySel('.previous', _datePicker).focus();
- }
- }
- else {
- _wasInsidePicker = true;
- }
- }
- }
- };
-
- // backward-compatibility for `$.ui.datepicker` shim
- window.__wcf_bc_datePicker = DatePicker;
-
- return DatePicker;
-});
--- /dev/null
+/**
+ * Date picker with time support.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Date/Picker
+ */
+
+import * as Core from '../Core';
+import * as DateUtil from './Util';
+import DomChangeListener from '../Dom/Change/Listener';
+import * as EventHandler from '../Event/Handler';
+import * as Language from '../Language';
+import * as UiAlignment from '../Ui/Alignment';
+import UiCloseOverlay from '../Ui/CloseOverlay';
+import DomUtil from '../Dom/Util';
+
+let _didInit = false;
+let _firstDayOfWeek = 0;
+let _wasInsidePicker = false;
+
+const _data = new Map<HTMLInputElement, DatePickerData>();
+let _input: HTMLInputElement | null = null;
+let _maxDate: Date;
+let _minDate: Date;
+
+const _dateCells: HTMLAnchorElement[] = [];
+let _dateGrid: HTMLUListElement;
+let _dateHour: HTMLSelectElement;
+let _dateMinute: HTMLSelectElement;
+let _dateMonth: HTMLSelectElement;
+let _dateMonthNext: HTMLAnchorElement;
+let _dateMonthPrevious: HTMLAnchorElement;
+let _dateTime: HTMLElement;
+let _dateYear: HTMLSelectElement;
+let _datePicker: HTMLElement | null = null;
+
+/**
+ * Creates the date picker DOM.
+ */
+function createPicker() {
+ if (_datePicker !== null) {
+ return;
+ }
+
+ _datePicker = document.createElement('div');
+ _datePicker.className = 'datePicker';
+ _datePicker.addEventListener('click', event => {
+ event.stopPropagation();
+ });
+
+ const header = document.createElement('header');
+ _datePicker.appendChild(header);
+
+ _dateMonthPrevious = document.createElement('a');
+ _dateMonthPrevious.className = 'previous jsTooltip';
+ _dateMonthPrevious.href = '#';
+ _dateMonthPrevious.setAttribute('role', 'button');
+ _dateMonthPrevious.tabIndex = 0;
+ _dateMonthPrevious.title = Language.get('wcf.date.datePicker.previousMonth');
+ _dateMonthPrevious.setAttribute('aria-label', Language.get('wcf.date.datePicker.previousMonth'));
+ _dateMonthPrevious.innerHTML = '<span class="icon icon16 fa-arrow-left"></span>';
+ _dateMonthPrevious.addEventListener('click', DatePicker.previousMonth);
+ header.appendChild(_dateMonthPrevious);
+
+ const monthYearContainer = document.createElement('span');
+ header.appendChild(monthYearContainer);
+
+ _dateMonth = document.createElement('select');
+ _dateMonth.className = 'month jsTooltip';
+ _dateMonth.title = Language.get('wcf.date.datePicker.month');
+ _dateMonth.setAttribute('aria-label', Language.get('wcf.date.datePicker.month'));
+ _dateMonth.addEventListener('change', changeMonth);
+ monthYearContainer.appendChild(_dateMonth);
+
+ let months = '';
+ const monthNames = Language.get('__monthsShort');
+ for (let i = 0; i < 12; i++) {
+ months += '<option value="' + i + '">' + monthNames[i] + '</option>';
+ }
+ _dateMonth.innerHTML = months;
+
+ _dateYear = document.createElement('select');
+ _dateYear.className = 'year jsTooltip';
+ _dateYear.title = Language.get('wcf.date.datePicker.year');
+ _dateYear.setAttribute('aria-label', Language.get('wcf.date.datePicker.year'));
+ _dateYear.addEventListener('change', changeYear);
+ monthYearContainer.appendChild(_dateYear);
+
+ _dateMonthNext = document.createElement('a');
+ _dateMonthNext.className = 'next jsTooltip';
+ _dateMonthNext.href = '#';
+ _dateMonthNext.setAttribute('role', 'button');
+ _dateMonthNext.tabIndex = 0;
+ _dateMonthNext.title = Language.get('wcf.date.datePicker.nextMonth');
+ _dateMonthNext.setAttribute('aria-label', Language.get('wcf.date.datePicker.nextMonth'));
+ _dateMonthNext.innerHTML = '<span class="icon icon16 fa-arrow-right"></span>';
+ _dateMonthNext.addEventListener('click', DatePicker.nextMonth);
+ header.appendChild(_dateMonthNext);
+
+ _dateGrid = document.createElement('ul');
+ _datePicker.appendChild(_dateGrid);
+
+ const item = document.createElement('li');
+ item.className = 'weekdays';
+ _dateGrid.appendChild(item);
+
+ const weekdays = Language.get('__daysShort');
+ for (let i = 0; i < 7; i++) {
+ let day = i + _firstDayOfWeek;
+ if (day > 6) day -= 7;
+
+ const span = document.createElement('span');
+ span.textContent = weekdays[day];
+ item.appendChild(span);
+ }
+
+ // create date grid
+ for (let i = 0; i < 6; i++) {
+ const row = document.createElement('li');
+ _dateGrid.appendChild(row);
+
+ for (let j = 0; j < 7; j++) {
+ const cell = document.createElement('a');
+ cell.addEventListener('click', click);
+ _dateCells.push(cell);
+
+ row.appendChild(cell);
+ }
+ }
+
+ _dateTime = document.createElement('footer');
+ _datePicker.appendChild(_dateTime);
+
+ _dateHour = document.createElement('select');
+ _dateHour.className = 'hour';
+ _dateHour.title = Language.get('wcf.date.datePicker.hour');
+ _dateHour.setAttribute('aria-label', Language.get('wcf.date.datePicker.hour'));
+ _dateHour.addEventListener('change', formatValue);
+
+ const date = new Date(2000, 0, 1);
+ const timeFormat = Language.get('wcf.date.timeFormat').replace(/:/, '').replace(/[isu]/g, '');
+ let tmp = '';
+ for (let i = 0; i < 24; i++) {
+ date.setHours(i);
+ tmp += '<option value="' + i + '">' + DateUtil.format(date, timeFormat) + "</option>";
+ }
+ _dateHour.innerHTML = tmp;
+
+ _dateTime.appendChild(_dateHour);
+
+ _dateTime.appendChild(document.createTextNode('\u00A0:\u00A0'));
+
+ _dateMinute = document.createElement('select');
+ _dateMinute.className = 'minute';
+ _dateMinute.title = Language.get('wcf.date.datePicker.minute');
+ _dateMinute.setAttribute('aria-label', Language.get('wcf.date.datePicker.minute'));
+ _dateMinute.addEventListener('change', formatValue);
+
+ tmp = '';
+ for (let i = 0; i < 60; i++) {
+ tmp += '<option value="' + i + '">' + (i < 10 ? '0' + i.toString() : i) + '</option>';
+ }
+ _dateMinute.innerHTML = tmp;
+
+ _dateTime.appendChild(_dateMinute);
+
+ document.body.appendChild(_datePicker);
+
+ document.body.addEventListener('focus', maintainFocus, {capture: true});
+}
+
+/**
+ * Initializes the minimum/maximum date range.
+ */
+function initDateRange(element: HTMLInputElement, now: Date, isMinDate: boolean): void {
+ const name = isMinDate ? 'minDate' : 'maxDate';
+ let value = (element.dataset[name] || '').trim();
+
+ if (value.match(/^(\d{4})-(\d{2})-(\d{2})$/)) {
+ // YYYY-mm-dd
+ value = new Date(value).getTime().toString();
+ } else if (value === 'now') {
+ value = now.getTime().toString();
+ } else if (value.match(/^\d{1,3}$/)) {
+ // relative time span in years
+ const date = new Date(now.getTime());
+ date.setFullYear(date.getFullYear() + ~~value * (isMinDate ? -1 : 1));
+
+ value = date.getTime().toString();
+ } else if (value.match(/^datePicker-(.+)$/)) {
+ // element id, e.g. `datePicker-someOtherElement`
+ value = RegExp.$1;
+
+ if (document.getElementById(value) === null) {
+ throw new Error("Reference date picker identified by '" + value + "' does not exists (element id: '" + element.id + "').");
+ }
+ } else if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
+ value = new Date(value).getTime().toString();
+ } else {
+ value = new Date((isMinDate ? 1902 : 2038), 0, 1).getTime().toString();
+ }
+
+ element.dataset[name] = value;
+}
+
+/**
+ * Sets up callbacks and event listeners.
+ */
+function setup() {
+ if (_didInit) return;
+ _didInit = true;
+
+ _firstDayOfWeek = parseInt(Language.get('wcf.date.firstDayOfTheWeek'), 10);
+
+ DomChangeListener.add('WoltLabSuite/Core/Date/Picker', DatePicker.init);
+ UiCloseOverlay.add('WoltLabSuite/Core/Date/Picker', close);
+}
+
+function getDateValue(attributeName: string): Date {
+ let date = _input!.dataset[attributeName] || '';
+ if (date.match(/^datePicker-(.+)$/)) {
+ const referenceElement = document.getElementById(RegExp.$1);
+ if (referenceElement === null) {
+ throw new Error(`Unable to find an element with the id '${RegExp.$1}'.`);
+ }
+ date = referenceElement.dataset.value || '';
+ }
+
+ return new Date(parseInt(date, 10));
+}
+
+/**
+ * Opens the date picker.
+ */
+function open(event: MouseEvent): void {
+ event.preventDefault();
+ event.stopPropagation();
+
+ createPicker();
+
+ const target = event.currentTarget as HTMLInputElement;
+ const input = (target.nodeName === 'INPUT') ? target : target.previousElementSibling as HTMLInputElement;
+ if (input === _input) {
+ close();
+ return;
+ }
+
+ const dialogContent = input.closest('.dialogContent') as HTMLElement;
+ if (dialogContent !== null) {
+ if (!Core.stringToBool(dialogContent.dataset.hasDatepickerScrollListener || '')) {
+ dialogContent.addEventListener('scroll', onDialogScroll);
+ dialogContent.dataset.hasDatepickerScrollListener = '1';
+ }
+ }
+
+ _input = input;
+ const data = _data.get(_input) as DatePickerData;
+ const value = _input.dataset.value!;
+ let date: Date;
+ if (value) {
+ date = new Date(parseInt(value, 10));
+
+ if (date.toString() === 'Invalid Date') {
+ date = new Date();
+ }
+ } else {
+ date = new Date();
+ }
+
+ // set min/max date
+ _minDate = getDateValue('minDate');
+ if (_minDate.getTime() > date.getTime()) {
+ date = _minDate;
+ }
+
+ _maxDate = getDateValue('maxDate');
+
+ if (data.isDateTime) {
+ _dateHour.value = date.getHours().toString();
+ _dateMinute.value = date.getMinutes().toString();
+
+ _datePicker!.classList.add('datePickerTime');
+ } else {
+ _datePicker!.classList.remove('datePickerTime');
+ }
+
+ _datePicker!.classList[(data.isTimeOnly) ? 'add' : 'remove']('datePickerTimeOnly');
+
+ renderPicker(date.getDate(), date.getMonth(), date.getFullYear());
+
+ UiAlignment.set(_datePicker!, _input);
+
+ _input.nextElementSibling!.setAttribute('aria-expanded', 'true');
+
+ _wasInsidePicker = false;
+}
+
+/**
+ * Closes the date picker.
+ */
+function close() {
+ if (_datePicker === null || !_datePicker.classList.contains('active')) {
+ return;
+ }
+
+ _datePicker.classList.remove('active');
+
+ const data = _data.get(_input!) as DatePickerData;
+ if (typeof data.onClose === 'function') {
+ data.onClose();
+ }
+
+ EventHandler.fire('WoltLabSuite/Core/Date/Picker', 'close', {element: _input});
+
+ const sibling = _input!.nextElementSibling as HTMLElement;
+ sibling.setAttribute('aria-expanded', 'false');
+ _input = null;
+}
+
+/**
+ * Updates the position of the date picker in a dialog if the dialog content
+ * is scrolled.
+ */
+function onDialogScroll(event: WheelEvent): void {
+ if (_input === null) {
+ return;
+ }
+
+ const dialogContent = event.currentTarget as HTMLElement;
+
+ const offset = DomUtil.offset(_input);
+ const dialogOffset = DomUtil.offset(dialogContent);
+
+ // check if date picker input field is still (partially) visible
+ if (offset.top + _input.clientHeight <= dialogOffset.top) {
+ // top check
+ close();
+ } else if (offset.top >= dialogOffset.top + dialogContent.offsetHeight) {
+ // bottom check
+ close();
+ } else if (offset.left <= dialogOffset.left) {
+ // left check
+ close();
+ } else if (offset.left >= dialogOffset.left + dialogContent.offsetWidth) {
+ // right check
+ close();
+ } else {
+ UiAlignment.set(_datePicker!, _input);
+ }
+}
+
+/**
+ * Renders the full picker on init.
+ */
+function renderPicker(day: number, month: number, year: number): void {
+ renderGrid(day, month, year);
+
+ // create options for month and year
+ let years = '';
+ for (let i = _minDate.getFullYear(), last = _maxDate.getFullYear(); i <= last; i++) {
+ years += '<option value="' + i + '">' + i + '</option>';
+ }
+ _dateYear.innerHTML = years;
+ _dateYear.value = year.toString();
+
+ _dateMonth.value = month.toString();
+
+ _datePicker!.classList.add('active');
+}
+
+/**
+ * Updates the date grid.
+ */
+function renderGrid(day?: number, month?: number, year?: number): void {
+ const hasDay = (day !== undefined);
+ const hasMonth = (month !== undefined);
+
+ if (typeof day !== 'number') {
+ day = parseInt(day || _dateGrid.dataset.day || '0', 10);
+ }
+ if (typeof month !== 'number') {
+ month = parseInt(month || '0', 10);
+ }
+ if (typeof year !== 'number') {
+ year = parseInt(year || '0', 10);
+ }
+
+ // rebuild cells
+ if (hasMonth || year) {
+ let rebuildMonths = (year !== 0);
+
+ // rebuild grid
+ const fragment = document.createDocumentFragment();
+ fragment.appendChild(_dateGrid);
+
+ if (!hasMonth) {
+ month = parseInt(_dateGrid.dataset.month!, 10);
+ }
+ if (!year) {
+ year = parseInt(_dateGrid.dataset.year!, 10);
+ }
+
+ // check if current selection exceeds min/max date
+ let date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-' + ('0' + day.toString()).slice(-2));
+ if (date < _minDate) {
+ year = _minDate.getFullYear();
+ month = _minDate.getMonth();
+ day = _minDate.getDate();
+
+ _dateMonth.value = month.toString();
+ _dateYear.value = year.toString();
+
+ rebuildMonths = true;
+ } else if (date > _maxDate) {
+ year = _maxDate.getFullYear();
+ month = _maxDate.getMonth();
+ day = _maxDate.getDate();
+
+ _dateMonth.value = month.toString();
+ _dateYear.value = year.toString();
+
+ rebuildMonths = true;
+ }
+
+ date = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+
+ // shift until first displayed day equals first day of week
+ while (date.getDay() !== _firstDayOfWeek) {
+ date.setDate(date.getDate() - 1);
+ }
+
+ // show the last row
+ DomUtil.show(_dateCells[35].parentNode as HTMLElement);
+
+ let selectable: boolean;
+ const comparableMinDate = new Date(_minDate.getFullYear(), _minDate.getMonth(), _minDate.getDate());
+ for (let i = 0; i < 42; i++) {
+ if (i === 35 && date.getMonth() !== month) {
+ // skip the last row if it only contains the next month
+ DomUtil.hide(_dateCells[35].parentNode as HTMLElement);
+
+ break;
+ }
+
+ const cell = _dateCells[i];
+
+ cell.textContent = date.getDate().toString();
+ selectable = (date.getMonth() === month);
+ if (selectable) {
+ if (date < comparableMinDate) selectable = false;
+ else if (date > _maxDate) selectable = false;
+ }
+
+ cell.classList[selectable ? 'remove' : 'add']('otherMonth');
+ if (selectable) {
+ cell.href = '#';
+ cell.setAttribute('role', 'button');
+ cell.tabIndex = 0;
+ cell.title = DateUtil.formatDate(date);
+ cell.setAttribute('aria-label', DateUtil.formatDate(date));
+ }
+
+ date.setDate(date.getDate() + 1);
+ }
+
+ _dateGrid.dataset.month = month.toString();
+ _dateGrid.dataset.year = year.toString();
+
+ _datePicker!.insertBefore(fragment, _dateTime);
+
+ if (!hasDay) {
+ // check if date is valid
+ date = new Date(year, month, day);
+ if (date.getDate() !== day) {
+ while (date.getMonth() !== month) {
+ date.setDate(date.getDate() - 1);
+ }
+
+ day = date.getDate();
+ }
+ }
+
+ if (rebuildMonths) {
+ for (let i = 0; i < 12; i++) {
+ const currentMonth = _dateMonth.children[i] as HTMLOptionElement;
+
+ currentMonth.disabled = (year === _minDate.getFullYear() && +currentMonth.value < _minDate.getMonth()) || (year === _maxDate.getFullYear() && +currentMonth.value > _maxDate.getMonth());
+ }
+
+ const nextMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+ nextMonth.setMonth(nextMonth.getMonth() + 1);
+
+ _dateMonthNext.classList[(nextMonth < _maxDate) ? 'add' : 'remove']('active');
+
+ const previousMonth = new Date(year + '-' + ('0' + (month + 1).toString()).slice(-2) + '-01');
+ previousMonth.setDate(previousMonth.getDate() - 1);
+
+ _dateMonthPrevious.classList[(previousMonth > _minDate) ? 'add' : 'remove']('active');
+ }
+ }
+
+ // update active day
+ if (day) {
+ for (let i = 0; i < 35; i++) {
+ const cell = _dateCells[i];
+
+ cell.classList[(!cell.classList.contains('otherMonth') && +cell.textContent! === day) ? 'add' : 'remove']('active');
+ }
+
+ _dateGrid.dataset.day = day.toString();
+ }
+
+ formatValue();
+}
+
+/**
+ * Sets the visible and shadow value
+ */
+function formatValue(): void {
+ const data = _data.get(_input!) as DatePickerData;
+ let date: Date;
+
+ if (Core.stringToBool(_input!.dataset.empty || '')) {
+ return;
+ }
+
+ if (data.isDateTime) {
+ date = new Date(
+ +_dateGrid.dataset.year!,
+ +_dateGrid.dataset.month!,
+ +_dateGrid.dataset.day!,
+ +_dateHour.value,
+ +_dateMinute.value,
+ );
+ } else {
+ date = new Date(
+ +_dateGrid.dataset.year!,
+ +_dateGrid.dataset.month!,
+ +_dateGrid.dataset.day!,
+ );
+ }
+
+ DatePicker.setDate(_input!, date);
+}
+
+/**
+ * Handles changes to the month select element.
+ */
+function changeMonth(event: Event): void {
+ const target = event.currentTarget as HTMLSelectElement;
+ renderGrid(undefined, +target.value);
+}
+
+/**
+ * Handles changes to the year select element.
+ */
+function changeYear(event: Event): void {
+ const target = event.currentTarget as HTMLSelectElement;
+ renderGrid(undefined, undefined, +target.value);
+}
+
+/**
+ * Handles clicks on an individual day.
+ */
+function click(event: MouseEvent): void {
+ event.preventDefault();
+
+ const target = event.currentTarget as HTMLAnchorElement;
+ if (target.classList.contains('otherMonth')) {
+ return;
+ }
+
+ _input!.dataset.empty = 'false';
+
+ renderGrid(+target.textContent!);
+
+ const data = _data.get(_input!) as DatePickerData;
+ if (!data.isDateTime) {
+ close();
+ }
+}
+
+/**
+ * Validates given element or id if it represents an active date picker.
+ */
+function getElement(element: InputElementOrString): HTMLInputElement {
+ if (typeof element === 'string') {
+ element = document.getElementById(element) as HTMLInputElement;
+ }
+
+ if (!(element instanceof HTMLInputElement) || !element.classList.contains('inputDatePicker') || !_data.has(element)) {
+ throw new Error("Expected a valid date picker input element or id.");
+ }
+
+ return element;
+}
+
+function maintainFocus(event: FocusEvent): void {
+ if (_datePicker === null || !_datePicker.classList.contains('active')) {
+ return;
+ }
+
+ if (!_datePicker.contains(event.target as HTMLElement)) {
+ if (_wasInsidePicker) {
+ const sibling = _input!.nextElementSibling as HTMLElement;
+ sibling.focus();
+ _wasInsidePicker = false;
+ } else {
+ _datePicker!.querySelector<HTMLElement>('.previous')!.focus();
+ }
+ } else {
+ _wasInsidePicker = true;
+ }
+}
+
+const DatePicker = {
+ /**
+ * Initializes all date and datetime input fields.
+ */
+ init(): void {
+ setup();
+
+ const now = new Date();
+ document.querySelectorAll<HTMLInputElement>('input[type="date"]:not(.inputDatePicker), input[type="datetime"]:not(.inputDatePicker)').forEach(element => {
+ element.classList.add('inputDatePicker');
+ element.readOnly = true;
+
+ const isDateTime = (element.type === 'datetime');
+ const isTimeOnly = isDateTime && Core.stringToBool(element.dataset.timeOnly || '');
+ const disableClear = Core.stringToBool(element.dataset.disableClear || '');
+ const ignoreTimezone = isDateTime && Core.stringToBool(element.dataset.ignoreTimezone || '');
+ const isBirthday = element.classList.contains('birthday');
+
+ element.dataset.isDateTime = isDateTime ? 'true' : 'false';
+ element.dataset.isTimeOnly = isTimeOnly ? 'true' : 'false';
+
+ // convert value
+ let date: Date | null = null;
+ let value = element.value;
+
+ // ignore the timezone, if the value is only a date (YYYY-MM-DD)
+ const isDateOnly = /^\d+-\d+-\d+$/.test(value);
+
+ if (value) {
+ if (isTimeOnly) {
+ date = new Date();
+ const tmp = value.split(':');
+ date.setHours(+tmp[0], +tmp[1]);
+ } else {
+ if (ignoreTimezone || isBirthday || isDateOnly) {
+ let timezoneOffset = new Date(value).getTimezoneOffset();
+ let timezone = (timezoneOffset > 0) ? '-' : '+'; // -120 equals GMT+0200
+ timezoneOffset = Math.abs(timezoneOffset);
+
+ const hours = (Math.floor(timezoneOffset / 60)).toString();
+ const minutes = (timezoneOffset % 60).toString();
+ timezone += (hours.length === 2) ? hours : '0' + hours;
+ timezone += ':';
+ timezone += (minutes.length === 2) ? minutes : '0' + minutes;
+
+ if (isBirthday || isDateOnly) {
+ value += 'T00:00:00' + timezone;
+ } else {
+ value = value.replace(/[+-][0-9]{2}:[0-9]{2}$/, timezone);
+ }
+ }
+
+ date = new Date(value);
+ }
+
+ const time = date.getTime();
+
+ // check for invalid dates
+ if (isNaN(time)) {
+ value = '';
+ } else {
+ element.dataset.value = time.toString();
+ const format = (isTimeOnly) ? 'formatTime' : ('formatDate' + (isDateTime ? 'Time' : ''));
+ value = DateUtil[format](date);
+ }
+ }
+
+ const isEmpty = (value.length === 0);
+
+ // handle birthday input
+ if (isBirthday) {
+ element.dataset.minDate = '120';
+
+ // do not use 'now' here, all though it makes sense, it causes bad UX
+ element.dataset.maxDate = new Date().getFullYear() + '-12-31';
+ } else {
+ if (element.min) {
+ element.dataset.minDate = element.min;
+ }
+ if (element.max) {
+ element.dataset.maxDate = element.max;
+ }
+ }
+
+ initDateRange(element, now, true);
+ initDateRange(element, now, false);
+
+ if ((element.dataset.minDate || '') === (element.dataset.maxDate || '')) {
+ throw new Error("Minimum and maximum date cannot be the same (element id '" + element.id + "').");
+ }
+
+ // change type to prevent browser's datepicker to trigger
+ element.type = 'text';
+ element.value = value;
+ element.dataset.empty = isEmpty ? 'true' : 'false';
+
+ const placeholder = element.dataset.placeholder || '';
+ if (placeholder) {
+ element.placeholder = placeholder;
+ }
+
+ // add a hidden element to hold the actual date
+ const shadowElement = document.createElement('input');
+ shadowElement.id = element.id + 'DatePicker';
+ shadowElement.name = element.name;
+ shadowElement.type = 'hidden';
+
+ if (date !== null) {
+ if (isTimeOnly) {
+ shadowElement.value = DateUtil.format(date, 'H:i');
+ } else if (ignoreTimezone) {
+ shadowElement.value = DateUtil.format(date, 'Y-m-dTH:i:s');
+ } else {
+ shadowElement.value = DateUtil.format(date, (isDateTime) ? 'c' : 'Y-m-d');
+ }
+ }
+
+ element.parentNode!.insertBefore(shadowElement, element);
+ element.removeAttribute('name');
+
+ element.addEventListener('click', open);
+
+ let clearButton: HTMLAnchorElement | null = null;
+ if (!element.disabled) {
+ // create input addon
+ const container = document.createElement('div');
+ container.className = 'inputAddon';
+
+ clearButton = document.createElement('a');
+
+ clearButton.className = 'inputSuffix button jsTooltip';
+ clearButton.href = '#';
+ clearButton.setAttribute('role', 'button');
+ clearButton.tabIndex = 0;
+ clearButton.title = Language.get('wcf.date.datePicker');
+ clearButton.setAttribute('aria-label', Language.get('wcf.date.datePicker'));
+ clearButton.setAttribute('aria-haspopup', 'true');
+ clearButton.setAttribute('aria-expanded', 'false');
+ clearButton.addEventListener('click', open);
+ container.appendChild(clearButton);
+
+ let icon = document.createElement('span');
+ icon.className = 'icon icon16 fa-calendar';
+ clearButton.appendChild(icon);
+
+ element.parentNode!.insertBefore(container, element);
+ container.insertBefore(element, clearButton);
+
+ if (!disableClear) {
+ const button = document.createElement('a');
+ button.className = 'inputSuffix button';
+ button.addEventListener('click', this.clear.bind(this, element));
+ if (isEmpty) button.style.setProperty('visibility', 'hidden', '');
+
+ container.appendChild(button);
+
+ icon = document.createElement('span');
+ icon.className = 'icon icon16 fa-times';
+ button.appendChild(icon);
+ }
+ }
+
+ // check if the date input has one of the following classes set otherwise default to 'short'
+ const knownClasses = ['tiny', 'short', 'medium', 'long'];
+ let hasClass = false;
+ for (let j = 0; j < 4; j++) {
+ if (element.classList.contains(knownClasses[j])) {
+ hasClass = true;
+ }
+ }
+
+ if (!hasClass) {
+ element.classList.add('short');
+ }
+
+ _data.set(element, {
+ clearButton,
+ shadow: shadowElement,
+
+ disableClear,
+ isDateTime,
+ isEmpty,
+ isTimeOnly,
+ ignoreTimezone,
+
+ onClose: null,
+ });
+ });
+ },
+
+ /**
+ * Shows the previous month.
+ */
+ previousMonth(event: MouseEvent): void {
+ event.preventDefault();
+
+ if (_dateMonth.value === '0') {
+ _dateMonth.value = '11';
+ _dateYear.value = (+_dateYear.value - 1).toString();
+ } else {
+ _dateMonth.value = (+_dateMonth.value - 1).toString();
+ }
+
+ renderGrid(undefined, +_dateMonth.value, +_dateYear.value);
+ },
+
+ /**
+ * Shows the next month.
+ */
+ nextMonth(event: MouseEvent): void {
+ event.preventDefault();
+
+ if (_dateMonth.value === '11') {
+ _dateMonth.value = '0';
+ _dateYear.value = (+_dateYear.value + 1).toString();
+ } else {
+ _dateMonth.value = (+_dateMonth.value + 1).toString();
+ }
+
+ renderGrid(undefined, +_dateMonth.value, +_dateYear.value);
+ },
+
+ /**
+ * Returns the current Date object or null.
+ */
+ getDate(element: InputElementOrString): Date | null {
+ element = getElement(element);
+
+ const value = element.dataset.value || '';
+ if (value) {
+ return new Date(+value);
+ }
+
+ return null;
+ },
+
+ /**
+ * Sets the date of given element.
+ *
+ * @param {(HTMLInputElement|string)} element input element or id
+ * @param {Date} date Date object
+ */
+ setDate(element: InputElementOrString, date: Date): void {
+ element = getElement(element);
+ const data = _data.get(element) as DatePickerData;
+
+ element.dataset.value = date.getTime().toString();
+
+ let format = '';
+ let value: string;
+ if (data.isDateTime) {
+ if (data.isTimeOnly) {
+ value = DateUtil.formatTime(date);
+ format = 'H:i';
+ } else if (data.ignoreTimezone) {
+ value = DateUtil.formatDateTime(date);
+ format = 'Y-m-dTH:i:s';
+ } else {
+ value = DateUtil.formatDateTime(date);
+ format = 'c';
+ }
+ } else {
+ value = DateUtil.formatDate(date);
+ format = 'Y-m-d';
+ }
+
+ element.value = value;
+ data.shadow.value = DateUtil.format(date, format);
+
+ // show clear button
+ if (!data.disableClear) {
+ data.clearButton!.style.removeProperty('visibility');
+ }
+ },
+
+ /**
+ * Returns the current value.
+ */
+ getValue(element: InputElementOrString): string {
+ element = getElement(element);
+ const data = _data.get(element);
+
+ if (data) {
+ return data.shadow.value;
+ }
+
+ return '';
+ },
+
+ /**
+ * Clears the date value of given element.
+ */
+ clear(element: InputElementOrString): void {
+ element = getElement(element);
+ const data = _data.get(element) as DatePickerData;
+
+ element.removeAttribute('data-value');
+ element.value = '';
+
+ if (!data.disableClear) {
+ data.clearButton!.style.setProperty('visibility', 'hidden', '');
+ }
+
+ data.isEmpty = true;
+ data.shadow.value = '';
+ },
+
+ /**
+ * Reverts the date picker into a normal input field.
+ */
+ destroy(element: InputElementOrString): void {
+ element = getElement(element);
+ const data = _data.get(element) as DatePickerData;
+
+ const container = element.parentNode as HTMLElement;
+ container.parentNode!.insertBefore(element, container);
+ container.remove();
+
+ element.type = 'date' + (data.isDateTime ? 'time' : '');
+ element.name = data.shadow.name;
+ element.value = data.shadow.value;
+
+ element.removeAttribute('data-value');
+ element.removeEventListener('click', open);
+ data.shadow.remove();
+
+ element.classList.remove('inputDatePicker');
+ element.readOnly = false;
+ _data.delete(element);
+ },
+
+ /**
+ * Sets the callback invoked on picker close.
+ */
+ setCloseCallback(element: InputElementOrString, callback: Callback): void {
+ element = getElement(element);
+ _data.get(element)!.onClose = callback;
+ },
+};
+
+// backward-compatibility for `$.ui.datepicker` shim
+window.__wcf_bc_datePicker = DatePicker;
+
+export = DatePicker;
+
+type InputElementOrString = HTMLInputElement | string;
+
+type Callback = () => void;
+
+interface DatePickerData {
+ clearButton: HTMLAnchorElement | null;
+ shadow: HTMLInputElement;
+
+ disableClear: boolean;
+ isDateTime: boolean;
+ isEmpty: boolean;
+ isTimeOnly: boolean;
+ ignoreTimezone: boolean;
+
+ onClose: Callback | null;
+}
const _callbackList = new CallbackList();
let _hot = false;
-export = {
+const DomChangeListener = {
/**
* @see CallbackList.add
*/
_hot = false;
}
},
-}
+};
+
+export = DomChangeListener
* @param {Element} referenceElement reference element
* @param {Object<string, *>} options list of options to alter the behavior
*/
-export function set(element: HTMLElement, referenceElement: HTMLElement, options: AlignmentOptions): void {
+export function set(element: HTMLElement, referenceElement: HTMLElement, options?: AlignmentOptions): void {
options = Core.extend({
// offset to reference element
verticalOffset: 0,
vertical: 'bottom',
// allow flipping over axis, possible values: both, horizontal, vertical and none
allowFlip: 'both',
- }, options) as AlignmentOptions;
+ }, options || {}) as AlignmentOptions;
if (!Array.isArray(options.pointerClassNames) || options.pointerClassNames.length !== (options.pointer ? 1 : 2)) {
options.pointerClassNames = [];