renderjs.js 31.4 KB
Newer Older
1
/*! RenderJs */
2
/*jslint nomen: true*/
3

Ivan Tyagov's avatar
Ivan Tyagov committed
4
/*
5
 * renderJs - Generic Gadget library renderer.
Ivan Tyagov's avatar
Ivan Tyagov committed
6 7
 * http://www.renderjs.org/documentation
 */
8
(function (document, window, RSVP, DOMParser, Channel, undefined) {
9
  "use strict";
10 11 12 13

  var gadget_model_dict = {},
    javascript_registration_dict = {},
    stylesheet_registration_dict = {},
Romain Courteaud's avatar
Romain Courteaud committed
14 15 16
    gadget_loading_klass,
    loading_gadget_promise,
    renderJS;
17

18 19 20
  /////////////////////////////////////////////////////////////////
  // RenderJSGadget
  /////////////////////////////////////////////////////////////////
21 22 23 24 25
  function RenderJSGadget() {
    if (!(this instanceof RenderJSGadget)) {
      return new RenderJSGadget();
    }
  }
26 27 28 29 30 31
  RenderJSGadget.prototype.__title = "";
  RenderJSGadget.prototype.__interface_list = [];
  RenderJSGadget.prototype.__path = "";
  RenderJSGadget.prototype.__html = "";
  RenderJSGadget.prototype.__required_css_list = [];
  RenderJSGadget.prototype.__required_js_list = [];
32

33
  function clearGadgetInternalParameters(g) {
34
    g.__sub_gadget_dict = {};
35 36
  }

37
  RenderJSGadget.__ready_list = [clearGadgetInternalParameters];
Romain Courteaud's avatar
Romain Courteaud committed
38
  RenderJSGadget.ready = function (callback) {
39
    this.__ready_list.push(callback);
Romain Courteaud's avatar
Romain Courteaud committed
40
    return this;
Romain Courteaud's avatar
Romain Courteaud committed
41 42
  };

43 44 45
  /////////////////////////////////////////////////////////////////
  // RenderJSGadget.declareMethod
  /////////////////////////////////////////////////////////////////
Romain Courteaud's avatar
Romain Courteaud committed
46 47
  RenderJSGadget.declareMethod = function (name, callback) {
    this.prototype[name] = function () {
48 49 50 51 52 53
      var context = this,
        argument_list = arguments;

      return new RSVP.Queue()
        .push(function () {
          return callback.apply(context, argument_list);
Romain Courteaud's avatar
Romain Courteaud committed
54
        });
Romain Courteaud's avatar
Romain Courteaud committed
55 56 57
    };
    // Allow chain
    return this;
58 59
  };

Romain Courteaud's avatar
Romain Courteaud committed
60
  RenderJSGadget
Romain Courteaud's avatar
Romain Courteaud committed
61 62
    .declareMethod('getInterfaceList', function () {
      // Returns the list of gadget prototype
63
      return this.__interface_list;
Romain Courteaud's avatar
Romain Courteaud committed
64 65 66
    })
    .declareMethod('getRequiredCSSList', function () {
      // Returns a list of CSS required by the gadget
67
      return this.__required_css_list;
Romain Courteaud's avatar
Romain Courteaud committed
68 69 70
    })
    .declareMethod('getRequiredJSList', function () {
      // Returns a list of JS required by the gadget
71
      return this.__required_js_list;
Romain Courteaud's avatar
Romain Courteaud committed
72 73 74
    })
    .declareMethod('getPath', function () {
      // Returns the path of the code of a gadget
75
      return this.__path;
Romain Courteaud's avatar
Romain Courteaud committed
76 77 78
    })
    .declareMethod('getTitle', function () {
      // Returns the title of a gadget
79
      return this.__title;
Romain Courteaud's avatar
Romain Courteaud committed
80
    })
81 82
    .declareMethod('getElement', function () {
      // Returns the DOM Element of a gadget
83
      if (this.__element === undefined) {
84 85
        throw new Error("No element defined");
      }
86
      return this.__element;
87 88
    });

Romain Courteaud's avatar
Romain Courteaud committed
89 90 91
  /////////////////////////////////////////////////////////////////
  // RenderJSGadget.declareAcquiredMethod
  /////////////////////////////////////////////////////////////////
92 93 94 95 96 97 98 99 100 101 102 103
  function acquire(child_gadget, method_name, argument_list) {
    var gadget = this,
      key,
      gadget_scope;

    for (key in gadget.__sub_gadget_dict) {
      if (gadget.__sub_gadget_dict.hasOwnProperty(key)) {
        if (gadget.__sub_gadget_dict[key] === child_gadget) {
          gadget_scope = key;
        }
      }
    }
Romain Courteaud's avatar
Romain Courteaud committed
104 105
    return new RSVP.Queue()
      .push(function () {
106 107 108 109 110
        // Do not specify default __acquired_method_dict on prototype
        // to prevent modifying this default value (with
        // allowPublicAcquiredMethod for example)
        var aq_dict = gadget.__acquired_method_dict || {};
        if (aq_dict.hasOwnProperty(method_name)) {
111 112
          return aq_dict[method_name].apply(gadget,
                                            [argument_list, gadget_scope]);
Romain Courteaud's avatar
Romain Courteaud committed
113 114 115 116 117
        }
        throw new renderJS.AcquisitionError("aq_dynamic is not defined");
      })
      .push(undefined, function (error) {
        if (error instanceof renderJS.AcquisitionError) {
118
          return gadget.__aq_parent(method_name, argument_list);
Romain Courteaud's avatar
Romain Courteaud committed
119 120 121 122 123 124 125 126
        }
        throw error;
      });
  }

  RenderJSGadget.declareAcquiredMethod =
    function (name, method_name_to_acquire) {
      this.prototype[name] = function () {
Romain Courteaud's avatar
Romain Courteaud committed
127 128 129 130 131 132
        var argument_list = Array.prototype.slice.call(arguments, 0),
          gadget = this;
        return new RSVP.Queue()
          .push(function () {
            return gadget.__aq_parent(method_name_to_acquire, argument_list);
          });
Romain Courteaud's avatar
Romain Courteaud committed
133 134 135 136 137 138
      };

      // Allow chain
      return this;
    };

139
  /////////////////////////////////////////////////////////////////
140
  // RenderJSGadget.allowPublicAcquisition
141 142 143 144 145 146 147 148 149
  /////////////////////////////////////////////////////////////////
  RenderJSGadget.allowPublicAcquisition =
    function (method_name, callback) {
      this.prototype.__acquired_method_dict[method_name] = callback;

      // Allow chain
      return this;
    };

150 151 152 153 154 155 156 157
  // Set aq_parent on gadget_instance which call acquire on parent_gadget
  function setAqParent(gadget_instance, parent_gadget) {
    gadget_instance.__aq_parent = function (method_name, argument_list) {
      return acquire.apply(parent_gadget, [gadget_instance, method_name,
                                           argument_list]);
    };
  }

158 159 160 161 162 163 164
  /////////////////////////////////////////////////////////////////
  // RenderJSEmbeddedGadget
  /////////////////////////////////////////////////////////////////
  // Class inheritance
  function RenderJSEmbeddedGadget() {
    if (!(this instanceof RenderJSEmbeddedGadget)) {
      return new RenderJSEmbeddedGadget();
165
    }
166 167
    RenderJSGadget.call(this);
  }
168
  RenderJSEmbeddedGadget.__ready_list = RenderJSGadget.__ready_list.slice();
169 170 171 172 173 174 175 176
  RenderJSEmbeddedGadget.ready =
    RenderJSGadget.ready;
  RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
  RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;

  /////////////////////////////////////////////////////////////////
  // privateDeclarePublicGadget
  /////////////////////////////////////////////////////////////////
177
  function privateDeclarePublicGadget(url, options, parent_gadget) {
178
    var gadget_instance;
179 180
    if (options.element === undefined) {
      options.element = document.createElement("div");
181
    }
182 183 184 185 186 187 188

    function loadDependency(method, url) {
      return function () {
        return method(url);
      };
    }

189
    return new RSVP.Queue()
190 191 192
      .push(function () {
        return renderJS.declareGadgetKlass(url);
      })
193
      // Get the gadget class and instanciate it
194 195
      .push(function (Klass) {
        var i,
196
          template_node_list = Klass.__template_element.body.childNodes;
197 198
        gadget_loading_klass = Klass;
        gadget_instance = new Klass();
199
        gadget_instance.__element = options.element;
200
        for (i = 0; i < template_node_list.length; i += 1) {
201
          gadget_instance.__element.appendChild(
202 203
            template_node_list[i].cloneNode(true)
          );
204
        }
205
        setAqParent(gadget_instance, parent_gadget);
206
        // Load dependencies if needed
207 208 209 210
        return RSVP.all([
          gadget_instance.getRequiredJSList(),
          gadget_instance.getRequiredCSSList()
        ]);
211
      })
212 213
      // Load all JS/CSS
      .push(function (all_list) {
214
        var q = new RSVP.Queue(),
215 216 217
          i;
        // Load JS
        for (i = 0; i < all_list[0].length; i += 1) {
218
          q.push(loadDependency(renderJS.declareJS, all_list[0][i]));
219 220 221
        }
        // Load CSS
        for (i = 0; i < all_list[1].length; i += 1) {
222
          q.push(loadDependency(renderJS.declareCSS, all_list[1][i]));
223
        }
224
        return q;
225
      })
226
      .push(function () {
227 228 229 230 231 232 233 234 235 236 237 238 239
        return gadget_instance;
      });
  }

  /////////////////////////////////////////////////////////////////
  // RenderJSIframeGadget
  /////////////////////////////////////////////////////////////////
  function RenderJSIframeGadget() {
    if (!(this instanceof RenderJSIframeGadget)) {
      return new RenderJSIframeGadget();
    }
    RenderJSGadget.call(this);
  }
240
  RenderJSIframeGadget.__ready_list = RenderJSGadget.__ready_list.slice();
241 242 243 244 245 246 247 248
  RenderJSIframeGadget.ready =
    RenderJSGadget.ready;
  RenderJSIframeGadget.prototype = new RenderJSGadget();
  RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;

  /////////////////////////////////////////////////////////////////
  // privateDeclareIframeGadget
  /////////////////////////////////////////////////////////////////
249
  function privateDeclareIframeGadget(url, options, parent_gadget) {
250 251 252 253 254
    var gadget_instance,
      iframe,
      node,
      iframe_loading_deferred = RSVP.defer();
    if (options.element === undefined) {
255 256
      throw new Error("DOM element is required to create Iframe Gadget " +
                      url);
257 258 259 260 261 262 263 264 265 266 267
    }

    // Check if the element is attached to the DOM
    node = options.element.parentNode;
    while (node !== null) {
      if (node === document) {
        break;
      }
      node = node.parentNode;
    }
    if (node === null) {
268 269
      throw new Error("The parent element is not attached to the DOM for " +
                      url);
270 271 272
    }

    gadget_instance = new RenderJSIframeGadget();
273
    setAqParent(gadget_instance, parent_gadget);
274 275 276
    iframe = document.createElement("iframe");
//    gadget_instance.element.setAttribute("seamless", "seamless");
    iframe.setAttribute("src", url);
277 278
    gadget_instance.__path = url;
    gadget_instance.__element = options.element;
279 280 281 282 283 284
    // Attach it to the DOM
    options.element.appendChild(iframe);

    // XXX Manage unbind when deleting the gadget

    // Create the communication channel with the iframe
285
    gadget_instance.__chan = Channel.build({
286 287 288 289 290 291
      window: iframe.contentWindow,
      origin: "*",
      scope: "renderJS"
    });

    // Create new method from the declareMethod call inside the iframe
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
    gadget_instance.__chan.bind("declareMethod",
                                function (trans, method_name) {
        gadget_instance[method_name] = function () {
          var argument_list = arguments;
          return new RSVP.Promise(function (resolve, reject) {
            gadget_instance.__chan.call({
              method: "methodCall",
              params: [
                method_name,
                Array.prototype.slice.call(argument_list, 0)],
              success: function (s) {
                resolve(s);
              },
              error: function (e) {
                reject(e);
              }
            });
309
          });
310 311 312
        };
        return "OK";
      });
313 314

    // Wait for the iframe to be loaded before continuing
315
    gadget_instance.__chan.bind("ready", function (trans) {
316 317 318
      iframe_loading_deferred.resolve(gadget_instance);
      return "OK";
    });
319
    gadget_instance.__chan.bind("failed", function (trans, params) {
320 321 322
      iframe_loading_deferred.reject(params);
      return "OK";
    });
323
    gadget_instance.__chan.bind("acquire", function (trans, params) {
Romain Courteaud's avatar
Romain Courteaud committed
324
      gadget_instance.__aq_parent.apply(gadget_instance, params)
325 326 327 328 329 330 331 332
        .then(function (g) {
          trans.complete(g);
        }).fail(function (e) {
          trans.error(e.toString());
        });
      trans.delayReturn(true);
    });

333 334 335 336 337 338 339 340 341 342 343
    return RSVP.any([
      iframe_loading_deferred.promise,
      // Timeout to prevent non renderJS embeddable gadget
      // XXX Maybe using iframe.onload/onerror would be safer?
      RSVP.timeout(5000)
    ]);
  }

  /////////////////////////////////////////////////////////////////
  // RenderJSGadget.declareGadget
  /////////////////////////////////////////////////////////////////
344 345 346 347 348
  RenderJSGadget
    .declareMethod('declareGadget', function (url, options) {
      var queue,
        parent_gadget = this,
        previous_loading_gadget_promise = loading_gadget_promise;
349

350 351 352 353 354 355
      if (options === undefined) {
        options = {};
      }
      if (options.sandbox === undefined) {
        options.sandbox = "public";
      }
356

357 358
      // transform url to absolute url if it is relative
      url = renderJS.getAbsoluteURL(url, this.__path);
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
      // Change the global variable to update the loading queue
      queue = new RSVP.Queue()
        // Wait for previous gadget loading to finish first
        .push(function () {
          return previous_loading_gadget_promise;
        })
        .push(undefined, function () {
          // Forget previous declareGadget error
          return;
        })
        .push(function () {
          var method;
          if (options.sandbox === "public") {
            method = privateDeclarePublicGadget;
          } else if (options.sandbox === "iframe") {
            method = privateDeclareIframeGadget;
          } else {
            throw new Error("Unsupported sandbox options '" +
                            options.sandbox + "'");
          }
379
          return method(url, options, parent_gadget);
380 381 382 383 384 385 386 387 388 389
        })
        // Set the HTML context
        .push(function (gadget_instance) {
          var i;
          // Drop the current loading klass info used by selector
          gadget_loading_klass = undefined;
          // Trigger calling of all ready callback
          function ready_wrapper() {
            return gadget_instance;
          }
390
          for (i = 0; i < gadget_instance.constructor.__ready_list.length;
391 392
               i += 1) {
            // Put a timeout?
393
            queue.push(gadget_instance.constructor.__ready_list[i]);
394 395 396
            // Always return the gadget instance after ready function
            queue.push(ready_wrapper);
          }
397

398 399
          // Store local reference to the gadget instance
          if (options.scope !== undefined) {
400
            parent_gadget.__sub_gadget_dict[options.scope] = gadget_instance;
401 402 403 404 405 406 407 408 409 410 411 412
          }
          return gadget_instance;
        })
        .push(undefined, function (e) {
          // Drop the current loading klass info used by selector
          // even in case of error
          gadget_loading_klass = undefined;
          throw e;
        });
      loading_gadget_promise = queue;
      return loading_gadget_promise;
    })
413
    .declareMethod('getDeclaredGadget', function (gadget_scope) {
414
      if (!this.__sub_gadget_dict.hasOwnProperty(gadget_scope)) {
415 416
        throw new Error("Gadget scope '" + gadget_scope + "' is not known.");
      }
417
      return this.__sub_gadget_dict[gadget_scope];
418 419
    })
    .declareMethod('dropGadget', function (gadget_scope) {
420
      if (!this.__sub_gadget_dict.hasOwnProperty(gadget_scope)) {
421 422 423
        throw new Error("Gadget scope '" + gadget_scope + "' is not known.");
      }
      // http://perfectionkills.com/understanding-delete/
424
      delete this.__sub_gadget_dict[gadget_scope];
425
    });
Romain Courteaud's avatar
Romain Courteaud committed
426

427 428 429
  /////////////////////////////////////////////////////////////////
  // renderJS selector
  /////////////////////////////////////////////////////////////////
430
  renderJS = function (selector) {
Romain Courteaud's avatar
Romain Courteaud committed
431 432
    var result;
    if (selector === window) {
433
      // window is the 'this' value when loading a javascript file
Romain Courteaud's avatar
Romain Courteaud committed
434 435 436 437 438 439 440
      // In this case, use the current loading gadget constructor
      result = gadget_loading_klass;
    }
    if (result === undefined) {
      throw new Error("Unknown selector '" + selector + "'");
    }
    return result;
441 442
  };

443 444 445 446 447 448 449 450 451 452 453 454 455 456
  /////////////////////////////////////////////////////////////////
  // renderJS.AcquisitionError
  /////////////////////////////////////////////////////////////////
  renderJS.AcquisitionError = function (message) {
    this.name = "AcquisitionError";
    if ((message !== undefined) && (typeof message !== "string")) {
      throw new TypeError('You must pass a string.');
    }
    this.message = message || "Acquisition failed";
  };
  renderJS.AcquisitionError.prototype = new Error();
  renderJS.AcquisitionError.prototype.constructor =
    renderJS.AcquisitionError;

457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477
  /////////////////////////////////////////////////////////////////
  // renderJS.getAbsoluteURL
  /////////////////////////////////////////////////////////////////
  renderJS.getAbsoluteURL = function (url, base_url) {
    var doc, base, link,
      html = "<!doctype><html><head></head></html>",
      isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i');

    if (url && base_url && !isAbsoluteOrDataURL.test(url)) {
      doc = (new DOMParser()).parseFromString(html, 'text/html');
      base = doc.createElement('base');
      link = doc.createElement('link');
      doc.head.appendChild(base);
      doc.head.appendChild(link);
      base.href = base_url;
      link.href = url;
      return link.href;
    }
    return url;
  };

478 479 480
  /////////////////////////////////////////////////////////////////
  // renderJS.declareJS
  /////////////////////////////////////////////////////////////////
481
  renderJS.declareJS = function (url) {
482 483 484
    // Prevent infinite recursion if loading render.js
    // more than once
    var result;
485
    if (javascript_registration_dict.hasOwnProperty(url)) {
486
      result = RSVP.resolve();
487
    } else {
488 489 490 491 492 493 494 495 496 497 498 499 500
      result = new RSVP.Promise(function (resolve, reject) {
        var newScript;
        newScript = document.createElement('script');
        newScript.type = 'text/javascript';
        newScript.src = url;
        newScript.onload = function () {
          javascript_registration_dict[url] = null;
          resolve();
        };
        newScript.onerror = function (e) {
          reject(e);
        };
        document.head.appendChild(newScript);
501 502
      });
    }
503
    return result;
504 505
  };

506 507 508
  /////////////////////////////////////////////////////////////////
  // renderJS.declareCSS
  /////////////////////////////////////////////////////////////////
509 510
  renderJS.declareCSS = function (url) {
    // https://github.com/furf/jquery-getCSS/blob/master/jquery.getCSS.js
511 512 513
    // No way to cleanly check if a css has been loaded
    // So, always resolve the promise...
    // http://requirejs.org/docs/faq-advanced.html#css
514
    var result;
515
    if (stylesheet_registration_dict.hasOwnProperty(url)) {
516
      result = RSVP.resolve();
517
    } else {
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
      result = new RSVP.Promise(function (resolve, reject) {
        var link;
        link = document.createElement('link');
        link.rel = 'stylesheet';
        link.type = 'text/css';
        link.href = url;
        link.onload = function () {
          stylesheet_registration_dict[url] = null;
          resolve();
        };
        link.onerror = function (e) {
          reject(e);
        };
        document.head.appendChild(link);
      });
    }
    return result;
  };
536

537 538 539
  /////////////////////////////////////////////////////////////////
  // renderJS.declareGadgetKlass
  /////////////////////////////////////////////////////////////////
540 541 542 543 544 545 546 547 548 549 550 551 552
  renderJS.declareGadgetKlass = function (url) {
    var result,
      xhr;

    function parse() {
      var tmp_constructor,
        key,
        parsed_html;
      if (!gadget_model_dict.hasOwnProperty(url)) {
        // Class inheritance
        tmp_constructor = function () {
          RenderJSGadget.call(this);
        };
553
        tmp_constructor.__ready_list = RenderJSGadget.__ready_list.slice();
554 555
        tmp_constructor.declareMethod =
          RenderJSGadget.declareMethod;
Romain Courteaud's avatar
Romain Courteaud committed
556 557
        tmp_constructor.declareAcquiredMethod =
          RenderJSGadget.declareAcquiredMethod;
558 559
        tmp_constructor.allowPublicAcquisition =
          RenderJSGadget.allowPublicAcquisition;
560 561 562 563
        tmp_constructor.ready =
          RenderJSGadget.ready;
        tmp_constructor.prototype = new RenderJSGadget();
        tmp_constructor.prototype.constructor = tmp_constructor;
564
        tmp_constructor.prototype.__path = url;
565
        tmp_constructor.prototype.__acquired_method_dict = {};
566 567 568
        // https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
        // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
        // https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
569
        tmp_constructor.__template_element =
570 571
          (new DOMParser()).parseFromString(xhr.responseText, "text/html");
        parsed_html = renderJS.parseGadgetHTMLDocument(
572 573
          tmp_constructor.__template_element,
          url
574 575 576
        );
        for (key in parsed_html) {
          if (parsed_html.hasOwnProperty(key)) {
577
            tmp_constructor.prototype['__' + key] = parsed_html[key];
578 579
          }
        }
580

581 582
        gadget_model_dict[url] = tmp_constructor;
      }
583

584 585
      return gadget_model_dict[url];
    }
586

587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609
    function resolver(resolve, reject) {
      function handler() {
        var tmp_result;
        try {
          if (xhr.readyState === 0) {
            // UNSENT
            reject(xhr);
          } else if (xhr.readyState === 4) {
            // DONE
            if ((xhr.status < 200) || (xhr.status >= 300) ||
                (!/^text\/html[;]?/.test(
                  xhr.getResponseHeader("Content-Type") || ""
                ))) {
              reject(xhr);
            } else {
              tmp_result = parse();
              resolve(tmp_result);
            }
          }
        } catch (e) {
          reject(e);
        }
      }
610

611 612 613 614 615 616
      xhr = new XMLHttpRequest();
      xhr.open("GET", url);
      xhr.onreadystatechange = handler;
      xhr.setRequestHeader('Accept', 'text/html');
      xhr.withCredentials = true;
      xhr.send();
617 618
    }

619 620 621 622 623
    function canceller() {
      if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
        xhr.abort();
      }
    }
Romain Courteaud's avatar
Romain Courteaud committed
624

625
    if (gadget_model_dict.hasOwnProperty(url)) {
626 627
      // Return klass object if it already exists
      result = RSVP.resolve(gadget_model_dict[url]);
628
    } else {
629 630
      // Fetch the HTML page and parse it
      result = new RSVP.Promise(resolver, canceller);
631
    }
632
    return result;
633 634
  };

635 636 637
  /////////////////////////////////////////////////////////////////
  // renderJS.clearGadgetKlassList
  /////////////////////////////////////////////////////////////////
638 639 640
  // For test purpose only
  renderJS.clearGadgetKlassList = function () {
    gadget_model_dict = {};
641 642
    javascript_registration_dict = {};
    stylesheet_registration_dict = {};
643 644
  };

645 646 647
  /////////////////////////////////////////////////////////////////
  // renderJS.parseGadgetHTMLDocument
  /////////////////////////////////////////////////////////////////
648
  renderJS.parseGadgetHTMLDocument = function (document_element, url) {
649
    var settings = {
650 651 652
        title: "",
        interface_list: [],
        required_css_list: [],
653
        required_js_list: []
654 655
      },
      i,
656 657 658 659 660 661 662
      element,
      isAbsoluteURL = new RegExp('^(?:[a-z]+:)?//', 'i');

    if (!url || !isAbsoluteURL.test(url)) {
      throw new Error("The second parameter should be an absolute url");
    }

663 664 665 666 667 668 669 670 671
    if (document_element.nodeType === 9) {
      settings.title = document_element.title;

      for (i = 0; i < document_element.head.children.length; i += 1) {
        element = document_element.head.children[i];
        if (element.href !== null) {
          // XXX Manage relative URL during extraction of URLs
          // element.href returns absolute URL in firefox but "" in chrome;
          if (element.rel === "stylesheet") {
672 673 674
            settings.required_css_list.push(
              renderJS.getAbsoluteURL(element.getAttribute("href"), url)
            );
675 676 677
          } else if (element.nodeName === "SCRIPT" &&
                     (element.type === "text/javascript" ||
                      !element.type)) {
678 679 680
            settings.required_js_list.push(
              renderJS.getAbsoluteURL(element.getAttribute("src"), url)
            );
681
          } else if (element.rel === "http://www.renderjs.org/rel/interface") {
682 683 684
            settings.interface_list.push(
              renderJS.getAbsoluteURL(element.getAttribute("href"), url)
            );
685 686
          }
        }
Romain Courteaud's avatar
Typo  
Romain Courteaud committed
687
      }
688
    } else {
689
      throw new Error("The first parameter should be an HTMLDocument");
690
    }
691
    return settings;
692
  };
693 694 695 696

  /////////////////////////////////////////////////////////////////
  // global
  /////////////////////////////////////////////////////////////////
697
  window.rJS = window.renderJS = renderJS;
698 699 700
  window.__RenderJSGadget = RenderJSGadget;
  window.__RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
  window.__RenderJSIframeGadget = RenderJSIframeGadget;
701 702 703 704 705

  ///////////////////////////////////////////////////
  // Bootstrap process. Register the self gadget.
  ///////////////////////////////////////////////////

Romain Courteaud's avatar
Romain Courteaud committed
706 707 708
  function bootstrap() {
    var url = window.location.href,
      tmp_constructor,
709 710 711 712 713
      root_gadget,
      declare_method_count = 0,
      embedded_channel,
      notifyReady,
      notifyDeclareMethod,
714 715
      gadget_ready = false,
      last_acquisition_gadget;
716

Romain Courteaud's avatar
Romain Courteaud committed
717 718 719
    // Create the gadget class for the current url
    if (gadget_model_dict.hasOwnProperty(url)) {
      throw new Error("bootstrap should not be called twice");
720
    }
721 722
    loading_gadget_promise = new RSVP.Promise(function (resolve, reject) {
      if (window.self === window.top) {
723 724 725 726 727 728 729 730 731 732 733 734 735 736 737

        last_acquisition_gadget = new RenderJSGadget();
        last_acquisition_gadget.__acquired_method_dict = {
          getTopURL: function () {
            return url;
          }
        };
        // Stop acquisition on the last acquisition gadget
        // Do not put this on the klass, as their could be multiple instances
        last_acquisition_gadget.__aq_parent = function (method_name) {
          throw new renderJS.AcquisitionError(
            "No gadget provides " + method_name
          );
        };

738 739 740 741 742
        // XXX Copy/Paste from declareGadgetKlass
        tmp_constructor = function () {
          RenderJSGadget.call(this);
        };
        tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
Romain Courteaud's avatar
Romain Courteaud committed
743 744
        tmp_constructor.declareAcquiredMethod =
          RenderJSGadget.declareAcquiredMethod;
745 746
        tmp_constructor.allowPublicAcquisition =
          RenderJSGadget.allowPublicAcquisition;
747
        tmp_constructor.__ready_list = RenderJSGadget.__ready_list.slice();
748 749 750
        tmp_constructor.ready = RenderJSGadget.ready;
        tmp_constructor.prototype = new RenderJSGadget();
        tmp_constructor.prototype.constructor = tmp_constructor;
751
        tmp_constructor.prototype.__path = url;
752 753 754 755
        gadget_model_dict[url] = tmp_constructor;

        // Create the root gadget instance and put it in the loading stack
        root_gadget = new gadget_model_dict[url]();
756

757
        setAqParent(root_gadget, last_acquisition_gadget);
758

759 760 761 762 763 764 765 766 767
      } else {
        // Create the communication channel
        embedded_channel = Channel.build({
          window: window.parent,
          origin: "*",
          scope: "renderJS"
        });
        // Create the root gadget instance and put it in the loading stack
        tmp_constructor = RenderJSEmbeddedGadget;
768 769
        tmp_constructor.__ready_list = RenderJSGadget.__ready_list.slice();
        tmp_constructor.prototype.__path = url;
770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
        root_gadget = new RenderJSEmbeddedGadget();

        // Bind calls to renderJS method on the instance
        embedded_channel.bind("methodCall", function (trans, v) {
          root_gadget[v[0]].apply(root_gadget, v[1]).then(function (g) {
            trans.complete(g);
          }).fail(function (e) {
            trans.error(e.toString());
          });
          trans.delayReturn(true);
        });

        // Notify parent about gadget instanciation
        notifyReady = function () {
          if ((declare_method_count === 0) && (gadget_ready === true)) {
            embedded_channel.notify({method: "ready"});
          }
        };

        // Inform parent gadget about declareMethod calls here.
        notifyDeclareMethod = function (name) {
          declare_method_count += 1;
          embedded_channel.call({
            method: "declareMethod",
            params: name,
            success: function () {
              declare_method_count -= 1;
              notifyReady();
            },
            error: function () {
              declare_method_count -= 1;
801
            }
802 803 804 805 806 807 808 809 810 811 812
          });
        };

        notifyDeclareMethod("getInterfaceList");
        notifyDeclareMethod("getRequiredCSSList");
        notifyDeclareMethod("getRequiredJSList");
        notifyDeclareMethod("getPath");
        notifyDeclareMethod("getTitle");

        // Surcharge declareMethod to inform parent window
        tmp_constructor.declareMethod = function (name, callback) {
813 814 815 816
          var result = RenderJSGadget.declareMethod.apply(
              this,
              [name, callback]
            );
817 818 819
          notifyDeclareMethod(name);
          return result;
        };
820

Romain Courteaud's avatar
Romain Courteaud committed
821 822
        tmp_constructor.declareAcquiredMethod =
          RenderJSGadget.declareAcquiredMethod;
823 824
        tmp_constructor.allowPublicAcquisition =
          RenderJSGadget.allowPublicAcquisition;
Romain Courteaud's avatar
Romain Courteaud committed
825

826 827
        // Define __aq_parent to inform parent window
        tmp_constructor.prototype.__aq_parent = function (method_name,
828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844
          argument_list) {
          return new RSVP.Promise(function (resolve, reject) {
            embedded_channel.call({
              method: "acquire",
              params: [
                method_name,
                argument_list
              ],
              success: function (s) {
                resolve(s);
              },
              error: function (e) {
                reject(e);
              }
            });
          });
        };
845
      }
846 847

      tmp_constructor.prototype.__acquired_method_dict = {};
848
      gadget_loading_klass = tmp_constructor;
Romain Courteaud's avatar
Romain Courteaud committed
849

850
      function init() {
851
        // XXX HTML properties can only be set when the DOM is fully loaded
852
        var settings = renderJS.parseGadgetHTMLDocument(document, url),
853
          j,
854 855 856
          key;
        for (key in settings) {
          if (settings.hasOwnProperty(key)) {
857
            tmp_constructor.prototype['__' + key] = settings[key];
858
          }
Romain Courteaud's avatar
Romain Courteaud committed
859
        }
860 861 862 863 864
        tmp_constructor.__template_element = document.createElement("div");
        root_gadget.__element = document.body;
        for (j = 0; j < root_gadget.__element.childNodes.length; j += 1) {
          tmp_constructor.__template_element.appendChild(
            root_gadget.__element.childNodes[j].cloneNode(true)
865 866 867 868 869 870 871 872 873 874 875 876 877 878 879
          );
        }
        RSVP.all([root_gadget.getRequiredJSList(),
                  root_gadget.getRequiredCSSList()])
          .then(function (all_list) {
            var i,
              js_list = all_list[0],
              css_list = all_list[1],
              queue;
            for (i = 0; i < js_list.length; i += 1) {
              javascript_registration_dict[js_list[i]] = null;
            }
            for (i = 0; i < css_list.length; i += 1) {
              stylesheet_registration_dict[css_list[i]] = null;
            }
880
            gadget_loading_klass = undefined;
881 882 883 884
            queue = new RSVP.Queue();
            function ready_wrapper() {
              return root_gadget;
            }
885 886 887 888 889 890 891 892 893 894 895 896 897

            if (window.top !== window.self) {
              tmp_constructor.ready(function () {
                var base = document.createElement('base');
                return root_gadget.__aq_parent('getTopURL', [])
                  .then(function (topURL) {
                    base.href = topURL;
                    base.target = "_top";
                    document.head.appendChild(base);
                  });
              });
            }

898
            queue.push(ready_wrapper);
899
            for (i = 0; i < tmp_constructor.__ready_list.length; i += 1) {
900
              // Put a timeout?
901
              queue.push(tmp_constructor.__ready_list[i])
902
              // Always return the gadget instance after ready function
903
                   .push(ready_wrapper);
904 905 906 907 908 909 910 911
            }
            queue.push(resolve, function (e) {
              reject(e);
              throw e;
            });
            return queue;
          }).fail(function (e) {
            reject(e);
912 913
            /*global console */
            console.error(e);
Romain Courteaud's avatar
Romain Courteaud committed
914
          });
915 916
      }
      document.addEventListener('DOMContentLoaded', init, false);
Romain Courteaud's avatar
Romain Courteaud committed
917
    });
918

919 920 921 922 923 924 925 926 927 928 929
    if (window.self !== window.top) {
      // Inform parent window that gadget is correctly loaded
      loading_gadget_promise.then(function () {
        gadget_ready = true;
        notifyReady();
      }).fail(function (e) {
        embedded_channel.notify({method: "failed", params: e.toString()});
        throw e;
      });
    }

Romain Courteaud's avatar
Romain Courteaud committed
930 931
  }
  bootstrap();
932

933
}(document, window, RSVP, DOMParser, Channel));