[ajax] refactor/cleanup low-level ajax functions
authorAdrien Di Mascio <Adrien.DiMascio@logilab.fr>
Thu, 03 Jun 2010 14:51:42 +0200
changeset 5658 7b9553a9db65
parent 5655 ef903fff826d
child 5663 d93a875a9d94
[ajax] refactor/cleanup low-level ajax functions * loadxhtml/replacePageChunck/reload_component/reload_box deprecated in favor of loadXHTML / ajaxFuncArgs * some other cleanups in cubicweb.ajax.js * add js_render which will replace js_component (more generic, nicer argument passing handling)
web/data/cubicweb.ajax.js
web/data/cubicweb.bookmarks.js
web/data/cubicweb.calendar.js
web/data/cubicweb.compat.js
web/data/cubicweb.edition.js
web/data/cubicweb.facets.js
web/data/cubicweb.flot.js
web/data/cubicweb.gmap.js
web/data/cubicweb.goa.js
web/data/cubicweb.htmlhelpers.js
web/data/cubicweb.iprogress.js
web/data/cubicweb.js
web/data/cubicweb.lazy.js
web/data/cubicweb.massmailing.js
web/data/cubicweb.preferences.js
web/data/cubicweb.python.js
web/data/cubicweb.rhythm.js
web/data/cubicweb.tabs.js
web/data/cubicweb.timeline-ext.js
web/data/cubicweb.widgets.js
web/data/jquery.tablesorter.js
web/data/uiprops.py
web/test/jstests/ajax_url0.html
web/test/jstests/ajax_url1.html
web/test/jstests/ajax_url2.html
web/test/jstests/ajaxresult.json
web/test/jstests/qunit.css
web/test/jstests/qunit.js
web/test/jstests/test_ajax.html
web/test/jstests/test_ajax.js
web/test/jstests/test_datetime.html
web/test/jstests/test_datetime.js
web/test/jstests/test_htmlhelpers.html
web/test/jstests/test_htmlhelpers.js
web/views/basecontrollers.py
web/views/bookmark.py
web/views/editforms.py
web/views/formrenderers.py
web/views/massmailing.py
web/webconfig.py
--- a/web/data/cubicweb.ajax.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.ajax.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,7 +1,20 @@
-/*
- *  :organization: Logilab
- *  :copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
- *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+/* copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ * contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
+ * This file is part of CubicWeb.
+ *
+ * CubicWeb is free software: you can redistribute it and/or modify it under the
+ * terms of the GNU Lesser General Public License as published by the Free
+ * Software Foundation, either version 2.1 of the License, or (at your option)
+ * any later version.
+ *
+ * CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along
+ * with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
  */
 
 CubicWeb.require('python.js');
@@ -9,6 +22,7 @@
 
 var JSON_BASE_URL = baseuri() + 'json?';
 
+//============= utility function handling remote calls responses. ==============//
 function _loadAjaxHtmlHead(node, head, tag, srcattr) {
     var loaded = [];
     var jqtagfilter = tag + '[' + srcattr + ']';
@@ -17,7 +31,7 @@
     });
     node.find(tag).each(function(i) {
         if (this.getAttribute(srcattr)) {
-            if (!loaded.contains(this.getAttribute(srcattr))) {
+            if (jQuery.inArray(this.getAttribute(srcattr), loaded) == -1) {
                 jQuery(this).appendTo(head);
             }
         } else {
@@ -27,7 +41,9 @@
     node.find(jqtagfilter).remove();
 }
 
-/*
+/**
+ * .. function:: function loadAjaxHtmlHead(response)
+ *
  * inspect dom response (as returned by getDomFromResponse), search for
  * a <div class="ajaxHtmlHead"> node and put its content into the real
  * document's head.
@@ -59,18 +75,13 @@
     //    we can safely return this node. Otherwise, the view itself
     //    returned several 'root' nodes and we need to keep the wrapper
     //    created by getDomFromResponse()
-    if (response.childNodes.length == 1 &&
-        response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
+    if (response.childNodes.length == 1 && response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
         return response.firstChild;
     }
     return response;
 }
 
-function preprocessAjaxLoad(node, newdomnode) {
-    return loadAjaxHtmlHead(newdomnode);
-}
-
-function postAjaxLoad(node) {
+function _postAjaxLoad(node) {
     // find sortable tables if there are some
     if (typeof(Sortable) != 'undefined') {
         Sortable.sortTables(node);
@@ -89,44 +100,77 @@
         roundedCorners(node);
     }
     if (typeof setFormsTarget != 'undefined') {
-       setFormsTarget(node);
+        setFormsTarget(node);
     }
-    loadDynamicFragments(node);
+    _loadDynamicFragments(node);
     // XXX [3.7] jQuery.one is now used instead jQuery.bind,
     // jquery.treeview.js can be unpatched accordingly.
     jQuery(CubicWeb).trigger('server-response', [true, node]);
+    jQuery(node).trigger('server-response', [true, node]);
+}
+
+function remoteCallFailed(err, req) {
+    cw.log(err);
+    if (req.status == 500) {
+        updateMessage(err);
+    } else {
+        updateMessage(_("an error occured while processing your request"));
+    }
 }
 
-/* cubicweb loadxhtml plugin to make jquery handle xhtml response
+//============= base AJAX functions to make remote calls =====================//
+/**
+ * .. function:: ajaxFuncArgs(fname, form, *args)
  *
- * fetches `url` and replaces this's content with the result
+ * extend `form` parameters to call the js_`fname` function of the json
+ * controller with `args` arguments.
+ */
+function ajaxFuncArgs(fname, form /* ... */) {
+    form = form || {};
+    $.extend(form, {
+        'fname': fname,
+        'pageid': pageid,
+        'arg': map(jQuery.toJSON, sliceList(arguments, 2))
+    });
+    return form;
+}
+
+/**
+ * .. function:: loadxhtml(url, form, reqtype='get', mode='replace', cursor=true)
  *
- * @param mode how the replacement should be done (default is 'replace')
- *  Possible values are :
+ * build url given by absolute or relative `url` and `form` parameters
+ * (dictionary), fetch it using `reqtype` method, then evaluate the
+ * returned XHTML and insert it according to `mode` in the
+ * document. Possible modes are :
+ *
  *    - 'replace' to replace the node's content with the generated HTML
  *    - 'swap' to replace the node itself with the generated HTML
  *    - 'append' to append the generated HTML to the node's content
+ *
+ * If `cursor`, turn mouse cursor into 'progress' cursor until the remote call
+ * is back.
  */
-jQuery.fn.loadxhtml = function(url, data, reqtype, mode) {
-    var ajax = null;
-    if (reqtype == 'post') {
-        ajax = jQuery.post;
-    } else {
-        ajax = jQuery.get;
+jQuery.fn.loadxhtml = function(url, form, reqtype, mode, cursor) {
+    if (this.size() > 1) {
+        cw.log('loadxhtml was called with more than one element');
     }
-    if (this.size() > 1) {
-        log('loadxhtml was called with more than one element');
+    var callback = null;
+    if (form && form.callback) {
+        cw.log('[3.9] callback given through form.callback is deprecated, add ' + 'callback on the defered');
+        callback = form.callback;
+        delete form.callback;
     }
     var node = this.get(0); // only consider the first element
-    mode = mode || 'replace';
-    var callback = null;
-    if (data && data.callback) {
-        callback = data.callback;
-        delete data.callback;
+    if (cursor) {
+        setProgressCursor();
     }
-    ajax(url, data, function(response) {
+    var d = loadRemote(url, form, reqtype);
+    d.addCallback(function(response) {
         var domnode = getDomFromResponse(response);
-        domnode = preprocessAjaxLoad(node, domnode);
+        domnode = loadAjaxHtmlHead(domnode);
+        mode = mode || 'replace';
+        // make sure the component is visible
+        $(node).removeClass("hidden");
         if (mode == 'swap') {
             var origId = node.id;
             node = swapDOM(node, domnode);
@@ -138,19 +182,97 @@
         } else if (mode == 'append') {
             jQuery(node).append(domnode);
         }
-        postAjaxLoad(node);
+        _postAjaxLoad(node);
         while (jQuery.isFunction(callback)) {
             callback = callback.apply(this, [domnode]);
         }
     });
-};
+    if (cursor) {
+        d.addCallback(resetCursor);
+        d.addErrback(resetCursor);
+        d.addErrback(remoteCallFailed);
+    }
+    return d;
+}
 
+/**
+ * .. function:: loadRemote(url, form, reqtype='GET', async=true)
+ *
+ * Asynchronously (unless `async` argument is set to false) load an url or path
+ * and return a deferred whose callbacks args are decoded according to the
+ * Content-Type response header. `form` should be additional form params
+ * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
+ */
+function loadRemote(url, form, reqtype, sync) {
+    if (!url.startswith(baseuri())) {
+        url = baseuri() + url;
+    }
+    if (!sync) {
+        var deferred = new Deferred();
+        jQuery.ajax({
+            url: url,
+            type: (reqtype || 'GET').toUpperCase(),
+            data: form,
+            async: true,
+
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+
+            success: function(data, status) {
+                if (deferred._req.getResponseHeader("content-type") == 'application/json') {
+                    data = cw.evalJSON(data);
+                }
+                deferred.success(data);
+            },
 
+            error: function(xhr, status, error) {
+                try {
+                    if (xhr.status == 500) {
+                        var reason_dict = cw.evalJSON(xhr.responseText);
+                        deferred.error(xhr, status, reason_dict['reason']);
+                        return;
+                    }
+                } catch(exc) {
+                    cw.log('error with server side error report:' + exc);
+                }
+                deferred.error(xhr, status, null);
+            }
+        });
+        return deferred;
+    } else {
+        var result = jQuery.ajax({
+            url: url,
+            type: (reqtype || 'GET').toUpperCase(),
+            data: form,
+            async: false
+        });
+        if (result) {
+            // XXX no good reason to force json here, 
+            // it should depends on request content-type
+            result = cw.evalJSON(result.responseText);
+        }
+        return result
+    }
+}
 
-/* finds each dynamic fragment in the page and executes the
+//============= higher level AJAX functions using remote calls ===============//
+/**
+ * .. function:: _(message)
+ *
+ * emulation of gettext's _ shortcut
+ */
+function _(message) {
+    return loadRemote('json', ajaxFuncArgs('i18n', null, [message]), 'GET', true)[0];
+}
+
+/**
+ * .. function:: _loadDynamicFragments(node)
+ *
+ * finds each dynamic fragment in the page and executes the
  * the associated RQL to build them (Async call)
  */
-function loadDynamicFragments(node) {
+function _loadDynamicFragments(node) {
     if (node) {
         var fragments = jQuery(node).find('div.dynamicFragment');
     } else {
@@ -162,246 +284,137 @@
     if (typeof LOADING_MSG == 'undefined') {
         LOADING_MSG = 'loading'; // this is only a safety belt, it should not happen
     }
-    for(var i=0; i<fragments.length; i++) {
+    for (var i = 0; i < fragments.length; i++) {
         var fragment = fragments[i];
         fragment.innerHTML = '<h3>' + LOADING_MSG + ' ... <img src="data/loading.gif" /></h3>';
+        var $fragment = jQuery(fragment);
         // if cubicweb:loadurl is set, just pick the url et send it to loadxhtml
-        var url = getNodeAttribute(fragment, 'cubicweb:loadurl');
+        var url = $fragment.attr('cubicweb:loadurl');
         if (url) {
-            jQuery(fragment).loadxhtml(url);
+            $fragment.loadxhtml(url);
             continue;
         }
         // else: rebuild full url by fetching cubicweb:rql, cubicweb:vid, etc.
-        var rql = getNodeAttribute(fragment, 'cubicweb:rql');
-        var items = getNodeAttribute(fragment, 'cubicweb:vid').split('&');
+        var rql = $fragment.attr('cubicweb:rql');
+        var items = $fragment.attr('cubicweb:vid').split('&');
         var vid = items[0];
         var extraparams = {};
         // case where vid='myvid&param1=val1&param2=val2': this is a deprecated abuse-case
         if (items.length > 1) {
-            console.log("[3.5] you're using extraargs in cubicweb:vid attribute, this is deprecated, consider using loadurl instead");
-            for (var j=1; j<items.length; j++) {
+            cw.log("[3.5] you're using extraargs in cubicweb:vid " +
+                   "attribute, this is deprecated, consider using " +
+                   "loadurl instead");
+            for (var j = 1; j < items.length; j++) {
                 var keyvalue = items[j].split('=');
                 extraparams[keyvalue[0]] = keyvalue[1];
             }
         }
-        var actrql = getNodeAttribute(fragment, 'cubicweb:actualrql');
-        if (actrql) { extraparams['actualrql'] = actrql; }
-        var fbvid = getNodeAttribute(fragment, 'cubicweb:fallbackvid');
-        if (fbvid) { extraparams['fallbackvid'] = fbvid; }
-        replacePageChunk(fragment.id, rql, vid, extraparams);
-    }
-}
-
-jQuery(document).ready(function() {loadDynamicFragments();});
-
-//============= base AJAX functions to make remote calls =====================//
-
-function remoteCallFailed(err, req) {
-    if (req.status == 500) {
-        updateMessage(err);
-    } else {
-        updateMessage(_("an error occured while processing your request"));
+        var actrql = $fragment.attr('cubicweb:actualrql');
+        if (actrql) {
+            extraparams['actualrql'] = actrql;
+        }
+        var fbvid = $fragment.attr('cubicweb:fallbackvid');
+        if (fbvid) {
+            extraparams['fallbackvid'] = fbvid;
+        }
+        extraparams['rql'] = rql;
+        extraparams['vid'] = vid;
+        $(fragment.id).loadxhtml('json', ajaxFuncArgs('view', extraparams));
     }
 }
-
-
-/*
- * This function will call **synchronously** a remote method on the cubicweb server
- * @param fname: the function name to call (as exposed by the JSONController)
- *
- * additional arguments will be directly passed to the specified function
- *
- * It looks at http headers to guess the response type.
- */
-function remoteExec(fname /* ... */) {
-    setProgressCursor();
-    var props = {'fname' : fname, 'pageid' : pageid,
-                      'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    var result  = jQuery.ajax({url: JSON_BASE_URL, data: props, async: false}).responseText;
-    if (result) {
-        result = evalJSON(result);
-    }
-    resetCursor();
-    return result;
-}
-
-/*
- * This function will call **asynchronously** a remote method on the json
- * controller of the cubicweb http server
- *
- * @param fname: the function name to call (as exposed by the JSONController)
- *
- * additional arguments will be directly passed to the specified function
- *
- * It looks at http headers to guess the response type.
- */
-
-function asyncRemoteExec(fname /* ... */) {
-    setProgressCursor();
-    var props = {'fname' : fname, 'pageid' : pageid,
-                 'arg': map(jQuery.toJSON, sliceList(arguments, 1))};
-    // XXX we should inline the content of loadRemote here
-    var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
-    deferred = deferred.addErrback(remoteCallFailed);
-    deferred = deferred.addErrback(resetCursor);
-    deferred = deferred.addCallback(resetCursor);
-    return deferred;
-}
-
-
-/* emulation of gettext's _ shortcut
- */
-function _(message) {
-    return remoteExec('i18n', [message])[0];
-}
-
-function userCallback(cbname) {
-    asyncRemoteExec('user_callback', cbname);
-}
+jQuery(document).ready(function() {
+    _loadDynamicFragments();
+});
 
 function unloadPageData() {
     // NOTE: do not make async calls on unload if you want to avoid
     //       strange bugs
-    remoteExec('unload_page_data');
+    loadRemote('json', ajaxFuncArgs('unload_page_data'), 'GET', true);
+}
+
+function removeBookmark(beid) {
+    var d = loadRemote('json', ajaxFuncArgs('delete_bookmark', null, beid));
+    d.addCallback(function(boxcontent) {
+        $('#bookmarks_box').loadxhtml('json',
+                                      ajaxFuncArgs('render', null, 'boxes',
+                                                   'bookmarks_box'));
+        document.location.hash = '#header';
+        updateMessage(_("bookmark has been removed"));
+    });
+}
+
+function userCallback(cbname) {
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('user_callback', null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
+    return d;
 }
 
+function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
+    var d = userCallback(cbname);
+    d.addCallback(function() {
+        $('#' + nodeid).loadxhtml('json', ajaxFuncArgs('render', {
+            'rql': rql
+        },
+        registry, compid));
+        if (msg) {
+            updateMessage(msg);
+        }
+    });
+}
+
+function userCallbackThenReloadPage(cbname, msg) {
+    var d = userCallback(cbname);
+    d.addCallback(function() {
+        window.location.reload();
+        if (msg) {
+            updateMessage(msg);
+        }
+    });
+}
+
+/**
+ * .. function:: unregisterUserCallback(cbname)
+ *
+ * unregisters the python function registered on the server's side
+ * while the page was generated.
+ */
+function unregisterUserCallback(cbname) {
+    setProgressCursor();
+    var d = loadRemote('json', ajaxFuncArgs('unregister_user_callback',
+                                            null, cbname));
+    d.addCallback(resetCursor);
+    d.addErrback(resetCursor);
+    d.addErrback(remoteCallFailed);
+}
+
+//============= XXX move those functions? ====================================//
 function openHash() {
     if (document.location.hash) {
         var nid = document.location.hash.replace('#', '');
         var node = jQuery('#' + nid);
-        if (node) { removeElementClass(node, "hidden"); }
+        if (node) {
+            $(node).removeClass("hidden");
+        }
     };
 }
 jQuery(document).ready(openHash);
 
-function reloadComponent(compid, rql, registry, nodeid, extraargs) {
-    registry = registry || 'components';
-    rql = rql || '';
-    nodeid = nodeid || (compid + 'Component');
-    extraargs = extraargs || {};
-    var node = getNode(nodeid);
-    var d = asyncRemoteExec('component', compid, rql, registry, extraargs);
-    d.addCallback(function(result, req) {
-        var domnode = getDomFromResponse(result);
-        if (node) {
-            // make sure the component is visible
-            removeElementClass(node, "hidden");
-            domnode = preprocessAjaxLoad(node, domnode);
-            swapDOM(node, domnode);
-            postAjaxLoad(domnode);
-        }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-    });
-  return d;
-}
-
-/* XXX: HTML architecture of cubicweb boxes is a bit strange */
-function reloadBox(boxid, rql) {
-    return reloadComponent(boxid, rql, 'boxes', boxid);
-}
-
-function userCallbackThenUpdateUI(cbname, compid, rql, msg, registry, nodeid) {
-    var d = asyncRemoteExec('user_callback', cbname);
-    d.addCallback(function() {
-        reloadComponent(compid, rql, registry, nodeid);
-        if (msg) { updateMessage(msg); }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-function userCallbackThenReloadPage(cbname, msg) {
-    var d = asyncRemoteExec('user_callback', cbname);
-    d.addCallback(function() {
-        window.location.reload();
-        if (msg) { updateMessage(msg); }
-    });
-    d.addCallback(resetCursor);
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-/*
- * unregisters the python function registered on the server's side
- * while the page was generated.
- */
-function unregisterUserCallback(cbname) {
-    var d = asyncRemoteExec('unregister_user_callback', cbname);
-    d.addCallback(function() {resetCursor();});
-    d.addErrback(function(xxx) {
-        updateMessage(_("an error occured"));
-        log(xxx);
-        return resetCursor();
-    });
-}
-
-
-/* executes an async query to the server and replaces a node's
- * content with the query result
+/**
+ * .. function:: buildWysiwygEditors(parent)
  *
- * @param nodeId the placeholder node's id
- * @param rql the RQL query
- * @param vid the vid to apply to the RQL selection (default if not specified)
- * @param extraparmas table of additional query parameters
- */
-function replacePageChunk(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
-    var params = null;
-    if (callback) {
-        params = {callback: callback};
-    }
-
-    var node = jQuery('#' + nodeId)[0];
-    var props = {};
-    if (node) {
-        props['rql'] = rql;
-        props['fname'] = 'view';
-        props['pageid'] = pageid;
-        if (vid) { props['vid'] = vid; }
-        if (extraparams) { jQuery.extend(props, extraparams); }
-        // FIXME we need to do asURL(props) manually instead of
-        // passing `props` directly to loadxml because replacePageChunk
-        // is sometimes called (abusively) with some extra parameters in `vid`
-        var mode = swap?'swap':'replace';
-        var url = JSON_BASE_URL + asURL(props);
-        jQuery(node).loadxhtml(url, params, 'get', mode);
-    } else {
-        log('Node', nodeId, 'not found');
-    }
-}
-
-/* XXX deprecates?
- * fetches `url` and replaces `nodeid`'s content with the result
- * @param replacemode how the replacement should be done (default is 'replace')
- *  Possible values are :
- *    - 'replace' to replace the node's content with the generated HTML
- *    - 'swap' to replace the node itself with the generated HTML
- *    - 'append' to append the generated HTML to the node's content
- */
-function loadxhtml(nodeid, url, /* ... */ replacemode) {
-    jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
-}
-
-/* XXX: this function should go in edition.js but as for now, htmlReplace
+ *XXX: this function should go in edition.js but as for now, htmlReplace
  * references it.
  *
  * replace all textareas with fckeditors.
  */
 function buildWysiwygEditors(parent) {
-    jQuery('textarea').each(function () {
+    jQuery('textarea').each(function() {
         if (this.getAttribute('cubicweb:type') == 'wysiwyg') {
             // mark editor as instanciated, we may be called a number of times
-            // (see postAjaxLoad)
+            // (see _postAjaxLoad)
             this.setAttribute('cubicweb:type', 'fckeditor');
             if (typeof FCKeditor != "undefined") {
                 var fck = new FCKeditor(this.id);
@@ -410,29 +423,29 @@
                 fck.BasePath = "fckeditor/";
                 fck.ReplaceTextarea();
             } else {
-                log('fckeditor could not be found.');
+                cw.log('fckeditor could not be found.');
             }
         }
     });
 }
-
 jQuery(document).ready(buildWysiwygEditors);
 
-
-/*
+/**
+ * .. function:: stripEmptyTextNodes(nodelist)
+ *
  * takes a list of DOM nodes and removes all empty text nodes
  */
 function stripEmptyTextNodes(nodelist) {
     /* this DROPS empty text nodes */
     var stripped = [];
-    for (var i=0; i < nodelist.length; i++) {
+    for (var i = 0; i < nodelist.length; i++) {
         var node = nodelist[i];
         if (isTextNode(node)) {
-             /* all browsers but FF -> innerText, FF -> textContent  */
-             var text = node.innerText || node.textContent;
-             if (text && !text.strip()) {
-               continue;
-             }
+            /* all browsers but FF -> innerText, FF -> textContent  */
+            var text = node.innerText || node.textContent;
+            if (text && ! text.strip()) {
+                continue;
+            }
         } else {
             stripped.push(node);
         }
@@ -440,7 +453,10 @@
     return stripped;
 }
 
-/* convenience function that returns a DOM node based on req's result.
+/**
+ * .. function:: getDomFromResponse(response)
+ *
+ * convenience function that returns a DOM node based on req's result.
  * XXX clarify the need to clone
  * */
 function getDomFromResponse(response) {
@@ -460,18 +476,123 @@
         return jQuery(children[0]).clone().context;
     }
     // several children => wrap them in a single node and return the wrap
-    return DIV({'cubicweb:type': "cwResponseWrapper"},
-               map(function(node) {
-                    return jQuery(node).clone().context;
-            }, children));
-}
-
-function postJSON(url, data, callback) {
-    return jQuery.post(url, data, callback, 'json');
-}
-
-function getJSON(url, data, callback){
-    return jQuery.get(url, data, callback, 'json');
+    return DIV({
+        'cubicweb:type': "cwResponseWrapper"
+    },
+    map(function(node) {
+        return jQuery(node).clone().context;
+    },
+    children));
 }
 
 CubicWeb.provide('ajax.js');
+
+/* DEPRECATED *****************************************************************/
+
+preprocessAjaxLoad = cw.utils.deprecatedFunction(
+    '[3.9] preprocessAjaxLoad() is deprecated, use loadAjaxHtmlHead instead',
+    function(node, newdomnode) {
+        return loadAjaxHtmlHead(newdomnode);
+    }
+);
+
+reloadComponent = cw.utils.deprecatedFunction(
+    '[3.9] reloadComponent() is deprecated, use loadxhtml instead',
+    function(compid, rql, registry, nodeid, extraargs) {
+        registry = registry || 'components';
+        rql = rql || '';
+        nodeid = nodeid || (compid + 'Component');
+        extraargs = extraargs || {};
+        var node = jqNode(nodeid);
+        return node.loadxhtml('json', ajaxFuncArgs('component', null, compid,
+                                                   rql, registry, extraargs));
+    }
+);
+
+reloadBox = cw.utils.deprecatedFunction(
+    '[3.9] reloadBox() is deprecated, use loadxhtml instead',
+    function(boxid, rql) {
+        return reloadComponent(boxid, rql, 'boxes', boxid);
+    }
+);
+
+replacePageChunk = cw.utils.deprecatedFunction(
+    '[3.9] replacePageChunk() is deprecated, use loadxhtml instead',
+    function(nodeId, rql, vid, extraparams, /* ... */ swap, callback) {
+        var params = null;
+        if (callback) {
+            params = {
+                callback: callback
+            };
+        }
+        var node = jQuery('#' + nodeId)[0];
+        var props = {};
+        if (node) {
+            props['rql'] = rql;
+            props['fname'] = 'view';
+            props['pageid'] = pageid;
+            if (vid) {
+                props['vid'] = vid;
+            }
+            if (extraparams) {
+                jQuery.extend(props, extraparams);
+            }
+            // FIXME we need to do asURL(props) manually instead of
+            // passing `props` directly to loadxml because replacePageChunk
+            // is sometimes called (abusively) with some extra parameters in `vid`
+            var mode = swap ? 'swap': 'replace';
+            var url = JSON_BASE_URL + asURL(props);
+            jQuery(node).loadxhtml(url, params, 'get', mode);
+        } else {
+            cw.log('Node', nodeId, 'not found');
+        }
+    }
+);
+
+loadxhtml = cw.utils.deprecatedFunction(
+    '[3.9] loadxhtml() function is deprecated, use loadxhtml method instead',
+    function(nodeid, url, /* ... */ replacemode) {
+        jQuery('#' + nodeid).loadxhtml(url, null, 'post', replacemode);
+    }
+);
+
+remoteExec = cw.utils.deprecatedFunction(
+    '[3.9] remoteExec() is deprecated, use loadRemote instead',
+    function(fname /* ... */) {
+        setProgressCursor();
+        var props = {
+            'fname': fname,
+            'pageid': pageid,
+            'arg': map(jQuery.toJSON, sliceList(arguments, 1))
+        };
+        var result = jQuery.ajax({
+            url: JSON_BASE_URL,
+            data: props,
+            async: false
+        }).responseText;
+        if (result) {
+            result = cw.evalJSON(result);
+        }
+        resetCursor();
+        return result;
+    }
+);
+
+asyncRemoteExec = cw.utils.deprecatedFunction(
+    '[3.9] asyncRemoteExec() is deprecated, use loadRemote instead',
+    function(fname /* ... */) {
+        setProgressCursor();
+        var props = {
+            'fname': fname,
+            'pageid': pageid,
+            'arg': map(jQuery.toJSON, sliceList(arguments, 1))
+        };
+        // XXX we should inline the content of loadRemote here
+        var deferred = loadRemote(JSON_BASE_URL, props, 'POST');
+        deferred = deferred.addErrback(remoteCallFailed);
+        deferred = deferred.addErrback(resetCursor);
+        deferred = deferred.addCallback(resetCursor);
+        return deferred;
+    }
+);
+
--- a/web/data/cubicweb.bookmarks.js	Thu Jun 03 10:17:44 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,10 +0,0 @@
-CubicWeb.require('ajax.js');
-
-function removeBookmark(beid) {
-    d = asyncRemoteExec('delete_bookmark', beid);
-    d.addCallback(function(boxcontent) {
-	    reloadComponent('bookmarks_box', '', 'boxes', 'bookmarks_box');
-  	document.location.hash = '#header';
- 	updateMessage(_("bookmark has been removed"));
-    });
-}
--- a/web/data/cubicweb.calendar.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.calendar.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,7 +1,7 @@
-/*
+/**
  *  This file contains Calendar utilities
  *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
@@ -10,14 +10,14 @@
 
 // IMPORTANT NOTE: the variables DAYNAMES AND MONTHNAMES will be added
 //                 by cubicweb automatically
-
 // dynamically computed (and cached)
 var _CAL_HEADER = null;
 
 TODAY = new Date();
 
-
-/*
+/**
+ * .. class:: Calendar
+ *
  * Calendar (graphical) widget
  * public methods are :
  *   __init__ :
@@ -31,7 +31,7 @@
  *
  *   toggle():
  *    show (resp. hide) the calendar if it's hidden (resp. displayed)
- * 
+ *
  *   displayNextMonth(): (resp. displayPreviousMonth())
  *    update the calendar to display next (resp. previous) month
  */
@@ -39,177 +39,219 @@
     this.containerId = containerId;
     this.inputId = inputId;
     this.year = year;
-    this.month = month-1; // Javascript's counter starts at 0 for january
+    this.month = month - 1; // Javascript's counter starts at 0 for january
     this.cssclass = cssclass || "popupCalendar";
     this.visible = false;
     this.domtable = null;
 
-    this.cellprops = { 'onclick'     : function() {dateSelected(this, containerId); },
-		       'onmouseover' : function() {this.style.fontWeight = 'bold'; },
-		       'onmouseout'  : function() {this.style.fontWeight = 'normal';}
-		     }
+    this.cellprops = {
+        'onclick': function() {
+            dateSelected(this, containerId);
+        },
+        'onmouseover': function() {
+            this.style.fontWeight = 'bold';
+        },
+        'onmouseout': function() {
+            this.style.fontWeight = 'normal';
+        }
+    }
 
-    this.todayprops = jQuery.extend({}, this.cellprops, {'class' : 'today'});
+    this.todayprops = jQuery.extend({},
+    this.cellprops, {
+        'class': 'today'
+    });
 
     this._rowdisplay = function(row) {
-	return TR(null, map(partial(TD, this.cellprops), row));
-    }
+        var _td = function(elt) {
+            return TD(this.cellprops, elt);
+        };
+        return TR(null, map(_td, row));
+    };
 
     this._makecell = function(cellinfo) {
-	return TD(cellinfo[0], cellinfo[1]);
-    }
+        return TD(cellinfo[0], cellinfo[1]);
+    };
 
-    /* utility function (the only use for now is inside the calendar) */
-    this._uppercaseFirst = function(s) { return s.charAt(0).toUpperCase(); }
-    
-    /* accepts the cells data and builds the corresponding TR nodes
-     * @param rows a list of list of couples (daynum, cssprops)
+    /**
+     * .. function:: Calendar._uppercaseFirst(s)
+     *
+     * utility function (the only use for now is inside the calendar)
      */
-    this._domForRows = function(rows) {
-	var lines = []
-	for (i=0; i<rows.length; i++) {
-	    lines.push(TR(null, map(this._makecell, rows[i])));
-	}
-	return lines;
+    this._uppercaseFirst = function(s) {
+        return s.charAt(0).toUpperCase();
     }
 
-    /* builds the calendar headers */
+    /**
+     * .. function:: Calendar._domForRows(rows)
+     *
+     * accepts the cells data and builds the corresponding TR nodes
+     *
+     * * `rows`, a list of list of couples (daynum, cssprops)
+     */
+    this._domForRows = function(rows) {
+        var lines = [];
+        for (i = 0; i < rows.length; i++) {
+            lines.push(TR(null, map(this._makecell, rows[i])));
+        }
+        return lines;
+    };
+
+    /**
+     * .. function:: Calendar._headdisplay(row)
+     *
+     * builds the calendar headers
+     */
     this._headdisplay = function(row) {
-	if (_CAL_HEADER) {
-	    return _CAL_HEADER;
-	}
-	daynames = map(this._uppercaseFirst, DAYNAMES);
-	_CAL_HEADER = TR(null, map(partial(TH, null), daynames));
-	return _CAL_HEADER;
-    }
-    
+        if (_CAL_HEADER) {
+            return _CAL_HEADER;
+        }
+        var self = this;
+        var _th = function(day) {
+            return TH(null, self._uppercaseFirst(day));
+        };
+        return TR(null, map(_th, DAYNAMES));
+    };
+
     this._getrows = function() {
-	var rows = [];
-	var firstday = new Date(this.year, this.month, 1);
-	var stopdate = firstday.nextMonth();
-	var curdate = firstday.sub(firstday.getRealDay());
-	while (curdate.getTime() < stopdate) {
-	    var row = []
-	    for (var i=0; i<7; i++) {
-		if (curdate.getMonth() == this.month) {
-		    props = curdate.equals(TODAY) ? this.todayprops:this.cellprops;
-		    row.push([props, curdate.getDate()]);
-		} else {
-		    row.push([this.cellprops, ""]);
-		}
-		curdate.iadd(1);
-	    }
-	    rows.push(row);
-	}
-	return rows;
+        var rows = [];
+        var firstday = new Date(this.year, this.month, 1);
+        var stopdate = firstday.nextMonth();
+        var curdate = firstday.sub(firstday.getRealDay());
+        while (curdate.getTime() < stopdate) {
+            var row = []
+            for (var i = 0; i < 7; i++) {
+                if (curdate.getMonth() == this.month) {
+                    props = curdate.equals(TODAY) ? this.todayprops: this.cellprops;
+                    row.push([props, curdate.getDate()]);
+                } else {
+                    row.push([this.cellprops, ""]);
+                }
+                curdate.iadd(1);
+            }
+            rows.push(row);
+        }
+        return rows;
     }
 
     this._makecal = function() {
-	var rows = this._getrows();
-	var monthname = MONTHNAMES[this.month] + " " + this.year;
-	var prevlink = "javascript: togglePreviousMonth('" + this.containerId + "');";
-	var nextlink = "javascript: toggleNextMonth('" + this.containerId + "');";
-	this.domtable = TABLE({'class': this.cssclass},
-			      THEAD(null, TR(null,
-					     TH(null, A({'href' : prevlink}, "<<")),
-					     // IE 6/7 requires colSpan instead of colspan
-					     TH({'colSpan': 5, 'colspan':5, 'style' : "text-align: center;"}, monthname),
-					     TH(null, A({'href' : nextlink}, ">>")))),
-			      TBODY(null,
-				    this._headdisplay(),
-				    this._domForRows(rows))
-			     );
-	return this.domtable;
+        var rows = this._getrows();
+        var monthname = MONTHNAMES[this.month] + " " + this.year;
+        var prevlink = "javascript: togglePreviousMonth('" + this.containerId + "');";
+        var nextlink = "javascript: toggleNextMonth('" + this.containerId + "');";
+        this.domtable = TABLE({
+            'class': this.cssclass
+        },
+        THEAD(null, TR(null, TH(null, A({
+            'href': prevlink
+        },
+        "<<")),
+        // IE 6/7 requires colSpan instead of colspan
+        TH({
+            'colSpan': 5,
+            'colspan': 5,
+            'style': "text-align: center;"
+        },
+        monthname), TH(null, A({
+            'href': nextlink
+        },
+        ">>")))), TBODY(null, this._headdisplay(), this._domForRows(rows)));
+        return this.domtable;
     }
 
     this._updateDiv = function() {
-	if (!this.domtable) {
-	    this._makecal();
-	}
-	jqNode(this.containerId).empty().append(this.domtable);
-	// replaceChildNodes($(this.containerId), this.domtable);
+        if (!this.domtable) {
+            this._makecal();
+        }
+        cw.jqNode(this.containerId).empty().append(this.domtable);
+        // replaceChildNodes($(this.containerId), this.domtable);
     }
 
     this.displayNextMonth = function() {
-	this.domtable = null;
-	if (this.month == 11) {
-	    this.year++;
-	}
-	this.month = (this.month+1) % 12;
-	this._updateDiv();
+        this.domtable = null;
+        if (this.month == 11) {
+            this.year++;
+        }
+        this.month = (this.month + 1) % 12;
+        this._updateDiv();
     }
 
     this.displayPreviousMonth = function() {
-	this.domtable = null;
-	if (this.month == 0) {
-	    this.year--;
-	}
-	this.month = (this.month+11) % 12;
-	this._updateDiv();
+        this.domtable = null;
+        if (this.month == 0) {
+            this.year--;
+        }
+        this.month = (this.month + 11) % 12;
+        this._updateDiv();
     }
-    
+
     this.show = function() {
-	if (!this.visible) {
-	    container = jqNode(this.containerId);
-	    if (!this.domtable) {
-		this._makecal();
-	    }
-	    container.empty().append(this.domtable);
-	    toggleVisibility(container);
-	    this.visible = true;
-	}
+        if (!this.visible) {
+            container = cw.jqNode(this.containerId);
+            if (!this.domtable) {
+                this._makecal();
+            }
+            container.empty().append(this.domtable);
+            toggleVisibility(container);
+            this.visible = true;
+        }
     }
 
     this.hide = function(event) {
-	var self;
-	if (event) {
-	    self = event.data.self;
-	} else {
-	    self = this;
-	}
-	if (self.visible) {
-	    toggleVisibility(self.containerId);
-	    self.visible = false;
-	}
+        var self;
+        if (event) {
+            self = event.data.self;
+        } else {
+            self = this;
+        }
+        if (self.visible) {
+            toggleVisibility(self.containerId);
+            self.visible = false;
+        }
     }
 
     this.toggle = function() {
-	if (this.visible) {
-	    this.hide();
-	}
-	else {
-	    this.show();
-	}
+        if (this.visible) {
+            this.hide();
+        }
+        else {
+            this.show();
+        }
     }
 
     // call hide() when the user explicitly sets the focus on the matching input
-    jqNode(inputId).bind('focus', {'self': this}, this.hide); // connect(inputId, 'onfocus', this, 'hide');
+    cw.jqNode(inputId).bind('focus', {
+        'self': this
+    },
+    this.hide); // connect(inputId, 'onfocus', this, 'hide');
 };
 
 // keep track of each calendar created
 Calendar.REGISTRY = {};
 
-/*
+/**
+ * .. function:: toggleCalendar(containerId, inputId, year, month)
+ *
  * popup / hide calendar associated to `containerId`
- */	    
+ */
 function toggleCalendar(containerId, inputId, year, month) {
     var cal = Calendar.REGISTRY[containerId];
     if (!cal) {
-	cal = new Calendar(containerId, inputId, year, month);
-	Calendar.REGISTRY[containerId] = cal;
+        cal = new Calendar(containerId, inputId, year, month);
+        Calendar.REGISTRY[containerId] = cal;
     }
     /* hide other calendars */
     for (containerId in Calendar.REGISTRY) {
-	var othercal = Calendar.REGISTRY[containerId];
-	if (othercal !== cal) {
-	    othercal.hide();
-	}
+        var othercal = Calendar.REGISTRY[containerId];
+        if (othercal !== cal) {
+            othercal.hide();
+        }
     }
     cal.toggle();
 }
 
-
-/*
+/**
+ * .. function:: toggleNextMonth(containerId)
+ *
  * ask for next month to calendar displayed in `containerId`
  */
 function toggleNextMonth(containerId) {
@@ -217,7 +259,9 @@
     cal.displayNextMonth();
 }
 
-/*
+/**
+ * .. function:: togglePreviousMonth(containerId)
+ *
  * ask for previous month to calendar displayed in `containerId`
  */
 function togglePreviousMonth(containerId) {
@@ -225,8 +269,9 @@
     cal.displayPreviousMonth();
 }
 
-
-/*
+/**
+ * .. function:: dateSelected(cell, containerId)
+ *
  * Callback called when the user clicked on a cell in the popup calendar
  */
 function dateSelected(cell, containerId) {
@@ -236,86 +281,82 @@
     //      the only way understood by both IE and Mozilla. Otherwise,
     //      IE accepts innerText and mozilla accepts textContent
     var selectedDate = new Date(cal.year, cal.month, cell.innerHTML, 12);
-    var xxx = remoteExec("format_date", toISOTimestamp(selectedDate));
+    var xxx = remoteExec("format_date", cw.utils.toISOTimestamp(selectedDate));
     input.value = xxx;
     cal.hide();
 }
 
-function whichElement(e)
-{
-var targ;
-if (!e)
-  {
-  var e=window.event;
-  }
-if (e.target)
-  {
-  targ=e.target;
-  }
-else if (e.srcElement)
-  {
-  targ=e.srcElement;
-  }
-if (targ.nodeType==3) // defeat Safari bug
-  {
-  targ = targ.parentNode;
-  }
-  return targ;
+function whichElement(e) {
+    var targ;
+    if (!e) {
+        var e = window.event;
+    }
+    if (e.target) {
+        targ = e.target;
+    }
+    else if (e.srcElement) {
+        targ = e.srcElement;
+    }
+    if (targ.nodeType == 3) // defeat Safari bug
+    {
+        targ = targ.parentNode;
+    }
+    return targ;
 }
 
 function getPosition(element) {
-  var left;
-  var top;
-  var offset;
-  // TODO: deal scrollbar positions also!
-  left = element.offsetLeft;
-  top = element.offsetTop;
+    var left;
+    var top;
+    var offset;
+    // TODO: deal scrollbar positions also!
+    left = element.offsetLeft;
+    top = element.offsetTop;
 
-  if (element.offsetParent != null)
-    {
-      offset = getPosition(element.offsetParent);
-      left = left + offset[0];
-      top = top + offset[1];
-      
+    if (element.offsetParent != null) {
+        offset = getPosition(element.offsetParent);
+        left = left + offset[0];
+        top = top + offset[1];
+
     }
-  return [left, top];
+    return [left, top];
 }
 
 function getMouseInBlock(event) {
-  var elt = event.target;
-  var x = event.clientX;
-  var y = event.clientY;
-  var w = elt.clientWidth;
-  var h = elt.clientHeight;
-  var offset = getPosition(elt);
+    var elt = event.target;
+    var x = event.clientX;
+    var y = event.clientY;
+    var w = elt.clientWidth;
+    var h = elt.clientHeight;
+    var offset = getPosition(elt);
 
-  x = 1.0*(x-offset[0])/w;
-  y = 1.0*(y-offset[1])/h;
-  return [x, y];
+    x = 1.0 * (x - offset[0]) / w;
+    y = 1.0 * (y - offset[1]) / h;
+    return [x, y];
 }
 function getHourFromMouse(event, hmin, hmax) {
-  var pos = getMouseInBlock(event);
-  var y = pos[1];
-  return Math.floor((hmax-hmin)*y + hmin);
+    var pos = getMouseInBlock(event);
+    var y = pos[1];
+    return Math.floor((hmax - hmin) * y + hmin);
 }
 
 function addCalendarItem(event, hmin, hmax, year, month, day, duration, baseurl) {
-  var hour = getHourFromMouse(event, hmin, hmax);
+    var hour = getHourFromMouse(event, hmin, hmax);
+
+    if (0 <= hour && hour < 24) {
+        baseurl += "&start=" + year + "%2F" + month + "%2F" + day + "%20" + hour + ":00";
+        baseurl += "&stop=" + year + "%2F" + month + "%2F" + day + "%20" + (hour + duration) + ":00";
 
-  if (0<=hour && hour < 24) {
-    baseurl += "&start="+year+"%2F"+month+"%2F"+day+"%20"+hour+":00";
-    baseurl += "&stop="+year+"%2F"+month+"%2F"+day+"%20"+(hour+duration)+":00";
-    
-    stopPropagation(event);
-    window.location.assign(baseurl);
-    return false;
-  }
-  return true;
+        stopPropagation(event);
+        window.location.assign(baseurl);
+        return false;
+    }
+    return true;
 }
 
 function stopPropagation(event) {
-  event.cancelBubble = true;
-  if (event.stopPropagation) event.stopPropagation();  
+    event.cancelBubble = true;
+    if (event.stopPropagation) event.stopPropagation();
 }
-     
+
 CubicWeb.provide('calendar.js');
+
--- a/web/data/cubicweb.compat.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.compat.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,546 +1,201 @@
-/* MochiKit -> jQuery compatibility module */
-
-function forEach(array, func) {
-    for (var i=0, length=array.length; i<length; i++) {
-	func(array[i]);
-    }
-}
-
-// XXX looks completely unused (candidate for removal)
-function getElementsByTagAndClassName(tag, klass, root) {
-    root = root || document;
-    // FIXME root is not used in this compat implementation
-    return jQuery(tag + '.' + klass);
-}
-
-/* jQUery flattens arrays returned by the mapping function:
-   >>> y = ['a:b:c', 'd:e']
-   >>> jQuery.map(y, function(y) { return y.split(':');})
-   ["a", "b", "c", "d", "e"]
-   // where one would expect:
-   [ ["a", "b", "c"], ["d", "e"] ]
-   XXX why not the same argument order as $.map and forEach ?
-*/
-function map(func, array) {
-    var result = [];
-    for (var i=0, length=array.length;
-         i<length;
-         i++) {
-	result.push(func(array[i]));
-    }
-    return result;
-}
-
-function findValue(array, element) {
-    return jQuery.inArray(element, array);
-}
-
-function filter(func, array) {
-    return jQuery.grep(array, func);
-}
-
-function noop() {}
-
-function addElementClass(node, klass) {
-    jQuery(node).addClass(klass);
-}
-
-// XXX looks completely unused (candidate for removal)
-function toggleElementClass(node, klass) {
-    jQuery(node).toggleClass(klass);
-}
-
-function removeElementClass(node, klass) {
-    jQuery(node).removeClass(klass);
-}
-
-hasElementClass = jQuery.className.has;
+cw.utils.movedToNamespace(['log', 'jqNode', 'getNode', 'evalJSON', 'urlEncode',
+                           'swapDOM'], cw);
+cw.utils.movedToNamespace(['nodeWalkDepthFirst', 'formContents', 'isArray',
+                           'isString', 'isArrayLike', 'sliceList',
+                           'toISOTimestamp'], cw.utils);
 
 
-function partial(func) {
-    var args = sliceList(arguments, 1);
-    return function() {
-	return func.apply(null, merge(args, arguments));
-    };
-}
-
-
-function log() {
-    // XXX dummy implementation
-    // console.log.apply(arguments); ???
-    var args = [];
-    for (var i=0; i<arguments.length; i++) {
-	args.push(arguments[i]);
-    }
-    if (typeof(window) != "undefined" && window.console
-        && window.console.log) {
-	window.console.log(args.join(' '));
-    }
-}
-
-function getNodeAttribute(node, attribute) {
-    return jQuery(node).attr(attribute);
-}
-
-function isArray(it){ // taken from dojo
-    return it && (it instanceof Array || typeof it == "array");
-}
-
-function isString(it){ // taken from dojo
-    return !!arguments.length && it != null && (typeof it == "string" || it instanceof String);
-}
-
-
-function isArrayLike(it) { // taken from dojo
-    return (it && it !== undefined &&
-	    // keep out built-in constructors (Number, String, ...) which have length
-	    // properties
-	    !isString(it) && !jQuery.isFunction(it) &&
-	    !(it.tagName && it.tagName.toLowerCase() == 'form') &&
-	    (isArray(it) || isFinite(it.length)));
-}
-
-
-function getNode(node) {
-    if (typeof(node) == 'string') {
-        return document.getElementById(node);
-    }
-    return node;
-}
-
-/* safe version of jQuery('#nodeid') because we use ':' in nodeids
- * which messes with jQuery selection mechanism
- */
-function jqNode(node) {
-    node = getNode(node);
-    if (node) {
-	return jQuery(node);
-    }
-    return null;
-}
-
-function evalJSON(json) { // trust source
-    return eval("(" + json + ")");
-}
-
-function urlEncode(str) {
-    if (typeof(encodeURIComponent) != "undefined") {
-        return encodeURIComponent(str).replace(/\'/g, '%27');
-    } else {
-        return escape(str).replace(/\+/g, '%2B').replace(/\"/g,'%22').rval.replace(/\'/g, '%27');
-    }
+if ($.noop === undefined) {
+    function noop() {}
+} else {
+    noop = cw.utils.deprecatedFunction(
+        '[3.9] noop() is deprecated, use $.noop() instead (XXX requires jQuery 1.4)',
+        $.noop);
 }
 
-function swapDOM(dest, src) {
-    dest = getNode(dest);
-    var parent = dest.parentNode;
-    if (src) {
-        src = getNode(src);
-        parent.replaceChild(src, dest);
-    } else {
-        parent.removeChild(dest);
+// ========== ARRAY EXTENSIONS ========== ///
+Array.prototype.contains = cw.utils.deprecatedFunction(
+    '[3.9] array.contains(elt) is deprecated, use $.inArray(elt, array) instead',
+    function(element) {
+        return jQuery.inArray(element, this) != - 1;
     }
-    return src;
-}
+);
 
-function replaceChildNodes(node/*, nodes...*/) {
-    var elem = getNode(node);
-    arguments[0] = elem;
-    var child;
-    while ((child = elem.firstChild)) {
-        elem.removeChild(child);
+// ========== END OF ARRAY EXTENSIONS ========== ///
+forEach = cw.utils.deprecatedFunction(
+    '[3.9] forEach() is deprecated, use $.each() instead',
+    function(array, func) {
+        return $.each(array, func);
     }
-    if (arguments.length < 2) {
-        return elem;
-    } else {
-	for (var i=1; i<arguments.length; i++) {
-	    elem.appendChild(arguments[i]);
-	}
-	return elem;
-    }
-}
-
-update = jQuery.extend;
-
-
-function createDomFunction(tag) {
+);
 
-    function builddom(params, children) {
-	var node = document.createElement(tag);
-	for (key in params) {
-	    var value = params[key];
-	    if (key.substring(0, 2) == 'on') {
-		// this is an event handler definition
-		if (typeof value == 'string') {
-		    // litteral definition
-		    value = new Function(value);
-		}
-		node[key] = value;
-	    } else { // normal node attribute
-		jQuery(node).attr(key, params[key]);
-	    }
-	}
-	if (children) {
-	    if (!isArrayLike(children)) {
-		children = [children];
-		for (var i=2; i<arguments.length; i++) {
-		    var arg = arguments[i];
-		    if (isArray(arg)) {
-			children = merge(children, arg);
-		    } else {
-			children.push(arg);
-		    }
-		}
-	    }
-	    for (var i=0; i<children.length; i++) {
-		var child = children[i];
-		if (typeof child == "string" || typeof child == "number") {
-		    child = document.createTextNode(child);
-		}
-		node.appendChild(child);
-	    }
-	}
-	return node;
+/**
+ * .. function:: cw.utils.deprecatedFunction(msg, function)
+ *
+ * jQUery flattens arrays returned by the mapping function:
+ * >>> y = ['a:b:c', 'd:e']
+ * >>> jQuery.map(y, function(y) { return y.split(':');})
+ * ["a", "b", "c", "d", "e"]
+ *  // where one would expect:
+ *  [ ["a", "b", "c"], ["d", "e"] ]
+ *  XXX why not the same argument order as $.map and forEach ?
+ */
+map = cw.utils.deprecatedFunction(
+    '[3.9] map() is deprecated, use $.map instead',
+    function(func, array) {
+        var result = [];
+        for (var i = 0, length = array.length; i < length; i++) {
+            result.push(func(array[i]));
+        }
+        return result;
     }
-    return builddom;
-}
+);
+
+findValue = cw.utils.deprecatedFunction(
+    '[3.9] findValue(array, elt) is deprecated, use $.inArray(elt, array) instead',
+    function(array, element) {
+        return jQuery.inArray(element, array);
+    }
+);
 
-A = createDomFunction('a');
-BUTTON = createDomFunction('button');
-BR = createDomFunction('br');
-CANVAS = createDomFunction('canvas');
-DD = createDomFunction('dd');
-DIV = createDomFunction('div');
-DL = createDomFunction('dl');
-DT = createDomFunction('dt');
-FIELDSET = createDomFunction('fieldset');
-FORM = createDomFunction('form');
-H1 = createDomFunction('H1');
-H2 = createDomFunction('H2');
-H3 = createDomFunction('H3');
-H4 = createDomFunction('H4');
-H5 = createDomFunction('H5');
-H6 = createDomFunction('H6');
-HR = createDomFunction('hr');
-IMG = createDomFunction('img');
-INPUT = createDomFunction('input');
-LABEL = createDomFunction('label');
-LEGEND = createDomFunction('legend');
-LI = createDomFunction('li');
-OL = createDomFunction('ol');
-OPTGROUP = createDomFunction('optgroup');
-OPTION = createDomFunction('option');
-P = createDomFunction('p');
-PRE = createDomFunction('pre');
-SELECT = createDomFunction('select');
-SPAN = createDomFunction('span');
-STRONG = createDomFunction('strong');
-TABLE = createDomFunction('table');
-TBODY = createDomFunction('tbody');
-TD = createDomFunction('td');
-TEXTAREA = createDomFunction('textarea');
-TFOOT = createDomFunction('tfoot');
-TH = createDomFunction('th');
-THEAD = createDomFunction('thead');
-TR = createDomFunction('tr');
-TT = createDomFunction('tt');
-UL = createDomFunction('ul');
+filter = cw.utils.deprecatedFunction(
+    '[3.9] filter(func, array) is deprecated, use $.grep(array, f) instead',
+    function(func, array) {
+        return $.grep(array, func);
+    }
+);
+
+addElementClass = cw.utils.deprecatedFunction(
+    '[3.9] addElementClass(node, cls) is depcreated, use $(node).addClass(cls) instead',
+    function(node, klass) {
+        $(node).addClass(klass);
+    }
+);
+
+removeElementClass = cw.utils.deprecatedFunction(
+    '[3.9] removeElementClass(node, cls) is depcreated, use $(node).removeClass(cls) instead',
+    function(node, klass) {
+        $(node).removeClass(klass);
+    }
+);
 
-// cubicweb specific
-//IFRAME = createDomFunction('iframe');
-function IFRAME(params){
-  if ('name' in params){
-    try {
-      var node = document.createElement('<iframe name="'+params['name']+'">');
-    } catch (ex) {
-      var node = document.createElement('iframe');
-      node.id = node.name = params.name;
+hasElementClass = cw.utils.deprecatedFunction(
+    '[3.9] hasElementClass(node, cls) is depcreated, use $.className.has(node, cls)',
+    function(node, klass) {
+        return $.className.has(node, klass);
+    }
+);
+
+getNodeAttribute = cw.utils.deprecatedFunction(
+    '[3.9] getNodeAttribute(node, attr) is deprecated, use $(node).attr(attr)',
+    function(node, attribute) {
+        return $(node).attr(attribute);
     }
-  }
-  else{
-    var node = document.createElement('iframe');
-  }
-  for (key in params) {
-    if (key != 'name'){
-      var value = params[key];
-      if (key.substring(0, 2) == 'on') {
-	// this is an event handler definition
-	if (typeof value == 'string') {
-	  // litteral definition
-	  value = new Function(value);
-	}
-	node[key] = value;
-      } else { // normal node attribute
-	node.setAttribute(key, params[key]);
-      }
+);
+
+getNode = cw.utils.deprecatedFunction(
+    '[3.9] getNode(nodeid) is deprecated, use $(#nodeid) instead',
+    function(node) {
+        if (typeof node == 'string') {
+            return document.getElementById(node);
+        }
+        return node;
     }
-  }
-  return node;
-}
+);
 
-
-// dummy ultra minimalist implementation on deferred for jQuery
+/**
+ * .. function:: Deferred
+ *
+ * dummy ultra minimalist implementation on deferred for jQuery
+ */
 function Deferred() {
     this.__init__(this);
 }
 
 jQuery.extend(Deferred.prototype, {
     __init__: function() {
-	this._onSuccess = [];
-	this._onFailure = [];
-	this._req = null;
+        this._onSuccess = [];
+        this._onFailure = [];
+        this._req = null;
         this._result = null;
         this._error = null;
     },
 
     addCallback: function(callback) {
         if (this._req.readyState == 4) {
-            if (this._result) { callback.apply(null, this._result, this._req); }
+            if (this._result) {
+                var args = [this._result, this._req];
+                jQuery.merge(args, cw.utils.sliceList(arguments, 1));
+                callback.apply(null, args);
+            }
         }
-        else { this._onSuccess.push([callback, sliceList(arguments, 1)]); }
-	return this;
+        else {
+            this._onSuccess.push([callback, cw.utils.sliceList(arguments, 1)]);
+        }
+        return this;
     },
 
     addErrback: function(callback) {
         if (this._req.readyState == 4) {
-            if (this._error) { callback.apply(null, this._error, this._req); }
+            if (this._error) {
+                callback.apply(null, [this._error, this._req]);
+            }
         }
-        else { this._onFailure.push([callback, sliceList(arguments, 1)]); }
-	return this;
+        else {
+            this._onFailure.push([callback, cw.utils.sliceList(arguments, 1)]);
+        }
+        return this;
     },
 
     success: function(result) {
         this._result = result;
-	try {
-	    for (var i=0; i<this._onSuccess.length; i++) {
-		var callback = this._onSuccess[i][0];
-		var args = merge([result, this._req], this._onSuccess[i][1]);
-		callback.apply(null, args);
-	    }
-	} catch (error) {
-	    this.error(this.xhr, null, error);
-	}
+        try {
+            for (var i = 0; i < this._onSuccess.length; i++) {
+                var callback = this._onSuccess[i][0];
+                var args = [result, this._req];
+                jQuery.merge(args, this._onSuccess[i][1]);
+                callback.apply(null, args);
+            }
+        } catch(error) {
+            this.error(this.xhr, null, error);
+        }
     },
 
     error: function(xhr, status, error) {
         this._error = error;
-	for (var i=0; i<this._onFailure.length; i++) {
-	    var callback = this._onFailure[i][0];
-	    var args = merge([error, this._req], this._onFailure[i][1]);
-	    callback.apply(null, args);
-	}
+        for (var i = 0; i < this._onFailure.length; i++) {
+            var callback = this._onFailure[i][0];
+            var args = [error, this._req];
+            jQuery.merge(args, this._onFailure[i][1]);
+            callback.apply(null, args);
+        }
     }
 
 });
 
-
-/*
- * Asynchronously load an url and return a deferred
- * whose callbacks args are decoded according to
- * the Content-Type response header
- */
-function loadRemote(url, data, reqtype) {
-    var d = new Deferred();
-    jQuery.ajax({
-	url: url,
-	type: reqtype,
-	data: data,
-
-	beforeSend: function(xhr) {
-	    d._req = xhr;
-	},
-
-	success: function(data, status) {
-            if (d._req.getResponseHeader("content-type") == 'application/json') {
-              data = evalJSON(data);
-            }
-	    d.success(data);
-	},
-
-	error: function(xhr, status, error) {
-          try {
-            if (xhr.status == 500) {
-                var reason_dict = evalJSON(xhr.responseText);
-                d.error(xhr, status, reason_dict['reason']);
-                return;
-            }
-          } catch(exc) {
-            log('error with server side error report:' + exc);
-          }
-          d.error(xhr, status, null);
-	}
-    });
-    return d;
-}
-
-
-/** @id MochiKit.DateTime.toISOTime */
-toISOTime = function (date, realISO/* = false */) {
-    if (typeof(date) == "undefined" || date === null) {
-        return null;
-    }
-    var hh = date.getHours();
-    var mm = date.getMinutes();
-    var ss = date.getSeconds();
-    var lst = [
-        ((realISO && (hh < 10)) ? "0" + hh : hh),
-        ((mm < 10) ? "0" + mm : mm),
-        ((ss < 10) ? "0" + ss : ss)
-    ];
-    return lst.join(":");
-};
-
-_padTwo = function (n) {
-    return (n > 9) ? n : "0" + n;
-};
-
-/** @id MochiKit.DateTime.toISODate */
-toISODate = function (date) {
-    if (typeof(date) == "undefined" || date === null) {
-        return null;
-    }
-    return [
-        date.getFullYear(),
-        _padTwo(date.getMonth() + 1),
-        _padTwo(date.getDate())
-    ].join("-");
-};
-
-
-/** @id MochiKit.DateTime.toISOTimeStamp */
-toISOTimestamp = function (date, realISO/* = false*/) {
-    if (typeof(date) == "undefined" || date === null) {
-        return null;
-    }
-    var sep = realISO ? "T" : " ";
-    var foot = realISO ? "Z" : "";
-    if (realISO) {
-        date = new Date(date.getTime() + (date.getTimezoneOffset() * 60000));
-    }
-    return toISODate(date) + sep + toISOTime(date, realISO) + foot;
-};
-
-
-
-/* depth-first implementation of the nodeWalk function found
- * in MochiKit.Base
- * cf. http://mochikit.com/doc/html/MochiKit/Base.html#fn-nodewalk
+/**
+ * The only known usage of KEYS is in the tag cube. Once cubicweb-tag 1.7.0 is out,
+ * this current definition can be removed.
  */
-function nodeWalkDepthFirst(node, visitor) {
-    var children = visitor(node);
-    if (children) {
-	for(var i=0; i<children.length; i++) {
-	    nodeWalkDepthFirst(children[i], visitor);
-	}
-    }
-}
-
-
-/* Returns true if all the given Array-like or string arguments are not empty (obj.length > 0) */
-function isNotEmpty(obj) {
-    for (var i = 0; i < arguments.length; i++) {
-        var o = arguments[i];
-        if (!(o && o.length)) {
-            return false;
-        }
-    }
-    return true;
-}
-
-/** this implementation comes from MochiKit  */
-function formContents(elem/* = document.body */) {
-    var names = [];
-    var values = [];
-    if (typeof(elem) == "undefined" || elem === null) {
-        elem = document.body;
-    } else {
-        elem = getNode(elem);
-    }
-    nodeWalkDepthFirst(elem, function (elem) {
-        var name = elem.name;
-        if (isNotEmpty(name)) {
-            var tagName = elem.tagName.toUpperCase();
-            if (tagName === "INPUT"
-                && (elem.type == "radio" || elem.type == "checkbox")
-                && !elem.checked
-               ) {
-                return null;
-            }
-            if (tagName === "SELECT") {
-                if (elem.type == "select-one") {
-                    if (elem.selectedIndex >= 0) {
-                        var opt = elem.options[elem.selectedIndex];
-                        var v = opt.value;
-                        if (!v) {
-                            var h = opt.outerHTML;
-                            // internet explorer sure does suck.
-                            if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                v = opt.text;
-                            }
-                        }
-                        names.push(name);
-                        values.push(v);
-                        return null;
-                    }
-                    // no form elements?
-                    names.push(name);
-                    values.push("");
-                    return null;
-                } else {
-                    var opts = elem.options;
-                    if (!opts.length) {
-                        names.push(name);
-                        values.push("");
-                        return null;
-                    }
-                    for (var i = 0; i < opts.length; i++) {
-                        var opt = opts[i];
-                        if (!opt.selected) {
-                            continue;
-                        }
-                        var v = opt.value;
-                        if (!v) {
-                            var h = opt.outerHTML;
-                            // internet explorer sure does suck.
-                            if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
-                                v = opt.text;
-                            }
-                        }
-                        names.push(name);
-                        values.push(v);
-                    }
-                    return null;
-                }
-            }
-            if (tagName === "FORM" || tagName === "P" || tagName === "SPAN"
-                || tagName === "DIV"
-               ) {
-                return elem.childNodes;
-            }
-            names.push(name);
-            values.push(elem.value || '');
-            return null;
-        }
-        return elem.childNodes;
-    });
-    return [names, values];
-}
-
-function merge(array1, array2) {
-    var result = [];
-    for (var i=0,length=arguments.length; i<length; i++) {
-	var array = arguments[i];
-	for (var j=0,alength=array.length; j<alength; j++) {
-	    result.push(array[j]);
-	}
-    }
-    return result;
-}
-
 var KEYS = {
     KEY_ESC: 27,
     KEY_ENTER: 13
 };
 
+// XXX avoid crashes / backward compat
+CubicWeb = {
+    require: function(module) {},
+    provide: function(module) {}
+};
 
+jQuery(document).ready(function() {
+    jQuery(CubicWeb).trigger('server-response', [false, document]);
+});
 
+// XXX as of 2010-04-07, no known cube uses this
+jQuery(CubicWeb).bind('ajax-loaded', function() {
+    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
+    jQuery(CubicWeb).trigger('server-response', [false, document]);
+});
+
+CubicWeb.provide('python.js');
--- a/web/data/cubicweb.edition.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.edition.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,141 +1,174 @@
-/*
+/**
+ * Functions dedicated to edition.
+ *
  *  :organization: Logilab
  *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+ *
  */
 
 CubicWeb.require('python.js');
 CubicWeb.require('htmlhelpers.js');
 CubicWeb.require('ajax.js');
 
-
 //============= Eproperty form functions =====================================//
-
-/* called on Eproperty key selection:
+/**
+ * .. function:: setPropValueWidget(varname, tabindex)
+ *
+ * called on Eproperty key selection:
  * - get the selected value
  * - get a widget according to the key by a sync query to the server
  * - fill associated div with the returned html
  *
- * @param varname the name of the variable as used in the original creation form
- * @param tabindex the tabindex that should be set on the widget
+ * * `varname`, the name of the variable as used in the original creation form
+ * * `tabindex`, the tabindex that should be set on the widget
  */
 function setPropValueWidget(varname, tabindex) {
-    var key = firstSelected(document.getElementById('pkey:'+varname));
+    var key = firstSelected(document.getElementById('pkey:' + varname));
     if (key) {
-	var args = {fname: 'prop_widget', pageid: pageid,
-     		    arg: map(jQuery.toJSON, [key, varname, tabindex])};
-	jqNode('div:value:'+varname).loadxhtml(JSON_BASE_URL, args, 'post');
+        var args = {
+            fname: 'prop_widget',
+            pageid: pageid,
+            arg: map(jQuery.toJSON, [key, varname, tabindex])
+        };
+        jqNode('div:value:' + varname).loadxhtml(JSON_BASE_URL, args, 'post');
     }
 }
 
-
 // *** EDITION FUNCTIONS ****************************************** //
-
-/*
+/**
+ * .. function:: reorderTabindex(start, formid)
+ *
  * this function is called when an AJAX form was generated to
  * make sure tabindex remains consistent
  */
 function reorderTabindex(start, formid) {
     var form = getNode(formid || 'entityForm');
     var inputTypes = ['INPUT', 'SELECT', 'TEXTAREA'];
-    var tabindex = (start==null)?15:start;
+    var tabindex = (start == null) ? 15: start;
     nodeWalkDepthFirst(form, function(elem) {
         var tagName = elem.tagName.toUpperCase();
-	if (inputTypes.contains(tagName)) {
-	    if (getNodeAttribute(elem, 'tabindex') != null) {
-		tabindex += 1;
-		elem.setAttribute('tabindex', tabindex);
-	    }
-	    return null;
-	}
-	return filter(isElementNode, elem.childNodes);
+        if (inputTypes.contains(tagName)) {
+            if (jQuery(elem).attr('tabindex') != null) {
+                tabindex += 1;
+		jQuery(elem).attr('tabindex', tabindex);
+            }
+            return null;
+        }
+        return jQuery.grep(elem.childNodes, isElementNode);
     });
 }
 
-
 function showMatchingSelect(selectedValue, eid) {
     if (selectedValue) {
-	divId = 'div' + selectedValue + '_' + eid;
-	var divNode = jQuery('#' + divId);
-	if (!divNode.length) {
-	    var args = {vid: 'unrelateddivs', relation: selectedValue,
-			rql: rql_for_eid(eid), '__notemplate': 1,
-			callback: function() {_showMatchingSelect(eid, jQuery('#' + divId));}};
-	    jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
-	} else {
-	    _showMatchingSelect(eid, divNode);
-	}
+        divId = 'div' + selectedValue + '_' + eid;
+        var divNode = jQuery('#' + divId);
+        if (!divNode.length) {
+            var args = {
+                vid: 'unrelateddivs',
+                relation: selectedValue,
+                rql: rql_for_eid(eid),
+                '__notemplate': 1,
+                callback: function() {
+                    _showMatchingSelect(eid, jQuery('#' + divId));
+                }
+            };
+            jQuery('#unrelatedDivs_' + eid).loadxhtml(baseuri() + 'view', args, 'post', 'append');
+        } else {
+            _showMatchingSelect(eid, divNode);
+        }
     } else {
-	_showMatchingSelect(eid, null);
+        _showMatchingSelect(eid, null);
     }
 }
 
-
-// @param divNode is a jQuery selection
+/**
+ * .. function:: _showMatchingSelect(eid, divNode)
+ *
+ * * `divNode`, a jQuery selection
+ */
 function _showMatchingSelect(eid, divNode) {
     // hide all divs, and then show the matching one
     // (would actually be better to directly hide the displayed one)
     jQuery('#unrelatedDivs_' + eid).children().hide();
     // divNode not found means 'no relation selected' (i.e. first blank item)
     if (divNode && divNode.length) {
-	divNode.show();
+        divNode.show();
     }
 }
 
-// this function builds a Handle to cancel pending insertion
+/**
+ * .. function:: buildPendingInsertHandle(elementId, element_name, selectNodeId, eid)
+ *
+ * this function builds a Handle to cancel pending insertion
+ */
 function buildPendingInsertHandle(elementId, element_name, selectNodeId, eid) {
-   jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
-   return A({'class' : 'handle', 'href' : jscall,
-	     'title' : _("cancel this insert")}, '[x]');
+    jscall = "javascript: cancelPendingInsert('" + [elementId, element_name, selectNodeId, eid].join("', '") + "')";
+    return A({
+        'class': 'handle',
+        'href': jscall,
+        'title': _("cancel this insert")
+    },
+    '[x]');
 }
 
 function buildEntityLine(relationName, selectedOptionNode, comboId, eid) {
-   // textContent doesn't seem to work on selectedOptionNode
-   var content = selectedOptionNode.firstChild.nodeValue;
-   var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
-   var link = A({'href' : 'view?rql=' + selectedOptionNode.value,
-	  	 'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id},
-		content);
-   var tr = TR({'id' : 'tr' + selectedOptionNode.id}, [ TH(null, relationName),
-							TD(null, [handle, link])
-						      ]);
-   try {
-      var separator = getNode('relationSelectorRow_' + eid);
-      //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
-      // XXX Warning: separator.parentNode is not (always ?) the
-      // table itself, but an intermediate node (TableSectionElement)
-      var tableBody = separator.parentNode;
-      tableBody.insertBefore(tr, separator);
-   } catch(ex) {
-      log("got exception(2)!" + ex);
-   }
+    // textContent doesn't seem to work on selectedOptionNode
+    var content = selectedOptionNode.firstChild.nodeValue;
+    var handle = buildPendingInsertHandle(selectedOptionNode.id, 'tr', comboId, eid);
+    var link = A({
+        'href': 'view?rql=' + selectedOptionNode.value,
+        'class': 'editionPending',
+        'id': 'a' + selectedOptionNode.id
+    },
+    content);
+    var tr = TR({
+        'id': 'tr' + selectedOptionNode.id
+    },
+    [TH(null, relationName), TD(null, [handle, link])]);
+    try {
+        var separator = getNode('relationSelectorRow_' + eid);
+        //dump('relationSelectorRow_' + eid) XXX warn dump is not implemented in konqueror (at least)
+        // XXX Warning: separator.parentNode is not (always ?) the
+        // table itself, but an intermediate node (TableSectionElement)
+        var tableBody = separator.parentNode;
+        tableBody.insertBefore(tr, separator);
+    } catch(ex) {
+        log("got exception(2)!" + ex);
+    }
 }
 
 function buildEntityCell(relationName, selectedOptionNode, comboId, eid) {
     var handle = buildPendingInsertHandle(selectedOptionNode.id, 'div_insert_', comboId, eid);
-    var link = A({'href' : 'view?rql=' + selectedOptionNode.value,
-		  'class' : 'editionPending', 'id' : 'a' + selectedOptionNode.id},
-		 content);
-    var div = DIV({'id' : 'div_insert_' + selectedOptionNode.id}, [handle, link]);
+    var link = A({
+        'href': 'view?rql=' + selectedOptionNode.value,
+        'class': 'editionPending',
+        'id': 'a' + selectedOptionNode.id
+    },
+    content);
+    var div = DIV({
+        'id': 'div_insert_' + selectedOptionNode.id
+    },
+    [handle, link]);
     try {
-	var td = jQuery('#cell'+ relationName +'_'+eid);
-	td.appendChild(div);
+        var td = jQuery('#cell' + relationName + '_' + eid);
+        td.appendChild(div);
     } catch(ex) {
-	alert("got exception(3)!" + ex);
+        alert("got exception(3)!" + ex);
     }
 }
 
 function addPendingInsert(optionNode, eid, cell, relname) {
-    var value = getNodeAttribute(optionNode, 'value');
+    var value = jQuery(optionNode).attr('value');
     if (!value) {
-	// occurs when the first element in the box is selected (which is not
-	// an entity but the combobox title)
+        // occurs when the first element in the box is selected (which is not
+        // an entity but the combobox title)
         return;
     }
     // 2nd special case
     if (value.indexOf('http') == 0) {
-	document.location = value;
-	return;
+        document.location = value;
+        return;
     }
     // add hidden parameter
     var entityForm = jQuery('#entityForm');
@@ -146,16 +179,16 @@
     selectNode.removeChild(optionNode);
     // add line in table
     if (cell) {
-      // new relation as a cell in multiple edit
-      // var relation_name = relationSelected.getAttribute('value');
-      // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
-      buildEntityCell(relname, optionNode, selectNode.id, eid);
+        // new relation as a cell in multiple edit
+        // var relation_name = relationSelected.getAttribute('value');
+        // relation_name = relation_name.slice(0, relation_name.lastIndexOf('_'));
+        buildEntityCell(relname, optionNode, selectNode.id, eid);
     }
     else {
-	var relationSelector = getNode('relationSelector_'+eid);
-	var relationSelected = relationSelector.options[relationSelector.selectedIndex];
-	// new relation as a line in simple edit
-	buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
+        var relationSelector = getNode('relationSelector_' + eid);
+        var relationSelected = relationSelector.options[relationSelector.selectedIndex];
+        // new relation as a line in simple edit
+        buildEntityLine(relationSelected.text, optionNode, selectNode.id, eid);
     }
 }
 
@@ -164,90 +197,122 @@
     var entityView = jqNode('a' + elementId).text();
     jqNode(element_name + elementId).remove();
     if (comboId) {
-	// re-insert option in combobox if it was taken from there
-	var selectNode = getNode(comboId);
+        // re-insert option in combobox if it was taken from there
+        var selectNode = getNode(comboId);
         // XXX what on object relation
-	if (selectNode){
-	   var options = selectNode.options;
-	   var node_id = elementId.substring(0, elementId.indexOf(':'));
-	   options[options.length] = OPTION({'id' : elementId, 'value' : node_id}, entityView);
-	}
+        if (selectNode) {
+            var options = selectNode.options;
+            var node_id = elementId.substring(0, elementId.indexOf(':'));
+            options[options.length] = OPTION({
+                'id': elementId,
+                'value': node_id
+            },
+            entityView);
+        }
     }
     elementId = elementId.substring(2, elementId.length);
     remoteExec('remove_pending_insert', elementId.split(':'));
 }
 
-// this function builds a Handle to cancel pending insertion
+/**
+ * .. function:: buildPendingDeleteHandle(elementId, eid)
+ *
+ * this function builds a Handle to cancel pending insertion
+ */
 function buildPendingDeleteHandle(elementId, eid) {
-  var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
-  return A({'href' : jscall, 'class' : 'pendingDeleteHandle',
-    'title' : _("delete this relation")}, '[x]');
+    var jscall = "javascript: addPendingDelete('" + elementId + ', ' + eid + "');";
+    return A({
+        'href': jscall,
+        'class': 'pendingDeleteHandle',
+        'title': _("delete this relation")
+    },
+    '[x]');
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: addPendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function addPendingDelete(nodeId, eid) {
-    var d = asyncRemoteExec('add_pending_delete', nodeId.split(':'));
-    d.addCallback(function () {
-	// and strike entity view
-	jqNode('span' + nodeId).addClass('pendingDelete');
-	// replace handle text
-	jqNode('handle' + nodeId).text('+');
+    var d = loadRemote('json', ajaxFuncArgs('add_pending_delete', null, nodeId.split(':')));
+    d.addCallback(function() {
+        // and strike entity view
+        jqNode('span' + nodeId).addClass('pendingDelete');
+        // replace handle text
+        jqNode('handle' + nodeId).text('+');
     });
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: cancelPendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function cancelPendingDelete(nodeId, eid) {
-    var d = asyncRemoteExec('remove_pending_delete', nodeId.split(':'));
-    d.addCallback(function () {
-	// reset link's CSS class
-	jqNode('span' + nodeId).removeClass('pendingDelete');
-	// replace handle text
-	jqNode('handle' + nodeId).text('x');
+    var d = loadRemote('json', ajaxFuncArgs('remove_pending_delete', null, nodeId.split(':')));
+    d.addCallback(function() {
+        // reset link's CSS class
+        jqNode('span' + nodeId).removeClass('pendingDelete');
+        // replace handle text
+        jqNode('handle' + nodeId).text('x');
     });
 }
 
-// @param nodeId eid_from:r_type:eid_to
+/**
+ * .. function:: togglePendingDelete(nodeId, eid)
+ *
+ * * `nodeId`, eid_from:r_type:eid_to
+ */
 function togglePendingDelete(nodeId, eid) {
     // node found means we should cancel deletion
-    if ( hasElementClass(getNode('span' + nodeId), 'pendingDelete') ) {
-	cancelPendingDelete(nodeId, eid);
+    if (jQuery.className.has(getNode('span' + nodeId), 'pendingDelete')) {
+        cancelPendingDelete(nodeId, eid);
     } else {
-	addPendingDelete(nodeId, eid);
+        addPendingDelete(nodeId, eid);
     }
 }
 
-
 function selectForAssociation(tripletIdsString, originalEid) {
-    var tripletlist = map(function (x) { return x.split(':'); },
-			  tripletIdsString.split('-'));
-    var d = asyncRemoteExec('add_pending_inserts', tripletlist);
-    d.addCallback(function () {
-	var args = {vid: 'edition', __mode: 'normal',
-		    rql: rql_for_eid(originalEid)};
-	document.location = 'view?' + asURL(args);
+    var tripletlist = map(function(x) {
+        return x.split(':');
+    },
+    tripletIdsString.split('-'));
+    var d = loadRemote('json', ajaxFuncArgs('add_pending_inserts', null, tripletlist));
+    d.addCallback(function() {
+        var args = {
+            vid: 'edition',
+            __mode: 'normal',
+            rql: rql_for_eid(originalEid)
+        };
+        document.location = 'view?' + asURL(args);
     });
 
 }
 
-
 function updateInlinedEntitiesCounters(rtype, role) {
-    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function (i) {
-	this.innerHTML = i+1;
+    jQuery('div.inline-' + rtype + '-' + role + '-slot span.icounter').each(function(i) {
+        this.innerHTML = i + 1;
     });
 }
 
-
-/*
+/**
+ * .. function:: addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore)
+ *
  * makes an AJAX request to get an inline-creation view's content
- * @param peid : the parent entity eid
- * @param petype : the parent entity type
- * @param ttype : the target (inlined) entity type
- * @param rtype : the relation type between both entities
+ * * `peid`, the parent entity eid
+ *
+ * * `petype`, the parent entity type
+ *
+ * * `ttype`, the target (inlined) entity type
+ *
+ * * `rtype`, the relation type between both entities
  */
 function addInlineCreationForm(peid, petype, ttype, rtype, role, i18nctx, insertBefore) {
     insertBefore = insertBefore || getNode('add' + rtype + ':' + peid + 'link').parentNode;
-    var d = asyncRemoteExec('inline_creation_form', peid, petype, ttype, rtype, role, i18nctx);
-    d.addCallback(function (response) {
+    var args = ajaxFuncArgs('inline_creation_form', null, peid, petype, ttype, rtype, role, i18nctx);
+    var d = loadRemote('json', args);
+    d.addCallback(function(response) {
         var dom = getDomFromResponse(response);
         preprocessAjaxLoad(null, dom);
         var form = jQuery(dom);
@@ -259,49 +324,54 @@
         // if the inlined form contains a file input, we must force
         // the form enctype to multipart/form-data
         if (form.find('input:file').length) {
-	    // NOTE: IE doesn't support dynamic enctype modification, we have
-	    //       to set encoding too.
-            form.closest('form').attr('enctype', 'multipart/form-data')
-		.attr('encoding', 'multipart/form-data');
+            // NOTE: IE doesn't support dynamic enctype modification, we have
+            //       to set encoding too.
+            form.closest('form').attr('enctype', 'multipart/form-data').attr('encoding', 'multipart/form-data');
         }
         postAjaxLoad(dom);
     });
-    d.addErrback(function (xxx) {
+    d.addErrback(function(xxx) {
         log('xxx =', xxx);
     });
 }
 
-/*
+/**
+ * .. function:: removeInlineForm(peid, rtype, role, eid, showaddnewlink)
+ *
  * removes the part of the form used to edit an inlined entity
  */
 function removeInlineForm(peid, rtype, role, eid, showaddnewlink) {
     jqNode(['div', peid, rtype, eid].join('-')).slideUp('fast', function() {
-	$(this).remove();
-	updateInlinedEntitiesCounters(rtype, role);
+        $(this).remove();
+        updateInlinedEntitiesCounters(rtype, role);
     });
     if (showaddnewlink) {
-	toggleVisibility(showaddnewlink);
+        toggleVisibility(showaddnewlink);
     }
 }
 
-/*
+/**
+ * .. function:: removeInlinedEntity(peid, rtype, eid)
+ *
  * alternatively adds or removes the hidden input that make the
  * edition of the relation `rtype` possible between `peid` and `eid`
- * @param peid : the parent entity eid
- * @param rtype : the relation type between both entities
- * @param eid : the inlined entity eid
+ * * `peid`, the parent entity eid
+ *
+ * * `rtype`, the relation type between both entities
+ *
+ * * `eid`, the inlined entity eid
  */
 function removeInlinedEntity(peid, rtype, eid) {
     // XXX work around the eid_param thing (eid + ':' + eid) for #471746
     var nodeid = ['rel', peid, rtype, eid + ':' + eid].join('-');
     var node = jqNode(nodeid);
-    if (! node.attr('cubicweb:type')) {
+    if (!node.attr('cubicweb:type')) {
         node.attr('cubicweb:type', node.val());
         node.val('');
-	var divid = ['div', peid, rtype, eid].join('-');
-	jqNode(divid).fadeTo('fast', 0.5);
-	var noticeid = ['notice', peid, rtype, eid].join('-');
-	jqNode(noticeid).fadeIn('fast');
+        var divid = ['div', peid, rtype, eid].join('-');
+        jqNode(divid).fadeTo('fast', 0.5);
+        var noticeid = ['notice', peid, rtype, eid].join('-');
+        jqNode(noticeid).fadeIn('fast');
     }
 }
 
@@ -312,23 +382,23 @@
     if (node.attr('cubicweb:type')) {
         node.val(node.attr('cubicweb:type'));
         node.attr('cubicweb:type', '');
-	jqNode(['fs', peid, rtype, eid].join('-')).append(node);
+        jqNode(['fs', peid, rtype, eid].join('-')).append(node);
         var divid = ['div', peid, rtype, eid].join('-');
-	jqNode(divid).fadeTo('fast', 1);
+        jqNode(divid).fadeTo('fast', 1);
         var noticeid = ['notice', peid, rtype, eid].join('-');
-	jqNode(noticeid).hide();
+        jqNode(noticeid).hide();
     }
 }
 
 function _clearPreviousErrors(formid) {
     // on some case (eg max request size exceeded, we don't know the formid
     if (formid) {
-	jQuery('#' + formid + 'ErrorMessage').remove();
-	jQuery('#' + formid + ' span.errorMsg').remove();
-	jQuery('#' + formid + ' .error').removeClass('error');
+        jQuery('#' + formid + 'ErrorMessage').remove();
+        jQuery('#' + formid + ' span.errorMsg').remove();
+        jQuery('#' + formid + ' .error').removeClass('error');
     } else {
-	jQuery('span.errorMsg').remove();
-	jQuery('.error').removeClass('error');
+        jQuery('span.errorMsg').remove();
+        jQuery('.error').removeClass('error');
     }
 }
 
@@ -336,69 +406,75 @@
     var globalerrors = [];
     var firsterrfield = null;
     for (fieldname in errors) {
-	var errmsg = errors[fieldname];
-	if (!fieldname) {
-	    globalerrors.push(errmsg);
-	} else {
-	    var fieldid = fieldname + ':' + eid;
-	    var suffixes = ['', '-subject', '-object'];
-	    var found = false;
-	    // XXX remove suffixes at some point
-	    for (var i=0, length=suffixes.length; i<length;i++) {
-		var field = jqNode(fieldname + suffixes[i] + ':' + eid);
-		if (field && getNodeAttribute(field, 'type') != 'hidden') {
-		    if ( !firsterrfield ) {
-			firsterrfield = 'err-' + fieldid;
-		    }
-		    addElementClass(field, 'error');
-		    var span = SPAN({'id': 'err-' + fieldid, 'class': "errorMsg"}, errmsg);
-		    field.before(span);
-		    found = true;
-		    break;
-		}
-	    }
-	    if (!found) {
-		firsterrfield = formid;
-		globalerrors.push(_(fieldname) + ' : ' + errmsg);
-	    }
-	}
+        var errmsg = errors[fieldname];
+        if (!fieldname) {
+            globalerrors.push(errmsg);
+        } else {
+            var fieldid = fieldname + ':' + eid;
+            var suffixes = ['', '-subject', '-object'];
+            var found = false;
+            // XXX remove suffixes at some point
+            for (var i = 0, length = suffixes.length; i < length; i++) {
+                var field = jqNode(fieldname + suffixes[i] + ':' + eid);
+                if (field && jQuery(field).attr('type') != 'hidden') {
+                    if (!firsterrfield) {
+                        firsterrfield = 'err-' + fieldid;
+                    }
+                    jQuery(field).addClass('error');
+                    var span = SPAN({
+                        'id': 'err-' + fieldid,
+                        'class': "errorMsg"
+                    },
+                    errmsg);
+                    field.before(span);
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                firsterrfield = formid;
+                globalerrors.push(_(fieldname) + ' : ' + errmsg);
+            }
+        }
     }
     if (globalerrors.length) {
-	if (globalerrors.length == 1) {
-	    var innernode = SPAN(null, globalerrors[0]);
-	} else {
-	    var innernode = UL(null, map(partial(LI, null), globalerrors));
-	}
-	// insert DIV and innernode before the form
-	var div = DIV({'class' : "errorMessage", 'id': formid + 'ErrorMessage'});
-	div.appendChild(innernode);
-	jQuery('#' + formid).before(div);
+        if (globalerrors.length == 1) {
+            var innernode = SPAN(null, globalerrors[0]);
+        } else {
+            var innernode = UL(null, map(partial(LI, null), globalerrors));
+        }
+        // insert DIV and innernode before the form
+        var div = DIV({
+            'class': "errorMessage",
+            'id': formid + 'ErrorMessage'
+        });
+        div.appendChild(innernode);
+        jQuery('#' + formid).before(div);
     }
     return firsterrfield || formid;
 }
 
-
 function handleFormValidationResponse(formid, onsuccess, onfailure, result, cbargs) {
     // Success
     if (result[0]) {
-	if (onsuccess) {
-             onsuccess(result, formid, cbargs);
-	} else {
-	    document.location.href = result[1];
-	}
-      return true;
+        if (onsuccess) {
+            onsuccess(result, formid, cbargs);
+        } else {
+            document.location.href = result[1];
+        }
+        return true;
     }
-    if (onfailure && !onfailure(result, formid, cbargs)) {
-	return false;
+    if (onfailure && ! onfailure(result, formid, cbargs)) {
+        return false;
     }
     unfreezeFormButtons(formid);
     // Failures
     _clearPreviousErrors(formid);
     var descr = result[1];
     // Unknown structure
-    if ( !isArrayLike(descr) || descr.length != 2 ) {
-	updateMessage(descr);
-	return false;
+    if (!isArrayLike(descr) || descr.length != 2) {
+        updateMessage(descr);
+        return false;
     }
     _displayValidationerrors(formid, descr[0], descr[1]);
     updateMessage(_('please correct errors below'));
@@ -407,68 +483,102 @@
     return false;
 }
 
-
-/* unfreeze form buttons when the validation process is over*/
+/**
+ * .. function:: unfreezeFormButtons(formid)
+ *
+ * unfreeze form buttons when the validation process is over
+ */
 function unfreezeFormButtons(formid) {
     jQuery('#progress').hide();
     // on some case (eg max request size exceeded, we don't know the formid
     if (formid) {
-	jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
+        jQuery('#' + formid + ' .validateButton').removeAttr('disabled');
     } else {
-	jQuery('.validateButton').removeAttr('disabled');
+        jQuery('.validateButton').removeAttr('disabled');
     }
     return true;
 }
 
-/* disable form buttons while the validation is being done */
+/**
+ * .. function:: freezeFormButtons(formid)
+ *
+ * disable form buttons while the validation is being done
+ */
 function freezeFormButtons(formid) {
     jQuery('#progress').show();
     jQuery('#' + formid + ' .validateButton').attr('disabled', 'disabled');
     return true;
 }
 
-/* used by additional submit buttons to remember which button was clicked */
+/**
+ * .. function:: postForm(bname, bvalue, formid)
+ *
+ * used by additional submit buttons to remember which button was clicked
+ */
 function postForm(bname, bvalue, formid) {
     var form = getNode(formid);
     if (bname) {
-	var child = form.appendChild(INPUT({type: 'hidden', name: bname, value: bvalue}));
+        var child = form.appendChild(INPUT({
+            type: 'hidden',
+            name: bname,
+            value: bvalue
+        }));
     }
     var onsubmit = form.onsubmit;
     if (!onsubmit || (onsubmit && onsubmit())) {
-	form.submit();
+        form.submit();
     }
     if (bname) {
-	jQuery(child).remove(); /* cleanup */
+        jQuery(child).remove();
+        /* cleanup */
     }
 }
 
-
-/* called on load to set target and iframeso object.
- * NOTE: this is a hack to make the XHTML compliant.
- * NOTE2: `object` nodes might be a potential replacement for iframes
- * NOTE3: there is a XHTML module allowing iframe elements but there
- *        is still the problem of the form's `target` attribute
+/**
+ * .. function:: setFormsTarget(node)
+ *
+ * called on load to set target and iframeso object.
+ *
+ * .. note::
+ *
+ *    this is a hack to make the XHTML compliant.
+ *
+ * .. note::
+ *
+ *   `object` nodes might be a potential replacement for iframes
+ *
+ * .. note::
+ *
+ *    there is a XHTML module allowing iframe elements but there
+ *    is still the problem of the form's `target` attribute
  */
 function setFormsTarget(node) {
     var $node = jQuery(node || document.body);
-    $node.find('form').each(function () {
-	var form = jQuery(this);
-	var target = form.attr('cubicweb:target');
-	if (target) {
-	    form.attr('target', target);
-	    /* do not use display: none because some browsers ignore iframe
+    $node.find('form').each(function() {
+        var form = jQuery(this);
+        var target = form.attr('cubicweb:target');
+        if (target) {
+            form.attr('target', target);
+            /* do not use display: none because some browsers ignore iframe
              * with no display */
-	    form.append(IFRAME({name: target, id: target,
-				src: 'javascript: void(0)',
-				width: '0px', height: '0px'}));
-	}
+            form.append(IFRAME({
+                name: target,
+                id: target,
+                src: 'javascript: void(0)',
+                width: '0px',
+                height: '0px'
+            }));
+        }
     });
 }
 
-jQuery(document).ready(function() {setFormsTarget();});
+jQuery(document).ready(function() {
+    setFormsTarget();
+});
 
-
-/*
+/**
+ * .. function:: validateForm(formid, action, onsuccess, onfailure)
+ *
  * called on traditionnal form submission : the idea is to try
  * to post the form. If the post is successful, `validateForm` redirects
  * to the appropriate URL. Otherwise, the validation errors are displayed
@@ -476,69 +586,110 @@
  */
 function validateForm(formid, action, onsuccess, onfailure) {
     try {
-	var zipped = formContents(formid);
-	var d = asyncRemoteExec('validate_form', action, zipped[0], zipped[1]);
-    } catch (ex) {
-	log('got exception', ex);
-	return false;
+        var zipped = formContents(formid);
+        var args = ajaxFuncArgs('validate_form', null, action, zipped[0], zipped[1]);
+        var d = loadRemote('json', args);
+    } catch(ex) {
+        log('got exception', ex);
+        return false;
     }
     function _callback(result, req) {
-	handleFormValidationResponse(formid, onsuccess, onfailure, result);
+        handleFormValidationResponse(formid, onsuccess, onfailure, result);
     }
     d.addCallback(_callback);
     return false;
 }
 
 
-/*
+/**
+ * .. function:: inlineValidateRelationFormOptions(rtype, eid, divid, options)
+ *
  * called by reledit forms to submit changes
- * @param formid : the dom id of the form used
- * @param rtype : the attribute being edited
- * @param eid : the eid of the entity being edited
- * @param reload: boolean to reload page if true (when changing URL dependant data)
- * @param default_value : value if the field is empty
- * @param lzone : html fragment (string) for a clic-zone triggering actual edition
+ * * `rtype`, the attribute being edited
+ *
+ * * `eid`, the eid of the entity being edited
+ *
+ * * `options`, a dictionnary of options used by the form validation handler such
+ *    as ``role``, ``onsuccess``, ``onfailure``, ``reload``, ``vid``, ``lzone``
+ *    and ``default_value``:
+ *
+ *     * `onsucess`, javascript function to execute on success, default is noop
+ *
+ *     * `onfailure`, javascript function to execute on failure, default is noop
+ *
+ *     * `default_value`, value if the field is empty
+ *
+ *     * `lzone`, html fragment (string) for a clic-zone triggering actual edition
  */
-function inlineValidateRelationForm(rtype, role, eid, divid, reload, vid,
-                                    default_value, lzone) {
+function inlineValidateRelationFormOptions(rtype, eid, divid, options) {
     try {
-	var form = getNode(divid+'-form');
+        var form = getNode(divid + '-form');
         var relname = rtype + ':' + eid;
         var newtarget = jQuery('[name=' + relname + ']').val();
-	var zipped = formContents(form);
-	var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
-    } catch (ex) {
-	return false;
+        var zipped = cw.utils.formContents(form);
+        var args = ajaxFuncArgs('validate_form', null, 'apply', zipped[0], zipped[1]);
+        var d = loadRemote(JSON_BASE_URL, args, 'POST')
+    } catch(ex) {
+        return false;
     }
-    d.addCallback(function (result, req) {
-	if (handleFormValidationResponse(divid+'-form', noop, noop, result)) {
-          if (reload) {
+    d.addCallback(function(result, req) {
+        execFormValidationResponse(rtype, eid, divid, options, result);
+    });
+    return false;
+}
+
+function execFormValidationResponse(rtype, eid, divid, options, result) {
+    options = $.extend({onsuccess: noop,
+                        onfailure: noop
+                       }, options);
+    if (handleFormValidationResponse(divid + '-form', options.onsucess , options.onfailure, result)) {
+        if (options.reload) {
             document.location.reload();
-          } else {
-              var args = {fname: 'reledit_form', rtype: rtype, role: role, eid: eid, divid: divid,
-                          reload: reload, vid: vid, default_value: default_value, landing_zone: lzone};
-              jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
-          }
-	}
-        return false;
-    });
-  return false;
+        } else {
+            var args = {
+                fname: 'reledit_form',
+                rtype: rtype,
+                role: options.role,
+                eid: eid,
+                divid: divid,
+                reload: options.reload,
+                vid: options.vid,
+                default_value: options.default_value,
+                landing_zone: options.lzone
+            };
+            jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+        }
+    }
+
 }
 
 
-/**** inline edition ****/
-function loadInlineEditionForm(eid, rtype, role, divid, reload, vid,
-                               default_value, lzone) {
-  var args = {fname: 'reledit_form', rtype: rtype, role: role, eid: eid, divid: divid,
-              reload: reload, vid: vid, default_value: default_value, landing_zone: lzone,
-              callback: function () {showInlineEditionForm(eid, rtype, divid);}};
-  jQuery('#'+divid+'-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+/**
+ * .. function:: loadInlineEditionFormOptions(eid, rtype, divid, options)
+ *
+ * inline edition
+ */
+function loadInlineEditionFormOptions(eid, rtype, divid, options) {
+    var args = {
+        fname: 'reledit_form',
+        rtype: rtype,
+        role: options.role,
+        eid: eid,
+        divid: divid,
+        reload: options.reload,
+        vid: options.vid,
+        default_value: options.default_value,
+        landing_zone: options.lzone,
+        callback: function() {
+            showInlineEditionForm(eid, rtype, divid);
+        }
+    };
+    jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
 }
-
 function showInlineEditionForm(eid, rtype, divid) {
     jQuery('#' + divid).hide();
-    jQuery('#' + divid + '-value' ).hide();
-    jQuery('#' + divid+ '-form').show();
+    jQuery('#' + divid + '-value').hide();
+    jQuery('#' + divid + '-form').show();
 }
 
 function hideInlineEdit(eid, rtype, divid) {
@@ -546,7 +697,56 @@
     jQuery('div.errorMessage').remove();
     jQuery('#' + divid).show();
     jQuery('#' + divid + '-value').show();
-    jQuery('#' + divid +'-form').hide();
+    jQuery('#' + divid + '-form').hide();
 }
 
 CubicWeb.provide('edition.js');
+
+// ======================= DEPRECATED FUNCTIONS ========================= //
+inlineValidateRelationForm = cw.utils.deprecatedFunction(
+    '[3.9] inlineValidateRelationForm() function is deprecated, use inlineValidateRelationFormOptions instead',
+    function(rtype, role, eid, divid, reload, vid, default_value, lzone, onsucess, onfailure) {
+        try {
+            var form = getNode(divid + '-form');
+            var relname = rtype + ':' + eid;
+            var newtarget = jQuery('[name=' + relname + ']').val();
+            var zipped = formContents(form);
+            var d = asyncRemoteExec('validate_form', 'apply', zipped[0], zipped[1]);
+        } catch(ex) {
+            return false;
+        }
+        d.addCallback(function(result, req) {
+        var options = {role : role,
+                       reload: reload,
+                       vid: vid,
+                       default_value: default_value,
+                       lzone: lzone,
+                       onsucess: onsucess || $.noop,
+                       onfailure: onfailure || $.noop
+                      };
+            execFormValidationResponse(rtype, eid, divid, options);
+        });
+        return false;
+    }
+);
+
+loadInlineEditionForm = cw.utils.deprecatedFunction(
+    '[3.9] loadInlineEditionForm() function is deprecated, use loadInlineEditionFormOptions instead',
+    function(eid, rtype, role, divid, reload, vid, default_value, lzone) {
+        var args = {
+            fname: 'reledit_form',
+            rtype: rtype,
+            role: role,
+            eid: eid,
+            divid: divid,
+            reload: reload,
+            vid: vid,
+            default_value: default_value,
+            landing_zone: lzone,
+            callback: function() {
+                showInlineEditionForm(eid, rtype, divid);
+            }
+        };
+        jQuery('#' + divid + '-reledit').parent().loadxhtml(JSON_BASE_URL, args, 'post');
+    }
+);
--- a/web/data/cubicweb.facets.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.facets.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,6 +1,6 @@
-/*
+/**
  *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
@@ -9,215 +9,233 @@
 
 //============= filter form functions ========================================//
 function copyParam(origparams, newparams, param) {
-    var index = findValue(origparams[0], param);
-    if (index > -1) {
-	newparams[param] = origparams[1][index];
+    var index = jQuery.inArray(param, origparams[0]);
+    if (index > - 1) {
+        newparams[param] = origparams[1][index];
     }
 }
 
 function facetFormContent(form) {
     var names = [];
     var values = [];
-    jQuery(form).find('.facet').each(function () {
+    jQuery(form).find('.facet').each(function() {
         var facetName = jQuery(this).find('.facetTitle').attr('cubicweb:facetName');
         var facetValues = jQuery(this).find('.facetValueSelected').each(function(x) {
-  	    names.push(facetName);
-  	    values.push(this.getAttribute('cubicweb:value'));
+            names.push(facetName);
+            values.push(this.getAttribute('cubicweb:value'));
         });
     });
-    jQuery(form).find('input').each(function () {
+    jQuery(form).find('input').each(function() {
         names.push(this.name);
         values.push(this.value);
     });
-    jQuery(form).find('select option[selected]').each(function () {
-	names.push(this.parentNode.name);
-	values.push(this.value);
+    jQuery(form).find('select option[selected]').each(function() {
+        names.push(this.parentNode.name);
+        values.push(this.value);
     });
     return [names, values];
 }
 
 function buildRQL(divid, vid, paginate, vidargs) {
     jQuery(CubicWeb).trigger('facets-content-loading', [divid, vid, paginate, vidargs]);
-    var form = getNode(divid+'Form');
+    var form = getNode(divid + 'Form');
     var zipped = facetFormContent(form);
     zipped[0].push('facetargs');
     zipped[1].push(vidargs);
-    var d = asyncRemoteExec('filter_build_rql', zipped[0], zipped[1]);
+    var d = loadRemote('json', ajaxFuncArgs('filter_build_rql', null, zipped[0], zipped[1]));
     d.addCallback(function(result) {
-	var rql = result[0];
-	var $bkLink = jQuery('#facetBkLink');
-	if ($bkLink.length) {
-	    var bkPath = 'view?rql=' + escape(rql);
-	    if (vid) {
-		bkPath += '&vid=' + escape(vid);
-	    }
-	    var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
-	    $bkLink.attr('href', bkUrl);
-	}
-	var toupdate = result[1];
-	var extraparams = vidargs;
-	var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions');
-	if (displayactions) { extraparams['displayactions'] = displayactions; }
-	if (paginate) { extraparams['paginate'] = '1'; }
-	// copy some parameters
-	// XXX cleanup vid/divid mess
-	// if vid argument is specified , the one specified in form params will
-	// be overriden by replacePageChunk
-	copyParam(zipped, extraparams, 'vid');
-	extraparams['divid'] = divid;
-	copyParam(zipped, extraparams, 'divid');
-	copyParam(zipped, extraparams, 'subvid');
-	copyParam(zipped, extraparams, 'fromformfilter');
-	// paginate used to know if the filter box is acting, in which case we
-	// want to reload action box to match current selection (we don't want
-	// this from a table filter)
-	replacePageChunk(divid, rql, vid, extraparams, true, function() {
-	  jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
-	});
-	if (paginate) {
-	    // FIXME the edit box might not be displayed in which case we don't
-	    // know where to put the potential new one, just skip this case
-	    // for now
-	    if (jQuery('#edit_box').length) {
-		reloadComponent('edit_box', rql, 'boxes', 'edit_box');
-	    }
-	    if (jQuery('#breadcrumbs').length) {
-		reloadComponent('breadcrumbs', rql, 'components', 'breadcrumbs');
-	    }
-	}
-	var d = asyncRemoteExec('filter_select_content', toupdate, rql);
-	d.addCallback(function(updateMap) {
-	    for (facetId in updateMap) {
-		var values = updateMap[facetId];
-		jqNode(facetId).find('.facetCheckBox').each(function () {
-		    var value = this.getAttribute('cubicweb:value');
-		    if (!values.contains(value)) {
-			if (!jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).addClass('facetValueDisabled');
-			}
-		    } else {
-			if (jQuery(this).hasClass('facetValueDisabled')) {
-			    jQuery(this).removeClass('facetValueDisabled');
-			}
-		    }
-		});
-	    }
-	});
+        var rql = result[0];
+        var $bkLink = jQuery('#facetBkLink');
+        if ($bkLink.length) {
+            var bkPath = 'view?rql=' + escape(rql);
+            if (vid) {
+                bkPath += '&vid=' + escape(vid);
+            }
+            var bkUrl = $bkLink.attr('cubicweb:target') + '&path=' + escape(bkPath);
+            $bkLink.attr('href', bkUrl);
+        }
+        var toupdate = result[1];
+        var extraparams = vidargs;
+        var displayactions = jQuery('#' + divid).attr('cubicweb:displayactions');
+        if (displayactions) {
+            extraparams['displayactions'] = displayactions;
+        }
+        if (paginate) {
+            extraparams['paginate'] = '1';
+        }
+        // copy some parameters
+        // XXX cleanup vid/divid mess
+        // if vid argument is specified , the one specified in form params will
+        // be overriden by replacePageChunk
+        copyParam(zipped, extraparams, 'vid');
+        extraparams['divid'] = divid;
+        copyParam(zipped, extraparams, 'divid');
+        copyParam(zipped, extraparams, 'subvid');
+        copyParam(zipped, extraparams, 'fromformfilter');
+        // paginate used to know if the filter box is acting, in which case we
+        // want to reload action box to match current selection (we don't want
+        // this from a table filter)
+        extraparams['rql'] = rql;
+        extraparams['vid'] = vid;
+        d = $('#' + divid).loadxhtml('json', ajaxFuncArgs('view', extraparams));
+        d.addCallback(function() {
+            // XXX rql/vid in extraparams
+            jQuery(CubicWeb).trigger('facets-content-loaded', [divid, rql, vid, extraparams]);
+        });
+        if (paginate) {
+            // FIXME the edit box might not be displayed in which case we don't
+            // know where to put the potential new one, just skip this case
+            // for now
+            var $node = jQuery('#edit_box');
+            if ($node.length) {
+                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                    'rql': rql
+                },
+                'boxes', 'edit_box'));
+            }
+            $node = jQuery('#breadcrumbs')
+            if ($node.length) {
+                $node.loadxhtml('json', ajaxFuncArgs('render', {
+                    'rql': rql
+                },
+                'components', 'breadcrumbs'));
+            }
+        }
+        var d = loadRemote('json', ajaxFuncArgs('filter_select_content', null, toupdate, rql));
+        d.addCallback(function(updateMap) {
+            for (facetId in updateMap) {
+                var values = updateMap[facetId];
+                cw.jqNode(facetId).find('.facetCheckBox').each(function() {
+                    var value = this.getAttribute('cubicweb:value');
+                    if (jQuery.inArray(value, values) == -1) {
+                        if (!jQuery(this).hasClass('facetValueDisabled')) {
+                            jQuery(this).addClass('facetValueDisabled');
+                        }
+                    } else {
+                        if (jQuery(this).hasClass('facetValueDisabled')) {
+                            jQuery(this).removeClass('facetValueDisabled');
+                        }
+                    }
+                });
+            }
+        });
     });
 }
 
-
-var SELECTED_IMG = baseuri()+"data/black-check.png";
-var UNSELECTED_IMG = baseuri()+"data/no-check-no-border.png";
-var UNSELECTED_BORDER_IMG = baseuri()+"data/black-uncheck.png";
+var SELECTED_IMG = baseuri() + "data/black-check.png";
+var UNSELECTED_IMG = baseuri() + "data/no-check-no-border.png";
+var UNSELECTED_BORDER_IMG = baseuri() + "data/black-uncheck.png";
 
 function initFacetBoxEvents(root) {
     // facetargs : (divid, vid, paginate, extraargs)
     root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	// NOTE: don't evaluate facetargs here but in callbacks since its value
-	//       may changes and we must send its value when the callback is
-	//       called, not when the page is initialized
-	var facetargs = form.attr('cubicweb:facetargs');
-	if (facetargs !== undefined) {
-	    form.submit(function() {
-	        buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-	        return false;
-	    });
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		facet.find('div.facetCheckBox').each(function (i) {
-		    this.setAttribute('cubicweb:idx', i);
-		});
-		facet.find('div.facetCheckBox').click(function () {
-		    var $this = jQuery(this);
-		    // NOTE : add test on the facet operator (i.e. OR, AND)
-		    // if ($this.hasClass('facetValueDisabled')){
-		    //  	    return
-		    // }
-		    if ($this.hasClass('facetValueSelected')) {
-			$this.removeClass('facetValueSelected');
-			$this.find('img').each(function (i){
-			if (this.getAttribute('cubicweb:unselimg')){
-			       this.setAttribute('src', UNSELECTED_BORDER_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			    else{
-			       this.setAttribute('src', UNSELECTED_IMG);
-			       this.setAttribute('alt', (_('not selected')));
-			    }
-			});
-			var index = parseInt($this.attr('cubicweb:idx'));
-			// we dont need to move the element when cubicweb:idx == 0
-			if (index > 0){
-			    var shift = jQuery.grep(facet.find('.facetValueSelected'), function (n) {
-				    var nindex = parseInt(n.getAttribute('cubicweb:idx'));
-				    return nindex > index;
-				}).length;
-			    index += shift;
-			    var parent = this.parentNode;
-			    var $insertAfter = jQuery(parent).find('.facetCheckBox:nth('+index+')');
-			    if ( ! ($insertAfter.length == 1 && shift == 0) ) {
-				// only rearrange element if necessary
-				$insertAfter.after(this);
-			    }
-			}
-		    } else {
-			var lastSelected = facet.find('.facetValueSelected:last');
-			if (lastSelected.length) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			jQuery(this).addClass('facetValueSelected');
-			var $img = jQuery(this).find('img');
-			$img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
-		    }
-		    buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    facet.find('.facetBody').animate({scrollTop: 0}, '');
-		});
-		facet.find('select.facetOperator').change(function() {
-		    var nbselected = facet.find('div.facetValueSelected').length;
-		    if (nbselected >= 2) {
-			buildRQL.apply(null, evalJSON(form.attr('cubicweb:facetargs')));
-		    }
-		});
-		facet.find('div.facetTitle').click(function() {
-		  facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
-		  jQuery(this).toggleClass('opened');
-		   });
+    jQuery(root).find('form').each(function() {
+        var form = jQuery(this);
+        // NOTE: don't evaluate facetargs here but in callbacks since its value
+        //       may changes and we must send its value when the callback is
+        //       called, not when the page is initialized
+        var facetargs = form.attr('cubicweb:facetargs');
+        if (facetargs !== undefined) {
+            form.submit(function() {
+                buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                return false;
+            });
+            form.find('div.facet').each(function() {
+                var facet = jQuery(this);
+                facet.find('div.facetCheckBox').each(function(i) {
+                    this.setAttribute('cubicweb:idx', i);
+                });
+                facet.find('div.facetCheckBox').click(function() {
+                    var $this = jQuery(this);
+                    // NOTE : add test on the facet operator (i.e. OR, AND)
+                    // if ($this.hasClass('facetValueDisabled')){
+                    //          return
+                    // }
+                    if ($this.hasClass('facetValueSelected')) {
+                        $this.removeClass('facetValueSelected');
+                        $this.find('img').each(function(i) {
+                            if (this.getAttribute('cubicweb:unselimg')) {
+                                this.setAttribute('src', UNSELECTED_BORDER_IMG);
+                                this.setAttribute('alt', (_('not selected')));
+                            }
+                            else {
+                                this.setAttribute('src', UNSELECTED_IMG);
+                                this.setAttribute('alt', (_('not selected')));
+                            }
+                        });
+                        var index = parseInt($this.attr('cubicweb:idx'));
+                        // we dont need to move the element when cubicweb:idx == 0
+                        if (index > 0) {
+                            var shift = jQuery.grep(facet.find('.facetValueSelected'), function(n) {
+                                var nindex = parseInt(n.getAttribute('cubicweb:idx'));
+                                return nindex > index;
+                            }).length;
+                            index += shift;
+                            var parent = this.parentNode;
+                            var $insertAfter = jQuery(parent).find('.facetCheckBox:nth(' + index + ')');
+                            if (! ($insertAfter.length == 1 && shift == 0)) {
+                                // only rearrange element if necessary
+                                $insertAfter.after(this);
+                            }
+                        }
+                    } else {
+                        var lastSelected = facet.find('.facetValueSelected:last');
+                        if (lastSelected.length) {
+                            lastSelected.after(this);
+                        } else {
+                            var parent = this.parentNode;
+                            jQuery(parent).prepend(this);
+                        }
+                        jQuery(this).addClass('facetValueSelected');
+                        var $img = jQuery(this).find('img');
+                        $img.attr('src', SELECTED_IMG).attr('alt', (_('selected')));
+                    }
+                    buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                    facet.find('.facetBody').animate({
+                        scrollTop: 0
+                    },
+                    '');
+                });
+                facet.find('select.facetOperator').change(function() {
+                    var nbselected = facet.find('div.facetValueSelected').length;
+                    if (nbselected >= 2) {
+                        buildRQL.apply(null, cw.evalJSON(form.attr('cubicweb:facetargs')));
+                    }
+                });
+                facet.find('div.facetTitle').click(function() {
+                    facet.find('div.facetBody').toggleClass('hidden').toggleClass('opened');
+                    jQuery(this).toggleClass('opened');
+                });
 
-	    });
-	}
+            });
+        }
     });
 }
 
 // trigger this function on document ready event if you provide some kind of
 // persistent search (eg crih)
-function reorderFacetsItems(root){
+function reorderFacetsItems(root) {
     root = root || document;
-    jQuery(root).find('form').each(function () {
-	var form = jQuery(this);
-	if (form.attr('cubicweb:facetargs')) {
-	    form.find('div.facet').each(function() {
-		var facet = jQuery(this);
-		var lastSelected = null;
-		facet.find('div.facetCheckBox').each(function (i) {
-		    var $this = jQuery(this);
-		    if ($this.hasClass('facetValueSelected')) {
-			if (lastSelected) {
-			    lastSelected.after(this);
-			} else {
-			    var parent = this.parentNode;
-			    jQuery(parent).prepend(this);
-			}
-			lastSelected = $this;
-		    }
-		});
-	    });
-	}
+    jQuery(root).find('form').each(function() {
+        var form = jQuery(this);
+        if (form.attr('cubicweb:facetargs')) {
+            form.find('div.facet').each(function() {
+                var facet = jQuery(this);
+                var lastSelected = null;
+                facet.find('div.facetCheckBox').each(function(i) {
+                    var $this = jQuery(this);
+                    if ($this.hasClass('facetValueSelected')) {
+                        if (lastSelected) {
+                            lastSelected.after(this);
+                        } else {
+                            var parent = this.parentNode;
+                            jQuery(parent).prepend(this);
+                        }
+                        lastSelected = $this;
+                    }
+                });
+            });
+        }
     });
 }
 
@@ -225,6 +243,9 @@
 // with one argument or without any argument. If we use `initFacetBoxEvents`
 // as the direct callback on the jQuery.ready event, jQuery will pass some argument
 // of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {initFacetBoxEvents();});
+jQuery(document).ready(function() {
+    initFacetBoxEvents();
+});
 
 CubicWeb.provide('facets.js');
+
--- a/web/data/cubicweb.flot.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.flot.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,14 +1,14 @@
 function showTooltip(x, y, contents) {
-    $('<div id="tooltip">' + contents + '</div>').css( {
-            position: 'absolute',
+    $('<div id="tooltip">' + contents + '</div>').css({
+        position: 'absolute',
         display: 'none',
         top: y + 5,
-            left: x + 5,
-            border: '1px solid #fdd',
-            padding: '2px',
-            'background-color': '#fee',
-            opacity: 0.80
-        }).appendTo("body").fadeIn(200);
+        left: x + 5,
+        border: '1px solid #fdd',
+        padding: '2px',
+        'background-color': '#fee',
+        opacity: 0.80
+    }).appendTo("body").fadeIn(200);
 }
 
 var previousPoint = null;
@@ -18,19 +18,19 @@
             previousPoint = item.datapoint;
             $("#tooltip").remove();
             var x = item.datapoint[0].toFixed(2),
-                y = item.datapoint[1].toFixed(2);
+            y = item.datapoint[1].toFixed(2);
             if (item.datapoint.length == 3) {
                 x = new Date(item.datapoint[2]);
                 x = x.toLocaleDateString() + ' ' + x.toLocaleTimeString();
             } else if (item.datapoint.length == 4) {
-               x = new Date(item.datapoint[2]);
-               x = x.strftime(item.datapoint[3]);
+                x = new Date(item.datapoint[2]);
+                x = x.strftime(item.datapoint[3]);
             }
-            showTooltip(item.pageX, item.pageY,
-            item.series.label + ': (' + x + ' ; ' + y + ')');
+            showTooltip(item.pageX, item.pageY, item.series.label + ': (' + x + ' ; ' + y + ')');
         }
     } else {
         $("#tooltip").remove();
         previousPoint = null;
     }
 }
+
--- a/web/data/cubicweb.gmap.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.gmap.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,72 +1,72 @@
-/*
+/**
  *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
- *
- *
  */
 
 Widgets.GMapWidget = defclass('GMapWidget', null, {
-  __init__: function(wdgnode) {
-    // Assume we have imported google maps JS
-    if (GBrowserIsCompatible()) {
-      var uselabelstr = wdgnode.getAttribute('cubicweb:uselabel');
-      var uselabel = true;
-      if (uselabelstr){
-	if (uselabelstr == 'True'){
-	  uselabel = true;
-	}
-	else{
-	  uselabel = false;
-	}
-      }
-      var map = new GMap2(wdgnode);
-      map.addControl(new GSmallMapControl());
-      var jsonurl = wdgnode.getAttribute('cubicweb:loadurl');
-      var self = this; // bind this to a local variable
-      jQuery.getJSON(jsonurl, function(geodata) {
-	if (geodata.center) {
-	  var zoomLevel = geodata.zoomlevel;
-	  map.setCenter(new GLatLng(geodata.center.latitude, geodata.center.longitude),
-		        zoomLevel);
-	}
-	for (var i=0; i<geodata.markers.length; i++) {
-	  var marker = geodata.markers[i];
-	  self.createMarker(map, marker, i+1, uselabel);
-	}
-      });
-      jQuery(wdgnode).after(this.legendBox);
-    } else { // incompatible browser
-      jQuery.unload(GUnload);
-    }
-  },
+    __init__: function(wdgnode) {
+        // Assume we have imported google maps JS
+        if (GBrowserIsCompatible()) {
+            var uselabelstr = wdgnode.getAttribute('cubicweb:uselabel');
+            var uselabel = true;
+            if (uselabelstr) {
+                if (uselabelstr == 'True') {
+                    uselabel = true;
+                }
+                else {
+                    uselabel = false;
+                }
+            }
+            var map = new GMap2(wdgnode);
+            map.addControl(new GSmallMapControl());
+            var jsonurl = wdgnode.getAttribute('cubicweb:loadurl');
+            var self = this; // bind this to a local variable
+            jQuery.getJSON(jsonurl, function(geodata) {
+                if (geodata.center) {
+                    var zoomLevel = geodata.zoomlevel;
+                    map.setCenter(new GLatLng(geodata.center.latitude, geodata.center.longitude), zoomLevel);
+                }
+                for (var i = 0; i < geodata.markers.length; i++) {
+                    var marker = geodata.markers[i];
+                    self.createMarker(map, marker, i + 1, uselabel);
+                }
+            });
+            jQuery(wdgnode).after(this.legendBox);
+        } else { // incompatible browser
+            jQuery.unload(GUnload);
+        }
+    },
 
-  createMarker: function(map, marker, i, uselabel) {
-    var point = new GLatLng(marker.latitude, marker.longitude);
-    var icon = new GIcon();
-    icon.image = marker.icon[0];
-    icon.iconSize = new GSize(marker.icon[1][0], marker.icon[1][1]) ;
-    icon.iconAnchor = new GPoint(marker.icon[2][0], marker.icon[2][1]);
-    if(marker.icon[3]){
-      icon.shadow4 =  marker.icon[3];
-    }
-    if (typeof LabeledMarker == "undefined") {
-	var gmarker = new GMarker(point, {icon: icon,
-	title: marker.title});
-    } else {
-        var gmarker = new LabeledMarker(point, {
-          icon: icon,
-          title: marker.title,
-          labelText: uselabel?'<strong>' + i + '</strong>':'',
-          labelOffset: new GSize(2, -32)
+    createMarker: function(map, marker, i, uselabel) {
+        var point = new GLatLng(marker.latitude, marker.longitude);
+        var icon = new GIcon();
+        icon.image = marker.icon[0];
+        icon.iconSize = new GSize(marker.icon[1][0], marker.icon[1][1]);
+        icon.iconAnchor = new GPoint(marker.icon[2][0], marker.icon[2][1]);
+        if (marker.icon[3]) {
+            icon.shadow4 = marker.icon[3];
+        }
+        if (typeof LabeledMarker == "undefined") {
+            var gmarker = new GMarker(point, {
+                icon: icon,
+                title: marker.title
+            });
+        } else {
+            var gmarker = new LabeledMarker(point, {
+                icon: icon,
+                title: marker.title,
+                labelText: uselabel ? '<strong>' + i + '</strong>': '',
+                labelOffset: new GSize(2, - 32)
+            });
+        }
+        map.addOverlay(gmarker);
+        GEvent.addListener(gmarker, 'click', function() {
+            jQuery.post(marker.bubbleUrl, function(data) {
+                map.openInfoWindowHtml(point, data);
+            });
         });
     }
-    map.addOverlay(gmarker);
-    GEvent.addListener(gmarker, 'click', function() {
-      jQuery.post(marker.bubbleUrl, function(data) {
-	map.openInfoWindowHtml(point, data);
-      });
-    });
-  }
 
 });
+
--- a/web/data/cubicweb.goa.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.goa.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,10 +1,17 @@
-/*
+/**
  *  functions specific to cubicweb on google appengine
  *
  *  :organization: Logilab
- *  :copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  */
 
-/* overrides rql_for_eid function from htmlhelpers.hs */
-function rql_for_eid(eid) { return 'Any X WHERE X eid "' + eid + '"'; }
+/**
+ * .. function:: rql_for_eid(eid)
+ *
+ * overrides rql_for_eid function from htmlhelpers.hs
+ */
+function rql_for_eid(eid) {
+	return 'Any X WHERE X eid "' + eid + '"';
+}
+
--- a/web/data/cubicweb.htmlhelpers.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.htmlhelpers.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,31 +1,37 @@
 CubicWeb.require('python.js');
 CubicWeb.require('jquery.corner.js');
 
-/* returns the document's baseURI. (baseuri() uses document.baseURI if
+/**
+ * .. function:: baseuri()
+ *
+ * returns the document's baseURI. (baseuri() uses document.baseURI if
  * available and inspects the <base> tag manually otherwise.)
-*/
+ */
 function baseuri() {
     var uri = document.baseURI;
     if (uri) { // some browsers don't define baseURI
-	return uri;
+        return uri;
     }
-    var basetags = document.getElementsByTagName('base');
-    if (basetags.length) {
-	return getNodeAttribute(basetags[0], 'href');
-    }
-    return '';
+    return jQuery('base').attr('href');
 }
 
-
-/* set body's cursor to 'progress' */
+/**
+ * .. function:: setProgressCursor()
+ *
+ * set body's cursor to 'progress'
+ */
 function setProgressCursor() {
     var body = document.getElementsByTagName('body')[0];
     body.style.cursor = 'progress';
 }
 
-/* reset body's cursor to default (mouse cursor). The main
+/**
+ * .. function:: resetCursor(result)
+ *
+ * reset body's cursor to default (mouse cursor). The main
  * purpose of this function is to be used as a callback in the
- * deferreds' callbacks chain. */
+ * deferreds' callbacks chain.
+ */
 function resetCursor(result) {
     var body = document.getElementsByTagName('body')[0];
     body.style.cursor = 'default';
@@ -34,14 +40,19 @@
 }
 
 function updateMessage(msg) {
-    var msgdiv = DIV({'class':'message'});
+    var msgdiv = DIV({
+        'class': 'message'
+    });
     // don't pass msg to DIV() directly because DIV will html escape it
     // and msg should alreay be html escaped at this point.
     msgdiv.innerHTML = msg;
     jQuery('#appMsg').removeClass('hidden').empty().append(msgdiv);
 }
 
-/* builds an url from an object (used as a dictionnary)
+/**
+ * .. function:: asURL(props)
+ *
+ * builds an url from an object (used as a dictionnary)
  *
  * >>> asURL({'rql' : "RQL", 'x': [1, 2], 'itemvid' : "oneline"})
  * rql=RQL&vid=list&itemvid=oneline&x=1&x=2
@@ -50,122 +61,145 @@
  */
 function asURL(props) {
     var chunks = [];
-    for(key in props) {
-	var value = props[key];
-	// generate a list of couple key=value if key is multivalued
-	if (isArrayLike(value)) {
-	    for (var i=0; i<value.length;i++) {
-		chunks.push(key + '=' + urlEncode(value[i]));
-	    }
-	} else {
-	    chunks.push(key + '=' + urlEncode(value));
-	}
+    for (key in props) {
+        var value = props[key];
+        // generate a list of couple key=value if key is multivalued
+        if (cw.utils.isArrayLike(value)) {
+            for (var i = 0; i < value.length; i++) {
+                chunks.push(key + '=' + urlEncode(value[i]));
+            }
+        } else {
+            chunks.push(key + '=' + urlEncode(value));
+        }
     }
     return chunks.join('&');
 }
 
-/* return selected value of a combo box if any
+/**
+ * .. function:: firstSelected(selectNode)
+ *
+ * return selected value of a combo box if any
  */
 function firstSelected(selectNode) {
-    var selection = filter(attrgetter('selected'), selectNode.options);
-    return (selection.length > 0) ? getNodeAttribute(selection[0], 'value'):null;
+    var $selection = $(selectNode).find('option:selected:first');
+    return ($selection.length > 0) ? $selection[0] : null;
 }
 
-/* toggle visibility of an element by its id
+/**
+ * .. function:: toggleVisibility(elemId)
+ *
+ * toggle visibility of an element by its id
  */
 function toggleVisibility(elemId) {
-    jqNode(elemId).toggleClass('hidden');
+    $('#' + elemId).toggleClass('hidden');
 }
 
-
-/* toggles visibility of login popup div */
+/**
+ * .. function:: popupLoginBox()
+ *
+ * toggles visibility of login popup div
+ */
 // XXX used exactly ONCE in basecomponents
 function popupLoginBox() {
-    toggleVisibility('popupLoginBox');
+    $('#popupLoginBox').toggleClass('hidden');
     jQuery('#__login:visible').focus();
 }
 
-
-/* returns the list of elements in the document matching the tag name
+/**
+ * .. function getElementsMatching(tagName, properties, \/* optional \*\/ parent)
+ *
+ * returns the list of elements in the document matching the tag name
  * and the properties provided
  *
- * @param tagName the tag's name
- * @param properties a js Object used as a dict
- * @return an iterator (if a *real* array is needed, you can use the
+ * * `tagName`, the tag's name
+ *
+ * * `properties`, a js Object used as a dict
+ *
+ * Return an iterator (if a *real* array is needed, you can use the
  *                      list() function)
  */
 function getElementsMatching(tagName, properties, /* optional */ parent) {
     parent = parent || document;
-    return filter(function elementMatches(element) {
-                     for (prop in properties) {
-                       if (getNodeAttribute(element, prop) != properties[prop]) {
-	                 return false;}}
-                    return true;},
-                  parent.getElementsByTagName(tagName));
+    return jQuery.grep(parent.getElementsByTagName(tagName), function elementMatches(element) {
+        for (prop in properties) {
+            if (jQuery(element).attr(prop) != properties[prop]) {
+                return false;
+            }
+        }
+        return true;
+    });
 }
 
-/*
+/**
+ * .. function:: setCheckboxesState(nameprefix, value, checked)
+ *
  * sets checked/unchecked status of checkboxes
  */
-function setCheckboxesState(nameprefix, checked){
+
+function setCheckboxesState(nameprefix, value, checked) {
     // XXX: this looks in *all* the document for inputs
-    var elements = getElementsMatching('input', {'type': "checkbox"});
-    filterfunc = function(cb) { return nameprefix && cb.name.startsWith(nameprefix); };
-    forEach(filter(filterfunc, elements), function(cb) {cb.checked=checked;});
+    jQuery('input:checkbox[name^=' + nameprefix + ']').each(function() {
+        if (value == null || this.value == value) {
+            this.checked = checked;
+        }
+    });
 }
 
-function setCheckboxesState2(nameprefix, value, checked){
-    // XXX: this looks in *all* the document for inputs
-    var elements = getElementsMatching('input', {'type': "checkbox"});
-    filterfunc = function(cb) { return nameprefix && cb.name.startsWith(nameprefix) && cb.value == value; };
-    forEach(filter(filterfunc, elements), function(cb) {cb.checked=checked;});
-}
-
-
-/* this function is a hack to build a dom node from html source */
+/**
+ * .. function:: html2dom(source)
+ *
+ * this function is a hack to build a dom node from html source
+ */
 function html2dom(source) {
     var tmpNode = SPAN();
     tmpNode.innerHTML = source;
     if (tmpNode.childNodes.length == 1) {
-	return tmpNode.firstChild;
+        return tmpNode.firstChild;
     }
     else {
-	// we leave the span node when `source` has no root node
-	// XXX This is cleary not the best solution, but css/html-wise,
-	///    a span not should not be too  much disturbing
-	return tmpNode;
+        // we leave the span node when `source` has no root node
+        // XXX This is cleary not the best solution, but css/html-wise,
+        ///    a span not should not be too  much disturbing
+        return tmpNode;
     }
 }
 
-
 // *** HELPERS **************************************************** //
-function rql_for_eid(eid) { return 'Any X WHERE X eid ' + eid; }
-function isTextNode(domNode) { return domNode.nodeType == 3; }
-function isElementNode(domNode) { return domNode.nodeType == 1; }
+function rql_for_eid(eid) {
+    return 'Any X WHERE X eid ' + eid;
+}
+function isTextNode(domNode) {
+    return domNode.nodeType == 3;
+}
+function isElementNode(domNode) {
+    return domNode.nodeType == 1;
+}
 
 function autogrow(area) {
-    if (area.scrollHeight > area.clientHeight && !window.opera) {
-	if (area.rows < 20) {
-	    area.rows += 2;
-	}
+    if (area.scrollHeight > area.clientHeight && ! window.opera) {
+        if (area.rows < 20) {
+            area.rows += 2;
+        }
     }
 }
 //============= page loading events ==========================================//
-
-CubicWeb.rounded = [
-		    ['div.sideBoxBody', 'bottom 6px'],
-		    ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']
-		    ];
+CubicWeb.rounded = [['div.sideBoxBody', 'bottom 6px'],
+                    ['div.boxTitle, div.sideBoxTitle, th.month', 'top 6px']];
 
 function roundedCorners(node) {
-    node = jQuery(node);
-    for(var r=0; r < CubicWeb.rounded.length; r++) {
-       node.find(CubicWeb.rounded[r][0]).corner(CubicWeb.rounded[r][1]);
+    if (jQuery.fn.corner !== undefined) {
+        node = jQuery(node);
+        for (var r = 0; r < CubicWeb.rounded.length; r++) {
+            node.find(CubicWeb.rounded[r][0]).corner(CubicWeb.rounded[r][1]);
+        }
     }
 }
 
-jQuery(document).ready(function () {roundedCorners(this.body);});
+jQuery(document).ready(function() {
+    roundedCorners(this.body);
+});
 
 CubicWeb.provide('corners.js');
 
 CubicWeb.provide('htmlhelpers.js');
+
--- a/web/data/cubicweb.iprogress.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.iprogress.js	Thu Jun 03 14:51:42 2010 +0200
@@ -6,7 +6,7 @@
     this.color_budget = "blue";
     this.color_todo = "#cccccc"; //  grey
     this.height = 16;
-    this.middle = this.height/2;
+    this.middle = this.height / 2;
     this.radius = 4;
 }
 
@@ -15,14 +15,14 @@
     ctx.lineWidth = 1;
     ctx.strokeStyle = color;
     if (fill) {
-	ctx.fillStyle = color;
-	ctx.fillRect(0,0,pos,this.middle*2);
+        ctx.fillStyle = color;
+        ctx.fillRect(0, 0, pos, this.middle * 2);
     } else {
-	ctx.lineWidth = 2;
-	ctx.strokeStyle = "black";
-	ctx.moveTo(pos,0);
-	ctx.lineTo(pos,this.middle*2);
-	ctx.stroke();
+        ctx.lineWidth = 2;
+        ctx.strokeStyle = "black";
+        ctx.moveTo(pos, 0);
+        ctx.lineTo(pos, this.middle * 2);
+        ctx.stroke();
     }
 };
 
@@ -30,36 +30,34 @@
     ctx.beginPath();
     ctx.lineWidth = 2;
     ctx.strokeStyle = color;
-    ctx.moveTo(0,this.middle);
-    ctx.lineTo(pos,this.middle);
-    ctx.arc(pos,this.middle,this.radius,0,Math.PI*2,true);
+    ctx.moveTo(0, this.middle);
+    ctx.lineTo(pos, this.middle);
+    ctx.arc(pos, this.middle, this.radius, 0, Math.PI * 2, true);
     ctx.stroke();
 };
 
-
 ProgressBar.prototype.draw_circ = function(ctx) {
-    this.draw_one_circ(ctx,this.budget,this.color_budget);
-    this.draw_one_circ(ctx,this.todo,this.color_todo);
-    this.draw_one_circ(ctx,this.done,this.color_done);
+    this.draw_one_circ(ctx, this.budget, this.color_budget);
+    this.draw_one_circ(ctx, this.todo, this.color_todo);
+    this.draw_one_circ(ctx, this.done, this.color_done);
 };
 
-
 ProgressBar.prototype.draw_rect = function(ctx) {
-    this.draw_one_rect(ctx,this.todo,this.color_todo,true);
-    this.draw_one_rect(ctx,this.done,this.color_done,true);
-    this.draw_one_rect(ctx,this.budget,this.color_budget,false);
+    this.draw_one_rect(ctx, this.todo, this.color_todo, true);
+    this.draw_one_rect(ctx, this.done, this.color_done, true);
+    this.draw_one_rect(ctx, this.budget, this.color_budget, false);
 };
 
-
 function draw_progressbar(cid, done, todo, budget, color) {
     var canvas = document.getElementById(cid);
     if (canvas.getContext) {
         var ctx = canvas.getContext("2d");
-	var bar = new ProgressBar();
-	bar.budget = budget;
-	bar.todo = todo;
-	bar.done = done;
+        var bar = new ProgressBar();
+        bar.budget = budget;
+        bar.todo = todo;
+        bar.done = done;
         bar.color_done = color;
-	bar.draw_rect(ctx);
+        bar.draw_rect(ctx);
     }
 }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/data/cubicweb.js	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,370 @@
+cw = {};
+
+jQuery.extend(cw, {
+    log: function () {
+        var args = [];
+        for (var i = 0; i < arguments.length; i++) {
+            args.push(arguments[i]);
+        }
+        if (typeof(window) != "undefined" && window.console && window.console.log) {
+            window.console.log(args.join(' '));
+        }
+    },
+
+    //removed: getElementsByTagAndClassName, replaceChildNodes, toggleElementClass
+    //         partial, merge, isNotEmpty, update,
+    //         String.in_, String.join, list, getattr, attrgetter, methodcaller,
+    //         min, max, dict, concat
+    jqNode: function (node) {
+    /**
+     * .. function:: jqNode(node)
+     *
+     * safe version of jQuery('#nodeid') because we use ':' in nodeids
+     * which messes with jQuery selection mechanism
+     */
+        if (typeof(node) == 'string') {
+            node = document.getElementById(node);
+        }
+        if (node) {
+            return $(node);
+        }
+        return null;
+    },
+
+    getNode: function (node) {
+        if (typeof(node) == 'string') {
+            return document.getElementById(node);
+        }
+        return node;
+    },
+
+    evalJSON: function (json) { // trust source
+        return eval("(" + json + ")");
+    },
+
+    urlEncode: function (str) {
+        if (typeof(encodeURIComponent) != "undefined") {
+            return encodeURIComponent(str).replace(/\'/g, '%27');
+        } else {
+            return escape(str).replace(/\+/g, '%2B').replace(/\"/g, '%22').
+                    rval.replace(/\'/g, '%27');
+        }
+    },
+
+    swapDOM: function (dest, src) {
+        dest = getNode(dest);
+        var parent = dest.parentNode;
+        if (src) {
+            src = getNode(src);
+            parent.replaceChild(src, dest);
+        } else {
+            parent.removeChild(dest);
+        }
+        return src;
+    }
+});
+
+
+cw.utils = {
+
+    deprecatedFunction: function (msg, newfunc) {
+        return function () {
+            cw.log(msg);
+            return newfunc.apply(this, arguments);
+        };
+    },
+
+    movedToNamespace: function (funcnames, namespace) {
+        for (var i = 0; i < funcnames.length; i++) {
+            var funcname = funcnames[i];
+            var msg = '[3.9] ' + funcname + ' is deprecated, use cw.' + funcname + ' instead';
+            window[funcname] = cw.utils.deprecatedFunction(msg, namespace[funcname]);
+        }
+    },
+
+    createDomFunction: function (tag) {
+        function builddom(params, children) {
+            var node = document.createElement(tag);
+            for (key in params) {
+                var value = params[key];
+                if (key.substring(0, 2) == 'on') {
+                    // this is an event handler definition
+                    if (typeof value == 'string') {
+                        // litteral definition
+                        value = new Function(value);
+                    }
+                    node[key] = value;
+                } else { // normal node attribute
+                    jQuery(node).attr(key, params[key]);
+                }
+            }
+            if (children) {
+                if (!cw.utils.isArrayLike(children)) {
+                    children = [children];
+                    for (var i = 2; i < arguments.length; i++) {
+                        var arg = arguments[i];
+                        if (cw.utils.isArray(arg)) {
+                            jQuery.merge(children, arg);
+                        } else {
+                            children.push(arg);
+                        }
+                    }
+                }
+                for (var i = 0; i < children.length; i++) {
+                    var child = children[i];
+                    if (typeof child == "string" || typeof child == "number") {
+                        child = document.createTextNode(child);
+                    }
+                    node.appendChild(child);
+                }
+            }
+            return node;
+        }
+        return builddom;
+    },
+
+    /**
+     * .. function:: toISOTimestamp(date)
+     *
+     */
+    toISOTimestamp: function (date) {
+        if (typeof(date) == "undefined" || date === null) {
+            return null;
+        }
+
+        function _padTwo(n) {
+            return (n > 9) ? n : "0" + n;
+        }
+        var isoTime = [_padTwo(date.getHours()), _padTwo(date.getMinutes()),
+                       _padTwo(date.getSeconds())].join(':');
+        var isoDate = [date.getFullYear(), _padTwo(date.getMonth() + 1),
+                       _padTwo(date.getDate())].join("-");
+        return isoDate + " " + isoTime;
+    },
+
+    /**
+     * .. function:: nodeWalkDepthFirst(node, visitor)
+     *
+     * depth-first implementation of the nodeWalk function found
+     * in `MochiKit.Base <http://mochikit.com/doc/html/MochiKit/Base.html#fn-nodewalk>`_
+     */
+    nodeWalkDepthFirst: function (node, visitor) {
+        var children = visitor(node);
+        if (children) {
+            for (var i = 0; i < children.length; i++) {
+                nodeWalkDepthFirst(children[i], visitor);
+            }
+        }
+    },
+
+    isArray: function (it) { // taken from dojo
+        return it && (it instanceof Array || typeof it == "array");
+    },
+
+    isString: function (it) { // taken from dojo
+        return !!arguments.length && it != null && (typeof it == "string" || it instanceof String);
+    },
+
+    isArrayLike: function (it) { // taken from dojo
+        return (it && it !== undefined &&
+                // keep out built-in constructors (Number, String, ...)
+                // which have length properties
+                !cw.utils.isString(it) && !jQuery.isFunction(it) &&
+                !(it.tagName && it.tagName.toLowerCase() == 'form') &&
+                (cw.utils.isArray(it) || isFinite(it.length)));
+    },
+
+    /**
+     * .. function:: formContents(elem \/* = document.body *\/)
+     *
+     * this implementation comes from MochiKit
+     */
+    formContents: function (elem /* = document.body */ ) {
+        var names = [];
+        var values = [];
+        if (typeof(elem) == "undefined" || elem === null) {
+            elem = document.body;
+        } else {
+            elem = getNode(elem);
+        }
+        cw.utils.nodeWalkDepthFirst(elem, function (elem) {
+            var name = elem.name;
+            if (name && name.length) {
+                var tagName = elem.tagName.toUpperCase();
+                if (tagName === "INPUT" && (elem.type == "radio" || elem.type == "checkbox") && !elem.checked) {
+                    return null;
+                }
+                if (tagName === "SELECT") {
+                    if (elem.type == "select-one") {
+                        if (elem.selectedIndex >= 0) {
+                            var opt = elem.options[elem.selectedIndex];
+                            var v = opt.value;
+                            if (!v) {
+                                var h = opt.outerHTML;
+                                // internet explorer sure does suck.
+                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
+                                    v = opt.text;
+                                }
+                            }
+                            names.push(name);
+                            values.push(v);
+                            return null;
+                        }
+                        // no form elements?
+                        names.push(name);
+                        values.push("");
+                        return null;
+                    } else {
+                        var opts = elem.options;
+                        if (!opts.length) {
+                            names.push(name);
+                            values.push("");
+                            return null;
+                        }
+                        for (var i = 0; i < opts.length; i++) {
+                            var opt = opts[i];
+                            if (!opt.selected) {
+                                continue;
+                            }
+                            var v = opt.value;
+                            if (!v) {
+                                var h = opt.outerHTML;
+                                // internet explorer sure does suck.
+                                if (h && !h.match(/^[^>]+\svalue\s*=/i)) {
+                                    v = opt.text;
+                                }
+                            }
+                            names.push(name);
+                            values.push(v);
+                        }
+                        return null;
+                    }
+                }
+                if (tagName === "FORM" || tagName === "P" || tagName === "SPAN" || tagName === "DIV") {
+                    return elem.childNodes;
+                }
+                names.push(name);
+                values.push(elem.value || '');
+                return null;
+            }
+            return elem.childNodes;
+        });
+        return [names, values];
+    },
+
+    /**
+     * .. function:: sliceList(lst, start, stop, step)
+     *
+     * returns a subslice of `lst` using `start`/`stop`/`step`
+     * start, stop might be negative
+     *
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2)
+     * ['c', 'd', 'e', 'f']
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2, -2)
+     * ['c', 'd']
+     * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], -3)
+     * ['d', 'e', 'f']
+     */
+    sliceList: function (lst, start, stop, step) {
+        start = start || 0;
+        stop = stop || lst.length;
+        step = step || 1;
+        if (stop < 0) {
+            stop = Math.max(lst.length + stop, 0);
+        }
+        if (start < 0) {
+            start = Math.min(lst.length + start, lst.length);
+        }
+        var result = [];
+        for (var i = start; i < stop; i += step) {
+            result.push(lst[i]);
+        }
+        return result;
+    }
+
+
+};
+
+String.prototype.startsWith = cw.utils.deprecatedFunction('[3.9] str.startsWith() is deprecated, use str.startswith() instead', function (prefix) {
+    return this.startswith(prefix);
+});
+
+String.prototype.endsWith = cw.utils.deprecatedFunction('[3.9] str.endsWith() is deprecated, use str.endswith() instead', function (suffix) {
+    return this.endswith(prefix);
+});
+
+/** DOM factories ************************************************************/
+A = cw.utils.createDomFunction('a');
+BUTTON = cw.utils.createDomFunction('button');
+BR = cw.utils.createDomFunction('br');
+CANVAS = cw.utils.createDomFunction('canvas');
+DD = cw.utils.createDomFunction('dd');
+DIV = cw.utils.createDomFunction('div');
+DL = cw.utils.createDomFunction('dl');
+DT = cw.utils.createDomFunction('dt');
+FIELDSET = cw.utils.createDomFunction('fieldset');
+FORM = cw.utils.createDomFunction('form');
+H1 = cw.utils.createDomFunction('H1');
+H2 = cw.utils.createDomFunction('H2');
+H3 = cw.utils.createDomFunction('H3');
+H4 = cw.utils.createDomFunction('H4');
+H5 = cw.utils.createDomFunction('H5');
+H6 = cw.utils.createDomFunction('H6');
+HR = cw.utils.createDomFunction('hr');
+IMG = cw.utils.createDomFunction('img');
+INPUT = cw.utils.createDomFunction('input');
+LABEL = cw.utils.createDomFunction('label');
+LEGEND = cw.utils.createDomFunction('legend');
+LI = cw.utils.createDomFunction('li');
+OL = cw.utils.createDomFunction('ol');
+OPTGROUP = cw.utils.createDomFunction('optgroup');
+OPTION = cw.utils.createDomFunction('option');
+P = cw.utils.createDomFunction('p');
+PRE = cw.utils.createDomFunction('pre');
+SELECT = cw.utils.createDomFunction('select');
+SPAN = cw.utils.createDomFunction('span');
+STRONG = cw.utils.createDomFunction('strong');
+TABLE = cw.utils.createDomFunction('table');
+TBODY = cw.utils.createDomFunction('tbody');
+TD = cw.utils.createDomFunction('td');
+TEXTAREA = cw.utils.createDomFunction('textarea');
+TFOOT = cw.utils.createDomFunction('tfoot');
+TH = cw.utils.createDomFunction('th');
+THEAD = cw.utils.createDomFunction('thead');
+TR = cw.utils.createDomFunction('tr');
+TT = cw.utils.createDomFunction('tt');
+UL = cw.utils.createDomFunction('ul');
+
+// cubicweb specific
+//IFRAME = cw.utils.createDomFunction('iframe');
+
+
+function IFRAME(params) {
+    if ('name' in params) {
+        try {
+            var node = document.createElement('<iframe name="' + params['name'] + '">');
+        } catch (ex) {
+            var node = document.createElement('iframe');
+            node.id = node.name = params.name;
+        }
+    }
+    else {
+        var node = document.createElement('iframe');
+    }
+    for (key in params) {
+        if (key != 'name') {
+            var value = params[key];
+            if (key.substring(0, 2) == 'on') {
+                // this is an event handler definition
+                if (typeof value == 'string') {
+                    // litteral definition
+                    value = new Function(value);
+                }
+                node[key] = value;
+            } else { // normal node attribute
+                node.setAttribute(key, params[key]);
+            }
+        }
+    }
+    return node;
+}
--- a/web/data/cubicweb.lazy.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.lazy.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,10 +1,9 @@
-
 function load_now(eltsel, holesel, reloadable) {
     var lazydiv = jQuery(eltsel);
     var hole = lazydiv.children(holesel);
-    if ((hole.length == 0) && !reloadable) {
-	/* the hole is already filled */
-	return;
+    if ((hole.length == 0) && ! reloadable) {
+        /* the hole is already filled */
+        return;
     }
     lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'));
 }
@@ -12,3 +11,4 @@
 function trigger_load(divid) {
     jQuery('#lazy-' + divid).trigger('load_' + divid);
 }
+
--- a/web/data/cubicweb.massmailing.js	Thu Jun 03 10:17:44 2010 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-
-function insertText(text, areaId) {
-    var textarea = jQuery('#' + areaId);
-    if (document.selection) { // IE
-        var selLength;
-        textarea.focus();
-        sel = document.selection.createRange();
-        selLength = sel.text.length;
-        sel.text = text;
-        sel.moveStart('character', selLength-text.length);
-        sel.select();
-    } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla
-        var startPos = textarea.selectionStart;
-        var endPos = textarea.selectionEnd;
-	// insert text so that it replaces the [startPos, endPos] part
-        textarea.value = textarea.value.substring(0,startPos) + text + textarea.value.substring(endPos,textarea.value.length);
-	// set cursor pos at the end of the inserted text
-        textarea.selectionStart = textarea.selectionEnd = startPos+text.length;
-        textarea.focus();
-    } else { // safety belt for other browsers
-        textarea.value += text;
-    }
-}
--- a/web/data/cubicweb.preferences.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.preferences.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,4 +1,5 @@
-/* toggle visibility of an element by its id
+/**
+ * toggle visibility of an element by its id
  * & set current visibility status in a cookie
  * XXX whenever used outside of preferences, don't forget to
  *     move me in a more appropriate place
@@ -11,56 +12,62 @@
     jQuery('#' + elemId).toggleClass('hidden');
 }
 
-function closeFieldset(fieldsetid){
+function closeFieldset(fieldsetid) {
     var linklabel = _('open all');
-    var linkhref = 'javascript:openFieldset("' +fieldsetid + '")';
+    var linkhref = 'javascript:openFieldset("' + fieldsetid + '")';
     _toggleFieldset(fieldsetid, 1, linklabel, linkhref);
 }
 
-function openFieldset(fieldsetid){
+function openFieldset(fieldsetid) {
     var linklabel = _('close all');
-    var linkhref = 'javascript:closeFieldset("'+ fieldsetid + '")';
+    var linkhref = 'javascript:closeFieldset("' + fieldsetid + '")';
     _toggleFieldset(fieldsetid, 0, linklabel, linkhref);
 }
 
-function _toggleFieldset(fieldsetid, closeaction, linklabel, linkhref){
-    jQuery('#'+fieldsetid).find('div.openlink').each(function(){
-	    var link = A({'href' : "javascript:noop();",
-			  'onclick' : linkhref},
-			  linklabel);
-	    jQuery(this).empty().append(link);
-	});
-    jQuery('#'+fieldsetid).find('fieldset[id]').each(function(){
-	    var fieldset = jQuery(this);
-	    if(closeaction){
-		fieldset.addClass('hidden');
-	    }else{
-		fieldset.removeClass('hidden');
-		linkLabel = (_('open all'));
-	    }
-	});
+function _toggleFieldset(fieldsetid, closeaction, linklabel, linkhref) {
+    jQuery('#' + fieldsetid).find('div.openlink').each(function() {
+        var link = A({
+            'href': "javascript:noop();",
+            'onclick': linkhref
+        },
+        linklabel);
+        jQuery(this).empty().append(link);
+    });
+    jQuery('#' + fieldsetid).find('fieldset[id]').each(function() {
+        var fieldset = jQuery(this);
+        if (closeaction) {
+            fieldset.addClass('hidden');
+        } else {
+            fieldset.removeClass('hidden');
+            linkLabel = (_('open all'));
+        }
+    });
 }
 
-function validatePrefsForm(formid){
+function validatePrefsForm(formid) {
     clearPreviousMessages();
     clearPreviousErrors(formid);
-    return validateForm(formid, null,  submitSucces, submitFailure);
+    return validateForm(formid, null, submitSucces, submitFailure);
 }
 
-function submitFailure(formid){
-    var form = jQuery('#'+formid);
-    var dom = DIV({'class':'critical'},
-		  _("please correct errors below"));
+function submitFailure(formid) {
+    var form = jQuery('#' + formid);
+    var dom = DIV({
+        'class': 'critical'
+    },
+    _("please correct errors below"));
     jQuery(form).find('div.formsg').empty().append(dom);
     // clearPreviousMessages()
     jQuery(form).find('span.error').next().focus();
 }
 
-function submitSucces(url, formid){
-    var form = jQuery('#'+formid);
+function submitSucces(url, formid) {
+    var form = jQuery('#' + formid);
     setCurrentValues(form);
-    var dom = DIV({'class':'msg'},
-		  _("changes applied"));
+    var dom = DIV({
+        'class': 'msg'
+    },
+    _("changes applied"));
     jQuery(form).find('div.formsg').empty().append(dom);
     jQuery(form).find('input').removeClass('changed');
     checkValues(form, true);
@@ -76,78 +83,79 @@
     jQuery('#err-value:' + formid).remove();
 }
 
-function checkValues(form, success){
+function checkValues(form, success) {
     var unfreezeButtons = false;
-    jQuery(form).find('select').each(function () {
-	    unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
-	});
-    jQuery(form).find('[type=text]').each(function () {
-	    unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
-	});
-    jQuery(form).find('input[type=radio]:checked').each(function () {
-            unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
-     });
+    jQuery(form).find('select').each(function() {
+        unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
+    });
+    jQuery(form).find('[type=text]').each(function() {
+        unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
+    });
+    jQuery(form).find('input[type=radio]:checked').each(function() {
+        unfreezeButtons = _checkValue(jQuery(this), unfreezeButtons);
+    });
 
-    if (unfreezeButtons){
-	unfreezeFormButtons(form.attr('id'));
-    }else{
-	if (!success){
-	    clearPreviousMessages();
-	}
-	clearPreviousErrors(form.attr('id'));
-	freezeFormButtons(form.attr('id'));
+    if (unfreezeButtons) {
+        unfreezeFormButtons(form.attr('id'));
+    } else {
+        if (!success) {
+            clearPreviousMessages();
+        }
+        clearPreviousErrors(form.attr('id'));
+        freezeFormButtons(form.attr('id'));
     }
 }
 
-function _checkValue(input, unfreezeButtons){
+function _checkValue(input, unfreezeButtons) {
     var currentValue = prefsValues[input.attr('name')];
-     if (currentValue != input.val()){
-	 input.addClass('changed');
-	 unfreezeButtons = true;
-     }else{
-	 input.removeClass('changed');
-	 jQuery("span[id=err-" + input.attr('id') + "]").remove();
-     }
-     input.removeClass('error');
-     return unfreezeButtons;
+    if (currentValue != input.val()) {
+        input.addClass('changed');
+        unfreezeButtons = true;
+    } else {
+        input.removeClass('changed');
+        jQuery("span[id=err-" + input.attr('id') + "]").remove();
+    }
+    input.removeClass('error');
+    return unfreezeButtons;
 }
 
-function setCurrentValues(form){
-    jQuery(form).find('[name^=value]').each(function () {
-	var input = jQuery(this);
-	var name = input.attr('name');
-	if(input.attr('type') == 'radio'){
-	    // NOTE: there seems to be a bug with jQuery(input).attr('checked')
-	    //       in our case, we can't rely on its value, we use
-	    //       the DOM API instead.
-	    if(input[0].checked){
-		prefsValues[name] = input.val();
-	    }
-	}else{
-	    prefsValues[name] = input.val();
-	}
-	jQuery(form).find('input[name=edits-'+ name + ']').val(prefsValues[name]);
+function setCurrentValues(form) {
+    jQuery(form).find('[name^=value]').each(function() {
+        var input = jQuery(this);
+        var name = input.attr('name');
+        if (input.attr('type') == 'radio') {
+            // NOTE: there seems to be a bug with jQuery(input).attr('checked')
+            //       in our case, we can't rely on its value, we use
+            //       the DOM API instead.
+            if (input[0].checked) {
+                prefsValues[name] = input.val();
+            }
+        } else {
+            prefsValues[name] = input.val();
+        }
+        jQuery(form).find('input[name=edits-' + name + ']').val(prefsValues[name]);
     });
 }
 
-function initEvents(){
+function initEvents() {
     jQuery('form').each(function() {
-	var form = jQuery(this);
-	//freezeFormButtons(form.attr('id'));
-	form.find('.validateButton').attr('disabled', 'disabled');
-	form.find('input[type=text]').keyup(function(){
-	    checkValues(form);
-	});
-	form.find('input[type=radio]').change(function(){
-	    checkValues(form);
-	});
-	form.find('select').change(function(){
-	    checkValues(form);
-	});
-	setCurrentValues(form);
+        var form = jQuery(this);
+        //freezeFormButtons(form.attr('id'));
+        form.find('.validateButton').attr('disabled', 'disabled');
+        form.find('input[type=text]').keyup(function() {
+            checkValues(form);
+        });
+        form.find('input[type=radio]').change(function() {
+            checkValues(form);
+        });
+        form.find('select').change(function() {
+            checkValues(form);
+        });
+        setCurrentValues(form);
     });
 }
 
 $(document).ready(function() {
-	initEvents();
+    initEvents();
 });
+
--- a/web/data/cubicweb.python.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.python.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,18 +1,14 @@
-/*
+/**
  * This file contains extensions for standard javascript types
  *
  */
 
 ONE_DAY = 86400000; // (in milliseconds)
-
 // ========== DATE EXTENSIONS ========== ///
-
 Date.prototype.equals = function(other) {
     /* compare with other date ignoring time differences */
-    if (this.getYear() == other.getYear() &&
-	this.getMonth() == other.getMonth() &&
-	this.getDate() == other.getDate()) {
-	return true;
+    if (this.getYear() == other.getYear() && this.getMonth() == other.getMonth() && this.getDate() == other.getDate()) {
+        return true;
     }
     return false;
 };
@@ -24,7 +20,7 @@
 };
 
 Date.prototype.sub = function(days) {
-    return this.add(-days);
+    return this.add( - days);
 };
 
 Date.prototype.iadd = function(days) {
@@ -39,33 +35,37 @@
     this.setTime(this.getTime() - (days * ONE_DAY));
 };
 
-/*
+/**
+ * .. function:: Date.prototype.nextMonth()
+ *
  * returns the first day of the next month
  */
 Date.prototype.nextMonth = function() {
     if (this.getMonth() == 11) {
-	var d =new Date(this.getFullYear()+1, 0, 1);
-	return d;
+        var d = new Date(this.getFullYear() + 1, 0, 1);
+        return d;
     } else {
-	var d2 = new Date(this.getFullYear(), this.getMonth()+1, 1);
-	return d2;
+        var d2 = new Date(this.getFullYear(), this.getMonth() + 1, 1);
+        return d2;
     }
 };
 
-/*
+/**
+ * .. function:: Date.prototype.getRealDay()
+ *
  * returns the day of week, 0 being monday, 6 being sunday
  */
 Date.prototype.getRealDay = function() {
     // getDay() returns 0 for Sunday ==> 6 for Saturday
-    return (this.getDay()+6) % 7;
+    return (this.getDay() + 6) % 7;
 };
 
 Date.prototype.strftime = function(fmt) {
     if (this.toLocaleFormat !== undefined) { // browser dependent
-	return this.toLocaleFormat(fmt);
+        return this.toLocaleFormat(fmt);
     }
     // XXX implement at least a decent fallback implementation
-    return this.getFullYear() + '/' + (this.getMonth()+1) + '/' + this.getDate();
+    return this.getFullYear() + '/' + (this.getMonth() + 1) + '/' + this.getDate();
 };
 
 var _DATE_FORMAT_REGXES = {
@@ -74,231 +74,131 @@
     'm': new RegExp('^[0-9]{1,2}'),
     'H': new RegExp('^[0-9]{1,2}'),
     'M': new RegExp('^[0-9]{1,2}')
-}
+};
 
-/*
+/**
+ * .. function:: _parseDate(datestring, format)
+ *
  * _parseData does the actual parsing job needed by `strptime`
  */
 function _parseDate(datestring, format) {
     var skip0 = new RegExp('^0*[0-9]+');
     var parsed = {};
-    for (var i1=0,i2=0;i1<format.length;i1++,i2++) {
-	var c1 = format.charAt(i1);
-	var c2 = datestring.charAt(i2);
-	if (c1 == '%') {
-	    c1 = format.charAt(++i1);
-	    var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
-	    if (!data.length) {
-		return null;
-	    }
-	    data = data[0];
-	    i2 += data.length-1;
-	    var value = parseInt(data, 10);
-	    if (isNaN(value)) {
-		return null;
-	    }
-	    parsed[c1] = value;
-	    continue;
-	}
-	if (c1 != c2) {
-	    return null;
-	}
+    for (var i1 = 0, i2 = 0; i1 < format.length; i1++, i2++) {
+        var c1 = format.charAt(i1);
+        var c2 = datestring.charAt(i2);
+        if (c1 == '%') {
+            c1 = format.charAt(++i1);
+            var data = _DATE_FORMAT_REGXES[c1].exec(datestring.substring(i2));
+            if (!data.length) {
+                return null;
+            }
+            data = data[0];
+            i2 += data.length - 1;
+            var value = parseInt(data, 10);
+            if (isNaN(value)) {
+                return null;
+            }
+            parsed[c1] = value;
+            continue;
+        }
+        if (c1 != c2) {
+            return null;
+        }
     }
     return parsed;
 }
 
-/*
+/**
+ * .. function:: strptime(datestring, format)
+ *
  * basic implementation of strptime. The only recognized formats
  * defined in _DATE_FORMAT_REGEXES (i.e. %Y, %d, %m, %H, %M)
  */
 function strptime(datestring, format) {
     var parsed = _parseDate(datestring, format);
     if (!parsed) {
-	return null;
+        return null;
     }
     // create initial date (!!! year=0 means 1900 !!!)
     var date = new Date(0, 0, 1, 0, 0);
     date.setFullYear(0); // reset to year 0
     if (parsed.Y) {
-	date.setFullYear(parsed.Y);
+        date.setFullYear(parsed.Y);
     }
     if (parsed.m) {
-	if (parsed.m < 1 || parsed.m > 12) {
-	    return null;
-	}
-	// !!! month indexes start at 0 in javascript !!!
-	date.setMonth(parsed.m - 1);
+        if (parsed.m < 1 || parsed.m > 12) {
+            return null;
+        }
+        // !!! month indexes start at 0 in javascript !!!
+        date.setMonth(parsed.m - 1);
     }
     if (parsed.d) {
-	if (parsed.m < 1 || parsed.m > 31) {
-	    return null;
-	}
-	date.setDate(parsed.d);
+        if (parsed.m < 1 || parsed.m > 31) {
+            return null;
+        }
+        date.setDate(parsed.d);
     }
     if (parsed.H) {
-	if (parsed.H < 0 || parsed.H > 23) {
-	    return null;
-	}
-	date.setHours(parsed.H);
+        if (parsed.H < 0 || parsed.H > 23) {
+            return null;
+        }
+        date.setHours(parsed.H);
     }
     if (parsed.M) {
-	if (parsed.M < 0 || parsed.M > 59) {
-	    return null;
-	}
-	date.setMinutes(parsed.M);
+        if (parsed.M < 0 || parsed.M > 59) {
+            return null;
+        }
+        date.setMinutes(parsed.M);
     }
     return date;
 }
 
 // ========== END OF DATE EXTENSIONS ========== ///
-
-
-
-// ========== ARRAY EXTENSIONS ========== ///
-Array.prototype.contains = function(element) {
-    return findValue(this, element) != -1;
-};
-
-// ========== END OF ARRAY EXTENSIONS ========== ///
-
-
-
 // ========== STRING EXTENSIONS ========== //
-
-/* python-like startsWith method for js strings
+/**
+ * .. function:: String.prototype.startswith(prefix)
+ *
+ * python-like startsWith method for js strings
  * >>>
  */
-String.prototype.startsWith = function(prefix) {
+String.prototype.startswith = function(prefix) {
     return this.indexOf(prefix) == 0;
 };
 
-/* python-like endsWith method for js strings */
-String.prototype.endsWith = function(suffix) {
+/**
+ * .. function:: String.prototype.endswith(suffix)
+ *
+ * python-like endsWith method for js strings
+ */
+String.prototype.endswith = function(suffix) {
     var startPos = this.length - suffix.length;
-    if (startPos < 0) { return false; }
+    if (startPos < 0) {
+        return false;
+    }
     return this.lastIndexOf(suffix, startPos) == startPos;
 };
 
-/* python-like strip method for js strings */
+/**
+ * .. function:: String.prototype.strip()
+ *
+ * python-like strip method for js strings
+ */
 String.prototype.strip = function() {
     return this.replace(/^\s*(.*?)\s*$/, "$1");
 };
 
-/* py-equiv: string in list */
-String.prototype.in_ = function(values) {
-    return findValue(values, this) != -1;
-};
-
-/* py-equiv: str.join(list) */
-String.prototype.join = function(args) {
-    return args.join(this);
-};
+// ========= class factories ========= //
 
-/* python-like list builtin
- * transforms an iterable in a js sequence
- * >>> gen = ifilter(function(x) {return x%2==0}, range(10))
- * >>> s = list(gen)
- * [0,2,4,6,8]
- */
-function list(iterable) {
-    var iterator = iter(iterable);
-    var result = [];
-    while (true) {
-	/* iterates until StopIteration occurs */
-	try {
-	    result.push(iterator.next());
-	} catch (exc) {
-	    if (exc != StopIteration) { throw exc; }
-	    return result;
-	}
-    }
-}
-
-/* py-equiv: getattr(obj, attrname, default=None) */
-function getattr(obj, attrname, defaultValue) {
-    // when not passed, defaultValue === undefined
-    return obj[attrname] || defaultValue;
-}
-
-/* py-equiv: operator.attrgetter */
-function attrgetter(attrname) {
-    return function(obj) { return getattr(obj, attrname); };
-}
-
-
-/* returns a subslice of `lst` using `start`/`stop`/`step`
- * start, stop might be negative
+/**
+ * .. function:: makeUnboundMethod(meth)
  *
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2)
- * ['c', 'd', 'e', 'f']
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], 2, -2)
- * ['c', 'd']
- * >>> sliceList(['a', 'b', 'c', 'd', 'e', 'f'], -3)
- * ['d', 'e', 'f']
+ * transforms a function into an unbound method
  */
-function sliceList(lst, start, stop, step) {
-    start = start || 0;
-    stop = stop || lst.length;
-    step = step || 1;
-    if (stop < 0) {
-	stop = max(lst.length+stop, 0);
-    }
-    if (start < 0) {
-	start = min(lst.length+start, lst.length);
-    }
-    var result = [];
-    for (var i=start; i < stop; i+=step) {
-	result.push(lst[i]);
-    }
-    return result;
-}
-
-/* returns a partial func that calls a mehod on its argument
- * py-equiv: return lambda obj: getattr(obj, methname)(*args)
- */
-// XXX looks completely unused (candidate for removal)
-function methodcaller(methname) {
-    var args = sliceList(arguments, 1);
-    return function(obj) {
-	return obj[methname].apply(obj, args);
-    };
-}
-
-/* use MochiKit's listMin / listMax */
-function min() { return listMin(arguments); }
-function max() { return listMax(arguments); }
-
-/*
- * >>> d = dict(["x", "y", "z"], [0, 1, 2])
- * >>> d['y']
- * 1
- * >>> d.y
- * 1
- */
-function dict(keys, values) {
-    if (keys.length != values.length) {
-	throw "got different number of keys and values !";
-    }
-    var newobj = {};
-    for(var i=0; i<keys.length; i++) {
-	newobj[keys[i]] = values[i];
-    }
-    return newobj;
-}
-
-
-function concat() {
-    return ''.join(list(arguments));
-}
-
-
-/**** class factories ****/
-
-// transforms a function into an unbound method
 function makeUnboundMethod(meth) {
     function unboundMeth(self) {
-	var newargs = sliceList(arguments, 1);
-	return meth.apply(self, newargs);
+        var newargs = sliceList(arguments, 1);
+        return meth.apply(self, newargs);
     }
     unboundMeth.__name__ = meth.__name__;
     return unboundMeth;
@@ -312,29 +212,40 @@
     cls.prototype[methname] = meth; // for the instance
 }
 
-// simple internal function that tells if the attribute should
-// be copied from baseclasses or not
+/**
+ * .. function:: _isAttrSkipped(attrname)
+ *
+ * simple internal function that tells if the attribute should
+ * be copied from baseclasses or not
+ */
 function _isAttrSkipped(attrname) {
     var skipped = ['__class__', '__dict__', '__bases__', 'prototype'];
-    for (var i=0; i < skipped.length; i++) {
-	if (skipped[i] == attrname) {
-	    return true;
-	}
+    for (var i = 0; i < skipped.length; i++) {
+        if (skipped[i] == attrname) {
+            return true;
+        }
     }
     return false;
 }
 
-// internal function used to build the class constructor
+/**
+ * .. function:: makeConstructor(userctor)
+ *
+ * internal function used to build the class constructor
+ */
 function makeConstructor(userctor) {
     return function() {
-	// this is a proxy to user's __init__
-	if (userctor) {
-	    userctor.apply(this, arguments);
-	}
+        // this is a proxy to user's __init__
+        if (userctor) {
+            userctor.apply(this, arguments);
+        }
     };
 }
 
-/* this is a js class factory. objects returned by this function behave
+/**
+ * .. function:: defclass(name, bases, classdict)
+ *
+ * this is a js class factory. objects returned by this function behave
  * more or less like a python class. The `class` function prototype is
  * inspired by the python `type` builtin
  * Important notes :
@@ -347,19 +258,21 @@
     // this is the static inheritance approach (<=> differs from python)
     var basemeths = {};
     var reverseLookup = [];
-    for(var i=baseclasses.length-1; i >= 0; i--) {
-	reverseLookup.push(baseclasses[i]);
+    for (var i = baseclasses.length - 1; i >= 0; i--) {
+        reverseLookup.push(baseclasses[i]);
     }
-    reverseLookup.push({'__dict__' : classdict});
+    reverseLookup.push({
+        '__dict__': classdict
+    });
 
-    for(var i=0; i < reverseLookup.length; i++) {
-	var cls = reverseLookup[i];
-	for (prop in cls.__dict__) {
-	    // XXX hack to avoid __init__, __bases__...
-	    if ( !_isAttrSkipped(prop) ) {
-		basemeths[prop] = cls.__dict__[prop];
-	    }
-	}
+    for (var i = 0; i < reverseLookup.length; i++) {
+        var cls = reverseLookup[i];
+        for (prop in cls.__dict__) {
+            // XXX hack to avoid __init__, __bases__...
+            if (!_isAttrSkipped(prop)) {
+                basemeths[prop] = cls.__dict__[prop];
+            }
+        }
     }
     var userctor = basemeths['__init__'];
     var constructor = makeConstructor(userctor);
@@ -371,38 +284,8 @@
     constructor.prototype.__class__ = constructor;
     // make bound / unbound methods
     for (methname in basemeths) {
-	attachMethodToClass(constructor, methname, basemeths[methname]);
+        attachMethodToClass(constructor, methname, basemeths[methname]);
     }
 
     return constructor;
 }
-
-// Not really python-like
-CubicWeb = {};
-// XXX backward compatibility
-Erudi = CubicWeb;
-CubicWeb.loaded = [];
-CubicWeb.require = function(module) {
-    if (!CubicWeb.loaded.contains(module)) {
-	// a CubicWeb.load_javascript(module) function would require a dependency on ajax.js
-	log(module, ' is required but not loaded');
-    }
-};
-
-CubicWeb.provide = function(module) {
-    if (!CubicWeb.loaded.contains(module)) {
-	CubicWeb.loaded.push(module);
-    }
-};
-
-jQuery(document).ready(function() {
-    jQuery(CubicWeb).trigger('server-response', [false, document]);
-});
-
-// XXX as of 2010-04-07, no known cube uses this
-jQuery(CubicWeb).bind('ajax-loaded', function() {
-    log('[3.7] "ajax-loaded" event is deprecated, use "server-response" instead');
-    jQuery(CubicWeb).trigger('server-response', [false, document]);
-});
-
-CubicWeb.provide('python.js');
--- a/web/data/cubicweb.rhythm.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.rhythm.js	Thu Jun 03 14:51:42 2010 +0200
@@ -2,6 +2,6 @@
     $('a.rhythm').click(function (event){
         $('div#pageContent').toggleClass('rhythm_bg');
         $('div#page').toggleClass('rhythm_bg');
-	event.preventDefault();
-	});
+        event.preventDefault();
     });
+});
--- a/web/data/cubicweb.tabs.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.tabs.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,6 +1,7 @@
 function set_tab(tabname, cookiename) {
     // set appropriate cookie
-    asyncRemoteExec('set_cookie', cookiename, tabname);
+    loadRemote('json', ajaxFuncArgs('set_cookie', null, cookiename, tabname));
     // trigger show + tabname event
     trigger_load(tabname);
 }
+
--- a/web/data/cubicweb.timeline-ext.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.timeline-ext.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,49 +1,49 @@
-/*
+/**
  *  :organization: Logilab
- *  :copyright: 2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2008-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  *
  */
 
-
-/* provide our own custom date parser since the default
+/**
+ * provide our own custom date parser since the default
  * one only understands iso8601 and gregorian dates
  */
 SimileAjax.NativeDateUnit.getParser = Timeline.NativeDateUnit.getParser = function(format) {
     if (typeof format == "string") {
-	if (format.indexOf('%') != -1) {
-	    return function(datestring) {
-		if (datestring) {
-		    return strptime(datestring, format);
-		}
-		return null;
-	    };
-	}
+        if (format.indexOf('%') != - 1) {
+            return function(datestring) {
+                if (datestring) {
+                    return strptime(datestring, format);
+                }
+                return null;
+            };
+        }
         format = format.toLowerCase();
     }
     if (format == "iso8601" || format == "iso 8601") {
-	return Timeline.DateTime.parseIso8601DateTime;
+        return Timeline.DateTime.parseIso8601DateTime;
     }
     return Timeline.DateTime.parseGregorianDateTime;
 };
 
 /*** CUBICWEB EVENT PAINTER *****************************************************/
 Timeline.CubicWebEventPainter = function(params) {
-//  Timeline.OriginalEventPainter.apply(this, arguments);
-   this._params = params;
-   this._onSelectListeners = [];
+    //  Timeline.OriginalEventPainter.apply(this, arguments);
+    this._params = params;
+    this._onSelectListeners = [];
 
-   this._filterMatcher = null;
-   this._highlightMatcher = null;
-   this._frc = null;
+    this._filterMatcher = null;
+    this._highlightMatcher = null;
+    this._frc = null;
 
-   this._eventIdToElmt = {};
+    this._eventIdToElmt = {};
 };
 
 Timeline.CubicWebEventPainter.prototype = new Timeline.OriginalEventPainter();
 
 Timeline.CubicWebEventPainter.prototype._paintEventLabel = function(
-  evt, text, left, top, width, height, theme) {
+evt, text, left, top, width, height, theme) {
     var doc = this._timeline.getDocument();
 
     var labelDiv = doc.createElement("div");
@@ -54,15 +54,21 @@
     labelDiv.style.top = top + "px";
 
     if (evt._obj.onclick) {
-	labelDiv.appendChild(A({'href': evt._obj.onclick}, text));
+        labelDiv.appendChild(A({
+            'href': evt._obj.onclick
+        },
+        text));
     } else if (evt._obj.image) {
-      labelDiv.appendChild(IMG({src: evt._obj.image, width: '30px', height: '30px'}));
+        labelDiv.appendChild(IMG({
+            src: evt._obj.image,
+            width: '30px',
+            height: '30px'
+        }));
     } else {
-      labelDiv.innerHTML = text;
+        labelDiv.innerHTML = text;
     }
 
-    if(evt._title != null)
-        labelDiv.title = evt._title;
+    if (evt._title != null) labelDiv.title = evt._title;
 
     var color = evt.getTextColor();
     if (color == null) {
@@ -72,29 +78,31 @@
         labelDiv.style.color = color;
     }
     var classname = evt.getClassName();
-    if(classname) labelDiv.className +=' ' + classname;
+    if (classname) labelDiv.className += ' ' + classname;
 
     this._eventLayer.appendChild(labelDiv);
 
     return {
-        left:   left,
-        top:    top,
-        width:  width,
+        left: left,
+        top: top,
+        width: width,
         height: height,
-        elmt:   labelDiv
+        elmt: labelDiv
     };
 };
 
+Timeline.CubicWebEventPainter.prototype._showBubble = function(x, y, evt) {
+    var div = DIV({
+        id: 'xxx'
+    });
+    var width = this._params.theme.event.bubble.width;
+    if (!evt._obj.bubbleUrl) {
+        evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
+    }
+    SimileAjax.WindowManager.cancelPopups();
+    SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, width);
+    if (evt._obj.bubbleUrl) {
+        jQuery('#xxx').loadxhtml(evt._obj.bubbleUrl, null, 'post', 'replace');
+    }
+};
 
-Timeline.CubicWebEventPainter.prototype._showBubble = function(x, y, evt) {
-  var div = DIV({id: 'xxx'});
-  var width = this._params.theme.event.bubble.width;
-  if (!evt._obj.bubbleUrl) {
-    evt.fillInfoBubble(div, this._params.theme, this._band.getLabeller());
-  }
-  SimileAjax.WindowManager.cancelPopups();
-  SimileAjax.Graphics.createBubbleForContentAndPoint(div, x, y, width);
-  if (evt._obj.bubbleUrl) {
-    jQuery('#xxx').loadxhtml(evt._obj.bubbleUrl, null, 'post', 'replace');
-  }
-};
--- a/web/data/cubicweb.widgets.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/cubicweb.widgets.js	Thu Jun 03 14:51:42 2010 +0200
@@ -1,6 +1,8 @@
-/*
+/**
+ * Functions dedicated to widgets.
+ *
  *  :organization: Logilab
- *  :copyright: 2003-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+ *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
  *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
  *
  *
@@ -9,144 +11,175 @@
 // widget namespace
 Widgets = {};
 
-
-/* this function takes a DOM node defining a widget and
+/**
+ * .. function:: buildWidget(wdgnode)
+ *
+ * this function takes a DOM node defining a widget and
  * instantiates / builds the appropriate widget class
  */
 function buildWidget(wdgnode) {
     var wdgclass = Widgets[wdgnode.getAttribute('cubicweb:wdgtype')];
     if (wdgclass) {
-	var wdg = new wdgclass(wdgnode);
+        var wdg = new wdgclass(wdgnode);
     }
 }
 
-/* This function is called on load and is in charge to build
+/**
+ * .. function:: buildWidgets(root)
+ *
+ * This function is called on load and is in charge to build
  * JS widgets according to DOM nodes found in the page
  */
 function buildWidgets(root) {
     root = root || document;
     jQuery(root).find('.widget').each(function() {
-	if (this.getAttribute('cubicweb:loadtype') == 'auto') {
-	    buildWidget(this);
-	}
+        if (this.getAttribute('cubicweb:loadtype') == 'auto') {
+            buildWidget(this);
+        }
     });
 }
 
-
 // we need to differenciate cases where initFacetBoxEvents is called
 // with one argument or without any argument. If we use `initFacetBoxEvents`
 // as the direct callback on the jQuery.ready event, jQuery will pass some argument
 // of his, so we use this small anonymous function instead.
-jQuery(document).ready(function() {buildWidgets();});
+jQuery(document).ready(function() {
+    buildWidgets();
+});
 
+function postJSON(url, data, callback) {
+    return jQuery.post(url, data, callback, 'json');
+}
+
+function getJSON(url, data, callback) {
+    return jQuery.get(url, data, callback, 'json');
+}
 
 Widgets.SuggestField = defclass('SuggestField', null, {
     __init__: function(node, options) {
-	var multi = node.getAttribute('cubicweb:multi') || "no";
-	options = options || {};
-	options.multiple = (multi == "yes") ? true : false;
-	var dataurl = node.getAttribute('cubicweb:dataurl');
+        var multi = node.getAttribute('cubicweb:multi') || "no";
+        options = options || {};
+        options.multiple = (multi == "yes") ? true: false;
+        var dataurl = node.getAttribute('cubicweb:dataurl');
         var method = postJSON;
-	if (options.method == 'get'){
-	  method = function(url, data, callback) {
-	    // We can't rely on jQuery.getJSON because the server
-	    // might set the Content-Type's response header to 'text/plain'
-	    jQuery.get(url, data, function(response) {
-	      callback(evalJSON(response));
-	    });
-	  };
-	}
-	var self = this; // closure
-	method(dataurl, null, function(data) {
-	    // in case we received a list of couple, we assume that the first
-	    // element is the real value to be sent, and the second one is the
-	    // value to be displayed
-	    if (data.length && data[0].length == 2) {
-		options.formatItem = function(row) { return row[1]; };
-		self.hideRealValue(node);
-		self.setCurrentValue(node, data);
-	    }
-	    jQuery(node).autocomplete(data, options);
-	});
+        if (options.method == 'get') {
+            method = function(url, data, callback) {
+                // We can't rely on jQuery.getJSON because the server
+                // might set the Content-Type's response header to 'text/plain'
+                jQuery.get(url, data, function(response) {
+                    callback(cw.evalJSON(response));
+                });
+            };
+        }
+        var self = this; // closure
+        method(dataurl, null, function(data) {
+            // in case we received a list of couple, we assume that the first
+            // element is the real value to be sent, and the second one is the
+            // value to be displayed
+            if (data.length && data[0].length == 2) {
+                options.formatItem = function(row) {
+                    return row[1];
+                };
+                self.hideRealValue(node);
+                self.setCurrentValue(node, data);
+            }
+            jQuery(node).autocomplete(data, options);
+        });
     },
 
     hideRealValue: function(node) {
-	var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': node.value});
-	node.parentNode.appendChild(hidden);
-	// remove 'name' attribute from visible input so that it is not submitted
-	// and set correct value in the corresponding hidden field
-	jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
-	    hidden.value = row[0];
-	});
+        var hidden = INPUT({
+            'type': "hidden",
+            'name': node.name,
+            'value': node.value
+        });
+        node.parentNode.appendChild(hidden);
+        // remove 'name' attribute from visible input so that it is not submitted
+        // and set correct value in the corresponding hidden field
+        jQuery(node).removeAttr('name').bind('result', function(_, row, _) {
+            hidden.value = row[0];
+        });
     },
 
     setCurrentValue: function(node, data) {
-	// called when the data is loaded to reset the correct displayed
-	// value in the visible input field (typically replacing an eid
-	// by a displayable value)
-	var curvalue = node.value;
-	if (!node.value) {
-	    return;
-	}
-	for (var i=0,length=data.length; i<length; i++) {
-	    var row = data[i];
-	    if (row[0] == curvalue) {
-		node.value = row[1];
-		return;
-	    }
-	}
+        // called when the data is loaded to reset the correct displayed
+        // value in the visible input field (typically replacing an eid
+        // by a displayable value)
+        var curvalue = node.value;
+        if (!node.value) {
+            return;
+        }
+        for (var i = 0, length = data.length; i < length; i++) {
+            var row = data[i];
+            if (row[0] == curvalue) {
+                node.value = row[1];
+                return;
+            }
+        }
     }
 });
 
 Widgets.StaticFileSuggestField = defclass('StaticSuggestField', [Widgets.SuggestField], {
 
-    __init__ : function(node) {
-	Widgets.SuggestField.__init__(this, node, {method: 'get'});
+    __init__: function(node) {
+        Widgets.SuggestField.__init__(this, node, {
+            method: 'get'
+        });
     }
 
 });
 
 Widgets.RestrictedSuggestField = defclass('RestrictedSuggestField', [Widgets.SuggestField], {
 
-    __init__ : function(node) {
-	Widgets.SuggestField.__init__(this, node, {mustMatch: true});
+    __init__: function(node) {
+        Widgets.SuggestField.__init__(this, node, {
+            mustMatch: true
+        });
     }
 
 });
 //remote version of RestrictedSuggestField
 Widgets.LazySuggestField = defclass('LazySuggestField', [Widgets.SuggestField], {
     __init__: function(node, options) {
-	var self = this;
-	var multi = "no";
-	options = options || {};
-	options.max = 50;
-	options.delay = 50;
-	options.cacheLength=0;
-	options.mustMatch = true;
+        var self = this;
+        var multi = "no";
+        options = options || {};
+        options.max = 50;
+        options.delay = 50;
+        options.cacheLength = 0;
+        options.mustMatch = true;
         // multiple selection not supported yet (still need to formalize correctly
         // initial values / display values)
-        var initialvalue = evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
+        var initialvalue = cw.evalJSON(node.getAttribute('cubicweb:initialvalue') || 'null');
         if (!initialvalue) {
             initialvalue = node.value;
         }
-        options = jQuery.extend({dataType: 'json',
-                                 multiple: (multi == "yes") ? true : false,
-                                 parse: this.parseResult
-                                }, options);
+        options = jQuery.extend({
+            dataType: 'json',
+            multiple: (multi == "yes") ? true: false,
+            parse: this.parseResult
+        },
+        options);
         var dataurl = node.getAttribute('cubicweb:dataurl');
         // remove 'name' from original input and add the hidden one that will
         // store the actual value
-        var hidden = INPUT({'type': "hidden", 'name': node.name, 'value': initialvalue});
+        var hidden = INPUT({
+            'type': "hidden",
+            'name': node.name,
+            'value': initialvalue
+        });
         node.parentNode.appendChild(hidden);
-        jQuery(node).bind('result', {hinput: hidden, input:node}, self.hideRealValue)
-            .removeAttr('name').autocomplete(dataurl, options);
+        jQuery(node).bind('result', {
+            hinput: hidden,
+            input: node
+        },
+        self.hideRealValue).removeAttr('name').autocomplete(dataurl, options);
     },
 
-
     hideRealValue: function(evt, data, value) {
-	if (!value){
-	    value="";
-	}
+        if (!value) {
+            value = "";
+        }
         evt.data.hinput.value = value;
     },
 
@@ -156,68 +189,80 @@
      */
     parseResult: function(data) {
         var parsed = [];
-        for (var i=0; i < data.length; i++) {
-                var value = ''+data[i][0]; // a string is required later by jquery.autocomplete.js
-                var label = data[i][1];
-                parsed[parsed.length] = {
-                    data: [label],
-                    value: value,
-                    result: label
-                };
+        for (var i = 0; i < data.length; i++) {
+            var value = '' + data[i][0]; // a string is required later by jquery.autocomplete.js
+            var label = data[i][1];
+            parsed[parsed.length] = {
+                data: [label],
+                value: value,
+                result: label
+            };
         };
         return parsed;
     }
 
 });
 
-/*
+/**
+ * .. class:: Widgets.SuggestForm
+ *
  * suggestform displays a suggest field and associated validate / cancel buttons
  * constructor's argumemts are the same that BaseSuggestField widget
  */
 Widgets.SuggestForm = defclass("SuggestForm", null, {
 
-    __init__ : function(inputid, initfunc, varargs, validatefunc, options) {
-	this.validatefunc = validatefunc || noop;
-	this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc,
-						    varargs, options);
-	this.oklabel = options.oklabel || 'ok';
-	this.cancellabel = options.cancellabel || 'cancel';
-	bindMethods(this);
-	connect(this.sgfield, 'validate', this, this.entryValidated);
+    __init__: function(inputid, initfunc, varargs, validatefunc, options) {
+        this.validatefunc = validatefunc || noop;
+        this.sgfield = new Widgets.BaseSuggestField(inputid, initfunc, varargs, options);
+        this.oklabel = options.oklabel || 'ok';
+        this.cancellabel = options.cancellabel || 'cancel';
+        bindMethods(this);
+        connect(this.sgfield, 'validate', this, this.entryValidated);
     },
 
-    show : function(parentnode) {
-	var sgnode = this.sgfield.builddom();
-	var buttons = DIV({'class' : "sgformbuttons"},
-			  [A({'href' : "javascript: noop();",
-			      'onclick' : this.onValidateClicked}, this.oklabel),
-			   ' / ',
-			   A({'href' : "javascript: noop();",
-			      'onclick' : this.destroy}, escapeHTML(this.cancellabel))]);
-	var formnode = DIV({'class' : "sgform"}, [sgnode, buttons]);
- 	appendChildNodes(parentnode, formnode);
-	this.sgfield.textinput.focus();
-	this.formnode = formnode;
-	return formnode;
+    show: function(parentnode) {
+        var sgnode = this.sgfield.builddom();
+        var buttons = DIV({
+            'class': "sgformbuttons"
+        },
+        [A({
+            'href': "javascript: noop();",
+            'onclick': this.onValidateClicked
+        },
+        this.oklabel), ' / ', A({
+            'href': "javascript: noop();",
+            'onclick': this.destroy
+        },
+        escapeHTML(this.cancellabel))]);
+        var formnode = DIV({
+            'class': "sgform"
+        },
+        [sgnode, buttons]);
+        appendChildNodes(parentnode, formnode);
+        this.sgfield.textinput.focus();
+        this.formnode = formnode;
+        return formnode;
     },
 
-    destroy : function() {
-	signal(this, 'destroy');
-	this.sgfield.destroy();
-	removeElement(this.formnode);
+    destroy: function() {
+        signal(this, 'destroy');
+        this.sgfield.destroy();
+        removeElement(this.formnode);
     },
 
-    onValidateClicked : function() {
-	this.validatefunc(this, this.sgfield.taglist());
+    onValidateClicked: function() {
+        this.validatefunc(this, this.sgfield.taglist());
     },
     /* just an indirection to pass the form instead of the sgfield as first parameter */
-    entryValidated : function(sgfield, taglist) {
-	this.validatefunc(this, taglist);
+    entryValidated: function(sgfield, taglist) {
+        this.validatefunc(this, taglist);
     }
 });
 
-
-/* called when the use clicks on a tree node
+/**
+ * .. function:: toggleTree(event)
+ *
+ * called when the use clicks on a tree node
  *  - if the node has a `cubicweb:loadurl` attribute, replace the content of the node
  *    by the url's content.
  *  - else, there's nothing to do, let the jquery plugin handle it.
@@ -227,91 +272,136 @@
     var url = linode.attr('cubicweb:loadurl');
     if (url) {
         linode.find('ul.placeholder').remove();
-	linode.loadxhtml(url, {callback: function(domnode) {
-	    linode.removeAttr('cubicweb:loadurl');
-	    jQuery(domnode).treeview({toggle: toggleTree,
-				      prerendered: true});
-	    return null;
-	}}, 'post', 'append');
+        linode.loadxhtml(url, {
+            callback: function(domnode) {
+                linode.removeAttr('cubicweb:loadurl');
+                jQuery(domnode).treeview({
+                    toggle: toggleTree,
+                    prerendered: true
+                });
+                return null;
+            }
+        },
+        'post', 'append');
     }
 }
 
-
-/* widget based on SIMILE's timeline widget
+/**
+ * .. class:: Widgets.TimelineWidget
+ *
+ * widget based on SIMILE's timeline widget
  * http://code.google.com/p/simile-widgets/
  *
  * Beware not to mess with SIMILE's Timeline JS namepsace !
  */
 
 Widgets.TimelineWidget = defclass("TimelineWidget", null, {
-    __init__: function (wdgnode) {
- 	var tldiv = DIV({id: "tl", style: 'height: 200px; border: 1px solid #ccc;'});
-	wdgnode.appendChild(tldiv);
-	var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
-	var eventSource = new Timeline.DefaultEventSource();
-	var bandData = {
-	  eventPainter:     Timeline.CubicWebEventPainter,
-	  eventSource:    eventSource,
-	  width:          "100%",
-	  intervalUnit:   Timeline.DateTime[tlunit.toUpperCase()],
-	  intervalPixels: 100
-	};
-	var bandInfos = [ Timeline.createBandInfo(bandData) ];
-	var tl = Timeline.create(tldiv, bandInfos);
-	var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
-	Timeline.loadJSON(loadurl, function(json, url) {
-			    eventSource.loadJSON(json, url); });
+    __init__: function(wdgnode) {
+        var tldiv = DIV({
+            id: "tl",
+            style: 'height: 200px; border: 1px solid #ccc;'
+        });
+        wdgnode.appendChild(tldiv);
+        var tlunit = wdgnode.getAttribute('cubicweb:tlunit') || 'YEAR';
+        var eventSource = new Timeline.DefaultEventSource();
+        var bandData = {
+            eventPainter: Timeline.CubicWebEventPainter,
+            eventSource: eventSource,
+            width: "100%",
+            intervalUnit: Timeline.DateTime[tlunit.toUpperCase()],
+            intervalPixels: 100
+        };
+        var bandInfos = [Timeline.createBandInfo(bandData)];
+        var tl = Timeline.create(tldiv, bandInfos);
+        var loadurl = wdgnode.getAttribute('cubicweb:loadurl');
+        Timeline.loadJSON(loadurl, function(json, url) {
+            eventSource.loadJSON(json, url);
+        });
 
     }
 });
 
 Widgets.TemplateTextField = defclass("TemplateTextField", null, {
 
-    __init__ : function(wdgnode) {
-	this.variables = getNodeAttribute(wdgnode, 'cubicweb:variables').split(',');
-	this.options = {'name'   : wdgnode.getAttribute('cubicweb:inputid'),
-			'rows' : wdgnode.getAttribute('cubicweb:rows') || 40,
-			'cols' : wdgnode.getAttribute('cubicweb:cols') || 80
-		       };
-	// this.variableRegexp = /%\((\w+)\)s/;
-	this.errorField = DIV({'class' : "errorMessage"});
-	this.textField = TEXTAREA(this.options);
-	jQuery(this.textField).bind('keyup', {'self': this}, this.highlightInvalidVariables);
-	jQuery('#substitutions').prepend(this.errorField);
-	jQuery('#substitutions .errorMessage').hide();
-	wdgnode.appendChild(this.textField);
+    __init__: function(wdgnode) {
+        this.variables = jQuery(wdgnode).attr('cubicweb:variables').split(',');
+        this.options = {
+            name: wdgnode.getAttribute('cubicweb:inputid'),
+            rows: wdgnode.getAttribute('cubicweb:rows') || 40,
+            cols: wdgnode.getAttribute('cubicweb:cols') || 80
+        };
+        // this.variableRegexp = /%\((\w+)\)s/;
+        this.errorField = DIV({
+            'class': "errorMessage"
+        });
+        this.textField = TEXTAREA(this.options);
+        jQuery(this.textField).bind('keyup', {
+            'self': this
+        },
+        this.highlightInvalidVariables);
+        jQuery('#substitutions').prepend(this.errorField);
+        jQuery('#substitutions .errorMessage').hide();
+        wdgnode.appendChild(this.textField);
     },
 
     /* signal callbacks */
 
-    highlightInvalidVariables : function(event) {
-	var self = event.data.self;
-	var text = self.textField.value;
-	var unknownVariables = [];
-	var it = 0;
-	var group = null;
-	var variableRegexp = /%\((\w+)\)s/g;
-	// emulates rgx.findAll()
-	while ( group=variableRegexp.exec(text) ) {
-	    if ( !self.variables.contains(group[1]) ) {
-		unknownVariables.push(group[1]);
-	    }
-	    it++;
-	    if (it > 5) {
-		break;
-	    }
-	}
-	var errText = '';
-	if (unknownVariables.length) {
-	    errText = "Detected invalid variables : " + ", ".join(unknownVariables);
-	    jQuery('#substitutions .errorMessage').show();
-	} else {
-	    jQuery('#substitutions .errorMessage').hide();
-	}
-	self.errorField.innerHTML = errText;
+    highlightInvalidVariables: function(event) {
+        var self = event.data.self;
+        var text = self.textField.value;
+        var unknownVariables = [];
+        var it = 0;
+        var group = null;
+        var variableRegexp = /%\((\w+)\)s/g;
+        // emulates rgx.findAll()
+        while (group = variableRegexp.exec(text)) {
+            if (!self.variables.contains(group[1])) {
+                unknownVariables.push(group[1]);
+            }
+            it++;
+            if (it > 5) {
+                break;
+            }
+        }
+        var errText = '';
+        if (unknownVariables.length) {
+            errText = "Detected invalid variables : " + unknownVariables.join(', ');
+            jQuery('#substitutions .errorMessage').show();
+        } else {
+            jQuery('#substitutions .errorMessage').hide();
+        }
+        self.errorField.innerHTML = errText;
     }
 
 });
 
-
-CubicWeb.provide('widgets.js');
+cw.widgets = {
+    /**
+     * .. function:: insertText(text, areaId)
+     *
+     * inspects textarea with id `areaId` and replaces the current selected text
+     * with `text`. Cursor is then set at the end of the inserted text.
+     */
+    insertText: function (text, areaId) {
+        var textarea = jQuery('#' + areaId);
+        if (document.selection) { // IE
+            var selLength;
+            textarea.focus();
+            var sel = document.selection.createRange();
+            selLength = sel.text.length;
+            sel.text = text;
+            sel.moveStart('character', selLength - text.length);
+            sel.select();
+        } else if (textarea.selectionStart || textarea.selectionStart == '0') { // mozilla
+            var startPos = textarea.selectionStart;
+            var endPos = textarea.selectionEnd;
+            // insert text so that it replaces the [startPos, endPos] part
+            textarea.value = textarea.value.substring(0, startPos) + text + textarea.value.substring(endPos, textarea.value.length);
+            // set cursor pos at the end of the inserted text
+            textarea.selectionStart = textarea.selectionEnd = startPos + text.length;
+            textarea.focus();
+        } else { // safety belt for other browsers
+            textarea.value += text;
+        }
+    }
+};
\ No newline at end of file
--- a/web/data/jquery.tablesorter.js	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/jquery.tablesorter.js	Thu Jun 03 14:51:42 2010 +0200
@@ -705,10 +705,10 @@
 	ts.addParser({
 	    id: "json",
 	    is: function(s) {
-	        return s.startsWith('json:');
+	        return s.startswith('json:');
 	    },
 	    format: function(s,table,cell) {
-		return evalJSON(s.slice(5));
+		return cw.evalJSON(s.slice(5));
 	    },
 	  type: "text"
 	});
--- a/web/data/uiprops.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/data/uiprops.py	Thu Jun 03 14:51:42 2010 +0200
@@ -12,6 +12,7 @@
 JAVASCRIPTS = [data('jquery.js'),
                data('jquery.corner.js'),
                data('jquery.json.js'),
+               data('cubicweb.js'),
                data('cubicweb.compat.js'),
                data('cubicweb.python.js'),
                data('cubicweb.htmlhelpers.js')]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url0.html	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,3 @@
+<div id="ajaxroot">
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url1.html	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,6 @@
+<div id="ajaxroot">
+  <div class="ajaxHtmlHead">
+    <script src="http://foo.js" type="text/javascript"> </script>
+  </div>
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajax_url2.html	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,7 @@
+<div id="ajaxroot">
+  <div class="ajaxHtmlHead">
+    <script src="http://foo.js" type="text/javascript"> </script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+  </div>
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/ajaxresult.json	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,1 @@
+['foo', 'bar']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/qunit.css	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,119 @@
+
+ol#qunit-tests {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	margin:0;
+	padding:0;
+	list-style-position:inside;
+
+	font-size: smaller;
+}
+ol#qunit-tests li{
+	padding:0.4em 0.5em 0.4em 2.5em;
+	border-bottom:1px solid #fff;
+	font-size:small;
+	list-style-position:inside;
+}
+ol#qunit-tests li ol{
+	box-shadow: inset 0px 2px 13px #999;
+	-moz-box-shadow: inset 0px 2px 13px #999;
+	-webkit-box-shadow: inset 0px 2px 13px #999;
+	margin-top:0.5em;
+	margin-left:0;
+	padding:0.5em;
+	background-color:#fff;
+	border-radius:15px;
+	-moz-border-radius: 15px;
+	-webkit-border-radius: 15px;
+}
+ol#qunit-tests li li{
+	border-bottom:none;
+	margin:0.5em;
+	background-color:#fff;
+	list-style-position: inside;
+	padding:0.4em 0.5em 0.4em 0.5em;
+}
+
+ol#qunit-tests li li.pass{
+	border-left:26px solid #C6E746;
+	background-color:#fff;
+	color:#5E740B;
+	}
+ol#qunit-tests li li.fail{
+	border-left:26px solid #EE5757;
+	background-color:#fff;
+	color:#710909;
+}
+ol#qunit-tests li.pass{
+	background-color:#D2E0E6;
+	color:#528CE0;
+}
+ol#qunit-tests li.fail{
+	background-color:#EE5757;
+	color:#000;
+}
+ol#qunit-tests li strong {
+	cursor:pointer;
+}
+h1#qunit-header{
+	background-color:#0d3349;
+	margin:0;
+	padding:0.5em 0 0.5em 1em;
+	color:#fff;
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	border-top-right-radius:15px;
+	border-top-left-radius:15px;
+	-moz-border-radius-topright:15px;
+	-moz-border-radius-topleft:15px;
+	-webkit-border-top-right-radius:15px;
+	-webkit-border-top-left-radius:15px;
+	text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px;
+}
+h2#qunit-banner{
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	height:5px;
+	margin:0;
+	padding:0;
+}
+h2#qunit-banner.qunit-pass{
+	background-color:#C6E746;
+}
+h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar {
+	background-color:#EE5757;
+}
+#qunit-testrunner-toolbar {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	padding:0;
+	/*width:80%;*/
+	padding:0em 0 0.5em 2em;
+	font-size: small;
+}
+h2#qunit-userAgent {
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	background-color:#2b81af;
+	margin:0;
+	padding:0;
+	color:#fff;
+	font-size: small;
+	padding:0.5em 0 0.5em 2.5em;
+	text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
+}
+p#qunit-testresult{
+	font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial;
+	margin:0;
+	font-size: small;
+	color:#2b81af;
+	border-bottom-right-radius:15px;
+	border-bottom-left-radius:15px;
+	-moz-border-radius-bottomright:15px;
+	-moz-border-radius-bottomleft:15px;
+	-webkit-border-bottom-right-radius:15px;
+	-webkit-border-bottom-left-radius:15px;
+	background-color:#D2E0E6;
+	padding:0.5em 0.5em 0.5em 2.5em;
+}
+strong b.fail{
+	color:#710909;
+	}
+strong b.pass{
+	color:#5E740B;
+	}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/qunit.js	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,1069 @@
+/*
+ * QUnit - A JavaScript Unit Testing Framework
+ * 
+ * http://docs.jquery.com/QUnit
+ *
+ * Copyright (c) 2009 John Resig, Jörn Zaefferer
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ */
+
+(function(window) {
+
+var QUnit = {
+
+	// Initialize the configuration options
+	init: function() {
+		config = {
+			stats: { all: 0, bad: 0 },
+			moduleStats: { all: 0, bad: 0 },
+			started: +new Date,
+			updateRate: 1000,
+			blocking: false,
+			autorun: false,
+			assertions: [],
+			filters: [],
+			queue: []
+		};
+
+		var tests = id("qunit-tests"),
+			banner = id("qunit-banner"),
+			result = id("qunit-testresult");
+
+		if ( tests ) {
+			tests.innerHTML = "";
+		}
+
+		if ( banner ) {
+			banner.className = "";
+		}
+
+		if ( result ) {
+			result.parentNode.removeChild( result );
+		}
+	},
+	
+	// call on start of module test to prepend name to all tests
+	module: function(name, testEnvironment) {
+		config.currentModule = name;
+
+		synchronize(function() {
+			if ( config.currentModule ) {
+				QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+			}
+
+			config.currentModule = name;
+			config.moduleTestEnvironment = testEnvironment;
+			config.moduleStats = { all: 0, bad: 0 };
+
+			QUnit.moduleStart( name, testEnvironment );
+		});
+	},
+
+	asyncTest: function(testName, expected, callback) {
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = 0;
+		}
+
+		QUnit.test(testName, expected, callback, true);
+	},
+	
+	test: function(testName, expected, callback, async) {
+		var name = testName, testEnvironment, testEnvironmentArg;
+
+		if ( arguments.length === 2 ) {
+			callback = expected;
+			expected = null;
+		}
+		// is 2nd argument a testEnvironment?
+		if ( expected && typeof expected === 'object') {
+			testEnvironmentArg =  expected;
+			expected = null;
+		}
+
+		if ( config.currentModule ) {
+			name = config.currentModule + " module: " + name;
+		}
+
+		if ( !validTest(name) ) {
+			return;
+		}
+
+		synchronize(function() {
+			QUnit.testStart( testName );
+
+			testEnvironment = extend({
+				setup: function() {},
+				teardown: function() {}
+			}, config.moduleTestEnvironment);
+			if (testEnvironmentArg) {
+				extend(testEnvironment,testEnvironmentArg);
+			}
+
+			// allow utility functions to access the current test environment
+			QUnit.current_testEnvironment = testEnvironment;
+			
+			config.assertions = [];
+			config.expected = expected;
+
+			try {
+				if ( !config.pollution ) {
+					saveGlobal();
+				}
+
+				testEnvironment.setup.call(testEnvironment);
+			} catch(e) {
+				QUnit.ok( false, "Setup failed on " + name + ": " + e.message );
+			}
+
+			if ( async ) {
+				QUnit.stop();
+			}
+
+			try {
+				callback.call(testEnvironment);
+			} catch(e) {
+				fail("Test " + name + " died, exception and test follows", e, callback);
+				QUnit.ok( false, "Died on test #" + (config.assertions.length + 1) + ": " + e.message );
+				// else next test will carry the responsibility
+				saveGlobal();
+
+				// Restart the tests if they're blocking
+				if ( config.blocking ) {
+					start();
+				}
+			}
+		});
+
+		synchronize(function() {
+			try {
+				checkPollution();
+				testEnvironment.teardown.call(testEnvironment);
+			} catch(e) {
+				QUnit.ok( false, "Teardown failed on " + name + ": " + e.message );
+			}
+
+			try {
+				QUnit.reset();
+			} catch(e) {
+				fail("reset() failed, following Test " + name + ", exception and reset fn follows", e, reset);
+			}
+
+			if ( config.expected && config.expected != config.assertions.length ) {
+				QUnit.ok( false, "Expected " + config.expected + " assertions, but " + config.assertions.length + " were run" );
+			}
+
+			var good = 0, bad = 0,
+				tests = id("qunit-tests");
+
+			config.stats.all += config.assertions.length;
+			config.moduleStats.all += config.assertions.length;
+
+			if ( tests ) {
+				var ol  = document.createElement("ol");
+				ol.style.display = "none";
+
+				for ( var i = 0; i < config.assertions.length; i++ ) {
+					var assertion = config.assertions[i];
+
+					var li = document.createElement("li");
+					li.className = assertion.result ? "pass" : "fail";
+					li.appendChild(document.createTextNode(assertion.message || "(no message)"));
+					ol.appendChild( li );
+
+					if ( assertion.result ) {
+						good++;
+					} else {
+						bad++;
+						config.stats.bad++;
+						config.moduleStats.bad++;
+					}
+				}
+
+				var b = document.createElement("strong");
+				b.innerHTML = name + " <b style='color:black;'>(<b class='fail'>" + bad + "</b>, <b class='pass'>" + good + "</b>, " + config.assertions.length + ")</b>";
+				
+				addEvent(b, "click", function() {
+					var next = b.nextSibling, display = next.style.display;
+					next.style.display = display === "none" ? "block" : "none";
+				});
+				
+				addEvent(b, "dblclick", function(e) {
+					var target = e && e.target ? e.target : window.event.srcElement;
+					if ( target.nodeName.toLowerCase() === "strong" ) {
+						var text = "", node = target.firstChild;
+
+						while ( node.nodeType === 3 ) {
+							text += node.nodeValue;
+							node = node.nextSibling;
+						}
+
+						text = text.replace(/(^\s*|\s*$)/g, "");
+
+						if ( window.location ) {
+							window.location.href = window.location.href.match(/^(.+?)(\?.*)?$/)[1] + "?" + encodeURIComponent(text);
+						}
+					}
+				});
+
+				var li = document.createElement("li");
+				li.className = bad ? "fail" : "pass";
+				li.appendChild( b );
+				li.appendChild( ol );
+				tests.appendChild( li );
+
+				if ( bad ) {
+					var toolbar = id("qunit-testrunner-toolbar");
+					if ( toolbar ) {
+						toolbar.style.display = "block";
+						id("qunit-filter-pass").disabled = null;
+						id("qunit-filter-missing").disabled = null;
+					}
+				}
+
+			} else {
+				for ( var i = 0; i < config.assertions.length; i++ ) {
+					if ( !config.assertions[i].result ) {
+						bad++;
+						config.stats.bad++;
+						config.moduleStats.bad++;
+					}
+				}
+			}
+
+			QUnit.testDone( testName, bad, config.assertions.length );
+
+			if ( !window.setTimeout && !config.queue.length ) {
+				done();
+			}
+		});
+
+		if ( window.setTimeout && !config.doneTimer ) {
+			config.doneTimer = window.setTimeout(function(){
+				if ( !config.queue.length ) {
+					done();
+				} else {
+					synchronize( done );
+				}
+			}, 13);
+		}
+	},
+	
+	/**
+	 * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through.
+	 */
+	expect: function(asserts) {
+		config.expected = asserts;
+	},
+
+	/**
+	 * Asserts true.
+	 * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" );
+	 */
+	ok: function(a, msg) {
+		QUnit.log(a, msg);
+
+		config.assertions.push({
+			result: !!a,
+			message: msg
+		});
+	},
+
+	/**
+	 * Checks that the first two arguments are equal, with an optional message.
+	 * Prints out both actual and expected values.
+	 *
+	 * Prefered to ok( actual == expected, message )
+	 *
+	 * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." );
+	 *
+	 * @param Object actual
+	 * @param Object expected
+	 * @param String message (optional)
+	 */
+	equal: function(actual, expected, message) {
+		push(expected == actual, actual, expected, message);
+	},
+
+	notEqual: function(actual, expected, message) {
+		push(expected != actual, actual, expected, message);
+	},
+	
+	deepEqual: function(a, b, message) {
+		push(QUnit.equiv(a, b), a, b, message);
+	},
+
+	notDeepEqual: function(a, b, message) {
+		push(!QUnit.equiv(a, b), a, b, message);
+	},
+
+	strictEqual: function(actual, expected, message) {
+		push(expected === actual, actual, expected, message);
+	},
+
+	notStrictEqual: function(actual, expected, message) {
+		push(expected !== actual, actual, expected, message);
+	},
+	
+	start: function() {
+		// A slight delay, to avoid any current callbacks
+		if ( window.setTimeout ) {
+			window.setTimeout(function() {
+				if ( config.timeout ) {
+					clearTimeout(config.timeout);
+				}
+
+				config.blocking = false;
+				process();
+			}, 13);
+		} else {
+			config.blocking = false;
+			process();
+		}
+	},
+	
+	stop: function(timeout) {
+		config.blocking = true;
+
+		if ( timeout && window.setTimeout ) {
+			config.timeout = window.setTimeout(function() {
+				QUnit.ok( false, "Test timed out" );
+				QUnit.start();
+			}, timeout);
+		}
+	},
+	
+	/**
+	 * Resets the test setup. Useful for tests that modify the DOM.
+	 */
+	reset: function() {
+		if ( window.jQuery ) {
+			jQuery("#main").html( config.fixture );
+			jQuery.event.global = {};
+			jQuery.ajaxSettings = extend({}, config.ajaxSettings);
+		}
+	},
+	
+	/**
+	 * Trigger an event on an element.
+	 *
+	 * @example triggerEvent( document.body, "click" );
+	 *
+	 * @param DOMElement elem
+	 * @param String type
+	 */
+	triggerEvent: function( elem, type, event ) {
+		if ( document.createEvent ) {
+			event = document.createEvent("MouseEvents");
+			event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView,
+				0, 0, 0, 0, 0, false, false, false, false, 0, null);
+			elem.dispatchEvent( event );
+
+		} else if ( elem.fireEvent ) {
+			elem.fireEvent("on"+type);
+		}
+	},
+	
+	// Safe object type checking
+	is: function( type, obj ) {
+		return Object.prototype.toString.call( obj ) === "[object "+ type +"]";
+	},
+	
+	// Logging callbacks
+	done: function(failures, total) {},
+	log: function(result, message) {},
+	testStart: function(name) {},
+	testDone: function(name, failures, total) {},
+	moduleStart: function(name, testEnvironment) {},
+	moduleDone: function(name, failures, total) {}
+};
+
+// Backwards compatibility, deprecated
+QUnit.equals = QUnit.equal;
+QUnit.same = QUnit.deepEqual;
+
+// Maintain internal state
+var config = {
+	// The queue of tests to run
+	queue: [],
+
+	// block until document ready
+	blocking: true
+};
+
+// Load paramaters
+(function() {
+	var location = window.location || { search: "", protocol: "file:" },
+		GETParams = location.search.slice(1).split('&');
+
+	for ( var i = 0; i < GETParams.length; i++ ) {
+		GETParams[i] = decodeURIComponent( GETParams[i] );
+		if ( GETParams[i] === "noglobals" ) {
+			GETParams.splice( i, 1 );
+			i--;
+			config.noglobals = true;
+		} else if ( GETParams[i].search('=') > -1 ) {
+			GETParams.splice( i, 1 );
+			i--;
+		}
+	}
+	
+	// restrict modules/tests by get parameters
+	config.filters = GETParams;
+	
+	// Figure out if we're running the tests from a server or not
+	QUnit.isLocal = !!(location.protocol === 'file:');
+})();
+
+// Expose the API as global variables, unless an 'exports'
+// object exists, in that case we assume we're in CommonJS
+if ( typeof exports === "undefined" || typeof require === "undefined" ) {
+	extend(window, QUnit);
+	window.QUnit = QUnit;
+} else {
+	extend(exports, QUnit);
+	exports.QUnit = QUnit;
+}
+
+if ( typeof document === "undefined" || document.readyState === "complete" ) {
+	config.autorun = true;
+}
+
+addEvent(window, "load", function() {
+	// Initialize the config, saving the execution queue
+	var oldconfig = extend({}, config);
+	QUnit.init();
+	extend(config, oldconfig);
+
+	config.blocking = false;
+
+	var userAgent = id("qunit-userAgent");
+	if ( userAgent ) {
+		userAgent.innerHTML = navigator.userAgent;
+	}
+	
+	var toolbar = id("qunit-testrunner-toolbar");
+	if ( toolbar ) {
+		toolbar.style.display = "none";
+		
+		var filter = document.createElement("input");
+		filter.type = "checkbox";
+		filter.id = "qunit-filter-pass";
+		filter.disabled = true;
+		addEvent( filter, "click", function() {
+			var li = document.getElementsByTagName("li");
+			for ( var i = 0; i < li.length; i++ ) {
+				if ( li[i].className.indexOf("pass") > -1 ) {
+					li[i].style.display = filter.checked ? "none" : "";
+				}
+			}
+		});
+		toolbar.appendChild( filter );
+
+		var label = document.createElement("label");
+		label.setAttribute("for", "qunit-filter-pass");
+		label.innerHTML = "Hide passed tests";
+		toolbar.appendChild( label );
+
+		var missing = document.createElement("input");
+		missing.type = "checkbox";
+		missing.id = "qunit-filter-missing";
+		missing.disabled = true;
+		addEvent( missing, "click", function() {
+			var li = document.getElementsByTagName("li");
+			for ( var i = 0; i < li.length; i++ ) {
+				if ( li[i].className.indexOf("fail") > -1 && li[i].innerHTML.indexOf('missing test - untested code is broken code') > - 1 ) {
+					li[i].parentNode.parentNode.style.display = missing.checked ? "none" : "block";
+				}
+			}
+		});
+		toolbar.appendChild( missing );
+
+		label = document.createElement("label");
+		label.setAttribute("for", "qunit-filter-missing");
+		label.innerHTML = "Hide missing tests (untested code is broken code)";
+		toolbar.appendChild( label );
+	}
+
+	var main = id('main');
+	if ( main ) {
+		config.fixture = main.innerHTML;
+	}
+
+	if ( window.jQuery ) {
+		config.ajaxSettings = window.jQuery.ajaxSettings;
+	}
+
+	QUnit.start();
+});
+
+function done() {
+	if ( config.doneTimer && window.clearTimeout ) {
+		window.clearTimeout( config.doneTimer );
+		config.doneTimer = null;
+	}
+
+	if ( config.queue.length ) {
+		config.doneTimer = window.setTimeout(function(){
+			if ( !config.queue.length ) {
+				done();
+			} else {
+				synchronize( done );
+			}
+		}, 13);
+
+		return;
+	}
+
+	config.autorun = true;
+
+	// Log the last module results
+	if ( config.currentModule ) {
+		QUnit.moduleDone( config.currentModule, config.moduleStats.bad, config.moduleStats.all );
+	}
+
+	var banner = id("qunit-banner"),
+		tests = id("qunit-tests"),
+		html = ['Tests completed in ',
+		+new Date - config.started, ' milliseconds.<br/>',
+		'<span class="passed">', config.stats.all - config.stats.bad, '</span> tests of <span class="total">', config.stats.all, '</span> passed, <span class="failed">', config.stats.bad,'</span> failed.'].join('');
+
+	if ( banner ) {
+		banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass");
+	}
+
+	if ( tests ) {	
+		var result = id("qunit-testresult");
+
+		if ( !result ) {
+			result = document.createElement("p");
+			result.id = "qunit-testresult";
+			result.className = "result";
+			tests.parentNode.insertBefore( result, tests.nextSibling );
+		}
+
+		result.innerHTML = html;
+	}
+
+	QUnit.done( config.stats.bad, config.stats.all );
+}
+
+function validTest( name ) {
+	var i = config.filters.length,
+		run = false;
+
+	if ( !i ) {
+		return true;
+	}
+	
+	while ( i-- ) {
+		var filter = config.filters[i],
+			not = filter.charAt(0) == '!';
+
+		if ( not ) {
+			filter = filter.slice(1);
+		}
+
+		if ( name.indexOf(filter) !== -1 ) {
+			return !not;
+		}
+
+		if ( not ) {
+			run = true;
+		}
+	}
+
+	return run;
+}
+
+function push(result, actual, expected, message) {
+	message = message || (result ? "okay" : "failed");
+	QUnit.ok( result, result ? message + ": " + QUnit.jsDump.parse(expected) : message + ", expected: " + QUnit.jsDump.parse(expected) + " result: " + QUnit.jsDump.parse(actual) );
+}
+
+function synchronize( callback ) {
+	config.queue.push( callback );
+
+	if ( config.autorun && !config.blocking ) {
+		process();
+	}
+}
+
+function process() {
+	var start = (new Date()).getTime();
+
+	while ( config.queue.length && !config.blocking ) {
+		if ( config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate) ) {
+			config.queue.shift()();
+
+		} else {
+			setTimeout( process, 13 );
+			break;
+		}
+	}
+}
+
+function saveGlobal() {
+	config.pollution = [];
+	
+	if ( config.noglobals ) {
+		for ( var key in window ) {
+			config.pollution.push( key );
+		}
+	}
+}
+
+function checkPollution( name ) {
+	var old = config.pollution;
+	saveGlobal();
+	
+	var newGlobals = diff( old, config.pollution );
+	if ( newGlobals.length > 0 ) {
+		ok( false, "Introduced global variable(s): " + newGlobals.join(", ") );
+		config.expected++;
+	}
+
+	var deletedGlobals = diff( config.pollution, old );
+	if ( deletedGlobals.length > 0 ) {
+		ok( false, "Deleted global variable(s): " + deletedGlobals.join(", ") );
+		config.expected++;
+	}
+}
+
+// returns a new Array with the elements that are in a but not in b
+function diff( a, b ) {
+	var result = a.slice();
+	for ( var i = 0; i < result.length; i++ ) {
+		for ( var j = 0; j < b.length; j++ ) {
+			if ( result[i] === b[j] ) {
+				result.splice(i, 1);
+				i--;
+				break;
+			}
+		}
+	}
+	return result;
+}
+
+function fail(message, exception, callback) {
+	if ( typeof console !== "undefined" && console.error && console.warn ) {
+		console.error(message);
+		console.error(exception);
+		console.warn(callback.toString());
+
+	} else if ( window.opera && opera.postError ) {
+		opera.postError(message, exception, callback.toString);
+	}
+}
+
+function extend(a, b) {
+	for ( var prop in b ) {
+		a[prop] = b[prop];
+	}
+
+	return a;
+}
+
+function addEvent(elem, type, fn) {
+	if ( elem.addEventListener ) {
+		elem.addEventListener( type, fn, false );
+	} else if ( elem.attachEvent ) {
+		elem.attachEvent( "on" + type, fn );
+	} else {
+		fn();
+	}
+}
+
+function id(name) {
+	return !!(typeof document !== "undefined" && document && document.getElementById) &&
+		document.getElementById( name );
+}
+
+// Test for equality any JavaScript type.
+// Discussions and reference: http://philrathe.com/articles/equiv
+// Test suites: http://philrathe.com/tests/equiv
+// Author: Philippe Rathé <prathe@gmail.com>
+QUnit.equiv = function () {
+
+    var innerEquiv; // the real equiv function
+    var callers = []; // stack to decide between skip/abort functions
+    var parents = []; // stack to avoiding loops from circular referencing
+
+
+    // Determine what is o.
+    function hoozit(o) {
+        if (QUnit.is("String", o)) {
+            return "string";
+            
+        } else if (QUnit.is("Boolean", o)) {
+            return "boolean";
+
+        } else if (QUnit.is("Number", o)) {
+
+            if (isNaN(o)) {
+                return "nan";
+            } else {
+                return "number";
+            }
+
+        } else if (typeof o === "undefined") {
+            return "undefined";
+
+        // consider: typeof null === object
+        } else if (o === null) {
+            return "null";
+
+        // consider: typeof [] === object
+        } else if (QUnit.is( "Array", o)) {
+            return "array";
+        
+        // consider: typeof new Date() === object
+        } else if (QUnit.is( "Date", o)) {
+            return "date";
+
+        // consider: /./ instanceof Object;
+        //           /./ instanceof RegExp;
+        //          typeof /./ === "function"; // => false in IE and Opera,
+        //                                          true in FF and Safari
+        } else if (QUnit.is( "RegExp", o)) {
+            return "regexp";
+
+        } else if (typeof o === "object") {
+            return "object";
+
+        } else if (QUnit.is( "Function", o)) {
+            return "function";
+        } else {
+            return undefined;
+        }
+    }
+
+    // Call the o related callback with the given arguments.
+    function bindCallbacks(o, callbacks, args) {
+        var prop = hoozit(o);
+        if (prop) {
+            if (hoozit(callbacks[prop]) === "function") {
+                return callbacks[prop].apply(callbacks, args);
+            } else {
+                return callbacks[prop]; // or undefined
+            }
+        }
+    }
+    
+    var callbacks = function () {
+
+        // for string, boolean, number and null
+        function useStrictEquality(b, a) {
+            if (b instanceof a.constructor || a instanceof b.constructor) {
+                // to catch short annotaion VS 'new' annotation of a declaration
+                // e.g. var i = 1;
+                //      var j = new Number(1);
+                return a == b;
+            } else {
+                return a === b;
+            }
+        }
+
+        return {
+            "string": useStrictEquality,
+            "boolean": useStrictEquality,
+            "number": useStrictEquality,
+            "null": useStrictEquality,
+            "undefined": useStrictEquality,
+
+            "nan": function (b) {
+                return isNaN(b);
+            },
+
+            "date": function (b, a) {
+                return hoozit(b) === "date" && a.valueOf() === b.valueOf();
+            },
+
+            "regexp": function (b, a) {
+                return hoozit(b) === "regexp" &&
+                    a.source === b.source && // the regex itself
+                    a.global === b.global && // and its modifers (gmi) ...
+                    a.ignoreCase === b.ignoreCase &&
+                    a.multiline === b.multiline;
+            },
+
+            // - skip when the property is a method of an instance (OOP)
+            // - abort otherwise,
+            //   initial === would have catch identical references anyway
+            "function": function () {
+                var caller = callers[callers.length - 1];
+                return caller !== Object &&
+                        typeof caller !== "undefined";
+            },
+
+            "array": function (b, a) {
+                var i, j, loop;
+                var len;
+
+                // b could be an object literal here
+                if ( ! (hoozit(b) === "array")) {
+                    return false;
+                }   
+                
+                len = a.length;
+                if (len !== b.length) { // safe and faster
+                    return false;
+                }
+                
+                //track reference to avoid circular references
+                parents.push(a);
+                for (i = 0; i < len; i++) {
+                    loop = false;
+                    for(j=0;j<parents.length;j++){
+                        if(parents[j] === a[i]){
+                            loop = true;//dont rewalk array
+                        }
+                    }
+                    if (!loop && ! innerEquiv(a[i], b[i])) {
+                        parents.pop();
+                        return false;
+                    }
+                }
+                parents.pop();
+                return true;
+            },
+
+            "object": function (b, a) {
+                var i, j, loop;
+                var eq = true; // unless we can proove it
+                var aProperties = [], bProperties = []; // collection of strings
+
+                // comparing constructors is more strict than using instanceof
+                if ( a.constructor !== b.constructor) {
+                    return false;
+                }
+
+                // stack constructor before traversing properties
+                callers.push(a.constructor);
+                //track reference to avoid circular references
+                parents.push(a);
+                
+                for (i in a) { // be strict: don't ensures hasOwnProperty and go deep
+                    loop = false;
+                    for(j=0;j<parents.length;j++){
+                        if(parents[j] === a[i])
+                            loop = true; //don't go down the same path twice
+                    }
+                    aProperties.push(i); // collect a's properties
+
+                    if (!loop && ! innerEquiv(a[i], b[i])) {
+                        eq = false;
+                        break;
+                    }
+                }
+
+                callers.pop(); // unstack, we are done
+                parents.pop();
+
+                for (i in b) {
+                    bProperties.push(i); // collect b's properties
+                }
+
+                // Ensures identical properties name
+                return eq && innerEquiv(aProperties.sort(), bProperties.sort());
+            }
+        };
+    }();
+
+    innerEquiv = function () { // can take multiple arguments
+        var args = Array.prototype.slice.apply(arguments);
+        if (args.length < 2) {
+            return true; // end transition
+        }
+
+        return (function (a, b) {
+            if (a === b) {
+                return true; // catch the most you can
+            } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || hoozit(a) !== hoozit(b)) {
+                return false; // don't lose time with error prone cases
+            } else {
+                return bindCallbacks(a, callbacks, [b, a]);
+            }
+
+        // apply transition with (1..n) arguments
+        })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length -1));
+    };
+
+    return innerEquiv;
+
+}();
+
+/**
+ * jsDump
+ * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
+ * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php)
+ * Date: 5/15/2008
+ * @projectDescription Advanced and extensible data dumping for Javascript.
+ * @version 1.0.0
+ * @author Ariel Flesler
+ * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html}
+ */
+QUnit.jsDump = (function() {
+	function quote( str ) {
+		return '"' + str.toString().replace(/"/g, '\\"') + '"';
+	};
+	function literal( o ) {
+		return o + '';	
+	};
+	function join( pre, arr, post ) {
+		var s = jsDump.separator(),
+			base = jsDump.indent(),
+			inner = jsDump.indent(1);
+		if ( arr.join )
+			arr = arr.join( ',' + s + inner );
+		if ( !arr )
+			return pre + post;
+		return [ pre, inner + arr, base + post ].join(s);
+	};
+	function array( arr ) {
+		var i = arr.length,	ret = Array(i);					
+		this.up();
+		while ( i-- )
+			ret[i] = this.parse( arr[i] );				
+		this.down();
+		return join( '[', ret, ']' );
+	};
+	
+	var reName = /^function (\w+)/;
+	
+	var jsDump = {
+		parse:function( obj, type ) { //type is used mostly internally, you can fix a (custom)type in advance
+			var	parser = this.parsers[ type || this.typeOf(obj) ];
+			type = typeof parser;			
+			
+			return type == 'function' ? parser.call( this, obj ) :
+				   type == 'string' ? parser :
+				   this.parsers.error;
+		},
+		typeOf:function( obj ) {
+			var type;
+			if ( obj === null ) {
+				type = "null";
+			} else if (typeof obj === "undefined") {
+				type = "undefined";
+			} else if (QUnit.is("RegExp", obj)) {
+				type = "regexp";
+			} else if (QUnit.is("Date", obj)) {
+				type = "date";
+			} else if (QUnit.is("Function", obj)) {
+				type = "function";
+			} else if (obj.setInterval && obj.document && !obj.nodeType) {
+				type = "window";
+			} else if (obj.nodeType === 9) {
+				type = "document";
+			} else if (obj.nodeType) {
+				type = "node";
+			} else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) {
+				type = "array";
+			} else {
+				type = typeof obj;
+			}
+			return type;
+		},
+		separator:function() {
+			return this.multiline ?	this.HTML ? '<br />' : '\n' : this.HTML ? '&nbsp;' : ' ';
+		},
+		indent:function( extra ) {// extra can be a number, shortcut for increasing-calling-decreasing
+			if ( !this.multiline )
+				return '';
+			var chr = this.indentChar;
+			if ( this.HTML )
+				chr = chr.replace(/\t/g,'   ').replace(/ /g,'&nbsp;');
+			return Array( this._depth_ + (extra||0) ).join(chr);
+		},
+		up:function( a ) {
+			this._depth_ += a || 1;
+		},
+		down:function( a ) {
+			this._depth_ -= a || 1;
+		},
+		setParser:function( name, parser ) {
+			this.parsers[name] = parser;
+		},
+		// The next 3 are exposed so you can use them
+		quote:quote, 
+		literal:literal,
+		join:join,
+		//
+		_depth_: 1,
+		// This is the list of parsers, to modify them, use jsDump.setParser
+		parsers:{
+			window: '[Window]',
+			document: '[Document]',
+			error:'[ERROR]', //when no parser is found, shouldn't happen
+			unknown: '[Unknown]',
+			'null':'null',
+			undefined:'undefined',
+			'function':function( fn ) {
+				var ret = 'function',
+					name = 'name' in fn ? fn.name : (reName.exec(fn)||[])[1];//functions never have name in IE
+				if ( name )
+					ret += ' ' + name;
+				ret += '(';
+				
+				ret = [ ret, this.parse( fn, 'functionArgs' ), '){'].join('');
+				return join( ret, this.parse(fn,'functionCode'), '}' );
+			},
+			array: array,
+			nodelist: array,
+			arguments: array,
+			object:function( map ) {
+				var ret = [ ];
+				this.up();
+				for ( var key in map )
+					ret.push( this.parse(key,'key') + ': ' + this.parse(map[key]) );
+				this.down();
+				return join( '{', ret, '}' );
+			},
+			node:function( node ) {
+				var open = this.HTML ? '&lt;' : '<',
+					close = this.HTML ? '&gt;' : '>';
+					
+				var tag = node.nodeName.toLowerCase(),
+					ret = open + tag;
+					
+				for ( var a in this.DOMAttrs ) {
+					var val = node[this.DOMAttrs[a]];
+					if ( val )
+						ret += ' ' + a + '=' + this.parse( val, 'attribute' );
+				}
+				return ret + close + open + '/' + tag + close;
+			},
+			functionArgs:function( fn ) {//function calls it internally, it's the arguments part of the function
+				var l = fn.length;
+				if ( !l ) return '';				
+				
+				var args = Array(l);
+				while ( l-- )
+					args[l] = String.fromCharCode(97+l);//97 is 'a'
+				return ' ' + args.join(', ') + ' ';
+			},
+			key:quote, //object calls it internally, the key part of an item in a map
+			functionCode:'[code]', //function calls it internally, it's the content of the function
+			attribute:quote, //node calls it internally, it's an html attribute value
+			string:quote,
+			date:quote,
+			regexp:literal, //regex
+			number:literal,
+			'boolean':literal
+		},
+		DOMAttrs:{//attributes to dump from nodes, name=>realName
+			id:'id',
+			name:'name',
+			'class':'className'
+		},
+		HTML:false,//if true, entities are escaped ( <, >, \t, space and \n )
+		indentChar:'   ',//indentation unit
+		multiline:false //if true, items in a collection, are separated by a \n, else just a space.
+	};
+
+	return jsDump;
+})();
+
+})(this);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.html	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,23 @@
+<html>
+  <head>
+    <!-- dependencies -->
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.dom.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.ajax.js" type="text/javascript"></script>
+    <!-- qunit files -->
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <!-- test suite -->
+    <script src="test_ajax.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main"> </div>
+    <h1 id="qunit-header">CubicWeb Ajax Test Suite</h1>
+    <h2 id="qunit-banner"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_ajax.js	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,222 @@
+$(document).ready(function() {
+
+    function jsSources() {
+        return $.map($('head script[src]'), function(script) {
+            return script.getAttribute('src');
+        });
+    }
+
+    test('test simple h1 inclusion (ajax_url0.html)', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('#main h1').html(), 'Hello');
+                start();
+            }
+        });
+    });
+
+    test('test simple html head inclusion (ajax_url1.html)', function() {
+        expect(6);
+        var scriptsIncluded = jsSources();
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url1.html', {
+            callback: function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+                // check that foo.js has been inserted in <head>
+                equals(scriptsIncluded.length, origLength + 1);
+                equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                // check that <div class="ajaxHtmlHead"> has been removed
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('div.ajaxHtmlHead').length, 0);
+                equals(jQuery('#main h1').html(), 'Hello');
+                start();
+            }
+        });
+    });
+
+    test('test addCallback', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function() {
+            equals(jQuery('#main').children().length, 1);
+            equals(jQuery('#main h1').html(), 'Hello');
+            start();
+        });
+    });
+
+    test('test callback after synchronous request', function() {
+        expect(1);
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: './ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        stop();
+        deferred.addCallback(function() {
+            // add an assertion to ensure the callback is executed
+            ok(true, "callback is executed");
+            start();
+        });
+    });
+
+    test('test addCallback with parameters', function() {
+        expect(3);
+        equals(jQuery('#main').children().length, 0);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function(data, req, arg1, arg2) {
+            equals(arg1, 'Hello');
+            equals(arg2, 'world');
+            start();
+        },
+        'Hello', 'world');
+    });
+
+    test('test callback after synchronous request with parameters', function() {
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: './ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        deferred.addCallback(function(data, req, arg1, arg2) {
+            // add an assertion to ensure the callback is executed
+            ok(true, "callback is executed");
+            equals(arg1, 'Hello');
+            equals(arg2, 'world');
+        },
+        'Hello', 'world');
+    });
+
+    test('test addErrback', function() {
+        expect(1);
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html');
+        d.addCallback(function() {
+            // throw an exception to start errback chain
+            throw new Error();
+        });
+        d.addErrback(function() {
+            ok(true, "errback is executed");
+            start();
+        });
+    });
+
+    test('test callback / errback execution order', function() {
+        expect(4);
+        var counter = 0;
+        stop();
+        var d = jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(++counter, 1); // should be executed first
+                start();
+            }
+        });
+        d.addCallback(function() {
+            equals(++counter, 2); // should be executed and break callback chain
+            throw new Error();
+        });
+        d.addCallback(function() {
+            // should not be executed since second callback raised an error
+            ok(false, "callback is executed");
+        });
+        d.addErrback(function() {
+            // should be executed after the second callback
+            equals(++counter, 3);
+        });
+        d.addErrback(function() {
+            // should be executed after the first errback
+            equals(++counter, 4);
+        });
+    });
+
+    test('test already included resources are ignored (ajax_url2.html)', function() {
+        expect(10);
+        var scriptsIncluded = jsSources();
+        equals(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        equals(jQuery('head link').length, 1);
+        equals(jQuery('head link').attr('href'), 'qunit.css');
+        stop();
+        jQuery('#main').loadxhtml('/../ajax_url1.html', {
+            callback: function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+                // check that foo.js has been inserted in <head>
+                equals(scriptsIncluded.length, origLength + 1);
+                equals(scriptsIncluded[origLength].indexOf('http://foo.js'), 0);
+                // check that <div class="ajaxHtmlHead"> has been removed
+                equals(jQuery('#main').children().length, 1);
+                equals(jQuery('div.ajaxHtmlHead').length, 0);
+                equals(jQuery('#main h1').html(), 'Hello');
+                // qunit.css is not added twice
+                equals(jQuery('head link').length, 1);
+                equals(jQuery('head link').attr('href'), 'qunit.css');
+                start();
+            }
+        });
+    });
+
+    test('test synchronous request loadRemote', function() {
+        var res = loadRemote('/../ajaxresult.json', {},
+        'GET', true);
+        same(res, ['foo', 'bar']);
+    });
+
+    test('test event on CubicWeb', function() {
+        expect(1);
+        stop();
+        var events = null;
+        jQuery(CubicWeb).bind('server-response', function() {
+            // check that server-response event on CubicWeb is triggered
+            events = 'CubicWeb';
+        });
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(events, 'CubicWeb');
+                start();
+            }
+        });
+    });
+
+    test('test event on node', function() {
+        expect(3);
+        stop();
+        var nodes = [];
+        jQuery('#main').bind('server-response', function() {
+            nodes.push('node');
+        });
+        jQuery(CubicWeb).bind('server-response', function() {
+            nodes.push('CubicWeb');
+        });
+        jQuery('#main').loadxhtml('/../ajax_url0.html', {
+            callback: function() {
+                equals(nodes.length, 2);
+                // check that server-response event on CubicWeb is triggered
+                // only once and event server-response on node is triggered
+                equals(nodes[0], 'CubicWeb');
+                equals(nodes[1], 'node');
+                start();
+            }
+        });
+    });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_datetime.html	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,18 @@
+<html>
+  <head>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <script src="test_datetime.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">QUnit example</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_datetime.js	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,20 @@
+$(document).ready(function() {
+
+  module("datetime tests");
+
+  test("test full datetime", function() {
+      equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
+	     '1986-04-18 10:30:00');
+  });
+
+  test("test only date", function() {
+      equals(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
+  });
+
+  test("test null", function() {
+      equals(cw.utils.toISOTimestamp(null), null);
+  });
+
+
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_htmlhelpers.html	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,20 @@
+<html>
+  <head>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script type="text/javascript" src="qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="qunit.css" />
+    <script src="test_htmlhelpers.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main">
+    </div>
+    <h1 id="qunit-header">QUnit example</h1>
+    <h2 id="qunit-banner"></h2>
+    <h2 id="qunit-userAgent"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/jstests/test_htmlhelpers.js	Thu Jun 03 14:51:42 2010 +0200
@@ -0,0 +1,36 @@
+$(document).ready(function() {
+
+    module("module2", {
+      setup: function() {
+        $('#main').append('<select id="theselect" multiple="multiple" size="2">' +
+    			'</select>');
+      }
+    });
+  
+    test("test first selected", function() {
+        $('#theselect').append('<option value="foo">foo</option>' +
+    			     '<option selected="selected" value="bar">bar</option>' +
+    			     '<option value="baz">baz</option>' +
+    			     '<option selected="selecetd"value="spam">spam</option>');
+        var selected = firstSelected(document.getElementById("theselect"));
+        equals(selected.value, 'bar');
+    });
+  
+    test("test first selected 2", function() {
+        $('#theselect').append('<option value="foo">foo</option>' +
+    			     '<option value="bar">bar</option>' +
+    			     '<option value="baz">baz</option>' +
+    			     '<option value="spam">spam</option>');
+        var selected = firstSelected(document.getElementById("theselect"));
+        equals(selected, null);
+    });
+  
+    module("visibilty");
+    test('toggleVisibility', function() {
+        $('#main').append('<div id="foo"></div>');
+        toggleVisibility('foo');
+        ok($('#foo').hasClass('hidden'), 'check hidden class is set');
+    });
+
+});
+
--- a/web/views/basecontrollers.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/views/basecontrollers.py	Thu Jun 03 14:51:42 2010 +0200
@@ -262,6 +262,12 @@
             self._cw.encoding)
         return self.response(domid, status, args, entity)
 
+def optional_kwargs(extraargs):
+    if extraargs is None:
+        return {}
+    else: # we receive unicode keys which is not supported by the **syntax
+        return dict((str(key), value)
+                    for key, value in extraargs.items())
 
 class JSonController(Controller):
     __regid__ = 'json'
@@ -421,6 +427,18 @@
         #    raise RemoteCallFailed('unselectable')
         return self._call_view(comp, **extraargs)
 
+    @xhtmlize
+    def js_render(self, registry, oid, eid=None, selectargs=None, renderargs=None):
+        if eid is not None:
+            rset = self._cw.eid_rset(eid)
+        elif 'rql' in self._cw.form:
+            rset = self._cw.execute(self._cw.form['rql'])
+        else:
+            rset = None
+        selectargs = optional_kwargs(selectargs)
+        view = self._cw.vreg[registry].select(oid, self._cw, rset=rset, **selectargs)
+        return self._call_view(view, **optional_kwargs(renderargs))
+
     @check_pageid
     @xhtmlize
     def js_inline_creation_form(self, peid, petype, ttype, rtype, role, i18nctx):
@@ -440,13 +458,13 @@
     @xhtmlize
     def js_reledit_form(self):
         req = self._cw
-        args = dict((x, self._cw.form[x])
+        args = dict((x, req.form[x])
                     for x in frozenset(('rtype', 'role', 'reload', 'landing_zone')))
-        entity = self._cw.entity_from_eid(int(self._cw.form['eid']))
+        entity = req.entity_from_eid(typed_eid(req.form['eid']))
         # note: default is reserved in js land
-        args['default'] = self._cw.form['default_value']
+        args['default'] = req.form['default_value']
         args['reload'] = json.loads(args['reload'])
-        rset = req.eid_rset(int(self._cw.form['eid']))
+        rset = req.eid_rset(typed_eid(req.form['eid']))
         view = req.vreg['views'].select('doreledit', req, rset=rset, rtype=args['rtype'])
         return self._call_view(view, **args)
 
--- a/web/views/bookmark.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/views/bookmark.py	Thu Jun 03 14:51:42 2010 +0200
@@ -96,7 +96,7 @@
         eschema = self._cw.vreg.schema.eschema(self.etype)
         candelete = rschema.has_perm(req, 'delete', toeid=ueid)
         if candelete:
-            req.add_js( ('cubicweb.ajax.js', 'cubicweb.bookmarks.js') )
+            req.add_js('cubicweb.ajax.js')
         else:
             dlink = None
         for bookmark in rset.entities():
--- a/web/views/editforms.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/views/editforms.py	Thu Jun 03 14:51:42 2010 +0200
@@ -283,8 +283,8 @@
     # FIXME editableField class could be toggleable from userprefs
 
     _onclick = u"showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
-    _onsubmit = ("return inlineValidateRelationForm('%(rtype)s', '%(role)s', '%(eid)s', "
-                 "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
+    _onsubmit = ("return inlineValidateRelationFormOptions('%(rtype)s', '%(eid)s', "
+                 "'%(divid)s', %(options)s);")
     _cancelclick = "hideInlineEdit(%s,\'%s\',\'%s\')"
     _defaultlandingzone = (u'<img title="%(msg)s" src="data/pen_icon.png" '
                            'alt="%(msg)s"/>')
@@ -406,9 +406,11 @@
     def _build_args(self, entity, rtype, role, formid, default, reload, lzone,
                     extradata=None):
         divid = '%s-%s-%s' % (rtype, role, entity.eid)
+        options = {'reload' : reload, 'default_value' : default,
+                   'role' : role, 'vid' : '',
+                   'lzone' : lzone}
         event_args = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype,
-                      'reload' : dumps(reload), 'default' : default, 'role' : role, 'vid' : u'',
-                      'lzone' : lzone}
+                      'options' : dumps(options)}
         if extradata:
             event_args.update(extradata)
         return divid, event_args
@@ -416,7 +418,7 @@
     def _build_form(self, entity, rtype, role, formid, default, reload, lzone,
                   extradata=None, **formargs):
         divid, event_args = self._build_args(entity, rtype, role, formid, default,
-                                      reload, lzone, extradata)
+                                             reload, lzone, extradata)
         onsubmit = self._onsubmit % event_args
         cancelclick = self._cancelclick % (entity.eid, rtype, divid)
         form = self._cw.vreg['forms'].select(
@@ -434,8 +436,8 @@
     by checking uicfg configuration and composite relation property.
     """
     __regid__ = 'reledit'
-    _onclick = (u"loadInlineEditionForm(%(eid)s, '%(rtype)s', '%(role)s', "
-                "'%(divid)s', %(reload)s, '%(vid)s', '%(default)s', '%(lzone)s');")
+    _onclick = (u"loadInlineEditionFormOptions(%(eid)s, '%(rtype)s', "
+                "'%(divid)s', %(options)s);")
 
     def should_edit_relation(self, entity, rschema, role, rvid):
         eschema = entity.e_schema
--- a/web/views/formrenderers.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/views/formrenderers.py	Thu Jun 03 14:51:42 2010 +0200
@@ -342,7 +342,7 @@
                 w(u'<th align="left">%s</th>' %
                   tags.input(type='checkbox',
                              title=self._cw._('toggle check boxes'),
-                             onclick="setCheckboxesState('eid', this.checked)"))
+                             onclick="setCheckboxesState('eid', null, this.checked)"))
                 for field in subfields:
                     w(u'<th>%s</th>' % field_label(form, field))
                 w(u'</tr>')
@@ -358,7 +358,7 @@
             entity = form.edited_entity
             values = form.form_previous_values
             qeid = eid_param('eid', entity.eid)
-            cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % \
+            cbsetstate = "setCheckboxesState('eid', %s, 'checked')" % \
                          xml_escape(dumps(entity.eid))
             w(u'<tr class="%s">' % (entity.cw_row % 2 and u'even' or u'odd'))
             # XXX turn this into a widget used on the eid field
--- a/web/views/massmailing.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/views/massmailing.py	Thu Jun 03 14:51:42 2010 +0200
@@ -57,7 +57,7 @@
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
-    needs_js = ('cubicweb.widgets.js', 'cubicweb.massmailing.js')
+    needs_js = ('cubicweb.widgets.js',)
     needs_css = ('cubicweb.mailform.css')
     domid = 'sendmail'
     action = 'sendmail'
@@ -94,7 +94,7 @@
         return sorted(reduce(operator.and_, attrs))
 
     def build_substitutions_help(self):
-        insertLink = u'<a href="javascript: insertText(\'%%(%s)s\', \'emailarea\');">%%(%s)s</a>'
+        insertLink = u'<a href="javascript: cw.widgets.insertText(\'%%(%s)s\', \'emailarea\');">%%(%s)s</a>'
         substs = (u'<div class="substitution">%s</div>' % (insertLink % (subst, subst))
                   for subst in self.get_allowed_substitutions())
         helpmsg = self._cw._('You can use any of the following substitutions in your text')
--- a/web/webconfig.py	Thu Jun 03 10:17:44 2010 +0200
+++ b/web/webconfig.py	Thu Jun 03 14:51:42 2010 +0200
@@ -346,14 +346,17 @@
         for path in reversed([self.apphome] + self.cubes_path()):
             self._load_ui_properties_file(uiprops, path)
         self._load_ui_properties_file(uiprops, self.apphome)
+        datadir_url = uiprops.context['datadir_url']
         # XXX pre 3.9 css compat
         if self['use-old-css']:
-            datadir_url = uiprops.context['datadir_url']
             if (datadir_url+'/cubicweb.css') in uiprops['STYLESHEETS']:
                 idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.css')
                 uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.old.css'
             if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
                 uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
+        cubicweb_js_url = datadir_url + '/cubicweb.js'
+        if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
+            uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
 
     def _load_ui_properties_file(self, uiprops, path):
         resourcesfile = join(path, 'data', 'external_resources')