/*!
* 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><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: '--',
/**
* 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<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
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><select></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><select></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><select></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><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 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><select></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><select></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;
});
});
})();