LnkWcb Framework

LnkWcb  2.5.0

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

var LnkLog, LnkWcb; // 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
 */
/**
 * Registry instance used for registering calendars.
 * @class Calendars
 * @namespace LnkWcb
 * @extends LnkWcb.Registry
 * @static
 * @private
 */
LnkWcb.Calendars = LnkWcb.core.createRegistry();

/**
 * <p>
 * Base Calendar. When using a calendar (or any subclass), you can chose one of three available flows.
 * <p>
 * <u><strong>Basic Flow</strong></u>
 * <p>
 * The basic flow (available by default) implements 2 states:
 * <ul>
 * <li><em>destroy</em> state: the initial state</li>
 * <li><em>show</em> state: the calendar is displayed to the websurfer, and allows to select a date/time</li>
 * </ul>
 * To perform transitions from one basic state to another:
 * <ul>
 * <li>From <em>destroy</em> to <em>show</em>: call <a href="LnkWcb.Bouton.html#method_horaires"><code>LnkWcb.Bouton.horaires()</code></a></li>
 * <li>From <em>show</em> to <em>destroy</em>: call <a href="#method_destroy"><code>LnkWcb.Calendar.destroy()</code></a></li>
 * </ul>
 * <p>
 * The basic flow relies on the default value of <a href="#config_showOnInit"><code>showOnInit</code></a> config which is <code>true</code>.
 * <p>
 * <u><strong>Alternate Basic Flow</strong></u>
 * <p>
 * The alternate basic flow (also available by default) uses 3 states:
 * <ul>
 * <li><em>destroy</em> state: the initial state</li>
 * <li><em>show</em> state: the calendar is displayed to the websurfer, and allows to select a date/time</li>
 * <li><em>hide</em> state: the calendar is hidden from the websurfer</li>
 * </ul>
 * To perform transitions from one basic state to another:
 * <ul>
 * <li>After creation, to <em>show</em> the calendar, call <a href="#method_refresh"><code>LnkWcb.Calendar.refresh()</code></a></li>
 * <li>From <em>show</em> to <em>hide</em>: call <a href="#method_destroy"><code>LnkWcb.Calendar.hide()</code></a></li>
 * <li>From <em>hide</em> to <em>show</em>: call <a href="#method_refresh"><code>LnkWcb.Calendar.refresh()</code></a></li>
 * </ul>
 * <p>
 * The alternate basic flow relies on the default value of <a href="#config_showOnInit"><code>showOnInit</code></a> config which is <code>true</code>.
 * <p>
 * <u><strong>Full Flow</strong></u>
 * <p>
 * When the <a href="#config_showOnInit"><code>showOnInit</code></a> config is set to <code>false</code>,
 * the calendar instance then implement 4 successive states:
 * <ul>
 * <li><em>destroy</em> state: the initial state. No internal data is initialized (or all data is destroyed).</li>
 * <li><em>init</em> state: the calendar is initialized with fresh open hours and closed days, received from the server.</li>
 * <li><em>show</em> state: the calendar is displayed to the websurfer, and allows to select a date/time.</li>
 * <li><em>hide</em> state: the calendar is hidden from the websurfer. All internal data is kept alive.</li>
 * </ul>
 * <p>
 * At creation time (after <code>new LnkWcb.Calendar()</code> is executed), the created instance is in the <em>destroy</em> state.
 * <p>
 * Transitions in the full flow are:
 * <ul>
 * <li>From <em>destroy</em> to <em>init</em>: call <a href="LnkWcb.Bouton.html#method_horaires"><code>LnkWcb.Bouton.horaires()</code></a></li>
 * <li>From <em>init</em> to <em>show</em>: call <a href="#method_show"><code>LnkWcb.Calendar.show()</code></a></li>
 * <li>From <em>show</em> to <em>hide</em>: call <a href="#method_hide"><code>LnkWcb.Calendar.hide()</code></a></li>
 * <li>From <em>hide</em> to <em>destroy</em>: call <a href="#method_destroy"><code>LnkWcb.Calendar.destroy()</code></a></li>
 * </ul>
 * @class Calendar
 * @namespace LnkWcb
 * @constructor
 * @param cfg {Object} the initial configuration settings
 */
(function () {
	var L = LnkWcb,
		U = L.util,
		T = L.Tests && L.Tests.testables,
		C,
		defaultSettings = {
			/**
			 * The <a href="LnkWcb.Bouton.html"><code>LnkWcb.Bouton</code></a> instance on which this calendar depends.
			 * <p>
			 * You should not modify this setting after the calendar is created.
			 * Use the <a href="#method_setBouton"><code>setBouton</code></a> method instead.
			 * @config bouton
			 * @type LnkWcb.Bouton
			 * @default null
			 * @public
			 * @see setBouton()
			 */
			bouton: null,
			/**
			 * Controls the feature that allows the calendar to be displayed as soon as possible.
			 * <p>
			 * This helps in simplifying the calendar 4 states.
			 * Which might then be reduced to <a href="#method_init"><code>init</code></a> and <a href="#method_destroy"><code>destroy</code></a> only.
			 * @config showOnInit
			 * @type Boolean
			 * @default true
			 * @protected
			 */
			showOnInit: true, // use the binary init/destroy state model instead of the 4 init/show/hide/destroy states
			/**
			 * The step used when displaying available minutes.
			 * @config minutesStep
			 * @type Number
			 * @default 5
			 * @public
			 */
			minutesStep: 5,
			/**
			 * The maximum days that can be selected ahead of now.
			 * @config maxDays
			 * @type Number
			 * @default 8
			 * @public
			 */
			maxDays: 8
		},
		defaultEvents = [
			/**
			 * Fires right after the calendar is initialized.
			 * <p>
			 * When the <a href="#config_showOnInit"><code>showOnInit</code></a> config is <code>true</code>,
			 * it is fired just before the calendar is <a href="#method_show">showed</a>.
			 * @event onInit
			 * @param horaires {Object} the open hours (as received by the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
			 * @protected
			 */
			'onInit',
			/**
			 * Fires when the calendar is to be shown.
			 * <p>
			 * Subclasses should attach listeners to this event in order to actually display the calendar.
			 * @event onShow
			 * @protected
			 */
			'onShow',
			/**
			 * Fires when the calendar is to be hidden.
			 * <p>
			 * Subclasses should attach listeners to this event in order to actually hide the calendar.
			 * @event onHide
			 * @protected
			 */
			'onHide',
			/**
			 * Fires when the calendar is to be destroyed.
			 * <p>
			 * Subclasses should attach listeners to this event in order to
			 * destroy data that might have been initialized at the <a href="#method_init"><code>init</code></a> step.
			 * @event onDestroy
			 * @protected
			 */
			'onDestroy'
		];

	/**
	 * Parse the open hours (as received by the
	 * <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
	 * and split them into week days time period.
	 * <p>
	 * Time period are arrays containing these 3 numbers.
	 * <ol>
	 * <li>Start of the time period in the day (number of minutes from midnight, between 0 and 1440)</li>
	 * <li>End of the time period in the day (number of minutes from midnight, greater than the start of period)</li>
	 * <li>Day shift (usually <code>0</code>) to know which closed day (if any)
	 * in the channel time zone applies to this time period.</li>
	 * </ol>
	 * <p>
	 * The result is, for every day of week with open hours, an array of such
	 * time periods.
	 * @method parsePlagesHoraires
	 * @param plagesHoraires {Array&lt;Object&lt;String&gt;&gt;} the open hours
	 * @return {Object&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;} the parsed open hours
	 * @private
	 */
	function parsePlagesHoraires(plagesHoraires) {
		var dowTimeIntervals = {},
			i, plage,
			debut, fin, dayOfWeek1, dayOfWeek2, time1, time2, dayShift,
			day;

		for (i = 0; i < plagesHoraires.length; ++i) {
			plage = plagesHoraires[i];
			debut = plage.debut;
			fin = plage.fin;
			dayOfWeek1 = Number(debut.charAt(3));
			dayOfWeek2 = Number(fin.charAt(3));
			time1 = Number(debut.substring(5, 7)) * 60 + Number(debut.substring(8));
			time2 = Number(fin.substring(5, 7)) * 60 + Number(fin.substring(8));
			dayShift = Number(plage.debutHeureCanal.charAt(3)) - dayOfWeek1;
			if (dayOfWeek1 === dayOfWeek2) {
				if (!dowTimeIntervals[dayOfWeek1]) {
					dowTimeIntervals[dayOfWeek1] = [];
				}
				dowTimeIntervals[dayOfWeek1].push([ time1, time2, dayShift ]);
			}
			else {
				for (day = dayOfWeek1; day <= dayOfWeek2; ++day) {
					if (!dowTimeIntervals[day]) {
						dowTimeIntervals[day] = [];
					}
					if (day === dayOfWeek1) {
						dowTimeIntervals[day].push([ time1, 24*60, dayShift ]);
					}
					else if (day === dayOfWeek2) {
						dowTimeIntervals[day].push([ 0, time2, dayShift ]);
					}
					else { // should never happen for the moment
						dowTimeIntervals[day].push([ 0, 24*60, dayShift ]);
					}
					--dayShift;
				}
			}
		}
		return dowTimeIntervals;
	}
	/**
	 * Dispatch the open time periods by day of week, hours of day, and minutes of hour.
	 * Example : <code>dowHoursMinutes[dow][hour][minutes]</code> is an array of the matching time periods for this instant.
	 * For hours and minutes arrays, sparse arrays are used.
	 * @method computeDowHoursMinutes
	 * @param dowTimeIntervals {Object&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;} the parsed open hours, as computed by the <a href="#method_parsePlagesHoraires"><code>parsePlagesHoraires</code></a> method
	 * @param that {LnkWcb.Calendar} the <code>LnkWcb.Calendar</code> instance
	 * @return {Object&lt;Array&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;&gt;} the time periods, dipatched by day of week, hours and minutes
	 * @private
	 */
	function computeDowHoursMinutes(dowTimeIntervals, that) {
		var indexOf = U.indexOf,
			dowHoursMinutes = {},
			day, timeIntervals, hoursMinutes, time,
			i, interval,
			oldHour, hour, minutes, refIntervals;
		for (day = 0; day < 7; ++day) {
			timeIntervals = dowTimeIntervals[day];
			if (!timeIntervals) {
				continue;
			}
			hoursMinutes = []; // sparse array
			hoursMinutes.refIntervals = [];
			time = 0;
			for (i = 0; i < timeIntervals.length; ++i) {
				interval = timeIntervals[i];
				time = interval[0];
				time += that.cfg.minutesStep - 1;
				time -= (time % 60) % that.cfg.minutesStep;
				hour = Math.floor(time / 60);
				minutes = time % 60;
				oldHour = undefined;
				while (time < interval[1]) {
					if (oldHour !== hour) {
						if (!hoursMinutes[hour]) {
							hoursMinutes[hour] = []; // sparse array
							hoursMinutes[hour].refIntervals = [];
						}
						refIntervals = hoursMinutes[hour].refIntervals;
						if (indexOf(refIntervals, interval) < 0) {
							refIntervals.push(interval);
						}
						oldHour = hour;
					}
					if (!hoursMinutes[hour][minutes]) {
						hoursMinutes[hour][minutes] = [];
					}
					refIntervals = hoursMinutes[hour][minutes];
					if (indexOf(refIntervals, interval) < 0) {
						refIntervals.push(interval);
					}
					time += that.cfg.minutesStep;
					hour = Math.floor(time / 60);
					minutes = time % 60;
				}
			}
			dowHoursMinutes[day] = hoursMinutes;
		}
		return dowHoursMinutes;
	}
	/*
	function logDowTimeIntervals(dowTimeIntervals) {
		var TPS = U.toPaddedString,
			prop;
		if (Array.prototype.map) {
			for (var prop in dowTimeIntervals) {
				//LnkLog.log('prop: '+prop);
				if (!dowTimeIntervals.hasOwnProperty || dowTimeIntervals.hasOwnProperty(prop)) {
					LnkLog.log(prop +' - timeIntervals: ['+dowTimeIntervals[prop].map(function(it) {
						return '['+Math.floor(it[0]/60)+':'+TPS(it[0]%60,2)+','+Math.floor(it[1]/60)+':'+TPS(it[1]%60,2)+','+it[2]+']';
					}).join(',')+']');
				}
			}
		}
	}
	*/
	/**
	 * Computes the days of week with no applicable open hours.
	 * @method computeDisabledDaysOfWeek
	 * @param dowTimeIntervals {Object&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;} the parsed open hours, as computed by the <a href="#method_parsePlagesHoraires"><code>parsePlagesHoraires</code></a> method
	 * @return {Array&lt;Number&gt;} the list of days of week with no open hours
	 * @private
	 */
	function computeDisabledDaysOfWeek(dowTimeIntervals) {
		var dow, disabledDaysOfWeek = [];
		for (dow = 0; dow < 7; ++dow) {
			if (!dowTimeIntervals[dow] || !dowTimeIntervals[dow].length) {
				disabledDaysOfWeek.push(dow);
			}
		}
		//LnkLog.log('disabledDaysOfWeek: ['+disabledDaysOfWeek.join(',')+']');
		return disabledDaysOfWeek;
	}
	/**
	 * Parse closed days (as received by the
	 * <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
	 * and convert them to <code>Date</code> objects with the custom <code>yearlyRepeatable</code> member,
	 * which is a boolean indicating whether the year is applicable (<code>yearlyRepeatable===false</code>)
	 * or not (<code>yearlyRepeatable===true</code>).
	 * @method toLocalDate
	 * @param joursFeries {Array&lt;Object&lt;String&gt;&gt;} the closed days
	 * @param now {Date} the date to use as the current instant
	 * @return {Array&lt;Date&gt;} the array of (possibly yearly repeatable) dates
	 * @private
	 */
	function toLocalDate(joursFeries, now) {
		var localDates = [],
			i, partialDate, localDate,
			year, month, day;
		for (i = 0; i < joursFeries.length; ++i) {
			partialDate = joursFeries[i].date;
			localDate = new Date();
			localDate.setHours(0, 0, 0, 0);
			localDate.yearlyRepeatable = /^-/.test(partialDate);
			if (localDate.yearlyRepeatable) {
				year = null;
				month = Number(partialDate.substring(2, 4));
				day = Number(partialDate.substring(5, 7));
				localDate.setMonth(month - 1, day);
				if (localDate.getTime() < now.getTime()) {
					localDate.setFullYear(localDate.getFullYear() + 1); // limits calendar validity to 1 year ahead
				}
			}
			else {
				year = Number(partialDate.substring(0, 4));
				month = Number(partialDate.substring(5, 7));
				day = Number(partialDate.substring(8, 10));
				localDate.setFullYear(year, month - 1, day);
			}
			//log ('year: '+year+', month: '+month+', day: '+day+', localDate: '+localDate+'.');
			localDates.push(localDate);
		}
		return localDates;
	}
	/*
	function hasPartialIntervals(timeIntervals) {
		var d;
		if (!timeIntervals) {
			return false;
		}
		d = 0;
		while (d < timeIntervals.length && timeIntervals[d][2] === 0) {
			++d;
		}
		return d < timeIntervals.length;
	}
	*/
	/**
	 * Tells whether a <code>Date</code> instance is a worked date or not.
	 * @method isWorkedDate
	 * @param d {Date} the date to test
	 * @param joursFeries {Array&lt;Object&lt;String&gt;&gt;} the closed days (as received by the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
	 * @return {Boolean} <code>true</code> if the date is not a closed day, <code>false</code> otherwise
	 * @private
	 */
	function isWorkedDate(d, joursFeries) {
		var suffix, testDateMD, testDateYMD,
			TPS = U.toPaddedString, i;
		if (!d) {
			return true;
		}
		suffix = "-" + TPS(d.getMonth() + 1, 2) + "-" + TPS(d.getDate(), 2);
		testDateMD = "-" + suffix;
		testDateYMD = TPS(d.getFullYear(), 4) + suffix;
		i = 0;
		while (i < joursFeries.length &&
				!(testDateYMD === joursFeries[i].date ||
					testDateMD === joursFeries[i].date)) {
			++i;
		}
		return i >= joursFeries.length;
	}
	/*
	function findTimeIntervalAfter(timeIntervals, time, date, matching) {
		var i, interval, dateFuseauCanal;
		for (i = 0; i < timeIntervals.length; i++) {
			interval = timeIntervals[i];
			if (interval && time < interval[1] && (!matching || time >= interval[0])) {
				dateFuseauCanal = new Date(date.getTime() + 24*60*60*1000*interval[2]);
				if (isWorkedDate(dateFuseauCanal)) {
					return interval;
				}
			}
		}
		return null;
	}
	*/
	/*
	function findMatchingTimeInterval(timeIntervals, time, date) {
		return findTimeIntervalAfter(timeIntervals, time, date, true);
	}
	*/
	/*
	/ * *
	 * @param timeIntervals : ordered intervals so that {@code timeIntervals[n][1] < timeIntervals[n+1][1]} ?
	 * No.
	 * What we have is {@code timeIntervals[n][0] <= timeIntervals[n+1][0]} for all {@code n}.
	 * So if there is a next time interval, it is the one with lower {@code interval[0]} value,
	 * so it is the one most likely to match.
	 * /
	function findNextTimeInterval(timeIntervals, time, date) {
		return findTimeIntervalAfter(timeIntervals, time, date, false);
	}
	*/
	/**
	 * Tells if a <code>Date</code> instance is worked or not,
	 * taking all information into account (open hours and closed days).
	 * @method hasWorkedInterval_Internal
	 * @param refIntervals {Array&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;} the dispatched time periods (see the <a href="#method_computeDowHoursMinutes"><code>computeDowHoursMinutes</code></a>) of the day of week of the <code>date</code> argument
	 * @param date {Date} the date to test
	 * @param joursFeries {Array&lt;Object&lt;String&gt;&gt;} the closed days (as received by the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
	 * @return {Boolean} <code>true</code> if the date is worked, <code>false</code> otherwise
	 * @private
	 */
	function hasWorkedInterval_Internal(refIntervals, date, joursFeries/*, cache*/) {
		var i, dateFuseauCanal/*, 
			cache = arguments[3] || {},
			cached*/;
		for (i = 0; i < refIntervals.length; ++i) {
			/*
			cached = cache[refIntervals[i][2]];
			if (cached !== undefined) {
				if (cached === true) {
					return true;
				}
				continue;
			}
			*/
			dateFuseauCanal = new Date(date.getTime() + 24*60*60*1000*refIntervals[i][2]);
			if (isWorkedDate(dateFuseauCanal, joursFeries)) {
				return true;
			}
		}
		return false;
	}
	/**
	 * Parse closed days (as received by the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
	 * and return a list of non worked local dates, in the websurfer time zone.
	 * @method parseJoursFeries
	 * @param joursFeries {Array&lt;Object&lt;String&gt;&gt;} the closed days (as received by the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
	 * @param dowTimeIntervals {Object&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;} the parsed open hours, as computed by the <a href="#method_parsePlagesHoraires"><code>parsePlagesHoraires</code></a> method
	 * @param now {Date} the date to use as the current instant
	 * @return {Array&lt;Date&gt;} a list of non worked local dates, in the websurfer time zone
	 * @private
	 */
	function parseJoursFeries(joursFeries, dowTimeIntervals/*, now*/) {
		var nonWorkedLocalDates = [],
			now = arguments[2] || new Date(),
			localDates,
			i, localDate, timeIntervals;
		localDates = toLocalDate(joursFeries, now);
		for (i = 0; i < localDates.length; ++i) {
			localDate = localDates[i];
			timeIntervals = dowTimeIntervals[localDate.getDay()];
			if (!timeIntervals || !hasWorkedInterval_Internal(timeIntervals, localDate, joursFeries)) {
				nonWorkedLocalDates.push(localDate);
			}
		}
		return nonWorkedLocalDates;
	}
	function resetInternalState() {
		this.initialized = false;
		this.showOnNextInit = false;
		this.displayed = false;
		this.dowTimeIntervals = {};
		this.dowHoursMinutes = {};
		this.disabledDaysOfWeek = [];
		this.joursFeries = [];
		this.nonWorkedLocalDates = [];
		this.startDate = null;
		this.endDate = null;
	}
	if (T) {
		// LnkLog.log('registering testables');
		T.parsePlagesHoraires = parsePlagesHoraires;
		T.computeDowHoursMinutes = computeDowHoursMinutes;
		T.computeDisabledDaysOfWeek = computeDisabledDaysOfWeek;
		T.toLocalDate = toLocalDate;
		//T.hasPartialIntervals = hasPartialIntervals;
		T.isWorkedDate = isWorkedDate;
		//T.findNextTimeInterval = findNextTimeInterval;
		T.hasWorkedInterval = hasWorkedInterval_Internal;
		T.parseJoursFeries = parseJoursFeries;
		T.resetInternalState = resetInternalState;
	}

	C = L.Calendar = function (cfg) {
		var that = this;
		this.id = L.Calendars.register(that);
		/**
		 * The current applicable configuration settings.
		 * @property cfg
		 * @type Object
		 * @public
		 */
		that.cfg = U.cloneObject(cfg, defaultSettings);
		that.events = L.events.create(that, defaultEvents);

		resetInternalState.call(that);

		that.onOpenHoursListener = function () {
			that.init.apply(that, arguments);
		}
		if (that.cfg.bouton) {
			that.cfg.bouton.onOpenHours(this.onOpenHoursListener);
		}

		C.onCreate.fire(that);
	};
	/**
	 * Class event. Fires when an <code>LnkWcb.Calendar</code> instance is just created.
	 * @event onCreate
	 * @param bouton {Object} the new instance
	 * @static
	 * @protected
	 */
	C.onCreate = new L.Event(C);
	// class method
	//C.findNextTimeInterval = findNextTimeInterval;

	L.events.register(C.prototype, defaultEvents);

	U.putAll(C.prototype, {
		/**
		 * Get the default <code>LnkWcb.Calendar</code> configuration settings.
		 * @method getDefaultSettings
		 * @return {Object} a clone of the default settings
		 * @protected
		 */
		getDefaultSettings: function () {
			return U.cloneObject(defaultSettings);
		},
		/**
		 * Set the <a href="LnkWcb.Bouton.html"><code>LnkWcb.Bouton</code></a> instance
		 * on which this calendar depends.
		 * @method setBouton
		 * @return {LnkWcb.Bouton} an bouton instance
		 * @public
		 */
		setBouton: function (bouton) {
			if (this.cfg.bouton) {
				this.cfg.bouton.events.onOpenHours.unregister(this.onOpenHoursListener);
			}
			this.cfg.bouton = bouton;
			if (bouton) {
				bouton.onOpenHours(this.onOpenHoursListener);
			}
		},
		/**
		 * Abstract method that should return the selected date and time.
		 * <p>
		 * The returned value should be 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 default implementation always returns <code>undefined</code>
		 * @public
		 */
		getWcbDate: function () { // actually abstract method
		},
		/**
		 * Abstract method. Not documented. Reserved for future use.
		 * @method getDateTime
		 * @return {Object} the selected date, hours and minutes
		 * @private
		 */
		getDateTime: function () { // actually abstract method
			return {
				date: null,
				hours: null,
				minutes: null
			};
		},
		/**
		 * Computes actual open hours in a sparse Array, with values being sparse Arrays of open minutes.
		 * <p>
		 * When requesting the open hours of the current day, this method removes passed open hours.
		 * Those that are before now.
		 * @method computeHoursMinutes
		 * @param date {Date} a date instance (never <code>null</code>) that specifies the selected day (today or a future day, never a passed day) of which open hours will be computed
		 * @param now {Date} an optional date that specifies the value for the current instant
		 * @return {Array&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;}
		 * @protected
		 */
		computeHoursMinutes: function (date/*, now*/) {
			var now = arguments[1] || new Date(),
				hoursMinutes, hour, minutes;
			try {
				hoursMinutes = this.dowHoursMinutes[date.getDay()];
				if (date.getDate() === now.getDate() &&
						date.getMonth() === now.getMonth() &&
						date.getYear() === now.getYear()) {
					hoursMinutes = U.cloneObject(hoursMinutes);
					for (hour = 0; hour < now.getHours(); ++hour) {
						if (hoursMinutes[hour]) {
							delete hoursMinutes[hour];
						}
					}
					if (hoursMinutes[now.getHours()]) {
						hoursMinutes[now.getHours()] = U.cloneObject(hoursMinutes[now.getHours()]);
						for (minutes = 0; minutes < now.getMinutes(); minutes += this.cfg.minutesStep) {
							if (hoursMinutes[now.getHours()][minutes] !== undefined) {
								delete hoursMinutes[now.getHours()][minutes];
							}
						}
					}
					
				}
				return hoursMinutes;
			}
			catch (exc) {
				LnkLog.log('LnkWcb.Calendar.computeHoursMinutes', exc);
			}
		},
		/**
		 * Tells if a <code>Date</code> instance is worked or not,
		 * taking all information into account (open hours and closed days).
		 * @method hasWorkedInterval
		 * @param refIntervals {Array&lt;Array&lt;Array&lt;Number&gt;&gt;&gt;} the dispatched time periods (see the <a href="#method_computeDowHoursMinutes"><code>computeDowHoursMinutes</code></a>) of the day of week of the <code>date</code> argument
		 * @param date {Date} the date to test
		 * @return {Boolean} <code>true</code> if the date is worked, <code>false</code> otherwise
		 * @protected
		 */
		hasWorkedInterval: function (refIntervals, date/*, cache*/) {
			return hasWorkedInterval_Internal(refIntervals, date, this.joursFeries/*, cache*/);
		},
		/**
		 * @method findFirstAvailableDate
		 * @return {Date}
		 * @protected
		 */
		findFirstAvailableDate: function () {
			var date = (this.startDate && new Date(this.startDate.getTime())) || new Date(),
				end = this.endDate || new Date(date.getTime() + 7 * 86400000), // ECMAScript dates have no leap seconds anyway
				timeIntervals, hoursMinutes, h;
			while (date.getTime() < end.getTime()) {
				timeIntervals = this.dowTimeIntervals[date.getDay()];
				if (timeIntervals && this.hasWorkedInterval(timeIntervals, date)) { // general case
					hoursMinutes = this.computeHoursMinutes(date); // handle the 'today' case
					for (h = 0; h < hoursMinutes.length; ++h) {
						if (hoursMinutes[h] && U.countDefined(hoursMinutes[h]) > 0) {
							date.setHours(0, 0, 0, 0);
							return date;
						}
					}
				}
				date.setDate(date.getDate() + 1);
			}
			return null;
		},
		/**
		 * This is <u>the</u> all-in-one method that refreshes the calendar. It asks the bouton for fresh open hours and closed days.
		 * <p>
		 * Get through the following states:
		 * <em>hide</em> (if needed),
		 * <em>destroy</em> (won't do no harm is instance is not yet initialized),
		 * <em>init</em> (after the fresh open hours have been received from the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event),
		 * <em>show</em> (if <a href="#config_showOnInit"><code>showOnInit</code></a> is <code>true</code>, which is the default).
		 * @method refresh
		 * @public
		 */
		refresh: function () {
			this.destroy();
			if (this.cfg.bouton) {
				this.cfg.bouton.horaires();
			}
		},
		/**
		 * Initialize the calendar with fresh open hours and closed days.
		 * <p>
		 * This method is called by default when the
		 * <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event
		 * of the <a href="#config_bouton">associated bouton</a> fires. Thus you should never need to call it directly.
		 * <p>
		 * Perform the transition from <em>destroy</em> state to <em>init</em> state.
		 * <p>
		 * If <a href="#config_showOnInit"><code>showOnInit</code></a> is <code>true</code> (the default),
		 * then the calendar is <a href="#method_show">shown</a> right after it is initialized.
		 * @method init
		 * @param horaires {Object} the open hours (as received by the <a href="LnkWcb.Bouton.html#event_onOpenHours"><code>onOpenHours</code></a> event)
		 * @protected
		 */
		init: function (horaires) { // may be called in any state (initialized or not, displayed or not)
			var startDate, now, todayHoursMinutes, h;
			try {
				this.initialized = false;
				//LnkLog.log('debut LnkWcb.Calendar.init');
				this.dowTimeIntervals = parsePlagesHoraires(horaires.horairesOuverture);
				// logDowTimeIntervals(this.dowTimeIntervals);
				this.dowHoursMinutes = computeDowHoursMinutes(this.dowTimeIntervals, this);
				this.disabledDaysOfWeek = computeDisabledDaysOfWeek(this.dowTimeIntervals);
				this.joursFeries = horaires.joursFeries;
				this.nonWorkedLocalDates = parseJoursFeries(this.joursFeries, this.dowTimeIntervals);
				//LnkLog.log('fin LnkWcb.Calendar.init');

				now = new Date();
				startDate = new Date(now.getTime());
				startDate.setHours(0, 0, 0, 0);
				todayHoursMinutes = this.computeHoursMinutes(startDate, now);
				h = 0;
				while (h < todayHoursMinutes.length && !(todayHoursMinutes[h] && U.countDefined(todayHoursMinutes[h]) > 0)) {
					++h;
				}
				if (h >= todayHoursMinutes.length) {
					startDate.setDate(startDate.getDate() + 1); // properly handles calendar rolling: 2010-01-32 becomes 2010-02-01
				}
				this.startDate = startDate;
				this.endDate = new Date(startDate.getTime());
				this.endDate.setDate(this.endDate.getDate() + this.cfg.maxDays);

				this.events.onInit.fire(horaires);
				this.initialized = true;
				if (this.showOnNextInit || this.cfg.showOnInit) {
					this.showOnNextInit = false;
					this.show();
				}
			}
			catch (exc) {
				LnkLog.log('LnkWcb.Calendar.init', exc);
			}
		},
		/**
		 * Display the calendar to the websurfer.
		 * <p>
		 * Perform the transition from <em>init</em> state to <em>show</em> state.
		 * <p>
		 * If the calendar is not yet <a href="#method_init">initilazed</a>, then it will be shown
		 * right after it is initialized. Even if the <a href="#config_showOnInit"><code>showOnInit</code></a>
		 * config is <code>false</code>.
		 * <p>
		 * Would you create a calendar with <a href="#config_showOnInit"><code>showOnInit</code></a>
		 * being <code>false</code>, this feature would allow you to initialize the calendar with
		 * <a href="LnkWcb.Bouton.html#method_horaires"><code>LnkWcb.Bouton.horaires()</code></a> and then call
		 * <a href="#method_show"><code>show()</code></a> right after <code>horaires()</code> has returned,
		 * which is <u>before</u> the fresh open hours are received from the server,
		 * and thus <u>before</u> the calendar is actually initialized.
		 * @method show
		 * @public
		 */
		show: function () { // only when initialized and not displayed (when not initialized, activate the showOnNextInit)
			try {
				if (!this.initialized) {
					//LnkLog.log('activating the showOnNextInit feature');
					this.showOnNextInit = true;
					return;
				}
				if (!this.displayed) {
					this.events.onShow.fire();
				}
				this.displayed = true;
			}
			catch (exc) {
				LnkLog.log('LnkWcb.Calendar.show', exc);
			}
		},
		/**
		 * Hide the calendar.
		 * <p>
		 * Perform the transition from <em>show</em> state to <em>hide</em> state.
		 * @method hide
		 * @public
		 */
		hide: function () { // only when initialized and displayed
			try {
				if (this.displayed) {
					this.events.onHide.fire(); // TODO: should fire in reverse order
				}
				this.displayed = false;
			}
			catch (exc) {
				LnkLog.log('LnkWcb.Calendar.hide', exc);
			}
		},
		/**
		 * Destroy the calendar.
		 * <p>
		 * Perform the transition from <em>hide</em> state to <em>destroy</em> state.
		 * <p>
		 * If the calendar is not yet hidden, then the <a href="#method_hide"><code>hide</code></a> method is called.
		 * @method destroy
		 * @public
		 */
		destroy: function () { // may be called in any state (initialized or not, displayed or not)
			try {
				if (this.displayed) {
					this.hide();
				}
				this.events.onDestroy.fire(); // TODO: should fire in reverse order
				resetInternalState.call(this);
			}
			catch (exc) {
				LnkLog.log('LnkWcb.Calendar.destroy', exc);
			}
		}
	});
})();
/*
(function () {
	var CAL_CMPT,
		defaultEvents = [ 'onInit', 'onShow', 'onHide', 'onDestroy' ];

	CAL_CMPT = C.Component = function () {
		this.events = L.events.create(this, defaultEvents);
	};

	L.events.register(CAL_CMPT.prototype, defaultEvents);

	U.putAll(CAL_CMPT.prototype, {
		init: function () {
			this.events.onInit.fire();
		},
		show: function () {
			this.events.onShow.fire();
		},
		hide: function () {
			this.events.onHide.fire();
		},
		destroy: function () {
			this.events.onDestroy.fire();
		}
	});
})();
*/

Copyright © 2010 Linkeo.com All rights reserved.