[ajax js/css] modconcat fix: load code exactly once (auc, fcayre)
authorAurelien Campeas <aurelien.campeas@logilab.fr>
Wed, 25 May 2011 11:28:58 +0200
changeset 7427 06444c1233e0
parent 7423 598a4f051259
child 7433 9aadb5a04b53
[ajax js/css] modconcat fix: load code exactly once (auc, fcayre) * introduces cw.ajax namespace (currently hosts only recent modconcat functionality) * handle load-one/load-many-at-once edge case * properly handle already-loaded js resource * avoid .appendTo to trigger script evaluation since from jquery 1.5 this results in uncached ajax calls
web/data/cubicweb.ajax.js
--- a/web/data/cubicweb.ajax.js	Mon May 23 11:36:43 2011 +0200
+++ b/web/data/cubicweb.ajax.js	Wed May 25 11:28:58 2011 +0200
@@ -22,6 +22,9 @@
  *
  * dummy ultra minimalist implementation of deferred for jQuery
  */
+
+cw.ajax = new Namespace('cw.ajax');
+
 function Deferred() {
     this.__init__(this);
 }
@@ -86,40 +89,64 @@
 
 var JSON_BASE_URL = baseuri() + 'json?';
 
-/**
- * returns true if `url` is a mod_concat-like url
- * (e.g. http://..../data??resource1.js,resource2.js)
- */
-function _modconcatLikeUrl(url) {
-    var base = baseuri();
-    if (!base.endswith('/')) {
-        base += '/';
-    }
-    var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
-    return modconcat_rgx.exec(url);
-}
+
+jQuery.extend(cw.ajax, {
+    /* variant of jquery evalScript with cache: true in ajax call */
+    _evalscript: function ( i, elem ) {
+       if ( elem.src ) {
+           jQuery.ajax({
+               url: elem.src,
+               async: false,
+               cache: true,
+               dataType: "script"
+           });
+       } else {
+           jQuery.globalEval( elem.text || elem.textContent || elem.innerHTML || "" );
+       }
+       if ( elem.parentNode ) {
+           elem.parentNode.removeChild( elem );
+       }
+    },
+
+    evalscripts: function ( scripts ) {
+        if ( scripts.length ) {
+            jQuery.each(scripts, cw.ajax._evalscript);
+        }
+    },
 
-/**
- * decomposes a mod_concat-like url into its corresponding list of
- * resources' urls
- *
- * >>> _listResources('http://foo.com/data/??a.js,b.js,c.js')
- * ['http://foo.com/data/a.js', 'http://foo.com/data/b.js', 'http://foo.com/data/c.js']
- */
-function _listResources(src) {
-    var resources = [];
-    var groups = _modconcatLikeUrl(src);
-    if (groups == null) {
-        resources.push(src);
-    } else {
-        var dataurl = groups[1];
-        $.each(cw.utils.lastOf(groups).split(','),
-               function() {
-                   resources.push(dataurl + this);
-               });
+    /**
+     * returns true if `url` is a mod_concat-like url
+     * (e.g. http://..../data??resource1.js,resource2.js)
+     */
+    _modconcatLikeUrl: function(url) {
+        var base = baseuri();
+        if (!base.endswith('/')) { base += '/'; }
+        var modconcat_rgx = new RegExp('(' + base + 'data/([a-z0-9]+/)?)\\?\\?(.+)');
+        return modconcat_rgx.exec(url);
+    },
+
+    /**
+     * decomposes a mod_concat-like url into its corresponding list of
+     * resources' urls
+     * >>> _listResources('http://foo.com/data/??a.js,b.js,c.js')
+     * ['http://foo.com/data/a.js', 'http://foo.com/data/b.js', 'http://foo.com/data/c.js']
+     */
+    _listResources: function(src) {
+        var resources = [];
+        var groups = cw.ajax._modconcatLikeUrl(src);
+        if (groups == null) {
+            resources.push(src);
+        } else {
+            var dataurl = groups[1];
+            $.each(cw.utils.lastOf(groups).split(','),
+                 function() {
+                     resources.push(dataurl + this);
+                 }
+            );
+        }
+        return resources;
     }
-    return resources;
-}
+});
 
 //============= utility function handling remote calls responses. ==============//
 function _loadAjaxHtmlHead($node, $head, tag, srcattr) {
@@ -129,31 +156,47 @@
         var loaded = cw['loaded_'+srcattr];
         jQuery('head ' + jqtagfilter).each(function(i) {
             // tab1.push.apply(tab1, tab2) <=> tab1 += tab2 (python-wise)
-            loaded.push.apply(loaded, _listResources(this.getAttribute(srcattr)));
+            loaded.push.apply(loaded, cw.ajax._listResources(this.getAttribute(srcattr)));
         });
     } else {
         var loaded = cw['loaded_'+srcattr];
     }
     $node.find(tag).each(function(i) {
-        var srcnode = this;
-        var url = srcnode.getAttribute(srcattr);
+        var $srcnode = jQuery(this);
+        var url = $srcnode.attr(srcattr);
         if (url) {
-            $.each(_listResources(url), function() {
-                var resource = '' + this; // implicit object->string cast
-                if ($.inArray(resource, loaded) == -1) {
-                    // take care to <script> tags: jQuery append method script nodes
-                    // don't appears in the DOM (See comments on
-                    // http://api.jquery.com/append/), which cause undesired
-                    // duplicated load in our case. After trying to use bare DOM api
-                    // to avoid this, we switched to handle a list of already loaded
-                    // stuff ourselves, since bare DOM api gives bug with the
-                    // server-response event, since we loose control on when the
-                    // script is loaded (jQuery load it immediatly).
-                    loaded.push(resource);
-                }
+            /* special handling of <script> tags: script nodes appended by jquery
+             * use uncached ajax calls and do not appear in the DOM
+             * (See comments in response to Syt on // http://api.jquery.com/append/),
+             * which cause undesired duplicated load in our case. We now handle
+             * a list of already loaded resources, since bare DOM api gives bugs with the
+             * server-response event, and we lose control on when the
+             * script is loaded (jQuery loads it immediately). */
+            var resources = cw.ajax._listResources(url);
+            var missingResources = $.grep(resources, function(resource) {
+                return $.inArray(resource, loaded) == -1;
             });
+            loaded.push.apply(loaded, missingResources);
+            if (missingResources.length == 1) {
+                // only one resource missing: build a node with a single resource url
+                // (maybe the browser has it in cache already)
+                $srcnode.attr(srcattr, missingResources[0]);
+            } else if (missingResources.length > 1) {
+                // several resources missing: build a node with a concatenated
+                // resources url
+                var dataurl = cw.ajax._modconcatLikeUrl(url)[1];
+                var missing_path = $.map(missingResources, function(resource) {
+                    return resource.substring(dataurl.length);
+                });
+                $srcnode.attr(srcattr, dataurl + '??' + missing_path.join(','));
+            }
+            // === will work if both arguments are of the same type
+            if ( $srcnode.attr('type') === 'text/javascript' ) {
+                cw.ajax.evalscripts($srcnode);
+            } else {
+                $srcnode.appendTo($head);
+            }
         }
-	jQuery(srcnode).appendTo($head);
     });
     $node.find(jqtagfilter).remove();
 }