diff options
Diffstat (limited to 'apps/system/js/value_selector/date_picker.js')
-rw-r--r-- | apps/system/js/value_selector/date_picker.js | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/apps/system/js/value_selector/date_picker.js b/apps/system/js/value_selector/date_picker.js new file mode 100644 index 0000000..9cdd484 --- /dev/null +++ b/apps/system/js/value_selector/date_picker.js @@ -0,0 +1,568 @@ +/** + * DatePicker is a html/js "widget" which will display + * all the days of a given month and allow selection of + * one specific day. It also implements controls to travel + * between months and jump into arbitrary time. + * + * The DatePicker itself contains no UI for the controls. + * + * Example usage: + * + * // the container will have elements for the month + * // added and removed from it. + * var picker = new DatePicker(container); + * + * // EVENTS: + * + * // called when the user clicks a day in the calendar. + * picker.onvaluechange = function(date) {} + * + * // called when the month of the calendar changes. + * // NOTE: at this time this can only happen programmatically + * // so there is only for control flow. + * picker.onmonthchange = function(date) {} + * + * // display a given year/month/date on the calendar the month + * // is zero based just like the JS date constructor. + * picker.display(2012, 0, 2); + * + * // move to the next month. + * picker.next(); + * + * // move to the previous month + * picker.previous(); + * + */ +var DatePicker = (function() { + 'use strict'; + + const SELECTED = 'selected'; + + var Calc = { + + NEXT_MONTH: 'next-month', + + OTHER_MONTH: 'other-month', + + PRESENT: 'present', + + FUTURE: 'future', + + PAST: 'past', + + get today() { + return new Date(); + }, + + daysInWeek: function() { + //XXX: We need to localize this... + return 7; + }, + + /** + * Checks is given date is today. + * + * @param {Date} date compare. + * @return {Boolean} true when today. + */ + isToday: function(date) { + return Calc.isSameDate(date, Calc.today); + }, + + /** + * Checks if two date objects occur + * on the same date (in the same month, year, day). + * Disregards time. + * + * @param {Date} first date. + * @param {Date} second date. + * @return {Boolean} true when they are the same date. + */ + isSameDate: function(first, second) { + return first.getMonth() == second.getMonth() && + first.getDate() == second.getDate() && + first.getFullYear() == second.getFullYear(); + }, + + /** + * Returns an identifier for a specific + * date in time for a given date + * + * @param {Date} date to get id for. + * @return {String} identifier. + */ + getDayId: function(date) { + return [ + date.getFullYear(), + date.getMonth(), + date.getDate() + ].join('-'); + }, + + /** + * Returns a date object from + * a string id for a date. + * + * @param {String} id identifier for date. + * @return {Date} date output. + */ + dateFromId: function(id) { + var parts = id.split('-'); + return new Date(parts[0], parts[1], parts[2]); + }, + + createDay: function(date, day, month, year) { + return new Date( + typeof year !== 'undefined' ? year : date.getFullYear(), + typeof month !== 'undefined' ? month : date.getMonth(), + typeof day !== 'undefined' ? day : date.getDate() + ); + }, + + /** + * Finds localized week start date of given date. + * + * @param {Date} date any day the week. + * @return {Date} first date in the week of given date. + */ + getWeekStartDate: function(date) { + var currentDay = date.getDay(); + var startDay = date.getDate() - currentDay; + + return Calc.createDay(date, startDay); + }, + + getWeekEndDate: function(date) { + // TODO: There are localization problems + // with this approach as we assume a 7 day week. + var start = Calc.getWeekStartDate(date); + start.setDate(start.getDate() + 7); + start.setMilliseconds(-1); + + return start; + }, + + /** + * Returns an array of dates objects. + * Inclusive. First and last are + * the given instances. + * + * @param {Date} start starting day. + * @param {Date} end ending day. + * @param {Boolean} includeTime include times start/end ? + */ + daysBetween: function(start, end, includeTime) { + if (!(start instanceof Date)) { + throw new Error('start date must be an instanceof Date'); + } + + if (!(end instanceof Date)) { + throw new Error('end date must be an instanceof Date'); + } + + if (start > end) { + var tmp = end; + end = start; + start = tmp; + tmp = null; + } + + var list = []; + var last = start.getDate(); + var cur; + + // for infinite loop protection. + var max = 500; + var macInc = 0; + + while (macInc++ < max) { + var next = new Date( + start.getFullYear(), + start.getMonth(), + ++last + ); + + if (next > end) { + throw new Error( + 'sanity fails next is greater then end' + ); + } + + if (!Calc.isSameDate(next, end)) { + list.push(next); + continue; + } + + break; + } + + if (includeTime) { + list.unshift(start); + list.push(end); + } else { + list.unshift(this.createDay(start)); + list.push(this.createDay(end)); + } + + return list; + }, + + /** + * Checks if date is in the past + * + * @param {Date} date to check. + * @return {Boolean} true when date is in the past. + */ + isPast: function(date) { + return (date.valueOf() < Calc.today.valueOf()); + }, + + /** + * Checks if date is in the future + * + * @param {Date} date to check. + * @return {Boolean} true when date is in the future. + */ + isFuture: function(date) { + return !Calc.isPast(date); + }, + + /** + * Based on the input date + * will return one of the following states + * + * past, present, future + * + * @param {Date} day for compare. + * @param {Date} month comparison month. + * @return {String} state. + */ + relativeState: function(day, month) { + var states; + //var today = Calc.today; + + // 1. the date is today (real time) + if (Calc.isToday(day)) { + return Calc.PRESENT; + } + + // 2. the date is in the past (real time) + if (Calc.isPast(day)) { + states = Calc.PAST; + // 3. the date is in the future (real time) + } else { + states = Calc.FUTURE; + } + + // 4. the date is not in the current month (relative time) + if (day.getMonth() !== month.getMonth()) { + states += ' ' + Calc.OTHER_MONTH; + } + + return states; + } + + }; + + /* expose calc */ + DatePicker.Calc = Calc; + + /** + * Initialize a date picker widget. + * + * @param {HTMLELement} element target of widget creation. + */ + function DatePicker(element) { + this.element = element; + // default time is set so next/previous work + // but we do not render the initial display here. + this._position = new Date(); + + // register events + element.addEventListener('click', this); + + //XXX: When the document is localized again + // we must also re-render the month because + // the week days may have changed? + // This will only happen when we change timezones + // unless we add this information to the locales. + } + + DatePicker.prototype = { + + /** + * Internal value not exposed so we can fire events + * when the getter/setter's are used. + * + * @type Date + */ + _value: null, + + SELECTED: 'selected', + + /** + * Gets current value + * + * @return {Null|Date} date or null. + */ + get value() { + return this._value; + }, + + /** + * Sets the current value of the date picker. + * When value differs from the currently set the + * `onvaluechange` event will be fired with the new/old value. + */ + set value(value) { + var old = this._value; + if (old !== value) { + this._value = value; + this._clearSelectedDay(value); + this.onvaluechange(value, old); + } + }, + + /** + * Clears the currently selected date of its 'selected' class. + * @private + */ + _clearSelectedDay: function(value) { + var target = this.element.querySelector('.' + SELECTED); + if (target) { + target.classList.remove(SELECTED); + } + }, + + handleEvent: function(e) { + switch (e.type) { + case 'click': + var target = e.target; + //XXX: if the html of the date elements changes + // this may also need to be altered as it + // assumes that there is no nesting of elements. + if (target.dataset.date) { + var date = Calc.dateFromId(target.dataset.date); + // order here is important as setting value will + // clear all the past selected dates... + this.value = date; + this._position = date; + // must come after setting selected date + target.classList.add(SELECTED); + } + break; + } + }, + + /** + * Getter is used for date normalization. + */ + get year() { + return this._position.getFullYear(); + }, + + /** + * Getter is used for date normalization. + */ + get month() { + return this._position.getMonth(); + }, + + get date() { + return this._position.getDate(); + }, + + /** + * Find the number of days in the given month/year. + * Month is zero based like the JS date constructor. + * + * @param {Numeric} year year value. + * @param {Numeric} month month value. + * @return {Numeric} number of days in month. + */ + _daysInMonth: function(year, month) { + var end = new Date(year, month + 1); + end.setMilliseconds(-1); + return end.getDate(); + }, + + /** + * Build the container for a day element. + * Each element has classes added to it based + * on what date it is created for. + * + * _today_ is based on today's actual date. + * Each date element also contains a data-date attribute + * with its current date as a string represented in + * the following format: "yyyy-mm-dd". + * + * Possible classes: + * - past + * - present (today) + * - future + * - other-month (day of another month but falls within same week) + * + * @param {Date} date date desired. + * @return {HTMLElement} dom element for day. + */ + _renderDay: function(date) { + var dayContainer = document.createElement('li'); + var dayEl = document.createElement('span'); + + dayContainer.className = Calc.relativeState( + date, + this._position + ); + + dayEl.dataset.date = Calc.getDayId(date); + dayEl.textContent = date.getDate(); + + dayContainer.appendChild(dayEl); + + return dayContainer; + }, + + /** + * Renders a set of dates and returns an ol element + * containing each date. + * + * @private + * @param {Array[Date]} dates array of dates. + * @return {HTMLELement} container for week. + */ + _renderWeek: function(dates) { + var container = document.createElement('ol'); + var i = 0; + var len = dates.length; + + for (; i < len; i++) { + container.appendChild( + this._renderDay(dates[i]) + ); + } + + return container; + }, + + /** + * Finds all dates in a given month by week. + * Includes leading and trailing days that occur + * outside the given year/month combination. + * + * @private + * @param {Numeric} year target year. + * @param {Numeric} month target month. + * @return {Array[Date]} array of dates. + */ + _getMonthDays: function(year, month) { + var date = new Date(year, month); + var dateEnd = new Date(year, month + 1); + dateEnd.setMilliseconds(-1); + + var start = Calc.getWeekStartDate(date); + var end = Calc.getWeekEndDate(dateEnd); + return Calc.daysBetween(start, end); + }, + + /** + * Returns a section element with all + * the days of the given month/year pair. + * + * Each month has a class for the number of weeks + * it contains. + * + * Possible values: + * - weeks-4 + * - weeks-5 + * - weeks-6 + * + * @private + */ + _renderMonth: function(year, month) { + var container = document.createElement('section'); + var days = this._getMonthDays(year, month); + var daysInWeek = Calc.daysInWeek(); + var weeks = days.length / daysInWeek; + var i = 0; + + container.classList.add('weeks-' + weeks); + + for (; i < weeks; i++) { + container.appendChild(this._renderWeek( + days.splice(0, daysInWeek) + )); + } + + return container; + }, + + /** + * Moves calendar one month into the future. + */ + next: function() { + this.display(this.year, this.month + 1, this.date); + }, + + /** + * Moves calendar one month into the past. + */ + previous: function() { + this.display(this.year, this.month - 1, this.date); + }, + + /** + * Primary method to display given month. + * Will remove the current display and replace + * it with the given month. + * + * @param {Numeric} year year to display. + * @param {Numeric} month month to display. + * @param {Numeric} date date to display. + */ + display: function(year, month, date) { + + // reset the date to the last date if overflow + var lastDate = new Date(year, month + 1, 0).getDate(); + if (lastDate < date) + date = lastDate; + + // Should come before render month + this._position = new Date(year, month, date); + + var element = this._renderMonth(year, month); + + if (this.monthDisplay) { + this.monthDisplay.parentNode.removeChild( + this.monthDisplay + ); + } + + this.monthDisplay = element; + this.element.appendChild(this.monthDisplay); + + this.onmonthchange(this._position); + + // Set the date as selected if presented + this._clearSelectedDay(); + if (date) { + var dayId = Calc.getDayId(this._position); + this.value = this._position; + var selector = '[data-date="' + dayId + '"]'; + var dateElement = document.querySelector(selector); + dateElement.classList.add(SELECTED); + } + }, + + /** + * Called when the month is changed. + */ + onmonthchange: function(month, year) {}, + + /** + * Called when the selected day changes. + */ + onvaluechange: function(date) {} + }; + + return DatePicker; +}()); |