Terminus = { isIE: /\bMSIE\b/.test(navigator.userAgent), connect: function(host, port) { if (this._bayeux) return; this._host = host; this._pageId = Faye.random(); this._id = window.name = window.name || document.name || Faye.random(); this._id = this._id.split('|')[0]; var iframes = document.getElementsByTagName('iframe'), i = iframes.length; while (i--) iframes[i].contentDocument.name = iframes[i].id; this.Registry.initialize(); this.Worker.initialize(); this.AjaxMonitor.initialize(); Faye.Event.on(window, 'beforeunload', function() { Terminus.disabled = true }); var endpoint = 'http://' + host + ':' + port + '/messaging', bayeux = this._bayeux = new Faye.Client(endpoint), self = this; bayeux.addExtension({ outgoing: function(message, callback) { message.href = window.location.href; if (message.connectionType === 'websocket') self._socketCapable = true; callback(message); } }); this.getId(function(id) { var url = window.name.split('|')[1]; if (!url) bayeux.subscribe('/terminus/sockets/' + id, function(message) { window.name += '|' + message.url; this.openSocket(message.url); }, this); var sub = bayeux.subscribe('/terminus/clients/' + id, this.handleMessage, this); sub.callback(function() { this.ping(); if (url) this.openSocket(url); }, this); }, this); }, browserDetails: function(callback, context) { this.getId(function(id) { callback.call(context, { host: this._host, id: id, infinite: !!window.TERMINUS_INFINITE_REDIRECT, page: this._pageId, sockets: this._socketCapable, ua: navigator.userAgent, url: window.location.href }); }, this); }, getId: function(callback, context) { var id = this._id; if (this.isIE) return callback.call(context, id); if (opener && opener.Terminus) { opener.Terminus.getId(function(prefix) { callback.call(context, prefix + '/' + id); }); } else if (parent && parent !== window) { var getParentId = function() { if (!parent.Terminus) return setTimeout(getParentId, 100); parent.Terminus.getId(function(prefix) { callback.call(context, prefix + '/' + id); }); }; getParentId(); } else { callback.call(context, id); } }, openSocket: function(endpoint) { if (this.disabled || this._socket) return; var self = this, WS = window.MozWebSocket || window.WebSocket, ws = new WS(endpoint); ws.onopen = function() { self._socket = ws; up = true; }; ws.onclose = function() { var up = !!self._socket; self._socket = null; if (up) self.openSocket(endpoint); else window.name = window.name.split('|')[0]; }; ws.onmessage = function(event) { self.handleMessage(JSON.parse(event.data)); }; }, ping: function() { if (this.disabled) return; this.browserDetails(function(details) { this._bayeux.publish('/terminus/ping', details); var self = this; setTimeout(function() { self.ping() }, 3000); }, this); }, handleMessage: function(message) { var command = message.command, method = command.shift(), driver = this.Driver, worker = this.Worker, posted = false, self = this; command.push(function(result) { if (posted) return; self.postResult(message.commandId, result); posted = true; }); worker.monitor = true; driver[method].apply(driver, command); worker.monitor = false; }, postResult: function(commandId, result) { if (this.disabled || !commandId) return; if (this._socket) return this._socket.send(JSON.stringify({value: result})); this.getId(function(id) { this._bayeux.publish('/terminus/results', { id: id, commandId: commandId, result: result }); }, this); }, getAttribute: function(node, name) { return Terminus.isIE ? (node.getAttributeNode(name) || {}).nodeValue || false : node.getAttribute(name); }, Driver: { _node: function(id) { return Terminus.Registry.get(id); }, attribute: function(nodeId, name, callback) { var node = this._node(nodeId); if (!node) return callback(null); if (!Terminus.isIE && (name === 'checked' || name === 'selected')) { callback(!!node[name]); } else { callback(Terminus.getAttribute(node, name)); } }, set_attribute: function(nodeId, name, value, callback) { var node = this._node(nodeId); if (!node) return callback(null); node.setAttribute(name, value); callback(true); }, body: function(callback) { var html = document.getElementsByTagName('html')[0]; callback(html.outerHTML || '\n' + html.innerHTML + '\n\n'); }, clear_cookies: function(callback) { var cookies = document.cookie.split(';'), name; var expiry = new Date(); expiry.setTime(expiry.getTime() - 24*60*60*1000); for (var i = 0, n = cookies.length; i < n; i++) { name = cookies[i].split('=')[0]; document.cookie = name + '=; expires=' + expiry.toGMTString() + '; path=/'; } callback(true); }, click: function(nodeId, options, callback) { var element = this._node(nodeId), timeout = options.resynchronization_timeout; if (!element) return callback(true); Syn.trigger('click', {}, element); if (options.resynchronize === false) return callback(true); if (timeout) Terminus.Worker._setTimeout.call(window, function() { callback('failed to resynchronize, ajax request timed out'); }, 1000 * timeout); Terminus.Worker.callback(function() { callback(true); }); }, current_url: function(callback) { Terminus.browserDetails(function(details) { callback(details.url); }); }, drag: function(options, callback) { var draggable = this._node(options.from), droppable = this._node(options.to); if (!draggable || !droppable) return callback(null); Syn.drag({to: droppable}, draggable, function() { callback(true); }); }, evaluate: function(expression, callback) { callback(eval(expression)); }, execute: function(expression, callback) { eval(expression); callback(true); }, find: function(xpath, nodeId, callback) { var root = nodeId ? this._node(nodeId) : document; if (!root) return callback([]); var result = document.evaluate(xpath, root, null, XPathResult.ANY_TYPE, null), list = [], element; while (element = result.iterateNext()) list.push(Terminus.Registry.put(element)); return callback(list); }, is_visible: function(nodeId, callback) { var node = this._node(nodeId); if (!node) return callback(null); while (node.tagName && node.tagName.toLowerCase() !== 'body') { if (node.style.display === 'none' || node.type === 'hidden') return callback(false); node = node.parentNode; } callback(true); }, select: function(nodeId, callback) { var option = this._node(nodeId); if (!option) return callback(null); option.selected = true; Syn.trigger('change', {}, option.parentNode); callback(true); }, set: function(nodeId, value, callback) { var field = this._node(nodeId), max = Terminus.getAttribute(field, 'maxlength'); if (!field) return callback(null); if (field.type === 'file') return callback('not_allowed'); Syn.trigger('focus', {}, field); Syn.trigger('click', {}, field); switch (typeof value) { case 'string': if (max) value = value.substr(0, parseInt(max)); field.value = value; break; case 'boolean': field.checked = value; break; } Syn.trigger('change', {}, field); callback(true); }, tag_name: function(nodeId, callback) { var node = this._node(nodeId); if (!node) return callback(null); callback(node.tagName.toLowerCase()); }, text: function(nodeId, callback) { var node = this._node(nodeId); if (!node) return callback(null); var text = node.textContent || node.innerText || '', scripts = node.getElementsByTagName('script'), i = scripts.length; while (i--) text = text.replace(scripts[i].textContent || scripts[i].innerText, ''); text = text.replace(/^\s*|\s*$/g, ''); callback(text); }, trigger: function(nodeId, eventType, callback) { var node = this._node(nodeId); if (!node) return callback(null); Syn.trigger(eventType, {}, node); callback(true); }, unselect: function(nodeId, callback) { var option = this._node(nodeId); if (!option) return callback(null); if (!option.parentNode.multiple) return callback(false); option.selected = false; Syn.trigger('change', {}, option.parentNode); callback(true); }, value: function(nodeId, callback) { var node = this._node(nodeId); if (!node) return callback(null); if (node.tagName.toLowerCase() !== 'select' || !node.multiple) return callback(node.value); var options = node.children, values = []; for (var i = 0, n = options.length; i < n; i++) { if (options[i].selected) values.push(options[i].value); } callback(values); }, visit: function(url, callback) { window.location.href = url; callback(url); } }, Registry: { initialize: function() { this._namespace = new Faye.Namespace(); this._elements = {}; }, get: function(id) { var node = this._elements[id], root = node; while (root && root.tagName !== 'BODY' && root.tagName !== 'HTML') root = root.parentNode; if (!root) return null; return node; }, put: function(element) { var id = this._namespace.generate(); this._elements[id] = element; return id; } }, Worker: { initialize: function() { this._callbacks = []; this._pending = 0; if (!Terminus.isIE) this._wrapTimeouts(); }, callback: function(callback, scope) { if (this._pending === 0) { if (this._setTimeout) this._setTimeout.call(window, function() { callback.call(scope) }, 0); else setTimeout(function() { callback.call(scope) }, 0); } else { this._callbacks.push([callback, scope]); } }, suspend: function() { this._pending += 1; }, resume: function() { if (this._pending === 0) return; this._pending -= 1; if (this._pending !== 0) return; var callback; for (var i = 0, n = this._callbacks.length; i < n; i++) { callback = this._callbacks[i]; callback[0].call(callback[1]); } this._callbacks = []; }, _wrapTimeouts: function() { var timeout = window.setTimeout, clear = window.clearTimeout, timeouts = {}, self = this; var finish = function(id) { if (!timeouts.hasOwnProperty(id)) return; delete timeouts[id]; self.resume(); }; window.setTimeout = function(callback, delay) { var id = timeout.call(window, function() { try { switch (typeof callback) { case 'function': callback(); break; case 'string': eval(callback); break; } } finally { finish(id); } }, delay); if (self.monitor) { timeouts[id] = true; self.suspend(); } return id; }; window.clearTimeout = function(id) { finish(id); return clear(id); }; this._setTimeout = timeout; } }, AjaxMonitor: { initialize: function() { if (window.jQuery) this._patchJquery(); }, _patchJquery: function() { var ajax = jQuery.ajax; jQuery.ajax = function(url, settings) { var options = ((typeof url === 'string') ? settings : url) || {}, complete = options.complete, monitor = Terminus.Worker.monitor; options.complete = function() { var result; try { result = complete.apply(this, arguments); } finally { if (monitor) Terminus.Worker.resume(); } return result; }; if (monitor) Terminus.Worker.suspend(); if (typeof url === 'string') return ajax.call(jQuery, url, options); else return ajax.call(jQuery, options); }; } } };