/*!
* 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><form></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><select></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><select></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<Array<Number>>} 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><select></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><select></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><select></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><select></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><select></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><select></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><select></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><select></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><select></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;
});
});
})();