LnkWcb Framework

LnkWcb  2.5.0

LnkWcb Framework > LnkWcb > calendar-jquery.js (source view)
Search:
 
Filters
/*!
 * LNK WCB Framework v2.5
 * (c) 2010 Linkeo.com
 * Use and redistribution are permitted. No warranty.
 * 
 * Calendar - jQuery implementation
 */

var LnkLog, LnkWcb, Ext; // if already defined, log won't get clobbered just defining it
LnkLog = LnkLog || {};
LnkLog.log = LnkLog.log || function () {}; // if not defined, use a safe default value instead
LnkWcb = LnkWcb || {};

/**
 * @module LnkWcb
 */
/**
 * Calendar implementation that uses jQuery to display a date picker.
 * <p>
 * Please refer to the <a href="LnkWcb.Calendar.html">base class</a>
 * for a full documentation of how to use a calendar instance.
 * 
 * @namespace LnkWcb
 * @class CalendarJquery
 * @extends LnkWcb.Calendar
 * @constructor
 * @param cfg {Object} the initial configuration settings
 * @public
 */

(function () {
	var L = LnkWcb,
		$ = L.jQuery,
		C = L.Calendar, CJQ,
		U = L.util,
		T = L.Tests && L.Tests.testables,
		INTL = L.intl,
		defaultSettings = U.putAll(C.prototype.getDefaultSettings(), {
			/**
			 * A CSS selector that locates the form container. This container element might not necessarily
			 * be a <code>&lt;form&gt;</code> element. It is just a root element in which other elements
			 * will be looked for.
			 * 
			 * @config formSel
			 * @type String
			 * @default #LnkWcbForm
			 * @public
			 */
			formSel: '#LnkWcbForm',
			/**
			 * The relative CSS selector that locates the <u>date</u> container element.
			 * Relative to the form root as specified by <a href="#config_formSel"><code>formSel</code></a>.
			 * 
			 * @config dateContainerRelSel
			 * @type String
			 * @default .lnk-wcb-date
			 * @public
			 */
			dateContainerRelSel: '.lnk-wcb-date',
			/**
			 * The relative CSS selector that locates the <u>time</u> container element.
			 * Relative to the form root as specified by <a href="#config_formSel"><code>formSel</code></a>.
			 * 
			 * @config timeContainerRelSel
			 * @type String
			 * @default .lnk-wcb-time
			 * @public
			 */
			timeContainerRelSel: '.lnk-wcb-time',
			/**
			 * The CSS class attribute of the created <u>hours</u> <code>&lt;select&gt;</code> element.
			 * 
			 * @config hoursSelectorClass
			 * @type String
			 * @default lnk-wcb-hours
			 * @protected
			 */
			hoursSelectorClass: 'lnk-wcb-hours',
			/**
			 * The CSS class attribute of the created <u>minutes</u> <code>&lt;select&gt;</code> element.
			 * 
			 * @config minutesSelectorClass
			 * @type String
			 * @default lnk-wcb-minutes
			 * @protected
			 */
			minutesSelectorClass: 'lnk-wcb-minutes',
			/**
			 * The text of any selector (hours or minutes) that has no available option.
			 * 
			 * @config voidChar
			 * @type String
			 * @default --
			 * @public
			 */
			voidChar: '--'
		}),
		defaultRsc = { // French locale by default
			pat: {
				hours: '{h24tps2}h',
				minutes: '{m60tps2}'
			}
		};

	/**
	 * Populate the minutes selector with the available minutes for the selected date, and selected hour.
	 * <p>
	 * When calling this method, <code>this</code> muse be set to the correct <code>LnkWcb.CalendarJquery</code> instance.
	 * @method populateMinutes
	 * @param hoursMinutes {Array&lt;Array&lt;Number&gt;&gt;} Optional opened minutes list for the currently selected hour.
	 * @private
	 */
	function populateMinutes(/*hoursMinutes*/) {
		var hoursSelectElem = this.hoursSelectElem,
			minutesSelectElem = this.minutesSelectElem,
			hoursMinutes = arguments[0],
			date = this.selectedDate,
			availableMinutes, minutes,
			cache = {};
		try {
			this.resetTimeComponent(minutesSelectElem);
			hoursMinutes = hoursMinutes || (date && this.computeHoursMinutes(date));
			availableMinutes = hoursMinutes && hoursMinutes[hoursSelectElem.value];
			if (availableMinutes) {
				for (minutes = 0; minutes < 60; minutes += this.cfg.minutesStep) {
					if (availableMinutes[minutes] && this.hasWorkedInterval(availableMinutes[minutes], date, cache)) {
						this.addOption(minutesSelectElem, [
							{ value: minutes },
							this.renderMinutes(minutes)
						]);
					}
				}
			}
			if (minutesSelectElem.options.length <= 0) { // no available minutes, meaning the selected hour is no more valid
				hoursSelectElem.remove(hoursSelectElem.selectedIndex);
				if (hoursSelectElem.selectedIndex >= 0 && // TODO: test (hoursSelectElem.options.length <= 0)
					hoursSelectElem.selectedIndex < hoursSelectElem.options.length) { // there are still other hours available
					arguments.callee.call(this); // refresh minutes
				}
				else { // no more hours available today
					this.resetTimeComponent(hoursSelectElem);
					this.populateEmptyTimeComponent(hoursSelectElem);
					this.populateEmptyTimeComponent(minutesSelectElem);
					// then add closed day
					this.nonWorkedLocalDates.push(date);
					// and change current selection
					date = this.selectedDate = this.findFirstAvailableDate();
					$(this.dateField).datepicker("setDate", date); // TODO: test if this fires the onSelect event?
					populateHours.call(this); // refresh hours and minutes
				}
			}
		}
		catch (exc) {
			LnkLog.log('LnkWcb.CalendarJquery.populateMinutes', exc);
		}
	}
	/**
	 * Populate the hours selector with the available hours for the selected date.
	 * <p>
	 * When calling this method, <code>this</code> muse be set to the correct <code>LnkWcb.CalendarJquery</code> instance.
	 * @method populateHours
	 * @private
	 */
	function populateHours() {
		var date,
			hoursSelectElem = this.hoursSelectElem,
			minutesSelectElem = this.minutesSelectElem,
			hoursMinutes, hour,
			availableMinutes,
			cache = {};
		try {
			date = this.selectedDate = $(this.dateField).datepicker("getDate");
			this.resetTimeComponent(hoursSelectElem);
			hoursMinutes = date && this.computeHoursMinutes(date);
			if (hoursMinutes) {
				for (hour = 0; hour < 24; ++hour) {
					availableMinutes = hoursMinutes[hour];
					if (availableMinutes && this.hasWorkedInterval(availableMinutes.refIntervals, date, cache)) {
						this.addOption(hoursSelectElem, [
							{ value: hour },
							this.renderHours(hour)
						]);
					}
				}
			}
			if (hoursSelectElem.options.length <= 0) {
				this.populateEmptyTimeComponent(hoursSelectElem);
				this.resetTimeComponent(minutesSelectElem); // cancel all available minutes either
				this.populateEmptyTimeComponent(minutesSelectElem);
			}
			else {
				(this.populateMinutes /* allows mocking */ || populateMinutes).call(this, hoursMinutes); // cascade on available minutes
			}
		}
		catch (exc) {
			LnkLog.log('LnkWcb.CalendarJquery.populateHours', exc);
		}
	}
	if (T) {
		T.populateHours = populateHours;
		T.populateMinutes = populateMinutes;
	}

	CJQ = L.CalendarJquery = function (cfg) {
		var that = this;

		C.apply(that, arguments);
		U.putAll(that.cfg, defaultSettings);
		U.putAll(that.cfg, cfg);

		/**
		 * The current resource used.
		 * @property rsc
		 * @type Object
		 * @public
		 */
		that.rsc = INTL.getRsc('calendar-jquery', INTL.getLang()) || defaultRsc;
		INTL.onLangChanged(function (lang) {
			that.rsc = INTL.getRsc('calendar-jquery', lang) || defaultRsc;
		});

		that.dateField = null;
		that.hoursSelectElem = null;
		that.minutesSelectElem = null;
		that.dateContainerElem = null;
		that.timeContainerElem = null;

		if (!$) {
			LnkLog.log('Warning: LnkWcb.jQuery is missing, did you forget to include the jQuery library?');
		}
		CJQ.onCreate.fire(that);
	};
	// TODO: actually that would be better to have it a provider rather than a subclass
	CJQ.prototype = U.object(C.prototype);
	CJQ.prototype.constructor = CJQ;
	CJQ.superclass = C.prototype;

	/**
	 * Class event. Fires when an <code>LnkWcb.CalendarJquery</code> instance is just created.
	 * @event onCreate
	 * @param bouton {Object} the new instance
	 * @static
	 * @protected
	 */
	CJQ.onCreate = new L.Event(CJQ); // custom event for subclass

	U.putAll(CJQ.prototype, {
		/**
		 * Get the selected date and time.
		 * <p>
		 * The returned value is formated according to the LnkWcb convention, as implemented
		 * in the <a href="LnkWcb.util.html#method_formatDateTime"><code>LnkWcb.util.formatDateTime</code></a> method.
		 * @method getWcbDate
		 * @return {String} the selected date and time
		 * @public
		 */
		getWcbDate: function () { // no validation possible
			var date = this.selectedDate,
				hours = this.hoursSelectElem,
				minutes = this.minutesSelectElem;
			if (!date || !hours || !minutes) {
				return null;
			}
			date = new Date(date.getTime());
			hours = Number(hours.value);
			minutes = Number(minutes.value);
			// TODO: resolve the validation issue that arise here. Cal should send date to button, and attach a validator to it.
			//if (!isNaN(hours) && !isNaN(minutes)) {
				date.setHours(hours);
				date.setMinutes(minutes);
			//}
			return U.formatDateTime(date);
		},
		/**
		 * Concrete method. Not documented. Reserved for future use.
		 * @method getDateTime
		 * @return {Object} the selected date, hours and minutes
		 * @private
		 */
		getDateTime: function () { // allows more precise subsequent validation
			var date = this.selectedDate,
				hours = this.hoursSelectElem,
				minutes = this.minutesSelectElem;
			return {
				date: date ? new Date(date.getTime()) : null,
				hours: hours ? Number(hours.value) : null,
				minutes: minutes ? Number(minutes.value) : null
			};
		},

		/**
		 * Computes the absolute CSS selector that locates the hours <code>&lt;select&gt;</code> element.
		 * 
		 * @method getHoursSelectorSel
		 * @return {String} the CSS (absolute) selector
		 * @protected
		 */
		getHoursSelectorSel: function () {
			return this.cfg.formSel + ' ' + this.cfg.timeContainerRelSel + ' .' + this.cfg.hoursSelectorClass;
		},
		/**
		 * Computes the absolute CSS selector that locates the minutes <code>&lt;select&gt;</code> element.
		 * 
		 * @method getMinutesSelectorSel
		 * @return {String} the CSS (absolute) selector
		 * @protected
		 */
		getMinutesSelectorSel: function () {
			return this.cfg.formSel + ' ' + this.cfg.timeContainerRelSel + ' .' + this.cfg.minutesSelectorClass;
		},

		/**
		 * Default listener for the <a href="http://docs.jquery.com/UI/Datepicker#event-beforeShowDay"><code>beforeShowDay</code></a>
		 * event of the jQuery <a href="http://docs.jquery.com/UI/Datepicker">date picker</a>.
		 * 
		 * @method beforeShowDayAction
		 * @param date {Date} a date instance
		 * @return {Array} [0] is a boolean indicating whether or not the date is selectable
		 * @protected
		 */
		beforeShowDayAction: function (date) {
			var days = this.disabledDaysOfWeek,
				dates = this.nonWorkedLocalDates,
				selectable = true,
				i, localDate;
			for (i = 0; i < days.length; ++i) {
				if (date.getDay() === days[i]) {
					selectable = false;
					break;
				}
			}
			if (selectable) {
				for (i = 0; i < dates.length; ++i) {
					localDate = dates[i];
					if (date.getDate() === localDate.getDate() && date.getMonth() === localDate.getMonth() &&
							(localDate.yearlyRepeatable || date.getFullYear() === localDate.getFullYear())) {
						selectable = false;
						break;
					}
				}
			}
			return [ selectable, '' ];
		},

		/**
		 * Fetch the date container element.
		 * @method createDateSelector
		 * @return {HTMLElement} the element that contains the date picker
		 * @protected
		 */
		createDateSelector: function () {
			var f = $(this.cfg.formSel),
				s = f.find(this.cfg.dateContainerRelSel);
			return s[0];
		},
		/**
		 * Create or fetch a time selector element. Whether hours or minutes is specified by the CSS selector.
		 * @method createTimeComponent
		 * @param selectorSel {String} the CSS selector that locates the time <code>&lt;select&gt;</code> element
		 * @return {HTMLElement} the created or fetched element
		 * @protected
		 */
		createTimeComponent: function (selectorSel) {
			var selectElem, className,
				that = this;
			selectElem = $(selectorSel)[0];
			if (!selectElem) {
				className = (selectorSel === this.getHoursSelectorSel()) ? this.cfg.hoursSelectorClass : this.cfg.minutesSelectorClass;
				selectElem = U.createMarkupNode({ select: [ { "class": className } ] });
				if (selectorSel === this.getHoursSelectorSel()) {
					$(selectElem).change(function () {
						populateMinutes.call(that);
					});
				}
			}
			this.resetTimeComponent(selectElem);
			this.populateEmptyTimeComponent(selectElem);
			return selectElem;
		},

		/**
		 * Format an hour, according to the <code>pat.hour</code> format of the resource.
		 * @method renderHours
		 * @param hours {Number} an hour
		 * @return {String} the formated hour
		 * @protected
		 */
		renderHours: function (hours) {
			var h = Number(hours),
				h12 = h <= 0 ? 12 : (((h - 1) % 12) + 1),
				attrs = {
					h24: h,
					h24tps2: U.toPaddedString(h, 2),
					h12: h12,
					h12tps2: U.toPaddedString(h12, 2),
					ampm: Math.floor(h / 12) ? 'p.m.' : 'a.m.'
				};
			return INTL.fmt(this.rsc.pat.hours, attrs);
		},
		/**
		 * Format some minutes, according to the <code>pat.minutes</code> format of the resource.
		 * @method renderMinutes
		 * @param minutes {Number} some minutes
		 * @return {String} the formated minutes
		 * @protected
		 */
		renderMinutes: function (minutes) {
			var attrs = {
				m60: minutes,
				m60tps2: U.toPaddedString(minutes, 2)
			};
			return INTL.fmt(this.rsc.pat.minutes, attrs);
		},
		/**
		 * Render the date picker. Displays it to the websurfer.
		 * @method renderDateSelector
		 * @protected
		 */
		renderDateSelector: function () {
			var that = this;
			$(this.dateField).datepicker({
				minDate: this.startDate,
				maxDate: this.endDate,
				beforeShowDay: function (date) {
					return that.beforeShowDayAction(date);
				},
				onSelect: function () {
					populateHours.call(that);
				}
			});
		},
		/**
		 * Render the time component passed as argument (hours or minutes). Displays it to the websurfer.
		 * @method renderTimeComponent
		 * @param selectElem {HTMLElement} the time <code>&lt;select&gt;</code> element
		 * @protected
		 */
		renderTimeComponent: function (selectElem) {
			this.timeContainerElem.appendChild(selectElem);
		},

		/**
		 * Reset the time component passed as argument (hours or minutes). Deletes all options in it.
		 * @method resetTimeComponent
		 * @param selectElem {HTMLElement} the time <code>&lt;select&gt;</code> element
		 * @protected
		 */
		resetTimeComponent: function (selectElem) {
			while (selectElem.options.length > 0) {
				selectElem.remove(0);
			}
		},
		/**
		 * Add a default <a href="#config_voidChar">void option</a> to the time component passed as argument (hours or minutes).
		 * @method populateEmptyTimeComponent
		 * @param selectElem {HTMLElement} the time <code>&lt;select&gt;</code> element
		 * @protected
		 */
		populateEmptyTimeComponent: function (selectElem) {
			this.addOption(selectElem, [
				{ value: this.cfg.voidChar },
				this.cfg.voidChar
			]);
		},
		/**
		 * Add an option to the time component passed as argument (hours or minutes).
		 * @method addOption
		 * @param selectElem {HTMLElement} the time <code>&lt;select&gt;</code> element
		 * @param content {String} the specification for the option (as documented for the <a href="LnkWcb.util.html#method_createMarkupNode"><code>LnkWcb.util.createMarkupNode</code></a> method)
		 * @protected
		 */
		addOption: function (selectElem, content) {
			selectElem.appendChild(U.createMarkupNode({ option: content }));
		},

		/**
		 * Hide the date picker.
		 * @method hideDateSelector
		 * @protected
		 */
		hideDateSelector: function () {
			$(this.dateField).datepicker("destroy");
		},
		/**
		 * Hide the time component passed as argument (hours or minutes).
		 * @method hideTimeComponent
		 * @param selectElem {HTMLElement} the time <code>&lt;select&gt;</code> element
		 * @protected
		 */
		hideTimeComponent: function (selectElem) {
			this.timeContainerElem.removeChild(selectElem);
		},

		/**
		 * Destroy the date picker.
		 * @method destroyDateSelector
		 * @protected
		 */
		destroyDateSelector: function () {
		},
		/**
		 * Destroy the time component passed as argument (hours or minutes).
		 * @method destroyTimeComponent
		 * @param selectElem {HTMLElement} the time <code>&lt;select&gt;</code> element
		 * @protected
		 */
		destroyTimeComponent: function (selectElem) {
		}
	});

	// default event listeners
	CJQ.onCreate.register(function (that) {
		that.onInit(function () {
			this.dateField = this.createDateSelector();
			this.hoursSelectElem = this.createTimeComponent(this.getHoursSelectorSel());
			this.minutesSelectElem = this.createTimeComponent(this.getMinutesSelectorSel());

			this.selectedDate = this.findFirstAvailableDate();
		});
		that.onShow(function () {
			this.dateContainerElem = $(this.cfg.formSel).find(this.cfg.dateContainerRelSel)[0];
			if (this.dateContainerElem) {
				this.renderDateSelector();
				$(this.dateField).datepicker('setDate', this.selectedDate);
				populateHours.call(this) ;
			}
			this.timeContainerElem = $(this.cfg.formSel).find(this.cfg.timeContainerRelSel)[0];
			if (this.timeContainerElem) {
				this.renderTimeComponent(this.hoursSelectElem);
				this.renderTimeComponent(this.minutesSelectElem);
			}
		});
		that.onHide(function () {
			if (this.dateContainerElem) {
				this.hideDateSelector();
			}
			this.dateContainerElem = null;
			if (this.timeContainerElem) {
				this.hideTimeComponent(this.hoursSelectElem);
				this.hideTimeComponent(this.minutesSelectElem);
			}
			this.timeContainerElem = null;
		});
		that.onDestroy(function () {
			this.destroyDateSelector();
			this.dateField = null;
			this.destroyTimeComponent(this.hoursSelectElem);
			this.hoursSelectElem = null;
			this.destroyTimeComponent(this.minutesSelectElem);
			this.minutesSelectElem = null;
		});
	});
})();

Copyright © 2010 Linkeo.com All rights reserved.