LnkWcb Framework

LnkWcb  2.5.0

LnkWcb Framework > LnkWcb > bouton-core.js (source view)
Search:
 
Filters
/*!
 * LNK WCB Framework v2.5
 * (c) 2010 Linkeo.com
 * Use and redistribution are permitted. No warranty.
 * 
 * Core utilities and components
 */
var LnkLog, LnkWcb, // if already defined, log won't get clobbered just defining it
	removeScript, // required by the frontal
	debordementsTest = {}; // required by the fallback system of frontal
LnkLog = LnkLog || {};
LnkLog.log = LnkLog.log || function () {}; // if not defined, use a safe default value instead
LnkWcb = LnkWcb || {};

/**
 * @module LnkWcb
 */
(function () {
	var U, C, T = LnkWcb.Tests && LnkWcb.Tests.testables, RREG, R, E, EU, BREG, BRM, B, BPROTO;

	/**
	 * Utility functions
	 * @class util
	 * @namespace LnkWcb
	 */
	U = LnkWcb.util = LnkWcb.util || {};

	/**
	 * Put all source keys/values into destination. Source is left unchanged. Undefined keys are copied. Destination is modified an returned.
	 * @method putAll
	 * @param dest {Object} destination object
	 * @param src {Object} source object
	 * @return {Object} the modified destination object
	 * @static
	 */
	U.putAll = function (dest, src) {
		var prop;
		for (prop in src) {
			dest[prop] = src[prop];
		}
		return dest;
	};
	U.putAll(U, {
		/**
		 * Create an object secretly linked to the object passed as argument.
		 * @method object
		 * @param o {Object} base object
		 * @return {Object} a newly created object
		 * @static
		 */
		object: function (o) {
			var F = function () {};
			F.prototype = o;
			return new F();
		},
		/**
		 * Tells whether a value is an Array.
		 * It might have been created with the <code>[]</code> literal construct
		 * or with the <code>new Array()</code> statement.
		 * When this method returns <code>true</code>, then the <code>length</code>
		 * attribute does have the special Array length semantic.
		 * @method isArray
		 * @param {Any} any value to test
		 * @return {boolean} <code>true</code> if the value is an Array, <code>false</code> otherwise.
		 * @static
		 */
		isArray: function (o) {
			return typeof o === 'object' && Object.prototype.toString.call(o) === '[object Array]'; // the YUI way
		},
		/**
		 * Creates a new object, puts default keys/values into it, and then source key/value pairs.
		 * The resulting clone is a one-level-deep copy.
		 * If <code>obj</code> is an Array, then the clone will also be an Array.
		 * Approximatively equivalent to <code>U.putAll(U.putAll(U.isArray(obj) ? [] : {}, defaults), obj);</code>
		 * @method cloneObject
		 * @param obj {Object|Array} source object (or array) to be cloned
		 * @param defaults {Object} default values to put first into the clone
		 * @return {Object|Array} a newly created object (an array if <code>obj</code> was).
		 * @static
		 */
		cloneObject: function (obj, defaults) {
			var clone = U.isArray(obj) ? [] : {};
			if (defaults) {
				U.putAll(clone, defaults);
			}
			U.putAll(clone, obj);
			return clone;
		},
		/**
		 * Search an element into an array. Strict equality <code>===</code> is used here.
		 * This method is useful to support old IE versions that don't have the <code>Array.indexOf()</code> method.
		 * @method indexOf
		 * @param array {Array} an array, or any object with a meaningful <code>length</code> attribute
		 * @param elt {Any} an element to look for
		 * @param from {Number} an optional index to start the search from, defaulting to <code>0</code>
		 * @return {number} the index of the first matching element, or <code>-1</code> if element was not found.
		 * @static
		 */
		indexOf: function (array, elt/*, from*/) {
			var len = array.length >>> 0, // ToUint32
				from = Number(arguments[2]) || 0;
			from = (from < 0) ? Math.ceil(from) : Math.floor(from);
			if (from < 0) {
				from += len;
			}
			for (; from < len; ++from) {
				if (from in array && array[from] === elt) {
					return from;
				}
			}
			return -1;
		},
		/*
		/  **
		 * @method remove
		 * @deprecated Use <code>Array.prototype.splice(array, index)</code> instead.
		 *  /
		remove: function (array, elt / *, equals* /) {
			var len = array.length >>> 0, // ToUint32
				idx, foundElt,
				equals = arguments[2] || function (a, b) { return a === b; };
			for (idx = 0; idx < len; ++idx) {
				if (equals(elt, array[idx])) {
					foundElt = array[idx];
					array.splice(idx, 1);
					return foundElt;
				}
			}
		},
		*/
		/**
		 * Format a number as a string. Optionally left-padded with zeroes.
		 * @method toPaddedString
		 * @param number {Number} the number to format
		 * @param length {Number} the minimum length of the resulting string
		 * @param radix {Number} an optional radix, defaulting to <code>10</code>
		 * @return {string} the formated number
		 * @static
		 */
		toPaddedString: function (number, length, radix) {
			var string = number.toString(radix || 10);
			while (length > string.length) {
				string = '0' + string
			}
			return string;
		},
		/**
		 * Format a <code>Date</code> instance as a string. The output complies with the LnkWcb format <code>"DD/MM/YYYY hh:mm"</code>.
		 * @method formatDateTime
		 * @param d {Date} the date instance to format
		 * @return {string} the formated date
		 * @static
		 */
		formatDateTime: function (d) {
			var TPS = U.toPaddedString;
			return TPS(d.getDate(), 2) +
				'/' + TPS(d.getMonth() + 1, 2) +
				'/' + TPS(d.getFullYear(), 2) +
				' ' + TPS(d.getHours(), 2) +
				':' + TPS(d.getMinutes(), 2);
		},
		/**
		 * Builds a string representation of the current websurfer's time offset.
		 * This is not strictly speaking a time zone, but it still helps to get some reasonable default value.
		 * @method buildTimeZone
		 * @param date {Date} an optional date value to get the time offset from
		 * @return {string} the representation of the current time offset
		 * @static
		 */
		buildTimeZone: function (date) {
			var d = date || new Date(),
				offset, hOffset, mOffset,
				TPS = U.toPaddedString;
			offset = d.getTimezoneOffset();
			hOffset = Math.floor(Math.abs(offset) / 60);
			mOffset = Math.abs(offset) % 60;
			return 'GMT' + (offset <= 0 ? '+' : '-') + TPS(hOffset, 2) + ':' + TPS(mOffset, 2);
		},
		/**
		 * Counts defined values in an object or an array. Sparse arrays are supported.
		 * Properties from any secretly linked object in the prototype chain are ignored.
		 * @method countDefined
		 * @param o {Object|Array} an object or an array
		 * @return {number} the number of defined values
		 * @static
		 */
		countDefined: function (o) {
			var i, c = 0;
			if (U.isArray(o)) {
				for (i = 0; i < o.length; ++i) {
					if (o[i] !== undefined) {
						++c;
					}
				}
			}
			else {
				for (i in o) {
					if (o.hasOwnProperty(i) && o[i] !== undefined) {
						++c;
					}
				}
			}
			return c;
		},
		/**
		 * Compute an array of all own keys of an object. Undefined values are taken into account.
		 * Properties from any secretly linked object in the prototype chain are ignored.
		 * @method keySet
		 * @param o {Object} an object
		 * @return {Array&lt;String&gt;} the array of keys
		 * @static
		 */
		keySet: function (o) {
			var p, keys = [];
			for (p in o) {
				if (o.hasOwnProperty(p)) {
					keys[keys.length] = p;
				}
			}
			return keys;
		},
		/**
		 * Accumulate a key/value pair in an object.
		 * Starting from an new empty object, accumulating values produces an object of arrays of values.
		 * @method accu
		 * @param obj {Object} the destination object
		 * @param key {String} the key under which the value has to be accumulated
		 * @param val {Any} any value to accumulate in the destination object
		 * @return {Object&lt;Array&gt;} the modified destination object
		 * @static
		 */
		accu: function (obj, key, val) {
			if (obj[key] === undefined) {
				obj[key] = [ val ];
			}
			else if (U.isArray(obj[key])) {
				obj[key].push(val);
			}
			else {
				obj[key] = [ obj[key], val ];
			}
			return obj;
		},
		/**
		 * Create a <code>Node</code> hierarchy based on some specification.
		 * <p>
		 * This hierarchy lives in some <code>Document</code>, defaulting to the current one.
		 * The specification is based on some data structure made of objects, arrays and strings.
		 * <p>
		 * Mapping rules are the following:
		 * <ul>
		 * <li>(0) <strong>markup</strong> is an <em>element</em> or a string</li>
		 * <li>(1) <strong>element</strong> is a 1-keyed object (tagName: content)</li>
		 * <li>(2) <strong>content</strong> is an 2-elements array (<em>attrs</em>, <em>body</em>), an <em>element</em> or a string</li>
		 * <li>(3) <strong>attrs</strong> is an object (name: value)</li>
		 * <li>(4) <strong>body</strong> is an array of <em>element</em>s or strings, an <em>element</em> or a string</li>
		 * </ul>
		 * 
		 * Empty string is the same as null or undefined. That's why string as body is optional.
		 * String as content is not optional though, but it can be replaced by null or undefined to specify an empty content.
		 * <pre>
		 *	{
		 *		ul: [ { style: 'background-color: gray;' },
		 *				[
		 *				// childrens illustrating rule (4)
		 *					{ li: [ { style: 'color: blue;' }, [ { span: 'toto' }, { span: 'titi' } ] ] },
		 *					{ li: [ { style: 'color: blue;' }, [ { span: 'toto' }, 'titi' ] ] },
		 *					{ li: [ { style: 'color: blue;' }, [ 'toto', { span: 'titi' } ] ] },
		 *					{ li: [ { style: 'color: blue;' }, { span: 'blue item' } ] },
		 *					{ li: [ { style: 'color: blue;' }, 'blue item' ] },
		 *				// childrens illustrating rule (2)
		 *					{ li: [ {}, [ 'black', ' ', 'item' ] ] },
		 *					{ li: [ {}, 'black item' ] },
		 *					{ li: { span: 'black item' } },
		 *					{ li: 'black item' }
		 *				]
		 *			]
		 *		]
		 *	}
		 * <pre>
		 * @method createMarkupNode
		 * @param markup {Object|String} the markup to generate, expressed as a JS value as explained above
		 * @param document {Document} an optional document used to create new <code>Node</code>s
		 * @return {Node} a text <code>Node</code>, an <code>Element</code> or an <code>HTMLElement</code>
		 * @static
		 */
		createMarkupNode: function (markup /*, document*/) {
			var DOC = this.document || arguments[1] || document,
				tagName, elem, content, attrName, i,
				myself = arguments.callee;
			if (typeof markup === 'string') {
				return DOC.createTextNode(markup);
			}
			else if (markup && typeof markup === 'object') {
				try {
					for (tagName in markup) {
						elem = DOC.createElement(tagName);
						content = markup[tagName];
						if (U.isArray(content)) {
							if (content[0] && typeof content[0] === 'object') {
								for (attrName in content[0]) {
									try {
										elem.setAttribute(attrName, content[0][attrName]);
									}
									catch (ignore) {}
								}
							}
							if (U.isArray(content[1])) {
								for (i = 0; i < content[1].length; ++i) {
									if (content[1][i]) {
										elem.appendChild(myself.call(this, content[1][i]));
									}
								}
							}
							else if (content[1]) {
								elem.appendChild(myself.call(this, content[1]));
							}
						}
						else if (content) {
							elem.appendChild(myself.call(this, content));
						}
						return elem;
					}
				}
				catch (ignore) {}
			}
		},
		/**
		 * Decodes a query string encoded set of key/value pairs. Produces an object of Strings or Arrays.
		 * Single values are Strings, and multiple values are arrays of strings.
		 * @method urlDecode
		 * @return {Object&lt;String|Array&lt;String&gt;&gt;}
		 * @static
		 */
		urlDecode: function (queryString) {
			var params = {}, pairs,
				d = decodeURIComponent,
				i, pair, name, value;
			if (queryString) {
				pairs = queryString.split('&');
				for (i = 0; i < pairs.length; ++i) {
					pair = pairs[i].split('=');
					name = d(pair[0]);
					value = d(pair[1]);
					if (params[name] === undefined) {
						params[name] = value;
					}
					else {
						U.accu(params, name, value);
					}
				}
			}
			return params;
		}
	});



	/**
	 * Core utilities functions.
	 * @class core
	 * @namespace LnkWcb
	 * @private
	 */
	C = LnkWcb.core = LnkWcb.core || {};

	(function () {
		/**
		 * Generates a fixed length string of figures. 
		 * @method randomString
		 * @param length {Number} the length of the generated string
		 * @return {String} the random string of figures
		 * @private
		 */
		function randomString(length) {
			var str = "", i = 0;
			for (i = 0; i < length; ++i) {
				str += Math.floor(Math.random() * 10);
			}
			return str;
		}
		if (T) {
			T.randomString = randomString;
		}
		/**
		 * Registry factory method.
		 * @method createRegistry
		 * @param expose {boolean} whether the created registry is <i>exposing</i> registered objects as public members or not
		 * @return {Object} the created registry instance
		 * @static
		 * @protected
		 */
		C.createRegistry = function (expose) {
			var registry = {};
			/**
			 * General purpose registry.
			 * <p>
			 * <i>Exposing</i> registries expose registered objects as public members, indexed by their ID.
			 * For internal use in the framework.
			 * @class Registry
			 * @private
			 */
			return {
				/**
				 * Register an object.
				 * <p>
				 * If the registry is <i>exposing</i> registered objects, then the object becomes
				 * a public member of the registry under the name of the returned ID.
				 * @method register
				 * @param obj {Object} an object to register
				 * @return {String} the generated ID
				 * @protected
				 */
				register: function (obj) {
					var id = randomString(12),
						attempt = 0;
					while (registry[id] && ++attempt < 1000) {
						id = randomString(12);
					}
					registry[id] = obj;
					if (expose) {
						this[id] = obj;
					}
					return id;
				},
				/**
				 * Unregister an object. If available, the <code>.finish()</code> method of the object is called.
				 * @method finish
				 * @param id {String} the unique identifier for the object to finish
				 * @protected
				 */
				finish: function (id) {
					if (registry[id] !== undefined) {
						if (typeof registry[id].finish === 'function') {
							registry[id].finish();
						}
						delete registry[id];
						if (expose) {
							delete this[id];
						}
					}
				}
			};
		};
	})();


	/**
	 * Registry instance used for registering requests.
	 * @class Requests
	 * @namespace LnkWcb
	 * @extends LnkWcb.Registry
	 * @static
	 * @private
	 * @see Registry
	 */
	RREG = LnkWcb.Requests = C.createRegistry();
	/**
	 * Global export of the <a href="LnkWcb.Registry.html#method_finish"><code>LnkWcb.Requests.finish()</code></a> method.
	 * Required by the current server API.
	 * @class removeScript
	 * @namespace (GLOBAL)
	 * @static
	 * @private
	 */
	removeScript = RREG.finish;


	/**
	 * Request abstraction class.
	 * <p>
	 * Uses <code>&lt;script/&gt;</code> element injection in the head of the document.
	 * @class Request
	 * @namespace LnkWcb
	 * @private
	 * @constructor
	 */
	(function () { // LnkWcb.Request
		/**
		 * Create a %-encoded query string from an object of strings.
		 * This is quite an opposite of <a href="LnkWcb.util.html#method_urlDecode"><code>LnkWcb.util.urlDecode()</code></a>.
		 * The difference is that multiple values are not supported.
		 * @method composeQueryString
		 * @param params {Object&lt;String&gt;} the query string parameters
		 * @return {String} the encoded query string
		 * @private
		 */
		function composeQueryString(params) {
			var queryString = '',
				name, value, enc = encodeURIComponent;
			try {
				for (name in params) {
					value = params[name];
					if (value !== null && value !== undefined) {
						if (queryString) {
							queryString += '&';
						}
						queryString += enc(name) + '=' + enc(value);
					}
				}
			}
			catch (exc) {
				LnkLog.log("LnkWcb.Request#composeQueryString", exc);
			}
			return queryString;
		}
		/**
		 * Injects a <code>&lt;script/&gt;</code> element in the head of the document.
		 * <ul>
		 * <li><code>attrs.id</code> specifies the element ID attribute</li>
		 * <li><code>attrs.baseUrl</code> specifies the beginning of the element SRC attribute</li>
		 * <li><code>attrs.params</code> specifies the SRC query string parameters</li>
		 * </ul>
		 * The <code>attrs.params.scriptId</code> gets filled with the ID of the injected element.
		 * So that it is forced for all generated queries.
		 * @method injectScriptElem
		 * @param attrs {Object} attributes of the script element
		 * @param that {Object} an optional object that might provide the <code>Document</code> instance to use (useful for isolation in unit tests)
		 * @private
		 */
		function injectScriptElem(attrs, that) {
			var DOC = (that && that.document) || this.document || document,
				head, elem;
			try {
				head = DOC.getElementsByTagName('head')[0];
				elem = DOC.createElement('script');
				elem.type = 'text/javascript';
				elem.defer = false; // execute ASAP
				elem.id = attrs.id;
				attrs.params.scriptId = attrs.id;
				elem.src = attrs.baseUrl + '?' + composeQueryString(attrs.params);
				head.appendChild(elem);
			}
			catch (exc) {
				LnkLog.log("LnkWcb.Request#injectScriptElem", exc);
			}
		}
		/**
		 * Removes a <code>&lt;script/&gt;</code> element from the head of the document.
		 * @method removeElem
		 * @param elemId {String} the script element ID
		 * @param that {Object} an optional object that might provide the <code>Document</code> instance to use (useful for isolation in unit tests)
		 * @private
		 */
		function removeElem(elemId, that) {
			var DOC = (that && that.document) || this.document || document,
				elem;
			try {
				elem = DOC.getElementById(elemId);
				if (elem) {
					elem.parentNode.removeChild(elem);
				}
			}
			catch (exc) {
				LnkLog.log("LnkWcb.Request#removeElem", exc);
			}
		}
		if (T) {
			// LnkLog.log('registering testables');
			T.composeQueryString = composeQueryString;
			T.injectScriptElem = injectScriptElem;
			T.removeElem = removeElem;
		}

		R = LnkWcb.Request = function () {
			this.id = RREG.register(this);
			this.isSent = false;
		};

		/**
		 * Sends a (cross-domain) request.
		 * <p>
		 * JSONP callback is implemented as it is by default in <a href="http://api.jquery.com/jQuery.ajax/">jQuery.ajax</a>.
		 * This means that the parameter name is <code>callback</code> and not <code style="text-decoration: line-through;">jsonp</code>
		 * as in <a href="http://en.wikipedia.org/wiki/JSON#JSONP">wikipedia</a>
		 * or <a href="http://bob.pythonmac.org/archives/2005/12/05/remote-json-jsonp/">Bob Ippolito's spec</a>.
		 * @method send
		 * @param baseUrl {String} the server base URL
		 * @param params {Object|null} the (optional) query parameters
		 * @param chainFnName {String} an optional JSONP callback name
		 * @public
		 */
		R.prototype.send = function (baseUrl, params, chainFnName) {
			try {
				params = params || {};
				if (chainFnName) {
					params.callback = chainFnName;
				}
				injectScriptElem({
					id: this.id,
					baseUrl: baseUrl,
					params: params
				}, this);
				this.isSent = true;
			}
			catch (exc) {
				LnkLog.log("LnkWcb.Request.send", exc);
			}
		};

		/**
		 * Terminate a request.
		 * <p>
		 * This method is automatically called at the end of the server response execution.
		 * You need not call it directly.
		 * @method finish
		 * @protected
		 */
		R.prototype.finish = function () {
			removeElem(this.id, this);
		};
	})();


	/**
	 * Event abstraction class.
	 * @class Event
	 * @namespace LnkWcb
	 * @constructor
	 * @param target {Object} the source of the event, i.e. the object used as <code>this</code> when calling any listeners.
	 * @private
	 */
	E = LnkWcb.Event = function (target) {
		this.target = target;
		this.listeners = [];
	};
	E.prototype = {
		/**
		 * Controls whether exceptions from listeners are re-thrown or not.
		 * If <code>true</code> and a listener throws an exception, then all subsequent listeners won't get called.
		 * @property rethrow
		 * @type boolean
		 * @private
		 */
		rethrow: false, // customizable re-throw behavior (useful when testing)
		/**
		 * Register a listener.
		 * @method register
		 * @param l {Function} the listener
		 */
		register: function (l) {
			this.listeners[this.listeners.length] = l;
		},
		/**
		 * Unregister a listener.
		 * @method unregister
		 * @param l {Function} the listener
		 * @protected
		 */
		unregister: function (l) {
			var indexOf = U.indexOf,
				idx = indexOf(this.listeners, l);
			while (idx >= 0) {
				this.listeners.splice(idx, 1);
				idx = indexOf(this.listeners, l, idx);
			}
		},
		/**
		 * Fire an event.
		 * <p>
		 * All registered listeners are sequentially called, in the order they were registered.
		 * Whenever a listener returns the (exact) <code>false</code> value, then no further listeners are called.
		 * <p>
		 * Exceptions are caught and logged (if any logger was previously defined).
		 * They are thrown again if the <code>rethrow</code> property is <code>true</code> for this event instance.
		 * @method fire
		 */
		fire: function () {
			var i, cont;
			for (i = 0; i < this.listeners.length && cont !== false; ++i) {
				if (this.listeners[i]) {
					try {
						cont = this.listeners[i].apply(this.target, arguments);
					}
					catch (exc) {
						LnkLog.log("LnkWcb.Event.fire, while calling listeners[" + i + "]", exc);
						if (this.rethrow) {
							if (typeof exc === 'string') {
								try {
									({}).crash();
								}
								catch (e) {
									e.message = exc;
									throw e;
								}
							}
							throw exc;
						}
					}
				}
			}
		}
	}


	/**
	 * Event utility functions.
	 * @class events
	 * @namespace LnkWcb
	 * @static
	 * @private
	 */
	EU = LnkWcb.events = LnkWcb.events || {}; // event util
	/**
	 * Create many events.
	 * @method create
	 * @param that {Object} the source of the event
	 * @param names {Array&lt;String&gt;} a set of event names (avoid any duplicates)
	 * @return {Object} the created events, indexed by their name
	 */
	EU.create = function (that, names) {
		var i, events = {};
		for (i = 0; i < names.length; ++i) {
			if (names[i]) {
				events[names[i]] = new E(that);
			}
		}
		return events;
	};
	/**
	 * Register many events into a prototype.
	 * If <code>obj</code> is the source of the event, then <code>obj.events</code> has to be
	 * an Object of <code>Event</code>s, indexed by their names.
	 * Typically one that has been created by <a href="LnkWcb.events.html#method_create"><code>LnkWcb.events.create()</code></a>.
	 * @method register
	 * @param proto {Object} the prototype
	 * @param names {Array&lt;String&gt;} a set of event names
	 */
	EU.register = function (proto, names) {
		var i;
		for (i = 0; i < names.length; ++i) {
			if (names[i]) {
				proto[names[i]] = (function () {
					var n = names[i];
					return function (fn) {
						var event = this.events[n];
						if (event) {
							event.register(fn);
						}
					};
				})();
			}
		}
	};

	/**
	 * Registry instance used for registering boutons.
	 * @class Boutons
	 * @namespace LnkWcb
	 * @extends LnkWcb.Registry
	 * @static
	 * @private
	 * @see Registry
	 */
	BREG = LnkWcb.Boutons = C.createRegistry(true); // buttons registry

	/**
	 * Outputs a JS expression that references a specific method
	 * of a specific <code>LnkWcb.Bouton</code> object.
	 * @method referencerMethode
	 * @return {String} a JS expression, that can be evaluated in the global namespace
	 * @static
	 * @private
	 */
	BRM = BREG.referencerMethode = function (bouton, methode) {
		return 'LnkWcb.Boutons["' + bouton.id + '"].' + methode;
	};



	/**
	 * Base Bouton.
	 * <p>
	 * The name is a <a href="http://en.wikipedia.org/wiki/Metonymy">metonymy</a>.
	 * This class actually defines instances that handle a WCB <u>form</u>.
	 * Strictly speaking, a "button" is a display trigger for such a form. And this is not handled here.
	 * <p>
	 * This class implements all server features. It does not make any presentation choice, and this is
	 * entirely left to subclasses.
	 * @class Bouton
	 * @namespace LnkWcb
	 * @constructor
	 * @param cfg {Object} the initial configuration settings
	 * @public
	 */
	(function () { // LnkWcb.Bouton
		var HTTP_SCHEME = "http://",
			WCB_SERVER = "wcb.linkeo.com",
			FRONTAL_PATH = "/wcbFrontal/services.do",
			EXTRANET_PATH = "/extranet/bouton/",
			FRONT_URL = HTTP_SCHEME + WCB_SERVER + FRONTAL_PATH,
			BACK_URL = HTTP_SCHEME + WCB_SERVER + EXTRANET_PATH,
			defaultSettings = {
				/**
				 * The channel code.
				 * <p>Refers to a channel configuration in the "WCB Extranet" back-office.
				 * @config canal
				 * @type String
				 * @default null
				 * @public
				 */
				canal: null, // e.g.: 'LINKEODEV.4.2'
				/**
				 * The polling frequency when following call statuses. Expressed in milliseconds.
				 * @config statusPollingTimeout
				 * @type Number
				 * @default 1000 (one second)
				 * @protected
				 */
				statusPollingTimeout: 1000,
				/**
				 * A validation function for <a href="#method_rappeler"><code>rappeler()</code></a> method.
				 * <p>
				 * Arguments are those of the <code>rappeler()</code> method.
				 * Must return a boolean, <code>true</code> indicating the input is correct, <code>false</code> otherwise.
				 * @config validator
				 * @type Function
				 * @default an empty function that always returns true
				 * @public
				 */
				validator: function () {
					return true;
				},
				/**
				 * The current "WCB Frontal" backend base URL. The one currently in use.
				 * @config frontUrl
				 * @type String
				 * @default http://wcb.linkeo.com/wcbFrontal/services.do
				 * @protected
				 */
				frontUrl: FRONT_URL, // "http://wcb.linkeo.com:80/wcbFrontal/services.do",
				/**
				 * The current "WCB Extranet" backend base URL. The one currently in use.
				 * @config backUrl
				 * @type String
				 * @default http://wcb.linkeo.com/extranet/bouton/
				 * @protected
				 */
				backUrl: BACK_URL // "http://wcb.linkeo.com:8080/extranet/bouton/",
			},
			defaultEvents = [
				/**
				 * Fires just before a call-back request is sent to the server. Allows for tweaking the request attributes.
				 * <p>
				 * Very useful to start a spinner while the call is being processed.
				 * @event onSendCall
				 * @param arguments {Array} the arguments (converted to a real array) passed to <a href="#method_rappeler"><code>rappeler()</code></a>
				 * @param attrs {Object} the attributes of the server request to be sent
				 * @public
				 */
				'onSendCall',
				/**
				 * Fires just before a call status is polled from the server. Allows for tweaking the request attributes.
				 * @event onPollStatus
				 * @param attrs {Object} the attributes of the server request to be sent
				 * @protected
				 */
				'onPollStatus',
				/**
				 * Fires when a received call status contains a pressed digit.
				 * The digit might have been pressed by the websurfer or the company agent.
				 * <p>
				 * Parameters are the same as those of the <a href="#event_onStatus"><code>onStatus</code></a> event.
				 * <p>
				 * This event does not shortcut the <code>onStatus</code> event which fires as it would normally.
				 * The difference is that this event fires on <u>all</u> received statuses with digit pressed.
				 * Even if duplicates, usually one per second. On the contrary, the <code>onStatus</code> event
				 * fires only when <u>new</u> statuses are received.
				 * is received.
				 * @event onDigit
				 * @param status {Object} the response object
				 * @param params {Object&lt;Array&lt;String&gt;&gt;} the request parameters
				 * @public
				 */
				'onDigit',
				/**
				 * Fires when some <u>new</u> call status is received (on any side).
				 * <ul>
				 * <li>The string tag in <code>status.uStatus</code> describes the websurfer status.</li>
				 * <li>The string tag in <code>status.aStatus</code> describes the company agent status.</li>
				 * </ul>
				 * At least one of these statuses is new to the boutton&mdash;among all that have already been
				 * received while following the current call. And possibly both are new.
				 * <p>
				 * Status tags are one of the following:
				 * <ul>
				 * <li><code>INCONNU</code>: before dialing</li>
				 * <li><code>APPEL_EN_COURS</code>: while dialing and ringing</li>
				 * <li><code>MESSAGE_BIENVENUE</code> (websurfer only): while the websurfer listen to the welcome message</li>
				 * <li><code>MUSIQUE_ATTENTE</code>: when music on hold is on</li>
				 * <li><code>COMMUNICATION_EN_COURS</code>: when line is up</li>
				 * <li><code>COMMUNICATION_ETABLIE</code>: when both lines are bridged together</li>
				 * <li><code>TOUCHE_APPUYEE</code>: when a key was pressed, with its value in <code>status.uDigit</code> or <code>status.aDigit</code></li>
				 * </ul>
				 * @event onStatus
				 * @param status {Object} the response object
				 * @param params {Object&lt;Array&lt;String&gt;&gt;} the request parameters
				 * @public
				 */
				'onStatus',
				/**
				 * Fires when a immediate call ends, or when a delayed call is accepted.
				 * The final status (in <code>status.status</code>) might be <code>"OK"</code>,
				 * or <code>"OK"</code> with a cause for the failure (in <code>status.cause</code>).
				 * <p>
				 * KO causes are:
				 * <ul>
				 * <li>Websurfer causes
				 *   <ul>
				 *     <li><code>MACHINE</code>: websurfer phone is an answering machine</li>
				 *     <li><code>INABOUTI_INTERNAUTE</code>: websurfer phone is wrong number or does not answer</li>
				 *     <li><code>INVERSE_INABOUTI_INTERNAUTE</code>: same as above, but in reversed mode</li>
				 *     <li><code>RACCROCHE_INTERNAUTE_AVANT_AGENT</code>: websurfer has hanged up before being bridged with a company agent</li>
				 *   </ul></li>
				 * <li>Technical causes</li>
				 *   <ul>
				 *     <li><code>CAUSE_INCONNUE</code>: unknown cause</li>
				 *   </ul></li>
				 * </ul>
				 * @event onEnded
				 * @param status {Object} the response object
				 * @param params {Object&lt;Array&lt;String&gt;&gt;} the request parameters
				 * @public
				 */
				'onEnded',
				/**
				 * Fires when a call-back request is rejected by the server.
				 * <p>
				 * You should not use this event.
				 * Prefer using <a href="#event_onErrorDigest"><code>onErrorDigest</code></a> instead.
				 * @event onError
				 * @param msg {String} a textual description of the error
				 * @param exc {Object} the exception, containing a <code>cause</code> being a kind of error code
				 * @param status {Object} the response object
				 * @param params {Object&lt;Array&lt;String&gt;&gt;} the request parameters
				 * @private
				 */
				'onError'
			];
		/**
		 * Optional request creation factory method.
		 * <p>Internally used for better isolation in unit tests. Usually undefined.
		 * @method createRequest
		 * @param that {Object} the object that might provide a <code>createRequest</code> method to use (usually the <code>LnkWcb.Bouton</code> instance)
		 * @private
		 */
		function newRequest(that) {
			return that.createRequest ? that.createRequest() : new LnkWcb.Request();
		}

		B = LnkWcb.Bouton = function (cfg) {
			this.id = BREG.register(this);
			/**
			 * The current applicable configuration settings.
			 * @property cfg
			 * @type Object
			 * @public
			 */
			this.cfg = U.cloneObject(cfg, defaultSettings);
			this.events = EU.create(this, defaultEvents);
			B.onCreate.fire(this);
		};
		/**
		 * Class event. Fires when an <code>LnkWcb.Bouton</code> instance is just created.
		 * @event onCreate
		 * @param bouton {Object} the new instance
		 * @static
		 * @protected
		 */
		B.onCreate = new E(B);
		BPROTO = B.prototype;
		EU.register(BPROTO, defaultEvents);

		U.putAll(BPROTO, {
			/**
			 * Get the default <code>LnkWcb.Bouton</code> configuration settings.
			 * @method getDefaultSettings
			 * @return {Object} a clone of the default settings
			 * @protected
			 */
			getDefaultSettings: function () {
				return U.cloneObject(defaultSettings);
			},
			/**
			 * Sends a call-back request.
			 * <p>
			 * Validator is called. If OK, the <a href="#event_onSendCall"><code>onSendCall</code></a> event is fired. The the request is sent.
			 * <p>
			 * At response time, listen to
			 * <ul>
			 * <li>the <a href="#event_onErrorDigest"><code>onErrorDigest</code></a> event to handle errors.</li>
			 * <li>the <a href="#event_onEnded"><code>onEnded</code></a> event if you placed a delayed call (immediately fired in this specific case)</li>
			 * <li>the <a href="#event_onStatus"><code>onStatus</code></a> event to follow an immediate call status</li>
			 * <li>the <a href="#event_onEnded"><code>onEnded</code></a> event to handle OK and KO endings</li>
			 * <li>the <a href="#event_onFallback"><code>onFallback</code></a> event to handle fallback cases (might be an initial error or a KO ending)</li>
			 * </ul>
			 * Only one of the events <code>onErrorDigest</code>, <code>onEnded</code> and <code>onFallback</code> is fired.
			 * This point is very important to help you handle all situations.
			 * 
			 * @method rappeler
			 * @param telephone {String} the callee telephone number
			 * @param date {Date} an optional date at which the delayed call should occur
			 * @public
			 */
			rappeler: function (telephone /* , date, ... */) {
				var validator, attrs, req;
				try {
					validator = this.cfg.validator;
					if (validator && !validator.apply(this, arguments)) {
						return;
					}
					delete this.callId;
					attrs = {
						t: 'call',
						codeBouton: this.cfg.canal,
						callee: telephone
					};
					this.receivedUserStatus = {};
					this.receivedAgentStatus = {};
					this.events.onSendCall.fire(Array.prototype.slice.call(arguments, 0), attrs);
					req = newRequest(this);
					req.send(this.cfg.frontUrl, attrs, BRM(this, 'reponseRappeler'));
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton.rappeler', exc);
				}
			},
			/**
			 * Call-back method called after <a href="#method_rappeler"><code>rappeler()</code></a>.
			 * <p>
			 * Receives the response of <code>call</code> and <code>delay</code> services.
			 * @method reponseRappeler
			 * @param resp {Object} the response
			 * @param request {Object} the request (attributes)
			 * @protected
			 */
			reponseRappeler: function (resp, request) {
				var status, params, errMsg;
				try {
					status = resp.responseObj;
					params = request.params; // be careful: all params are strings, wrapped into arrays
					errMsg = status.error;
					if (errMsg) { // non empty error message
						this.traiterErreur(errMsg, status.excObj, status, params);
					}
					else {
						this.callId = status.callId;
						if (String(params.t) === 'call' || String(params.behavior) === 'poll') { // auto-unwrap arrays, so that String(['call']) === 'call'
							this.suivreAppel();
						}
						else {
							this.events.onEnded.fire(status, params);
						}
					}
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton.reponseRappeler', exc);
				}
			},
			/**
			 * Start following the current call status. It's unique ID must be in <code>this.callId</code>.
			 * This method is automatically called by <a href="#method_reponseRappeler"><code>reponseRappeler()</code></a>
			 * when an immediate call-back request is accepted.
			 * Thus it should not be called directly.
			 * It is provided in the prototype in order to allow instance customization.
			 * @method suivreAppel
			 * @protected
			 */
			suivreAppel: function () {
				var timerId, attrs, req,
					WIN = this.window || window,
					that = this;
				try {
					if (!this.callId) {
						return;
					}
					timerId = WIN.setTimeout(function () {
						return that.suivreAppel();
					}, this.cfg.statusPollingTimeout);
					attrs = {
						t: 'lastEvent',
						callId: this.callId,
						timerId: timerId
					};
					this.events.onPollStatus.fire(attrs);
					req = newRequest(this);
					req.send(this.cfg.frontUrl, attrs, BRM(this, 'reponseSuivreAppel'));
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton.suivreAppel', exc);
				}
			},
			/**
			 * Call-back method called after <a href="#method_suivreAppel"><code>suivreAppel()</code></a>.
			 * <p>
			 * Receives the response of <code>lastEvent</code> service and fires
			 * the <a href="#event_onStatus"><code>onStatus</code></a>
			 * or the <a href="#event_onEnded"><code>onEnded</code></a> event (never both).
			 * <p>
			 * The <code>onStatus</code> event is fired only when a new websurfer status
			 * or a new company agent status is received. Any duplicate status won't fire the <code>onStatus</code> event.
			 * With the exception of digit pressed statuses (on both sides) which always do fire the <code>onStatus</code> event.
			 * @method reponseSuivreAppel
			 * @param resp {Object} the response
			 * @param attrs {Object} the request attributes
			 * @protected
			 */
			reponseSuivreAppel: function (resp, attrs) {
				var status, params,
					WIN = this.window || window,
					RUS = this.receivedUserStatus,
					RAS = this.receivedAgentStatus,
					TA = 'TOUCHE_APPUYEE', // DTFM event, which are a special case because they may happen several times
					doFire;
				try {
					status = resp.responseObj;
					params = attrs.params; // be careful: all params are strings, wrapped into arrays
					if (status.ended !== "1") {
						if (status.uStatus === TA || status.aStatus === TA) {
							this.events.onDigit.fire(status, params);
						}
						doFire = false;
						if (!RUS[status.uStatus]) {
							RUS[status.uStatus] = 0;
							doFire = true;
						}
						if (!RAS[status.aStatus]) {
							RAS[status.aStatus] = 0;
							doFire = true;
						}
						if (doFire) {
							++RUS[status.uStatus];
							++RAS[status.aStatus];
							this.events.onStatus.fire(status, params);
						}
					}
					else {
						WIN.clearTimeout(Number(params.timerId)); // auto-unwrap the string-ified timerId long, so that ['25'] becomes 25
						this.events.onEnded.fire(status, params);
						delete this.callId; // delete the callId *after* the event is fired, so that listeners can still access it
					}
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton.reponseSuivreAppel', exc);
				}
			},
			/**
			 * Handle a call-back reject. Receives the response of <code>call</code> or <code>delay</code> services.
			 * This method is automatically called by <a href="#method_reponseRappeler"><code>reponseRappeler()</code></a>
			 * when an call-back request is rejected.
			 * Thus it should not be called directly. It is provided in the prototype in order to allow instances customization.
			 * @method traiterErreur
			 * @param msg {String} a message describing the error
			 * @param exc {Object} the exception, containing a <code>cause</code> being a kind of error code
			 * @param status {Object} the response
			 * @param params {Object} the request attributes
			 * @protected
			 */
			traiterErreur: function (msg, exc, status, params) {
				try {
					this.events.onError.fire(msg, exc, status, params);
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton.traiterErreur', exc);
				}
			}
		});

		// the 'rawErrors' Trait
		(function () {
			var NULL = null,
				rawErrors = [ // Origine des erreurs - I: intégration, F: frontal, E: extranet, C: config (extranet), B: bouton, Q: quotas
					// ServicesServlet
					{ code: NULL, regexp: NULL,
						msg: "Le code bouton est invalide.",
						msgCode: 'techError', errCode: 'I001' }, // erreur technique (intégration)
					{ code: NULL, regexp: /^La date est invalide/,
						msg: NULL /* "La date est invalide. Le format attendu est 'dd/MM/yyyy HH:mm' (ex: '...') .La planification a echoue." */,
						msgCode: 'userErrorDate', errCode: 'U001' }, // erreur utilisateur (saisie)
					{ code: NULL /* "APPEL_EN_COURS" */, regexp: NULL,
						msg: "Erreur: un appel est deja en cours.",
						msgCode: 'userErrorDuplicateCall', errCode: 'U002' }, // erreur utilisateur (double soumission)
					{ code: "CONFIG_SCENARIO", regexp: NULL,
						msg: NULL /* "La configuration serveur du scenario est incorrecte." */,
						msgCode: 'techError', errCode: 'F001' }, // erreur technique (frontal)
					// LnkScenario
					{ code: NULL, regexp: NULL,
						msg: "Invalid track id.",
						msgCode: 'techError', errCode: 'F002' }, // erreur technique (frontal)
					{ code: NULL, regexp: NULL,
						msg: "This track id already exists.",
						msgCode: 'techError', errCode: 'F003' }, // erreur technique (frontal)
					{ code: NULL, regexp: /^Socket closed/,
						msg: NULL /* "Socket closed" */,
						msgCode: 'techError', errCode: 'F004' }, // erreur technique (frontal)
					{ code: "PROBLEME_REQUETE_HTTP", regexp: NULL,
						msg: NULL /* "Probleme requete http vers back office pour codeBouton=... userPhone=... : ..." */,
						msgCode: 'techError', errCode: 'E001' }, // erreur technique (extranet)
					{ code: "PROBLEME_PARSING_REPONSE_BO",
						msg: NULL /* "Probleme parsing reponse back office pour codeBouton=... userPhone=... :..." */,
						regexp: NULL, msgCode: 'techError', errCode: 'E002' }, // erreur technique (extranet)
		
					{ code: NULL /* "MESSAGE_ERREUR_BO" */, regexp: /^Message erreur back office pour codeBouton=.* userPhone=.*\s*:\s*AucunCanal$/,
						msg: NULL /* "Message erreur back office pour codeBouton=... userPhone=... :AucunCanal" */,
						msgCode: 'techError', errCode: 'I003' }, // erreur technique (intégration)
					{ code: NULL /* "MESSAGE_ERREUR_BO" */, regexp: /^Message erreur back office pour codeBouton=.* userPhone=.*\s*:\s*TypeCanalNonRequ\S+table$/,
						msg: NULL /* "Message erreur back office pour codeBouton=... userPhone=... :TypeCanalNonRequêtable" */,
						msgCode: 'techError', errCode: 'I004' }, // erreur technique (intégration)
					{ code: NULL /* "MESSAGE_ERREUR_BO" */, regexp: /^Message erreur back office pour codeBouton=.* userPhone=.*\s*:\s*TypeCanalNonG\S+r\S+$/,
						msg: NULL /* "Message erreur back office pour codeBouton=... userPhone=... :TypeCanalNonGéré" */,
						msgCode: 'techError', errCode: 'E003' }, // erreur technique (extranet)
					{ code: "MESSAGE_ERREUR_BO",
						msg: NULL /* "Message erreur back office pour codeBouton=... userPhone=... :..." */,
						regexp: NULL, msgCode: 'techError', errCode: 'B002' }, // erreur technique (bouton)
		
					{ code: NULL /* "SCENARIO_INVALIDE" */, regexp: /^Le nom du scenario est null ou vide\s+\(codeBouton=.*,\s*userPhone=.*\).$/,
						msg: NULL /* "Le nom du scenario est null ou vide  (codeBouton=..., userPhone=...)." */,
						msgCode: 'techError', errCode: 'C001' }, // erreur technique (config)
					{ code: NULL /* "SCENARIO_INVALIDE" */, regexp: /^Le scenario .* est indefini\s+\(codeBouton=.*,\s*userPhone=.*\).$/,
						msg: NULL /* "Le scenario ... est indefini  (codeBouton=..., userPhone=...)." */,
						msgCode: 'techError', errCode: 'C002' }, // erreur technique (config)
					{ code: "QUOTA_DEPASSE", regexp: NULL, msg: NULL /* "Quota depasse." */,
						msgCode: 'techError', errCode: 'Q001' },
					{ code: NULL /* "APPEL_EN_COURS" */, regexp: NULL,
						msg: "Erreur: un appel de priorite superieure est deja en cours.",
						msgCode: 'userErrorDuplicateCall', errCode: 'U003' }, // erreur utilisateur (soumission)
					{ code: "TELEPHONE_INTERNAUTE_INVALIDE", regexp: NULL,
						msg: NULL /* "Numero de telephone invalide (codeBouton=..., userPhone=...) ." */,
						msgCode: 'userErrorTelephone', errCode: 'U004' },
					{ code: "CLE_CONFIDENTIELLE_INVALIDE", regexp: NULL,
						msg: NULL /* "La cle confidentielle d'acces est invalide(null ou vide)." */,
						msgCode: 'techError', errCode: 'F005' }, // TODO: remove this (see WCB-663)
					{ code: "CHECKSUM_INVALIDE", regexp: NULL,
						msg: NULL /* "Le checksum n'est pas valide" */,
						msgCode: 'techError', errCode: 'I002' },
					{ code: "TELEPHONE_AGENT_INVALIDE", regexp: NULL,
						msg: NULL /* "Le numero agent ... est invalide (codeBouton=..., userPhone=...)." */,
						msgCode: 'techError', errCode: 'C003' },
					{ code: NULL, regexp: /.*/, msg: NULL,
						msgCode: 'techError', errCode: 'B001' } // catch-all pour les erreurs inconnues
				],
				fallbackRawErrors = [
					/*CallException en différé*/ /*{ msg: "Nous ne pouvons donner suite a votre demande.", code: "CAUSE_BLOQUE",
						regexp: NULL, msgCode: 'debBloque' },*/
					/*CallException ou ExceptionWithDebordement*/ {
						code: "CAUSE_BLOQUE", regexp: NULL,
						msg: NULL /* "Nous ne pouvons donner suite a votre demande." */,
						msgCode: 'debErrorBlocked', errCode: 'D001' },
					/*CallException ou ExceptionWithDebordement*/ {
						code: "CAUSE_FERME", regexp: NULL,
						msg: NULL /* "Les bureaux sont actuellement fermés." */,
						msgCode: 'debErrorClosedHour', errCode: 'D002' },
					/*CallException ou ExceptionWithDebordement*/ {
						code: "CAUSE_FERIE", regexp: NULL,
						msg: NULL /* "Les bureaux sont actuellement fermés." */,
						msgCode: 'debErrorClosedDay', errCode: 'D003' },
					/*ExceptionWithDebordement*/ {
						code: "CAUSE_SATURE", regexp: NULL,
						msg: NULL /* "Nombre d'appels maximum atteint." */,
						msgCode: 'debErrorOverwhelmed', errCode: 'D004' }
				],
				CO = U.cloneObject;
			// <a href="http://"></a> // !! KEEP THIS LINE HERE - IT FIXES A YUIDOC BUG THAT DISCARDS THE FOLLOWING YUIDOC BLOCS
			/**
			 * Lookup an error in a database.
			 * Matching algorithm is:
			 * <ul>
			 * <li>(1) Code (strict) equality</li>
			 * <li>(2) Message matching the reference regexp</li>
			 * <li>(3) Message (strict) equality</li>
			 * </ul>
			 * @method find
			 * @param rawErrs {Object} a raw errors database
			 * @param code {String} the cause of the error to look for
			 * @param msg {String} the message of the error to look for
			 * @return {Object} a matching error, if found in the database, otherwise <code>null</code>
			 * @private
			 */
			function find(rawErrs, code, msg) { // algo de matching : (1) égalité du code, (2) matching de la regexp, (3) égalité du message
				var i, err;
				for (i = 0; i < rawErrs.length; ++i) {
					err = rawErrs[i];
					if (err && ((err.code && err.code === code) ||
								(err.regexp && err.regexp.test(msg)) ||
								(err.msg && err.msg === msg))) {
						return err;
					}
				}
				return null;
			}

			/**
			 * Get a copy of the error details database.
			 * @method getRawErrors
			 * @return {Object} a clone of the raw errors database
			 * @static
			 * @protected
			 */
			B.getRawErrors = function () {
				return CO(rawErrors);
			};
			/**
			 * Get a copy of the fallback error details database.
			 * @method getFallbackRawErrors
			 * @return {Object} a clone of the raw errors database
			 * @static
			 * @protected
			 */
			B.getFallbackRawErrors = function () {
				return CO(fallbackRawErrors);
			};
			/**
			 * Add a raw error description in the database.
			 * The <code>rawError</code> is inserted before the last position to keep the catch-all last record.
			 * @method addRawError
			 * @param rawError {Object} the details of some new raw error
			 * @static
			 * @protected
			 */
			B.addRawError = function (rawError) {
				rawErrors.splice(-1, 0, rawError); // insert before last (keeps the catch-all entry at end)
			};
			/**
			 * Add a fallback raw error description in the database.
			 * @method addFallbackRawError
			 * @param fallbackRawError {Object} the details of some fallback error
			 * @static
			 * @protected
			 */
			B.addFallbackRawError = function (fallbackRawError) {
				fallbackRawErrors.push(fallbackRawError); // insert last
			};
			/**
			 * Find an error details.
			 * Any error always match because the database maintains a catch-all record in last position.
			 * @method findRawError
			 * @param code {String} the error code
			 * @param msg {Object} the error message
			 * @return {Object} the details of the matching error (never <code>null</code>)
			 * @static
			 * @protected
			 */
			B.findRawError = function (code, msg) {
				return find(rawErrors, code, msg);
			};
			/**
			 * Find a fallback error details.
			 * This lookup differs from <a href="#method_findRawError"><code>findRawError()</code></a>
			 * in that it might fail and return <code>null</code>.
			 * @method findFallbackRawError
			 * @param code {String} the error code
			 * @param msg {Object} the error message
			 * @return {Object} the details of a matching fallback error, or <code>null</code> otherwise 
			 * @static
			 * @protected
			 */
			B.findFallbackRawError = function (code, msg) {
				return find(fallbackRawErrors, code, msg);
			};
		})();

		// the 'fallback' Trait
		(function () {
			var fallbackDefaultEvents = [
					/**
					 * Fires on a fallback condition.
					 * <p>This might be an error (when a call is rejected) or a KO ending
					 * (when a call ends with a <code>"KO"</code> final status),
					 * but you won't have to know because <a href="#event_onEnded"><code>onEnded</code></a>
					 * won't be fired, nor <a href="#event_onErrorDigest"><code>onErrorDigest</code></a>,
					 * nor <a href="#event_onError"><code>onError</code></a>.
					 * @event onFallback
					 * @param status {Object} the response object
					 * @param params {Object&lt;Array&lt;String&gt;&gt;} the request parameters
					 * @public
					 */
					'onFallback'
				],
				fallbackKoStatuses = [ "INABOUTI_AGENT", "INVERSE_INABOUTI_AGENT", "NON_CONFIRMATION_APPEL_AGENT", "RACCROCHE_AGENT_AVANT_INTERNAUTE" ];

			U.putAll(defaultSettings, {
				/**
				 * Activate the (deprecated) compatibility mode in handling fallbacks.
				 * <p>
				 * <code>false</code> by default. Do not change it unless you know what you are doing.
				 * @config serverSideManagedFallbacks
				 * @type Boolean
				 * @default false
				 * @private
				 * @deprecated Fallbacks should be handled by web integration not controlled by server-side configuration.
				 */
				serverSideManagedFallbacks: false, // you are not encouraged to set this to true (only for backward compatibility)
				/**
				 * Activate fallbacks (on errors or KO endings) even if none is set up in the channel setup
				 * (of the "WCB Extranet" Back-Office).
				 * <p>
				 * <code>true</code> by default here, which is the recommended value.
				 * Indeed fallbacks should be handled by web integration not controlled by server-side configuration.
				 * <p>
				 * Do not set it to <code>false</code> unless you really know what you are doing. This will revert to
				 * compatibility mode which is now deprecated.
				 * @config forceFallbacks
				 * @type Boolean
				 * @default true
				 * @private
				 */
				forceFallbacks: true // <- client-side forced fallbacks always fire all fallback events, event if not setup in channel config
			});

			defaultEvents = defaultEvents.concat(fallbackDefaultEvents);
			EU.register(BPROTO, fallbackDefaultEvents);

			U.putAll(BPROTO, {
				/**
				 * Called by the server response when a falback occurs.
				 * Only valid in the <code>serverSideManagedFallbacks=false</code> (deprecated) compatibility mode.
				 * @method deborder
				 * @param resp {Object} the response
				 * @param request {Object} the request (attributes)
				 * @protected
				 * @deprecated Fallbacks should be handled by web integration not controlled by server-side configuration.
				 */
				deborder: function (resp, request) { // appelé en + du suivi d'appel, si débt server-side, et une seule fois par callId
					var status, params;
					try {
						status = resp.responseObj;
						params = request.params; // be careful: all params are strings, wrapped into arrays
						this.events.onFallback.fire(status, params);
					}
					catch (exc) {
						LnkLog.log('LnkWcb.Bouton.deborder', exc);
					}
				}
			});
			B.onCreate.register(function (that) {
				that.onSendCall(function (args, attrs) {
					if (this.cfg.serverSideManagedFallbacks) {
						attrs.debordementHandler = BRM(this, 'deborder');
					}
				});
				that.onPollStatus(function (attrs) {
					if (this.cfg.serverSideManagedFallbacks) {
						attrs.debordementHandler = BRM(this, 'deborder');
					}
				});
				that.onEnded(function (status, params) {
					if (!this.cfg.serverSideManagedFallbacks) {
						if (status.debordementType) {
							status.debordementCause = status.debordementCause || 'CAUSE_DEBORDE'; // Fixup cause. See http://jira.linkeo.com/browse/WCB-664
							this.events.onFallback.fire(status, params);
							return false; // <- client-side managed fallbacks allow a clear separation of ended and fallback events
						}
						else if (this.cfg.forceFallbacks && status.status === 'KO' && U.indexOf(fallbackKoStatuses, status.cause) >= 0) {
							status.debordementCause = 'CAUSE_DEBORDE';
							this.events.onFallback.fire(status, params); // <- client-side forced fallbacks always fire all fallback events, event if not setup in channel config
							return false;
						}
					}
				});
				that.onError(function (msg, exc, status, params) {
					var err, cause;
					if (!this.cfg.serverSideManagedFallbacks && this.cfg.forceFallbacks) {
						cause = exc && exc.cause;
						err = B.findFallbackRawError(cause, msg);
						if (err) { // <- client-side forced fallbacks always fire all fallback events, event if not setup in channel config
							status.debordementCause = cause;
							this.events.onFallback.fire(status, params);
							return false;
						}
					}
				});
			});
		})();

		// the 'delayedCall' Trait
		(function () {
			U.putAll(defaultSettings, {
				/**
				 * The current websurfer timezone.
				 * <p>
				 * By default, this is set to the current websurfer time offset with 
				 * <a href="LnkWcb.util.html#method_buildTimeZone"><code>LnkWcb.util.buildTimeZone()</code></a>.
				 * This gives something like <code>"GMT+02:00"</code>.
				 * If you get a better information (or ask it directly to the websurfer), then you can put any ID
				 * from this <a href="http://joda-time.sourceforge.net/timezones.html">list of available time zones</a>.
				 * @config timeZone
				 * @type String
				 * @default the websurfer time offset, such as <code>"GMT+02:00"</code> for instance
				 * @protected
				 */
				timeZone: U.buildTimeZone() // auto-sense time-zone, but full text tz like 'Europe/Paris' is also OK
			});
			B.onCreate.register(function (that) {
				that.onSendCall(function (args, attrs) {
					var date = args[1];
					if (date) {
						attrs.t = 'delay';
						attrs.planedDate = date; // be careful to this mis-spelled 'planed' instead of 'planned'
						attrs.tz = this.cfg.timeZone;
					}
				});
			});
		})();

		// the 'https' Trait
		(function () {
			var HTTPS_SCHEME = "https://";
			function setUrls(that) {
				var cfg = that.cfg;
				cfg.frontUrl = cfg.https ? cfg.frontUrlHttps : cfg.frontUrlHttp;
				cfg.backUrl = cfg.https ? cfg.backUrlHttps : cfg.backUrlHttp;
			}
			U.putAll(defaultSettings, {
				/**
				 * Standard URL of the "WCB Frontal" server &ndash; HTTP.
				 * @config frontUrlHttp
				 * @type String
				 * @default http://wcb.linkeo.com/wcbFrontal/services.do
				 * @protected
				 */
				frontUrlHttp: FRONT_URL, // 80
				/**
				 * Standard URL of the "WCB Extranet" Back-Office server &ndash; HTTP.
				 * @config backUrlHttp
				 * @type String
				 * @default http://wcb.linkeo.com/extranet/bouton/
				 * @protected
				 */
				backUrlHttp: BACK_URL, // 80 or 8080
				/**
				 * Secure URL of the "WCB Frontal" server &ndash; HTTPS.
				 * @config frontUrlHttps
				 * @type String
				 * @default https://wcb.linkeo.com/wcbFrontal/services.do
				 * @protected
				 */
				frontUrlHttps: HTTPS_SCHEME + WCB_SERVER + FRONTAL_PATH, // 443
				/**
				 * Secure URL of the "WCB Extranet" Back-Office server &ndash; HTTPS.
				 * @config backUrlHttps
				 * @type String
				 * @default https://wcb.linkeo.com/extranet/bouton/
				 * @protected
				 */
				backUrlHttps: HTTPS_SCHEME + WCB_SERVER + EXTRANET_PATH, // 443 or 8443
				/**
				 * Whether to use HTTPS (if <code>true</code>) or HTTP (if <code> false</code>).
				 * <p>
				 * Only use this at creation time. To change this setting in live,
				 * call the <a href="#method_useHttps"><code>useHttps()</code></a> instead.
				 * <p>
				 * In standard use cases, you should not need modifying this setting,
				 * because it is auto-configured based on the current window URL.
				 * @config https
				 * @type Boolean
				 * @default true if <code>document.location.protocol</code> is <code>'https:'</code> at instanciation time, false otherwise
				 * @protected
				 */
				https: false
			});

			B.onCreate.register(function (that) {
				that.cfg.https = (document.location.protocol === 'https:'); // auto setup
				setUrls(that);
			});
			/**
			 * Force the use of HTTP or HTTPS queries.
			 * <p>
			 * This method has to be used instead of modifying the <code>https</code> config directly.
			 * <p>
			 * In standard use cases, you should not need calling this method.
			 * Because the <code>https</code> config is auto-configured,
			 * based on the current window URL.
			 * @method useHttps
			 * @param https {Boolean} whether to force the use of HTTPS (if <code>true</code>) or HTTP (if <code>false</code>)
			 * @protected
			 */
			BPROTO.useHttps = function (https) {
				this.cfg.https = https;
				setUrls(this);
			};
		})();

		// the 'channelState' Trait
		(function () {
			defaultEvents.push('onChannelState');
			EU.register(BPROTO, [
				/**
				 * Fires when the channel state is received.
				 * <p>
				 * The server response is an object that contains these members:
				 * <ul>
				 * <li><code>codeCanal</code> &lt;String&gt;: the queried channel code. It is the same as the <code>canal</code> config property.</li>
				 * <li><code>estActif</code> &lt;Boolean&gt;: whether the channel is active or not.</li>
				 * <li><code>peutRecevoirAppel</code> &lt;Boolean&gt;: whether the channel can receive calls (is under quota) or not (over quota).</li>
				 * <li><code>estOuvert</code> &lt;Boolean&gt;: whether the channel is open (in open hours at request time) or not.</li>
				 * <li><code>estSature</code> &lt;Boolean&gt;: whether the channel is overwhelmed (too many concurrent calls) or not.</li>
				 * <li><code>etatOuverture</code> &lt;String&gt;: the open status, which one of these 3 values
				 *   <ul>
				 *     <li><code>OUVERT</code>: when in open hours (OPEN)</li>
				 *     <li><code>FERME</code>: when out of open hours (CLOSED)</li>
				 *     <li><code>FERIE</code>: when in open hours BUT an exceptionally closed day (HOLIDAY)</li>
				 *   </ul>
				 * </li>
				 * </ul>
				 * The digested channel status, is one of these values:
				 * <ul>
				 * <li><code>INACTIF</code>: not active.</li>
				 * <li><code>HORS_LIMITES</code>: over quota.</li>
				 * <li><code>SATURE</code>: overwhelmed.</li>
				 * <li><code>FERME</code>: closed (i.e. out of open hours).</li>
				 * <li><code>FERIE</code>: holiday (in open hours).</li>
				 * <li><code>OUVERT</code>: open.</li>
				 * </ul>
				 * @event onChannelState
				 * @param response {Object} the server response
				 * @param etatOuverture {String} a digest channel status (recommended)
				 * @public
				 */
				'onChannelState'
			]);

			U.putAll(BPROTO, {
				/**
				 * Queries the back-end for the channel state.
				 * <p>
				 * At response time, listen to the <a href="#event_onChannelState"><code>onChannelState</code></a>
				 * event to perform actions based on the channel state.
				 * @method estOuvert
				 * @public
				 */
				estOuvert: function () {
					var req, url;
					try {
						req = newRequest(this);
						url = this.cfg.backUrl + "estOuvert/" + this.cfg.canal;
						req.send(url, null, BRM(this, 'reponseEstOuvert'));
					}
					catch (exc) {
						LnkLog.log('LnkWcb.Bouton.estOuvert', exc);
					}
				},
				/**
				 * Call-back method called after <a href="#method_estOuvert"><code>estOuvert()</code></a>.
				 * <p>
				 * Fires the the <a href="#event_onChannelState"><code>onChannelState</code></a> event.
				 * @method reponseEstOuvert
				 * @param reponse {Object} the server response
				 * @protected
				 */
				reponseEstOuvert: function (reponse){
					var etatOuverture;
					try {
						if (!reponse.error) {
							etatOuverture = !reponse.estActif ? 'INACTIF' : (!reponse.peutRecevoirAppel ? 'HORS_LIMITES' : (reponse.estSature ? 'SATURE' : reponse.etatOuverture));
							this.events.onChannelState.fire(reponse, etatOuverture);
						}
					}
					catch (exc) {
						LnkLog.log('LnkWcb.Bouton.reponseEstOuvert', exc);
					}
				}
			});
		})();

		// the 'grabForm' Trait
		(function () {
			/**
			 * Extract the data of a specific <code>&lt;form&gt;</code> element.
			 * <p>
			 * This method iterates over the form inputs, and build an object containing
			 * fields valued, indexed by fields names.
			 * <p>
			 * Multiple values are not supported.
			 * Only the first selected option of a multiple <code>&lt;select&gt;</code> element is returned.
			 * If many fields have the same name, then the value of the last one is returned.
			 * 
			 * @method grabFormFields
			 * @param elemId {String} a <code>&lt;form&gt;</code> element ID
			 * @return {Object}
			 * @private
			 */
			function grabFormFields(elemId) {
				var fields = {},
					DOC = this.document || document,
					form, i, e;
				try {
					form = DOC.getElementById(elemId);
					if (form && form.elements) {
						for (i = 0; i < form.elements.length; ++i) {
							e = form.elements[i];
							if (e && e.name && e.value !== undefined) {
								fields[e.name] = e.value;
							}
						}
					}
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton#grabFormFields', exc);
				}
				return fields;
			}
			/**
			 * Collects successive window openers locations if any.
			 * <p>
			 * First array element is the window location.
			 * (Thus the returned array contains at least one element.)
			 * Subsequent values are successive window openers locations, if any.
			 * @method collectOpeners
			 * @return {Array} the location URLs of the window and its openers openers if any
			 * @private
			 */
			function collectOpeners() {
				var openers = [],
					o = this.window || window;
				try {
					while (o) {
						openers[openers.length] = o.location;
						o = o.opener;
					}
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton#collectOpeners', exc)
				}
				return openers;
			}
			/**
			 * Transforms an array of strings into a JSON string.
			 * The array is reversed while being serialized.
			 * This method does not support double quotes in input strings.
			 * @method serializeArrayReverse
			 * @param a {Array&lt;String&gt;} an array of strings
			 * @return {String} the reversed array serialized as JSON
			 * @private
			 */
			function serializeArrayReverse(a) {
				var str = "[",
					i;
				try {
					if (!a) {
						return null;
					}
					if (a.length) {
						for (i = a.length - 1; i >= 0; --i) {
							str += '"' + a[i] + '"';
							if (i > 0) {
								str += ",";
							}
						}
					}
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton#serializeArrayReverse', exc)
				}
				return str + "]";
			}
			if (T) {
				T.grabFormFields = grabFormFields;
				T.collectOpeners = collectOpeners;
				T.serializeArrayReverse = serializeArrayReverse;
			}

			U.putAll(defaultSettings, {
				/**
				 * The HTML ID of some form to "grab".
				 * <p>
				 * <code>null</code> by default. If set to some "truthy" value, it enable the "form-grabbing" feature.
				 * <p>
				 * If enabled, the "grab" feature will collect the values of all fields in the form and
				 * send them along with any call-back request (as supplementary parameters).
				 * Multiple values in the form are not supported.
				 * <p>
				 * The "grab" feature will also collect window location and any window openers locations.
				 * These will be reversed and stored as JSON in the <code>userUrls</code> request attribute.
				 * @config grabbedFormId
				 * @type String
				 * @default null
				 * @public
				 */
				grabbedFormId: null // an HTML ID (like "LnkWcbForm") to enable form-grabbing
			});

			B.onCreate.register(function (that) {
				that.onSendCall(function (args, attrs) {
					var fields;
					if (this.cfg.grabbedFormId) {
						fields = grabFormFields(this.cfg.grabbedFormId);
						fields.userUrls = serializeArrayReverse(collectOpeners()); // actually no use reversing it here: should be done in server if really needed
						U.putAll(attrs, fields);
					}
				});
			});
		})();

		// the 'hangup' Trait
		(function () {
			/**
			 * Hangs up a call.
			 * @method raccrocher
			 * @public
			 */
			BPROTO.raccrocher = function () {
				var attrs, req;
				try {
					if (!this.callId) {
						return;
					}
					attrs = {
						t: 'hangup',
						callId: this.callId
					};
					req = newRequest(this);
					req.send(this.cfg.frontUrl, attrs);
				}
				catch (exc) {
					LnkLog.log('LnkWcb.Bouton.hangup', exc);
				}
			};
		})();

		// the 'horaires' Trait
		(function () {
			defaultEvents.push('onOpenHours');
			EU.register(BPROTO, [
				/**
				 * Fires when the channel open hours are known.
				 * <p>
				 * The server response is an object that contains these members:
				 * <ul>
				 * <li><code>codeCanal</code> &lt;String&gt;: the queried channel code. It is the same as the <code>canal</code> config property.</li>
				 * <li><code>horairesOuverture</code> &lt;Array&gt;: a list of open hours schedules, which are objects containing these members
				 *   <ul>
				 *     <li><code>debut</code>: start of open schedule (websurfer time zone)</li>
				 *     <li><code>fin</code>: end of open schedule (websurfer time zone)</li>
				 *     <li><code>debutHeureCanal</code>: start of open schedule (channel time zone)</li>
				 *     <li><code>finHeureCanal</code>: end of open schedule (channel time zone)</li>
				 *   </ul>
				 *   <p>An empty array means that the channel is never open.
				 *   <p>All hours are ISO compliant partial dates encoded as Strings.
				 *   They contain the week day, the hours of day, and minutes of hour.
				 *   For example, <code>"-W-1T14:55"</code> means Monday on 14:55 (2:55 PM). Hours are 24-hours based.
				 * </li>
				 * <li><code>joursFeries</code> &lt;Array&gt;: a list of holidays, which are objects containing this member
				 *   <ul>
				 *     <li><code>date</code>: a partial date, which might be yearly repeatable</li>
				 *   </ul>
				 *   <p>An empty array means the channel has no exception to its weekly schedule.
				 *   <p>Dates are ISO compliant partial dates encoded as Strings.
				 *   They contain the month of year, the day of month, and optionally a 4 digits year.
				 *   For instance, <code>{date: "2010-07-14"}</code> means the 14th of July, 2010.
				 *   And <code>{date: "--08-15"}</code> is the 15th of August, applicable every year.
				 * </li>
				 * </ul>
				 * @event onOpenHours
				 * @param response {Object} the server response
				 * @protected
				 */
				'onOpenHours'
			]);

			U.putAll(BPROTO, {
				/**
				 * Queries the back-end for the channel open hours.
				 * <p>
				 * At response time, listen to the <a href="#event_onOpenHours"><code>onOpenHours</code></a> event
				 * to perform actions based on the channel open hours.
				 * <p>
				 * In standard use cases, you need not listen to this event because
				 * any <code>LnkWcb.Calendar</code> (and subclasses) will do it automatically for you.
				 * @method horaires
				 * @public
				 */
				horaires: function () {
					var req, url, attrs;
					try {
						req = newRequest(this);
						url = this.cfg.backUrl + "horaires/" + this.cfg.canal;
						attrs = {
							tz: this.cfg.timeZone
						};
						req.send(url, attrs, BRM(this, 'reponseHoraires'));
					}
					catch (exc) {
						LnkLog.log('LnkWcb.Bouton.horaires', exc);
					}
				},
				/**
				 * Receives the requested open hours
				 * and fires the <a href="#event_onOpenHours"><code>onOpenHours</code></a> event.
				 * @method reponseHoraires
				 * @protected
				 */
				reponseHoraires: function (reponse){
					try {
						if (!reponse.error) {
							this.events.onOpenHours.fire(reponse);
						}
					}
					catch (exc) {
						LnkLog.log('LnkWcb.Bouton.reponseHoraires', exc);
						if (E.prototype.rethrow) {
							throw exc;
						}
					}
				}
			});
		})();

		// the 'errorDigest' Trait
		(function () {
			defaultEvents.push('onErrorDigest');
			EU.register(BPROTO, [
				/**
				 * Fires when a call-back request is rejected by the server.
				 * <p>
				 * Provides value-added information about the error, such as a message code and an error code.
				 * Please refer to the <a href="http://wiki.linkeo.com/rd/WCB_v2/D%C3%A9pannage#Codes_d%27erreur">documentation</a>
				 * for details about those codes.
				 * <p>
				 * Message codes are:
				 * <ul>
				 * <li><code>userErrorTelephone</code>: invalid phone number</li>
				 * <li><code>userErrorDate</code>: invalid date for delayed call</li>
				 * <li><code>userErrorDuplicateCall</code>: duplicate request</li>
				 * <li><code>techError</code>: technical error</li>
				 * </ul>
				 * When the <a href="#config_forceFallbacks"><code>forceFallbacks</code></a> config is set to
				 * <code>false</code> (not recommended, and deprecated), then other message code might occur
				 * for fallbacks: <code>debErrorBlocked</code>, <code>debErrorClosedHour</code>,
				 * <code>debErrorClosedDay</code>, and <code>debErrorOverwhelmed</code>. You should never need
				 * these. They are listed here only for comprehensiveness.
				 * @event onErrorDigest
				 * @param msgCode {String} a message code for the error
				 * @param errCode {String} an error code for the error
				 * @param status {Object} the response object
				 * @param params {Object&lt;Array&lt;String&gt;&gt;} the request parameters
				 * @public
				 */
				'onErrorDigest'
			]);

			B.onCreate.register(function (that) {
				that.onError(function (msg, exc, status, params) {
					var err, cause = exc && exc.cause;
					err = B.findFallbackRawError(cause, msg) || B.findRawError(cause, msg); // findRawErrors always return non-null match (because of the catch-all entry)
					if (err) {
						this.events.onErrorDigest.fire(err.msgCode, err.errCode, status, params);
					}
				});
			});
		})();

	})();

})();

Copyright © 2010 Linkeo.com All rights reserved.