LnkWcb Framework

LnkWcb  2.5.0

LnkWcb Framework > LnkWcb > calendar-extjs.js (source view)
Search:
 
Filters
/*!
 * LNK WCB Framework v2.5
 * (c) 2010 Linkeo.com
 * Use and redistribution are permitted. No warranty.
 * 
 * Calendar - ExtJS 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 based on ExtJS
 * (<a href="http://dev.sencha.com/deploy/dev/docs/output/Ext.DatePicker.html"><code>Ext.Datepicker</code></a>
 * or <a href="http://dev.sencha.com/deploy/dev/docs/output/Ext.form.DateField.html"><code>DateField</code></a>).
 * <p>
 * Please refer to the <a href="LnkWcb.Calendar.html">base class</a>
 * for a full documentation of how to use a calendar instance.
 * 
 * @class CalendarExtJS
 * @namespace LnkWcb
 * @extends LnkWcb.Calendar
 * @constructor
 * @param cfg {Object} the initial configuration settings
 * @private
 * @deprecated <code>LnkWcb.CalendarJquery</code> is much more lightweight than this ExtJS one. Use that jQuery version instead.
 */
(function () {
	var L = LnkWcb,
		C = L.Calendar, CEJ,
		U = L.util,
		T = L.Tests && L.Tests.testables,
		INTL = L.intl,
		defaultSettings = U.putAll(C.prototype.getDefaultSettings(), {
			/**
			 * An HTML ID that identifies the <u>date</u> container HTML element.
			 * 
			 * @config dateContainerElemId
			 * @type String
			 * @default lnk-wcb-date
			 * @public
			 */
			dateContainerElemId: 'lnk-wcb-date',
			/**
			 * An HTML ID that identifies the <u>time</u> container HTML element.
			 * 
			 * @config timeContainerElemId
			 * @type String
			 * @default lnk-wcb-time
			 * @public
			 */
			timeContainerElemId: '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: '--',
			/**
			 * The display type.
			 * Can be of <code>FORM_FIELD</code> (uses an
			 * <a href="http://dev.sencha.com/deploy/dev/docs/output/Ext.form.DateField.html"><code>Ext.form.DateField</code></a>)
			 * or <code>INLINE</code> (uses an
			 * <a href="http://dev.sencha.com/deploy/dev/docs/output/Ext.DatePicker.html"><code>Ext.Datepicker</code></a>)
			 * 
			 * @config displayType
			 * @type String
			 * @default FORM_FIELD
			 * @public
			 */
			displayType: 'FORM_FIELD', // or 'INLINE'
			/**
			 * A CSS class that will be added to the date container element.
			 * This helps a lot in customizing the CSS properties of the date picker.
			 *  
			 * @config containerCss
			 * @type String
			 * @default extjs-css
			 * @public
			 */
			containerCss: 'extjs-css'
		}),
		defaultRsc = { // French locale by default
			cal: {
				dateFormat: 'd/m/Y',
				repeatableDateFormat: 'd/m'
			},
			pat: {
				hours: '{h24tps2}h',
				minutes: '{m60tps2}'
			}
		};

	/**
	 * Computes the list of disabled dates, formated  as strings, following the ExtJS.DatePicker requirements.
	 * 
	 * @method computeExtDisabledDates
	 * @param that {LnkWcb.CalendarExtJS} the calendar instance
	 * @return {Arrays} the list of disabled dates
	 * @private
	 */
	function computeExtDisabledDates(that) {
		var disabledDates = [],
			i, localDate, formattedDate;
		for (i = 0; i < that.nonWorkedLocalDates.length; ++i) {
			localDate = that.nonWorkedLocalDates[i];
			formattedDate = localDate.format(localDate.yearlyRepeatable ? that.rsc.cal.repeatableDateFormat : that.rsc.cal.dateFormat); // use the Date.prototype.format() method brought by ExtJS
			disabledDates.push(formattedDate);
		}
		return disabledDates;
	}

	/**
	 * 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
						hoursSelectElem.onchange(); // refresh minutes
				}
				else { // no more hours available today
					this.resetTimeComponent(hoursSelectElem);
					this.populateEmptyTimeComponent(hoursSelectElem);
					this.populateEmptyTimeComponent(minutesSelectElem);
					// then add closed day
					this.disabledDates.push(date.format(this.rsc.cal.dateFormat)); // use the Date.prototype.format() method brought by ExtJS
					this.dateField.setDisabledDates(this.disabledDates);
					// and change current selection
					date = this.selectedDate = this.findFirstAvailableDate();
					this.dateField.setValue(date);
					this.dateField.fireEvent('select', this.dateField, date); // refresh hours and minutes
				}
			}
		}
		catch (exc) {
			LnkLog.log('LnkWcb.CalendarExtJS.populateMinutes', exc);
		}
	}
	/**
	 * Populate the hours selector with the available hours for the selected date.
	 * Registered by default as a listener to the
	 * <a href="http://dev.sencha.com/deploy/dev/docs/output/Ext.DatePicker.html#Ext.DatePicker-select"><code>select</code></a>
	 * event of the date picker (or the
	 * <a href="http://dev.sencha.com/deploy/dev/docs/output/Ext.form.DateField.html#Ext.form.DateField-select">one</a>
	 * of the date field).
	 * <p>
	 * When calling this method, <code>this</code> muse be set to the correct <code>LnkWcb.CalendarJquery</code> instance.
	 * @method populateHours
	 * @param field {Ext.DatePicker|Ext.form.DateField} the date selector
	 * @param date {Date} the selected date
	 * @private
	 */
	function populateHours(/*Ext.form.DateField*/field, /*Date*/date) {
		var hoursSelectElem = this.hoursSelectElem,
			minutesSelectElem = this.minutesSelectElem,
			hoursMinutes, hour,
			availableMinutes,
			cache = {};
		try {
			this.selectedDate = date;
			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.CalendarExtJS.populateHours', exc);
		}
	}
	if (T) {
		// LnkLog.log('registering testables');
		T.computeExtDisabledDates = computeExtDisabledDates;
		T.populateHours = populateHours;
		T.populateMinutes = populateMinutes;
	}

	CEJ = L.CalendarExtJS = 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-extjs', INTL.getLang()) || defaultRsc;
		INTL.onLangChanged(function (lang) {
			that.rsc = INTL.getRsc('calendar-extjs', lang) || defaultRsc;
		});

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

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

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

	U.putAll(CEJ.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 (unique) HTML ID for the date selector.
		 * 
		 * @method getDateSelectorId
		 * @return {String} the HTML ID of the date selector
		 * @protected
		 */
		getDateSelectorId: function () {
			return this.cfg.dateContainerElemId + '_datefield_' + this.id;
		},
		/**
		 * Computes the (unique) HTML ID for the hours <code>&lt;select&gt;</code> element.
		 * 
		 * @method getHoursSelectorId
		 * @return {String} the HTML ID of the hours selector
		 * @protected
		 */
		getHoursSelectorId: function () {
			return this.cfg.timeContainerElemId + '_hours_' + this.id;
		},
		/**
		 * Computes the (unique) HTML ID for the minutes <code>&lt;select&gt;</code> element.
		 * 
		 * @method getMinutesSelectorId
		 * @return {String} the HTML ID of the minutes selector
		 * @protected
		 */
		getMinutesSelectorId: function () {
			return this.cfg.timeContainerElemId + '_minutes_' + this.id;
		},

		/**
		 * Crate or fetch the date selector.
		 * @method createDateSelector
		 * @return {Ext.form.DateField|Ext.DatePicke} the date selector
		 * @protected
		 */
		createDateSelector: function () {
			var componentId = this.getDateSelectorId(),
				dateField;
			//LnkLog.log('createDateSelector, id: [' + componentId + ']');
			dateField = this.dateField || Ext.getCmp(componentId);
			if (this.cfg.displayType === 'FORM_FIELD') {
				if (dateField) { // already created, reset
					dateField.setMinValue(this.startDate);
					dateField.setMaxValue(this.endDate); // null for no end date
					dateField.setDisabledDays(this.disabledDaysOfWeek);
					dateField.setDisabledDates(this.disabledDates);
					dateField.reset();
				}
				else {
					//LnkLog.log('createDateSelector, id: [' + componentId + '] - creating new DateField - disabledDaysOfWeek: ['+this.disabledDaysOfWeek.join(',')+'], disabledDates: ['+this.disabledDates.join(',')+']');
					dateField = new Ext.form.DateField({
						id: componentId,
						value: this.findFirstAvailableDate(),
						format: this.rsc.cal.dateFormat,
						ctCls: this.cfg.containerCss,
						minValue: this.startDate,
						maxValue: this.endDate, // null for no end date
						disabledDays: this.disabledDaysOfWeek,
						disabledDates: (!this.disabledDates || !this.disabledDates.length) ? null : this.disabledDates // Ext 3.0.0 up to 3.2.0 misinterprets empty array here
					});
					dateField.on('select', populateHours, this);
				}
			}
			else if (this.cfg.displayType === 'INLINE') {
				if (dateField) { // already created, reset
					dateField.setMinDate(this.startDate);
					dateField.setMaxDate(this.endDate); // null for no end date
					dateField.setDisabledDays(this.disabledDaysOfWeek);
					dateField.setDisabledDates(this.disabledDates);
				}
				else {
					dateField = new Ext.DatePicker({
						id: componentId,
						value: this.findFirstAvailableDate(),
						format: this.rsc.cal.dateFormat,
						ctCls: this.cfg.containerCss,
						minDate: this.startDate,
						maxDate: this.endDate, // null for no end date
						disabledDays: this.disabledDaysOfWeek,
						disabledDates: (!this.disabledDates || !this.disabledDates.length) ? null : this.disabledDates // Ext 3.0.0 up to 3.2.0 misinterprets empty array here
					});
					dateField.on('select', populateHours, this);
				}
			}
			return dateField;
		},
		/**
		 * Create or fetch a time selector element. Whether hours or minutes is specified by the HTML ID.
		 * @method createTimeComponent
		 * @param selectorSel {String} the HTML ID of the time <code>&lt;select&gt;</code> element
		 * @return {HTMLElement} the created or fetched element
		 * @protected
		 */
		createTimeComponent: function (selectorId) {
			var DOC = this.document || document,
				selectElem, className,
				that = this;
			selectElem = DOC.getElementById(selectorId);
			if (!selectElem) {
				className = (selectorId === this.getHoursSelectorId()) ? this.cfg.hoursSelectorClass : this.cfg.minutesSelectorClass;
				selectElem = U.createMarkupNode({ select: [ { id: selectorId, "class": className } ] });
				if (selectorId === this.getHoursSelectorId()) {
					selectElem.onchange = 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 () {
			this.dateField.render(this.dateContainerElem);
			this.dateField.show();
		},
		/**
		 * 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 selector.
		 * @method hideDateSelector
		 * @protected
		 */
		hideDateSelector: function () {
			this.dateField.hide();
		},
		/**
		 * 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 selector.
		 * @method destroyDateSelector
		 * @protected
		 */
		destroyDateSelector: function () {
			if (this.dateField) {
				this.dateField.destroy();
			}
		},
		/**
		 * 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) {
			if (selectElem && selectElem.parentNode) {
				selectElem.parentNode.removeChild(selectElem);
			}
		}
	});

	// default event listeners
	CEJ.onCreate.register(function (that) {
		that.onInit(function () {
			this.disabledDates = computeExtDisabledDates(this);

			this.dateField = this.createDateSelector();
			this.hoursSelectElem = this.createTimeComponent(this.getHoursSelectorId());
			this.minutesSelectElem = this.createTimeComponent(this.getMinutesSelectorId());

			this.selectedDate = this.findFirstAvailableDate();
			this.dateField.setValue(this.selectedDate);
			this.dateField.fireEvent('select', this.dateField, this.selectedDate);
		});
		that.onShow(function () {
			var DOC = this.document || document;
			this.dateContainerElem = DOC.getElementById(this.cfg.dateContainerElemId);
			if (this.dateContainerElem) {
				this.renderDateSelector();
			}
			this.timeContainerElem = DOC.getElementById(this.cfg.timeContainerElemId);
			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.