├── 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 | --------------------------------------------------------------------------------