1 /*
  2  * SIMPLICITE - runtime & framework
  3  * http://www.simplicite.fr
  4  * Copyright (c)2006-2013 Simplicite Software. All rights reserved.
  5  */
  6 
  7 /**
  8  * UI toolkit (requires jQuery with $j alias).
  9  * @class
 10  */
 11 Simplicite.UI = {
 12 
 13 /**
 14  * Returns text for specified code (if no text is found code is returned).
 15  * @param code Text code
 16  * @function
 17  */
 18 T: function(code) {
 19 	var t = Simplicite.TEXT !== undefined ? Simplicite.TEXT[code] : code;
 20 	return t === undefined ? code : t;
 21 },
 22 
 23 /** @ignore */
 24 _spec: function(spec) {
 25 	return spec === undefined ? {} : (typeof spec === "function" ? { callback: spec } : spec);
 26 },
 27 /** @ignore */
 28 _target: function(target) {
 29 	if (target !== undefined && typeof(target) == "string")
 30 		return $j("#" + target);
 31 	else
 32 		return target;
 33 },
 34 
 35 /** @ignore */
 36 _style: function(spec) {
 37 	var s = "";
 38 	if (spec === undefined) return s;
 39 	if (spec.width !== undefined && spec.width > 0) s += "width: " + spec.width + "px;";
 40 	if (spec.height !== undefined && spec.height > 0) s += "height: " + spec.height + "px;";
 41 	if (spec.top !== undefined) s += "top: " + spec.top + "px;";
 42 	if (spec.bottom !== undefined) s += "bottom: " + spec.bottom + "px;";
 43 	if (spec.left !== undefined) s += "left: " + spec.left + "px;";
 44 	if (spec.right !== undefined) s += "right: " + spec.right + "px;";
 45 	if (spec.style !== undefined) s += spec.style;
 46 	return s;
 47 },
 48 
 49 /**
 50  * Returns a new div.
 51  * @param spec Specification:
 52  * <ul>
 53  * <li>id: DOM ID</li>
 54  * <li>name: Optional name</li>
 55  * <li>width: Width in px</li>
 56  * <li>height: Height in px</li>
 57  * <li>style: CSS styles</li>
 58  * </ul>
 59  * @function
 60  */
 61 div: function(spec) {
 62 	spec = this._spec(spec);
 63 	var p = { id: spec.id, style: this._style(spec) };
 64 	if (spec.name) p.name = spec.name;
 65 	return $j("<div/>", p).append(spec.content);
 66 },
 67 
 68 /**
 69  * Returns a new button.
 70  * @param spec Specification:
 71  * <ul>
 72  * <li>id: DOM ID</li>
 73  * <li>label: Label</li>
 74  * <li>click: Click handler</li>
 75  * <li>color: Color (red, green, blue, orange, purple)</li>
 76  * <li>style: CSS styles</li>
 77  * </ul>
 78  * @function
 79  */
 80 button: function(spec) {
 81 	spec = this._spec(spec);
 82 	return $j("<input/>", { type: "button", id: spec.id, style: this._style(spec) }).addClass("ui_button" + (spec.color === undefined ? "" : "_" + spec.color)).val(spec.label).click(spec.click);
 83 },
 84 
 85 image: function(name) {
 86 	return $j("<img/>", { src: name != undefined ? Simplicite.ROOT + "/images/image/" + name : undefined });
 87 },
 88 
 89 icon: function(name) {
 90 	return $j("<img/>", { src: name != undefined ? Simplicite.ROOT + "/images/icon/" + name : undefined });
 91 },
 92 
 93 /**
 94  * Returns a new image button.
 95  * @param spec Specification:
 96  * <ul>
 97  * <li>id: DOM ID</li>
 98  * <li>src: Image source</li>
 99  * <li>label: Label set as alt text and title</li>
100  * <li>click: Click handler</li>
101  * <li>style: CSS styles</li>
102  * </ul>
103  * @function
104  */
105 imageButton: function(spec) {
106 	spec = this._spec(spec);
107 	var src = spec.src;
108 	if (src === undefined) {
109 		if (spec.icon !== undefined) src = Simplicite.ROOT + "/images/icon/" + spec.icon;
110 		else if (spec.image !== undefined) src = Simplicite.ROOT + "/images/image/" + spec.image;
111 	}
112 	return $j("<img/>", { id: spec.id, src: src, alt: spec.label, title: spec.label, style: this._style(spec) }).addClass("ui_button").click(spec.click);
113 },
114 
115 /**
116  * Returns a new form.
117  * @param spec Specification:
118  * <ul>
119  * <li>id: DOM ID</li>
120  * <li>action: Form action URL</li>
121  * <li>method: Form method</li>
122  * <li>multipart: Multipart form ?</li>
123  * <li>submit: Submit handler (overrides default submit)</li>
124  * <li>style: CSS styles</li>
125  * </ul>
126  * @function
127  */
128 form: function(spec) {
129 	var ui = this;
130 	spec = this._spec(spec);
131 	var f = $j("<form/>", { id: spec.id, method: spec.method, action: spec.action, style: this._style(spec) });
132 	if (spec.multipart == true)
133 		f.attr("enctype", "multipart/form-data");
134 	if (spec.submit !== undefined)
135 		f.submit(function() {
136 			spec.submit.call(ui, f);
137 			return false;
138 		});
139 	return f;
140 },
141 
142 /**
143  * Returns a new iframe.
144  * @param url Source URL
145  */
146 iframe: function(url) {
147 	return $j("<iframe/>", { style: "border: none; margin: 0px; padding: 0px; width: 100%; height: 100%;", frameborder: "no", scrolling: "auto", src: url });
148 },
149 
150 /**
151  * Is a popup active ?
152  */
153 popupActive: false,
154 
155 /**
156  * Returns and display a modal popup.
157  * <br/>Sample usage:
158  * <pre>
159  * popup({ width: 300, height: 200, beforeClose: function() { console.log("Popup closing..."); return true; }).append("<p>Hello world</p>");
160  * </pre>
161  * @param spec Specification:
162  * <ul>
163  * <li>name: Popup name (ui_popup default)</li>
164  * <li>content: Popup content</li>
165  * <li>url: URL content (in an IFRAME)</li>
166  * <li>afterOpen After open handler</li>
167  * <li>closeable: Closeable ?</li>
168  * <li>beforeClose Before close handler (must return false to prevent popup closing)</li>
169  * <li>afterClose After close handler</li>
170  * <li>margin: Margin in px around the popup (not compatible with width and heigth)</li>
171  * <li>width: Width in px (not compatible with margin)</li>
172  * <li>height: Height in px (not compatible with margin)</li>
173  * <li>target: Target element (defaults to document body)</li>
174  * <li>style: CSS styles</li>
175  * </ul>
176  * @function
177  */
178 popup: function(spec) {
179 	var ui = this;
180 	ui.popupActive = true;
181 	spec = ui._spec(spec);
182 	if (!spec.name) spec.name = "ui_popup";
183 	var b = ui._target(spec.target);
184 	if (b === undefined || b.length == 0) {
185 		b = $j("#body");
186 		if (b.length == 0) b = $j("body");
187 	}
188 	var cl = spec.closeable === undefined || spec.closeable == true;
189 	if (spec.margin === undefined || spec.margin <= 0) {
190 		spec.margin = 0;
191 		if (spec.width === undefined || spec.width <= 0) spec.width = 200;
192 		if (spec.height === undefined || spec.height <= 0) spec.height = 100;
193 	} else {
194 		spec.width = 0;
195 		spec.height = 0;
196 	}
197 	var close = function() { ui.popupClose(spec.beforeClose, spec.afterClose, spec.name); };
198 	b.addClass("ui_popupmask");
199 	var m = ui.div({ id:"ui_popupmask", name:spec.name+"mask" });
200 	if (Simplicite.Tools.isWinIE && (Simplicite.Tools.winIEVersion <= 8 || Simplicite.USE_HTML4))
201 		m.addClass("ui_opaque_ie8");
202 	else
203 		m.addClass("ui_opaque");
204 	if (cl) m.click(close);
205 	b.append(m);
206 	m.show();
207 	var hm = Math.max(spec.margin > 0 ? spec.margin : Math.round((m.width() - spec.width) / 2), 30);
208 	var vm = Math.max(spec.margin > 0 ? spec.margin : Math.round((m.height() - spec.height) / 2), 30);
209 	var st = "left: " + hm + "px; right: " + hm + "px; top: " + vm + "px; bottom: " + vm + "px;";
210 	var p = ui.div({ id:"ui_popup", name: spec.name, style: st + ui._style(spec) });
211 	if (spec.url !== undefined) {
212 		p.css("overflow", "hidden");
213 		p.addClass("ui_box").append(ui.iframe(spec.url));
214 	}
215 	else if (spec.content !== undefined)
216 		p.addClass("ui_box").append(spec.content);
217 	b.append(p);
218 	if (cl) {
219 		var cst = "left: " + hm + "px; top: " + vm + "px;";
220 		var c = ui.div({ id:"ui_popupclose", name: spec.name+"close", style: cst });
221 		b.append(c.click(close));
222 	}
223 	if (spec.afterOpen && spec.afterOpen.call)
224 		spec.afterOpen.call(ui);
225 	return p;
226 },
227 
228 /** @ignore */
229 popupClose: function(beforeClose, afterClose, name) {
230 	if (!name) name = "ui_popup";
231 	var c = true;
232 	if (beforeClose && beforeClose.call) {
233 		var r = beforeClose.call(this);
234 		if (r !== undefined) c = r;
235 	}
236 	if (c) {
237 		try { $j("[name='"+name+"']").remove(); } catch(e) {}
238 		try { $j("[name='"+name+"close']").remove(); } catch(e) {}
239 		try { $j("[name='"+name+"mask']").remove(); } catch(e) {}
240 		$j("body").removeClass("ui_popupmask").removeClass("ui_popupmask_ie");
241 		this.popupActive = false;
242 		if (afterClose && afterClose.call)
243 			afterClose.call(this);
244 	}
245 },
246 
247 /**
248  * Displays information popup.
249  * @param msg Information message
250  * @param callback Callback
251  * @function
252  */
253 infoPopup: function(msg, callback) {
254 	return this.popup({ afterClose: callback, content: $j("<h1/>").addClass("ui_info").html(this.T("INFO")) }).append($j("<p/>").addClass("ui_info").html(msg));
255 },
256 
257 /**
258  * Displays warning popup.
259  * @param msg Warning message
260  * @param callback Callback
261  * @function
262  */
263 warningPopup: function(msg, callback) {
264 	return this.popup({ afterClose: callback, content: $j("<h1/>").addClass("ui_warning").html(this.T("WARNING")) }).append($j("<p/>").addClass("ui_warning").html(msg));
265 },
266 
267 /**
268  * Get plain error message from error or exception.
269  * @param err Error message or exception
270  * @function
271  */
272 getErrorMessage: function(err) {
273 	var msg = "";
274 	if (err.messages !== undefined) {
275 		for (var i = 0; i < err.messages.length; i++)
276 			msg += (msg == "" ? "" : "\n") + err.messages[i];
277 	} else if (err.message !== undefined)
278 		msg = err.message;
279 	else if (err.description !== undefined)
280 		msg = err.description;
281 	else
282 		msg = err;
283 	return msg;
284 },
285 
286 /**
287  * Displays error popup.
288  * @param err Error (can be a message or an exception)
289  * @param callback Callback
290  * @function
291  */
292 errorPopup: function(err, callback) {
293 	var msg = this.getErrorMessage(err);
294 	return this.popup({ afterClose: callback, content: $j("<h1/>").addClass("ui_error").html(this.T("ERROR")) }).append($j("<p/>").addClass("ui_error").html(msg));
295 },
296 
297 /**
298  * Displays dialog popup.
299  * @param msg Warning message
300  * @param buttonSpecs Array of button specifications (cf. button() function)
301  * @function
302  */
303 dialog: function(msg, buttonSpecs) {
304 	var ui = this;
305 	var c = $j("<p/>").addClass("ui_confirm").html(msg);
306 	var bf = undefined;
307 	if (buttonSpecs !== undefined) {
308 		var b = ui.div({ style: "position: absolute; left: 0px; right: 0px; bottom: 5px;" });
309 		for (var i = 0; i < buttonSpecs.length; i++) {
310 			var bs = ui._spec(buttonSpecs[i]);
311 			var bi = ui.button(bs);
312 			if (bs.focus === true) bf = bi;
313 			b.append(bi);
314 		}
315 		c.append(b);
316 	}
317 	return ui.popup({ closeable: false, height: (msg.length/50+2)*25+10, content: c, afterOpen: bf == undefined ? undefined : function() { bf.focus(); } });
318 },
319 
320 /**
321  * Displays simple dialog popup.
322  * @param msg Message
323  * @param callback Callback called when the OK button is clicked
324  * @function
325  */
326 okDialog: function(msg, callback) {
327 	var ui = this;
328 	if (Simplicite.USE_HTML4)
329 		alert(m);
330 	else
331 		return this.dialog(msg, [{ label: ui.T("OK"), color: "green", click: function() { ui.popupClose(undefined, callback); }, focus: true }]);
332 },
333 
334 /**
335  * Displays simple alert dialog popup.
336  * @param msg Message (can be simple message or a text code or a text code:message)
337  * @param callback Callback called when the OK button is clicked
338  * @function
339  */
340 alert : function(msg, callback) {
341 	var ui = this;
342 	if (msg === undefined || msg == "") return;
343 	var i = msg.indexOf(":");
344 	var m = (i < 0) ? this.T(msg) : this.T(msg.substring(0, i)) + (i+1 < msg.length ? ": " + msg.substring(i+1) : "");
345 	if (Simplicite.USE_HTML4)
346 		alert(m);
347 	else
348 		return ui.okDialog(m, callback);
349 },
350 
351 /**
352  * Displays OK/CANCEL dialog popup.
353  * @param msg Message
354  * @param okCallback Callback called if the OK button is clicked
355  * @param cancelCallback Callback called if the CANCEL button is clicked
356  * @function
357  */
358 okCancelDialog: function(msg, okCallback, cancelCallback) {
359 	var ui = this;
360 	if (Simplicite.USE_HTML4) {
361 		if (confirm(msg))
362 			okCallback.call(ui);
363 		else
364 			cancelCallback.call(ui);
365 	} else {
366 		return ui.dialog(msg, [
367 			{ label: ui.T("OK"), color: "green", click: function() { ui.popupClose(undefined, okCallback); }, focus: true },
368 			{ label: ui.T("CANCEL"), color: "red", click: function() { ui.popupClose(undefined, cancelCallback); } }
369 		]);
370 	}
371 },
372 
373 /**
374  * Displays YES/NO dialog popup.
375  * @param msg Message
376  * @param yesCallback Callback called if the YES button is clicked
377  * @param noCallback Callback called if the NO button is clicked
378  * @function
379  */
380 yesNoDialog: function(msg, yesCallback, noCallback) {
381 	var ui = this;
382 	if (Simplicite.USE_HTML4) {
383 		if (confirm(msg))
384 			yesCallback.call(ui);
385 		else
386 			noCallback.call(ui);
387 	} else {
388 		return ui.dialog(msg, [
389 			{ label: ui.T("YES"), color: "green", click: function() { ui.popupClose(undefined, yesCallback); }, focus: true },
390 			{ label: ui.T("NO"), color: "red", click: function() { ui.popupClose(undefined, noCallback); } }
391 		]);
392 	}
393 },
394 
395 /**
396  * Displays YES/NO/CANCEL dialog popup.
397  * @param msg Message
398  * @param yesCallback Callback called if the YES button is clicked
399  * @param noCallback Callback called if the NO button is clicked
400  * @param cancelCallback Callback called the CANCEL button is clicked
401  * @function
402  */
403 yesNoCancelDialog: function(msg, yesCallback, noCallback, cancelCallback) {
404 	var ui = this;
405 	if (Simplicite.USE_HTML4) {
406 		if (confirm(msg))
407 			yesCallback.call(ui);
408 		else
409 			noCallback.call(ui);
410 	} else {
411 		return ui.dialog(msg, [
412 			{ label: ui.T("YES"), color: "green", click: function() { ui.popupClose(undefined, yesCallback); }, focus: true },
413 			{ label: ui.T("NO"), color: "red", click: function() { ui.popupClose(undefined, noCallback); } },
414 			{ label: ui.T("CANCEL"), color: "orange", click: function() { ui.popupClose(undefined, cancelCallback); } }
415 		]);
416 	}
417 },
418 
419 /**
420  * Show loading mask.
421  * @param spec Specification:
422  * <ul>
423  * <li>message: Message (mandatory)</li>
424  * <li>anim: Add animated gif (defaults to true) ?</li>
425  * <li>target: Target element (defaults to document body)</li>
426  * <li>callback: Callback</li>
427  * </ul>
428  * @function
429  */
430 showLoading: function(spec) {
431 	var l = undefined;
432 	spec = this._spec(spec);
433 	var t = (Simplicite.Tester && Simplicite.Tester.testId);
434 	if (!t) { // no popup during testcase recording
435 		var m = spec.message;
436 		l = this.popup({ closeable: false, width: 200, height: 80, target: spec.target }).addClass("ui_loading");
437 		if (spec.anim === undefined || spec.anim == true)
438 			l.append($j("<div/>").append(this.image("inprogress.gif")));
439 		if (m) l.addClass("ui_box").append(m);
440 	}
441 	var c = spec.callback;
442 	if (c) {
443 		var self = this;
444 		// ZZZ a 100ms delay is required for the browser to display the spinner GIF
445 		setTimeout(function() { c.call(self); }, 100);
446 	}
447 	return l;
448 },
449 
450 /**
451  * Hide loading mask.
452  * @param spec Specification:
453  * <ul>
454  * <li>callback: Callback</li>
455  * </ul>
456  */
457 hideLoading: function(spec) {
458 	this.popupClose();
459 	spec = this._spec(spec);
460 	var c = spec.callback;
461 	if (c !== undefined)
462 		c.call(this);
463 },
464 
465 /**
466  * Show a toast message (hidden after specified delay).
467  * @param spec Specification:
468  * <ul>
469  * <li>message: Message (mandatory)</li>
470  * <li>delay: Hide delay (default is 1.5s)</li>
471  * <li>callback: Callback called after toast message is closed</li>
472  * <li>target: Target element (defaults to document body)</li>
473  * </ul>
474  * @function
475  */
476 toast: function(spec) {
477 	spec = this._spec(spec);
478 	var m = spec.message;
479 	var ui = this;
480 	if (m === undefined || m == "") return;
481 	ui.popup({ closeable: false, width: 200, height: (m.length/50+1)*25, target: spec.target }).addClass("ui_toast").addClass("ui_box").append(m);
482 	setTimeout(function() {
483 		ui.popupClose(undefined, spec.callback);
484 	}, spec.delay === undefined ? 1500 : spec.delay);
485 },
486 
487 popupWindow: function(spec) {
488 	spec = this._spec(spec);
489 	var p = "location=no,menubar=no,status=no,toolbar=no,directories=no";
490 	if (spec.width != undefined) p = p + ",width=" + spec.width;
491 	if (spec.height != undefined) p = p + ",height=" + spec.height;
492 	if (spec.top != undefined) p = p + ",top=" + spec.top;
493 	if (spec.left != undefined) p = p + ",left=" + spec.left;
494 	return window.open(spec.url, spec.name === undefined ? "_blank" : spec.name, p);
495 },
496 
497 newWindow: function(url) {
498 	return window.open(url, "_blank");
499 },
500 
501 /** @ignore */
502 _component: function(target, type, spec) {
503 	var ui = this;
504 	target = ui._target(target);
505 	spec = ui._spec(spec);
506 	var mc = $j("<div/>").addClass("ui_" + type);
507 	target.append(mc);
508 	var l = $j("<div/>").addClass("ui_" + type + "_label").append(spec.label);
509 	mc.append(l);
510 	var t = $j("<div/>").addClass("ui_" + type + "_toggle");
511 	if (spec.expandable == true && spec.expanded == false) t.hide();
512 	mc.append(t);
513 	var c = $j("<div/>", { id: spec.id, style: ui._style(spec) }).addClass("ui_" + type + "_content").append(spec.content);
514 	t.append(c);
515 	var comp = {
516 		label: l,
517 		content: c,
518 		spec: spec,
519 		append: function(elt) {
520 			this.content.append(elt);
521 			return this;
522 		},
523 		toggle: function() {
524 			var c = this.content.parent();
525 			if (this.spec.expandEffect == "slide")
526 				c.slideToggle();
527 			else if (this.spec.expandEffect == "fade")
528 				c.fadeToggle();
529 			else
530 				c.toggle();
531 			var l = this.label;
532 			l.toggleClass("ui_plus");
533 			l.toggleClass("ui_minus");
534 			if (spec.toggle !== undefined)
535 				spec.toggle.call(ui, this.content);
536 			return this;
537 		}
538 	};
539 	if (spec.expandable == true) {
540 		l.addClass("ui_button");
541 		l.addClass(spec.expanded == true ? "ui_minus" : "ui_plus");
542 		l.click(function() { comp.toggle(); });
543 	}
544 	return comp;
545 },
546 
547 /**
548  * Add a new simple area to target
549  * @param target Target DOM ID or element
550  * @param spec Specification:
551  * <ul>
552  * <li>id: DOM ID</li>
553  * <li>label: Area label</li>
554  * <li>content: Area content</li>
555  * <li>expandable Expandable (false by default) ?</li>
556  * <li>expanded Expanded by default (true by default) ?</li>
557  * <li>expandEffect Expand effect : "slide" or "fade"</li>
558  * <li>toggle Toggle handler</li>
559  * <li>width: Width in px</li>
560  * <li>height: Height in px</li>
561  * <li>style: CSS styles</li>
562  * </ul>
563  * @function
564  */
565 area: function(target, spec) {
566 	return this._component(target, "area", spec);
567 },
568 
569 /**
570  * Add a new gadget to target
571  * @param target Target DOM ID or element
572  * @param spec Specification:
573  * <ul>
574  * <li>id: DOM ID</li>
575  * <li>label: Gadget label</li>
576  * <li>content: Gadget content</li>
577  * <li>expandable Expandable (false by default) ?</li>
578  * <li>expanded Expanded by default (true by default) ?</li>
579  * <li>expandEffect Expand effect : "slide" or "fade"</li>
580  * <li>toggle Toggle handler</li>
581  * <li>layout Gadget layout (e.g. "modern")</li>
582  * <li>width: Width in px</li>
583  * <li>height: Height in px</li>
584  * <li>style: CSS styles</li>
585  * </ul>
586  * @function
587  */
588 gadget: function(target, spec) {
589 	spec = this._spec(spec);
590 	return this._component(target, "gadget" + (spec.layout !== undefined ? "_" + spec.layout : ""), spec);
591 },
592 
593 contextualpopup: function(spec) {
594 	spec = this._spec(spec);
595 	var p = this.div({ top: spec.top, left: spec.left, style: "position: absolute;" });
596 	delete spec.top;
597 	delete spec.left;
598 	var c = this._component(p, "contextualpopup", spec);
599 	c.popup = p;
600 	c.close = function() { p.remove(); };
601 	$j("body").append(p);
602 	return c;
603 },
604 
605 /**
606  * Add new tabs to target
607  * @param target Target DOM ID or element
608  * @param specs Specifications, each one for one tab:
609  * <ul>
610  * <li>id: DOM ID</li>
611  * <li>label: Tab label</li>
612  * <li>content: Tab content</li>
613  * <li>changeEffect Change tab effect : "slide" or "fade"</li>
614  * <li>change Change tab handler</li>
615  * <li>width: Width in px</li>
616  * <li>height: Height in px</li>
617  * <li>style: CSS styles</li>
618  * </ul>
619  * @function
620  */
621 tabs: function(target, specs) {
622 	var ui = this;
623 	target = ui._target(target);
624 	var ts = {
625 		specs: specs,
626 		labels: [],
627 		contents: [],
628 		append: function(n, elt) {
629 			if (n < 0 || n >= this.specs.length) return;
630 			this.contents[n].append(elt);
631 			return this;
632 		},
633 		toggle: function(n) {
634 			if (n < 0 || n >= this.specs.length) return;
635 			var spec = this.specs[n];
636 			if (this.labels[n].hasClass("ui_tab_off")) {
637 				for (var i = 0; i < this.labels.length; i++) {
638 					var l = this.labels[i];
639 					var c = this.contents[i];
640 					if (i == n) {
641 						l.removeClass("ui_tab_off");
642 						if (spec.activateEffect == "slide")
643 							c.slideDown();
644 						else if (spec.activateEffect == "fade")
645 							c.fadeIn();
646 						else
647 							c.show();
648 						if (spec.activate !== undefined)
649 							spec.activate.call(ui, c);
650 					} else {
651 						l.addClass("ui_tab_off");
652 						c.hide();
653 					}
654 				}
655 			}
656 			return this;
657 		}
658 	};
659 	var g = $j("<div/>").addClass("ui_tab_group");
660 	target.append(g);
661 	var n = 0;
662 	for (var k in specs) {
663 		specs[k] = ui._spec(specs[k]);
664 		if (specs[k].active == true) n = k;
665 	}
666 	for (var i in specs) {
667 		var spec = specs[i];
668 		var l = $j("<div/>").addClass("ui_tab").data("index", i).click(function() { ts.toggle($j(this).data("index")); });
669 		l.append(spec.label !== undefined ? spec.label : " ");
670 		g.append(l);
671 		ts.labels.push(l);
672 		var c = $j("<div/>", { id: spec.id, style: ui._style(spec) }).addClass("ui_tab_content").append(spec.content);
673 		target.append(c);
674 		ts.contents.push(c);
675 		if (i != n) {
676 			l.addClass("ui_tab_off");
677 			c.hide();
678 		} else {
679 			if (spec.activate !== undefined)
680 				spec.activate.call(ui, c);
681 		}
682 	}
683 	return ts;
684 },
685 
686 /**
687  * Pie chart using JQPlot
688  * @param target Target DOM ID or element
689  * @param spec Specifications: <ul>
690  * <li>values: Values</li>
691  * <li>labels: Labels</li>
692  * <li>options: Chart options</li>
693  */
694 pieChart: function(target, spec) {
695 	if ($j.jqplot == undefined) return;
696 	spec = this._spec(spec);
697 	if (typeof(target) != "string") target = target[0].id;
698 	var v = [];
699 	if (spec.labels !== undefined)
700 		for (var i = 0; i < spec.values.length; i++)
701 			v[i] = [ spec.labels[i], spec.values[i] ];
702 	else
703 		v = spec.values;
704 	var o = {
705 		title: spec.title,
706 		seriesDefaults: { renderer: $j.jqplot.PieRenderer, rendererOptions: { showDataLabels: true } },
707 		legend: { show: true, location: "ne" }
708 	};
709 	if (spec.options !== undefined) $j.extend(true, o, spec.options);
710 	setTimeout(function() { $j.jqplot(target, [ v ], o); }, 0);
711 },
712 
713 _chart: function(target, spec) {
714 	if ($j.jqplot == undefined) return;
715 	spec = this._spec(spec);
716 	if (typeof(target) != "string") target = target[0].id;
717 	var o = {
718 		title: spec.title,
719 		seriesDefaults: { rendererOptions: { showDataLabels: true } },
720 		axes: { xaxis: { renderer: $j.jqplot.CategoryAxisRenderer } }
721 	};
722 	if (spec.labels !== undefined) {
723 		o.axes.xaxis.ticks = spec.labels;
724 	}
725 	if (spec.names !== undefined) {
726 		o.legend = { show: true, location: "nw" };
727 		o.series = new Array();
728 		for (var i = 0; i < spec.names.length; i++) {
729 			o.series[i] = { label: spec.names[i] };
730 		}
731 	}
732 	if (spec.options !== undefined) $j.extend(true, o, spec.options);
733 	setTimeout(function() { $j.jqplot(target, spec.values, o); }, 0);
734 },
735 
736 /**
737  * Line chart using JQPlot
738  * @param target Target DOM ID or element
739  * @param spec Specifications: <ul>
740  * <li>values: Values</li>
741  * <li>names: Names</li>
742  * <li>labels: Labels</li>
743  * <li>options: Chart options</li>
744  */
745 lineChart: function(target, spec) {
746 	if ($j.jqplot == undefined) return;
747 	spec = this._spec(spec);
748 	var o = { seriesDefaults: { pointLabels: { show: true } } };
749 	spec.options = spec.options !== undefined ? $j.extend(true, o, spec.options) : o;
750 	this._chart(target, spec);
751 },
752 
753 /**
754  * Bar chart using JQPlot
755  * @param target Target DOM ID or element
756  * @param spec Specifications: <ul>
757  * <li>values: Values</li>
758  * <li>names: Names</li>
759  * <li>labels: Labels</li>
760  * <li>options: Chart options</li>
761  */
762 barChart: function(target, spec) {
763 	if ($j.jqplot == undefined) return;
764 	spec = this._spec(spec);
765 	var o = { seriesDefaults: { renderer: $j.jqplot.BarRenderer, pointLabels: { show: true } } };
766 	spec.options = spec.options !== undefined ? $j.extend(true, o, spec.options) : o;
767 	this._chart(target, spec);
768 },
769 
770 /**
771  * Stack bar chart using JQPlot
772  * @param target Target DOM ID or element
773  * @param spec Specifications: <ul>
774  * <li>values: Values</li>
775  * <li>names: Names</li>
776  * <li>labels: Labels</li>
777  * <li>options: Chart options</li>
778  */
779 stackBarChart: function(target, spec) {
780 	if ($j.jqplot == undefined) return;
781 	spec = this._spec(spec);
782 	var o = { stackSeries: true, seriesDefaults: { renderer: $j.jqplot.BarRenderer, pointLabels: { show: true } } };
783 	spec.options = spec.options !== undefined ? $j.extend(true, o, spec.options) : o;
784 	this._chart(target, spec, $j.jqplot.BarRenderer);
785 },
786 
787 /**
788  * Area bar chart using JQPlot
789  * @param target Target DOM ID or element
790  * @param spec Specifications: <ul>
791  * <li>values: Values</li>
792  * <li>names: Names</li>
793  * <li>labels: Labels</li>
794  * <li>options: Chart options</li>
795  */
796 areaChart: function(target, spec) {
797 	if ($j.jqplot == undefined) return;
798 	spec = this._spec(spec);
799 	var o = { seriesDefaults: { fill: true } };
800 	spec.options = spec.options !== undefined ? $j.extend(true, o, spec.options) : o;
801 	this._chart(target, spec);
802 },
803 
804 /**
805  * Stack area bar chart using JQPlot
806  * @param target Target DOM ID or element
807  * @param spec Specifications: <ul>
808  * <li>values: Values</li>
809  * <li>names: Names</li>
810  * <li>labels: Labels</li>
811  * <li>options: Chart options</li>
812  */
813 stackAreaChart: function(target, spec) {
814 	if ($j.jqplot == undefined) return;
815 	spec = this._spec(spec);
816 	var o = { stackSeries: true, seriesDefaults: { fill: true } };
817 	spec.options = spec.options !== undefined ? $j.extend(true, o, spec.options) : o;
818 	this._chart(target, spec);
819 },
820 
821 /**
822  * Returns a new social post bloc
823  * @param spec Specifications: <ul>
824  * <li>id: DOM ID</li>
825  * <li>date: Post date</li>
826  * <li>message: Post message (if undefined a form for new post is generated)</li>
827  * <li>about: Post reference</li>
828  * <li>followersOnly: Followers only post ?</li>
829  * <li>pictureURL: Picture URL</li>
830  * <li>userName: User full name</li>
831  * <li>userLogin: User login</li>
832  * <li>userID: User ID</li>
833  * <li>messagePlaceholder: New post form message textarea placeholder</li>
834  * <li>buttonLabel: New post form button label</li>
835  * <li>post: New post form handler</li>
836  */
837 socialPost: function(spec) {
838 	spec = this._spec(spec);
839 	var d = this.div({ id: spec.id }).addClass("ui_social");
840 	if (spec.followersOnly !== undefined && spec.followersOnly === true)
841 		d.addClass("ui_social_followers");
842 	var t = $j("<table/>", { style: "width: 100%;" });
843 	var tr = $j("<tr/>");
844 	t.append(tr);
845 	if (spec.pictureURL)
846 		tr.append($j("<td/>", { style: "width: 50px;" }).append($j("<img/>", { src: spec.pictureURL, title: spec.userName })));
847 	var td = $j("<td/>");
848 	td.append($j("<div/>").addClass("ui_social_user").text(spec.userName));
849 	if (spec.date !== undefined)
850 		td.append($j("<div/>").addClass("ui_social_date").text(spec.date));
851 	var m = $j("<div/>").addClass("ui_social_post");
852 	if (spec.message === undefined) {
853 		m.append($j("<textarea/>", { id: spec.id + "_message", style: "float: left;", cols: 80, rows: 2, placeholder: spec.messagePlaceholder }).addClass("ui_social_textarea"));
854 		m.append(this.button({ label: spec.buttonLabel, click: spec.post !== undefined ? function() {
855 			spec.post.call(this, spec.userID, new Date(), $j("#" + spec.id + "_message").val());
856 		} : undefined }));
857 	}
858 	else
859 		m.text(spec.message);
860 	td.append(m);
861 	if (spec.about !== undefined)
862 		td.append($j("<div/>").addClass("ui_social_about").text(spec.about));
863 	tr.append(td);
864 	d.append(t);
865 	return d;
866 },
867 
868 /**
869  * Return a news block
870  * @param n news
871  */
872 news: function(n) {
873 	var d = $j("<div/>");
874 	var t = $j("<table/>");
875 	var tr = $j("<tr/>");
876 	if (n.image && n.image.content)
877 		tr.append($j("<td/>").append($j("<img/>", { src: "data:" + n.image.mime + ";base64," + n.image.content, style: "width: 75px;" }))).append($j("<td/>").append(" "));
878 	tr.append($j("<td/>").append($j("<p/>").addClass("rsstext").append($j("<span/>").addClass("rsstextdate").append(new Date().fromDateTimeValue(n.date))).append("<br/>").append($j("<span/>").addClass("rsstexttitle").append(n.title))));
879 	t.append(tr);
880 	d.append(t).append($j("<p/>").addClass("rsstext").append(n.content.replace(/[\r\n]+/, "<br/>")));
881 	return d;
882 },
883 
884 /**
885  * Creates a new rotating ticker in designated target
886  * @param target Target element
887  * @param delay Rotation delay in milliseconds
888  */
889 ticker: function(target, delay) {
890 	/** @ignore */
891 	var ul = $j("<ul/>", { style: "margin: 0px; padding: 0px; list-style-type: none; overflow: hidden; height: " + target.height() + "px;" });
892 	ul.mouseover(function() { stop(); });
893 	ul.mouseout(function() { start(); });
894 	target.append(ul);
895 	/**
896 	 * Clear ticker items
897 	 * @param item Item to append
898 	 */
899 	function clear() {
900 		ul.clear();
901 	}
902 	/**
903 	 * Appends an item to ticker
904 	 * @param item Item to append
905 	 */
906 	function append(item) {
907 		ul.append($j("<li/>", { style: "margin: 0px; padding: 0px; list-style-type: none; height: " + target.height() + "px;" }).append(item));
908 	}
909 	/** @ignore */
910 	var timer = undefined;
911 	/**
912 	 * Start ticker
913 	 */
914 	function start() {
915 		timer = setInterval(function() {
916 			ul.find("li:first").slideUp(1500, function () { $j(this).appendTo(ul).show(); });
917 		}, delay);
918 	}
919 	/**
920 	 * Stop ticker
921 	 */
922 	function stop() {
923 		if (timer) clearInterval(timer);
924 		timer = undefined;
925 	}
926 	return { start: start, stop: stop, clear: clear, append: append };
927 }
928 
929 };