├── test
├── embedded_empty.html
├── inject_script.html
├── embedded_crashing_service.js
├── embedded_fail.js
├── embedded_non_renderjs.html
├── unload_next.html
├── trigger_rjsready_event_on_ready_gadget.js
├── inject_script.js
├── cancel_gadget.html
├── trigger_rjsready_event_on_ready_gadget.html
├── embedded.html
├── embedded_fail.html
├── embedded_404_js.html
├── embedded_heavy.html
├── embedded_crashing_service.html
├── unload_gadget.html
├── not_declared_gadget.html
├── error_gadget.html
├── not_declared_gadget.js
├── index.html
├── run-qunit.js
├── embedded.js
└── mutex_test.js
├── .gitignore
├── TODO
├── .htaccess
├── lib
├── iefix
│ ├── doccontains.js
│ └── url.js
├── domparser
│ └── domparser.js
└── jschannel
│ └── jschannel.js
├── README.md
├── package.json
├── perf
├── method.js
├── onloop.js
├── sub2.html
├── sub1.html
├── onloop.html
├── method.html
├── declareGadget.html
└── declareGadget.js
├── Gruntfile.js
└── dist
├── renderjs-0.3.2.js
├── renderjs-0.3.3.js
├── renderjs-0.4.0.js
└── renderjs-0.4.1.js
/test/embedded_empty.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #vi
2 | *.swp
3 | *~
4 | node_modules/
5 | tmp/
6 | .tern-port
7 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | how to manage local script tag #parseGadgetHTML TODO
2 | check that gadget/dom context is kept in promise TODO
3 | keep css file media query #declareCSS TODO
4 | test selector TODO
5 |
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 | # purpose of placing this file is to allow this folder to
2 | # accessible for anonymous users inside testnode's apache
3 | # frontend.
4 | # Without this file by automatic unit testing will not work
5 | Require all granted
6 |
--------------------------------------------------------------------------------
/lib/iefix/doccontains.js:
--------------------------------------------------------------------------------
1 | // IE does not support have Document.prototype.contains.
2 | if (typeof document.contains !== 'function') {
3 | Document.prototype.contains = function(node) {
4 | if (node === this || node.parentNode === this)
5 | return true;
6 | return this.documentElement.contains(node);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RenderJS
2 |
3 | RenderJS is a promised-based JavaScript library for developing modularized single page web applications. It uses HTML5 to specify self-contained components consisting of HTML5, CSS and Javascript (called "gadgets"). Gadgets are reusable in different applications just as easily as linking an HTML5 page to another and can combined to build complex applications.
4 |
5 | ### RenderJS Documentation
6 |
7 | The documentation can be found on [https://renderjs.nexedi.com](https://renderjs.nexedi.com)
8 |
9 | ### RenderJS Quickstart
10 | git clone https://lab.nexedi.com/nexedi/renderjs.git
11 | npm install
12 | grunt server
13 |
14 |
15 | ### RenderJS Code
16 |
17 | RenderJS source code is hosted on Gitlab at [https://lab.nexedi.com/nexedi/renderjs/](https://lab.nexedi.com/nexedi/renderjs) (Github [mirror](https://github.com/nexedi/renderjs/) - please use the issue tracker on Gitlab)
18 |
19 | ### RenderJS Test
20 | You can run tests after installing and building RenderJS by opening the */test/* folder
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "renderjs",
3 | "version": "0.29.0",
4 | "description": "RenderJs provides HTML5 gadgets",
5 | "main": "dist/renderjs-latest.js",
6 | "dependencies": {
7 | "rsvp": "git+https://lab.nexedi.com/nexedi/rsvp.js.git"
8 | },
9 | "devDependencies": {
10 | "URIjs": "~1.12.x",
11 | "connect-livereload": "~0.3.0",
12 | "grunt": "~0.4.1",
13 | "grunt-cli": "~0.1.11",
14 | "grunt-contrib-concat": "~0.3.0",
15 | "grunt-contrib-connect": "~0.5.0",
16 | "grunt-contrib-copy": "~0.4.1",
17 | "grunt-contrib-qunit": "~0.3.0",
18 | "grunt-contrib-watch": "~0.5.3",
19 | "grunt-curl": "~1.2.1",
20 | "grunt-jslint": "1.1.14",
21 | "grunt-open": "~0.2.2",
22 | "sinon": "~1.7.3"
23 | },
24 | "scripts": {
25 | "test": "./node_modules/.bin/grunt test",
26 | "lint": "./node_modules/.bin/grunt lint",
27 | "prepublish": "./node_modules/.bin/grunt build"
28 | },
29 | "keywords": [
30 | "component",
31 | "html5"
32 | ],
33 | "author": "Nexedi SA",
34 | "license": "LGPL 3"
35 | }
36 |
--------------------------------------------------------------------------------
/test/inject_script.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/embedded_crashing_service.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | (function (window, rJS) {
21 | "use strict";
22 |
23 | rJS(window)
24 | .declareService(function () {
25 | throw new TypeError("Cannot read property 'bar' of undefined");
26 | });
27 |
28 | }(window, rJS));
29 |
--------------------------------------------------------------------------------
/test/embedded_fail.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | (function (window, rJS) {
21 | "use strict";
22 |
23 | var gk = rJS(window);
24 |
25 | gk.ready(function (g) {
26 | return RSVP.delay(50).then(function () {
27 | throw new Error("Manually rejected");
28 | });
29 | });
30 |
31 | }(window, rJS));
32 |
--------------------------------------------------------------------------------
/perf/method.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /*jslint nomen: true*/
21 | (function (window, rJS) {
22 | "use strict";
23 |
24 |
25 | rJS(window)
26 | .declareMethod('doNothing', function doNothing() {
27 | return;
28 | })
29 | .onLoop(function iterateLoop() {
30 | return this.doNothing();
31 | });
32 |
33 | }(window, rJS));
34 |
--------------------------------------------------------------------------------
/perf/onloop.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /*jslint nomen: true*/
21 | (function (window, rJS) {
22 | "use strict";
23 |
24 |
25 | rJS(window)
26 | .declareMethod('doNothing', function doNothing() {
27 | return;
28 | })
29 | .onLoop(function iterateLoop() {
30 | return new RSVP.Queue();
31 | });
32 |
33 | }(window, rJS));
34 |
--------------------------------------------------------------------------------
/test/embedded_non_renderjs.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/test/unload_next.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Next static page for renderJS test
25 |
26 |
27 |
28 | Page changed
29 |
30 |
--------------------------------------------------------------------------------
/test/trigger_rjsready_event_on_ready_gadget.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /*global window, rJS, jIO, FormData */
21 | /*jslint indent: 2, maxerr: 3 */
22 |
23 | (function (window, rJS) {
24 | "use strict";
25 | rJS(window)
26 | .ready(function (gadget) {
27 | return gadget.element.dispatchEvent(new Event("rjsready",
28 | {bubbles: true}));
29 | });
30 | }(window, rJS));
31 |
--------------------------------------------------------------------------------
/test/inject_script.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | (function (document) {
21 | "use strict";
22 |
23 | // can't use RSVP here because its not loaded (neccessarily)
24 | window.inject_script = function (src, resolve) {
25 | // inject RSVP
26 | var script = document.createElement("script");
27 | script.onload = function () {
28 | resolve();
29 | };
30 | script.src = src;
31 | document.head.appendChild(script);
32 | };
33 |
34 | }(document));
--------------------------------------------------------------------------------
/perf/sub2.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | Sub2 gadget
24 |
25 |
26 |
27 |
28 |
29 |
30 | Foobar
31 |
32 |
33 |
--------------------------------------------------------------------------------
/test/cancel_gadget.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/test/trigger_rjsready_event_on_ready_gadget.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 |
24 | Test Gadget
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/perf/sub1.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | Sub1 gadget
24 |
25 |
26 |
27 |
28 |
29 |
30 | Subgadget 1
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lib/domparser/domparser.js:
--------------------------------------------------------------------------------
1 | /*
2 | * DOMParser HTML extension
3 | * 2012-09-04
4 | *
5 | * By Eli Grey, http://eligrey.com
6 | * Public domain.
7 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 | */
9 | /*! @source https://gist.github.com/1129031 */
10 | (function (DOMParser) {
11 | "use strict";
12 | var DOMParser_proto = DOMParser.prototype,
13 | real_parseFromString = DOMParser_proto.parseFromString;
14 |
15 | // Firefox/Opera/IE throw errors on unsupported types
16 | try {
17 | // WebKit returns null on unsupported types
18 | if ((new DOMParser()).parseFromString("", "text/html")) {
19 | // text/html parsing is natively supported
20 | return;
21 | }
22 | } catch (ignore) {}
23 |
24 | DOMParser_proto.parseFromString = function (markup, type) {
25 | var result, doc, doc_elt, first_elt;
26 | if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
27 | doc = document.implementation.createHTMLDocument("");
28 | doc_elt = doc.documentElement;
29 |
30 | doc_elt.innerHTML = markup;
31 | first_elt = doc_elt.firstElementChild;
32 |
33 | if (doc_elt.childElementCount === 1
34 | && first_elt.localName.toLowerCase() === "html") {
35 | doc.replaceChild(first_elt, doc_elt);
36 | }
37 |
38 | result = doc;
39 | } else {
40 | result = real_parseFromString.apply(this, arguments);
41 | }
42 | return result;
43 | };
44 | }(DOMParser));
45 |
46 |
--------------------------------------------------------------------------------
/perf/onloop.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | Memory consumption for onLoop
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/perf/method.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | Memory consumption for declareMethd
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/perf/declareGadget.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | Memory consumption for declareGadget
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/perf/declareGadget.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /*jslint nomen: true*/
21 | (function (window, rJS) {
22 | "use strict";
23 |
24 | rJS(window)
25 | .onLoop(function createNewChildOnLoop() {
26 | var gadget = this;
27 | return this.declareGadget('sub1.html', {scope: 'foo'})
28 | .push(function (sub_gadget) {
29 | var div = gadget.element.querySelector('div');
30 | while (div.firstChild) {
31 | div.removeChild(div.firstChild);
32 | }
33 | div.appendChild(sub_gadget.element);
34 | });
35 | });
36 |
37 | }(window, rJS));
38 |
--------------------------------------------------------------------------------
/test/embedded.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/embedded_fail.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/embedded_404_js.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/embedded_heavy.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/lib/iefix/url.js:
--------------------------------------------------------------------------------
1 | (function (DOMParser) {
2 | "use strict";
3 |
4 | try {
5 | if ((new window.URL("../a", "https://example.com/")).href === "https://example.com/a") {
6 | return;
7 | }
8 | } catch (ignore) {}
9 |
10 | var isAbsoluteOrDataURL = /^(?:[a-z]+:)?\/\/|data:/i;
11 |
12 | function resolveUrl(url, base_url) {
13 | var doc, base, link,
14 | html = "";
15 |
16 | if (url && base_url) {
17 | doc = (new DOMParser()).parseFromString(html, 'text/html');
18 | base = doc.createElement('base');
19 | link = doc.createElement('link');
20 | doc.head.appendChild(base);
21 | doc.head.appendChild(link);
22 | base.href = base_url;
23 | link.href = url;
24 | return link.href;
25 | }
26 | return url;
27 | }
28 |
29 | function URL(url, base) {
30 | if (base !== undefined) {
31 | if (!isAbsoluteOrDataURL.test(base)) {
32 | throw new TypeError("Failed to construct 'URL': Invalid base URL");
33 | }
34 | url = resolveUrl(url, base);
35 | }
36 | if (!isAbsoluteOrDataURL.test(url)) {
37 | throw new TypeError("Failed to construct 'URL': Invalid URL");
38 | }
39 | this.href = url;
40 | }
41 | URL.prototype.href = "";
42 |
43 | if (window.URL && window.URL.createObjectURL) {
44 | URL.createObjectURL = window.URL.createObjectURL;
45 | }
46 | if (window.URL && window.URL.revokeObjectURL) {
47 | URL.revokeObjectURL = window.URL.revokeObjectURL;
48 | }
49 |
50 | window.URL = URL;
51 |
52 | }(DOMParser));
--------------------------------------------------------------------------------
/test/embedded_crashing_service.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Embedded page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/unload_gadget.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Unload event for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | Next page
33 |
34 |
35 |
--------------------------------------------------------------------------------
/test/not_declared_gadget.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Not declared page for renderJS test
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/test/error_gadget.html:
--------------------------------------------------------------------------------
1 |
3 |
22 |
23 |
24 | Error event for renderJS test
25 |
26 |
27 |
28 |
29 |
34 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/test/not_declared_gadget.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2014, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /*jslint nomen: true*/
21 | (function (window, rJS) {
22 | "use strict";
23 | var gk = rJS(window);
24 | gk.ready(function (g) {
25 | g.props = {};
26 | return g.getElement()
27 | .push(function (element) {
28 | g.props.element = element;
29 | });
30 | })
31 | .declareAcquiredMethod('willFail', 'willFail')
32 | .declareService(function () {
33 | var context = this;
34 | return RSVP.all([
35 | context.checkAcquisitionError(),
36 | context.checkKlass()
37 | ]);
38 | })
39 | .declareMethod('checkAcquisitionError', function () {
40 | var g = this;
41 | return g.willFail()
42 | .push(undefined, function (e) {
43 | g.props.element.querySelector('.acquisitionError')
44 | .innerHTML = e;
45 | });
46 | })
47 | .declareMethod('checkKlass', function () {
48 | var g = this;
49 | g.props.element.querySelector('.klass')
50 | .innerHTML = "klass" +
51 | ((g instanceof window.__RenderJSEmbeddedGadget) ?
52 | " = embedded" : " != embedded");
53 | });
54 | }(window, rJS));
55 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
21 |
22 |
23 | Test renderJS
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
QUnit renderJS test suite
37 |
38 |
39 |
40 |
41 |
test markup, will be hidden
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/test/run-qunit.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /**
21 | * Wait until the test condition is true or a timeout occurs. Useful for waiting
22 | * on a server response or for a ui change (fadeIn, etc.) to occur.
23 | *
24 | * @param testFx javascript condition that evaluates to a boolean,
25 | * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
26 | * as a callback function.
27 | * @param onReady what to do when testFx condition is fulfilled,
28 | * it can be passed in as a string (e.g.: "1 == 1" or "$('#bar').is(':visible')" or
29 | * as a callback function.
30 | * @param timeOutMillis the max amount of time to wait. If not specified, 3 sec is used.
31 | */
32 | function waitFor(testFx, onReady, timeOutMillis) {
33 | var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s
34 | start = new Date().getTime(),
35 | condition = false,
36 | interval = setInterval(function() {
37 | if ( (new Date().getTime() - start < maxtimeOutMillis) && !condition ) {
38 | // If not time-out yet and condition not yet fulfilled
39 | condition = (typeof(testFx) === "string" ? eval(testFx) : testFx()); //< defensive code
40 | } else {
41 | if(!condition) {
42 | // If condition still not fulfilled (timeout but condition is 'false')
43 | console.log("'waitFor()' timeout");
44 | phantom.exit(1);
45 | } else {
46 | // Condition fulfilled (timeout and/or condition is 'true')
47 | console.log("'waitFor()' finished in " + (new Date().getTime() - start) + "ms.");
48 | typeof(onReady) === "string" ? eval(onReady) : onReady(); //< Do what it's supposed to do once the condition is fulfilled
49 | clearInterval(interval); //< Stop this interval
50 | }
51 | }
52 | }, 100); //< repeat check every 250ms
53 | };
54 |
55 |
56 | if (phantom.args.length === 0 || phantom.args.length > 2) {
57 | console.log('Usage: run-qunit.js URL');
58 | phantom.exit(1);
59 | }
60 |
61 | var page = new WebPage();
62 |
63 | // Route "console.log()" calls from within the Page context to the main Phantom context (i.e. current "this")
64 | page.onConsoleMessage = function(msg) {
65 | console.log(msg);
66 | };
67 |
68 | page.open(phantom.args[0], function(status){
69 | if (status !== "success") {
70 | console.log("Unable to access network");
71 | phantom.exit(1);
72 | } else {
73 | waitFor(function(){
74 | return page.evaluate(function(){
75 | var el = document.getElementById('qunit-testresult');
76 | if (el && el.innerText.match('completed')) {
77 | return true;
78 | }
79 | return false;
80 | });
81 | }, function(){
82 | var failedNum = page.evaluate(function(){
83 |
84 | var tests = document.getElementById("qunit-tests").childNodes;
85 | console.log("\nTest name (failed, passed, total)\n");
86 | for(var i in tests){
87 | var text = tests[i].innerText;
88 | if(text !== undefined){
89 | if(/Rerun$/.test(text)) text = text.substring(0, text.length - 5);
90 | console.log(text + "\n");
91 | }
92 | }
93 |
94 | var el = document.getElementById('qunit-testresult');
95 | console.log(el.innerText);
96 | try {
97 | return el.getElementsByClassName('failed')[0].innerHTML;
98 | } catch (e) { }
99 | return 10000;
100 | });
101 | phantom.exit((parseInt(failedNum, 10) > 0) ? 1 : 0);
102 | });
103 | }
104 | });
105 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | /*global require */
21 | module.exports = function (grunt) {
22 | "use strict";
23 |
24 | var LIVERELOAD_PORT, lrSnippet, livereloadMiddleware;
25 |
26 | // This is the default port that livereload listens on;
27 | // change it if you configure livereload to use another port.
28 | LIVERELOAD_PORT = 35729;
29 | // lrSnippet is just a function.
30 | // It's a piece of Connect middleware that injects
31 | // a script into the static served html.
32 | lrSnippet = require('connect-livereload')({ port: LIVERELOAD_PORT });
33 | // All the middleware necessary to serve static files.
34 | livereloadMiddleware = function (connect, options) {
35 | return [
36 | // Inject a livereloading script into static files.
37 | lrSnippet,
38 | // Serve static files.
39 | connect.static(options.base),
40 | // Make empty directories browsable.
41 | connect.directory(options.base)
42 | ];
43 | };
44 |
45 | grunt.loadNpmTasks("grunt-jslint");
46 | // grunt.loadNpmTasks("grunt-contrib-uglify");
47 | grunt.loadNpmTasks('grunt-contrib-watch');
48 | grunt.loadNpmTasks('grunt-contrib-qunit');
49 | grunt.loadNpmTasks('grunt-contrib-concat');
50 | grunt.loadNpmTasks('grunt-contrib-connect');
51 | grunt.loadNpmTasks('grunt-contrib-copy');
52 | grunt.loadNpmTasks('grunt-curl');
53 | grunt.loadNpmTasks('grunt-open');
54 |
55 | grunt.initConfig({
56 | pkg: grunt.file.readJSON('package.json'),
57 |
58 | jslint: {
59 | config: {
60 | src: ['package.json', 'Gruntfile.js'],
61 | directives: {
62 | maxlen: 100,
63 | indent: 2,
64 | maxerr: 3,
65 | predef: [
66 | 'module'
67 | ]
68 | }
69 | },
70 | client: {
71 | src: ['renderjs.js'],
72 | directives: {
73 | maxlen: 79,
74 | indent: 2,
75 | maxerr: 3,
76 | unparam: true,
77 | predef: [
78 | 'RSVP',
79 | 'window',
80 | 'document',
81 | 'DOMParser',
82 | 'Channel',
83 | 'XMLHttpRequest',
84 | 'MutationObserver',
85 | 'Blob',
86 | 'FileReader',
87 | 'Node',
88 | 'navigator',
89 | 'Event',
90 | 'URL'
91 | ]
92 | }
93 | },
94 | test: {
95 | src: ['test/embedded.js', 'test/renderjs_test.js',
96 | 'test/inject_script.js',
97 | 'test/embedded_crashing_service.js',
98 | 'test/embedded_fails.js',
99 | 'test/mutex_test.js', 'test/not_declared_gadget.js',
100 | 'test/trigger_rjsready_event_on_ready_gadget.js'],
101 | directives: {
102 | maxlen: 79,
103 | indent: 2,
104 | maxerr: 3,
105 | unparam: true,
106 | predef: [
107 | 'window',
108 | 'document',
109 | 'QUnit',
110 | 'renderJS',
111 | 'rJS',
112 | '__RenderJSGadget',
113 | 'sinon',
114 | 'RSVP',
115 | 'DOMParser',
116 | 'URI',
117 | 'URL',
118 | '__RenderJSIframeGadget',
119 | '__RenderJSEmbeddedGadget',
120 | 'FileReader',
121 | 'Blob',
122 | 'Event',
123 | 'MutationObserver'
124 | ]
125 | }
126 | }
127 | },
128 |
129 | concat: {
130 | options: {
131 | separator: ';'
132 | },
133 | dist: {
134 | src: ['<%= curl.jschannel.dest %>',
135 | '<%= curl.domparser.dest %>',
136 | 'lib/iefix/*.js',
137 | 'renderjs.js'],
138 | dest: 'dist/<%= pkg.name %>-<%= pkg.version %>.js'
139 | }
140 | },
141 |
142 | uglify: {
143 | renderjs: {
144 | src: "<%= concat.dist.dest %>",
145 | dest: "dist/<%= pkg.name %>-<%= pkg.version %>.min.js"
146 | }
147 | },
148 |
149 | copy: {
150 | latest: {
151 | files: [{
152 | src: '<%= concat.dist.dest %>',
153 | dest: "dist/<%= pkg.name %>-latest.js"
154 | /*
155 | }, {
156 | src: '<%= uglify.renderjs.dest %>',
157 | dest: "dist/<%= pkg.name %>-latest.min.js"
158 | */
159 | }]
160 | }
161 | },
162 |
163 | watch: {
164 | src: {
165 | files: [
166 | '<%= jslint.client.src %>',
167 | '<%= jslint.config.src %>',
168 | '<%= jslint.test.src %>',
169 | ['lib/**'],
170 | ['test/*.html', 'test/*.js']
171 | ],
172 | tasks: ['default'],
173 | options: {
174 | livereload: LIVERELOAD_PORT
175 | }
176 | }
177 | },
178 |
179 | curl: {
180 | domparser: {
181 | src: 'https://gist.github.com/eligrey/1129031/raw/' +
182 | 'e26369ee7939db745087beb98b4bb4bbcf460cf3/html-domparser.js',
183 | dest: 'lib/domparser/domparser.js'
184 | },
185 | jschannel: {
186 | src: 'http://mozilla.github.io/jschannel/src/jschannel.js',
187 | dest: 'lib/jschannel/jschannel.js'
188 | }
189 | },
190 |
191 | qunit: {
192 | all: ['test/index.html']
193 | },
194 |
195 | connect: {
196 | client: {
197 | options: {
198 | port: 9000,
199 | base: '.',
200 | directory: '.',
201 | middleware: livereloadMiddleware
202 | }
203 | }
204 | },
205 |
206 | open: {
207 | all: {
208 | // Gets the port from the connect configuration
209 | path: 'http://localhost:<%= connect.client.options.port%>/test/'
210 | }
211 | }
212 | });
213 |
214 | grunt.registerTask('default', ['all']);
215 | grunt.registerTask('all', ['lint', 'build']);
216 | grunt.registerTask('lint', ['jslint']);
217 | grunt.registerTask('test', ['qunit']);
218 | grunt.registerTask('server', ['connect:client', 'watch']);
219 | grunt.registerTask('build', ['concat', 'copy']);
220 |
221 | };
222 |
--------------------------------------------------------------------------------
/test/embedded.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 |
21 | /*jslint nomen: true*/
22 | (function (window, rJS, RSVP) {
23 | "use strict";
24 |
25 | var gk = rJS(window),
26 | ready_called = false,
27 | service_started = false,
28 | job_started = false,
29 | event_started = false,
30 | method_cancel_called = false,
31 | acquired_method_cancel_called = false,
32 | state_change_callback_called = false,
33 | state_change_count = 0,
34 | init_state = {bar: 'foo'},
35 | state_change_callback = function (modification_dict) {
36 | state_change_callback_called = (state_change_count === 0) &&
37 | (modification_dict.foo === 'bar');
38 | state_change_count += 1;
39 | };
40 | gk.ready(function () {
41 | ready_called = true;
42 | })
43 | .setState(init_state)
44 | .onStateChange(state_change_callback)
45 | .onEvent('bar', function () {
46 | event_started = true;
47 | })
48 | .declareService(function () {
49 | service_started = true;
50 | var event = new Event("bar");
51 | this.element.dispatchEvent(event);
52 | })
53 | .declareMethod('wasStateInitialized', function () {
54 | return ((this.hasOwnProperty("state")) &&
55 | (JSON.stringify(this.state) === '{"bar":"foo"}')) &&
56 | (this.state !== init_state);
57 | })
58 | .declareMethod('wasStateHandlerDeclared', function () {
59 | return ((!this.hasOwnProperty("__state_change_callback")) &&
60 | (this.__state_change_callback === state_change_callback));
61 | })
62 | .declareMethod('wasReadyCalled', function () {
63 | return ready_called;
64 | })
65 | .declareMethod('wasServiceStarted', function () {
66 | return service_started;
67 | })
68 | .declareMethod('triggerJob', function () {
69 | return this.runJob();
70 | })
71 | .declareMethod('wasEventStarted', function () {
72 | return event_started;
73 | })
74 | .declareMethod('wasJobStarted', function () {
75 | return job_started;
76 | })
77 | .declareMethod('triggerStateChange', function () {
78 | return this.changeState({foo: 'bar'});
79 | })
80 | .declareMethod('wasStateChangeHandled', function () {
81 | return state_change_callback_called;
82 | })
83 | .declareJob('runJob', function () {
84 | job_started = true;
85 | })
86 | .declareMethod('canReportServiceError', function () {
87 | return (this.aq_reportServiceError !== undefined);
88 | })
89 | .declareMethod('isSubGadgetDictInitialize', function () {
90 | return ((this.hasOwnProperty("__sub_gadget_dict")) &&
91 | (JSON.stringify(this.__sub_gadget_dict) === "{}"));
92 | })
93 | .declareMethod('isAcquisitionDictInitialize', function () {
94 | return (this.__acquired_method_dict !== undefined);
95 | })
96 | .declareMethod('isServiceListInitialize', function () {
97 | return (this.constructor.__service_list !== undefined);
98 | })
99 | .declareMethod('triggerError', function (value) {
100 | throw new Error("Manually triggered embedded error");
101 | })
102 | .declareMethod('triggerStringError', function (value) {
103 | throw "Manually triggered embedded error as string";
104 | })
105 | .declareMethod('setContent', function (value) {
106 | this.embedded_property = value;
107 | })
108 | .declareMethod('getContent', function () {
109 | return this.embedded_property;
110 | })
111 | .declareAcquiredMethod('plugOKAcquire', 'acquireMethodRequested')
112 | .declareMethod('callOKAcquire', function (param1, param2) {
113 | return this.plugOKAcquire(param1, param2);
114 | })
115 | .declareAcquiredMethod('plugErrorAcquire',
116 | 'acquireMethodRequestedWithAcquisitionError')
117 | .declareMethod('callErrorAcquire', function (param1, param2) {
118 | return this.plugErrorAcquire(param1, param2)
119 | .push(undefined, function (error) {
120 | if (error instanceof renderJS.AcquisitionError) {
121 | throw error;
122 | }
123 | throw new Error(
124 | 'Expected AcquisitionError: ' + JSON.stringify(error)
125 | );
126 | });
127 | })
128 | .declareMethod('triggerMethodToCancel', function () {
129 | return new RSVP.Promise(function () {
130 | return;
131 | }, function () {
132 | method_cancel_called = true;
133 | });
134 | })
135 | .declareMethod('wasMethodCancelCalled', function () {
136 | return method_cancel_called;
137 | })
138 | .declareAcquiredMethod('acquireCancellationError',
139 | 'acquireCancellationError')
140 | .declareMethod('triggerAcquiredMethodToCancel', function () {
141 | return this.acquireCancellationError()
142 | .push(undefined, function (error) {
143 | if (error instanceof RSVP.CancellationError) {
144 | acquired_method_cancel_called = true;
145 | throw error;
146 | }
147 | throw new Error(
148 | 'Expected CancellationError: ' + JSON.stringify(error)
149 | );
150 | });
151 | })
152 | .declareMethod('wasAcquiredMethodCancelCalled', function () {
153 | return acquired_method_cancel_called;
154 | })
155 | .declareAcquiredMethod('acquiredStringError',
156 | 'acquiredStringError')
157 | .declareMethod('triggerAcquiredStringError',
158 | function () {
159 | return this.acquiredStringError();
160 | })
161 | .declareAcquiredMethod("acquiredManualCancellationError",
162 | "acquiredManualCancellationError")
163 | .declareMethod('acquirePromiseToCancel',
164 | function () {
165 | return this.acquiredManualCancellationError();
166 | })
167 | .declareAcquiredMethod("isAcquiredMethodCancelCalled",
168 | "isAcquiredMethodCancelCalled")
169 | .declareMethod('wasAcquiredMethodCancelCalledFromParent', function () {
170 | return this.isAcquiredMethodCancelCalled();
171 | })
172 |
173 | .declareMethod('returnNotTransferrable', function () {
174 | var a = {};
175 | a.a = a;
176 | return a;
177 | })
178 | .declareMethod('throwNotTransferrable', function () {
179 | var a = {};
180 | a.a = a;
181 | throw a;
182 | });
183 |
184 | }(window, rJS, RSVP));
185 |
--------------------------------------------------------------------------------
/test/mutex_test.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018, Nexedi SA
3 | *
4 | * This program is free software: you can Use, Study, Modify and Redistribute
5 | * it under the terms of the GNU General Public License version 3, or (at your
6 | * option) any later version, as published by the Free Software Foundation.
7 | *
8 | * You can also Link and Combine this program with other software covered by
9 | * the terms of any of the Free Software licenses or any of the Open Source
10 | * Initiative approved licenses and Convey the resulting work. Corresponding
11 | * source of such a combination shall include the source code for all other
12 | * software used.
13 | *
14 | * This program is distributed WITHOUT ANY WARRANTY; without even the implied
15 | * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
16 | *
17 | * See COPYING file for full licensing terms.
18 | * See https://www.nexedi.com/licensing for rationale and options.
19 | */
20 | (function (Mutex, QUnit) {
21 | "use strict";
22 | var test = QUnit.test,
23 | stop = QUnit.stop,
24 | start = QUnit.start,
25 | ok = QUnit.ok,
26 | expect = QUnit.expect,
27 | equal = QUnit.equal,
28 | module = QUnit.module;
29 |
30 | /////////////////////////////////////////////////////////////////
31 | // parseGadgetHTMLDocument
32 | /////////////////////////////////////////////////////////////////
33 | module("renderJS.Mutex");
34 |
35 | test('constructor', function () {
36 | equal(Mutex.length, 0);
37 | var mutex = new Mutex();
38 | equal(Object.getPrototypeOf(mutex), Mutex.prototype);
39 | equal(mutex.constructor, Mutex);
40 | equal(Mutex.prototype.constructor, Mutex);
41 | });
42 |
43 | test('lockAndRun execute callback', function () {
44 | var mutex = new Mutex(),
45 | counter = 0;
46 | stop();
47 | expect(6);
48 | function assertCounter(value) {
49 | equal(counter, value);
50 | counter += 1;
51 | }
52 | function callback1() {
53 | assertCounter(0);
54 | return new RSVP.Queue()
55 | .push(function () {
56 | return RSVP.delay(100);
57 | })
58 | .push(function () {
59 | assertCounter(1);
60 | return 'callback1 result';
61 | });
62 | }
63 | function callback2() {
64 | assertCounter(3);
65 | }
66 | return new RSVP.Queue()
67 | .push(function () {
68 | return mutex.lockAndRun(callback1);
69 | })
70 | .push(function (result) {
71 | equal(result, 'callback1 result');
72 | assertCounter(2);
73 | return mutex.lockAndRun(callback2);
74 | })
75 | .push(function () {
76 | assertCounter(4);
77 | })
78 | .always(function () {
79 | start();
80 | });
81 | });
82 |
83 | test('lockAndRun handle exception', function () {
84 | var mutex = new Mutex(),
85 | counter = 0;
86 | stop();
87 | expect(5);
88 | function assertCounter(value) {
89 | equal(counter, value);
90 | counter += 1;
91 | }
92 | function callback1() {
93 | assertCounter(0);
94 | throw new Error('Error in callback1');
95 | }
96 | function callback2() {
97 | assertCounter(2);
98 | }
99 | return new RSVP.Queue()
100 | .push(function () {
101 | return mutex.lockAndRun(callback1);
102 | })
103 | .push(undefined, function (error) {
104 | equal(error.message, 'Error in callback1');
105 | assertCounter(1);
106 | return mutex.lockAndRun(callback2);
107 | })
108 | .push(function () {
109 | assertCounter(3);
110 | })
111 | .always(function () {
112 | start();
113 | });
114 | });
115 |
116 | test('lockAndRun prevent concurrent execution', function () {
117 | var mutex = new Mutex(),
118 | counter = 0;
119 | stop();
120 | expect(9);
121 | function assertCounter(value) {
122 | equal(counter, value);
123 | counter += 1;
124 | }
125 | function callback1() {
126 | assertCounter(0);
127 | return new RSVP.Queue()
128 | .push(function () {
129 | return RSVP.delay(50);
130 | })
131 | .push(function () {
132 | assertCounter(1);
133 | return 'callback1 result';
134 | });
135 | }
136 | function callback2() {
137 | assertCounter(2);
138 | return new RSVP.Queue()
139 | .push(function () {
140 | return RSVP.delay(50);
141 | })
142 | .push(function () {
143 | assertCounter(3);
144 | return 'callback2 result';
145 | });
146 | }
147 | function callback3() {
148 | assertCounter(4);
149 | return 'callback3 result';
150 | }
151 | return new RSVP.Queue()
152 | .push(function () {
153 | return RSVP.all([
154 | mutex.lockAndRun(callback1),
155 | mutex.lockAndRun(callback2),
156 | mutex.lockAndRun(callback3)
157 | ]);
158 | })
159 | .push(function (result_list) {
160 | equal(result_list[0], 'callback1 result');
161 | equal(result_list[1], 'callback2 result');
162 | equal(result_list[2], 'callback3 result');
163 | assertCounter(5);
164 | })
165 | .always(function () {
166 | start();
167 | });
168 | });
169 |
170 | test('lockAndRun handle concurrent exception', function () {
171 | var mutex = new Mutex(),
172 | counter = 0;
173 | stop();
174 | expect(4);
175 | function assertCounter(value) {
176 | equal(counter, value);
177 | counter += 1;
178 | }
179 | function callback1() {
180 | return new RSVP.Queue()
181 | .push(function () {
182 | assertCounter(0);
183 | throw new Error('error in callback1');
184 | });
185 | }
186 | function callback2() {
187 | assertCounter(1);
188 | throw new Error('error in callback2');
189 | }
190 | function callback3() {
191 | assertCounter(2);
192 | return 'callback3 result';
193 | }
194 | return new RSVP.Queue()
195 | .push(function () {
196 | return RSVP.all([
197 | mutex.lockAndRun(callback1),
198 | mutex.lockAndRun(callback2),
199 | mutex.lockAndRun(callback3)
200 | ]);
201 | })
202 | .push(undefined, function (error) {
203 | equal(error.message, 'error in callback1');
204 | // Callback 2 is called before RSVP.all is rejected
205 | // Callback 3 is cancelled by RSVP.all
206 | assertCounter(2);
207 | })
208 | .always(function () {
209 | start();
210 | });
211 | });
212 |
213 | test('lockAndRun cancel does not prevent next execution', function () {
214 | var mutex = new Mutex(),
215 | counter = 0;
216 | stop();
217 | expect(6);
218 | function assertCounter(value) {
219 | equal(counter, value);
220 | counter += 1;
221 | }
222 | function callback1() {
223 | return new RSVP.Queue()
224 | .push(function () {
225 | ok(false, 'Should not reach that code');
226 | });
227 | }
228 | function callback2() {
229 | assertCounter(1);
230 | return 'callback2 result';
231 | }
232 | return new RSVP.Queue()
233 | .push(function () {
234 | var promise1 = mutex.lockAndRun(callback1);
235 | return RSVP.all([
236 | promise1
237 | .then(function () {
238 | ok(false, 'Should not reach that code 2');
239 | }, function (error) {
240 | assertCounter(0);
241 | equal(error.message, 'cancel callback1');
242 | return 'handler1 result';
243 | }),
244 | mutex.lockAndRun(callback2),
245 | promise1.cancel('cancel callback1')
246 | ]);
247 | })
248 | .push(function (result_list) {
249 | equal(result_list[0], 'handler1 result');
250 | equal(result_list[1], 'callback2 result');
251 | assertCounter(2);
252 | })
253 | .always(function () {
254 | start();
255 | });
256 | });
257 |
258 | test('lockAndRun cancel stop first execution', function () {
259 | var mutex = new Mutex(),
260 | counter = 0;
261 | stop();
262 | expect(2);
263 | function assertCounter(value) {
264 | equal(counter, value);
265 | counter += 1;
266 | }
267 | function callback1() {
268 | assertCounter(0);
269 | return new RSVP.Queue()
270 | .push(function () {
271 | return RSVP.delay(50);
272 | })
273 | .push(function () {
274 | assertCounter(-999);
275 | ok(false, 'Should not reach that code');
276 | });
277 | }
278 | return new RSVP.Queue()
279 | .push(function () {
280 | var promise = mutex.lockAndRun(callback1);
281 | promise.cancel('cancel callback1');
282 | return RSVP.delay(200);
283 | })
284 | .push(function () {
285 | assertCounter(1);
286 | })
287 | .always(function () {
288 | start();
289 | });
290 | });
291 |
292 | test('lockAndRun cancel stop second execution', function () {
293 | var mutex = new Mutex(),
294 | counter = 0;
295 | stop();
296 | expect(3);
297 | function assertCounter(value) {
298 | equal(counter, value);
299 | counter += 1;
300 | }
301 | function callback1() {
302 | assertCounter(0);
303 | return new RSVP.Queue()
304 | .push(function () {
305 | return RSVP.delay(50);
306 | })
307 | .push(function () {
308 | assertCounter(1);
309 | });
310 | }
311 | function callback2() {
312 | assertCounter(2);
313 | return new RSVP.Queue()
314 | .push(function () {
315 | return RSVP.delay(50);
316 | })
317 | .push(function () {
318 | assertCounter(-999);
319 | ok(false, 'Should not reach that code');
320 | });
321 | }
322 | return new RSVP.Queue()
323 | .push(function () {
324 | return mutex.lockAndRun(callback1);
325 | })
326 | .push(function () {
327 | var promise = mutex.lockAndRun(callback2);
328 | promise.cancel('cancel callback2');
329 | return RSVP.delay(200);
330 | })
331 | .push(function () {
332 | assertCounter(2);
333 | })
334 | .always(function () {
335 | start();
336 | });
337 | });
338 |
339 | test('lockAndRun cancel does not cancel previous execution', function () {
340 | var mutex = new Mutex(),
341 | counter = 0,
342 | defer = RSVP.defer();
343 | stop();
344 | expect(10);
345 | function assertCounter(value) {
346 | equal(counter, value);
347 | counter += 1;
348 | }
349 | function callback2() {
350 | ok(false, 'Should not reach that code');
351 | }
352 | function callback1() {
353 | return new RSVP.Queue()
354 | .push(function () {
355 | return RSVP.delay(50);
356 | })
357 | .push(function () {
358 | assertCounter(0);
359 | defer.resolve();
360 | return RSVP.delay(50);
361 | })
362 | .push(function () {
363 | assertCounter(3);
364 | return 'callback1 result';
365 | });
366 | }
367 | function callback3() {
368 | // Ensure that callback3 is executed only when callback1 is finished
369 | assertCounter(4);
370 | return 'callback3 result';
371 | }
372 | return new RSVP.Queue()
373 | .push(function () {
374 | var promise1 = mutex.lockAndRun(callback1),
375 | promise2 = mutex.lockAndRun(callback2),
376 | promise3 = mutex.lockAndRun(callback3);
377 | return RSVP.all([
378 | promise1,
379 | promise2
380 | .then(function () {
381 | ok(false, 'Should not reach that code');
382 | }, function (error) {
383 | assertCounter(2);
384 | equal(error.message, 'cancel callback2');
385 | return 'handler2 result';
386 | }),
387 | promise3,
388 | defer.promise
389 | .then(function () {
390 | assertCounter(1);
391 | promise2.cancel('cancel callback2');
392 | })
393 | ]);
394 | })
395 | .push(function (result_list) {
396 | equal(result_list[0], 'callback1 result');
397 | equal(result_list[1], 'handler2 result');
398 | equal(result_list[2], 'callback3 result');
399 | assertCounter(5);
400 | })
401 | .always(function () {
402 | start();
403 | });
404 | });
405 |
406 | }(renderJS.Mutex, QUnit));
--------------------------------------------------------------------------------
/dist/renderjs-0.3.2.js:
--------------------------------------------------------------------------------
1 | /*! RenderJs v0.3.2 */
2 |
3 | /*
4 | * DOMParser HTML extension
5 | * 2012-09-04
6 | *
7 | * By Eli Grey, http://eligrey.com
8 | * Public domain.
9 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
10 | */
11 | /*! @source https://gist.github.com/1129031 */
12 | (function (DOMParser) {
13 | "use strict";
14 | var DOMParser_proto = DOMParser.prototype,
15 | real_parseFromString = DOMParser_proto.parseFromString;
16 |
17 | // Firefox/Opera/IE throw errors on unsupported types
18 | try {
19 | // WebKit returns null on unsupported types
20 | if ((new DOMParser()).parseFromString("", "text/html")) {
21 | // text/html parsing is natively supported
22 | return;
23 | }
24 | } catch (ignore) {}
25 |
26 | DOMParser_proto.parseFromString = function (markup, type) {
27 | var result, doc, doc_elt, first_elt;
28 | if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
29 | doc = document.implementation.createHTMLDocument("");
30 | doc_elt = doc.documentElement;
31 |
32 | doc_elt.innerHTML = markup;
33 | first_elt = doc_elt.firstElementChild;
34 |
35 | if (doc_elt.childElementCount === 1
36 | && first_elt.localName.toLowerCase() === "html") {
37 | doc.replaceChild(first_elt, doc_elt);
38 | }
39 |
40 | result = doc;
41 | } else {
42 | result = real_parseFromString.apply(this, arguments);
43 | }
44 | return result;
45 | };
46 | }(DOMParser));
47 |
48 | /*
49 | * renderJs - Generic Gadget library renderer.
50 | * http://www.renderjs.org/documentation
51 | */
52 | (function (document, window, RSVP, DOMParser, Channel, undefined) {
53 | "use strict";
54 |
55 | var gadget_model_dict = {},
56 | javascript_registration_dict = {},
57 | stylesheet_registration_dict = {},
58 | gadget_loading_klass,
59 | loading_gadget_promise,
60 | renderJS;
61 |
62 | /////////////////////////////////////////////////////////////////
63 | // RenderJSGadget
64 | /////////////////////////////////////////////////////////////////
65 | function RenderJSGadget() {
66 | if (!(this instanceof RenderJSGadget)) {
67 | return new RenderJSGadget();
68 | }
69 | }
70 | RenderJSGadget.prototype.title = "";
71 | RenderJSGadget.prototype.interface_list = [];
72 | RenderJSGadget.prototype.path = "";
73 | RenderJSGadget.prototype.html = "";
74 | RenderJSGadget.prototype.required_css_list = [];
75 | RenderJSGadget.prototype.required_js_list = [];
76 |
77 | RenderJSGadget.ready_list = [];
78 | RenderJSGadget.ready = function (callback) {
79 | this.ready_list.push(callback);
80 | return this;
81 | };
82 |
83 | /////////////////////////////////////////////////////////////////
84 | // RenderJSGadget.declareMethod
85 | /////////////////////////////////////////////////////////////////
86 | RenderJSGadget.declareMethod = function (name, callback) {
87 | this.prototype[name] = function () {
88 | var context = this,
89 | argument_list = arguments;
90 |
91 | return new RSVP.Queue()
92 | .push(function () {
93 | return callback.apply(context, argument_list);
94 | });
95 | };
96 | // Allow chain
97 | return this;
98 | };
99 |
100 | RenderJSGadget
101 | .declareMethod('getInterfaceList', function () {
102 | // Returns the list of gadget prototype
103 | return this.interface_list;
104 | })
105 | .declareMethod('getRequiredCSSList', function () {
106 | // Returns a list of CSS required by the gadget
107 | return this.required_css_list;
108 | })
109 | .declareMethod('getRequiredJSList', function () {
110 | // Returns a list of JS required by the gadget
111 | return this.required_js_list;
112 | })
113 | .declareMethod('getPath', function () {
114 | // Returns the path of the code of a gadget
115 | return this.path;
116 | })
117 | .declareMethod('getTitle', function () {
118 | // Returns the title of a gadget
119 | return this.title;
120 | })
121 | .declareMethod('getElement', function () {
122 | // Returns the DOM Element of a gadget
123 | if (this.element === undefined) {
124 | throw new Error("No element defined");
125 | }
126 | return this.element;
127 | });
128 |
129 | /////////////////////////////////////////////////////////////////
130 | // RenderJSEmbeddedGadget
131 | /////////////////////////////////////////////////////////////////
132 | // Class inheritance
133 | function RenderJSEmbeddedGadget() {
134 | if (!(this instanceof RenderJSEmbeddedGadget)) {
135 | return new RenderJSEmbeddedGadget();
136 | }
137 | RenderJSGadget.call(this);
138 | }
139 | RenderJSEmbeddedGadget.ready_list = [];
140 | RenderJSEmbeddedGadget.ready =
141 | RenderJSGadget.ready;
142 | RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
143 | RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;
144 |
145 | /////////////////////////////////////////////////////////////////
146 | // privateDeclarePublicGadget
147 | /////////////////////////////////////////////////////////////////
148 | function privateDeclarePublicGadget(url, options) {
149 | var gadget_instance;
150 | if (options.element === undefined) {
151 | options.element = document.createElement("div");
152 | }
153 | return new RSVP.Queue()
154 | .push(function () {
155 | return renderJS.declareGadgetKlass(url);
156 | })
157 | // Get the gadget class and instanciate it
158 | .push(function (Klass) {
159 | var i,
160 | template_node_list = Klass.template_element.body.childNodes;
161 | gadget_loading_klass = Klass;
162 | gadget_instance = new Klass();
163 | gadget_instance.element = options.element;
164 | for (i = 0; i < template_node_list.length; i += 1) {
165 | gadget_instance.element.appendChild(
166 | template_node_list[i].cloneNode(true)
167 | );
168 | }
169 | // Load dependencies if needed
170 | return RSVP.all([
171 | gadget_instance.getRequiredJSList(),
172 | gadget_instance.getRequiredCSSList()
173 | ]);
174 | })
175 | // Load all JS/CSS
176 | .push(function (all_list) {
177 | var parameter_list = [],
178 | i;
179 | // Load JS
180 | for (i = 0; i < all_list[0].length; i += 1) {
181 | parameter_list.push(renderJS.declareJS(all_list[0][i]));
182 | }
183 | // Load CSS
184 | for (i = 0; i < all_list[1].length; i += 1) {
185 | parameter_list.push(renderJS.declareCSS(all_list[1][i]));
186 | }
187 | return RSVP.all(parameter_list);
188 | })
189 | .push(function () {
190 | return gadget_instance;
191 | });
192 | }
193 |
194 | /////////////////////////////////////////////////////////////////
195 | // RenderJSIframeGadget
196 | /////////////////////////////////////////////////////////////////
197 | function RenderJSIframeGadget() {
198 | if (!(this instanceof RenderJSIframeGadget)) {
199 | return new RenderJSIframeGadget();
200 | }
201 | RenderJSGadget.call(this);
202 | }
203 | RenderJSIframeGadget.ready_list = [];
204 | RenderJSIframeGadget.ready =
205 | RenderJSGadget.ready;
206 | RenderJSIframeGadget.prototype = new RenderJSGadget();
207 | RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;
208 |
209 | /////////////////////////////////////////////////////////////////
210 | // privateDeclareIframeGadget
211 | /////////////////////////////////////////////////////////////////
212 | function privateDeclareIframeGadget(url, options) {
213 | var gadget_instance,
214 | iframe,
215 | node,
216 | iframe_loading_deferred = RSVP.defer();
217 |
218 | if (options.element === undefined) {
219 | throw new Error("DOM element is required to create Iframe Gadget " +
220 | url);
221 | }
222 |
223 | // Check if the element is attached to the DOM
224 | node = options.element.parentNode;
225 | while (node !== null) {
226 | if (node === document) {
227 | break;
228 | }
229 | node = node.parentNode;
230 | }
231 | if (node === null) {
232 | throw new Error("The parent element is not attached to the DOM for " +
233 | url);
234 | }
235 |
236 | gadget_instance = new RenderJSIframeGadget();
237 | iframe = document.createElement("iframe");
238 | // gadget_instance.element.setAttribute("seamless", "seamless");
239 | iframe.setAttribute("src", url);
240 | gadget_instance.path = url;
241 | gadget_instance.element = options.element;
242 |
243 | // Attach it to the DOM
244 | options.element.appendChild(iframe);
245 |
246 | // XXX Manage unbind when deleting the gadget
247 |
248 | // Create the communication channel with the iframe
249 | gadget_instance.chan = Channel.build({
250 | window: iframe.contentWindow,
251 | origin: "*",
252 | scope: "renderJS"
253 | });
254 |
255 | // Create new method from the declareMethod call inside the iframe
256 | gadget_instance.chan.bind("declareMethod", function (trans, method_name) {
257 | gadget_instance[method_name] = function () {
258 | var argument_list = arguments;
259 | return new RSVP.Promise(function (resolve, reject) {
260 | gadget_instance.chan.call({
261 | method: "methodCall",
262 | params: [
263 | method_name,
264 | Array.prototype.slice.call(argument_list, 0)],
265 | success: function (s) {
266 | resolve(s);
267 | },
268 | error: function (e) {
269 | reject(e);
270 | }
271 | });
272 | });
273 | };
274 | return "OK";
275 | });
276 |
277 | // Wait for the iframe to be loaded before continuing
278 | gadget_instance.chan.bind("ready", function (trans) {
279 | iframe_loading_deferred.resolve(gadget_instance);
280 | return "OK";
281 | });
282 | gadget_instance.chan.bind("failed", function (trans, params) {
283 | iframe_loading_deferred.reject(params);
284 | return "OK";
285 | });
286 | return RSVP.any([
287 | iframe_loading_deferred.promise,
288 | // Timeout to prevent non renderJS embeddable gadget
289 | // XXX Maybe using iframe.onload/onerror would be safer?
290 | RSVP.timeout(5000)
291 | ]);
292 | }
293 |
294 | /////////////////////////////////////////////////////////////////
295 | // RenderJSGadget.declareGadget
296 | /////////////////////////////////////////////////////////////////
297 | RenderJSGadget.prototype.declareGadget = function (url, options) {
298 | var queue,
299 | previous_loading_gadget_promise = loading_gadget_promise;
300 |
301 | if (options === undefined) {
302 | options = {};
303 | }
304 | if (options.sandbox === undefined) {
305 | options.sandbox = "public";
306 | }
307 |
308 | // Change the global variable to update the loading queue
309 | queue = new RSVP.Queue()
310 | // Wait for previous gadget loading to finish first
311 | .push(function () {
312 | return previous_loading_gadget_promise;
313 | })
314 | .push(function () {
315 | var method;
316 | if (options.sandbox === "public") {
317 | method = privateDeclarePublicGadget;
318 | } else if (options.sandbox === "iframe") {
319 | method = privateDeclareIframeGadget;
320 | } else {
321 | throw new Error("Unsupported sandbox options '" +
322 | options.sandbox + "'");
323 | }
324 | return method(url, options);
325 | })
326 | // Set the HTML context
327 | .push(function (gadget_instance) {
328 | var i;
329 | // Drop the current loading klass info used by selector
330 | gadget_loading_klass = undefined;
331 | // Trigger calling of all ready callback
332 | function ready_wrapper() {
333 | return gadget_instance;
334 | }
335 | for (i = 0; i < gadget_instance.constructor.ready_list.length;
336 | i += 1) {
337 | // Put a timeout?
338 | queue.push(gadget_instance.constructor.ready_list[i]);
339 | // Always return the gadget instance after ready function
340 | queue.push(ready_wrapper);
341 | }
342 | return gadget_instance;
343 | })
344 | .push(undefined, function (e) {
345 | // Drop the current loading klass info used by selector
346 | // even in case of error
347 | gadget_loading_klass = undefined;
348 | throw e;
349 | });
350 | loading_gadget_promise = queue;
351 | return loading_gadget_promise;
352 | };
353 |
354 | /////////////////////////////////////////////////////////////////
355 | // renderJS selector
356 | /////////////////////////////////////////////////////////////////
357 | renderJS = function (selector) {
358 | var result;
359 | if (selector === window) {
360 | // window is the 'this' value when loading a javascript file
361 | // In this case, use the current loading gadget constructor
362 | result = gadget_loading_klass;
363 | } else if (selector instanceof RenderJSGadget) {
364 | result = selector;
365 | }
366 | if (result === undefined) {
367 | throw new Error("Unknown selector '" + selector + "'");
368 | }
369 | return result;
370 | };
371 |
372 | /////////////////////////////////////////////////////////////////
373 | // renderJS.declareJS
374 | /////////////////////////////////////////////////////////////////
375 | renderJS.declareJS = function (url) {
376 | // Prevent infinite recursion if loading render.js
377 | // more than once
378 | var result;
379 | if (javascript_registration_dict.hasOwnProperty(url)) {
380 | result = RSVP.resolve();
381 | } else {
382 | result = new RSVP.Promise(function (resolve, reject) {
383 | var newScript;
384 | newScript = document.createElement('script');
385 | newScript.type = 'text/javascript';
386 | newScript.src = url;
387 | newScript.onload = function () {
388 | javascript_registration_dict[url] = null;
389 | resolve();
390 | };
391 | newScript.onerror = function (e) {
392 | reject(e);
393 | };
394 | document.head.appendChild(newScript);
395 | });
396 | }
397 | return result;
398 | };
399 |
400 | /////////////////////////////////////////////////////////////////
401 | // renderJS.declareCSS
402 | /////////////////////////////////////////////////////////////////
403 | renderJS.declareCSS = function (url) {
404 | // https://github.com/furf/jquery-getCSS/blob/master/jquery.getCSS.js
405 | // No way to cleanly check if a css has been loaded
406 | // So, always resolve the promise...
407 | // http://requirejs.org/docs/faq-advanced.html#css
408 | var result;
409 | if (stylesheet_registration_dict.hasOwnProperty(url)) {
410 | result = RSVP.resolve();
411 | } else {
412 | result = new RSVP.Promise(function (resolve, reject) {
413 | var link;
414 | link = document.createElement('link');
415 | link.rel = 'stylesheet';
416 | link.type = 'text/css';
417 | link.href = url;
418 | link.onload = function () {
419 | stylesheet_registration_dict[url] = null;
420 | resolve();
421 | };
422 | link.onerror = function (e) {
423 | reject(e);
424 | };
425 | document.head.appendChild(link);
426 | });
427 | }
428 | return result;
429 | };
430 |
431 | /////////////////////////////////////////////////////////////////
432 | // renderJS.declareGadgetKlass
433 | /////////////////////////////////////////////////////////////////
434 | renderJS.declareGadgetKlass = function (url) {
435 | var result,
436 | xhr;
437 |
438 | function parse() {
439 | var tmp_constructor,
440 | key,
441 | parsed_html;
442 | if (!gadget_model_dict.hasOwnProperty(url)) {
443 | // Class inheritance
444 | tmp_constructor = function () {
445 | RenderJSGadget.call(this);
446 | };
447 | tmp_constructor.ready_list = [];
448 | tmp_constructor.declareMethod =
449 | RenderJSGadget.declareMethod;
450 | tmp_constructor.ready =
451 | RenderJSGadget.ready;
452 | tmp_constructor.prototype = new RenderJSGadget();
453 | tmp_constructor.prototype.constructor = tmp_constructor;
454 | tmp_constructor.prototype.path = url;
455 | // https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
456 | // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
457 | // https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
458 | tmp_constructor.template_element =
459 | (new DOMParser()).parseFromString(xhr.responseText, "text/html");
460 | parsed_html = renderJS.parseGadgetHTMLDocument(
461 | tmp_constructor.template_element
462 | );
463 | for (key in parsed_html) {
464 | if (parsed_html.hasOwnProperty(key)) {
465 | tmp_constructor.prototype[key] = parsed_html[key];
466 | }
467 | }
468 |
469 | gadget_model_dict[url] = tmp_constructor;
470 | }
471 |
472 | return gadget_model_dict[url];
473 | }
474 |
475 | function resolver(resolve, reject) {
476 | function handler() {
477 | var tmp_result;
478 | try {
479 | if (xhr.readyState === 0) {
480 | // UNSENT
481 | reject(xhr);
482 | } else if (xhr.readyState === 4) {
483 | // DONE
484 | if ((xhr.status < 200) || (xhr.status >= 300) ||
485 | (!/^text\/html[;]?/.test(
486 | xhr.getResponseHeader("Content-Type") || ""
487 | ))) {
488 | reject(xhr);
489 | } else {
490 | tmp_result = parse();
491 | resolve(tmp_result);
492 | }
493 | }
494 | } catch (e) {
495 | reject(e);
496 | }
497 | }
498 |
499 | xhr = new XMLHttpRequest();
500 | xhr.open("GET", url);
501 | xhr.onreadystatechange = handler;
502 | xhr.setRequestHeader('Accept', 'text/html');
503 | xhr.withCredentials = true;
504 | xhr.send();
505 | }
506 |
507 | function canceller() {
508 | if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
509 | xhr.abort();
510 | }
511 | }
512 |
513 | if (gadget_model_dict.hasOwnProperty(url)) {
514 | // Return klass object if it already exists
515 | result = RSVP.resolve(gadget_model_dict[url]);
516 | } else {
517 | // Fetch the HTML page and parse it
518 | result = new RSVP.Promise(resolver, canceller);
519 | }
520 | return result;
521 | };
522 |
523 | /////////////////////////////////////////////////////////////////
524 | // renderJS.clearGadgetKlassList
525 | /////////////////////////////////////////////////////////////////
526 | // For test purpose only
527 | renderJS.clearGadgetKlassList = function () {
528 | gadget_model_dict = {};
529 | javascript_registration_dict = {};
530 | stylesheet_registration_dict = {};
531 | };
532 |
533 | /////////////////////////////////////////////////////////////////
534 | // renderJS.parseGadgetHTMLDocument
535 | /////////////////////////////////////////////////////////////////
536 | renderJS.parseGadgetHTMLDocument = function (document_element) {
537 | var settings = {
538 | title: "",
539 | interface_list: [],
540 | required_css_list: [],
541 | required_js_list: []
542 | },
543 | i,
544 | element;
545 | if (document_element.nodeType === 9) {
546 | settings.title = document_element.title;
547 |
548 | for (i = 0; i < document_element.head.children.length; i += 1) {
549 | element = document_element.head.children[i];
550 | if (element.href !== null) {
551 | // XXX Manage relative URL during extraction of URLs
552 | // element.href returns absolute URL in firefox but "" in chrome;
553 | if (element.rel === "stylesheet") {
554 | settings.required_css_list.push(element.getAttribute("href"));
555 | } else if (element.type === "text/javascript") {
556 | settings.required_js_list.push(element.getAttribute("src"));
557 | } else if (element.rel === "http://www.renderjs.org/rel/interface") {
558 | settings.interface_list.push(element.getAttribute("href"));
559 | }
560 | }
561 | }
562 | } else {
563 | throw new Error("The first parameter should be an HTMLDocument");
564 | }
565 | return settings;
566 | };
567 |
568 | /////////////////////////////////////////////////////////////////
569 | // global
570 | /////////////////////////////////////////////////////////////////
571 | window.rJS = window.renderJS = renderJS;
572 | window.RenderJSGadget = RenderJSGadget;
573 | window.RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
574 | window.RenderJSIframeGadget = RenderJSIframeGadget;
575 |
576 | ///////////////////////////////////////////////////
577 | // Bootstrap process. Register the self gadget.
578 | ///////////////////////////////////////////////////
579 |
580 | function bootstrap() {
581 | var url = window.location.href,
582 | tmp_constructor,
583 | root_gadget,
584 | declare_method_count = 0,
585 | embedded_channel,
586 | notifyReady,
587 | notifyDeclareMethod,
588 | gadget_ready = false;
589 |
590 |
591 | // Create the gadget class for the current url
592 | if (gadget_model_dict.hasOwnProperty(url)) {
593 | throw new Error("bootstrap should not be called twice");
594 | }
595 | loading_gadget_promise = new RSVP.Promise(function (resolve, reject) {
596 | if (window.self === window.top) {
597 | // XXX Copy/Paste from declareGadgetKlass
598 | tmp_constructor = function () {
599 | RenderJSGadget.call(this);
600 | };
601 | tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
602 | tmp_constructor.ready_list = [];
603 | tmp_constructor.ready = RenderJSGadget.ready;
604 | tmp_constructor.prototype = new RenderJSGadget();
605 | tmp_constructor.prototype.constructor = tmp_constructor;
606 | tmp_constructor.prototype.path = url;
607 | gadget_model_dict[url] = tmp_constructor;
608 |
609 | // Create the root gadget instance and put it in the loading stack
610 | root_gadget = new gadget_model_dict[url]();
611 |
612 | } else {
613 | // Create the communication channel
614 | embedded_channel = Channel.build({
615 | window: window.parent,
616 | origin: "*",
617 | scope: "renderJS"
618 | });
619 | // Create the root gadget instance and put it in the loading stack
620 | tmp_constructor = RenderJSEmbeddedGadget;
621 | root_gadget = new RenderJSEmbeddedGadget();
622 |
623 | // Bind calls to renderJS method on the instance
624 | embedded_channel.bind("methodCall", function (trans, v) {
625 | root_gadget[v[0]].apply(root_gadget, v[1]).then(function (g) {
626 | trans.complete(g);
627 | }).fail(function (e) {
628 | trans.error(e.toString());
629 | });
630 | trans.delayReturn(true);
631 | });
632 |
633 | // Notify parent about gadget instanciation
634 | notifyReady = function () {
635 | if ((declare_method_count === 0) && (gadget_ready === true)) {
636 | embedded_channel.notify({method: "ready"});
637 | }
638 | };
639 |
640 | // Inform parent gadget about declareMethod calls here.
641 | notifyDeclareMethod = function (name) {
642 | declare_method_count += 1;
643 | embedded_channel.call({
644 | method: "declareMethod",
645 | params: name,
646 | success: function () {
647 | declare_method_count -= 1;
648 | notifyReady();
649 | },
650 | error: function () {
651 | declare_method_count -= 1;
652 | }
653 | });
654 | };
655 |
656 | notifyDeclareMethod("getInterfaceList");
657 | notifyDeclareMethod("getRequiredCSSList");
658 | notifyDeclareMethod("getRequiredJSList");
659 | notifyDeclareMethod("getPath");
660 | notifyDeclareMethod("getTitle");
661 |
662 | // Surcharge declareMethod to inform parent window
663 | tmp_constructor.declareMethod = function (name, callback) {
664 | var result = RenderJSGadget.declareMethod.apply(
665 | this,
666 | [name, callback]
667 | );
668 | notifyDeclareMethod(name);
669 | return result;
670 | };
671 | }
672 |
673 | gadget_loading_klass = tmp_constructor;
674 |
675 | function init() {
676 | // XXX HTML properties can only be set when the DOM is fully loaded
677 | var settings = renderJS.parseGadgetHTMLDocument(document),
678 | j,
679 | key;
680 | for (key in settings) {
681 | if (settings.hasOwnProperty(key)) {
682 | tmp_constructor.prototype[key] = settings[key];
683 | }
684 | }
685 | tmp_constructor.template_element = document.createElement("div");
686 | root_gadget.element = document.body;
687 | for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
688 | tmp_constructor.template_element.appendChild(
689 | root_gadget.element.childNodes[j].cloneNode(true)
690 | );
691 | }
692 | RSVP.all([root_gadget.getRequiredJSList(),
693 | root_gadget.getRequiredCSSList()])
694 | .then(function (all_list) {
695 | var i,
696 | js_list = all_list[0],
697 | css_list = all_list[1],
698 | queue;
699 | for (i = 0; i < js_list.length; i += 1) {
700 | javascript_registration_dict[js_list[i]] = null;
701 | }
702 | for (i = 0; i < css_list.length; i += 1) {
703 | stylesheet_registration_dict[css_list[i]] = null;
704 | }
705 | gadget_loading_klass = undefined;
706 | queue = new RSVP.Queue();
707 | function ready_wrapper() {
708 | return root_gadget;
709 | }
710 | queue.push(ready_wrapper);
711 | for (i = 0; i < tmp_constructor.ready_list.length; i += 1) {
712 | // Put a timeout?
713 | queue.push(tmp_constructor.ready_list[i]);
714 | // Always return the gadget instance after ready function
715 | queue.push(ready_wrapper);
716 | }
717 | queue.push(resolve, function (e) {
718 | reject(e);
719 | throw e;
720 | });
721 | return queue;
722 | }).fail(function (e) {
723 | reject(e);
724 | });
725 | }
726 | document.addEventListener('DOMContentLoaded', init, false);
727 | });
728 |
729 | if (window.self !== window.top) {
730 | // Inform parent window that gadget is correctly loaded
731 | loading_gadget_promise.then(function () {
732 | gadget_ready = true;
733 | notifyReady();
734 | }).fail(function (e) {
735 | embedded_channel.notify({method: "failed", params: e.toString()});
736 | throw e;
737 | });
738 | }
739 |
740 | }
741 | bootstrap();
742 |
743 | }(document, window, RSVP, DOMParser, Channel));
744 |
--------------------------------------------------------------------------------
/dist/renderjs-0.3.3.js:
--------------------------------------------------------------------------------
1 | /*! RenderJs */
2 |
3 | /*
4 | * DOMParser HTML extension
5 | * 2012-09-04
6 | *
7 | * By Eli Grey, http://eligrey.com
8 | * Public domain.
9 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
10 | */
11 | /*! @source https://gist.github.com/1129031 */
12 | (function (DOMParser) {
13 | "use strict";
14 | var DOMParser_proto = DOMParser.prototype,
15 | real_parseFromString = DOMParser_proto.parseFromString;
16 |
17 | // Firefox/Opera/IE throw errors on unsupported types
18 | try {
19 | // WebKit returns null on unsupported types
20 | if ((new DOMParser()).parseFromString("", "text/html")) {
21 | // text/html parsing is natively supported
22 | return;
23 | }
24 | } catch (ignore) {}
25 |
26 | DOMParser_proto.parseFromString = function (markup, type) {
27 | var result, doc, doc_elt, first_elt;
28 | if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
29 | doc = document.implementation.createHTMLDocument("");
30 | doc_elt = doc.documentElement;
31 |
32 | doc_elt.innerHTML = markup;
33 | first_elt = doc_elt.firstElementChild;
34 |
35 | if (doc_elt.childElementCount === 1
36 | && first_elt.localName.toLowerCase() === "html") {
37 | doc.replaceChild(first_elt, doc_elt);
38 | }
39 |
40 | result = doc;
41 | } else {
42 | result = real_parseFromString.apply(this, arguments);
43 | }
44 | return result;
45 | };
46 | }(DOMParser));
47 |
48 | /*
49 | * renderJs - Generic Gadget library renderer.
50 | * http://www.renderjs.org/documentation
51 | */
52 | (function (document, window, RSVP, DOMParser, Channel, undefined) {
53 | "use strict";
54 |
55 | var gadget_model_dict = {},
56 | javascript_registration_dict = {},
57 | stylesheet_registration_dict = {},
58 | gadget_loading_klass,
59 | loading_gadget_promise,
60 | renderJS;
61 |
62 | /////////////////////////////////////////////////////////////////
63 | // RenderJSGadget
64 | /////////////////////////////////////////////////////////////////
65 | function RenderJSGadget() {
66 | if (!(this instanceof RenderJSGadget)) {
67 | return new RenderJSGadget();
68 | }
69 | }
70 | RenderJSGadget.prototype.title = "";
71 | RenderJSGadget.prototype.interface_list = [];
72 | RenderJSGadget.prototype.path = "";
73 | RenderJSGadget.prototype.html = "";
74 | RenderJSGadget.prototype.required_css_list = [];
75 | RenderJSGadget.prototype.required_js_list = [];
76 |
77 | RenderJSGadget.ready_list = [];
78 | RenderJSGadget.ready = function (callback) {
79 | this.ready_list.push(callback);
80 | return this;
81 | };
82 |
83 | /////////////////////////////////////////////////////////////////
84 | // RenderJSGadget.declareMethod
85 | /////////////////////////////////////////////////////////////////
86 | RenderJSGadget.declareMethod = function (name, callback) {
87 | this.prototype[name] = function () {
88 | var context = this,
89 | argument_list = arguments;
90 |
91 | return new RSVP.Queue()
92 | .push(function () {
93 | return callback.apply(context, argument_list);
94 | });
95 | };
96 | // Allow chain
97 | return this;
98 | };
99 |
100 | RenderJSGadget
101 | .declareMethod('getInterfaceList', function () {
102 | // Returns the list of gadget prototype
103 | return this.interface_list;
104 | })
105 | .declareMethod('getRequiredCSSList', function () {
106 | // Returns a list of CSS required by the gadget
107 | return this.required_css_list;
108 | })
109 | .declareMethod('getRequiredJSList', function () {
110 | // Returns a list of JS required by the gadget
111 | return this.required_js_list;
112 | })
113 | .declareMethod('getPath', function () {
114 | // Returns the path of the code of a gadget
115 | return this.path;
116 | })
117 | .declareMethod('getTitle', function () {
118 | // Returns the title of a gadget
119 | return this.title;
120 | })
121 | .declareMethod('getElement', function () {
122 | // Returns the DOM Element of a gadget
123 | if (this.element === undefined) {
124 | throw new Error("No element defined");
125 | }
126 | return this.element;
127 | });
128 |
129 | /////////////////////////////////////////////////////////////////
130 | // RenderJSEmbeddedGadget
131 | /////////////////////////////////////////////////////////////////
132 | // Class inheritance
133 | function RenderJSEmbeddedGadget() {
134 | if (!(this instanceof RenderJSEmbeddedGadget)) {
135 | return new RenderJSEmbeddedGadget();
136 | }
137 | RenderJSGadget.call(this);
138 | }
139 | RenderJSEmbeddedGadget.ready_list = [];
140 | RenderJSEmbeddedGadget.ready =
141 | RenderJSGadget.ready;
142 | RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
143 | RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;
144 |
145 | /////////////////////////////////////////////////////////////////
146 | // privateDeclarePublicGadget
147 | /////////////////////////////////////////////////////////////////
148 | function privateDeclarePublicGadget(url, options) {
149 | var gadget_instance;
150 | if (options.element === undefined) {
151 | options.element = document.createElement("div");
152 | }
153 | return new RSVP.Queue()
154 | .push(function () {
155 | return renderJS.declareGadgetKlass(url);
156 | })
157 | // Get the gadget class and instanciate it
158 | .push(function (Klass) {
159 | var i,
160 | template_node_list = Klass.template_element.body.childNodes;
161 | gadget_loading_klass = Klass;
162 | gadget_instance = new Klass();
163 | gadget_instance.element = options.element;
164 | for (i = 0; i < template_node_list.length; i += 1) {
165 | gadget_instance.element.appendChild(
166 | template_node_list[i].cloneNode(true)
167 | );
168 | }
169 | // Load dependencies if needed
170 | return RSVP.all([
171 | gadget_instance.getRequiredJSList(),
172 | gadget_instance.getRequiredCSSList()
173 | ]);
174 | })
175 | // Load all JS/CSS
176 | .push(function (all_list) {
177 | var parameter_list = [],
178 | i;
179 | // Load JS
180 | for (i = 0; i < all_list[0].length; i += 1) {
181 | parameter_list.push(renderJS.declareJS(all_list[0][i]));
182 | }
183 | // Load CSS
184 | for (i = 0; i < all_list[1].length; i += 1) {
185 | parameter_list.push(renderJS.declareCSS(all_list[1][i]));
186 | }
187 | return RSVP.all(parameter_list);
188 | })
189 | .push(function () {
190 | return gadget_instance;
191 | });
192 | }
193 |
194 | /////////////////////////////////////////////////////////////////
195 | // RenderJSIframeGadget
196 | /////////////////////////////////////////////////////////////////
197 | function RenderJSIframeGadget() {
198 | if (!(this instanceof RenderJSIframeGadget)) {
199 | return new RenderJSIframeGadget();
200 | }
201 | RenderJSGadget.call(this);
202 | }
203 | RenderJSIframeGadget.ready_list = [];
204 | RenderJSIframeGadget.ready =
205 | RenderJSGadget.ready;
206 | RenderJSIframeGadget.prototype = new RenderJSGadget();
207 | RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;
208 |
209 | /////////////////////////////////////////////////////////////////
210 | // privateDeclareIframeGadget
211 | /////////////////////////////////////////////////////////////////
212 | function privateDeclareIframeGadget(url, options) {
213 | var gadget_instance,
214 | iframe,
215 | node,
216 | iframe_loading_deferred = RSVP.defer();
217 |
218 | if (options.element === undefined) {
219 | throw new Error("DOM element is required to create Iframe Gadget " +
220 | url);
221 | }
222 |
223 | // Check if the element is attached to the DOM
224 | node = options.element.parentNode;
225 | while (node !== null) {
226 | if (node === document) {
227 | break;
228 | }
229 | node = node.parentNode;
230 | }
231 | if (node === null) {
232 | throw new Error("The parent element is not attached to the DOM for " +
233 | url);
234 | }
235 |
236 | gadget_instance = new RenderJSIframeGadget();
237 | iframe = document.createElement("iframe");
238 | // gadget_instance.element.setAttribute("seamless", "seamless");
239 | iframe.setAttribute("src", url);
240 | gadget_instance.path = url;
241 | gadget_instance.element = options.element;
242 |
243 | // Attach it to the DOM
244 | options.element.appendChild(iframe);
245 |
246 | // XXX Manage unbind when deleting the gadget
247 |
248 | // Create the communication channel with the iframe
249 | gadget_instance.chan = Channel.build({
250 | window: iframe.contentWindow,
251 | origin: "*",
252 | scope: "renderJS"
253 | });
254 |
255 | // Create new method from the declareMethod call inside the iframe
256 | gadget_instance.chan.bind("declareMethod", function (trans, method_name) {
257 | gadget_instance[method_name] = function () {
258 | var argument_list = arguments;
259 | return new RSVP.Promise(function (resolve, reject) {
260 | gadget_instance.chan.call({
261 | method: "methodCall",
262 | params: [
263 | method_name,
264 | Array.prototype.slice.call(argument_list, 0)],
265 | success: function (s) {
266 | resolve(s);
267 | },
268 | error: function (e) {
269 | reject(e);
270 | }
271 | });
272 | });
273 | };
274 | return "OK";
275 | });
276 |
277 | // Wait for the iframe to be loaded before continuing
278 | gadget_instance.chan.bind("ready", function (trans) {
279 | iframe_loading_deferred.resolve(gadget_instance);
280 | return "OK";
281 | });
282 | gadget_instance.chan.bind("failed", function (trans, params) {
283 | iframe_loading_deferred.reject(params);
284 | return "OK";
285 | });
286 | return RSVP.any([
287 | iframe_loading_deferred.promise,
288 | // Timeout to prevent non renderJS embeddable gadget
289 | // XXX Maybe using iframe.onload/onerror would be safer?
290 | RSVP.timeout(5000)
291 | ]);
292 | }
293 |
294 | /////////////////////////////////////////////////////////////////
295 | // RenderJSGadget.declareGadget
296 | /////////////////////////////////////////////////////////////////
297 | RenderJSGadget.prototype.declareGadget = function (url, options) {
298 | var queue,
299 | previous_loading_gadget_promise = loading_gadget_promise;
300 |
301 | if (options === undefined) {
302 | options = {};
303 | }
304 | if (options.sandbox === undefined) {
305 | options.sandbox = "public";
306 | }
307 |
308 | // Change the global variable to update the loading queue
309 | queue = new RSVP.Queue()
310 | // Wait for previous gadget loading to finish first
311 | .push(function () {
312 | return previous_loading_gadget_promise;
313 | })
314 | .push(undefined, function () {
315 | // Forget previous declareGadget error
316 | return;
317 | })
318 | .push(function () {
319 | var method;
320 | if (options.sandbox === "public") {
321 | method = privateDeclarePublicGadget;
322 | } else if (options.sandbox === "iframe") {
323 | method = privateDeclareIframeGadget;
324 | } else {
325 | throw new Error("Unsupported sandbox options '" +
326 | options.sandbox + "'");
327 | }
328 | return method(url, options);
329 | })
330 | // Set the HTML context
331 | .push(function (gadget_instance) {
332 | var i;
333 | // Drop the current loading klass info used by selector
334 | gadget_loading_klass = undefined;
335 | // Trigger calling of all ready callback
336 | function ready_wrapper() {
337 | return gadget_instance;
338 | }
339 | for (i = 0; i < gadget_instance.constructor.ready_list.length;
340 | i += 1) {
341 | // Put a timeout?
342 | queue.push(gadget_instance.constructor.ready_list[i]);
343 | // Always return the gadget instance after ready function
344 | queue.push(ready_wrapper);
345 | }
346 | return gadget_instance;
347 | })
348 | .push(undefined, function (e) {
349 | // Drop the current loading klass info used by selector
350 | // even in case of error
351 | gadget_loading_klass = undefined;
352 | throw e;
353 | });
354 | loading_gadget_promise = queue;
355 | return loading_gadget_promise;
356 | };
357 |
358 | /////////////////////////////////////////////////////////////////
359 | // renderJS selector
360 | /////////////////////////////////////////////////////////////////
361 | renderJS = function (selector) {
362 | var result;
363 | if (selector === window) {
364 | // window is the 'this' value when loading a javascript file
365 | // In this case, use the current loading gadget constructor
366 | result = gadget_loading_klass;
367 | } else if (selector instanceof RenderJSGadget) {
368 | result = selector;
369 | }
370 | if (result === undefined) {
371 | throw new Error("Unknown selector '" + selector + "'");
372 | }
373 | return result;
374 | };
375 |
376 | /////////////////////////////////////////////////////////////////
377 | // renderJS.declareJS
378 | /////////////////////////////////////////////////////////////////
379 | renderJS.declareJS = function (url) {
380 | // Prevent infinite recursion if loading render.js
381 | // more than once
382 | var result;
383 | if (javascript_registration_dict.hasOwnProperty(url)) {
384 | result = RSVP.resolve();
385 | } else {
386 | result = new RSVP.Promise(function (resolve, reject) {
387 | var newScript;
388 | newScript = document.createElement('script');
389 | newScript.type = 'text/javascript';
390 | newScript.src = url;
391 | newScript.onload = function () {
392 | javascript_registration_dict[url] = null;
393 | resolve();
394 | };
395 | newScript.onerror = function (e) {
396 | reject(e);
397 | };
398 | document.head.appendChild(newScript);
399 | });
400 | }
401 | return result;
402 | };
403 |
404 | /////////////////////////////////////////////////////////////////
405 | // renderJS.declareCSS
406 | /////////////////////////////////////////////////////////////////
407 | renderJS.declareCSS = function (url) {
408 | // https://github.com/furf/jquery-getCSS/blob/master/jquery.getCSS.js
409 | // No way to cleanly check if a css has been loaded
410 | // So, always resolve the promise...
411 | // http://requirejs.org/docs/faq-advanced.html#css
412 | var result;
413 | if (stylesheet_registration_dict.hasOwnProperty(url)) {
414 | result = RSVP.resolve();
415 | } else {
416 | result = new RSVP.Promise(function (resolve, reject) {
417 | var link;
418 | link = document.createElement('link');
419 | link.rel = 'stylesheet';
420 | link.type = 'text/css';
421 | link.href = url;
422 | link.onload = function () {
423 | stylesheet_registration_dict[url] = null;
424 | resolve();
425 | };
426 | link.onerror = function (e) {
427 | reject(e);
428 | };
429 | document.head.appendChild(link);
430 | });
431 | }
432 | return result;
433 | };
434 |
435 | /////////////////////////////////////////////////////////////////
436 | // renderJS.declareGadgetKlass
437 | /////////////////////////////////////////////////////////////////
438 | renderJS.declareGadgetKlass = function (url) {
439 | var result,
440 | xhr;
441 |
442 | function parse() {
443 | var tmp_constructor,
444 | key,
445 | parsed_html;
446 | if (!gadget_model_dict.hasOwnProperty(url)) {
447 | // Class inheritance
448 | tmp_constructor = function () {
449 | RenderJSGadget.call(this);
450 | };
451 | tmp_constructor.ready_list = [];
452 | tmp_constructor.declareMethod =
453 | RenderJSGadget.declareMethod;
454 | tmp_constructor.ready =
455 | RenderJSGadget.ready;
456 | tmp_constructor.prototype = new RenderJSGadget();
457 | tmp_constructor.prototype.constructor = tmp_constructor;
458 | tmp_constructor.prototype.path = url;
459 | // https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
460 | // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
461 | // https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
462 | tmp_constructor.template_element =
463 | (new DOMParser()).parseFromString(xhr.responseText, "text/html");
464 | parsed_html = renderJS.parseGadgetHTMLDocument(
465 | tmp_constructor.template_element
466 | );
467 | for (key in parsed_html) {
468 | if (parsed_html.hasOwnProperty(key)) {
469 | tmp_constructor.prototype[key] = parsed_html[key];
470 | }
471 | }
472 |
473 | gadget_model_dict[url] = tmp_constructor;
474 | }
475 |
476 | return gadget_model_dict[url];
477 | }
478 |
479 | function resolver(resolve, reject) {
480 | function handler() {
481 | var tmp_result;
482 | try {
483 | if (xhr.readyState === 0) {
484 | // UNSENT
485 | reject(xhr);
486 | } else if (xhr.readyState === 4) {
487 | // DONE
488 | if ((xhr.status < 200) || (xhr.status >= 300) ||
489 | (!/^text\/html[;]?/.test(
490 | xhr.getResponseHeader("Content-Type") || ""
491 | ))) {
492 | reject(xhr);
493 | } else {
494 | tmp_result = parse();
495 | resolve(tmp_result);
496 | }
497 | }
498 | } catch (e) {
499 | reject(e);
500 | }
501 | }
502 |
503 | xhr = new XMLHttpRequest();
504 | xhr.open("GET", url);
505 | xhr.onreadystatechange = handler;
506 | xhr.setRequestHeader('Accept', 'text/html');
507 | xhr.withCredentials = true;
508 | xhr.send();
509 | }
510 |
511 | function canceller() {
512 | if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
513 | xhr.abort();
514 | }
515 | }
516 |
517 | if (gadget_model_dict.hasOwnProperty(url)) {
518 | // Return klass object if it already exists
519 | result = RSVP.resolve(gadget_model_dict[url]);
520 | } else {
521 | // Fetch the HTML page and parse it
522 | result = new RSVP.Promise(resolver, canceller);
523 | }
524 | return result;
525 | };
526 |
527 | /////////////////////////////////////////////////////////////////
528 | // renderJS.clearGadgetKlassList
529 | /////////////////////////////////////////////////////////////////
530 | // For test purpose only
531 | renderJS.clearGadgetKlassList = function () {
532 | gadget_model_dict = {};
533 | javascript_registration_dict = {};
534 | stylesheet_registration_dict = {};
535 | };
536 |
537 | /////////////////////////////////////////////////////////////////
538 | // renderJS.parseGadgetHTMLDocument
539 | /////////////////////////////////////////////////////////////////
540 | renderJS.parseGadgetHTMLDocument = function (document_element) {
541 | var settings = {
542 | title: "",
543 | interface_list: [],
544 | required_css_list: [],
545 | required_js_list: []
546 | },
547 | i,
548 | element;
549 | if (document_element.nodeType === 9) {
550 | settings.title = document_element.title;
551 |
552 | for (i = 0; i < document_element.head.children.length; i += 1) {
553 | element = document_element.head.children[i];
554 | if (element.href !== null) {
555 | // XXX Manage relative URL during extraction of URLs
556 | // element.href returns absolute URL in firefox but "" in chrome;
557 | if (element.rel === "stylesheet") {
558 | settings.required_css_list.push(element.getAttribute("href"));
559 | } else if (element.type === "text/javascript") {
560 | settings.required_js_list.push(element.getAttribute("src"));
561 | } else if (element.rel === "http://www.renderjs.org/rel/interface") {
562 | settings.interface_list.push(element.getAttribute("href"));
563 | }
564 | }
565 | }
566 | } else {
567 | throw new Error("The first parameter should be an HTMLDocument");
568 | }
569 | return settings;
570 | };
571 |
572 | /////////////////////////////////////////////////////////////////
573 | // global
574 | /////////////////////////////////////////////////////////////////
575 | window.rJS = window.renderJS = renderJS;
576 | window.RenderJSGadget = RenderJSGadget;
577 | window.RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
578 | window.RenderJSIframeGadget = RenderJSIframeGadget;
579 |
580 | ///////////////////////////////////////////////////
581 | // Bootstrap process. Register the self gadget.
582 | ///////////////////////////////////////////////////
583 |
584 | function bootstrap() {
585 | var url = window.location.href,
586 | tmp_constructor,
587 | root_gadget,
588 | declare_method_count = 0,
589 | embedded_channel,
590 | notifyReady,
591 | notifyDeclareMethod,
592 | gadget_ready = false;
593 |
594 |
595 | // Create the gadget class for the current url
596 | if (gadget_model_dict.hasOwnProperty(url)) {
597 | throw new Error("bootstrap should not be called twice");
598 | }
599 | loading_gadget_promise = new RSVP.Promise(function (resolve, reject) {
600 | if (window.self === window.top) {
601 | // XXX Copy/Paste from declareGadgetKlass
602 | tmp_constructor = function () {
603 | RenderJSGadget.call(this);
604 | };
605 | tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
606 | tmp_constructor.ready_list = [];
607 | tmp_constructor.ready = RenderJSGadget.ready;
608 | tmp_constructor.prototype = new RenderJSGadget();
609 | tmp_constructor.prototype.constructor = tmp_constructor;
610 | tmp_constructor.prototype.path = url;
611 | gadget_model_dict[url] = tmp_constructor;
612 |
613 | // Create the root gadget instance and put it in the loading stack
614 | root_gadget = new gadget_model_dict[url]();
615 |
616 | } else {
617 | // Create the communication channel
618 | embedded_channel = Channel.build({
619 | window: window.parent,
620 | origin: "*",
621 | scope: "renderJS"
622 | });
623 | // Create the root gadget instance and put it in the loading stack
624 | tmp_constructor = RenderJSEmbeddedGadget;
625 | root_gadget = new RenderJSEmbeddedGadget();
626 |
627 | // Bind calls to renderJS method on the instance
628 | embedded_channel.bind("methodCall", function (trans, v) {
629 | root_gadget[v[0]].apply(root_gadget, v[1]).then(function (g) {
630 | trans.complete(g);
631 | }).fail(function (e) {
632 | trans.error(e.toString());
633 | });
634 | trans.delayReturn(true);
635 | });
636 |
637 | // Notify parent about gadget instanciation
638 | notifyReady = function () {
639 | if ((declare_method_count === 0) && (gadget_ready === true)) {
640 | embedded_channel.notify({method: "ready"});
641 | }
642 | };
643 |
644 | // Inform parent gadget about declareMethod calls here.
645 | notifyDeclareMethod = function (name) {
646 | declare_method_count += 1;
647 | embedded_channel.call({
648 | method: "declareMethod",
649 | params: name,
650 | success: function () {
651 | declare_method_count -= 1;
652 | notifyReady();
653 | },
654 | error: function () {
655 | declare_method_count -= 1;
656 | }
657 | });
658 | };
659 |
660 | notifyDeclareMethod("getInterfaceList");
661 | notifyDeclareMethod("getRequiredCSSList");
662 | notifyDeclareMethod("getRequiredJSList");
663 | notifyDeclareMethod("getPath");
664 | notifyDeclareMethod("getTitle");
665 |
666 | // Surcharge declareMethod to inform parent window
667 | tmp_constructor.declareMethod = function (name, callback) {
668 | var result = RenderJSGadget.declareMethod.apply(
669 | this,
670 | [name, callback]
671 | );
672 | notifyDeclareMethod(name);
673 | return result;
674 | };
675 | }
676 |
677 | gadget_loading_klass = tmp_constructor;
678 |
679 | function init() {
680 | // XXX HTML properties can only be set when the DOM is fully loaded
681 | var settings = renderJS.parseGadgetHTMLDocument(document),
682 | j,
683 | key;
684 | for (key in settings) {
685 | if (settings.hasOwnProperty(key)) {
686 | tmp_constructor.prototype[key] = settings[key];
687 | }
688 | }
689 | tmp_constructor.template_element = document.createElement("div");
690 | root_gadget.element = document.body;
691 | for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
692 | tmp_constructor.template_element.appendChild(
693 | root_gadget.element.childNodes[j].cloneNode(true)
694 | );
695 | }
696 | RSVP.all([root_gadget.getRequiredJSList(),
697 | root_gadget.getRequiredCSSList()])
698 | .then(function (all_list) {
699 | var i,
700 | js_list = all_list[0],
701 | css_list = all_list[1],
702 | queue;
703 | for (i = 0; i < js_list.length; i += 1) {
704 | javascript_registration_dict[js_list[i]] = null;
705 | }
706 | for (i = 0; i < css_list.length; i += 1) {
707 | stylesheet_registration_dict[css_list[i]] = null;
708 | }
709 | gadget_loading_klass = undefined;
710 | queue = new RSVP.Queue();
711 | function ready_wrapper() {
712 | return root_gadget;
713 | }
714 | queue.push(ready_wrapper);
715 | for (i = 0; i < tmp_constructor.ready_list.length; i += 1) {
716 | // Put a timeout?
717 | queue.push(tmp_constructor.ready_list[i]);
718 | // Always return the gadget instance after ready function
719 | queue.push(ready_wrapper);
720 | }
721 | queue.push(resolve, function (e) {
722 | reject(e);
723 | throw e;
724 | });
725 | return queue;
726 | }).fail(function (e) {
727 | reject(e);
728 | });
729 | }
730 | document.addEventListener('DOMContentLoaded', init, false);
731 | });
732 |
733 | if (window.self !== window.top) {
734 | // Inform parent window that gadget is correctly loaded
735 | loading_gadget_promise.then(function () {
736 | gadget_ready = true;
737 | notifyReady();
738 | }).fail(function (e) {
739 | embedded_channel.notify({method: "failed", params: e.toString()});
740 | throw e;
741 | });
742 | }
743 |
744 | }
745 | bootstrap();
746 |
747 | }(document, window, RSVP, DOMParser, Channel));
748 |
--------------------------------------------------------------------------------
/dist/renderjs-0.4.0.js:
--------------------------------------------------------------------------------
1 | /*! RenderJs */
2 |
3 | /*
4 | * DOMParser HTML extension
5 | * 2012-09-04
6 | *
7 | * By Eli Grey, http://eligrey.com
8 | * Public domain.
9 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
10 | */
11 | /*! @source https://gist.github.com/1129031 */
12 | (function (DOMParser) {
13 | "use strict";
14 | var DOMParser_proto = DOMParser.prototype,
15 | real_parseFromString = DOMParser_proto.parseFromString;
16 |
17 | // Firefox/Opera/IE throw errors on unsupported types
18 | try {
19 | // WebKit returns null on unsupported types
20 | if ((new DOMParser()).parseFromString("", "text/html")) {
21 | // text/html parsing is natively supported
22 | return;
23 | }
24 | } catch (ignore) {}
25 |
26 | DOMParser_proto.parseFromString = function (markup, type) {
27 | var result, doc, doc_elt, first_elt;
28 | if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
29 | doc = document.implementation.createHTMLDocument("");
30 | doc_elt = doc.documentElement;
31 |
32 | doc_elt.innerHTML = markup;
33 | first_elt = doc_elt.firstElementChild;
34 |
35 | if (doc_elt.childElementCount === 1
36 | && first_elt.localName.toLowerCase() === "html") {
37 | doc.replaceChild(first_elt, doc_elt);
38 | }
39 |
40 | result = doc;
41 | } else {
42 | result = real_parseFromString.apply(this, arguments);
43 | }
44 | return result;
45 | };
46 | }(DOMParser));
47 |
48 | /*
49 | * renderJs - Generic Gadget library renderer.
50 | * http://www.renderjs.org/documentation
51 | */
52 | (function (document, window, RSVP, DOMParser, Channel, undefined) {
53 | "use strict";
54 |
55 | var gadget_model_dict = {},
56 | javascript_registration_dict = {},
57 | stylesheet_registration_dict = {},
58 | gadget_loading_klass,
59 | loading_gadget_promise,
60 | renderJS;
61 |
62 | /////////////////////////////////////////////////////////////////
63 | // RenderJSGadget
64 | /////////////////////////////////////////////////////////////////
65 | function RenderJSGadget() {
66 | if (!(this instanceof RenderJSGadget)) {
67 | return new RenderJSGadget();
68 | }
69 | }
70 | RenderJSGadget.prototype.title = "";
71 | RenderJSGadget.prototype.interface_list = [];
72 | RenderJSGadget.prototype.path = "";
73 | RenderJSGadget.prototype.html = "";
74 | RenderJSGadget.prototype.required_css_list = [];
75 | RenderJSGadget.prototype.required_js_list = [];
76 |
77 | RSVP.EventTarget.mixin(RenderJSGadget.prototype);
78 |
79 | RenderJSGadget.ready_list = [];
80 | RenderJSGadget.ready = function (callback) {
81 | this.ready_list.push(callback);
82 | return this;
83 | };
84 |
85 | /////////////////////////////////////////////////////////////////
86 | // RenderJSGadget.declareMethod
87 | /////////////////////////////////////////////////////////////////
88 | RenderJSGadget.declareMethod = function (name, callback) {
89 | this.prototype[name] = function () {
90 | var context = this,
91 | argument_list = arguments;
92 |
93 | return new RSVP.Queue()
94 | .push(function () {
95 | return callback.apply(context, argument_list);
96 | });
97 | };
98 | // Allow chain
99 | return this;
100 | };
101 |
102 | RenderJSGadget
103 | .declareMethod('getInterfaceList', function () {
104 | // Returns the list of gadget prototype
105 | return this.interface_list;
106 | })
107 | .declareMethod('getRequiredCSSList', function () {
108 | // Returns a list of CSS required by the gadget
109 | return this.required_css_list;
110 | })
111 | .declareMethod('getRequiredJSList', function () {
112 | // Returns a list of JS required by the gadget
113 | return this.required_js_list;
114 | })
115 | .declareMethod('getPath', function () {
116 | // Returns the path of the code of a gadget
117 | return this.path;
118 | })
119 | .declareMethod('getTitle', function () {
120 | // Returns the title of a gadget
121 | return this.title;
122 | })
123 | .declareMethod('getElement', function () {
124 | // Returns the DOM Element of a gadget
125 | if (this.element === undefined) {
126 | throw new Error("No element defined");
127 | }
128 | return this.element;
129 | });
130 |
131 | /////////////////////////////////////////////////////////////////
132 | // RenderJSEmbeddedGadget
133 | /////////////////////////////////////////////////////////////////
134 | // Class inheritance
135 | function RenderJSEmbeddedGadget() {
136 | if (!(this instanceof RenderJSEmbeddedGadget)) {
137 | return new RenderJSEmbeddedGadget();
138 | }
139 | RenderJSGadget.call(this);
140 | }
141 | RenderJSEmbeddedGadget.ready_list = [];
142 | RenderJSEmbeddedGadget.ready =
143 | RenderJSGadget.ready;
144 | RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
145 | RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;
146 |
147 | /////////////////////////////////////////////////////////////////
148 | // privateDeclarePublicGadget
149 | /////////////////////////////////////////////////////////////////
150 | function privateDeclarePublicGadget(url, options) {
151 | var gadget_instance;
152 | if (options.element === undefined) {
153 | options.element = document.createElement("div");
154 | }
155 | return new RSVP.Queue()
156 | .push(function () {
157 | return renderJS.declareGadgetKlass(url);
158 | })
159 | // Get the gadget class and instanciate it
160 | .push(function (Klass) {
161 | var i,
162 | template_node_list = Klass.template_element.body.childNodes;
163 | gadget_loading_klass = Klass;
164 | gadget_instance = new Klass();
165 | gadget_instance.element = options.element;
166 | for (i = 0; i < template_node_list.length; i += 1) {
167 | gadget_instance.element.appendChild(
168 | template_node_list[i].cloneNode(true)
169 | );
170 | }
171 | // Load dependencies if needed
172 | return RSVP.all([
173 | gadget_instance.getRequiredJSList(),
174 | gadget_instance.getRequiredCSSList()
175 | ]);
176 | })
177 | // Load all JS/CSS
178 | .push(function (all_list) {
179 | var parameter_list = [],
180 | i;
181 | // Load JS
182 | for (i = 0; i < all_list[0].length; i += 1) {
183 | parameter_list.push(renderJS.declareJS(all_list[0][i]));
184 | }
185 | // Load CSS
186 | for (i = 0; i < all_list[1].length; i += 1) {
187 | parameter_list.push(renderJS.declareCSS(all_list[1][i]));
188 | }
189 | return RSVP.all(parameter_list);
190 | })
191 | .push(function () {
192 | return gadget_instance;
193 | });
194 | }
195 |
196 | /////////////////////////////////////////////////////////////////
197 | // RenderJSIframeGadget
198 | /////////////////////////////////////////////////////////////////
199 | function RenderJSIframeGadget() {
200 | if (!(this instanceof RenderJSIframeGadget)) {
201 | return new RenderJSIframeGadget();
202 | }
203 | RenderJSGadget.call(this);
204 | }
205 | RenderJSIframeGadget.ready_list = [];
206 | RenderJSIframeGadget.ready =
207 | RenderJSGadget.ready;
208 | RenderJSIframeGadget.prototype = new RenderJSGadget();
209 | RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;
210 |
211 | /////////////////////////////////////////////////////////////////
212 | // privateDeclareIframeGadget
213 | /////////////////////////////////////////////////////////////////
214 | function privateDeclareIframeGadget(url, options) {
215 | var gadget_instance,
216 | iframe,
217 | node,
218 | iframe_loading_deferred = RSVP.defer();
219 |
220 | if (options.element === undefined) {
221 | throw new Error("DOM element is required to create Iframe Gadget " +
222 | url);
223 | }
224 |
225 | // Check if the element is attached to the DOM
226 | node = options.element.parentNode;
227 | while (node !== null) {
228 | if (node === document) {
229 | break;
230 | }
231 | node = node.parentNode;
232 | }
233 | if (node === null) {
234 | throw new Error("The parent element is not attached to the DOM for " +
235 | url);
236 | }
237 |
238 | gadget_instance = new RenderJSIframeGadget();
239 | iframe = document.createElement("iframe");
240 | // gadget_instance.element.setAttribute("seamless", "seamless");
241 | iframe.setAttribute("src", url);
242 | gadget_instance.path = url;
243 | gadget_instance.element = options.element;
244 |
245 | // Attach it to the DOM
246 | options.element.appendChild(iframe);
247 |
248 | // XXX Manage unbind when deleting the gadget
249 |
250 | // Create the communication channel with the iframe
251 | gadget_instance.chan = Channel.build({
252 | window: iframe.contentWindow,
253 | origin: "*",
254 | scope: "renderJS"
255 | });
256 |
257 | // Create new method from the declareMethod call inside the iframe
258 | gadget_instance.chan.bind("declareMethod", function (trans, method_name) {
259 | gadget_instance[method_name] = function () {
260 | var argument_list = arguments;
261 | return new RSVP.Promise(function (resolve, reject) {
262 | gadget_instance.chan.call({
263 | method: "methodCall",
264 | params: [
265 | method_name,
266 | Array.prototype.slice.call(argument_list, 0)],
267 | success: function (s) {
268 | resolve(s);
269 | },
270 | error: function (e) {
271 | reject(e);
272 | }
273 | });
274 | });
275 | };
276 | return "OK";
277 | });
278 |
279 | // Wait for the iframe to be loaded before continuing
280 | gadget_instance.chan.bind("ready", function (trans) {
281 | iframe_loading_deferred.resolve(gadget_instance);
282 | return "OK";
283 | });
284 | gadget_instance.chan.bind("failed", function (trans, params) {
285 | iframe_loading_deferred.reject(params);
286 | return "OK";
287 | });
288 | gadget_instance.chan.bind("trigger", function (trans, params) {
289 | return gadget_instance.trigger(params.event_name, params.options);
290 | });
291 | return RSVP.any([
292 | iframe_loading_deferred.promise,
293 | // Timeout to prevent non renderJS embeddable gadget
294 | // XXX Maybe using iframe.onload/onerror would be safer?
295 | RSVP.timeout(5000)
296 | ]);
297 | }
298 |
299 | /////////////////////////////////////////////////////////////////
300 | // RenderJSGadget.declareGadget
301 | /////////////////////////////////////////////////////////////////
302 | RenderJSGadget.prototype.declareGadget = function (url, options) {
303 | var queue,
304 | previous_loading_gadget_promise = loading_gadget_promise;
305 |
306 | if (options === undefined) {
307 | options = {};
308 | }
309 | if (options.sandbox === undefined) {
310 | options.sandbox = "public";
311 | }
312 |
313 | // Change the global variable to update the loading queue
314 | queue = new RSVP.Queue()
315 | // Wait for previous gadget loading to finish first
316 | .push(function () {
317 | return previous_loading_gadget_promise;
318 | })
319 | .push(undefined, function () {
320 | // Forget previous declareGadget error
321 | return;
322 | })
323 | .push(function () {
324 | var method;
325 | if (options.sandbox === "public") {
326 | method = privateDeclarePublicGadget;
327 | } else if (options.sandbox === "iframe") {
328 | method = privateDeclareIframeGadget;
329 | } else {
330 | throw new Error("Unsupported sandbox options '" +
331 | options.sandbox + "'");
332 | }
333 | return method(url, options);
334 | })
335 | // Set the HTML context
336 | .push(function (gadget_instance) {
337 | var i;
338 | // Drop the current loading klass info used by selector
339 | gadget_loading_klass = undefined;
340 | // Trigger calling of all ready callback
341 | function ready_wrapper() {
342 | return gadget_instance;
343 | }
344 | for (i = 0; i < gadget_instance.constructor.ready_list.length;
345 | i += 1) {
346 | // Put a timeout?
347 | queue.push(gadget_instance.constructor.ready_list[i]);
348 | // Always return the gadget instance after ready function
349 | queue.push(ready_wrapper);
350 | }
351 | return gadget_instance;
352 | })
353 | .push(undefined, function (e) {
354 | // Drop the current loading klass info used by selector
355 | // even in case of error
356 | gadget_loading_klass = undefined;
357 | throw e;
358 | });
359 | loading_gadget_promise = queue;
360 | return loading_gadget_promise;
361 | };
362 |
363 | /////////////////////////////////////////////////////////////////
364 | // renderJS selector
365 | /////////////////////////////////////////////////////////////////
366 | renderJS = function (selector) {
367 | var result;
368 | if (selector === window) {
369 | // window is the 'this' value when loading a javascript file
370 | // In this case, use the current loading gadget constructor
371 | result = gadget_loading_klass;
372 | } else if (selector instanceof RenderJSGadget) {
373 | result = selector;
374 | }
375 | if (result === undefined) {
376 | throw new Error("Unknown selector '" + selector + "'");
377 | }
378 | return result;
379 | };
380 |
381 | /////////////////////////////////////////////////////////////////
382 | // renderJS.declareJS
383 | /////////////////////////////////////////////////////////////////
384 | renderJS.declareJS = function (url) {
385 | // Prevent infinite recursion if loading render.js
386 | // more than once
387 | var result;
388 | if (javascript_registration_dict.hasOwnProperty(url)) {
389 | result = RSVP.resolve();
390 | } else {
391 | result = new RSVP.Promise(function (resolve, reject) {
392 | var newScript;
393 | newScript = document.createElement('script');
394 | newScript.type = 'text/javascript';
395 | newScript.src = url;
396 | newScript.onload = function () {
397 | javascript_registration_dict[url] = null;
398 | resolve();
399 | };
400 | newScript.onerror = function (e) {
401 | reject(e);
402 | };
403 | document.head.appendChild(newScript);
404 | });
405 | }
406 | return result;
407 | };
408 |
409 | /////////////////////////////////////////////////////////////////
410 | // renderJS.declareCSS
411 | /////////////////////////////////////////////////////////////////
412 | renderJS.declareCSS = function (url) {
413 | // https://github.com/furf/jquery-getCSS/blob/master/jquery.getCSS.js
414 | // No way to cleanly check if a css has been loaded
415 | // So, always resolve the promise...
416 | // http://requirejs.org/docs/faq-advanced.html#css
417 | var result;
418 | if (stylesheet_registration_dict.hasOwnProperty(url)) {
419 | result = RSVP.resolve();
420 | } else {
421 | result = new RSVP.Promise(function (resolve, reject) {
422 | var link;
423 | link = document.createElement('link');
424 | link.rel = 'stylesheet';
425 | link.type = 'text/css';
426 | link.href = url;
427 | link.onload = function () {
428 | stylesheet_registration_dict[url] = null;
429 | resolve();
430 | };
431 | link.onerror = function (e) {
432 | reject(e);
433 | };
434 | document.head.appendChild(link);
435 | });
436 | }
437 | return result;
438 | };
439 |
440 | /////////////////////////////////////////////////////////////////
441 | // renderJS.declareGadgetKlass
442 | /////////////////////////////////////////////////////////////////
443 | renderJS.declareGadgetKlass = function (url) {
444 | var result,
445 | xhr;
446 |
447 | function parse() {
448 | var tmp_constructor,
449 | key,
450 | parsed_html;
451 | if (!gadget_model_dict.hasOwnProperty(url)) {
452 | // Class inheritance
453 | tmp_constructor = function () {
454 | RenderJSGadget.call(this);
455 | };
456 | tmp_constructor.ready_list = [];
457 | tmp_constructor.declareMethod =
458 | RenderJSGadget.declareMethod;
459 | tmp_constructor.ready =
460 | RenderJSGadget.ready;
461 | tmp_constructor.prototype = new RenderJSGadget();
462 | tmp_constructor.prototype.constructor = tmp_constructor;
463 | tmp_constructor.prototype.path = url;
464 | // https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
465 | // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
466 | // https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
467 | tmp_constructor.template_element =
468 | (new DOMParser()).parseFromString(xhr.responseText, "text/html");
469 | parsed_html = renderJS.parseGadgetHTMLDocument(
470 | tmp_constructor.template_element
471 | );
472 | for (key in parsed_html) {
473 | if (parsed_html.hasOwnProperty(key)) {
474 | tmp_constructor.prototype[key] = parsed_html[key];
475 | }
476 | }
477 |
478 | gadget_model_dict[url] = tmp_constructor;
479 | }
480 |
481 | return gadget_model_dict[url];
482 | }
483 |
484 | function resolver(resolve, reject) {
485 | function handler() {
486 | var tmp_result;
487 | try {
488 | if (xhr.readyState === 0) {
489 | // UNSENT
490 | reject(xhr);
491 | } else if (xhr.readyState === 4) {
492 | // DONE
493 | if ((xhr.status < 200) || (xhr.status >= 300) ||
494 | (!/^text\/html[;]?/.test(
495 | xhr.getResponseHeader("Content-Type") || ""
496 | ))) {
497 | reject(xhr);
498 | } else {
499 | tmp_result = parse();
500 | resolve(tmp_result);
501 | }
502 | }
503 | } catch (e) {
504 | reject(e);
505 | }
506 | }
507 |
508 | xhr = new XMLHttpRequest();
509 | xhr.open("GET", url);
510 | xhr.onreadystatechange = handler;
511 | xhr.setRequestHeader('Accept', 'text/html');
512 | xhr.withCredentials = true;
513 | xhr.send();
514 | }
515 |
516 | function canceller() {
517 | if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
518 | xhr.abort();
519 | }
520 | }
521 |
522 | if (gadget_model_dict.hasOwnProperty(url)) {
523 | // Return klass object if it already exists
524 | result = RSVP.resolve(gadget_model_dict[url]);
525 | } else {
526 | // Fetch the HTML page and parse it
527 | result = new RSVP.Promise(resolver, canceller);
528 | }
529 | return result;
530 | };
531 |
532 | /////////////////////////////////////////////////////////////////
533 | // renderJS.clearGadgetKlassList
534 | /////////////////////////////////////////////////////////////////
535 | // For test purpose only
536 | renderJS.clearGadgetKlassList = function () {
537 | gadget_model_dict = {};
538 | javascript_registration_dict = {};
539 | stylesheet_registration_dict = {};
540 | };
541 |
542 | /////////////////////////////////////////////////////////////////
543 | // renderJS.parseGadgetHTMLDocument
544 | /////////////////////////////////////////////////////////////////
545 | renderJS.parseGadgetHTMLDocument = function (document_element) {
546 | var settings = {
547 | title: "",
548 | interface_list: [],
549 | required_css_list: [],
550 | required_js_list: []
551 | },
552 | i,
553 | element;
554 | if (document_element.nodeType === 9) {
555 | settings.title = document_element.title;
556 |
557 | for (i = 0; i < document_element.head.children.length; i += 1) {
558 | element = document_element.head.children[i];
559 | if (element.href !== null) {
560 | // XXX Manage relative URL during extraction of URLs
561 | // element.href returns absolute URL in firefox but "" in chrome;
562 | if (element.rel === "stylesheet") {
563 | settings.required_css_list.push(element.getAttribute("href"));
564 | } else if (element.type === "text/javascript") {
565 | settings.required_js_list.push(element.getAttribute("src"));
566 | } else if (element.rel === "http://www.renderjs.org/rel/interface") {
567 | settings.interface_list.push(element.getAttribute("href"));
568 | }
569 | }
570 | }
571 | } else {
572 | throw new Error("The first parameter should be an HTMLDocument");
573 | }
574 | return settings;
575 | };
576 |
577 | /////////////////////////////////////////////////////////////////
578 | // global
579 | /////////////////////////////////////////////////////////////////
580 | window.rJS = window.renderJS = renderJS;
581 | window.RenderJSGadget = RenderJSGadget;
582 | window.RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
583 | window.RenderJSIframeGadget = RenderJSIframeGadget;
584 |
585 | ///////////////////////////////////////////////////
586 | // Bootstrap process. Register the self gadget.
587 | ///////////////////////////////////////////////////
588 |
589 | function bootstrap() {
590 | var url = window.location.href,
591 | tmp_constructor,
592 | root_gadget,
593 | declare_method_count = 0,
594 | embedded_channel,
595 | notifyReady,
596 | notifyDeclareMethod,
597 | notifyTrigger,
598 | gadget_ready = false;
599 |
600 |
601 | // Create the gadget class for the current url
602 | if (gadget_model_dict.hasOwnProperty(url)) {
603 | throw new Error("bootstrap should not be called twice");
604 | }
605 | loading_gadget_promise = new RSVP.Promise(function (resolve, reject) {
606 | if (window.self === window.top) {
607 | // XXX Copy/Paste from declareGadgetKlass
608 | tmp_constructor = function () {
609 | RenderJSGadget.call(this);
610 | };
611 | tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
612 | tmp_constructor.ready_list = [];
613 | tmp_constructor.ready = RenderJSGadget.ready;
614 | tmp_constructor.prototype = new RenderJSGadget();
615 | tmp_constructor.prototype.constructor = tmp_constructor;
616 | tmp_constructor.prototype.path = url;
617 | gadget_model_dict[url] = tmp_constructor;
618 |
619 | // Create the root gadget instance and put it in the loading stack
620 | root_gadget = new gadget_model_dict[url]();
621 |
622 | } else {
623 | // Create the communication channel
624 | embedded_channel = Channel.build({
625 | window: window.parent,
626 | origin: "*",
627 | scope: "renderJS"
628 | });
629 | // Create the root gadget instance and put it in the loading stack
630 | tmp_constructor = RenderJSEmbeddedGadget;
631 | root_gadget = new RenderJSEmbeddedGadget();
632 |
633 | // Bind calls to renderJS method on the instance
634 | embedded_channel.bind("methodCall", function (trans, v) {
635 | root_gadget[v[0]].apply(root_gadget, v[1]).then(function (g) {
636 | trans.complete(g);
637 | }).fail(function (e) {
638 | trans.error(e.toString());
639 | });
640 | trans.delayReturn(true);
641 | });
642 |
643 | // Notify parent about gadget instanciation
644 | notifyReady = function () {
645 | if ((declare_method_count === 0) && (gadget_ready === true)) {
646 | embedded_channel.notify({method: "ready"});
647 | }
648 | };
649 |
650 | // Inform parent gadget about declareMethod calls here.
651 | notifyDeclareMethod = function (name) {
652 | declare_method_count += 1;
653 | embedded_channel.call({
654 | method: "declareMethod",
655 | params: name,
656 | success: function () {
657 | declare_method_count -= 1;
658 | notifyReady();
659 | },
660 | error: function () {
661 | declare_method_count -= 1;
662 | }
663 | });
664 | };
665 |
666 | notifyDeclareMethod("getInterfaceList");
667 | notifyDeclareMethod("getRequiredCSSList");
668 | notifyDeclareMethod("getRequiredJSList");
669 | notifyDeclareMethod("getPath");
670 | notifyDeclareMethod("getTitle");
671 |
672 | // Surcharge declareMethod to inform parent window
673 | tmp_constructor.declareMethod = function (name, callback) {
674 | var result = RenderJSGadget.declareMethod.apply(
675 | this,
676 | [name, callback]
677 | );
678 | notifyDeclareMethod(name);
679 | return result;
680 | };
681 |
682 | notifyTrigger = function (eventName, options) {
683 | embedded_channel.notify({
684 | method: "trigger",
685 | params: {
686 | event_name: eventName,
687 | options: options
688 | }
689 | });
690 | };
691 |
692 | // Surcharge trigger to inform parent window
693 | tmp_constructor.prototype.trigger = function (eventName, options) {
694 | var result = RenderJSGadget.prototype.trigger.apply(
695 | this,
696 | [eventName, options]
697 | );
698 | notifyTrigger(eventName, options);
699 | return result;
700 | };
701 | }
702 |
703 | gadget_loading_klass = tmp_constructor;
704 |
705 | function init() {
706 | // XXX HTML properties can only be set when the DOM is fully loaded
707 | var settings = renderJS.parseGadgetHTMLDocument(document),
708 | j,
709 | key;
710 | for (key in settings) {
711 | if (settings.hasOwnProperty(key)) {
712 | tmp_constructor.prototype[key] = settings[key];
713 | }
714 | }
715 | tmp_constructor.template_element = document.createElement("div");
716 | root_gadget.element = document.body;
717 | for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
718 | tmp_constructor.template_element.appendChild(
719 | root_gadget.element.childNodes[j].cloneNode(true)
720 | );
721 | }
722 | RSVP.all([root_gadget.getRequiredJSList(),
723 | root_gadget.getRequiredCSSList()])
724 | .then(function (all_list) {
725 | var i,
726 | js_list = all_list[0],
727 | css_list = all_list[1],
728 | queue;
729 | for (i = 0; i < js_list.length; i += 1) {
730 | javascript_registration_dict[js_list[i]] = null;
731 | }
732 | for (i = 0; i < css_list.length; i += 1) {
733 | stylesheet_registration_dict[css_list[i]] = null;
734 | }
735 | gadget_loading_klass = undefined;
736 | queue = new RSVP.Queue();
737 | function ready_wrapper() {
738 | return root_gadget;
739 | }
740 | queue.push(ready_wrapper);
741 | for (i = 0; i < tmp_constructor.ready_list.length; i += 1) {
742 | // Put a timeout?
743 | queue.push(tmp_constructor.ready_list[i]);
744 | // Always return the gadget instance after ready function
745 | queue.push(ready_wrapper);
746 | }
747 | queue.push(resolve, function (e) {
748 | reject(e);
749 | throw e;
750 | });
751 | return queue;
752 | }).fail(function (e) {
753 | reject(e);
754 | });
755 | }
756 | document.addEventListener('DOMContentLoaded', init, false);
757 | });
758 |
759 | if (window.self !== window.top) {
760 | // Inform parent window that gadget is correctly loaded
761 | loading_gadget_promise.then(function () {
762 | gadget_ready = true;
763 | notifyReady();
764 | }).fail(function (e) {
765 | embedded_channel.notify({method: "failed", params: e.toString()});
766 | throw e;
767 | });
768 | }
769 |
770 | }
771 | bootstrap();
772 |
773 | }(document, window, RSVP, DOMParser, Channel));
774 |
--------------------------------------------------------------------------------
/dist/renderjs-0.4.1.js:
--------------------------------------------------------------------------------
1 | /*! RenderJs */
2 |
3 | /*
4 | * DOMParser HTML extension
5 | * 2012-09-04
6 | *
7 | * By Eli Grey, http://eligrey.com
8 | * Public domain.
9 | * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
10 | */
11 | /*! @source https://gist.github.com/1129031 */
12 | (function (DOMParser) {
13 | "use strict";
14 | var DOMParser_proto = DOMParser.prototype,
15 | real_parseFromString = DOMParser_proto.parseFromString;
16 |
17 | // Firefox/Opera/IE throw errors on unsupported types
18 | try {
19 | // WebKit returns null on unsupported types
20 | if ((new DOMParser()).parseFromString("", "text/html")) {
21 | // text/html parsing is natively supported
22 | return;
23 | }
24 | } catch (ignore) {}
25 |
26 | DOMParser_proto.parseFromString = function (markup, type) {
27 | var result, doc, doc_elt, first_elt;
28 | if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
29 | doc = document.implementation.createHTMLDocument("");
30 | doc_elt = doc.documentElement;
31 |
32 | doc_elt.innerHTML = markup;
33 | first_elt = doc_elt.firstElementChild;
34 |
35 | if (doc_elt.childElementCount === 1
36 | && first_elt.localName.toLowerCase() === "html") {
37 | doc.replaceChild(first_elt, doc_elt);
38 | }
39 |
40 | result = doc;
41 | } else {
42 | result = real_parseFromString.apply(this, arguments);
43 | }
44 | return result;
45 | };
46 | }(DOMParser));
47 |
48 | /*
49 | * renderJs - Generic Gadget library renderer.
50 | * http://www.renderjs.org/documentation
51 | */
52 | (function (document, window, RSVP, DOMParser, Channel, undefined) {
53 | "use strict";
54 |
55 | var gadget_model_dict = {},
56 | javascript_registration_dict = {},
57 | stylesheet_registration_dict = {},
58 | gadget_loading_klass,
59 | loading_gadget_promise,
60 | renderJS;
61 |
62 | /////////////////////////////////////////////////////////////////
63 | // RenderJSGadget
64 | /////////////////////////////////////////////////////////////////
65 | function RenderJSGadget() {
66 | if (!(this instanceof RenderJSGadget)) {
67 | return new RenderJSGadget();
68 | }
69 | }
70 | RenderJSGadget.prototype.title = "";
71 | RenderJSGadget.prototype.interface_list = [];
72 | RenderJSGadget.prototype.path = "";
73 | RenderJSGadget.prototype.html = "";
74 | RenderJSGadget.prototype.required_css_list = [];
75 | RenderJSGadget.prototype.required_js_list = [];
76 |
77 | RSVP.EventTarget.mixin(RenderJSGadget.prototype);
78 |
79 | RenderJSGadget.ready_list = [];
80 | RenderJSGadget.ready = function (callback) {
81 | this.ready_list.push(callback);
82 | return this;
83 | };
84 |
85 | /////////////////////////////////////////////////////////////////
86 | // RenderJSGadget.declareMethod
87 | /////////////////////////////////////////////////////////////////
88 | RenderJSGadget.declareMethod = function (name, callback) {
89 | this.prototype[name] = function () {
90 | var context = this,
91 | argument_list = arguments;
92 |
93 | return new RSVP.Queue()
94 | .push(function () {
95 | return callback.apply(context, argument_list);
96 | });
97 | };
98 | // Allow chain
99 | return this;
100 | };
101 |
102 | RenderJSGadget
103 | .declareMethod('getInterfaceList', function () {
104 | // Returns the list of gadget prototype
105 | return this.interface_list;
106 | })
107 | .declareMethod('getRequiredCSSList', function () {
108 | // Returns a list of CSS required by the gadget
109 | return this.required_css_list;
110 | })
111 | .declareMethod('getRequiredJSList', function () {
112 | // Returns a list of JS required by the gadget
113 | return this.required_js_list;
114 | })
115 | .declareMethod('getPath', function () {
116 | // Returns the path of the code of a gadget
117 | return this.path;
118 | })
119 | .declareMethod('getTitle', function () {
120 | // Returns the title of a gadget
121 | return this.title;
122 | })
123 | .declareMethod('getElement', function () {
124 | // Returns the DOM Element of a gadget
125 | if (this.element === undefined) {
126 | throw new Error("No element defined");
127 | }
128 | return this.element;
129 | });
130 |
131 | /////////////////////////////////////////////////////////////////
132 | // RenderJSEmbeddedGadget
133 | /////////////////////////////////////////////////////////////////
134 | // Class inheritance
135 | function RenderJSEmbeddedGadget() {
136 | if (!(this instanceof RenderJSEmbeddedGadget)) {
137 | return new RenderJSEmbeddedGadget();
138 | }
139 | RenderJSGadget.call(this);
140 | }
141 | RenderJSEmbeddedGadget.ready_list = [];
142 | RenderJSEmbeddedGadget.ready =
143 | RenderJSGadget.ready;
144 | RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
145 | RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;
146 |
147 | /////////////////////////////////////////////////////////////////
148 | // privateDeclarePublicGadget
149 | /////////////////////////////////////////////////////////////////
150 | function privateDeclarePublicGadget(url, options) {
151 | var gadget_instance;
152 | if (options.element === undefined) {
153 | options.element = document.createElement("div");
154 | }
155 | return new RSVP.Queue()
156 | .push(function () {
157 | return renderJS.declareGadgetKlass(url);
158 | })
159 | // Get the gadget class and instanciate it
160 | .push(function (Klass) {
161 | var i,
162 | template_node_list = Klass.template_element.body.childNodes;
163 | gadget_loading_klass = Klass;
164 | gadget_instance = new Klass();
165 | gadget_instance.element = options.element;
166 | for (i = 0; i < template_node_list.length; i += 1) {
167 | gadget_instance.element.appendChild(
168 | template_node_list[i].cloneNode(true)
169 | );
170 | }
171 | // Load dependencies if needed
172 | return RSVP.all([
173 | gadget_instance.getRequiredJSList(),
174 | gadget_instance.getRequiredCSSList()
175 | ]);
176 | })
177 | // Load all JS/CSS
178 | .push(function (all_list) {
179 | var parameter_list = [],
180 | i;
181 | // Load JS
182 | for (i = 0; i < all_list[0].length; i += 1) {
183 | parameter_list.push(renderJS.declareJS(all_list[0][i]));
184 | }
185 | // Load CSS
186 | for (i = 0; i < all_list[1].length; i += 1) {
187 | parameter_list.push(renderJS.declareCSS(all_list[1][i]));
188 | }
189 | return RSVP.all(parameter_list);
190 | })
191 | .push(function () {
192 | return gadget_instance;
193 | });
194 | }
195 |
196 | /////////////////////////////////////////////////////////////////
197 | // RenderJSIframeGadget
198 | /////////////////////////////////////////////////////////////////
199 | function RenderJSIframeGadget() {
200 | if (!(this instanceof RenderJSIframeGadget)) {
201 | return new RenderJSIframeGadget();
202 | }
203 | RenderJSGadget.call(this);
204 | }
205 | RenderJSIframeGadget.ready_list = [];
206 | RenderJSIframeGadget.ready =
207 | RenderJSGadget.ready;
208 | RenderJSIframeGadget.prototype = new RenderJSGadget();
209 | RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;
210 |
211 | /////////////////////////////////////////////////////////////////
212 | // privateDeclareIframeGadget
213 | /////////////////////////////////////////////////////////////////
214 | function privateDeclareIframeGadget(url, options) {
215 | var gadget_instance,
216 | iframe,
217 | node,
218 | iframe_loading_deferred = RSVP.defer();
219 |
220 | if (options.element === undefined) {
221 | throw new Error("DOM element is required to create Iframe Gadget " +
222 | url);
223 | }
224 |
225 | // Check if the element is attached to the DOM
226 | node = options.element.parentNode;
227 | while (node !== null) {
228 | if (node === document) {
229 | break;
230 | }
231 | node = node.parentNode;
232 | }
233 | if (node === null) {
234 | throw new Error("The parent element is not attached to the DOM for " +
235 | url);
236 | }
237 |
238 | gadget_instance = new RenderJSIframeGadget();
239 | iframe = document.createElement("iframe");
240 | // gadget_instance.element.setAttribute("seamless", "seamless");
241 | iframe.setAttribute("src", url);
242 | gadget_instance.path = url;
243 | gadget_instance.element = options.element;
244 |
245 | // Attach it to the DOM
246 | options.element.appendChild(iframe);
247 |
248 | // XXX Manage unbind when deleting the gadget
249 |
250 | // Create the communication channel with the iframe
251 | gadget_instance.chan = Channel.build({
252 | window: iframe.contentWindow,
253 | origin: "*",
254 | scope: "renderJS"
255 | });
256 |
257 | // Create new method from the declareMethod call inside the iframe
258 | gadget_instance.chan.bind("declareMethod", function (trans, method_name) {
259 | gadget_instance[method_name] = function () {
260 | var argument_list = arguments;
261 | return new RSVP.Promise(function (resolve, reject) {
262 | gadget_instance.chan.call({
263 | method: "methodCall",
264 | params: [
265 | method_name,
266 | Array.prototype.slice.call(argument_list, 0)],
267 | success: function (s) {
268 | resolve(s);
269 | },
270 | error: function (e) {
271 | reject(e);
272 | }
273 | });
274 | });
275 | };
276 | return "OK";
277 | });
278 |
279 | // Wait for the iframe to be loaded before continuing
280 | gadget_instance.chan.bind("ready", function (trans) {
281 | iframe_loading_deferred.resolve(gadget_instance);
282 | return "OK";
283 | });
284 | gadget_instance.chan.bind("failed", function (trans, params) {
285 | iframe_loading_deferred.reject(params);
286 | return "OK";
287 | });
288 | gadget_instance.chan.bind("trigger", function (trans, params) {
289 | return gadget_instance.trigger(params.event_name, params.options);
290 | });
291 | return RSVP.any([
292 | iframe_loading_deferred.promise,
293 | // Timeout to prevent non renderJS embeddable gadget
294 | // XXX Maybe using iframe.onload/onerror would be safer?
295 | RSVP.timeout(5000)
296 | ]);
297 | }
298 |
299 | /////////////////////////////////////////////////////////////////
300 | // RenderJSGadget.declareGadget
301 | /////////////////////////////////////////////////////////////////
302 | RenderJSGadget.prototype.declareGadget = function (url, options) {
303 | var queue,
304 | previous_loading_gadget_promise = loading_gadget_promise;
305 |
306 | if (options === undefined) {
307 | options = {};
308 | }
309 | if (options.sandbox === undefined) {
310 | options.sandbox = "public";
311 | }
312 |
313 | // Change the global variable to update the loading queue
314 | queue = new RSVP.Queue()
315 | // Wait for previous gadget loading to finish first
316 | .push(function () {
317 | return previous_loading_gadget_promise;
318 | })
319 | .push(undefined, function () {
320 | // Forget previous declareGadget error
321 | return;
322 | })
323 | .push(function () {
324 | var method;
325 | if (options.sandbox === "public") {
326 | method = privateDeclarePublicGadget;
327 | } else if (options.sandbox === "iframe") {
328 | method = privateDeclareIframeGadget;
329 | } else {
330 | throw new Error("Unsupported sandbox options '" +
331 | options.sandbox + "'");
332 | }
333 | return method(url, options);
334 | })
335 | // Set the HTML context
336 | .push(function (gadget_instance) {
337 | var i;
338 | // Drop the current loading klass info used by selector
339 | gadget_loading_klass = undefined;
340 | // Trigger calling of all ready callback
341 | function ready_wrapper() {
342 | return gadget_instance;
343 | }
344 | for (i = 0; i < gadget_instance.constructor.ready_list.length;
345 | i += 1) {
346 | // Put a timeout?
347 | queue.push(gadget_instance.constructor.ready_list[i]);
348 | // Always return the gadget instance after ready function
349 | queue.push(ready_wrapper);
350 | }
351 | return gadget_instance;
352 | })
353 | .push(undefined, function (e) {
354 | // Drop the current loading klass info used by selector
355 | // even in case of error
356 | gadget_loading_klass = undefined;
357 | throw e;
358 | });
359 | loading_gadget_promise = queue;
360 | return loading_gadget_promise;
361 | };
362 |
363 | /////////////////////////////////////////////////////////////////
364 | // renderJS selector
365 | /////////////////////////////////////////////////////////////////
366 | renderJS = function (selector) {
367 | var result;
368 | if (selector === window) {
369 | // window is the 'this' value when loading a javascript file
370 | // In this case, use the current loading gadget constructor
371 | result = gadget_loading_klass;
372 | } else if (selector instanceof RenderJSGadget) {
373 | result = selector;
374 | }
375 | if (result === undefined) {
376 | throw new Error("Unknown selector '" + selector + "'");
377 | }
378 | return result;
379 | };
380 |
381 | /////////////////////////////////////////////////////////////////
382 | // renderJS.declareJS
383 | /////////////////////////////////////////////////////////////////
384 | renderJS.declareJS = function (url) {
385 | // Prevent infinite recursion if loading render.js
386 | // more than once
387 | var result;
388 | if (javascript_registration_dict.hasOwnProperty(url)) {
389 | result = RSVP.resolve();
390 | } else {
391 | result = new RSVP.Promise(function (resolve, reject) {
392 | var newScript;
393 | newScript = document.createElement('script');
394 | newScript.type = 'text/javascript';
395 | newScript.src = url;
396 | newScript.onload = function () {
397 | javascript_registration_dict[url] = null;
398 | resolve();
399 | };
400 | newScript.onerror = function (e) {
401 | reject(e);
402 | };
403 | document.head.appendChild(newScript);
404 | });
405 | }
406 | return result;
407 | };
408 |
409 | /////////////////////////////////////////////////////////////////
410 | // renderJS.declareCSS
411 | /////////////////////////////////////////////////////////////////
412 | renderJS.declareCSS = function (url) {
413 | // https://github.com/furf/jquery-getCSS/blob/master/jquery.getCSS.js
414 | // No way to cleanly check if a css has been loaded
415 | // So, always resolve the promise...
416 | // http://requirejs.org/docs/faq-advanced.html#css
417 | var result;
418 | if (stylesheet_registration_dict.hasOwnProperty(url)) {
419 | result = RSVP.resolve();
420 | } else {
421 | result = new RSVP.Promise(function (resolve, reject) {
422 | var link;
423 | link = document.createElement('link');
424 | link.rel = 'stylesheet';
425 | link.type = 'text/css';
426 | link.href = url;
427 | link.onload = function () {
428 | stylesheet_registration_dict[url] = null;
429 | resolve();
430 | };
431 | link.onerror = function (e) {
432 | reject(e);
433 | };
434 | document.head.appendChild(link);
435 | });
436 | }
437 | return result;
438 | };
439 |
440 | /////////////////////////////////////////////////////////////////
441 | // renderJS.declareGadgetKlass
442 | /////////////////////////////////////////////////////////////////
443 | renderJS.declareGadgetKlass = function (url) {
444 | var result,
445 | xhr;
446 |
447 | function parse() {
448 | var tmp_constructor,
449 | key,
450 | parsed_html;
451 | if (!gadget_model_dict.hasOwnProperty(url)) {
452 | // Class inheritance
453 | tmp_constructor = function () {
454 | RenderJSGadget.call(this);
455 | };
456 | tmp_constructor.ready_list = [];
457 | tmp_constructor.declareMethod =
458 | RenderJSGadget.declareMethod;
459 | tmp_constructor.ready =
460 | RenderJSGadget.ready;
461 | tmp_constructor.prototype = new RenderJSGadget();
462 | tmp_constructor.prototype.constructor = tmp_constructor;
463 | tmp_constructor.prototype.path = url;
464 | // https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
465 | // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
466 | // https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
467 | tmp_constructor.template_element =
468 | (new DOMParser()).parseFromString(xhr.responseText, "text/html");
469 | parsed_html = renderJS.parseGadgetHTMLDocument(
470 | tmp_constructor.template_element
471 | );
472 | for (key in parsed_html) {
473 | if (parsed_html.hasOwnProperty(key)) {
474 | tmp_constructor.prototype[key] = parsed_html[key];
475 | }
476 | }
477 |
478 | gadget_model_dict[url] = tmp_constructor;
479 | }
480 |
481 | return gadget_model_dict[url];
482 | }
483 |
484 | function resolver(resolve, reject) {
485 | function handler() {
486 | var tmp_result;
487 | try {
488 | if (xhr.readyState === 0) {
489 | // UNSENT
490 | reject(xhr);
491 | } else if (xhr.readyState === 4) {
492 | // DONE
493 | if ((xhr.status < 200) || (xhr.status >= 300) ||
494 | (!/^text\/html[;]?/.test(
495 | xhr.getResponseHeader("Content-Type") || ""
496 | ))) {
497 | reject(xhr);
498 | } else {
499 | tmp_result = parse();
500 | resolve(tmp_result);
501 | }
502 | }
503 | } catch (e) {
504 | reject(e);
505 | }
506 | }
507 |
508 | xhr = new XMLHttpRequest();
509 | xhr.open("GET", url);
510 | xhr.onreadystatechange = handler;
511 | xhr.setRequestHeader('Accept', 'text/html');
512 | xhr.withCredentials = true;
513 | xhr.send();
514 | }
515 |
516 | function canceller() {
517 | if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
518 | xhr.abort();
519 | }
520 | }
521 |
522 | if (gadget_model_dict.hasOwnProperty(url)) {
523 | // Return klass object if it already exists
524 | result = RSVP.resolve(gadget_model_dict[url]);
525 | } else {
526 | // Fetch the HTML page and parse it
527 | result = new RSVP.Promise(resolver, canceller);
528 | }
529 | return result;
530 | };
531 |
532 | /////////////////////////////////////////////////////////////////
533 | // renderJS.clearGadgetKlassList
534 | /////////////////////////////////////////////////////////////////
535 | // For test purpose only
536 | renderJS.clearGadgetKlassList = function () {
537 | gadget_model_dict = {};
538 | javascript_registration_dict = {};
539 | stylesheet_registration_dict = {};
540 | };
541 |
542 | /////////////////////////////////////////////////////////////////
543 | // renderJS.parseGadgetHTMLDocument
544 | /////////////////////////////////////////////////////////////////
545 | renderJS.parseGadgetHTMLDocument = function (document_element) {
546 | var settings = {
547 | title: "",
548 | interface_list: [],
549 | required_css_list: [],
550 | required_js_list: []
551 | },
552 | i,
553 | element;
554 | if (document_element.nodeType === 9) {
555 | settings.title = document_element.title;
556 |
557 | for (i = 0; i < document_element.head.children.length; i += 1) {
558 | element = document_element.head.children[i];
559 | if (element.href !== null) {
560 | // XXX Manage relative URL during extraction of URLs
561 | // element.href returns absolute URL in firefox but "" in chrome;
562 | if (element.rel === "stylesheet") {
563 | settings.required_css_list.push(element.getAttribute("href"));
564 | } else if (element.type === "text/javascript") {
565 | settings.required_js_list.push(element.getAttribute("src"));
566 | } else if (element.rel === "http://www.renderjs.org/rel/interface") {
567 | settings.interface_list.push(element.getAttribute("href"));
568 | }
569 | }
570 | }
571 | } else {
572 | throw new Error("The first parameter should be an HTMLDocument");
573 | }
574 | return settings;
575 | };
576 |
577 | /////////////////////////////////////////////////////////////////
578 | // global
579 | /////////////////////////////////////////////////////////////////
580 | window.rJS = window.renderJS = renderJS;
581 | window.RenderJSGadget = RenderJSGadget;
582 | window.RenderJSEmbeddedGadget = RenderJSEmbeddedGadget;
583 | window.RenderJSIframeGadget = RenderJSIframeGadget;
584 |
585 | ///////////////////////////////////////////////////
586 | // Bootstrap process. Register the self gadget.
587 | ///////////////////////////////////////////////////
588 |
589 | function bootstrap() {
590 | var url = window.location.href,
591 | tmp_constructor,
592 | root_gadget,
593 | declare_method_count = 0,
594 | embedded_channel,
595 | notifyReady,
596 | notifyDeclareMethod,
597 | notifyTrigger,
598 | gadget_ready = false;
599 |
600 |
601 | // Create the gadget class for the current url
602 | if (gadget_model_dict.hasOwnProperty(url)) {
603 | throw new Error("bootstrap should not be called twice");
604 | }
605 | loading_gadget_promise = new RSVP.Promise(function (resolve, reject) {
606 | if (window.self === window.top) {
607 | // XXX Copy/Paste from declareGadgetKlass
608 | tmp_constructor = function () {
609 | RenderJSGadget.call(this);
610 | };
611 | tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
612 | tmp_constructor.ready_list = [];
613 | tmp_constructor.ready = RenderJSGadget.ready;
614 | tmp_constructor.prototype = new RenderJSGadget();
615 | tmp_constructor.prototype.constructor = tmp_constructor;
616 | tmp_constructor.prototype.path = url;
617 | gadget_model_dict[url] = tmp_constructor;
618 |
619 | // Create the root gadget instance and put it in the loading stack
620 | root_gadget = new gadget_model_dict[url]();
621 |
622 | } else {
623 | // Create the communication channel
624 | embedded_channel = Channel.build({
625 | window: window.parent,
626 | origin: "*",
627 | scope: "renderJS"
628 | });
629 | // Create the root gadget instance and put it in the loading stack
630 | tmp_constructor = RenderJSEmbeddedGadget;
631 | root_gadget = new RenderJSEmbeddedGadget();
632 |
633 | // Bind calls to renderJS method on the instance
634 | embedded_channel.bind("methodCall", function (trans, v) {
635 | root_gadget[v[0]].apply(root_gadget, v[1]).then(function (g) {
636 | trans.complete(g);
637 | }).fail(function (e) {
638 | trans.error(e.toString());
639 | });
640 | trans.delayReturn(true);
641 | });
642 |
643 | // Notify parent about gadget instanciation
644 | notifyReady = function () {
645 | if ((declare_method_count === 0) && (gadget_ready === true)) {
646 | embedded_channel.notify({method: "ready"});
647 | }
648 | };
649 |
650 | // Inform parent gadget about declareMethod calls here.
651 | notifyDeclareMethod = function (name) {
652 | declare_method_count += 1;
653 | embedded_channel.call({
654 | method: "declareMethod",
655 | params: name,
656 | success: function () {
657 | declare_method_count -= 1;
658 | notifyReady();
659 | },
660 | error: function () {
661 | declare_method_count -= 1;
662 | }
663 | });
664 | };
665 |
666 | notifyDeclareMethod("getInterfaceList");
667 | notifyDeclareMethod("getRequiredCSSList");
668 | notifyDeclareMethod("getRequiredJSList");
669 | notifyDeclareMethod("getPath");
670 | notifyDeclareMethod("getTitle");
671 |
672 | // Surcharge declareMethod to inform parent window
673 | tmp_constructor.declareMethod = function (name, callback) {
674 | var result = RenderJSGadget.declareMethod.apply(
675 | this,
676 | [name, callback]
677 | );
678 | notifyDeclareMethod(name);
679 | return result;
680 | };
681 |
682 | notifyTrigger = function (eventName, options) {
683 | embedded_channel.notify({
684 | method: "trigger",
685 | params: {
686 | event_name: eventName,
687 | options: options
688 | }
689 | });
690 | };
691 |
692 | // Surcharge trigger to inform parent window
693 | tmp_constructor.prototype.trigger = function (eventName, options) {
694 | var result = RenderJSGadget.prototype.trigger.apply(
695 | this,
696 | [eventName, options]
697 | );
698 | notifyTrigger(eventName, options);
699 | return result;
700 | };
701 | }
702 |
703 | gadget_loading_klass = tmp_constructor;
704 |
705 | function init() {
706 | // XXX HTML properties can only be set when the DOM is fully loaded
707 | var settings = renderJS.parseGadgetHTMLDocument(document),
708 | j,
709 | key;
710 | for (key in settings) {
711 | if (settings.hasOwnProperty(key)) {
712 | tmp_constructor.prototype[key] = settings[key];
713 | }
714 | }
715 | tmp_constructor.template_element = document.createElement("div");
716 | root_gadget.element = document.body;
717 | for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
718 | tmp_constructor.template_element.appendChild(
719 | root_gadget.element.childNodes[j].cloneNode(true)
720 | );
721 | }
722 | RSVP.all([root_gadget.getRequiredJSList(),
723 | root_gadget.getRequiredCSSList()])
724 | .then(function (all_list) {
725 | var i,
726 | js_list = all_list[0],
727 | css_list = all_list[1],
728 | queue;
729 | for (i = 0; i < js_list.length; i += 1) {
730 | javascript_registration_dict[js_list[i]] = null;
731 | }
732 | for (i = 0; i < css_list.length; i += 1) {
733 | stylesheet_registration_dict[css_list[i]] = null;
734 | }
735 | gadget_loading_klass = undefined;
736 | queue = new RSVP.Queue();
737 | function ready_wrapper() {
738 | return root_gadget;
739 | }
740 | queue.push(ready_wrapper);
741 | for (i = 0; i < tmp_constructor.ready_list.length; i += 1) {
742 | // Put a timeout?
743 | queue.push(tmp_constructor.ready_list[i]);
744 | // Always return the gadget instance after ready function
745 | queue.push(ready_wrapper);
746 | }
747 | queue.push(resolve, function (e) {
748 | reject(e);
749 | throw e;
750 | });
751 | return queue;
752 | }).fail(function (e) {
753 | reject(e);
754 | /*global console */
755 | console.error(e);
756 | });
757 | }
758 | document.addEventListener('DOMContentLoaded', init, false);
759 | });
760 |
761 | if (window.self !== window.top) {
762 | // Inform parent window that gadget is correctly loaded
763 | loading_gadget_promise.then(function () {
764 | gadget_ready = true;
765 | notifyReady();
766 | }).fail(function (e) {
767 | embedded_channel.notify({method: "failed", params: e.toString()});
768 | throw e;
769 | });
770 | }
771 |
772 | }
773 | bootstrap();
774 |
775 | }(document, window, RSVP, DOMParser, Channel));
776 |
--------------------------------------------------------------------------------
/lib/jschannel/jschannel.js:
--------------------------------------------------------------------------------
1 | /*
2 | * js_channel is a very lightweight abstraction on top of
3 | * postMessage which defines message formats and semantics
4 | * to support interactions more rich than just message passing
5 | * js_channel supports:
6 | * + query/response - traditional rpc
7 | * + query/update/response - incremental async return of results
8 | * to a query
9 | * + notifications - fire and forget
10 | * + error handling
11 | *
12 | * js_channel is based heavily on json-rpc, but is focused at the
13 | * problem of inter-iframe RPC.
14 | *
15 | * Message types:
16 | * There are 5 types of messages that can flow over this channel,
17 | * and you may determine what type of message an object is by
18 | * examining its parameters:
19 | * 1. Requests
20 | * + integer id
21 | * + string method
22 | * + (optional) any params
23 | * 2. Callback Invocations (or just "Callbacks")
24 | * + integer id
25 | * + string callback
26 | * + (optional) params
27 | * 3. Error Responses (or just "Errors)
28 | * + integer id
29 | * + string error
30 | * + (optional) string message
31 | * 4. Responses
32 | * + integer id
33 | * + (optional) any result
34 | * 5. Notifications
35 | * + string method
36 | * + (optional) any params
37 | */
38 |
39 | ;var Channel = (function() {
40 | "use strict";
41 |
42 | // current transaction id, start out at a random *odd* number between 1 and a million
43 | // There is one current transaction counter id per page, and it's shared between
44 | // channel instances. That means of all messages posted from a single javascript
45 | // evaluation context, we'll never have two with the same id.
46 | var s_curTranId = Math.floor(Math.random()*1000001);
47 |
48 | // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window.
49 | // futher if two bound channels have the same window and scope, they may not have *overlapping* origins
50 | // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently
51 | // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message
52 | // handlers. Request and Notification messages are routed using this table.
53 | // Finally, channels are inserted into this table when built, and removed when destroyed.
54 | var s_boundChans = { };
55 |
56 | // add a channel to s_boundChans, throwing if a dup exists
57 | function s_addBoundChan(win, origin, scope, handler) {
58 | function hasWin(arr) {
59 | for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true;
60 | return false;
61 | }
62 |
63 | // does she exist?
64 | var exists = false;
65 |
66 |
67 | if (origin === '*') {
68 | // we must check all other origins, sadly.
69 | for (var k in s_boundChans) {
70 | if (!s_boundChans.hasOwnProperty(k)) continue;
71 | if (k === '*') continue;
72 | if (typeof s_boundChans[k][scope] === 'object') {
73 | exists = hasWin(s_boundChans[k][scope]);
74 | if (exists) break;
75 | }
76 | }
77 | } else {
78 | // we must check only '*'
79 | if ((s_boundChans['*'] && s_boundChans['*'][scope])) {
80 | exists = hasWin(s_boundChans['*'][scope]);
81 | }
82 | if (!exists && s_boundChans[origin] && s_boundChans[origin][scope])
83 | {
84 | exists = hasWin(s_boundChans[origin][scope]);
85 | }
86 | }
87 | if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'";
88 |
89 | if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { };
90 | if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ];
91 | s_boundChans[origin][scope].push({win: win, handler: handler});
92 | }
93 |
94 | function s_removeBoundChan(win, origin, scope) {
95 | var arr = s_boundChans[origin][scope];
96 | for (var i = 0; i < arr.length; i++) {
97 | if (arr[i].win === win) {
98 | arr.splice(i,1);
99 | }
100 | }
101 | if (s_boundChans[origin][scope].length === 0) {
102 | delete s_boundChans[origin][scope];
103 | }
104 | }
105 |
106 | function s_isArray(obj) {
107 | if (Array.isArray) return Array.isArray(obj);
108 | else {
109 | return (obj.constructor.toString().indexOf("Array") != -1);
110 | }
111 | }
112 |
113 | // No two outstanding outbound messages may have the same id, period. Given that, a single table
114 | // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and
115 | // Response messages. Entries are added to this table when requests are sent, and removed when
116 | // responses are received.
117 | var s_transIds = { };
118 |
119 | // class singleton onMessage handler
120 | // this function is registered once and all incoming messages route through here. This
121 | // arrangement allows certain efficiencies, message data is only parsed once and dispatch
122 | // is more efficient, especially for large numbers of simultaneous channels.
123 | var s_onMessage = function(e) {
124 | try {
125 | var m = JSON.parse(e.data);
126 | if (typeof m !== 'object' || m === null) throw "malformed";
127 | } catch(e) {
128 | // just ignore any posted messages that do not consist of valid JSON
129 | return;
130 | }
131 |
132 | var w = e.source;
133 | var o = e.origin;
134 | var s, i, meth;
135 |
136 | if (typeof m.method === 'string') {
137 | var ar = m.method.split('::');
138 | if (ar.length == 2) {
139 | s = ar[0];
140 | meth = ar[1];
141 | } else {
142 | meth = m.method;
143 | }
144 | }
145 |
146 | if (typeof m.id !== 'undefined') i = m.id;
147 |
148 | // w is message source window
149 | // o is message origin
150 | // m is parsed message
151 | // s is message scope
152 | // i is message id (or undefined)
153 | // meth is unscoped method name
154 | // ^^ based on these factors we can route the message
155 |
156 | // if it has a method it's either a notification or a request,
157 | // route using s_boundChans
158 | if (typeof meth === 'string') {
159 | var delivered = false;
160 | if (s_boundChans[o] && s_boundChans[o][s]) {
161 | for (var j = 0; j < s_boundChans[o][s].length; j++) {
162 | if (s_boundChans[o][s][j].win === w) {
163 | s_boundChans[o][s][j].handler(o, meth, m);
164 | delivered = true;
165 | break;
166 | }
167 | }
168 | }
169 |
170 | if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) {
171 | for (var j = 0; j < s_boundChans['*'][s].length; j++) {
172 | if (s_boundChans['*'][s][j].win === w) {
173 | s_boundChans['*'][s][j].handler(o, meth, m);
174 | break;
175 | }
176 | }
177 | }
178 | }
179 | // otherwise it must have an id (or be poorly formed
180 | else if (typeof i != 'undefined') {
181 | if (s_transIds[i]) s_transIds[i](o, meth, m);
182 | }
183 | };
184 |
185 | // Setup postMessage event listeners
186 | if (window.addEventListener) window.addEventListener('message', s_onMessage, false);
187 | else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage);
188 |
189 | /* a messaging channel is constructed from a window and an origin.
190 | * the channel will assert that all messages received over the
191 | * channel match the origin
192 | *
193 | * Arguments to Channel.build(cfg):
194 | *
195 | * cfg.window - the remote window with which we'll communicate
196 | * cfg.origin - the expected origin of the remote window, may be '*'
197 | * which matches any origin
198 | * cfg.scope - the 'scope' of messages. a scope string that is
199 | * prepended to message names. local and remote endpoints
200 | * of a single channel must agree upon scope. Scope may
201 | * not contain double colons ('::').
202 | * cfg.debugOutput - A boolean value. If true and window.console.log is
203 | * a function, then debug strings will be emitted to that
204 | * function.
205 | * cfg.debugOutput - A boolean value. If true and window.console.log is
206 | * a function, then debug strings will be emitted to that
207 | * function.
208 | * cfg.postMessageObserver - A function that will be passed two arguments,
209 | * an origin and a message. It will be passed these immediately
210 | * before messages are posted.
211 | * cfg.gotMessageObserver - A function that will be passed two arguments,
212 | * an origin and a message. It will be passed these arguments
213 | * immediately after they pass scope and origin checks, but before
214 | * they are processed.
215 | * cfg.onReady - A function that will be invoked when a channel becomes "ready",
216 | * this occurs once both sides of the channel have been
217 | * instantiated and an application level handshake is exchanged.
218 | * the onReady function will be passed a single argument which is
219 | * the channel object that was returned from build().
220 | */
221 | return {
222 | build: function(cfg) {
223 | var debug = function(m) {
224 | if (cfg.debugOutput && window.console && window.console.log) {
225 | // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic
226 | try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { }
227 | console.log("["+chanId+"] " + m);
228 | }
229 | };
230 |
231 | /* browser capabilities check */
232 | if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage");
233 | if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) {
234 | throw("jschannel cannot run this browser, no JSON parsing/serialization");
235 | }
236 |
237 | /* basic argument validation */
238 | if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument");
239 |
240 | if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument");
241 |
242 | /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same
243 | * window... Not sure if we care to support that */
244 | if (window === cfg.window) throw("target window is same as present window -- not allowed");
245 |
246 | // let's require that the client specify an origin. if we just assume '*' we'll be
247 | // propagating unsafe practices. that would be lame.
248 | var validOrigin = false;
249 | if (typeof cfg.origin === 'string') {
250 | var oMatch;
251 | if (cfg.origin === "*") validOrigin = true;
252 | // allow valid domains under http and https. Also, trim paths off otherwise valid origins.
253 | else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) {
254 | cfg.origin = oMatch[0].toLowerCase();
255 | validOrigin = true;
256 | }
257 | }
258 |
259 | if (!validOrigin) throw ("Channel.build() called with an invalid origin");
260 |
261 | if (typeof cfg.scope !== 'undefined') {
262 | if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string';
263 | if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'";
264 | }
265 |
266 | /* private variables */
267 | // generate a random and psuedo unique id for this channel
268 | var chanId = (function () {
269 | var text = "";
270 | var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
271 | for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length));
272 | return text;
273 | })();
274 |
275 | // registrations: mapping method names to call objects
276 | var regTbl = { };
277 | // current oustanding sent requests
278 | var outTbl = { };
279 | // current oustanding received requests
280 | var inTbl = { };
281 | // are we ready yet? when false we will block outbound messages.
282 | var ready = false;
283 | var pendingQueue = [ ];
284 |
285 | var createTransaction = function(id,origin,callbacks) {
286 | var shouldDelayReturn = false;
287 | var completed = false;
288 |
289 | return {
290 | origin: origin,
291 | invoke: function(cbName, v) {
292 | // verify in table
293 | if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id;
294 | // verify that the callback name is valid
295 | var valid = false;
296 | for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; }
297 | if (!valid) throw "request supports no such callback '" + cbName + "'";
298 |
299 | // send callback invocation
300 | postMessage({ id: id, callback: cbName, params: v});
301 | },
302 | error: function(error, message) {
303 | // verify in table
304 | if (!inTbl[id]) throw "error called for nonexistent message: " + id;
305 |
306 | // send error
307 | postMessage({ id: id, error: error, message: message });
308 |
309 | completed = true;
310 |
311 | // remove transaction from table
312 | delete inTbl[id];
313 | },
314 | complete: function(v) {
315 | // verify in table
316 | if (!inTbl[id]) throw "complete called for nonexistent message: " + id;
317 |
318 | // send complete
319 | postMessage({ id: id, result: v });
320 |
321 | completed = true;
322 |
323 | // remove transaction from table
324 | delete inTbl[id];
325 | },
326 | delayReturn: function(delay) {
327 | if (typeof delay === 'boolean') {
328 | shouldDelayReturn = (delay === true);
329 | }
330 | return shouldDelayReturn;
331 | },
332 | completed: function() {
333 | return completed;
334 | }
335 | };
336 | };
337 |
338 | var setTransactionTimeout = function(transId, timeout, method) {
339 | return window.setTimeout(function() {
340 | if (outTbl[transId]) {
341 | // XXX: what if client code raises an exception here?
342 | var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'";
343 | (1,outTbl[transId].error)("timeout_error", msg);
344 | delete outTbl[transId];
345 | delete s_transIds[transId];
346 | }
347 | }, timeout);
348 | };
349 |
350 | var onMessage = function(origin, method, m) {
351 | // if an observer was specified at allocation time, invoke it
352 | if (typeof cfg.gotMessageObserver === 'function') {
353 | // pass observer a clone of the object so that our
354 | // manipulations are not visible (i.e. method unscoping).
355 | // This is not particularly efficient, but then we expect
356 | // that message observers are primarily for debugging anyway.
357 | try {
358 | cfg.gotMessageObserver(origin, m);
359 | } catch (e) {
360 | debug("gotMessageObserver() raised an exception: " + e.toString());
361 | }
362 | }
363 |
364 | // now, what type of message is this?
365 | if (m.id && method) {
366 | // a request! do we have a registered handler for this request?
367 | if (regTbl[method]) {
368 | var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]);
369 | inTbl[m.id] = { };
370 | try {
371 | // callback handling. we'll magically create functions inside the parameter list for each
372 | // callback
373 | if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) {
374 | for (var i = 0; i < m.callbacks.length; i++) {
375 | var path = m.callbacks[i];
376 | var obj = m.params;
377 | var pathItems = path.split('/');
378 | for (var j = 0; j < pathItems.length - 1; j++) {
379 | var cp = pathItems[j];
380 | if (typeof obj[cp] !== 'object') obj[cp] = { };
381 | obj = obj[cp];
382 | }
383 | obj[pathItems[pathItems.length - 1]] = (function() {
384 | var cbName = path;
385 | return function(params) {
386 | return trans.invoke(cbName, params, m.id);
387 | };
388 | })();
389 | }
390 | }
391 | var resp = regTbl[method](trans, m.params, m.id);
392 | if (!trans.delayReturn() && !trans.completed()) trans.complete(resp);
393 | } catch(e) {
394 | // automagic handling of exceptions:
395 | var error = "runtime_error";
396 | var message = null;
397 | // * if it's a string then it gets an error code of 'runtime_error' and string is the message
398 | if (typeof e === 'string') {
399 | message = e;
400 | } else if (typeof e === 'object') {
401 | // either an array or an object
402 | // * if it's an array of length two, then array[0] is the code, array[1] is the error message
403 | if (e && s_isArray(e) && e.length == 2) {
404 | error = e[0];
405 | message = e[1];
406 | }
407 | // * if it's an object then we'll look form error and message parameters
408 | else if (typeof e.error === 'string') {
409 | error = e.error;
410 | if (!e.message) message = "";
411 | else if (typeof e.message === 'string') message = e.message;
412 | else e = e.message; // let the stringify/toString message give us a reasonable verbose error string
413 | }
414 | }
415 |
416 | // message is *still* null, let's try harder
417 | if (message === null) {
418 | try {
419 | message = JSON.stringify(e);
420 | /* On MSIE8, this can result in 'out of memory', which
421 | * leaves message undefined. */
422 | if (typeof(message) == 'undefined')
423 | message = e.toString();
424 | } catch (e2) {
425 | message = e.toString();
426 | }
427 | }
428 |
429 | trans.error(error,message);
430 | }
431 | }
432 | } else if (m.id && m.callback) {
433 | if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback])
434 | {
435 | debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")");
436 | } else {
437 | // XXX: what if client code raises an exception here?
438 | outTbl[m.id].callbacks[m.callback](m.params);
439 | }
440 | } else if (m.id) {
441 | if (!outTbl[m.id]) {
442 | debug("ignoring invalid response: " + m.id);
443 | } else {
444 | // XXX: what if client code raises an exception here?
445 | if (m.error) {
446 | (1,outTbl[m.id].error)(m.error, m.message);
447 | } else {
448 | if (m.result !== undefined) (1,outTbl[m.id].success)(m.result);
449 | else (1,outTbl[m.id].success)();
450 | }
451 | delete outTbl[m.id];
452 | delete s_transIds[m.id];
453 | }
454 | } else if (method) {
455 | // tis a notification.
456 | if (regTbl[method]) {
457 | // yep, there's a handler for that.
458 | // transaction has only origin for notifications.
459 | regTbl[method]({ origin: origin }, m.params);
460 | // if the client throws, we'll just let it bubble out
461 | // what can we do? Also, here we'll ignore return values
462 | }
463 | }
464 | };
465 |
466 | // now register our bound channel for msg routing
467 | s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage);
468 |
469 | // scope method names based on cfg.scope specified when the Channel was instantiated
470 | var scopeMethod = function(m) {
471 | if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::");
472 | return m;
473 | };
474 |
475 | // a small wrapper around postmessage whose primary function is to handle the
476 | // case that clients start sending messages before the other end is "ready"
477 | var postMessage = function(msg, force) {
478 | if (!msg) throw "postMessage called with null message";
479 |
480 | // delay posting if we're not ready yet.
481 | var verb = (ready ? "post " : "queue ");
482 | debug(verb + " message: " + JSON.stringify(msg));
483 | if (!force && !ready) {
484 | pendingQueue.push(msg);
485 | } else {
486 | if (typeof cfg.postMessageObserver === 'function') {
487 | try {
488 | cfg.postMessageObserver(cfg.origin, msg);
489 | } catch (e) {
490 | debug("postMessageObserver() raised an exception: " + e.toString());
491 | }
492 | }
493 |
494 | cfg.window.postMessage(JSON.stringify(msg), cfg.origin);
495 | }
496 | };
497 |
498 | var onReady = function(trans, type) {
499 | debug('ready msg received');
500 | if (ready) throw "received ready message while in ready state. help!";
501 |
502 | if (type === 'ping') {
503 | chanId += '-R';
504 | } else {
505 | chanId += '-L';
506 | }
507 |
508 | obj.unbind('__ready'); // now this handler isn't needed any more.
509 | ready = true;
510 | debug('ready msg accepted.');
511 |
512 | if (type === 'ping') {
513 | obj.notify({ method: '__ready', params: 'pong' });
514 | }
515 |
516 | // flush queue
517 | while (pendingQueue.length) {
518 | postMessage(pendingQueue.pop());
519 | }
520 |
521 | // invoke onReady observer if provided
522 | if (typeof cfg.onReady === 'function') cfg.onReady(obj);
523 | };
524 |
525 | var obj = {
526 | // tries to unbind a bound message handler. returns false if not possible
527 | unbind: function (method) {
528 | if (regTbl[method]) {
529 | if (!(delete regTbl[method])) throw ("can't delete method: " + method);
530 | return true;
531 | }
532 | return false;
533 | },
534 | bind: function (method, cb) {
535 | if (!method || typeof method !== 'string') throw "'method' argument to bind must be string";
536 | if (!cb || typeof cb !== 'function') throw "callback missing from bind params";
537 |
538 | if (regTbl[method]) throw "method '"+method+"' is already bound!";
539 | regTbl[method] = cb;
540 | return this;
541 | },
542 | call: function(m) {
543 | if (!m) throw 'missing arguments to call function';
544 | if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string";
545 | if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call";
546 |
547 | // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument
548 | // object and pick out all of the functions that were passed as arguments.
549 | var callbacks = { };
550 | var callbackNames = [ ];
551 |
552 | var pruneFunctions = function (path, obj) {
553 | if (typeof obj === 'object') {
554 | for (var k in obj) {
555 | if (!obj.hasOwnProperty(k)) continue;
556 | var np = path + (path.length ? '/' : '') + k;
557 | if (typeof obj[k] === 'function') {
558 | callbacks[np] = obj[k];
559 | callbackNames.push(np);
560 | delete obj[k];
561 | } else if (typeof obj[k] === 'object') {
562 | pruneFunctions(np, obj[k]);
563 | }
564 | }
565 | }
566 | };
567 | pruneFunctions("", m.params);
568 |
569 | // build a 'request' message and send it
570 | var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params };
571 | if (callbackNames.length) msg.callbacks = callbackNames;
572 |
573 | if (m.timeout)
574 | // XXX: This function returns a timeout ID, but we don't do anything with it.
575 | // We might want to keep track of it so we can cancel it using clearTimeout()
576 | // when the transaction completes.
577 | setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method));
578 |
579 | // insert into the transaction table
580 | outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success };
581 | s_transIds[s_curTranId] = onMessage;
582 |
583 | // increment current id
584 | s_curTranId++;
585 |
586 | postMessage(msg);
587 | // return the transaction id
588 | return s_curTranId - 1;
589 | },
590 | notify: function(m) {
591 | if (!m) throw 'missing arguments to notify function';
592 | if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string";
593 |
594 | // no need to go into any transaction table
595 | postMessage({ method: scopeMethod(m.method), params: m.params });
596 | },
597 | destroy: function () {
598 | s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''));
599 | if (window.removeEventListener) window.removeEventListener('message', onMessage, false);
600 | else if(window.detachEvent) window.detachEvent('onmessage', onMessage);
601 | ready = false;
602 | regTbl = { };
603 | inTbl = { };
604 | outTbl = { };
605 | cfg.origin = null;
606 | pendingQueue = [ ];
607 | debug("channel destroyed");
608 | chanId = "";
609 | }
610 | };
611 |
612 | obj.bind('__ready', onReady);
613 | setTimeout(function() {
614 | postMessage({ method: scopeMethod('__ready'), params: "ping" }, true);
615 | }, 0);
616 |
617 | return obj;
618 | }
619 | };
620 | })();
621 |
--------------------------------------------------------------------------------