UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

422 lines (353 loc) 12 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2014 1&1 Internet AG, Germany, http://www.1und1.de License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Alexander Steitz (aback) ************************************************************************ */ /** * This is a date picker widget used to combine an input element with a calendar widget * to select a date. The calendar itself is opened as popup to save visual space. * * <h2>Markup</h2> * Each Date Picker widget is connected to an existing input element. * * <h2>CSS Classes</h2> * <table> * <thead> * <tr> * <td>Class Name</td> * <td>Applied to</td> * <td>Description</td> * </tr> * </thead> * <tbody> * <tr> * <td><code>qx-datepicker</code></td> * <td>Input element</td> * <td>Identifies the date picker widget</td> * </tr> * <tr> * <td><code>qx-datepicker-icon</code></td> * <td>Icon element</td> * <td>Identifies the (if configured) image element to open the date picker</td> * </tr> * </tbody> * </table> * * @require(qx.module.Template) * * @group (Widget) */ qx.Bootstrap.define('qx.ui.website.DatePicker', { extend : qx.ui.website.Widget, statics : { /** List of valid positions to check against */ __validPositions : null, /** * *format* * * Function which is provided with a JavaScript Date object instance. You can provide * an own format function to manipulate the value which is set to the associated input element. * * Default value: * <pre>function(date) { return date.toLocaleDateString(); }</pre> * * *readonly* * * Boolean value to control if the connected input element is read-only. * * Default value: * <pre>true</pre> * * *icon* * * Path to an icon which will be placed next to the input element as additional opener. If configured * a necessary <code>img</code> element is created and equipped with the <code>qx-datepicker-icon</code> * CSS class to style it. * * Default value: * <pre>null</pre> * * *mode* * * Which control should trigger showing the date picker. * Possible values are <code>input</code>, <code>icon</code>, <code>both</code>. * * Default value: * <pre>input</pre> * * *position* * * Position of the calendar popup from the point of view of the <code>INPUT</code> element. * Possible values are * * * <code>top-left</code> * * <code>top-center</code> * * <code>top-right</code> * * <code>bottom-left</code> * * <code>bottom-center</code> * * <code>bottom-right</code> * * <code>left-top</code> * * <code>left-middle</code> * * <code>left-bottom</code> * * <code>right-top</code> * * <code>right-middle</code> * * <code>right-bottom</code> * * Default value: * <pre>bottom-left</pre> */ _config : { format : function(date) { return date.toLocaleDateString(); }, readonly : true, icon : null, mode : 'input', position : 'bottom-left' }, /** * Factory method which converts the current collection into a collection of * Date Picker widgets. Therefore, an initialization process needs to be done which * can be configured with some parameter. * * @param date {Date?null} The initial Date of the calendar. * @return {qx.ui.website.DatePicker} A new date picker collection. * @attach {qxWeb} */ datepicker : function(date) { var datepicker = new qx.ui.website.DatePicker(this); datepicker.init(date); return datepicker; } }, construct : function(selector, context) { this.base(arguments, selector, context); }, members : { _calendarId: null, _iconId: null, _uniqueId: null, /** * Get the associated calendar widget * @return {qx.ui.website.Calendar} calendar widget instance */ getCalendar : function() { var calendarCollection = qxWeb(); calendarCollection = calendarCollection.concat(qxWeb('div#' + this._calendarId)); return calendarCollection; }, // overridden /** * Initializes the date picker widget * * @param date {Date} A JavaScript Date object to set the current date * @return {Boolean} <code>true</code> if the widget has been initialized */ init : function(date) { if (!this.base(arguments)) { return false; } var uniqueId = Math.round(Math.random() * 10000); this._uniqueId = uniqueId; this.__setReadOnly(this); this.__setIcon(this); this.__addInputListener(this); var calendarId = 'datepicker-calendar-' + uniqueId; var calendar = qxWeb.create('<div id="' + calendarId + '"></div>').calendar(); calendar.on('tap', this._onCalendarTap); calendar.appendTo(document.body).hide(); // create the connection between the date picker and the corresponding calendar widget this._calendarId = calendarId; // grab tap events at the body element to be able to hide the calender popup // if the user taps outside var bodyElement = qxWeb.getDocument(this).body; qxWeb(bodyElement).on('tap', this._onBodyTap, this); // react on date selection calendar.on('changeValue', this._calendarChangeValue, this); if (date !== undefined) { calendar.setValue(date); } return true; }, // overridden render : function() { this.getCalendar().render(); this.__setReadOnly(this); this.__setIcon(this); this.__addInputListener(this); this.setEnabled(this.getEnabled()); return this; }, // overridden setConfig : function(name, config) { if (name === 'position') { var validPositions = qx.ui.website.DatePicker.__validPositions; if (validPositions.indexOf(config) === -1) { throw new Error('Wrong config value for "position"! ' + 'Only the values "' + validPositions.join('", "') + '" are supported!'); } } this.base(arguments, name, config); return this; }, /** * Listener which handles clicks/taps on the associated input element and * opens / hides the calendar. * * @param e {Event} tap event */ _onTap : function(e) { if (!this.getEnabled()) { return; } var calendar = this.getCalendar(); if (calendar.getStyle('display') == 'none') { // set position to make sure the width of the DOM element is correct - otherwise the DOM // element would be as wide as the parent (e.g. the body element). This would mess up the // positioning with 'placeTo' calendar.setStyle('position', 'absolute').show().placeTo(this, this.getConfig('position')); } else { calendar.hide(); } }, /** * Stop tap events from reaching the body so the calendar won't close * @param e {Event} Tap event */ _onCalendarTap : function(e) { e.stopPropagation(); }, /** * Listener to the body element to be able to hide the calendar if the user clicks * or taps outside the calendar. * * @param e {Event} tap event */ _onBodyTap : function(e) { var target = qxWeb(e.getTarget()); // fast check for tap on the connected input field if (this.length > 0 && target.length > 0 && this[0] == target[0]) { return; } // fast check for tap on the configured icon if (this.getConfig('icon') !== null) { var icon = qxWeb('#' + this._iconId); if (icon.length > 0 && target.length > 0 && icon[0] == target[0]) { return; } } // otherwise check if the target is a child of the (rendered) calendar if (this.getCalendar().isRendered()) { var tappedCol = qxWeb(e.getTarget()); if (tappedCol.isChildOf(this.getCalendar()) === false) { this.getCalendar().hide(); } } }, /** * Listens to value selection of the calendar, Whenever the user selected a day * we write it back to the input element and hide the calendar. * * The format of the date can be controlled with the 'format' config function * * @param e {Event} selected date value */ _calendarChangeValue : function(e) { var formattedValue = this.getConfig('format').call(this, e); this.setValue(formattedValue); this.getCalendar().hide(); }, /** * Helper method to set the readonly status on the input element * * @param collection {qxWeb} collection to work on */ __setReadOnly : function(collection) { if (collection.getConfig('readonly')) { collection.setAttribute('readonly', 'readonly'); } else { collection.removeAttribute('readonly'); } }, /** * Helper method to add / remove an icon next to the input element * * @param collection {qxWeb} collection to work on */ __setIcon : function(collection) { var icon; if (collection.getConfig('icon') === null) { icon = collection.getNext('img#' + collection._iconId); if (icon.length === 1) { icon.off('tap', this._onTap, collection); icon.remove(); } } else { var iconId = 'datepicker-icon-' + collection._uniqueId; // check if there is already an icon if (collection._iconId == undefined) { collection._iconId = iconId; icon = qxWeb.create('<img>'); icon.setAttributes({ id: iconId, src: collection.getConfig('icon') }); icon.addClass(this.getCssPrefix() + '-icon'); var openingMode = collection.getConfig('mode'); if (openingMode === 'icon' || openingMode === 'both') { if (!icon.hasListener('tap', this._onTap, collection)) { icon.on('tap', this._onTap, collection); } } icon.insertAfter(collection); } } }, /** * Helper method to add a listener to the connected input element * if the configured mode is set. * * @param collection {qxWeb} collection to work on */ __addInputListener : function(collection) { if (collection.getConfig('mode') === 'icon') { collection.off('tap', collection._onTap); } else { if(!collection.hasListener('tap', collection._onTap)) { collection.on('tap', collection._onTap); } } }, // overridden dispose : function() { this.removeAttribute('readonly'); this.getNext('img#' + this._iconId).remove(); this.off('tap', this._onTap); var bodyElement = qxWeb.getDocument(this).body; qxWeb(bodyElement).off('tap', this._onBodyTap, this); this.getCalendar().off('changeValue', this._calendarChangeValue, this) .off('tap', this._onCalendarTap); var calendar = qxWeb('div#' + this._calendarId); calendar.remove(); calendar.dispose(); return this.base(arguments); } }, defer : function(statics) { qxWeb.$attach({datepicker : statics.datepicker}); statics.__validPositions = [ 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right', 'left-top', 'left-middle', 'left-bottom', 'right-top', 'right-middle', 'right-bottom' ]; } });