From 7c5c67452406aac036cf0867298379ba01ce2951 Mon Sep 17 00:00:00 2001 From: Romain Courteaud Date: Wed, 2 Nov 2016 10:04:55 +0100 Subject: [PATCH] Release version 0.11.0 * add an internal gadget state * rename __element property to element * add declareJob function * add onEvent method * 'this' in ready is now the gadget instance InlineSide-by-side --- dist/renderjs-0.11.0.js | 2328 +++++++++++++++++++++++++++++++++++ dist/renderjs-0.11.0.min.js | 1 + dist/renderjs-latest.js | 230 +++- dist/renderjs-latest.min.js | 2 +- package.json | 2 +- 5 files changed, 2541 insertions(+), 22 deletions(-) create mode 100644 dist/renderjs-0.11.0.js create mode 100644 dist/renderjs-0.11.0.min.js diff --git a/dist/renderjs-0.11.0.js b/dist/renderjs-0.11.0.js new file mode 100644 index 0000000..3e49117 --- /dev/null +++ b/dist/renderjs-0.11.0.js @@ -0,0 +1,2328 @@ +/* + * js_channel is a very lightweight abstraction on top of + * postMessage which defines message formats and semantics + * to support interactions more rich than just message passing + * js_channel supports: + * + query/response - traditional rpc + * + query/update/response - incremental async return of results + * to a query + * + notifications - fire and forget + * + error handling + * + * js_channel is based heavily on json-rpc, but is focused at the + * problem of inter-iframe RPC. + * + * Message types: + * There are 5 types of messages that can flow over this channel, + * and you may determine what type of message an object is by + * examining its parameters: + * 1. Requests + * + integer id + * + string method + * + (optional) any params + * 2. Callback Invocations (or just "Callbacks") + * + integer id + * + string callback + * + (optional) params + * 3. Error Responses (or just "Errors) + * + integer id + * + string error + * + (optional) string message + * 4. Responses + * + integer id + * + (optional) any result + * 5. Notifications + * + string method + * + (optional) any params + */ + +;var Channel = (function() { + "use strict"; + + // current transaction id, start out at a random *odd* number between 1 and a million + // There is one current transaction counter id per page, and it's shared between + // channel instances. That means of all messages posted from a single javascript + // evaluation context, we'll never have two with the same id. + var s_curTranId = Math.floor(Math.random()*1000001); + + // no two bound channels in the same javascript evaluation context may have the same origin, scope, and window. + // futher if two bound channels have the same window and scope, they may not have *overlapping* origins + // (either one or both support '*'). This restriction allows a single onMessage handler to efficiently + // route messages based on origin and scope. The s_boundChans maps origins to scopes, to message + // handlers. Request and Notification messages are routed using this table. + // Finally, channels are inserted into this table when built, and removed when destroyed. + var s_boundChans = { }; + + // add a channel to s_boundChans, throwing if a dup exists + function s_addBoundChan(win, origin, scope, handler) { + function hasWin(arr) { + for (var i = 0; i < arr.length; i++) if (arr[i].win === win) return true; + return false; + } + + // does she exist? + var exists = false; + + + if (origin === '*') { + // we must check all other origins, sadly. + for (var k in s_boundChans) { + if (!s_boundChans.hasOwnProperty(k)) continue; + if (k === '*') continue; + if (typeof s_boundChans[k][scope] === 'object') { + exists = hasWin(s_boundChans[k][scope]); + if (exists) break; + } + } + } else { + // we must check only '*' + if ((s_boundChans['*'] && s_boundChans['*'][scope])) { + exists = hasWin(s_boundChans['*'][scope]); + } + if (!exists && s_boundChans[origin] && s_boundChans[origin][scope]) + { + exists = hasWin(s_boundChans[origin][scope]); + } + } + if (exists) throw "A channel is already bound to the same window which overlaps with origin '"+ origin +"' and has scope '"+scope+"'"; + + if (typeof s_boundChans[origin] != 'object') s_boundChans[origin] = { }; + if (typeof s_boundChans[origin][scope] != 'object') s_boundChans[origin][scope] = [ ]; + s_boundChans[origin][scope].push({win: win, handler: handler}); + } + + function s_removeBoundChan(win, origin, scope) { + var arr = s_boundChans[origin][scope]; + for (var i = 0; i < arr.length; i++) { + if (arr[i].win === win) { + arr.splice(i,1); + } + } + if (s_boundChans[origin][scope].length === 0) { + delete s_boundChans[origin][scope]; + } + } + + function s_isArray(obj) { + if (Array.isArray) return Array.isArray(obj); + else { + return (obj.constructor.toString().indexOf("Array") != -1); + } + } + + // No two outstanding outbound messages may have the same id, period. Given that, a single table + // mapping "transaction ids" to message handlers, allows efficient routing of Callback, Error, and + // Response messages. Entries are added to this table when requests are sent, and removed when + // responses are received. + var s_transIds = { }; + + // class singleton onMessage handler + // this function is registered once and all incoming messages route through here. This + // arrangement allows certain efficiencies, message data is only parsed once and dispatch + // is more efficient, especially for large numbers of simultaneous channels. + var s_onMessage = function(e) { + try { + var m = JSON.parse(e.data); + if (typeof m !== 'object' || m === null) throw "malformed"; + } catch(e) { + // just ignore any posted messages that do not consist of valid JSON + return; + } + + var w = e.source; + var o = e.origin; + var s, i, meth; + + if (typeof m.method === 'string') { + var ar = m.method.split('::'); + if (ar.length == 2) { + s = ar[0]; + meth = ar[1]; + } else { + meth = m.method; + } + } + + if (typeof m.id !== 'undefined') i = m.id; + + // w is message source window + // o is message origin + // m is parsed message + // s is message scope + // i is message id (or undefined) + // meth is unscoped method name + // ^^ based on these factors we can route the message + + // if it has a method it's either a notification or a request, + // route using s_boundChans + if (typeof meth === 'string') { + var delivered = false; + if (s_boundChans[o] && s_boundChans[o][s]) { + for (var j = 0; j < s_boundChans[o][s].length; j++) { + if (s_boundChans[o][s][j].win === w) { + s_boundChans[o][s][j].handler(o, meth, m); + delivered = true; + break; + } + } + } + + if (!delivered && s_boundChans['*'] && s_boundChans['*'][s]) { + for (var j = 0; j < s_boundChans['*'][s].length; j++) { + if (s_boundChans['*'][s][j].win === w) { + s_boundChans['*'][s][j].handler(o, meth, m); + break; + } + } + } + } + // otherwise it must have an id (or be poorly formed + else if (typeof i != 'undefined') { + if (s_transIds[i]) s_transIds[i](o, meth, m); + } + }; + + // Setup postMessage event listeners + if (window.addEventListener) window.addEventListener('message', s_onMessage, false); + else if(window.attachEvent) window.attachEvent('onmessage', s_onMessage); + + /* a messaging channel is constructed from a window and an origin. + * the channel will assert that all messages received over the + * channel match the origin + * + * Arguments to Channel.build(cfg): + * + * cfg.window - the remote window with which we'll communicate + * cfg.origin - the expected origin of the remote window, may be '*' + * which matches any origin + * cfg.scope - the 'scope' of messages. a scope string that is + * prepended to message names. local and remote endpoints + * of a single channel must agree upon scope. Scope may + * not contain double colons ('::'). + * cfg.debugOutput - A boolean value. If true and window.console.log is + * a function, then debug strings will be emitted to that + * function. + * cfg.debugOutput - A boolean value. If true and window.console.log is + * a function, then debug strings will be emitted to that + * function. + * cfg.postMessageObserver - A function that will be passed two arguments, + * an origin and a message. It will be passed these immediately + * before messages are posted. + * cfg.gotMessageObserver - A function that will be passed two arguments, + * an origin and a message. It will be passed these arguments + * immediately after they pass scope and origin checks, but before + * they are processed. + * cfg.onReady - A function that will be invoked when a channel becomes "ready", + * this occurs once both sides of the channel have been + * instantiated and an application level handshake is exchanged. + * the onReady function will be passed a single argument which is + * the channel object that was returned from build(). + */ + return { + build: function(cfg) { + var debug = function(m) { + if (cfg.debugOutput && window.console && window.console.log) { + // try to stringify, if it doesn't work we'll let javascript's built in toString do its magic + try { if (typeof m !== 'string') m = JSON.stringify(m); } catch(e) { } + console.log("["+chanId+"] " + m); + } + }; + + /* browser capabilities check */ + if (!window.postMessage) throw("jschannel cannot run this browser, no postMessage"); + if (!window.JSON || !window.JSON.stringify || ! window.JSON.parse) { + throw("jschannel cannot run this browser, no JSON parsing/serialization"); + } + + /* basic argument validation */ + if (typeof cfg != 'object') throw("Channel build invoked without a proper object argument"); + + if (!cfg.window || !cfg.window.postMessage) throw("Channel.build() called without a valid window argument"); + + /* we'd have to do a little more work to be able to run multiple channels that intercommunicate the same + * window... Not sure if we care to support that */ + if (window === cfg.window) throw("target window is same as present window -- not allowed"); + + // let's require that the client specify an origin. if we just assume '*' we'll be + // propagating unsafe practices. that would be lame. + var validOrigin = false; + if (typeof cfg.origin === 'string') { + var oMatch; + if (cfg.origin === "*") validOrigin = true; + // allow valid domains under http and https. Also, trim paths off otherwise valid origins. + else if (null !== (oMatch = cfg.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))) { + cfg.origin = oMatch[0].toLowerCase(); + validOrigin = true; + } + } + + if (!validOrigin) throw ("Channel.build() called with an invalid origin"); + + if (typeof cfg.scope !== 'undefined') { + if (typeof cfg.scope !== 'string') throw 'scope, when specified, must be a string'; + if (cfg.scope.split('::').length > 1) throw "scope may not contain double colons: '::'"; + } + + /* private variables */ + // generate a random and psuedo unique id for this channel + var chanId = (function () { + var text = ""; + var alpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + for(var i=0; i < 5; i++) text += alpha.charAt(Math.floor(Math.random() * alpha.length)); + return text; + })(); + + // registrations: mapping method names to call objects + var regTbl = { }; + // current oustanding sent requests + var outTbl = { }; + // current oustanding received requests + var inTbl = { }; + // are we ready yet? when false we will block outbound messages. + var ready = false; + var pendingQueue = [ ]; + + var createTransaction = function(id,origin,callbacks) { + var shouldDelayReturn = false; + var completed = false; + + return { + origin: origin, + invoke: function(cbName, v) { + // verify in table + if (!inTbl[id]) throw "attempting to invoke a callback of a nonexistent transaction: " + id; + // verify that the callback name is valid + var valid = false; + for (var i = 0; i < callbacks.length; i++) if (cbName === callbacks[i]) { valid = true; break; } + if (!valid) throw "request supports no such callback '" + cbName + "'"; + + // send callback invocation + postMessage({ id: id, callback: cbName, params: v}); + }, + error: function(error, message) { + completed = true; + // verify in table + if (!inTbl[id]) throw "error called for nonexistent message: " + id; + + // remove transaction from table + delete inTbl[id]; + + // send error + postMessage({ id: id, error: error, message: message }); + }, + complete: function(v) { + completed = true; + // verify in table + if (!inTbl[id]) throw "complete called for nonexistent message: " + id; + // remove transaction from table + delete inTbl[id]; + // send complete + postMessage({ id: id, result: v }); + }, + delayReturn: function(delay) { + if (typeof delay === 'boolean') { + shouldDelayReturn = (delay === true); + } + return shouldDelayReturn; + }, + completed: function() { + return completed; + } + }; + }; + + var setTransactionTimeout = function(transId, timeout, method) { + return window.setTimeout(function() { + if (outTbl[transId]) { + // XXX: what if client code raises an exception here? + var msg = "timeout (" + timeout + "ms) exceeded on method '" + method + "'"; + (1,outTbl[transId].error)("timeout_error", msg); + delete outTbl[transId]; + delete s_transIds[transId]; + } + }, timeout); + }; + + var onMessage = function(origin, method, m) { + // if an observer was specified at allocation time, invoke it + if (typeof cfg.gotMessageObserver === 'function') { + // pass observer a clone of the object so that our + // manipulations are not visible (i.e. method unscoping). + // This is not particularly efficient, but then we expect + // that message observers are primarily for debugging anyway. + try { + cfg.gotMessageObserver(origin, m); + } catch (e) { + debug("gotMessageObserver() raised an exception: " + e.toString()); + } + } + + // now, what type of message is this? + if (m.id && method) { + // a request! do we have a registered handler for this request? + if (regTbl[method]) { + var trans = createTransaction(m.id, origin, m.callbacks ? m.callbacks : [ ]); + inTbl[m.id] = { }; + try { + // callback handling. we'll magically create functions inside the parameter list for each + // callback + if (m.callbacks && s_isArray(m.callbacks) && m.callbacks.length > 0) { + for (var i = 0; i < m.callbacks.length; i++) { + var path = m.callbacks[i]; + var obj = m.params; + var pathItems = path.split('/'); + for (var j = 0; j < pathItems.length - 1; j++) { + var cp = pathItems[j]; + if (typeof obj[cp] !== 'object') obj[cp] = { }; + obj = obj[cp]; + } + obj[pathItems[pathItems.length - 1]] = (function() { + var cbName = path; + return function(params) { + return trans.invoke(cbName, params); + }; + })(); + } + } + var resp = regTbl[method](trans, m.params); + if (!trans.delayReturn() && !trans.completed()) trans.complete(resp); + } catch(e) { + // automagic handling of exceptions: + var error = "runtime_error"; + var message = null; + // * if it's a string then it gets an error code of 'runtime_error' and string is the message + if (typeof e === 'string') { + message = e; + } else if (typeof e === 'object') { + // either an array or an object + // * if it's an array of length two, then array[0] is the code, array[1] is the error message + if (e && s_isArray(e) && e.length == 2) { + error = e[0]; + message = e[1]; + } + // * if it's an object then we'll look form error and message parameters + else if (typeof e.error === 'string') { + error = e.error; + if (!e.message) message = ""; + else if (typeof e.message === 'string') message = e.message; + else e = e.message; // let the stringify/toString message give us a reasonable verbose error string + } + } + + // message is *still* null, let's try harder + if (message === null) { + try { + message = JSON.stringify(e); + /* On MSIE8, this can result in 'out of memory', which + * leaves message undefined. */ + if (typeof(message) == 'undefined') + message = e.toString(); + } catch (e2) { + message = e.toString(); + } + } + + trans.error(error,message); + } + } + } else if (m.id && m.callback) { + if (!outTbl[m.id] ||!outTbl[m.id].callbacks || !outTbl[m.id].callbacks[m.callback]) + { + debug("ignoring invalid callback, id:"+m.id+ " (" + m.callback +")"); + } else { + // XXX: what if client code raises an exception here? + outTbl[m.id].callbacks[m.callback](m.params); + } + } else if (m.id) { + if (!outTbl[m.id]) { + debug("ignoring invalid response: " + m.id); + } else { + // XXX: what if client code raises an exception here? + if (m.error) { + (1,outTbl[m.id].error)(m.error, m.message); + } else { + if (m.result !== undefined) (1,outTbl[m.id].success)(m.result); + else (1,outTbl[m.id].success)(); + } + delete outTbl[m.id]; + delete s_transIds[m.id]; + } + } else if (method) { + // tis a notification. + if (regTbl[method]) { + // yep, there's a handler for that. + // transaction has only origin for notifications. + regTbl[method]({ origin: origin }, m.params); + // if the client throws, we'll just let it bubble out + // what can we do? Also, here we'll ignore return values + } + } + }; + + // now register our bound channel for msg routing + s_addBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : ''), onMessage); + + // scope method names based on cfg.scope specified when the Channel was instantiated + var scopeMethod = function(m) { + if (typeof cfg.scope === 'string' && cfg.scope.length) m = [cfg.scope, m].join("::"); + return m; + }; + + // a small wrapper around postmessage whose primary function is to handle the + // case that clients start sending messages before the other end is "ready" + var postMessage = function(msg, force) { + if (!msg) throw "postMessage called with null message"; + + // delay posting if we're not ready yet. + var verb = (ready ? "post " : "queue "); + debug(verb + " message: " + JSON.stringify(msg)); + if (!force && !ready) { + pendingQueue.push(msg); + } else { + if (typeof cfg.postMessageObserver === 'function') { + try { + cfg.postMessageObserver(cfg.origin, msg); + } catch (e) { + debug("postMessageObserver() raised an exception: " + e.toString()); + } + } + + cfg.window.postMessage(JSON.stringify(msg), cfg.origin); + } + }; + + var onReady = function(trans, type) { + debug('ready msg received'); + if (ready) throw "received ready message while in ready state. help!"; + + if (type === 'ping') { + chanId += '-R'; + } else { + chanId += '-L'; + } + + obj.unbind('__ready'); // now this handler isn't needed any more. + ready = true; + debug('ready msg accepted.'); + + if (type === 'ping') { + obj.notify({ method: '__ready', params: 'pong' }); + } + + // flush queue + while (pendingQueue.length) { + postMessage(pendingQueue.pop()); + } + + // invoke onReady observer if provided + if (typeof cfg.onReady === 'function') cfg.onReady(obj); + }; + + var obj = { + // tries to unbind a bound message handler. returns false if not possible + unbind: function (method) { + if (regTbl[method]) { + if (!(delete regTbl[method])) throw ("can't delete method: " + method); + return true; + } + return false; + }, + bind: function (method, cb) { + if (!method || typeof method !== 'string') throw "'method' argument to bind must be string"; + if (!cb || typeof cb !== 'function') throw "callback missing from bind params"; + + if (regTbl[method]) throw "method '"+method+"' is already bound!"; + regTbl[method] = cb; + return this; + }, + call: function(m) { + if (!m) throw 'missing arguments to call function'; + if (!m.method || typeof m.method !== 'string') throw "'method' argument to call must be string"; + if (!m.success || typeof m.success !== 'function') throw "'success' callback missing from call"; + + // now it's time to support the 'callback' feature of jschannel. We'll traverse the argument + // object and pick out all of the functions that were passed as arguments. + var callbacks = { }; + var callbackNames = [ ]; + + var pruneFunctions = function (path, obj) { + if (typeof obj === 'object') { + for (var k in obj) { + if (!obj.hasOwnProperty(k)) continue; + var np = path + (path.length ? '/' : '') + k; + if (typeof obj[k] === 'function') { + callbacks[np] = obj[k]; + callbackNames.push(np); + delete obj[k]; + } else if (typeof obj[k] === 'object') { + pruneFunctions(np, obj[k]); + } + } + } + }; + pruneFunctions("", m.params); + + // build a 'request' message and send it + var msg = { id: s_curTranId, method: scopeMethod(m.method), params: m.params }; + if (callbackNames.length) msg.callbacks = callbackNames; + + if (m.timeout) + // XXX: This function returns a timeout ID, but we don't do anything with it. + // We might want to keep track of it so we can cancel it using clearTimeout() + // when the transaction completes. + setTransactionTimeout(s_curTranId, m.timeout, scopeMethod(m.method)); + + // insert into the transaction table + outTbl[s_curTranId] = { callbacks: callbacks, error: m.error, success: m.success }; + s_transIds[s_curTranId] = onMessage; + + // increment current id + s_curTranId++; + + postMessage(msg); + }, + notify: function(m) { + if (!m) throw 'missing arguments to notify function'; + if (!m.method || typeof m.method !== 'string') throw "'method' argument to notify must be string"; + + // no need to go into any transaction table + postMessage({ method: scopeMethod(m.method), params: m.params }); + }, + destroy: function () { + s_removeBoundChan(cfg.window, cfg.origin, ((typeof cfg.scope === 'string') ? cfg.scope : '')); + if (window.removeEventListener) window.removeEventListener('message', onMessage, false); + else if(window.detachEvent) window.detachEvent('onmessage', onMessage); + ready = false; + regTbl = { }; + inTbl = { }; + outTbl = { }; + cfg.origin = null; + pendingQueue = [ ]; + debug("channel destroyed"); + chanId = ""; + } + }; + + obj.bind('__ready', onReady); + setTimeout(function() { + postMessage({ method: scopeMethod('__ready'), params: "ping" }, true); + }, 0); + + return obj; + } + }; +})(); +;/* + * DOMParser HTML extension + * 2012-09-04 + * + * By Eli Grey, http://eligrey.com + * Public domain. + * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + */ +/*! @source https://gist.github.com/1129031 */ +(function (DOMParser) { + "use strict"; + var DOMParser_proto = DOMParser.prototype, + real_parseFromString = DOMParser_proto.parseFromString; + + // Firefox/Opera/IE throw errors on unsupported types + try { + // WebKit returns null on unsupported types + if ((new DOMParser()).parseFromString("", "text/html")) { + // text/html parsing is natively supported + return; + } + } catch (ignore) {} + + DOMParser_proto.parseFromString = function (markup, type) { + var result, doc, doc_elt, first_elt; + if (/^\s*text\/html\s*(?:;|$)/i.test(type)) { + doc = document.implementation.createHTMLDocument(""); + doc_elt = doc.documentElement; + + doc_elt.innerHTML = markup; + first_elt = doc_elt.firstElementChild; + + if (doc_elt.childElementCount === 1 + && first_elt.localName.toLowerCase() === "html") { + doc.replaceChild(first_elt, doc_elt); + } + + result = doc; + } else { + result = real_parseFromString.apply(this, arguments); + } + return result; + }; +}(DOMParser)); + +;// IE does not support have Document.prototype.contains. +if (typeof document.contains !== 'function') { + Document.prototype.contains = function(node) { + if (node === this || node.parentNode === this) + return true; + return this.documentElement.contains(node); + } +} +;/*! RenderJs */ +/*jslint nomen: true*/ + +/* + * renderJs - Generic Gadget library renderer. + * http://www.renderjs.org/documentation + */ +(function (document, window, RSVP, DOMParser, Channel, MutationObserver, + Node, FileReader, Blob, navigator, Event) { + "use strict"; + + function readBlobAsDataURL(blob) { + var fr = new FileReader(); + return new RSVP.Promise(function (resolve, reject) { + fr.addEventListener("load", function (evt) { + resolve(evt.target.result); + }); + fr.addEventListener("error", reject); + fr.readAsDataURL(blob); + }, function () { + fr.abort(); + }); + } + + function loopEventListener(target, type, useCapture, callback, + prevent_default) { + ////////////////////////// + // Infinite event listener (promise is never resolved) + // eventListener is removed when promise is cancelled/rejected + ////////////////////////// + var handle_event_callback, + callback_promise; + + if (prevent_default === undefined) { + prevent_default = true; + } + + function cancelResolver() { + if ((callback_promise !== undefined) && + (typeof callback_promise.cancel === "function")) { + callback_promise.cancel(); + } + } + + function canceller() { + if (handle_event_callback !== undefined) { + target.removeEventListener(type, handle_event_callback, useCapture); + } + cancelResolver(); + } + function itsANonResolvableTrap(resolve, reject) { + var result; + handle_event_callback = function (evt) { + if (prevent_default) { + evt.stopPropagation(); + evt.preventDefault(); + } + + cancelResolver(); + + try { + result = callback(evt); + } catch (e) { + result = RSVP.reject(e); + } + + callback_promise = result; + new RSVP.Queue() + .push(function () { + return result; + }) + .push(undefined, function (error) { + if (!(error instanceof RSVP.CancellationError)) { + canceller(); + reject(error); + } + }); + }; + + target.addEventListener(type, handle_event_callback, useCapture); + } + return new RSVP.Promise(itsANonResolvableTrap, canceller); + } + + function ajax(url) { + var xhr; + function resolver(resolve, reject) { + function handler() { + try { + if (xhr.readyState === 0) { + // UNSENT + reject(xhr); + } else if (xhr.readyState === 4) { + // DONE + if ((xhr.status < 200) || (xhr.status >= 300) || + (!/^text\/html[;]?/.test( + xhr.getResponseHeader("Content-Type") || "" + ))) { + reject(xhr); + } else { + resolve(xhr); + } + } + } catch (e) { + reject(e); + } + } + + xhr = new XMLHttpRequest(); + xhr.open("GET", url); + xhr.onreadystatechange = handler; + xhr.setRequestHeader('Accept', 'text/html'); + xhr.withCredentials = true; + xhr.send(); + } + + function canceller() { + if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) { + xhr.abort(); + } + } + return new RSVP.Promise(resolver, canceller); + } + + var gadget_model_dict = {}, + javascript_registration_dict = {}, + stylesheet_registration_dict = {}, + gadget_loading_klass, + loading_klass_promise, + renderJS, + Monitor, + scope_increment = 0, + isAbsoluteOrDataURL = new RegExp('^(?:[a-z]+:)?//|data:', 'i'), + is_page_unloaded = false, + error_list = []; + + window.addEventListener('error', function (error) { + error_list.push(error); + }); + + window.addEventListener('beforeunload', function () { + // XXX If another listener cancel the page unload, + // it will not restore renderJS crash report + is_page_unloaded = true; + }); + + ///////////////////////////////////////////////////////////////// + // Helper functions + ///////////////////////////////////////////////////////////////// + function removeHash(url) { + var index = url.indexOf('#'); + if (index > 0) { + url = url.substring(0, index); + } + return url; + } + + function letsCrash(e) { + var i, + body, + container, + paragraph, + link, + error; + if (is_page_unloaded) { + /*global console*/ + console.info('-- Error dropped, as page is unloaded'); + console.info(e); + return; + } + + error_list.push(e); + // Add error handling stack + error_list.push(new Error('stopping renderJS')); + + body = document.getElementsByTagName('body')[0]; + while (body.firstChild) { + body.removeChild(body.firstChild); + } + + container = document.createElement("section"); + paragraph = document.createElement("h1"); + paragraph.textContent = 'Unhandled Error'; + container.appendChild(paragraph); + + paragraph = document.createElement("p"); + paragraph.textContent = 'Please report this error to the support team'; + container.appendChild(paragraph); + + paragraph = document.createElement("p"); + paragraph.textContent = 'Location: '; + link = document.createElement("a"); + link.href = link.textContent = window.location.toString(); + paragraph.appendChild(link); + container.appendChild(paragraph); + + paragraph = document.createElement("p"); + paragraph.textContent = 'User-agent: ' + navigator.userAgent; + container.appendChild(paragraph); + + paragraph = document.createElement("p"); + paragraph.textContent = 'Date: ' + new Date(Date.now()).toISOString(); + container.appendChild(paragraph); + + body.appendChild(container); + + for (i = 0; i < error_list.length; i += 1) { + error = error_list[i]; + + if (error instanceof Event) { + error = { + string: error.toString(), + message: error.message, + type: error.type, + target: error.target + }; + if (error.target !== undefined) { + error_list.splice(i + 1, 0, error.target); + } + } + + if (error instanceof XMLHttpRequest) { + error = { + message: error.toString(), + readyState: error.readyState, + status: error.status, + statusText: error.statusText, + response: error.response, + responseUrl: error.responseUrl, + response_headers: error.getAllResponseHeaders() + }; + } + if (error.constructor === Array || + error.constructor === String || + error.constructor === Object) { + try { + error = JSON.stringify(error); + } catch (ignore) { + } + } + + container = document.createElement("section"); + + paragraph = document.createElement("h2"); + paragraph.textContent = error.message || error; + container.appendChild(paragraph); + + if (error.fileName !== undefined) { + paragraph = document.createElement("p"); + paragraph.textContent = 'File: ' + + error.fileName + + ': ' + error.lineNumber; + container.appendChild(paragraph); + } + + if (error.stack !== undefined) { + paragraph = document.createElement("pre"); + paragraph.textContent = 'Stack: ' + error.stack; + container.appendChild(paragraph); + } + + body.appendChild(container); + } + // XXX Do not crash the application if it fails + // Where to write the error? + /*global console*/ + console.error(e.stack); + console.error(e); + } + + ///////////////////////////////////////////////////////////////// + // Service Monitor promise + ///////////////////////////////////////////////////////////////// + function ResolvedMonitorError(message) { + this.name = "resolved"; + if ((message !== undefined) && (typeof message !== "string")) { + throw new TypeError('You must pass a string.'); + } + this.message = message || "Default Message"; + } + ResolvedMonitorError.prototype = new Error(); + ResolvedMonitorError.prototype.constructor = ResolvedMonitorError; + + Monitor = function () { + var monitor = this, + promise_list = [], + promise, + reject, + notify, + resolved; + + if (!(this instanceof Monitor)) { + return new Monitor(); + } + + function canceller() { + var len = promise_list.length, + i; + for (i = 0; i < len; i += 1) { + promise_list[i].cancel(); + } + // Clean it to speed up other canceller run + promise_list = []; + } + + promise = new RSVP.Promise(function (done, fail, progress) { + reject = function (rejectedReason) { + if (resolved) { + return; + } + monitor.isRejected = true; + monitor.rejectedReason = rejectedReason; + resolved = true; + canceller(); + return fail(rejectedReason); + }; + notify = progress; + }, canceller); + + monitor.cancel = function () { + if (resolved) { + return; + } + resolved = true; + promise.cancel(); + promise.fail(function (rejectedReason) { + monitor.isRejected = true; + monitor.rejectedReason = rejectedReason; + }); + }; + monitor.then = function () { + return promise.then.apply(promise, arguments); + }; + monitor.fail = function () { + return promise.fail.apply(promise, arguments); + }; + + monitor.monitor = function (promise_to_monitor) { + if (resolved) { + throw new ResolvedMonitorError(); + } + var queue = new RSVP.Queue() + .push(function () { + return promise_to_monitor; + }) + .push(function (fulfillmentValue) { + // Promise to monitor is fullfilled, remove it from the list + var len = promise_list.length, + sub_promise_to_monitor, + new_promise_list = [], + i; + for (i = 0; i < len; i += 1) { + sub_promise_to_monitor = promise_list[i]; + if (!(sub_promise_to_monitor.isFulfilled || + sub_promise_to_monitor.isRejected)) { + new_promise_list.push(sub_promise_to_monitor); + } + } + promise_list = new_promise_list; + }, function (rejectedReason) { + if (rejectedReason instanceof RSVP.CancellationError) { + if (!(promise_to_monitor.isFulfilled && + promise_to_monitor.isRejected)) { + // The queue could be cancelled before the first push is run + promise_to_monitor.cancel(); + } + } + reject(rejectedReason); + throw rejectedReason; + }, function (notificationValue) { + notify(notificationValue); + return notificationValue; + }); + + promise_list.push(queue); + + return this; + }; + }; + + Monitor.prototype = Object.create(RSVP.Promise.prototype); + Monitor.prototype.constructor = Monitor; + + ///////////////////////////////////////////////////////////////// + // RenderJSGadget + ///////////////////////////////////////////////////////////////// + function RenderJSGadget() { + if (!(this instanceof RenderJSGadget)) { + return new RenderJSGadget(); + } + } + RenderJSGadget.prototype.__title = ""; + RenderJSGadget.prototype.__interface_list = []; + RenderJSGadget.prototype.__path = ""; + RenderJSGadget.prototype.__html = ""; + RenderJSGadget.prototype.__required_css_list = []; + RenderJSGadget.prototype.__required_js_list = []; + + function createMonitor(g) { + if (g.__monitor !== undefined) { + g.__monitor.cancel(); + } + g.__monitor = new Monitor(); + g.__job_dict = {}; + g.__job_list = []; + g.__job_triggered = false; + g.__monitor.fail(function (error) { + if (!(error instanceof RSVP.CancellationError)) { + return g.aq_reportServiceError(error); + } + }).fail(function (error) { + // Crash the application if the acquisition generates an error. + return letsCrash(error); + }); + } + + function clearGadgetInternalParameters(g) { + g.__sub_gadget_dict = {}; + createMonitor(g); + } + + function loadSubGadgetDOMDeclaration(g) { + var element_list = g.element.querySelectorAll('[data-gadget-url]'), + element, + promise_list = [], + scope, + url, + sandbox, + i; + + for (i = 0; i < element_list.length; i += 1) { + element = element_list[i]; + scope = element.getAttribute("data-gadget-scope"); + url = element.getAttribute("data-gadget-url"); + sandbox = element.getAttribute("data-gadget-sandbox"); + if (url !== null) { + promise_list.push(g.declareGadget(url, { + element: element, + scope: scope || undefined, + sandbox: sandbox || undefined + })); + } + } + + return RSVP.all(promise_list); + } + + RenderJSGadget.__ready_list = [clearGadgetInternalParameters, + loadSubGadgetDOMDeclaration]; + RenderJSGadget.ready = function (callback) { + this.__ready_list.push(callback); + return this; + }; + RenderJSGadget.setState = function (state_dict) { + var json_state = JSON.stringify(state_dict); + return this.ready(function () { + this.state = JSON.parse(json_state); + }); + }; + RenderJSGadget.onStateChange = function (callback) { + this.prototype.__state_change_callback = callback; + return this; + }; + + RenderJSGadget.__service_list = []; + RenderJSGadget.declareService = function (callback) { + this.__service_list.push(callback); + return this; + }; + RenderJSGadget.onEvent = function (type, callback, use_capture, + prevent_default) { + this.__service_list.push(function () { + return loopEventListener(this.element, type, use_capture, + callback.bind(this), prevent_default); + }); + return this; + }; + + function runJob(gadget, name, callback, argument_list) { + var job_promise = new RSVP.Queue() + .push(function () { + return callback.apply(gadget, argument_list); + }); + if (gadget.__job_dict.hasOwnProperty(name)) { + gadget.__job_dict[name].cancel(); + } + gadget.__job_dict[name] = job_promise; + gadget.__monitor.monitor(new RSVP.Queue() + .push(function () { + return job_promise; + }) + .push(undefined, function (error) { + if (!(error instanceof RSVP.CancellationError)) { + throw error; + } + })); + } + + function startService(gadget) { + gadget.__monitor.monitor(new RSVP.Queue() + .push(function () { + var i, + service_list = gadget.constructor.__service_list, + job_list = gadget.__job_list; + for (i = 0; i < service_list.length; i += 1) { + gadget.__monitor.monitor(service_list[i].apply(gadget)); + } + for (i = 0; i < job_list.length; i += 1) { + runJob(gadget, job_list[i][0], job_list[i][1], job_list[i][2]); + } + gadget.__job_list = []; + gadget.__job_triggered = true; + }) + ); + } + + ///////////////////////////////////////////////////////////////// + // RenderJSGadget.declareJob + // gadget internal method, which trigger execution + // of a function inside a service + ///////////////////////////////////////////////////////////////// + RenderJSGadget.declareJob = function (name, callback) { + this.prototype[name] = function () { + var context = this, + argument_list = arguments; + + if (context.__job_triggered) { + runJob(context, name, callback, argument_list); + } else { + context.__job_list.push([name, callback, argument_list]); + } + }; + // Allow chain + return this; + }; + + ///////////////////////////////////////////////////////////////// + // RenderJSGadget.declareMethod + ///////////////////////////////////////////////////////////////// + RenderJSGadget.declareMethod = function (name, callback) { + this.prototype[name] = function () { + var context = this, + argument_list = arguments; + + return new RSVP.Queue() + .push(function () { + return callback.apply(context, argument_list); + }); + }; + // Allow chain + return this; + }; + + RenderJSGadget + .declareMethod('getInterfaceList', function () { + // Returns the list of gadget prototype + return this.__interface_list; + }) + .declareMethod('getRequiredCSSList', function () { + // Returns a list of CSS required by the gadget + return this.__required_css_list; + }) + .declareMethod('getRequiredJSList', function () { + // Returns a list of JS required by the gadget + return this.__required_js_list; + }) + .declareMethod('getPath', function () { + // Returns the path of the code of a gadget + return this.__path; + }) + .declareMethod('getTitle', function () { + // Returns the title of a gadget + return this.__title; + }) + .declareMethod('getElement', function () { + // Returns the DOM Element of a gadget + // XXX Kept for compatibility. Use element property directly + if (this.element === undefined) { + throw new Error("No element defined"); + } + return this.element; + }) + .declareMethod('changeState', function (state_dict) { + var key, + modified = false, + modification_dict = {}; + for (key in state_dict) { + if (state_dict[key] !== this.state[key]) { + this.state[key] = state_dict[key]; + modification_dict[key] = state_dict[key]; + modified = true; + } + } + if (modified && this.__state_change_callback !== undefined) { + return this.__state_change_callback(modification_dict); + } + }); + + ///////////////////////////////////////////////////////////////// + // RenderJSGadget.declareAcquiredMethod + ///////////////////////////////////////////////////////////////// + function acquire(child_gadget, method_name, argument_list) { + var gadget = this, + key, + gadget_scope; + + for (key in gadget.__sub_gadget_dict) { + if (gadget.__sub_gadget_dict.hasOwnProperty(key)) { + if (gadget.__sub_gadget_dict[key] === child_gadget) { + gadget_scope = key; + } + } + } + return new RSVP.Queue() + .push(function () { + // Do not specify default __acquired_method_dict on prototype + // to prevent modifying this default value (with + // allowPublicAcquiredMethod for example) + var aq_dict = gadget.__acquired_method_dict || {}; + if (aq_dict.hasOwnProperty(method_name)) { + return aq_dict[method_name].apply(gadget, + [argument_list, gadget_scope]); + } + throw new renderJS.AcquisitionError("aq_dynamic is not defined"); + }) + .push(undefined, function (error) { + if (error instanceof renderJS.AcquisitionError) { + return gadget.__aq_parent(method_name, argument_list); + } + throw error; + }); + } + + RenderJSGadget.declareAcquiredMethod = + function (name, method_name_to_acquire) { + this.prototype[name] = function () { + var argument_list = Array.prototype.slice.call(arguments, 0), + gadget = this; + return new RSVP.Queue() + .push(function () { + return gadget.__aq_parent(method_name_to_acquire, argument_list); + }); + }; + + // Allow chain + return this; + }; + RenderJSGadget.declareAcquiredMethod("aq_reportServiceError", + "reportServiceError"); + + ///////////////////////////////////////////////////////////////// + // RenderJSGadget.allowPublicAcquisition + ///////////////////////////////////////////////////////////////// + RenderJSGadget.allowPublicAcquisition = + function (method_name, callback) { + this.prototype.__acquired_method_dict[method_name] = callback; + + // Allow chain + return this; + }; + + // Set aq_parent on gadget_instance which call acquire on parent_gadget + function setAqParent(gadget_instance, parent_gadget) { + gadget_instance.__aq_parent = function (method_name, argument_list) { + return acquire.apply(parent_gadget, [gadget_instance, method_name, + argument_list]); + }; + } + + ///////////////////////////////////////////////////////////////// + // RenderJSEmbeddedGadget + ///////////////////////////////////////////////////////////////// + // Class inheritance + function RenderJSEmbeddedGadget() { + if (!(this instanceof RenderJSEmbeddedGadget)) { + return new RenderJSEmbeddedGadget(); + } + RenderJSGadget.call(this); + } + RenderJSEmbeddedGadget.__ready_list = RenderJSGadget.__ready_list.slice(); + RenderJSEmbeddedGadget.__service_list = + RenderJSGadget.__service_list.slice(); + RenderJSEmbeddedGadget.ready = + RenderJSGadget.ready; + RenderJSEmbeddedGadget.setState = + RenderJSGadget.setState; + RenderJSEmbeddedGadget.onStateChange = + RenderJSGadget.onStateChange; + RenderJSEmbeddedGadget.declareService = + RenderJSGadget.declareService; + RenderJSEmbeddedGadget.onEvent = + RenderJSGadget.onEvent; + RenderJSEmbeddedGadget.prototype = new RenderJSGadget(); + RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget; + + ///////////////////////////////////////////////////////////////// + // privateDeclarePublicGadget + ///////////////////////////////////////////////////////////////// + function privateDeclarePublicGadget(url, options, parent_gadget) { + var gadget_instance; + if (options.element === undefined) { + options.element = document.createElement("div"); + } + + function loadDependency(method, url) { + return function () { + return method(url); + }; + } + + return new RSVP.Queue() + .push(function () { + return renderJS.declareGadgetKlass(url); + }) + // Get the gadget class and instanciate it + .push(function (Klass) { + var i, + template_node_list = Klass.__template_element.body.childNodes; + gadget_loading_klass = Klass; + gadget_instance = new Klass(); + gadget_instance.element = options.element; + gadget_instance.state = {}; + for (i = 0; i < template_node_list.length; i += 1) { + gadget_instance.element.appendChild( + template_node_list[i].cloneNode(true) + ); + } + setAqParent(gadget_instance, parent_gadget); + // Load dependencies if needed + return RSVP.all([ + gadget_instance.getRequiredJSList(), + gadget_instance.getRequiredCSSList() + ]); + }) + // Load all JS/CSS + .push(function (all_list) { + var q = new RSVP.Queue(), + i; + // Load JS + for (i = 0; i < all_list[0].length; i += 1) { + q.push(loadDependency(renderJS.declareJS, all_list[0][i])); + } + // Load CSS + for (i = 0; i < all_list[1].length; i += 1) { + q.push(loadDependency(renderJS.declareCSS, all_list[1][i])); + } + return q; + }) + .push(function () { + return gadget_instance; + }); + } + + ///////////////////////////////////////////////////////////////// + // RenderJSIframeGadget + ///////////////////////////////////////////////////////////////// + function RenderJSIframeGadget() { + if (!(this instanceof RenderJSIframeGadget)) { + return new RenderJSIframeGadget(); + } + RenderJSGadget.call(this); + } + RenderJSIframeGadget.__ready_list = RenderJSGadget.__ready_list.slice(); + RenderJSIframeGadget.ready = + RenderJSGadget.ready; + RenderJSIframeGadget.setState = + RenderJSGadget.setState; + RenderJSIframeGadget.onStateChange = + RenderJSGadget.onStateChange; + RenderJSIframeGadget.__service_list = RenderJSGadget.__service_list.slice(); + RenderJSIframeGadget.declareService = + RenderJSGadget.declareService; + RenderJSIframeGadget.onEvent = + RenderJSGadget.onEvent; + RenderJSIframeGadget.prototype = new RenderJSGadget(); + RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget; + + ///////////////////////////////////////////////////////////////// + // privateDeclareIframeGadget + ///////////////////////////////////////////////////////////////// + function privateDeclareIframeGadget(url, options, parent_gadget) { + var gadget_instance, + iframe, + iframe_loading_deferred = RSVP.defer(); + if (options.element === undefined) { + throw new Error("DOM element is required to create Iframe Gadget " + + url); + } + + // Check if the element is attached to the DOM + if (!document.contains(options.element)) { + throw new Error("The parent element is not attached to the DOM for " + + url); + } + + gadget_instance = new RenderJSIframeGadget(); + setAqParent(gadget_instance, parent_gadget); + iframe = document.createElement("iframe"); +// gadget_instance.element.setAttribute("seamless", "seamless"); + iframe.setAttribute("src", url); + gadget_instance.__path = url; + gadget_instance.element = options.element; + gadget_instance.state = {}; + // Attach it to the DOM + options.element.appendChild(iframe); + + // XXX Manage unbind when deleting the gadget + + // Create the communication channel with the iframe + gadget_instance.__chan = Channel.build({ + window: iframe.contentWindow, + origin: "*", + scope: "renderJS" + }); + + // Create new method from the declareMethod call inside the iframe + gadget_instance.__chan.bind("declareMethod", + function (trans, method_name) { + gadget_instance[method_name] = function () { + var argument_list = arguments, + wait_promise = new RSVP.Promise(function (resolve, reject) { + gadget_instance.__chan.call({ + method: "methodCall", + params: [ + method_name, + Array.prototype.slice.call(argument_list, 0)], + success: function (s) { + resolve(s); + }, + error: function (e) { + reject(e); + } + }); + }); + return new RSVP.Queue() + .push(function () { + return wait_promise; + }); + }; + return "OK"; + }); + + // Wait for the iframe to be loaded before continuing + gadget_instance.__chan.bind("ready", function (trans) { + iframe_loading_deferred.resolve(gadget_instance); + return "OK"; + }); + gadget_instance.__chan.bind("failed", function (trans, params) { + iframe_loading_deferred.reject(params); + return "OK"; + }); + gadget_instance.__chan.bind("acquire", function (trans, params) { + gadget_instance.__aq_parent.apply(gadget_instance, params) + .then(function (g) { + trans.complete(g); + }).fail(function (e) { + trans.error(e.toString()); + }); + trans.delayReturn(true); + }); + + return RSVP.any([ + iframe_loading_deferred.promise, + // Timeout to prevent non renderJS embeddable gadget + // XXX Maybe using iframe.onload/onerror would be safer? + new RSVP.Queue() + .push(function () { + return RSVP.timeout(5000); + }) + .push(undefined, function () { + throw new Error('Timeout while loading: ' + url); + }) + ]); + } + + ///////////////////////////////////////////////////////////////// + // privateDeclareDataUrlGadget + ///////////////////////////////////////////////////////////////// + function privateDeclareDataUrlGadget(url, options, parent_gadget) { + + return new RSVP.Queue() + .push(function () { + return ajax(url); + }) + .push(function (xhr) { + // Insert a "base" element, in order to resolve all relative links + // which could get broken with a data url + var doc = (new DOMParser()).parseFromString(xhr.responseText, + 'text/html'), + base = doc.createElement('base'), + blob; + base.href = url; + doc.head.insertBefore(base, doc.head.firstChild); + blob = new Blob([doc.documentElement.outerHTML], + {type: "text/html;charset=UTF-8"}); + return readBlobAsDataURL(blob); + }) + .push(function (data_url) { + return privateDeclareIframeGadget(data_url, options, parent_gadget); + }); + } + + ///////////////////////////////////////////////////////////////// + // RenderJSGadget.declareGadget + ///////////////////////////////////////////////////////////////// + RenderJSGadget + .declareMethod('declareGadget', function (url, options) { + var queue, + parent_gadget = this, + local_loading_klass_promise, + previous_loading_klass_promise = loading_klass_promise; + + if (options === undefined) { + options = {}; + } + if (options.sandbox === undefined) { + options.sandbox = "public"; + } + + // transform url to absolute url if it is relative + url = renderJS.getAbsoluteURL(url, this.__path); + // Change the global variable to update the loading queue + loading_klass_promise = new RSVP.Queue() + // Wait for previous gadget loading to finish first + .push(function () { + return previous_loading_klass_promise; + }) + .push(undefined, function () { + // Forget previous declareGadget error + return; + }) + .push(function () { + var method; + if (options.sandbox === "public") { + method = privateDeclarePublicGadget; + } else if (options.sandbox === "iframe") { + method = privateDeclareIframeGadget; + } else if (options.sandbox === "dataurl") { + method = privateDeclareDataUrlGadget; + } else { + throw new Error("Unsupported sandbox options '" + + options.sandbox + "'"); + } + return method(url, options, parent_gadget); + }) + // Set the HTML context + .push(function (gadget_instance) { + // Drop the current loading klass info used by selector + gadget_loading_klass = undefined; + return gadget_instance; + }) + .push(undefined, function (e) { + // Drop the current loading klass info used by selector + // even in case of error + gadget_loading_klass = undefined; + throw e; + }); + local_loading_klass_promise = loading_klass_promise; + + queue = new RSVP.Queue() + .push(function () { + return local_loading_klass_promise; + }) + // Set the HTML context + .push(function (gadget_instance) { + var i, + scope; + // Trigger calling of all ready callback + function ready_wrapper() { + return gadget_instance; + } + function ready_executable_wrapper(fct) { + return function (g) { + return fct.call(g, g); + }; + } + for (i = 0; i < gadget_instance.constructor.__ready_list.length; + i += 1) { + // Put a timeout? + queue.push(ready_executable_wrapper( + gadget_instance.constructor.__ready_list[i] + )); + // Always return the gadget instance after ready function + queue.push(ready_wrapper); + } + + // Store local reference to the gadget instance + scope = options.scope; + if (scope === undefined) { + scope = 'RJS_' + scope_increment; + scope_increment += 1; + while (parent_gadget.__sub_gadget_dict.hasOwnProperty(scope)) { + scope = 'RJS_' + scope_increment; + scope_increment += 1; + } + } + parent_gadget.__sub_gadget_dict[scope] = gadget_instance; + gadget_instance.element.setAttribute("data-gadget-scope", + scope); + + // Put some attribute to ease page layout comprehension + gadget_instance.element.setAttribute("data-gadget-url", url); + gadget_instance.element.setAttribute("data-gadget-sandbox", + options.sandbox); + gadget_instance.element._gadget = gadget_instance; + + if (document.contains(gadget_instance.element)) { + // Put a timeout + queue.push(startService); + } + // Always return the gadget instance after ready function + queue.push(ready_wrapper); + + return gadget_instance; + }); + return queue; + }) + .declareMethod('getDeclaredGadget', function (gadget_scope) { + if (!this.__sub_gadget_dict.hasOwnProperty(gadget_scope)) { + throw new Error("Gadget scope '" + gadget_scope + "' is not known."); + } + return this.__sub_gadget_dict[gadget_scope]; + }) + .declareMethod('dropGadget', function (gadget_scope) { + if (!this.__sub_gadget_dict.hasOwnProperty(gadget_scope)) { + throw new Error("Gadget scope '" + gadget_scope + "' is not known."); + } + // http://perfectionkills.com/understanding-delete/ + delete this.__sub_gadget_dict[gadget_scope]; + }); + + ///////////////////////////////////////////////////////////////// + // renderJS selector + ///////////////////////////////////////////////////////////////// + renderJS = function (selector) { + var result; + if (selector === window) { + // window is the 'this' value when loading a javascript file + // In this case, use the current loading gadget constructor + result = gadget_loading_klass; + } + if (result === undefined) { + throw new Error("Unknown selector '" + selector + "'"); + } + return result; + }; + + ///////////////////////////////////////////////////////////////// + // renderJS.AcquisitionError + ///////////////////////////////////////////////////////////////// + renderJS.AcquisitionError = function (message) { + this.name = "AcquisitionError"; + if ((message !== undefined) && (typeof message !== "string")) { + throw new TypeError('You must pass a string.'); + } + this.message = message || "Acquisition failed"; + }; + renderJS.AcquisitionError.prototype = new Error(); + renderJS.AcquisitionError.prototype.constructor = + renderJS.AcquisitionError; + + ///////////////////////////////////////////////////////////////// + // renderJS.getAbsoluteURL + ///////////////////////////////////////////////////////////////// + renderJS.getAbsoluteURL = function (url, base_url) { + var doc, base, link, + html = ""; + + if (url && base_url && !isAbsoluteOrDataURL.test(url)) { + doc = (new DOMParser()).parseFromString(html, 'text/html'); + base = doc.createElement('base'); + link = doc.createElement('link'); + doc.head.appendChild(base); + doc.head.appendChild(link); + base.href = base_url; + link.href = url; + return link.href; + } + return url; + }; + + ///////////////////////////////////////////////////////////////// + // renderJS.declareJS + ///////////////////////////////////////////////////////////////// + renderJS.declareJS = function (url) { + // Prevent infinite recursion if loading render.js + // more than once + var result; + if (javascript_registration_dict.hasOwnProperty(url)) { + result = RSVP.resolve(); + } else { + result = new RSVP.Promise(function (resolve, reject) { + var newScript; + newScript = document.createElement('script'); + newScript.type = 'text/javascript'; + newScript.src = url; + newScript.onload = function () { + javascript_registration_dict[url] = null; + resolve(); + }; + newScript.onerror = function (e) { + reject(e); + }; + document.head.appendChild(newScript); + }); + } + return result; + }; + + ///////////////////////////////////////////////////////////////// + // renderJS.declareCSS + ///////////////////////////////////////////////////////////////// + renderJS.declareCSS = function (url) { + // https://github.com/furf/jquery-getCSS/blob/master/jquery.getCSS.js + // No way to cleanly check if a css has been loaded + // So, always resolve the promise... + // http://requirejs.org/docs/faq-advanced.html#css + var result; + if (stylesheet_registration_dict.hasOwnProperty(url)) { + result = RSVP.resolve(); + } else { + result = new RSVP.Promise(function (resolve, reject) { + var link; + link = document.createElement('link'); + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = url; + link.onload = function () { + stylesheet_registration_dict[url] = null; + resolve(); + }; + link.onerror = function (e) { + reject(e); + }; + document.head.appendChild(link); + }); + } + return result; + }; + + ///////////////////////////////////////////////////////////////// + // renderJS.declareGadgetKlass + ///////////////////////////////////////////////////////////////// + renderJS.declareGadgetKlass = function (url) { + var result; + + function parse(xhr) { + var tmp_constructor, + key, + parsed_html; + if (!gadget_model_dict.hasOwnProperty(url)) { + // Class inheritance + tmp_constructor = function () { + RenderJSGadget.call(this); + }; + tmp_constructor.__ready_list = RenderJSGadget.__ready_list.slice(); + tmp_constructor.__service_list = RenderJSGadget.__service_list.slice(); + tmp_constructor.declareMethod = + RenderJSGadget.declareMethod; + tmp_constructor.declareJob = + RenderJSGadget.declareJob; + tmp_constructor.declareAcquiredMethod = + RenderJSGadget.declareAcquiredMethod; + tmp_constructor.allowPublicAcquisition = + RenderJSGadget.allowPublicAcquisition; + tmp_constructor.ready = + RenderJSGadget.ready; + tmp_constructor.setState = + RenderJSGadget.setState; + tmp_constructor.onStateChange = + RenderJSGadget.onStateChange; + tmp_constructor.declareService = + RenderJSGadget.declareService; + tmp_constructor.onEvent = + RenderJSGadget.onEvent; + tmp_constructor.prototype = new RenderJSGadget(); + tmp_constructor.prototype.constructor = tmp_constructor; + tmp_constructor.prototype.__path = url; + tmp_constructor.prototype.__acquired_method_dict = {}; + // https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest + // https://developer.mozilla.org/en-US/docs/Web/API/DOMParser + // https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM + tmp_constructor.__template_element = + (new DOMParser()).parseFromString(xhr.responseText, "text/html"); + parsed_html = renderJS.parseGadgetHTMLDocument( + tmp_constructor.__template_element, + url + ); + for (key in parsed_html) { + if (parsed_html.hasOwnProperty(key)) { + tmp_constructor.prototype['__' + key] = parsed_html[key]; + } + } + + gadget_model_dict[url] = tmp_constructor; + } + + return gadget_model_dict[url]; + } + + if (gadget_model_dict.hasOwnProperty(url)) { + // Return klass object if it already exists + result = RSVP.resolve(gadget_model_dict[url]); + } else { + // Fetch the HTML page and parse it + result = new RSVP.Queue() + .push(function () { + return ajax(url); + }) + .push(function (xhr) { + return parse(xhr); + }); + } + return result; + }; + + ///////////////////////////////////////////////////////////////// + // renderJS.clearGadgetKlassList + ///////////////////////////////////////////////////////////////// + // For test purpose only + renderJS.clearGadgetKlassList = function () { + gadget_model_dict = {}; + javascript_registration_dict = {}; + stylesheet_registration_dict = {}; + }; + + ///////////////////////////////////////////////////////////////// + // renderJS.parseGadgetHTMLDocument + ///////////////////////////////////////////////////////////////// + renderJS.parseGadgetHTMLDocument = function (document_element, url) { + var settings = { + title: "", + interface_list: [], + required_css_list: [], + required_js_list: [] + }, + i, + element; + + if (!url || !isAbsoluteOrDataURL.test(url)) { + throw new Error("The url should be absolute: " + url); + } + + if (document_element.nodeType === 9) { + settings.title = document_element.title; + + if (document_element.head !== null) { + for (i = 0; i < document_element.head.children.length; i += 1) { + element = document_element.head.children[i]; + if (element.href !== null) { + // XXX Manage relative URL during extraction of URLs + // element.href returns absolute URL in firefox but "" in chrome; + if (element.rel === "stylesheet") { + settings.required_css_list.push( + renderJS.getAbsoluteURL(element.getAttribute("href"), url) + ); + } else if (element.nodeName === "SCRIPT" && + (element.type === "text/javascript" || + !element.type)) { + settings.required_js_list.push( + renderJS.getAbsoluteURL(element.getAttribute("src"), url) + ); + } else if (element.rel === + "http://www.renderjs.org/rel/interface") { + settings.interface_list.push( + renderJS.getAbsoluteURL(element.getAttribute("href"), url) + ); + } + } + } + } + } else { + throw new Error("The first parameter should be an HTMLDocument"); + } + return settings; + }; + + ///////////////////////////////////////////////////////////////// + // global + ///////////////////////////////////////////////////////////////// + window.rJS = window.renderJS = renderJS; + window.__RenderJSGadget = RenderJSGadget; + window.__RenderJSEmbeddedGadget = RenderJSEmbeddedGadget; + window.__RenderJSIframeGadget = RenderJSIframeGadget; + + /////////////////////////////////////////////////// + // Bootstrap process. Register the self gadget. + /////////////////////////////////////////////////// + + function bootstrap() { + var url = removeHash(window.location.href), + tmp_constructor, + root_gadget, + loading_gadget_promise = new RSVP.Queue(), + declare_method_count = 0, + embedded_channel, + notifyReady, + notifyDeclareMethod, + gadget_ready = false, + iframe_top_gadget, + last_acquisition_gadget, + declare_method_list_waiting = [], + gadget_failed = false, + gadget_error, + connection_ready = false; + + // Create the gadget class for the current url + if (gadget_model_dict.hasOwnProperty(url)) { + throw new Error("bootstrap should not be called twice"); + } + loading_klass_promise = new RSVP.Promise(function (resolve, reject) { + + last_acquisition_gadget = new RenderJSGadget(); + last_acquisition_gadget.__acquired_method_dict = { + reportServiceError: function (param_list) { + letsCrash(param_list[0]); + } + }; + // Stop acquisition on the last acquisition gadget + // Do not put this on the klass, as their could be multiple instances + last_acquisition_gadget.__aq_parent = function (method_name) { + throw new renderJS.AcquisitionError( + "No gadget provides " + method_name + ); + }; + + //we need to determine tmp_constructor's value before exit bootstrap + //because of function : renderJS + //but since the channel checking is async, + //we can't use code structure like: + // if channel communication is ok + // tmp_constructor = RenderJSGadget + // else + // tmp_constructor = RenderJSEmbeddedGadget + if (window.self === window.top) { + // XXX Copy/Paste from declareGadgetKlass + tmp_constructor = function () { + RenderJSGadget.call(this); + }; + tmp_constructor.declareMethod = RenderJSGadget.declareMethod; + tmp_constructor.declareJob = RenderJSGadget.declareJob; + tmp_constructor.declareAcquiredMethod = + RenderJSGadget.declareAcquiredMethod; + tmp_constructor.allowPublicAcquisition = + RenderJSGadget.allowPublicAcquisition; + tmp_constructor.__ready_list = RenderJSGadget.__ready_list.slice(); + tmp_constructor.ready = RenderJSGadget.ready; + tmp_constructor.setState = RenderJSGadget.setState; + tmp_constructor.onStateChange = RenderJSGadget.onStateChange; + tmp_constructor.__service_list = RenderJSGadget.__service_list.slice(); + tmp_constructor.declareService = + RenderJSGadget.declareService; + tmp_constructor.onEvent = + RenderJSGadget.onEvent; + tmp_constructor.prototype = new RenderJSGadget(); + tmp_constructor.prototype.constructor = tmp_constructor; + tmp_constructor.prototype.__path = url; + gadget_model_dict[url] = tmp_constructor; + + // Create the root gadget instance and put it in the loading stack + root_gadget = new gadget_model_dict[url](); + + setAqParent(root_gadget, last_acquisition_gadget); + + } else { + // Create the root gadget instance and put it in the loading stack + tmp_constructor = RenderJSEmbeddedGadget; + tmp_constructor.__ready_list = RenderJSGadget.__ready_list.slice(); + tmp_constructor.__service_list = RenderJSGadget.__service_list.slice(); + tmp_constructor.prototype.__path = url; + root_gadget = new RenderJSEmbeddedGadget(); + setAqParent(root_gadget, last_acquisition_gadget); + + // Create the communication channel + embedded_channel = Channel.build({ + window: window.parent, + origin: "*", + scope: "renderJS", + onReady: function () { + var k; + iframe_top_gadget = false; + //Default: Define __aq_parent to inform parent window + root_gadget.__aq_parent = + tmp_constructor.prototype.__aq_parent = function (method_name, + argument_list, time_out) { + return new RSVP.Promise(function (resolve, reject) { + embedded_channel.call({ + method: "acquire", + params: [ + method_name, + argument_list + ], + success: function (s) { + resolve(s); + }, + error: function (e) { + reject(e); + }, + timeout: time_out + }); + }); + }; + + // Channel is ready, so now declare Function + notifyDeclareMethod = function (name) { + declare_method_count += 1; + embedded_channel.call({ + method: "declareMethod", + params: name, + success: function () { + declare_method_count -= 1; + notifyReady(); + }, + error: function () { + declare_method_count -= 1; + } + }); + }; + for (k = 0; k < declare_method_list_waiting.length; k += 1) { + notifyDeclareMethod(declare_method_list_waiting[k]); + } + declare_method_list_waiting = []; + // If Gadget Failed Notify Parent + if (gadget_failed) { + embedded_channel.notify({ + method: "failed", + params: gadget_error + }); + return; + } + connection_ready = true; + notifyReady(); + //the channel is ok + //so bind calls to renderJS method on the instance + embedded_channel.bind("methodCall", function (trans, v) { + root_gadget[v[0]].apply(root_gadget, v[1]) + .then(function (g) { + trans.complete(g); + }).fail(function (e) { + trans.error(e.toString()); + }); + trans.delayReturn(true); + }); + } + }); + + // Notify parent about gadget instanciation + notifyReady = function () { + if ((declare_method_count === 0) && (gadget_ready === true)) { + embedded_channel.notify({method: "ready"}); + } + }; + + // Inform parent gadget about declareMethod calls here. + notifyDeclareMethod = function (name) { + declare_method_list_waiting.push(name); + }; + + notifyDeclareMethod("getInterfaceList"); + notifyDeclareMethod("getRequiredCSSList"); + notifyDeclareMethod("getRequiredJSList"); + notifyDeclareMethod("getPath"); + notifyDeclareMethod("getTitle"); + + // Surcharge declareMethod to inform parent window + tmp_constructor.declareMethod = function (name, callback) { + var result = RenderJSGadget.declareMethod.apply( + this, + [name, callback] + ); + notifyDeclareMethod(name); + return result; + }; + + tmp_constructor.declareService = + RenderJSGadget.declareService; + tmp_constructor.declareJob = + RenderJSGadget.declareJob; + tmp_constructor.onEvent = + RenderJSGadget.onEvent; + tmp_constructor.declareAcquiredMethod = + RenderJSGadget.declareAcquiredMethod; + tmp_constructor.allowPublicAcquisition = + RenderJSGadget.allowPublicAcquisition; + + iframe_top_gadget = true; + } + + tmp_constructor.prototype.__acquired_method_dict = {}; + gadget_loading_klass = tmp_constructor; + + function init() { + // XXX HTML properties can only be set when the DOM is fully loaded + var settings = renderJS.parseGadgetHTMLDocument(document, url), + j, + key; + for (key in settings) { + if (settings.hasOwnProperty(key)) { + tmp_constructor.prototype['__' + key] = settings[key]; + } + } + tmp_constructor.__template_element = document.createElement("div"); + root_gadget.element = document.body; + root_gadget.state = {}; + for (j = 0; j < root_gadget.element.childNodes.length; j += 1) { + tmp_constructor.__template_element.appendChild( + root_gadget.element.childNodes[j].cloneNode(true) + ); + } + RSVP.all([root_gadget.getRequiredJSList(), + root_gadget.getRequiredCSSList()]) + .then(function (all_list) { + var i, + js_list = all_list[0], + css_list = all_list[1]; + for (i = 0; i < js_list.length; i += 1) { + javascript_registration_dict[js_list[i]] = null; + } + for (i = 0; i < css_list.length; i += 1) { + stylesheet_registration_dict[css_list[i]] = null; + } + gadget_loading_klass = undefined; + }).then(function () { + + // select the target node + var target = document.querySelector('body'), + // create an observer instance + observer = new MutationObserver(function (mutations) { + var i, k, len, len2, node, added_list; + mutations.forEach(function (mutation) { + if (mutation.type === 'childList') { + + len = mutation.removedNodes.length; + for (i = 0; i < len; i += 1) { + node = mutation.removedNodes[i]; + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.hasAttribute("data-gadget-url") && + (node._gadget !== undefined)) { + createMonitor(node._gadget); + } + added_list = + node.querySelectorAll("[data-gadget-url]"); + len2 = added_list.length; + for (k = 0; k < len2; k += 1) { + node = added_list[k]; + if (node._gadget !== undefined) { + createMonitor(node._gadget); + } + } + } + } + + len = mutation.addedNodes.length; + for (i = 0; i < len; i += 1) { + node = mutation.addedNodes[i]; + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.hasAttribute("data-gadget-url") && + (node._gadget !== undefined)) { + if (document.contains(node)) { + startService(node._gadget); + } + } + added_list = + node.querySelectorAll("[data-gadget-url]"); + len2 = added_list.length; + for (k = 0; k < len2; k += 1) { + node = added_list[k]; + if (document.contains(node)) { + if (node._gadget !== undefined) { + startService(node._gadget); + } + } + } + } + } + + } + }); + }), + // configuration of the observer: + config = { + childList: true, + subtree: true, + attributes: false, + characterData: false + }; + + // pass in the target node, as well as the observer options + observer.observe(target, config); + + return root_gadget; + }).then(resolve, function (e) { + reject(e); + console.error(e); + throw e; + }); + } + document.addEventListener('DOMContentLoaded', init, false); + }); + + loading_gadget_promise + .push(function () { + return loading_klass_promise; + }) + .push(function (root_gadget) { + var i; + + function ready_wrapper() { + return root_gadget; + } + function ready_executable_wrapper(fct) { + return function (g) { + return fct.call(g, g); + }; + } + tmp_constructor.ready(function (g) { + return startService(g); + }); + + loading_gadget_promise.push(ready_wrapper); + for (i = 0; i < tmp_constructor.__ready_list.length; i += 1) { + // Put a timeout? + loading_gadget_promise + .push(ready_executable_wrapper(tmp_constructor.__ready_list[i])) + // Always return the gadget instance after ready function + .push(ready_wrapper); + } + }); + if (window.self === window.top) { + loading_gadget_promise + .fail(function (e) { + letsCrash(e); + throw e; + }); + } else { + // Inform parent window that gadget is correctly loaded + loading_gadget_promise + .then(function () { + gadget_ready = true; + if (connection_ready) { + notifyReady(); + } + }) + .fail(function (e) { + //top gadget in iframe + if (iframe_top_gadget) { + gadget_failed = true; + gadget_error = e.toString(); + letsCrash(e); + } else { + embedded_channel.notify({method: "failed", params: e.toString()}); + } + throw e; + }); + } + + } + bootstrap(); + +}(document, window, RSVP, DOMParser, Channel, MutationObserver, Node, + FileReader, Blob, navigator, Event)); diff --git a/dist/renderjs-0.11.0.min.js b/dist/renderjs-0.11.0.min.js new file mode 100644 index 0000000..ff1caf8 --- /dev/null +++ b/dist/renderjs-0.11.0.min.js @@ -0,0 +1 @@ +var Channel=function(){"use strict";function a(a,b,c,d){function f(b){for(var c=0;c1)throw"scope may not contain double colons: '::'"}var j=function(){for(var a="",b="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",c=0;5>c;c++)a+=b.charAt(Math.floor(Math.random()*b.length));return a}(),k={},l={},m={},n=!1,o=[],p=function(a,b,c){var d=!1,e=!1;return{origin:b,invoke:function(b,d){if(!m[a])throw"attempting to invoke a callback of a nonexistent transaction: "+a;for(var e=!1,f=0;f0)for(var j=0;j=300||!/^text\/html[;]?/.test(e.getResponseHeader("Content-Type")||"")?c(e):b(e))}catch(a){c(a)}}e=new XMLHttpRequest,e.open("GET",a),e.onreadystatechange=d,e.setRequestHeader("Accept","text/html"),e.withCredentials=!0,e.send()}function d(){void 0!==e&&e.readyState!==e.DONE&&e.abort()}var e;return new c.Promise(b,d)}function o(a){var b=a.indexOf("#");return b>0&&(a=a.substring(0,b)),a}function p(c){var d,e,f,g,h,i;if(O)return console.info("-- Error dropped, as page is unloaded"),void console.info(c);for(P.push(c),P.push(new Error("stopping renderJS")),e=a.getElementsByTagName("body")[0];e.firstChild;)e.removeChild(e.firstChild);for(f=a.createElement("section"),g=a.createElement("h1"),g.textContent="Unhandled Error",f.appendChild(g),g=a.createElement("p"),g.textContent="Please report this error to the support team",f.appendChild(g),g=a.createElement("p"),g.textContent="Location: ",h=a.createElement("a"),h.href=h.textContent=b.location.toString(),g.appendChild(h),f.appendChild(g),g=a.createElement("p"),g.textContent="User-agent: "+j.userAgent,f.appendChild(g),g=a.createElement("p"),g.textContent="Date: "+new Date(Date.now()).toISOString(),f.appendChild(g),e.appendChild(f),d=0;dc;c+=1)if(h=b.removedNodes[c],h.nodeType===g.ELEMENT_NODE)for(h.hasAttribute("data-gadget-url")&&void 0!==h._gadget&&s(h._gadget),i=h.querySelectorAll("[data-gadget-url]"),f=i.length,d=0;f>d;d+=1)h=i[d],void 0!==h._gadget&&s(h._gadget);for(e=b.addedNodes.length,c=0;e>c;c+=1)if(h=b.addedNodes[c],h.nodeType===g.ELEMENT_NODE)for(h.hasAttribute("data-gadget-url")&&void 0!==h._gadget&&a.contains(h)&&w(h._gadget),i=h.querySelectorAll("[data-gadget-url]"),f=i.length,d=0;f>d;d+=1)h=i[d],a.contains(h)&&void 0!==h._gadget&&w(h._gadget)}})}),d={childList:!0,subtree:!0,attributes:!1,characterData:!1};return c.observe(b,d),h}).then(o,function(a){throw t(a),console.error(a),a})}m=new r,m.__acquired_method_dict={reportServiceError:function(a){p(a[0])}},m.__aq_parent=function(a){throw new H.AcquisitionError("No gadget provides "+a)},b.self===b.top?(d=function(){r.call(this)},d.declareMethod=r.declareMethod,d.declareJob=r.declareJob,d.declareAcquiredMethod=r.declareAcquiredMethod,d.allowPublicAcquisition=r.allowPublicAcquisition,d.__ready_list=r.__ready_list.slice(),d.ready=r.ready,d.setState=r.setState,d.onStateChange=r.onStateChange,d.__service_list=r.__service_list.slice(),d.declareService=r.declareService,d.onEvent=r.onEvent,d.prototype=new r,d.prototype.constructor=d,d.prototype.__path=q,J[q]=d,h=new J[q],y(h,m)):(d=z,d.__ready_list=r.__ready_list.slice(),d.__service_list=r.__service_list.slice(),d.prototype.__path=q,h=new z,y(h,m),i=e.build({window:b.parent,origin:"*",scope:"renderJS",onReady:function(){var a;for(l=!1,h.__aq_parent=d.prototype.__aq_parent=function(a,b,d){return new c.Promise(function(c,e){i.call({method:"acquire",params:[a,b],success:function(a){c(a)},error:function(a){e(a)},timeout:d})})},k=function(a){u+=1,i.call({method:"declareMethod",params:a,success:function(){u-=1,j()},error:function(){u-=1}})},a=0;aa;a+=1)h[a].cancel();h=[]}var b,d,e,f,g=this,h=[];return this instanceof I?(b=new c.Promise(function(b,c,h){d=function(b){return f?void 0:(g.isRejected=!0,g.rejectedReason=b,f=!0,a(),c(b))},e=h},a),g.cancel=function(){f||(f=!0,b.cancel(),b.fail(function(a){g.isRejected=!0,g.rejectedReason=a}))},g.then=function(){return b.then.apply(b,arguments)},g.fail=function(){return b.fail.apply(b,arguments)},void(g.monitor=function(a){if(f)throw new q;var b=(new c.Queue).push(function(){return a}).push(function(a){var b,c,d=h.length,e=[];for(c=0;d>c;c+=1)b=h[c],b.isFulfilled||b.isRejected||e.push(b);h=e},function(b){throw b instanceof c.CancellationError&&(a.isFulfilled&&a.isRejected||a.cancel()),d(b),b},function(a){return e(a),a});return h.push(b),this})):new I},I.prototype=Object.create(c.Promise.prototype),I.prototype.constructor=I,r.prototype.__title="",r.prototype.__interface_list=[],r.prototype.__path="",r.prototype.__html="",r.prototype.__required_css_list=[],r.prototype.__required_js_list=[],r.__ready_list=[t,u],r.ready=function(a){return this.__ready_list.push(a),this},r.setState=function(a){var b=JSON.stringify(a);return this.ready(function(){this.state=JSON.parse(b)})},r.onStateChange=function(a){return this.prototype.__state_change_callback=a,this},r.__service_list=[],r.declareService=function(a){return this.__service_list.push(a),this},r.onEvent=function(a,b,c,d){return this.__service_list.push(function(){return m(this.element,a,c,b.bind(this),d)}),this},r.declareJob=function(a,b){return this.prototype[a]=function(){var c=this,d=arguments;c.__job_triggered?v(c,a,b,d):c.__job_list.push([a,b,d])},this},r.declareMethod=function(a,b){return this.prototype[a]=function(){var a=this,d=arguments;return(new c.Queue).push(function(){return b.apply(a,d)})},this},r.declareMethod("getInterfaceList",function(){return this.__interface_list}).declareMethod("getRequiredCSSList",function(){return this.__required_css_list}).declareMethod("getRequiredJSList",function(){return this.__required_js_list}).declareMethod("getPath",function(){return this.__path}).declareMethod("getTitle",function(){return this.__title}).declareMethod("getElement",function(){if(void 0===this.element)throw new Error("No element defined");return this.element}).declareMethod("changeState",function(a){var b,c=!1,d={};for(b in a)a[b]!==this.state[b]&&(this.state[b]=a[b],d[b]=a[b],c=!0);return c&&void 0!==this.__state_change_callback?this.__state_change_callback(d):void 0}),r.declareAcquiredMethod=function(a,b){return this.prototype[a]=function(){var a=Array.prototype.slice.call(arguments,0),d=this;return(new c.Queue).push(function(){return d.__aq_parent(b,a)})},this},r.declareAcquiredMethod("aq_reportServiceError","reportServiceError"),r.allowPublicAcquisition=function(a,b){return this.prototype.__acquired_method_dict[a]=b,this},z.__ready_list=r.__ready_list.slice(),z.__service_list=r.__service_list.slice(),z.ready=r.ready,z.setState=r.setState,z.onStateChange=r.onStateChange,z.declareService=r.declareService,z.onEvent=r.onEvent,z.prototype=new r,z.prototype.constructor=z,B.__ready_list=r.__ready_list.slice(),B.ready=r.ready,B.setState=r.setState,B.onStateChange=r.onStateChange,B.__service_list=r.__service_list.slice(),B.declareService=r.declareService,B.onEvent=r.onEvent,B.prototype=new r,B.prototype.constructor=B,r.declareMethod("declareGadget",function(b,d){var e,f,g=this,h=G;return void 0===d&&(d={}),void 0===d.sandbox&&(d.sandbox="public"),b=H.getAbsoluteURL(b,this.__path),G=(new c.Queue).push(function(){return h}).push(void 0,function(){}).push(function(){var a;if("public"===d.sandbox)a=A;else if("iframe"===d.sandbox)a=C;else{if("dataurl"!==d.sandbox)throw new Error("Unsupported sandbox options '"+d.sandbox+"'");a=D}return a(b,d,g)}).push(function(a){return F=void 0,a}).push(void 0,function(a){throw F=void 0,a}),f=G,e=(new c.Queue).push(function(){return f}).push(function(c){function f(){return c}function h(a){return function(b){return a.call(b,b)}}var i,j;for(i=0;i1)throw"scope may not contain double colons: '::'"}var j=function(){for(var a="",b="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",c=0;5>c;c++)a+=b.charAt(Math.floor(Math.random()*b.length));return a}(),k={},l={},m={},n=!1,o=[],p=function(a,b,c){var d=!1,e=!1;return{origin:b,invoke:function(b,d){if(!m[a])throw"attempting to invoke a callback of a nonexistent transaction: "+a;for(var e=!1,f=0;f0)for(var j=0;j=300||!/^text\/html[;]?/.test(e.getResponseHeader("Content-Type")||"")?c(e):b(e))}catch(a){c(a)}}e=new XMLHttpRequest,e.open("GET",a),e.onreadystatechange=d,e.setRequestHeader("Accept","text/html"),e.withCredentials=!0,e.send()}function d(){void 0!==e&&e.readyState!==e.DONE&&e.abort()}var e;return new c.Promise(b,d)}function n(a){var b=a.indexOf("#");return b>0&&(a=a.substring(0,b)),a}function o(c){var d,e,f,g,h,i;if(M)return console.info("-- Error dropped, as page is unloaded"),void console.info(c);for(N.push(c),N.push(new Error("stopping renderJS")),e=a.getElementsByTagName("body")[0];e.firstChild;)e.removeChild(e.firstChild);for(f=a.createElement("section"),g=a.createElement("h1"),g.textContent="Unhandled Error",f.appendChild(g),g=a.createElement("p"),g.textContent="Please report this error to the support team",f.appendChild(g),g=a.createElement("p"),g.textContent="Location: ",h=a.createElement("a"),h.href=h.textContent=b.location.toString(),g.appendChild(h),f.appendChild(g),g=a.createElement("p"),g.textContent="User-agent: "+j.userAgent,f.appendChild(g),e.appendChild(f),d=0;dc;c+=1)if(h=b.removedNodes[c],h.nodeType===g.ELEMENT_NODE)for(h.hasAttribute("data-gadget-url")&&void 0!==h._gadget&&r(h._gadget),i=h.querySelectorAll("[data-gadget-url]"),f=i.length,d=0;f>d;d+=1)h=i[d],void 0!==h._gadget&&r(h._gadget);for(e=b.addedNodes.length,c=0;e>c;c+=1)if(h=b.addedNodes[c],h.nodeType===g.ELEMENT_NODE)for(h.hasAttribute("data-gadget-url")&&void 0!==h._gadget&&a.contains(h)&&u(h._gadget),i=h.querySelectorAll("[data-gadget-url]"),f=i.length,d=0;f>d;d+=1)h=i[d],a.contains(h)&&void 0!==h._gadget&&u(h._gadget)}})}),d={childList:!0,subtree:!0,attributes:!1,characterData:!1};return c.observe(b,d),h}).then(n,function(a){throw t(a),console.error(a),a})}m=new q,m.__acquired_method_dict={reportServiceError:function(a){o(a[0])}},m.__aq_parent=function(a){throw new F.AcquisitionError("No gadget provides "+a)},b.self===b.top?(d=function(){q.call(this)},d.declareMethod=q.declareMethod,d.declareAcquiredMethod=q.declareAcquiredMethod,d.allowPublicAcquisition=q.allowPublicAcquisition,d.__ready_list=q.__ready_list.slice(),d.ready=q.ready,d.__service_list=q.__service_list.slice(),d.declareService=q.declareService,d.prototype=new q,d.prototype.constructor=d,d.prototype.__path=s,H[s]=d,h=new H[s],w(h,m)):(d=x,d.__ready_list=q.__ready_list.slice(),d.__service_list=q.__service_list.slice(),d.prototype.__path=s,h=new x,w(h,m),i=e.build({window:b.parent,origin:"*",scope:"renderJS",onReady:function(){var a;for(l=!1,h.__aq_parent=d.prototype.__aq_parent=function(a,b,d){return new c.Promise(function(c,e){i.call({method:"acquire",params:[a,b],success:function(a){c(a)},error:function(a){e(a)},timeout:d})})},k=function(a){v+=1,i.call({method:"declareMethod",params:a,success:function(){v-=1,j()},error:function(){v-=1}})},a=0;aa;a+=1)h[a].cancel();h=[]}var b,d,e,f,g=this,h=[];return this instanceof G?(b=new c.Promise(function(b,c,h){d=function(b){return f?void 0:(g.isRejected=!0,g.rejectedReason=b,f=!0,a(),c(b))},e=h},a),g.cancel=function(){f||(f=!0,b.cancel(),b.fail(function(a){g.isRejected=!0,g.rejectedReason=a}))},g.then=function(){return b.then.apply(b,arguments)},g.fail=function(){return b.fail.apply(b,arguments)},void(g.monitor=function(a){if(f)throw new p;var b=(new c.Queue).push(function(){return a}).push(function(a){var b,c,d=h.length,e=[];for(c=0;d>c;c+=1)b=h[c],b.isFulfilled||b.isRejected||e.push(b);h=e},function(b){throw b instanceof c.CancellationError&&(a.isFulfilled&&a.isRejected||a.cancel()),d(b),b},function(a){return e(a),a});return h.push(b),this})):new G},G.prototype=Object.create(c.Promise.prototype),G.prototype.constructor=G,q.prototype.__title="",q.prototype.__interface_list=[],q.prototype.__path="",q.prototype.__html="",q.prototype.__required_css_list=[],q.prototype.__required_js_list=[],q.__ready_list=[s,t],q.ready=function(a){return this.__ready_list.push(a),this},q.__service_list=[],q.declareService=function(a){return this.__service_list.push(a),this},q.declareMethod=function(a,b){return this.prototype[a]=function(){var a=this,d=arguments;return(new c.Queue).push(function(){return b.apply(a,d)})},this},q.declareMethod("getInterfaceList",function(){return this.__interface_list}).declareMethod("getRequiredCSSList",function(){return this.__required_css_list}).declareMethod("getRequiredJSList",function(){return this.__required_js_list}).declareMethod("getPath",function(){return this.__path}).declareMethod("getTitle",function(){return this.__title}).declareMethod("getElement",function(){if(void 0===this.__element)throw new Error("No element defined");return this.__element}),q.declareAcquiredMethod=function(a,b){return this.prototype[a]=function(){var a=Array.prototype.slice.call(arguments,0),d=this;return(new c.Queue).push(function(){return d.__aq_parent(b,a)})},this},q.declareAcquiredMethod("aq_reportServiceError","reportServiceError"),q.allowPublicAcquisition=function(a,b){return this.prototype.__acquired_method_dict[a]=b,this},x.__ready_list=q.__ready_list.slice(),x.__service_list=q.__service_list.slice(),x.ready=q.ready,x.declareService=q.declareService,x.prototype=new q,x.prototype.constructor=x,z.__ready_list=q.__ready_list.slice(),z.ready=q.ready,z.__service_list=q.__service_list.slice(),z.declareService=q.declareService,z.prototype=new q,z.prototype.constructor=z,q.declareMethod("declareGadget",function(b,d){var e,f,g=this,h=E;return void 0===d&&(d={}),void 0===d.sandbox&&(d.sandbox="public"),b=F.getAbsoluteURL(b,this.__path),E=(new c.Queue).push(function(){return h}).push(void 0,function(){}).push(function(){var a;if("public"===d.sandbox)a=y;else if("iframe"===d.sandbox)a=A;else{if("dataurl"!==d.sandbox)throw new Error("Unsupported sandbox options '"+d.sandbox+"'");a=B}return a(b,d,g)}).push(function(a){return D=void 0,a}).push(void 0,function(a){throw D=void 0,a}),f=E,e=(new c.Queue).push(function(){return f}).push(function(c){function f(){return c}var h,i;for(h=0;h1)throw"scope may not contain double colons: '::'"}var j=function(){for(var a="",b="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",c=0;5>c;c++)a+=b.charAt(Math.floor(Math.random()*b.length));return a}(),k={},l={},m={},n=!1,o=[],p=function(a,b,c){var d=!1,e=!1;return{origin:b,invoke:function(b,d){if(!m[a])throw"attempting to invoke a callback of a nonexistent transaction: "+a;for(var e=!1,f=0;f0)for(var j=0;j=300||!/^text\/html[;]?/.test(e.getResponseHeader("Content-Type")||"")?c(e):b(e))}catch(a){c(a)}}e=new XMLHttpRequest,e.open("GET",a),e.onreadystatechange=d,e.setRequestHeader("Accept","text/html"),e.withCredentials=!0,e.send()}function d(){void 0!==e&&e.readyState!==e.DONE&&e.abort()}var e;return new c.Promise(b,d)}function o(a){var b=a.indexOf("#");return b>0&&(a=a.substring(0,b)),a}function p(c){var d,e,f,g,h,i;if(O)return console.info("-- Error dropped, as page is unloaded"),void console.info(c);for(P.push(c),P.push(new Error("stopping renderJS")),e=a.getElementsByTagName("body")[0];e.firstChild;)e.removeChild(e.firstChild);for(f=a.createElement("section"),g=a.createElement("h1"),g.textContent="Unhandled Error",f.appendChild(g),g=a.createElement("p"),g.textContent="Please report this error to the support team",f.appendChild(g),g=a.createElement("p"),g.textContent="Location: ",h=a.createElement("a"),h.href=h.textContent=b.location.toString(),g.appendChild(h),f.appendChild(g),g=a.createElement("p"),g.textContent="User-agent: "+j.userAgent,f.appendChild(g),g=a.createElement("p"),g.textContent="Date: "+new Date(Date.now()).toISOString(),f.appendChild(g),e.appendChild(f),d=0;dc;c+=1)if(h=b.removedNodes[c],h.nodeType===g.ELEMENT_NODE)for(h.hasAttribute("data-gadget-url")&&void 0!==h._gadget&&s(h._gadget),i=h.querySelectorAll("[data-gadget-url]"),f=i.length,d=0;f>d;d+=1)h=i[d],void 0!==h._gadget&&s(h._gadget);for(e=b.addedNodes.length,c=0;e>c;c+=1)if(h=b.addedNodes[c],h.nodeType===g.ELEMENT_NODE)for(h.hasAttribute("data-gadget-url")&&void 0!==h._gadget&&a.contains(h)&&w(h._gadget),i=h.querySelectorAll("[data-gadget-url]"),f=i.length,d=0;f>d;d+=1)h=i[d],a.contains(h)&&void 0!==h._gadget&&w(h._gadget)}})}),d={childList:!0,subtree:!0,attributes:!1,characterData:!1};return c.observe(b,d),h}).then(o,function(a){throw t(a),console.error(a),a})}m=new r,m.__acquired_method_dict={reportServiceError:function(a){p(a[0])}},m.__aq_parent=function(a){throw new H.AcquisitionError("No gadget provides "+a)},b.self===b.top?(d=function(){r.call(this)},d.declareMethod=r.declareMethod,d.declareJob=r.declareJob,d.declareAcquiredMethod=r.declareAcquiredMethod,d.allowPublicAcquisition=r.allowPublicAcquisition,d.__ready_list=r.__ready_list.slice(),d.ready=r.ready,d.setState=r.setState,d.onStateChange=r.onStateChange,d.__service_list=r.__service_list.slice(),d.declareService=r.declareService,d.onEvent=r.onEvent,d.prototype=new r,d.prototype.constructor=d,d.prototype.__path=q,J[q]=d,h=new J[q],y(h,m)):(d=z,d.__ready_list=r.__ready_list.slice(),d.__service_list=r.__service_list.slice(),d.prototype.__path=q,h=new z,y(h,m),i=e.build({window:b.parent,origin:"*",scope:"renderJS",onReady:function(){var a;for(l=!1,h.__aq_parent=d.prototype.__aq_parent=function(a,b,d){return new c.Promise(function(c,e){i.call({method:"acquire",params:[a,b],success:function(a){c(a)},error:function(a){e(a)},timeout:d})})},k=function(a){u+=1,i.call({method:"declareMethod",params:a,success:function(){u-=1,j()},error:function(){u-=1}})},a=0;aa;a+=1)h[a].cancel();h=[]}var b,d,e,f,g=this,h=[];return this instanceof I?(b=new c.Promise(function(b,c,h){d=function(b){return f?void 0:(g.isRejected=!0,g.rejectedReason=b,f=!0,a(),c(b))},e=h},a),g.cancel=function(){f||(f=!0,b.cancel(),b.fail(function(a){g.isRejected=!0,g.rejectedReason=a}))},g.then=function(){return b.then.apply(b,arguments)},g.fail=function(){return b.fail.apply(b,arguments)},void(g.monitor=function(a){if(f)throw new q;var b=(new c.Queue).push(function(){return a}).push(function(a){var b,c,d=h.length,e=[];for(c=0;d>c;c+=1)b=h[c],b.isFulfilled||b.isRejected||e.push(b);h=e},function(b){throw b instanceof c.CancellationError&&(a.isFulfilled&&a.isRejected||a.cancel()),d(b),b},function(a){return e(a),a});return h.push(b),this})):new I},I.prototype=Object.create(c.Promise.prototype),I.prototype.constructor=I,r.prototype.__title="",r.prototype.__interface_list=[],r.prototype.__path="",r.prototype.__html="",r.prototype.__required_css_list=[],r.prototype.__required_js_list=[],r.__ready_list=[t,u],r.ready=function(a){return this.__ready_list.push(a),this},r.setState=function(a){var b=JSON.stringify(a);return this.ready(function(){this.state=JSON.parse(b)})},r.onStateChange=function(a){return this.prototype.__state_change_callback=a,this},r.__service_list=[],r.declareService=function(a){return this.__service_list.push(a),this},r.onEvent=function(a,b,c,d){return this.__service_list.push(function(){return m(this.element,a,c,b.bind(this),d)}),this},r.declareJob=function(a,b){return this.prototype[a]=function(){var c=this,d=arguments;c.__job_triggered?v(c,a,b,d):c.__job_list.push([a,b,d])},this},r.declareMethod=function(a,b){return this.prototype[a]=function(){var a=this,d=arguments;return(new c.Queue).push(function(){return b.apply(a,d)})},this},r.declareMethod("getInterfaceList",function(){return this.__interface_list}).declareMethod("getRequiredCSSList",function(){return this.__required_css_list}).declareMethod("getRequiredJSList",function(){return this.__required_js_list}).declareMethod("getPath",function(){return this.__path}).declareMethod("getTitle",function(){return this.__title}).declareMethod("getElement",function(){if(void 0===this.element)throw new Error("No element defined");return this.element}).declareMethod("changeState",function(a){var b,c=!1,d={};for(b in a)a[b]!==this.state[b]&&(this.state[b]=a[b],d[b]=a[b],c=!0);return c&&void 0!==this.__state_change_callback?this.__state_change_callback(d):void 0}),r.declareAcquiredMethod=function(a,b){return this.prototype[a]=function(){var a=Array.prototype.slice.call(arguments,0),d=this;return(new c.Queue).push(function(){return d.__aq_parent(b,a)})},this},r.declareAcquiredMethod("aq_reportServiceError","reportServiceError"),r.allowPublicAcquisition=function(a,b){return this.prototype.__acquired_method_dict[a]=b,this},z.__ready_list=r.__ready_list.slice(),z.__service_list=r.__service_list.slice(),z.ready=r.ready,z.setState=r.setState,z.onStateChange=r.onStateChange,z.declareService=r.declareService,z.onEvent=r.onEvent,z.prototype=new r,z.prototype.constructor=z,B.__ready_list=r.__ready_list.slice(),B.ready=r.ready,B.setState=r.setState,B.onStateChange=r.onStateChange,B.__service_list=r.__service_list.slice(),B.declareService=r.declareService,B.onEvent=r.onEvent,B.prototype=new r,B.prototype.constructor=B,r.declareMethod("declareGadget",function(b,d){var e,f,g=this,h=G;return void 0===d&&(d={}),void 0===d.sandbox&&(d.sandbox="public"),b=H.getAbsoluteURL(b,this.__path),G=(new c.Queue).push(function(){return h}).push(void 0,function(){}).push(function(){var a;if("public"===d.sandbox)a=A;else if("iframe"===d.sandbox)a=C;else{if("dataurl"!==d.sandbox)throw new Error("Unsupported sandbox options '"+d.sandbox+"'");a=D}return a(b,d,g)}).push(function(a){return F=void 0,a}).push(void 0,function(a){throw F=void 0,a}),f=G,e=(new c.Queue).push(function(){return f}).push(function(c){function f(){return c}function h(a){return function(b){return a.call(b,b)}}var i,j;for(i=0;i