All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.javascripts.portal.js Maven / Gradle / Ivy

The newest version!
/*
 * Portal v1.0rc3
 * http://github.com/flowersinthesand/portal
 * 
 * Copyright 2011-2013, Donghwan Kim 
 * Licensed under the Apache License, Version 2.0
 * http://www.apache.org/licenses/LICENSE-2.0
 */
(function() {
	
	"use strict";
	
	var // A global identifier
		guid,
		// Is the unload event being processed?
		unloading,
		// Portal
		portal = {},
		// Socket instances
		sockets = {},
		// Callback names for JSONP
		jsonpCallbacks = [],
		// Core prototypes
		toString = Object.prototype.toString,
		hasOwn = Object.prototype.hasOwnProperty,
		slice = Array.prototype.slice;
	
	// Convenience utilities
	// Most utility functions are borrowed from jQuery
	portal.support = {
		now: function() {
			return new Date().getTime();
		},
		isArray: function(array) {
			return toString.call(array) === "[object Array]";
		},
		isBinary: function(data) {
			var string = toString.call(data);
			return string === "[object Blob]" || string === "[object ArrayBuffer]";
		}, 
		isFunction: function(fn) {
			return toString.call(fn) === "[object Function]";
		},
		getAbsoluteURL: function(url) {
			var div = document.createElement("div");
			
			// Uses an innerHTML property to obtain an absolute URL
			div.innerHTML = '';
			
			// encodeURI and decodeURI are needed to normalize URL between IE and non-IE, 
			// since IE doesn't encode the href property value and return it - http://jsfiddle.net/Yq9M8/1/
			return encodeURI(decodeURI(div.firstChild.href));
		},
		iterate: function(fn) {
			var timeoutId;
			
			// Though the interval is 1ms for real-time application, there is a delay between setTimeout calls
			// For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
			(function loop() {
				timeoutId = setTimeout(function() {
					if (fn() === false) {
						return;
					}
					
					loop();
				}, 1);
			})();
			
			return function() {
				clearTimeout(timeoutId);
			};
		},
		each: function(array, callback) {
			var i;
			
			for (i = 0; i < array.length; i++) {
				callback(i, array[i]);
			}
		},
		extend: function(target) {
			var i, options, name;
			
			for (i = 1; i < arguments.length; i++) {
				if ((options = arguments[i]) != null) {
					for (name in options) {
						target[name] = options[name];
					}
				}
			}
			
			return target;
		},
		on: function(elem, type, fn) {
			if (elem.addEventListener) {
				elem.addEventListener(type, fn, false);
			} else if (elem.attachEvent) {
				elem.attachEvent("on" + type, fn);
			}
		},
		off: function(elem, type, fn) {
			if (elem.removeEventListener) {
				elem.removeEventListener(type, fn, false);
			} else if (elem.detachEvent) {
				elem.detachEvent("on" + type, fn);
			}
		},
		param: function(params) {
			var prefix,
				s = [];
			
			function add(key, value) {
				value = portal.support.isFunction(value) ? value() : (value == null ? "" : value);
				s.push(encodeURIComponent(key) + "=" + encodeURIComponent(value));
			}
			
			function buildParams(prefix, obj) {
				var name;
				
				if (portal.support.isArray(obj)) {
					portal.support.each(obj, function(i, v) {
						if (/\[\]$/.test(prefix)) {
							add(prefix, v);
						} else {
							buildParams(prefix + "[" + (typeof v === "object" ? i : "") + "]", v);
						}
					});
				} else if (toString.call(obj) === "[object Object]") {
					for (name in obj) {
						buildParams(prefix + "[" + name + "]", obj[name]);
					}
				} else {
					add(prefix, obj);
				}
			}
			
			for (prefix in params) {
				buildParams(prefix, params[prefix]);
			}
			
			return s.join("&").replace(/%20/g, "+");
		},
		xhr: function() {
			try {
				return new window.XMLHttpRequest();
			} catch(e1) {
				try {
					return new window.ActiveXObject("Microsoft.XMLHTTP");
				} catch(e2) {}
			}
		},
		parseJSON: function(data) {
			return !data ? 
				null : 
				window.JSON && window.JSON.parse ? 
					window.JSON.parse(data) : 
					new Function("return " + data)();
		},
		// http://github.com/flowersinthesand/stringifyJSON
		stringifyJSON: function(value) {
			var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 
				meta = {
					'\b' : '\\b',
					'\t' : '\\t',
					'\n' : '\\n',
					'\f' : '\\f',
					'\r' : '\\r',
					'"' : '\\"',
					'\\' : '\\\\'
				};
			
			function quote(string) {
				return '"' + string.replace(escapable, function(a) {
					var c = meta[a];
					return typeof c === "string" ? c : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
				}) + '"';
			}
			
			function f(n) {
				return n < 10 ? "0" + n : n;
			}
			
			return window.JSON && window.JSON.stringify ? 
				window.JSON.stringify(value) : 
				(function str(key, holder) {
					var i, v, len, partial, value = holder[key], type = typeof value;
							
					if (value && typeof value === "object" && typeof value.toJSON === "function") {
						value = value.toJSON(key);
						type = typeof value;
					}
					
					switch (type) {
					case "string":
						return quote(value);
					case "number":
						return isFinite(value) ? String(value) : "null";
					case "boolean":
						return String(value);
					case "object":
						if (!value) {
							return "null";
						}
						
						switch (toString.call(value)) {
						case "[object Date]":
							return isFinite(value.valueOf()) ? 
								'"' + value.getUTCFullYear() + "-" + f(value.getUTCMonth() + 1) + "-" + f(value.getUTCDate()) + 
								"T" + f(value.getUTCHours()) + ":" + f(value.getUTCMinutes()) + ":" + f(value.getUTCSeconds()) + "Z" + '"' : 
								"null";
						case "[object Array]":
							len = value.length;
							partial = [];
							for (i = 0; i < len; i++) {
								partial.push(str(i, value) || "null");
							}
							
							return "[" + partial.join(",") + "]";
						default:
							partial = [];
							for (i in value) {
								if (hasOwn.call(value, i)) {
									v = str(i, value);
									if (v) {
										partial.push(quote(i) + ":" + v);
									}
								}
							}
							
							return "{" + partial.join(",") + "}";
						}
					}
				})("", {"": value});
		},
		browser: {},
		storage: !!(window.localStorage && window.StorageEvent)
	};
	portal.support.corsable = "withCredentials" in portal.support.xhr();
	guid = portal.support.now();
	
	// Browser sniffing
	(function() {
		var ua = navigator.userAgent.toLowerCase(), 
			match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
				/(webkit)[ \/]([\w.]+)/.exec(ua) ||
				/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
				/(msie) ([\w.]+)/.exec(ua) ||
				ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
				[];
		
		portal.support.browser[match[1] || ""] = true;
		portal.support.browser.version = match[2] || "0";
		
		// The storage event of Internet Explorer and Firefox 3 works strangely
		if (portal.support.browser.msie || (portal.support.browser.mozilla && portal.support.browser.version.split(".")[0] === "1")) {
			portal.support.storage = false;
		}
	})();
	
	// Finds the socket object which is mapped to the given url
	portal.find = function(url) {
		var i;
		
		// Returns the first socket in the document
		if (!arguments.length) {
			for (i in sockets) {
				if (sockets[i]) {
					return sockets[i];
				}
			}
			return null;
		}
		
		// The url is a identifier of this socket within the document
		return sockets[portal.support.getAbsoluteURL(url)] || null;
	};
	// Creates a new socket and connects to the given url 
	portal.open = function(url, options) {
		// Makes url absolute to normalize URL
		url = portal.support.getAbsoluteURL(url);
		sockets[url] = socket(url, options);
		
		return portal.find(url);
	};
	// Default options
	portal.defaults = {
		// Socket options
		transports: ["ws", "sse", "stream", "longpoll"],
		timeout: false,
		heartbeat: false,
		_heartbeat: 5000,
		lastEventId: 0,
		sharing: false,
		prepare: function(connect) {
			connect();
		},
		reconnect: function(lastDelay) {
			return 2 * (lastDelay || 250);
		},
		idGenerator: function() {
			// Generates a random UUID 
			// Logic borrowed from http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
			return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) {
				var r = Math.random() * 16 | 0,
					v = c === "x" ? r : (r & 0x3 | 0x8);
				
				return v.toString(16);
			});
		},
		urlBuilder: function(url, params) {
			return url + (/\?/.test(url) ? "&" : "?") + portal.support.param(params);
		},
		inbound: portal.support.parseJSON,
		outbound: portal.support.stringifyJSON,
		
		// Transport options
		credentials: false,
		longpollTest: true,
		xdrURL: function(url) {
			// Maintaining session by rewriting URL
			// http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url
			var match = /(?:^|; )(JSESSIONID|PHPSESSID)=([^;]*)/.exec(document.cookie);
			
			switch (match && match[1]) {
			case "JSESSIONID":
				return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + match[2] + "$1");
			case "PHPSESSID":
				return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + match[2] + "&").replace(/&$/, "");
			default:
				return false;
			}
		},
		streamParser: function(chunk) {
			// Chunks are formatted according to the event stream format 
			// http://www.w3.org/TR/eventsource/#event-stream-interpretation
			var reol = /\r\n|[\r\n]/g, lines = [], data = this.data("data"), array = [], i = 0, 
				match, line;
			
			// Strips off the left padding of the chunk
			// the first chunk of some streaming transports and every chunk for Android browser 2 and 3 has padding
			chunk = chunk.replace(/^\s+/g, "");
			
			// String.prototype.split is not reliable cross-browser
			while (match = reol.exec(chunk)) {
				lines.push(chunk.substring(i, match.index));
				i = match.index + match[0].length;
			}
			lines.push(chunk.length === i ? "" : chunk.substring(i));
			
			if (!data) {
				data = [];
				this.data("data", data);
			}
			
			// Processes the data field only
			for (i = 0; i < lines.length; i++) {
				line = lines[i];
				if (!line) {
					// Finish
					array.push(data.join("\n"));
					data = [];
					this.data("data", data);
				} else if (/^data:\s/.test(line)) {
					// A single data field
					data.push(line.substring("data: ".length));
				} else {
					// A fragment of a data field
					data[data.length - 1] += line;
				}
			}
			
			return array;
		}
		// Undocumented
		// method: null
		// initIframe: null
	};
	
	// Callback function
	function callbacks(deferred) {
		var list = [],
			locked,
			memory,
			firing,
			firingStart,
			firingLength,
			firingIndex,
			fire = function(context, args) {
				args = args || [];
				memory = !deferred || [context, args];
				firing = true;
				firingIndex = firingStart || 0;
				firingStart = 0;
				firingLength = list.length;
				for (; firingIndex < firingLength; firingIndex++) {
					list[firingIndex].apply(context, args);
				}
				firing = false;
			},
			self = {
				add: function(fn) {
					var length = list.length;
					
					list.push(fn);
					if (firing) {
						firingLength = list.length;
					} else if (!locked && memory && memory !== true) {
						firingStart = length;
						fire(memory[0], memory[1]);
					}
				},
				remove: function(fn) {
					var i;
					
					for (i = 0; i < list.length; i++) {
						if (fn === list[i] || (fn.guid && fn.guid === list[i].guid)) {
							if (firing) {
								if (i <= firingLength) {
									firingLength--;
									if (i <= firingIndex) {
										firingIndex--;
									}
								}
							}
							list.splice(i--, 1);
						}
					}
				},
				fire: function(context, args) {
					if (!locked && !firing && !(deferred && memory)) {
						fire(context, args);
					}
				},
				lock: function() {
					locked = true;
				},
				locked: function() {
					return !!locked;
				},
				unlock: function() {
					locked = memory = firing = firingStart = firingLength = firingIndex = undefined;
				}
			};
		
		return self;
	}
	
	// Socket function
	function socket(url, options) {
		var	// Final options
			opts,
			// Transport
			transport,
			// The state of the connection
			state,
			// Event helpers
			events = {},
			eventId = 0, 
			// Reply callbacks
			replyCallbacks = {},
			// Buffer
			buffer = [],
			// Reconnection
			reconnectTimer,
			reconnectDelay,
			reconnectTry,
			// Map of the connection-scoped values
			connection = {},
			parts = /^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/.exec(url.toLowerCase()),
			// Socket object
			self = {
				// Finds the value of an option
				option: function(key, /* undocumented */ value) {
					if (value === undefined) {
						return opts[key];
					}
					
					opts[key] = value;
					
					return this;
				},
				// Gets or sets a connection-scoped value
				data: function(key, value) {
					if (value === undefined) {
						return connection[key];
					}
					
					connection[key] = value;
					
					return this;
				},
				// Returns the state
				state: function() {
					return state;
				},
				// Adds event handler
				on: function(type, fn) {
					var event;
					
					// Handles a map of type and handler
					if (typeof type === "object") {
						for (event in type) {
							self.on(event, type[event]);
						}
						return this;
					}
					
					// For custom event
					event = events[type];
					if (!event) {
						if (events.message.locked()) {
							return this;
						}
						
						event = events[type] = callbacks();
						event.order = events.message.order;
					}
					
					event.add(fn);
					
					return this;
				},
				// Removes event handler
				off: function(type, fn) {
					var event = events[type];
					
					if (event) {
						event.remove(fn);
					}
					
					return this;
				},
				// Adds one time event handler
				one: function(type, fn) {
					function proxy() {
						self.off(type, proxy);
						fn.apply(self, arguments);
					}
					
					fn.guid = fn.guid || guid++;
					proxy.guid = fn.guid;
					
					return self.on(type, proxy);
				},
				// Fires event handlers
				fire: function(type) {
					var event = events[type];
					
					if (event) {
						event.fire(self, slice.call(arguments, 1));
					}
					
					return this;
				},
				// Establishes a connection
				open: function() {
					var type,
						latch,
						connect = function() {
							var candidates, type;
							
							if (!latch) {
								latch = true;
								candidates = connection.candidates = slice.call(opts.transports);
								while (!transport && candidates.length) {
									type = candidates.shift();
									connection.transport = type;
									connection.url = self.buildURL();
									transport = portal.transports[type](self, opts);
								}
								
								// Increases the number of reconnection attempts
								if (reconnectTry) {
									reconnectTry++;
								}
								
								// Fires the connecting event and connects
								if (transport) {
									self.fire("connecting");
									transport.open();
								} else {
									self.fire("close", "notransport");
								}
							}
						},
						cancel = function() {
							if (!latch) {
								latch = true;
								self.fire("close", "canceled");
							}
						};
					
					// Cancels the scheduled connection
					if (reconnectTimer) {
						clearTimeout(reconnectTimer);
					}
					
					// Resets the connection scope and event helpers
					connection = {};
					for (type in events) {
						events[type].unlock();
					}
					
					// Chooses transport
					transport = undefined;
					
					// From null or waiting state
					state = "preparing";
					
					// Check if possible to make use of a shared socket
					if (opts.sharing) {
						connection.transport = "session";
						transport = portal.transports.session(self, opts);
					}
					
					// Executes the prepare handler if a physical connection is needed
					if (transport) {
						connect();
					} else {
						opts.prepare.call(self, connect, cancel, opts);
					}
					
					return this;
				},
				// Sends an event to the server via the connection
				send: function(type, data, doneCallback, failCallback) {
					var event;
					
					// Defers sending an event until the state become opened
					if (state !== "opened") {
						buffer.push(arguments);
						return this;
					}
					
					// Outbound event
					event = {
						id: ++eventId,
						socket: opts.id,
						type: type,
						data: data,
						reply: !!(doneCallback || failCallback)
					};
					
					if (event.reply) {
						// Shared socket needs to know the callback event name 
						// because it fires the callback event directly instead of using reply event 
						if (connection.transport === "session") {
							event.doneCallback = doneCallback;
							event.failCallback = failCallback;
						} else {
							replyCallbacks[eventId] = {done: doneCallback, fail: failCallback};
						}
					}
					
					// Delegates to the transport
					transport.send(portal.support.isBinary(data) ? data : opts.outbound.call(self, event));
					
					return this;
				},
				// Disconnects the connection
				close: function() {
					// Prevents reconnection
					opts.reconnect = false;
					if (reconnectTimer) {
						clearTimeout(reconnectTimer);
					}
					
					// Fires the close event immediately for transport which doesn't give feedback on disconnection
					if (unloading || !transport || !transport.feedback) {
						self.fire("close", unloading ? "error" : "aborted");
					}
					
					// Delegates to the transport
					if (transport) {
						transport.close();
					}
					
					return this;
				},
				// Broadcasts event to session sockets
				broadcast: function(type, data) {
					// TODO rename
					var broadcastable = connection.broadcastable;
					if (broadcastable) {
						broadcastable.broadcast({type: "fire", data: {type: type, data: data}});
					}
					
					return this;
				},
				// For internal use only
				// fires events from the server
				_fire: function(data, isChunk) {
					var array;
					
					if (isChunk) {
						data = opts.streamParser.call(self, data);
						while (data.length) {
							self._fire(data.shift());
						}
						return this;
					}
					
					if (portal.support.isBinary(data)) {
						array = [{type: "message", data: data}];
					} else {
						array = opts.inbound.call(self, data);
						array = array == null ? [] : !portal.support.isArray(array) ? [array] : array;
					}
					
					portal.support.each(array, function(i, event) {
						var latch, args = [event.type, event.data];
						
						opts.lastEventId = event.id;
						if (event.reply) {
							args.push(function(result) {
								if (!latch) {
									latch = true;
									self.send("reply", {id: event.id, data: result});
								}
							});
						}
						
						self.fire.apply(self, args).fire("_message", args);
					});
					
					return this;
				},
				// For internal use only
				// builds an effective URL
				buildURL: function(params) {
					return opts.urlBuilder.call(self, url, portal.support.extend({
						id: opts.id, 
						transport: connection.transport, 
						heartbeat: opts.heartbeat, 
						lastEventId: opts.lastEventId,
						_: guid++
					}, opts.params, params));
				}
			};
		
		// Create the final options
		opts = portal.support.extend({}, portal.defaults, options);
		if (options) {
			// Array should not be deep extended
			if (options.transports) {
				opts.transports = slice.call(options.transports);
			}
		}
		// Saves original URL
		opts.url = url;
		// Generates socket id,
		opts.id = opts.idGenerator.call(self);
		opts.crossDomain = !!(parts && 
			// protocol and hostname
			(parts[1] != location.protocol || parts[2] != location.hostname ||
			// port
			(parts[3] || (parts[1] === "http:" ? 80 : 443)) != (location.port || (location.protocol === "http:" ? 80 : 443))));
		
		portal.support.each(["connecting", "open", "message", "close", "waiting"], function(i, type) {
			// Creates event helper
			events[type] = callbacks(type !== "message");
			events[type].order = i;
			
			// Shortcuts for on method
			var old = self[type],
				on = function(fn) {
					return self.on(type, fn);
				};
			
			self[type] = !old ? on : function(fn) {
				return (portal.support.isFunction(fn) ? on : old).apply(this, arguments);
			};
		});
		
		// Initializes
		self.on({
			connecting: function() {
				// From preparing state
				state = "connecting";
				
				var timeoutTimer;
				
				// Sets timeout timer
				function setTimeoutTimer() {
					timeoutTimer = setTimeout(function() {
						transport.close();
						self.fire("close", "timeout");
					}, opts.timeout);
				}
				
				// Clears timeout timer
				function clearTimeoutTimer() {
					clearTimeout(timeoutTimer);
				}
				
				// Makes the socket sharable
				function share() {
					var traceTimer,
						server, 
						name = "socket-" + url,
						servers = {
							// Powered by the storage event and the localStorage
							// http://www.w3.org/TR/webstorage/#event-storage
							storage: function() {
								if (!portal.support.storage) {
									return;
								}
								
								var storage = window.localStorage;
								
								return {
									init: function() {
										function onstorage(event) {
											// When a deletion, newValue initialized to null
											if (event.key === name && event.newValue) {
												listener(event.newValue);
											}
										}
										
										// Handles the storage event 
										portal.support.on(window, "storage", onstorage);
										self.one("close", function() {
											portal.support.off(window, "storage", onstorage);
											// Defers again to clean the storage
											self.one("close", function() {
												storage.removeItem(name);
												storage.removeItem(name + "-opened");
												storage.removeItem(name + "-children");
											});
										});
									},
									broadcast: function(obj) {
										var string = portal.support.stringifyJSON(obj);
										storage.setItem(name, string);
										setTimeout(function() {
											listener(string);
										}, 50);
									},
									get: function(key) {
										return portal.support.parseJSON(storage.getItem(name + "-" + key));
									},
									set: function(key, value) {
										storage.setItem(name + "-" + key, portal.support.stringifyJSON(value));
									}
								};
							},
							// Powered by the window.open method
							// https://developer.mozilla.org/en/DOM/window.open
							windowref: function() {
								// Internet Explorer raises an invalid argument error
								// when calling the window.open method with the name containing non-word characters
								var neim = name.replace(/\W/g, ""),
									container = document.getElementById(neim),
									win;
								
								if (!container) {
									container = document.createElement("div");
									container.id = neim;
									container.style.display = "none";
									container.innerHTML = '';
				
				textarea = form.firstChild;
				textarea.value = data;
				
				iframe = form.lastChild;
				portal.support.on(iframe, "load", function() {
					document.body.removeChild(form);
					post();
				});
				
				document.body.appendChild(form);
				form.submit();
			};
			
			return {
				send: function(data) {
					queue.push(data);
					
					if (!sending) {
						sending = true;
						post();
					}
				}
			};
		},
		// Server-Sent Events
		sse: function(socket, options) {
			var es, 
				EventSource = window.EventSource;
			
			if (!EventSource) {
				return;
			} else if (options.crossDomain) {
				try {
					if (!portal.support.corsable || !("withCredentials" in new EventSource("about:blank"))) {
						return;
					}
				} catch(e) {
					return;
				}
			}
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					var url = socket.data("url");
					
					// Uses proper constructor for Chrome 10-15
					es = !options.crossDomain ? new EventSource(url) : new EventSource(url, {withCredentials: options.credentials});
					es.onopen = function(event) {
						socket.data("event", event).fire("open");
					};
					es.onmessage = function(event) {
						socket.data("event", event)._fire(event.data);
					};
					es.onerror = function(event) {
						es.close();
						
						// There is no way to find whether this connection closed normally or not 
						socket.data("event", event).fire("close", "done");
					};
				},
				close: function() {
					es.close();
				}
			});
		},
		// Streaming facade
		stream: function(socket) {
			socket.data("candidates").unshift("streamxhr", "streamxdr", "streamiframe");
		},
		// Streaming - XMLHttpRequest
		streamxhr: function(socket, options) {
			var xhr;
			
			if ((portal.support.browser.msie && +portal.support.browser.version < 10) || (options.crossDomain && !portal.support.corsable)) {
				return;
			}
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					var stop;
					
					xhr = portal.support.xhr();
					xhr.onreadystatechange = function() {
						function onprogress() {
							var index = socket.data("index"),
								length = xhr.responseText.length;
							
							if (!index) {
								socket.fire("open")._fire(xhr.responseText, true);
							} else if (length > index) {
								socket._fire(xhr.responseText.substring(index, length), true);
							}
							
							socket.data("index", length);
						}
						
						if (xhr.readyState === 3 && xhr.status === 200) {
							// Despite the change in response, Opera doesn't fire the readystatechange event
							if (portal.support.browser.opera && !stop) {
								stop = portal.support.iterate(onprogress);
							} else {
								onprogress();
							}
						} else if (xhr.readyState === 4) {
							if (stop) {
								stop();
							}
							
							socket.fire("close", xhr.status === 200 ? "done" : "error");
						}
					};
					
					xhr.open(options.method || "GET", socket.data("url"));
					if (portal.support.corsable) {
						xhr.withCredentials = options.credentials;
					}
					
					xhr.send(null);
				},
				close: function() {
					xhr.abort();
				}
			});
		},
		// Streaming - Iframe
		streamiframe: function(socket, options) {
			var doc, 
				stop, 
				ActiveXObject = window.ActiveXObject;
			
			if (!ActiveXObject || options.crossDomain) {
				return;
			} else {
				// IE 10 Metro doesn't support ActiveXObject
				try {
					new ActiveXObject("htmlfile");
				} catch(e) {
					return;
				}
			}
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					var iframe, cdoc;
					
					doc = new ActiveXObject("htmlfile");
					doc.open();
					doc.close();
					
					iframe = doc.createElement("iframe");
					iframe.src = socket.data("url");
					doc.body.appendChild(iframe);
					
					cdoc = iframe.contentDocument || iframe.contentWindow.document;
					stop = portal.support.iterate(function() {
						// Response container
						var container;
						
						function readDirty() {
							var clone = container.cloneNode(true), 
								text;
							
							// Adds a character not CR and LF to circumvent an Internet Explorer bug
							// If the contents of an element ends with one or more CR or LF, Internet Explorer ignores them in the innerText property 
							clone.appendChild(cdoc.createTextNode("."));
							text = clone.innerText;
							
							return text.substring(0, text.length - 1);
						}
						
						// Waits the server's container ignorantly
						if (!cdoc.firstChild) {
							return;
						}
						
						if (options.initIframe) {
							options.initIframe.call(socket, iframe);
						}
						
						container = cdoc.body.lastChild;
						
						// Detects connection failure
						if (!container) {
							socket.fire("close", "error");
							return false;
						}
						
						socket.fire("open")._fire(readDirty(), true);
						container.innerText = "";
						
						stop = portal.support.iterate(function() {
							var text = readDirty();
							
							if (text) {
								container.innerText = "";
								socket._fire(text, true);
							}
							
							if (cdoc.readyState === "complete") {
								socket.fire("close", "done");
								return false;
							}
						});
						
						return false;
					});
				},
				close: function() {
					stop();
					doc.execCommand("Stop");
				}
			});
		},
		// Streaming - XDomainRequest
		streamxdr: function(socket, options) {
			var xdr, 
				XDomainRequest = window.XDomainRequest;
			
			if (!XDomainRequest || !options.xdrURL || !options.xdrURL.call(socket, "t")) {
				return;
			}
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					var url = options.xdrURL.call(socket, socket.data("url"));
					
					socket.data("url", url);
					
					xdr = new XDomainRequest();
					xdr.onprogress = function() {
						var index = socket.data("index"), 
							length = xdr.responseText.length;
						
						if (!index) {
							socket.fire("open")._fire(xdr.responseText, true);
						} else {
							socket._fire(xdr.responseText.substring(index, length), true);
						}
						
						socket.data("index", length);
					};
					xdr.onerror = function() {
						socket.fire("close", "error");
					};
					xdr.onload = function() {
						socket.fire("close", "done");
					};
					
					xdr.open(options.method || "GET", url);
					xdr.send();
				},
				close: function() {
					xdr.abort();
				}
			});
		},
		// Long polling facade
		longpoll: function(socket) {
			socket.data("candidates").unshift("longpollajax", "longpollxdr", "longpolljsonp");
		},
		// Long polling - AJAX
		longpollajax: function(socket, options) {
			var xhr, 
				aborted,
				count = 0;
			
			if (options.crossDomain && !portal.support.corsable) {
				return;
			}
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					function poll() {
						var url = socket.buildURL({count: ++count});
						
						socket.data("url", url);
						
						xhr = portal.support.xhr();
						xhr.onreadystatechange = function() {
							var data;
							
							// Avoids c00c023f error on Internet Explorer 9
							if (!aborted && xhr.readyState === 4) {
								if (xhr.status === 200) {
									data = xhr.responseText;
									if (data || count === 1) {
										if (count === 1) {
											socket.fire("open");
										}
										if (data) {
											socket._fire(data);
										}
										poll();
									} else {
										socket.fire("close", "done");
									}
								} else {
									socket.fire("close", "error");
								}
							}
						};
						
						xhr.open(options.method || "GET", url);
						if (portal.support.corsable) {
							xhr.withCredentials = options.credentials;
						}
						
						xhr.send(null);
					}
					
					if (!options.longpollTest) {
						// Skips the test that checks the server's status
						setTimeout(function() {
							socket.fire("open");
							poll();
						}, 50);
					} else {
						poll();
					}
				},
				close: function() {
					aborted = true;
					xhr.abort();
				}
			});
		},
		// Long polling - XDomainRequest
		longpollxdr: function(socket, options) {
			var xdr, 
				count = 0, 
				XDomainRequest = window.XDomainRequest;
			
			if (!XDomainRequest || !options.xdrURL || !options.xdrURL.call(socket, "t")) {
				return;
			}
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					function poll() {
						var url = options.xdrURL.call(socket, socket.buildURL({count: ++count}));
						
						socket.data("url", url);
						
						xdr = new XDomainRequest();
						xdr.onload = function() {
							var data = xdr.responseText;
							
							if (data || count === 1) {
								if (count === 1) {
									socket.fire("open");
								}
								if (data) {
									socket._fire(data);
								}
								poll();
							} else {
								socket.fire("close", "done");
							}
						};
						xdr.onerror = function() {
							socket.fire("close", "error");
						};
						
						xdr.open(options.method || "GET", url);
						xdr.send();
					}
					
					if (!options.longpollTest) {
						setTimeout(function() {
							socket.fire("open");
							poll();
						}, 50);
					} else {
						poll();
					}
				},
				close: function() {
					xdr.abort();
				}
			});
		},
		// Long polling - JSONP
		longpolljsonp: function(socket, options) {
			var script, 
				called, 
				count = 0, 
				callback = jsonpCallbacks.pop() || ("socket_" + (++guid));
			
			return portal.support.extend(portal.transports.httpbase(socket, options), {
				open: function() {
					function poll() {
						var url = socket.buildURL({callback: callback, count: ++count}), 
							head = document.head || document.getElementsByTagName("head")[0] || document.documentElement;
						
						
						socket.data("url", url);
						
						script = document.createElement("script");
						script.async = true;
						script.src = url;
						script.clean = function() {
							script.clean = script.onerror = script.onload = script.onreadystatechange = null;
							if (head && script.parentNode) {
								head.removeChild(script);
							}
						};
						script.onload = script.onreadystatechange = function() {
							if (!script.readyState || /loaded|complete/.test(script.readyState)) {
								script.clean();
								if (called) {
									called = false;
									poll();
								} else if (count === 1) {
									socket.fire("open");
									poll();
								} else {
									socket.fire("close", "done");
								}
							}
						};
						script.onerror = function() {
							script.clean();
							socket.fire("close", "error");
						}; 
						
						head.insertBefore(script, head.firstChild);
					}
					
					// Attaches callback
					window[callback] = function(data) {
						called = true;
						if (count === 1) {
							socket.fire("open");
						}
						socket._fire(data);
					};
					socket.one("close", function() {
						// Assings an empty function for browsers which are not able to cancel a request made from script tag
						window[callback] = function() {};
						jsonpCallbacks.push(callback);
					});
					
					if (!options.longpollTest) {
						setTimeout(function() {
							socket.fire("open");
							poll();
						}, 50);
					} else {
						poll();
					}
				},
				close: function() {
					if (script.clean) {
						script.clean();
					}
				}
			});
		}
	};
	
	// Closes all sockets
	portal.finalize = function() {
		var url, socket;
		
		for (url in sockets) {
			socket = sockets[url];
			if (socket.state() !== "closed") {
				socket.close();
			}
			
			// To run the test suite
			delete sockets[url];
		}
	};
	
	portal.support.on(window, "unload", function() {
		// Check the unload event is fired by the browser
		unloading = true;
		// Closes all sockets when the document is unloaded 
		portal.finalize();
	});
	portal.support.on(window, "online", function() {
		var url, socket;
		
		for (url in sockets) {
			socket = sockets[url];
			// There is no reason to wait
			if (socket.state() === "waiting") {
				socket.open();
			}
		}
	});
	portal.support.on(window, "offline", function() {
		var url, socket;
		
		for (url in sockets) {
			socket = sockets[url];
			// Closes sockets which cannot detect disconnection manually
			if (socket.state() === "opened") {
				socket.fire("close", "error");
			}
		}
	});
	
	// Exposes portal to the global object
	window.portal = portal;
	
})();




© 2015 - 2025 Weber Informatics LLC | Privacy Policy