web/data/cubicweb.ajax.js
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 /* copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2  * contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3  *
       
     4  * This file is part of CubicWeb.
       
     5  *
       
     6  * CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7  * terms of the GNU Lesser General Public License as published by the Free
       
     8  * Software Foundation, either version 2.1 of the License, or (at your option)
       
     9  * any later version.
       
    10  *
       
    11  * CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13  * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14  * details.
       
    15  *
       
    16  * You should have received a copy of the GNU Lesser General Public License along
       
    17  * with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18  */
       
    19 
       
    20 /**
       
    21  * .. function:: Deferred
       
    22  *
       
    23  * dummy ultra minimalist implementation of deferred for jQuery
       
    24  */
       
    25 
       
    26 cw.ajax = new Namespace('cw.ajax');
       
    27 
       
    28 function Deferred() {
       
    29     this.__init__(this);
       
    30 }
       
    31 
       
    32 jQuery.extend(Deferred.prototype, {
       
    33     __init__: function() {
       
    34         this._onSuccess = [];
       
    35         this._onFailure = [];
       
    36         this._req = null;
       
    37         this._result = null;
       
    38         this._error = null;
       
    39     },
       
    40 
       
    41     addCallback: function(callback) {
       
    42         if (this._req && (this._req.readyState == 4) && this._result) {
       
    43             var args = [this._result, this._req];
       
    44             jQuery.merge(args, cw.utils.sliceList(arguments, 1));
       
    45             callback.apply(null, args);
       
    46         }
       
    47         else {
       
    48             this._onSuccess.push([callback, cw.utils.sliceList(arguments, 1)]);
       
    49         }
       
    50         return this;
       
    51     },
       
    52 
       
    53     addErrback: function(callback) {
       
    54         if (this._req && this._req.readyState == 4 && this._error) {
       
    55             callback.apply(null, [this._error, this._req]);
       
    56         }
       
    57         else {
       
    58             this._onFailure.push([callback, cw.utils.sliceList(arguments, 1)]);
       
    59         }
       
    60         return this;
       
    61     },
       
    62 
       
    63     success: function(result) {
       
    64         this._result = result;
       
    65         for (var i = 0; i < this._onSuccess.length; i++) {
       
    66             var callback = this._onSuccess[i][0];
       
    67             var args = [result, this._req];
       
    68             jQuery.merge(args, this._onSuccess[i][1]);
       
    69             callback.apply(null, args);
       
    70         }
       
    71     },
       
    72 
       
    73     error: function(xhr, status, error) {
       
    74         this._error = error;
       
    75         for (var i = 0; i < this._onFailure.length; i++) {
       
    76             var callback = this._onFailure[i][0];
       
    77             var args = [error, this._req];
       
    78             jQuery.merge(args, this._onFailure[i][1]);
       
    79             if (callback !== undefined)
       
    80                 callback.apply(null, args);
       
    81         }
       
    82     }
       
    83 
       
    84 });
       
    85 
       
    86 var AJAX_PREFIX_URL = 'ajax';
       
    87 var JSON_BASE_URL = BASE_URL + 'json?';
       
    88 var AJAX_BASE_URL = BASE_URL + AJAX_PREFIX_URL + '?';
       
    89 
       
    90 
       
    91 jQuery.extend(cw.ajax, {
       
    92     /* variant of jquery evalScript with cache: true in ajax call */
       
    93     _evalscript: function ( i, elem ) {
       
    94        var src = elem.getAttribute('src');
       
    95        if (src) {
       
    96            jQuery.ajax({
       
    97                url: src,
       
    98                async: false,
       
    99                cache: true,
       
   100                dataType: "script"
       
   101            });
       
   102        } else {
       
   103            jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
       
   104        }
       
   105        if ( elem.parentNode ) {
       
   106            elem.parentNode.removeChild( elem );
       
   107        }
       
   108     },
       
   109 
       
   110     evalscripts: function ( scripts ) {
       
   111         if ( scripts.length ) {
       
   112             jQuery.each(scripts, cw.ajax._evalscript);
       
   113         }
       
   114     },
       
   115 
       
   116     /**
       
   117      * returns true if `url` is a mod_concat-like url
       
   118      * (e.g. http://..../data??resource1.js,resource2.js)
       
   119      */
       
   120     _modconcatLikeUrl: function(url) {
       
   121         var modconcat_rgx = new RegExp('(' + BASE_URL + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
       
   122         return modconcat_rgx.exec(url);
       
   123     },
       
   124 
       
   125     /**
       
   126      * decomposes a mod_concat-like url into its corresponding list of
       
   127      * resources' urls
       
   128      * >>> _listResources('http://foo.com/data/??a.js,b.js,c.js')
       
   129      * ['http://foo.com/data/a.js', 'http://foo.com/data/b.js', 'http://foo.com/data/c.js']
       
   130      */
       
   131     _listResources: function(src) {
       
   132         var resources = [];
       
   133         var groups = cw.ajax._modconcatLikeUrl(src);
       
   134         if (groups == null) {
       
   135             resources.push(src);
       
   136         } else {
       
   137             var dataurl = groups[1];
       
   138             $.each(cw.utils.lastOf(groups).split(','),
       
   139                  function() {
       
   140                      resources.push(dataurl + this);
       
   141                  }
       
   142             );
       
   143         }
       
   144         return resources;
       
   145     },
       
   146 
       
   147     _buildMissingResourcesUrl: function(url, loadedResources) {
       
   148         var resources = cw.ajax._listResources(url);
       
   149         var missingResources = $.grep(resources, function(resource) {
       
   150             return $.inArray(resource, loadedResources) == -1;
       
   151         });
       
   152         cw.utils.extend(loadedResources, missingResources);
       
   153         var missingResourceUrl = null;
       
   154         if (missingResources.length == 1) {
       
   155             // only one resource missing: build a node with a single resource url
       
   156             // (maybe the browser has it in cache already)
       
   157             missingResourceUrl = missingResources[0];
       
   158         } else if (missingResources.length > 1) {
       
   159             // several resources missing: build a node with a concatenated
       
   160             // resources url
       
   161             var dataurl = cw.ajax._modconcatLikeUrl(url)[1];
       
   162             var missing_path = $.map(missingResources, function(resource) {
       
   163                 return resource.substring(dataurl.length);
       
   164             });
       
   165             missingResourceUrl = dataurl + '??' + missing_path.join(',');
       
   166         }
       
   167         return missingResourceUrl;
       
   168     },
       
   169 
       
   170     _loadAjaxStylesheets: function($responseHead, $head) {
       
   171         $responseHead.find('link[href]').each(function(i) {
       
   172             var $srcnode = $(this);
       
   173             var url = $srcnode.attr('href');
       
   174             if (url) {
       
   175                 var missingStylesheetsUrl = cw.ajax._buildMissingResourcesUrl(url, cw.loaded_links);
       
   176                 // compute concat-like url for missing resources and append <link>
       
   177                 // element to $head
       
   178                 if (missingStylesheetsUrl) {
       
   179                     // IE has problems with dynamic CSS insertions. One symptom (among others)
       
   180                     // is a "1 item remaining" message in the status bar. (cf. #2356261)
       
   181                     // document.createStyleSheet needs to be used for this, although it seems
       
   182                     // that IE can't create more than 31 additional stylesheets with
       
   183                     // document.createStyleSheet.
       
   184                     if ($.browser.msie) {
       
   185                         document.createStyleSheet(missingStylesheetsUrl);
       
   186                     } else {
       
   187                         $srcnode.attr('href', missingStylesheetsUrl);
       
   188                         $srcnode.appendTo($head);
       
   189                     }
       
   190                 }
       
   191             }
       
   192         });
       
   193         $responseHead.find('link[href]').remove();
       
   194     },
       
   195 
       
   196     _loadAjaxScripts: function($responseHead, $head) {
       
   197         $responseHead.find('cubicweb\\:script').each(function(i) {
       
   198             var $srcnode = $(this);
       
   199             var url = $srcnode.attr('src');
       
   200             if (url) {
       
   201                 var missingScriptsUrl = cw.ajax._buildMissingResourcesUrl(url, cw.loaded_scripts);
       
   202                 if (missingScriptsUrl) {
       
   203                     $srcnode.attr('src', missingScriptsUrl);
       
   204                     /* special handling of <script> tags: script nodes appended by jquery
       
   205                      * use uncached ajax calls and do not appear in the DOM
       
   206                      * (See comments in response to Syt on // http://api.jquery.com/append/),
       
   207                      * which cause undesired duplicated load in our case. We now handle
       
   208                      * a list of already loaded resources, since bare DOM api gives bugs with the
       
   209                      * server-response event, and we lose control on when the
       
   210                      * script is loaded (jQuery loads it immediately). */
       
   211                     cw.ajax.evalscripts($srcnode);
       
   212                 }
       
   213             } else {
       
   214                 // <script> contains inlined javascript code, node content
       
   215                 // must be evaluated
       
   216     	        jQuery.globalEval($srcnode.text());
       
   217     	    }
       
   218         });
       
   219         $responseHead.find('cubicweb\\:script').remove();
       
   220     }
       
   221 });
       
   222 
       
   223 //============= utility function handling remote calls responses. ==============//
       
   224 /**
       
   225  * .. function:: function loadAjaxHtmlHead(response)
       
   226  *
       
   227  * inspect dom response (as returned by getDomFromResponse), search for
       
   228  * a <div class="ajaxHtmlHead"> node and put its content into the real
       
   229  * document's head.
       
   230  * This enables dynamic css and js loading and is used by replacePageChunk
       
   231  */
       
   232 function loadAjaxHtmlHead(response) {
       
   233     var $head = jQuery('head');
       
   234     var $responseHead = jQuery(response).find('div.ajaxHtmlHead');
       
   235     // no ajaxHtmlHead found, no processing required
       
   236     if (!$responseHead.length) {
       
   237         return response;
       
   238     }
       
   239     cw.ajax._loadAjaxStylesheets($responseHead, $head);
       
   240     cw.ajax._loadAjaxScripts($responseHead, $head);
       
   241     // add any remaining children (e.g. meta)
       
   242     $responseHead.children().appendTo($head);
       
   243     // remove original container, which is now empty
       
   244     $responseHead.remove();
       
   245     // if there was only one actual node in the reponse besides
       
   246     // the ajaxHtmlHead, then remove the wrapper created by
       
   247     // getDomFromResponse() and return this single element
       
   248     // For instance :
       
   249     // 1/ CW returned the following content :
       
   250     //    <div>the-actual-content</div><div class="ajaxHtmlHead">...</div>
       
   251     // 2/ getDomFromReponse() wrapped this into a single DIV to hold everything
       
   252     //    in one, unique, dom element
       
   253     // 3/ now that we've removed the ajaxHtmlHead div, the only
       
   254     //    node left in the wrapper if the 'real' node built by the view,
       
   255     //    we can safely return this node. Otherwise, the view itself
       
   256     //    returned several 'root' nodes and we need to keep the wrapper
       
   257     //    created by getDomFromResponse()
       
   258     if (response.childNodes.length == 1 && response.getAttribute('cubicweb:type') == 'cwResponseWrapper') {
       
   259         return response.firstChild;
       
   260     }
       
   261     return response;
       
   262 }
       
   263 
       
   264 function _postAjaxLoad(node) {
       
   265     // find textareas and wrap them if there are some
       
   266     if (typeof(FCKeditor) != 'undefined') {
       
   267         buildWysiwygEditors();
       
   268     }
       
   269     if (typeof initFacetBoxEvents != 'undefined') {
       
   270         initFacetBoxEvents(node);
       
   271     }
       
   272     if (typeof buildWidgets != 'undefined') {
       
   273         buildWidgets(node);
       
   274     }
       
   275     if (typeof roundedCorners != 'undefined') {
       
   276         roundedCorners(node);
       
   277     }
       
   278     _loadDynamicFragments(node);
       
   279     jQuery(cw).trigger('server-response', [true, node]);
       
   280     jQuery(node).trigger('server-response', [true, node]);
       
   281 }
       
   282 
       
   283 function remoteCallFailed(err, req) {
       
   284     cw.log(err);
       
   285     if (req.status == 500) {
       
   286         updateMessage(err);
       
   287     } else {
       
   288         updateMessage(_("an error occurred while processing your request"));
       
   289     }
       
   290 }
       
   291 
       
   292 //============= base AJAX functions to make remote calls =====================//
       
   293 /**
       
   294  * .. function:: ajaxFuncArgs(fname, form, *args)
       
   295  *
       
   296  * extend `form` parameters to call the js_`fname` function of the json
       
   297  * controller with `args` arguments.
       
   298  */
       
   299 function ajaxFuncArgs(fname, form /* ... */) {
       
   300     form = form || {};
       
   301     $.extend(form, {
       
   302         'fname': fname,
       
   303         'pageid': pageid,
       
   304         'arg': $.map(cw.utils.sliceList(arguments, 2), JSON.stringify)
       
   305     });
       
   306     return form;
       
   307 }
       
   308 
       
   309 /**
       
   310  * .. function:: loadxhtml(url, form, reqtype='get', mode='replace', cursor=false)
       
   311  *
       
   312  * build url given by absolute or relative `url` and `form` parameters
       
   313  * (dictionary), fetch it using `reqtype` method, then evaluate the
       
   314  * returned XHTML and insert it according to `mode` in the
       
   315  * document. Possible modes are :
       
   316  *
       
   317  *    - 'replace' to replace the node's content with the generated HTML
       
   318  *    - 'swap' to replace the node itself with the generated HTML
       
   319  *    - 'append' to append the generated HTML to the node's content
       
   320  *
       
   321  * If `cursor`, turn mouse cursor into 'progress' cursor until the remote call
       
   322  * is back.
       
   323  */
       
   324 jQuery.fn.loadxhtml = function(url, form, reqtype, mode, cursor) {
       
   325     if (this.size() > 1) {
       
   326         cw.log('loadxhtml called with more than one element');
       
   327     } else if (this.size() < 1) {
       
   328         cw.log('loadxhtml called without an element');
       
   329     }
       
   330     var node = this.get(0); // only consider the first element
       
   331     if (cursor) {
       
   332         setProgressCursor();
       
   333     }
       
   334     var d = loadRemote(url, form, reqtype);
       
   335     d.addCallback(function(response) {
       
   336         var domnode = getDomFromResponse(response);
       
   337         domnode = loadAjaxHtmlHead(domnode);
       
   338         mode = mode || 'replace';
       
   339         // make sure the component is visible
       
   340         $(node).removeClass("hidden");
       
   341         if (mode == 'swap') {
       
   342             var origId = node.id;
       
   343             node = cw.swapDOM(node, domnode);
       
   344             if (!node.id) {
       
   345                 node.id = origId;
       
   346             }
       
   347         } else if (mode == 'replace') {
       
   348             jQuery(node).empty().append(domnode);
       
   349         } else if (mode == 'append') {
       
   350             jQuery(node).append(domnode);
       
   351         }
       
   352         _postAjaxLoad(node);
       
   353     });
       
   354     d.addErrback(remoteCallFailed);
       
   355     if (cursor) {
       
   356         d.addCallback(resetCursor);
       
   357         d.addErrback(resetCursor);
       
   358     }
       
   359     return d;
       
   360 }
       
   361 
       
   362 /**
       
   363  * .. function:: loadRemote(url, form, reqtype='POST', sync=false)
       
   364  *
       
   365  * Asynchronously (unless `sync` argument is set to true) load a URL or path
       
   366  * and return a deferred whose callbacks args are decoded according to the
       
   367  * Content-Type response header. `form` should be additional form params
       
   368  * dictionary, `reqtype` the HTTP request type (get 'GET' or 'POST').
       
   369  */
       
   370 function loadRemote(url, form, reqtype, sync) {
       
   371     if (!url.toLowerCase().startswith(BASE_URL.toLowerCase())) {
       
   372         url = BASE_URL + url;
       
   373     }
       
   374     if (!sync) {
       
   375         var deferred = new Deferred();
       
   376         jQuery.ajax({
       
   377             url: url,
       
   378             type: (reqtype || 'POST').toUpperCase(),
       
   379             data: form,
       
   380             traditional: true,
       
   381             async: true,
       
   382 
       
   383             beforeSend: function(xhr) {
       
   384                 deferred._req = xhr;
       
   385             },
       
   386 
       
   387             success: function(data, status) {
       
   388                 deferred.success(data);
       
   389             },
       
   390 
       
   391             error: function(xhr, status, error) {
       
   392                 try {
       
   393                     if (xhr.status == 500) {
       
   394                         var reason_dict = cw.evalJSON(xhr.responseText);
       
   395                         deferred.error(xhr, status, reason_dict['reason']);
       
   396                         return;
       
   397                     }
       
   398                 } catch(exc) {
       
   399                     cw.log('error with server side error report:' + exc);
       
   400                 }
       
   401                 deferred.error(xhr, status, null);
       
   402             }
       
   403         });
       
   404         return deferred;
       
   405     } else {
       
   406         var result;
       
   407         // jQuery.ajax returns the XHR object, even for synchronous requests,
       
   408         // but in that case, the success callback will be called before
       
   409         // jQuery.ajax returns. The first argument of the callback will be
       
   410         // the server result, interpreted by jQuery according to the reponse's
       
   411         // content-type (i.e. json or xml)
       
   412         jQuery.ajax({
       
   413             url: url,
       
   414             type: (reqtype || 'GET').toUpperCase(),
       
   415             data: form,
       
   416             traditional: true,
       
   417             async: false,
       
   418             success: function(res) {
       
   419                 result = res;
       
   420             }
       
   421         });
       
   422         return result;
       
   423     }
       
   424 }
       
   425 
       
   426 //============= higher level AJAX functions using remote calls ===============//
       
   427 
       
   428 var _i18ncache = {};
       
   429 
       
   430 /**
       
   431  * .. function:: _(message)
       
   432  *
       
   433  * emulation of gettext's _ shortcut
       
   434  */
       
   435 function _(message) {
       
   436     var form;
       
   437     if (!(message in _i18ncache)) {
       
   438         form = ajaxFuncArgs('i18n', null, [message]);
       
   439         _i18ncache[message] = loadRemote(AJAX_BASE_URL, form, 'GET', true)[0];
       
   440     }
       
   441     return _i18ncache[message];
       
   442 }
       
   443 
       
   444 /**
       
   445  * .. function:: _loadDynamicFragments(node)
       
   446  *
       
   447  * finds each dynamic fragment in the page and executes the
       
   448  * the associated RQL to build them (Async call)
       
   449  */
       
   450 function _loadDynamicFragments(node) {
       
   451     if (node) {
       
   452         var fragments = jQuery(node).find('div.dynamicFragment');
       
   453     } else {
       
   454         var fragments = jQuery('div.dynamicFragment');
       
   455     }
       
   456     if (fragments.length == 0) {
       
   457         return;
       
   458     }
       
   459     if (typeof LOADING_MSG == 'undefined') {
       
   460         LOADING_MSG = 'loading'; // this is only a safety belt, it should not happen
       
   461     }
       
   462     for (var i = 0; i < fragments.length; i++) {
       
   463         var fragment = fragments[i];
       
   464         fragment.innerHTML = '<h3>' + LOADING_MSG + ' ... <img src="data/loading.gif" /></h3>';
       
   465         var $fragment = jQuery(fragment);
       
   466         // if cubicweb:loadurl is set, just pick the url et send it to loadxhtml
       
   467         var url = $fragment.attr('cubicweb:loadurl');
       
   468         if (url) {
       
   469             $fragment.loadxhtml(url);
       
   470             continue;
       
   471         }
       
   472         // else: rebuild full url by fetching cubicweb:rql, cubicweb:vid, etc.
       
   473         var rql = $fragment.attr('cubicweb:rql');
       
   474         var items = $fragment.attr('cubicweb:vid').split('&');
       
   475         var vid = items[0];
       
   476         var extraparams = {};
       
   477         // case where vid='myvid&param1=val1&param2=val2': this is a deprecated abuse-case
       
   478         if (items.length > 1) {
       
   479             cw.log("[3.5] you're using extraargs in cubicweb:vid " +
       
   480                    "attribute, this is deprecated, consider using " +
       
   481                    "loadurl instead");
       
   482             for (var j = 1; j < items.length; j++) {
       
   483                 var keyvalue = items[j].split('=');
       
   484                 extraparams[keyvalue[0]] = keyvalue[1];
       
   485             }
       
   486         }
       
   487         var actrql = $fragment.attr('cubicweb:actualrql');
       
   488         if (actrql) {
       
   489             extraparams['actualrql'] = actrql;
       
   490         }
       
   491         var fbvid = $fragment.attr('cubicweb:fallbackvid');
       
   492         if (fbvid) {
       
   493             extraparams['fallbackvid'] = fbvid;
       
   494         }
       
   495         extraparams['rql'] = rql;
       
   496         extraparams['vid'] = vid;
       
   497         $fragment.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('view', extraparams));
       
   498     }
       
   499 }
       
   500 function unloadPageData() {
       
   501     // NOTE: do not make async calls on unload if you want to avoid
       
   502     //       strange bugs
       
   503     loadRemote(AJAX_BASE_URL, ajaxFuncArgs('unload_page_data'), 'POST', true);
       
   504 }
       
   505 
       
   506 function removeBookmark(beid) {
       
   507     var d = loadRemote(AJAX_BASE_URL, ajaxFuncArgs('delete_bookmark', null, beid));
       
   508     d.addCallback(function(boxcontent) {
       
   509         $('#bookmarks_box').loadxhtml(AJAX_BASE_URL,
       
   510                                       ajaxFuncArgs('render', null, 'ctxcomponents',
       
   511                                                    'bookmarks_box'),
       
   512                                       null, 'swap');
       
   513         document.location.hash = '#header';
       
   514         updateMessage(_("bookmark has been removed"));
       
   515     });
       
   516 }
       
   517 
       
   518 
       
   519 //============= XXX move those functions? ====================================//
       
   520 function openHash() {
       
   521     if (document.location.hash) {
       
   522         var nid = document.location.hash.replace('#', '');
       
   523         var node = jQuery('#' + nid);
       
   524         if (node) {
       
   525             $(node).removeClass("hidden");
       
   526         }
       
   527     };
       
   528 }
       
   529 jQuery(document).ready(openHash);
       
   530 
       
   531 /**
       
   532  * .. function:: buildWysiwygEditors(parent)
       
   533  *
       
   534  *XXX: this function should go in edition.js but as for now, htmlReplace
       
   535  * references it.
       
   536  *
       
   537  * replace all textareas with fckeditors.
       
   538  */
       
   539 function buildWysiwygEditors(parent) {
       
   540     jQuery('textarea').each(function() {
       
   541         if (this.getAttribute('cubicweb:type') == 'wysiwyg') {
       
   542             // mark editor as instanciated, we may be called a number of times
       
   543             // (see _postAjaxLoad)
       
   544             this.setAttribute('cubicweb:type', 'fckeditor');
       
   545             if (typeof FCKeditor != "undefined") {
       
   546                 var fck = new FCKeditor(this.id);
       
   547                 fck.Config['CustomConfigurationsPath'] = fckconfigpath;
       
   548                 fck.Config['DefaultLanguage'] = fcklang;
       
   549                 fck.BasePath = BASE_URL + "fckeditor/";
       
   550                 fck.ReplaceTextarea();
       
   551             } else {
       
   552                 cw.log('fckeditor could not be found.');
       
   553             }
       
   554         }
       
   555     });
       
   556 }
       
   557 jQuery(document).ready(buildWysiwygEditors);
       
   558 
       
   559 /**
       
   560  * .. function:: stripEmptyTextNodes(nodelist)
       
   561  *
       
   562  * takes a list of DOM nodes and removes all empty text nodes
       
   563  */
       
   564 function stripEmptyTextNodes(nodelist) {
       
   565     /* this DROPS empty text nodes */
       
   566     var stripped = [];
       
   567     for (var i = 0; i < nodelist.length; i++) {
       
   568         var node = nodelist[i];
       
   569         if (isTextNode(node)) {
       
   570             /* all browsers but FF -> innerText, FF -> textContent  */
       
   571             var text = node.innerText || node.textContent;
       
   572             if (text && ! text.strip()) {
       
   573                 continue;
       
   574             }
       
   575         } else {
       
   576             stripped.push(node);
       
   577         }
       
   578     }
       
   579     return stripped;
       
   580 }
       
   581 
       
   582 /**
       
   583  * .. function:: getDomFromResponse(response)
       
   584  *
       
   585  * convenience function that returns a DOM node based on req's result.
       
   586  * XXX clarify the need to clone
       
   587  * */
       
   588 function getDomFromResponse(response) {
       
   589     if (typeof(response) == 'string') {
       
   590         var doc = html2dom(response);
       
   591     } else {
       
   592         var doc = response.documentElement;
       
   593     }
       
   594     var children = doc.childNodes;
       
   595     if (!children.length) {
       
   596         // no child (error cases) => return the whole document
       
   597         return jQuery(doc).clone().context;
       
   598     }
       
   599     children = stripEmptyTextNodes(children);
       
   600     if (children.length == 1) {
       
   601         // only one child => return it
       
   602         return jQuery(children[0]).clone().context;
       
   603     }
       
   604     // several children => wrap them in a single node and return the wrap
       
   605     return DIV({'cubicweb:type': "cwResponseWrapper"},
       
   606                $.map(children, function(node) {
       
   607                        return jQuery(node).clone().context;})
       
   608                );
       
   609 }
       
   610 
       
   611 /* High-level functions *******************************************************/
       
   612 
       
   613 /**
       
   614  * .. function:: reloadCtxComponentsSection(context, actualEid, creationEid=None)
       
   615  *
       
   616  * reload all components in the section for a given `context`.
       
   617  *
       
   618  * This is necessary for cases where the parent entity (on which the section
       
   619  * apply) has been created during post, hence the section has to be reloaded to
       
   620  * consider its new eid, hence the two additional arguments `actualEid` and
       
   621  * `creationEid`: `actualEid` is the eid of newly created top level entity and
       
   622  * `creationEid` the fake eid that was given as form creation marker (e.g. A).
       
   623  *
       
   624  * You can still call this function with only the actual eid if you're not in
       
   625  * such creation case.
       
   626  */
       
   627 function reloadCtxComponentsSection(context, actualEid, creationEid) {
       
   628     // in this case, actualEid is the eid of newly created top level entity and
       
   629     // creationEid the fake eid given as form creation marker (e.g. A)
       
   630     if (!creationEid) { creationEid = actualEid ; }
       
   631     var $compsholder = $('#' + context + creationEid);
       
   632     // reload the whole components section
       
   633     $compsholder.children().each(function (index) {
       
   634 	// XXX this.id[:-len(eid)]
       
   635 	var compid = this.id.replace("_", ".").rstrip(creationEid);
       
   636 	var params = ajaxFuncArgs('render', null, 'ctxcomponents',
       
   637 				  compid, actualEid);
       
   638 	$(this).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
       
   639     });
       
   640     $compsholder.attr('id', context + actualEid);
       
   641 }
       
   642 
       
   643 
       
   644 /**
       
   645  * .. function:: reload(domid, compid, registry, formparams, *render_args)
       
   646  *
       
   647  * `js_render` based reloading of views and components.
       
   648  */
       
   649 function reload(domid, compid, registry, formparams  /* ... */) {
       
   650     var ajaxArgs = ['render', formparams, registry, compid];
       
   651     ajaxArgs = ajaxArgs.concat(cw.utils.sliceList(arguments, 4));
       
   652     var params = ajaxFuncArgs.apply(null, ajaxArgs);
       
   653     return $('#'+domid).loadxhtml(AJAX_BASE_URL, params, null, 'swap', true);
       
   654 }
       
   655 
       
   656 /* ajax tabs ******************************************************************/
       
   657 
       
   658 function setTab(tabname, cookiename) {
       
   659     // set appropriate cookie
       
   660     jQuery.cookie(cookiename, tabname, {path: '/'});
       
   661     // trigger show + tabname event
       
   662     triggerLoad(tabname);
       
   663 }
       
   664 
       
   665 function loadNow(eltsel, holesel, reloadable) {
       
   666     var lazydiv = jQuery(eltsel);
       
   667     var hole = lazydiv.children(holesel);
       
   668     hole.show();
       
   669     if ((hole.length == 0) && ! reloadable) {
       
   670         /* the hole is already filed */
       
   671         return;
       
   672     }
       
   673     lazydiv.loadxhtml(lazydiv.attr('cubicweb:loadurl'), {'pageid': pageid});
       
   674 }
       
   675 
       
   676 function triggerLoad(divid) {
       
   677     jQuery('#lazy-' + divid).trigger('load_' + divid);
       
   678 }
       
   679 
       
   680 /* DEPRECATED *****************************************************************/
       
   681 
       
   682 // still used in cwo and keyword cubes at least
       
   683 reloadComponent = cw.utils.deprecatedFunction(
       
   684     '[3.9] reloadComponent() is deprecated, use loadxhtml instead',
       
   685     function(compid, rql, registry, nodeid, extraargs) {
       
   686         registry = registry || 'components';
       
   687         rql = rql || '';
       
   688         nodeid = nodeid || (compid + 'Component');
       
   689         extraargs = extraargs || {};
       
   690         var node = cw.jqNode(nodeid);
       
   691         return node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('component', null, compid,
       
   692                                                           rql, registry, extraargs));
       
   693     }
       
   694 );
       
   695 
       
   696 
       
   697 function remoteExec(fname /* ... */) {
       
   698     setProgressCursor();
       
   699     var props = {
       
   700         fname: fname,
       
   701         pageid: pageid,
       
   702         arg: $.map(cw.utils.sliceList(arguments, 1), JSON.stringify)
       
   703     };
       
   704     var result = jQuery.ajax({
       
   705         url: AJAX_BASE_URL,
       
   706         data: props,
       
   707         async: false,
       
   708         traditional: true
       
   709     }).responseText;
       
   710     if (result) {
       
   711         result = cw.evalJSON(result);
       
   712     }
       
   713     resetCursor();
       
   714     return result;
       
   715 }
       
   716 
       
   717 function asyncRemoteExec(fname /* ... */) {
       
   718     setProgressCursor();
       
   719     var props = {
       
   720         fname: fname,
       
   721         pageid: pageid,
       
   722         arg: $.map(cw.utils.sliceList(arguments, 1), JSON.stringify)
       
   723     };
       
   724     // XXX we should inline the content of loadRemote here
       
   725     var deferred = loadRemote(AJAX_BASE_URL, props, 'POST');
       
   726     deferred = deferred.addErrback(remoteCallFailed);
       
   727     deferred = deferred.addErrback(resetCursor);
       
   728     deferred = deferred.addCallback(resetCursor);
       
   729     return deferred;
       
   730 }
       
   731 
       
   732 jQuery(document).ready(function() {
       
   733     _loadDynamicFragments();
       
   734     // build loaded_scripts / loaded_links lists
       
   735     cw.loaded_scripts = [];
       
   736     jQuery('head script[src]').each(function(i) {
       
   737         cw.utils.extend(cw.loaded_scripts, cw.ajax._listResources(this.getAttribute('src')));
       
   738     });
       
   739     cw.loaded_links = [];
       
   740     jQuery('head link[href]').each(function(i) {
       
   741         cw.utils.extend(cw.loaded_links, cw.ajax._listResources(this.getAttribute('href')));
       
   742     });
       
   743 });