Convert `Date/Picker` to TypeScript
authorAlexander Ebert <ebert@woltlab.com>
Thu, 22 Oct 2020 23:16:03 +0000 (01:16 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Wed, 28 Oct 2020 11:39:33 +0000 (12:39 +0100)
global.d.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Date/Picker.js
wcfsetup/install/files/js/WoltLabSuite/Core/Dom/Change/Listener.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Alignment.js
wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker.js [deleted file]
wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker.ts [new file with mode: 0644]
wcfsetup/install/files/ts/WoltLabSuite/Core/Dom/Change/Listener.ts
wcfsetup/install/files/ts/WoltLabSuite/Core/Ui/Alignment.ts

index 66ac5f7551d949c433260397bb8ecb7352eb141a..77c90443613578ca00e036f81255e46ebbdf35be 100644 (file)
@@ -1,3 +1,4 @@
+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';
@@ -14,8 +15,9 @@ declare global {
 
     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 {
index e1b0910b976305b86ffde964ac1c63feb1b1a416..f9d4d0109d17c5c4d25568e08c57cde57680b27f 100644 (file)
 /**
  * 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;
@@ -78,44 +577,47 @@ define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'Obj
                         }
                         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';
@@ -132,42 +634,44 @@ define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'Obj
                 }
                 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;
                     }
@@ -176,515 +680,68 @@ define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'Obj
                     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);
@@ -712,13 +769,10 @@ define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'Obj
         },
         /**
          * 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;
             }
@@ -726,83 +780,44 @@ define(['DateUtil', 'Dom/Traverse', 'Dom/Util', 'EventHandler', 'Language', 'Obj
         },
         /**
          * 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;
index 15771affc88d3e87d75c50f61da349424a25385a..fc76a104ba04f3e6b704db3c6c6671d3a888ae0a 100644 (file)
@@ -16,7 +16,7 @@ define(["require", "exports", "../../CallbackList"], function (require, exports,
     CallbackList_1 = __importDefault(CallbackList_1);
     const _callbackList = new CallbackList_1.default();
     let _hot = false;
-    return {
+    const DomChangeListener = {
         /**
          * @see CallbackList.add
          */
@@ -43,4 +43,5 @@ define(["require", "exports", "../../CallbackList"], function (require, exports,
             }
         },
     };
+    return DomChangeListener;
 });
index a6b6dbc8f79fd1d9e8218667835f479377678114..66c613f198cd7fc3b0deba3bc20e66fb4778754a 100644 (file)
@@ -150,7 +150,7 @@ define(["require", "exports", "../Core", "../Dom/Traverse", "../Dom/Util", "../L
             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 = [];
         }
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker.js
deleted file mode 100644 (file)
index fa2a5ba..0000000
+++ /dev/null
@@ -1,983 +0,0 @@
-/**
- * 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;
-});
diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Date/Picker.ts
new file mode 100644 (file)
index 0000000..6a9b700
--- /dev/null
@@ -0,0 +1,978 @@
+/**
+ * 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;
+}
index 230fbb4216f22e4faf554d9d15f55cc293e1cc25..95dbaf2e082052b6ad60cc957c433c77aa531886 100644 (file)
@@ -14,7 +14,7 @@ import CallbackList from '../../CallbackList';
 const _callbackList = new CallbackList();
 let _hot = false;
 
-export = {
+const DomChangeListener = {
   /**
    * @see CallbackList.add
    */
@@ -41,4 +41,6 @@ export = {
       _hot = false;
     }
   },
-}
+};
+
+export = DomChangeListener
index 00d37cf38e51c389b38e35200a805c7ff61df892..393d1cb5d426c4a352a17fd0a5fd1d49e92c82e1 100644 (file)
@@ -139,7 +139,7 @@ function tryAlignmentVertical(alignment: VerticalAlignment, elDimensions, refDim
  * @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,
@@ -154,7 +154,7 @@ export function set(element: HTMLElement, referenceElement: HTMLElement, options
     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 = [];