/*!
* 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<String>} 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<Array>} 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<String|Array<String>>}
* @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><script/></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<String>} 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><script/></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><script/></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<String>} 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<String>} 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<Array<String>>} 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—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<Array<String>>} 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<Array<String>>} 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<Array<String>>} 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<Array<String>>} 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 – 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 – 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 – 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 – 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> <String>: the queried channel code. It is the same as the <code>canal</code> config property.</li>
* <li><code>estActif</code> <Boolean>: whether the channel is active or not.</li>
* <li><code>peutRecevoirAppel</code> <Boolean>: whether the channel can receive calls (is under quota) or not (over quota).</li>
* <li><code>estOuvert</code> <Boolean>: whether the channel is open (in open hours at request time) or not.</li>
* <li><code>estSature</code> <Boolean>: whether the channel is overwhelmed (too many concurrent calls) or not.</li>
* <li><code>etatOuverture</code> <String>: 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><form></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><select></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><form></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<String>} 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> <String>: the queried channel code. It is the same as the <code>canal</code> config property.</li>
* <li><code>horairesOuverture</code> <Array>: 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> <Array>: 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<Array<String>>} 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);
}
});
});
})();
})();
})();