changeset 32438:f789627c4f0d

Update Template.html from Adium's hg repo.
author Elliott Sales de Andrade <qulogic@pidgin.im>
date Wed, 04 Jan 2012 00:44:36 +0000
parents 5dea9a26078d
children 5022cf604d6e
files pidgin/themes/Template.html
diffstat 1 files changed, 271 insertions(+), 92 deletions(-) [+]
line wrap: on
line diff
--- a/pidgin/themes/Template.html	Wed Jan 04 00:14:51 2012 +0000
+++ b/pidgin/themes/Template.html	Wed Jan 04 00:44:36 2012 +0000
@@ -3,66 +3,215 @@
 <head>
 	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
 	<base href="%@">
-	<script type="text/ecmascript" defer="defer">
-	
+	<script type="text/javascript" defer="defer">
+		// NOTE:
+		// Any percent signs in this file must be escaped!
+		// Use two escape signs (%%) to display it, this is passed through a format call!
+
+		function appendHTML(html) {
+			var node = document.getElementById("Chat");
+			var range = document.createRange();
+			range.selectNode(node);
+			var documentFragment = range.createContextualFragment(html);
+			node.appendChild(documentFragment);
+		}
+
+		// a coalesced HTML object buffers and outputs DOM objects en masse.
+		// saves A LOT of CSS recalculation time when loading many messages.
+		// (ex. a long twitter timeline)
+		function CoalescedHTML() {
+			var self = this;
+			this.fragment = document.createDocumentFragment();
+			this.timeoutID = 0;
+			this.coalesceRounds = 0;
+			this.isCoalescing = false;
+			this.isConsecutive = undefined;
+			this.shouldScroll = undefined;
+
+			var appendElement = function (elem) {
+				document.getElementById("Chat").appendChild(elem);
+			};
+
+			function outputHTML() {
+				var insert = document.getElementById("insert");
+				if(!!insert && self.isConsecutive) {
+					insert.parentNode.replaceChild(self.fragment, insert);
+				} else {
+					if(insert)
+						insert.parentNode.removeChild(insert);
+					// insert the documentFragment into the live DOM
+					appendElement(self.fragment);
+				}
+				alignChat(self.shouldScroll);
+
+				// reset state to empty/non-coalescing
+				self.shouldScroll = undefined;
+				self.isConsecutive = undefined;
+				self.isCoalescing = false;
+				self.coalesceRounds = 0;
+			}
+
+			// creates and returns a new documentFragment, containing all content nodes
+			// which can be inserted as a single node.
+			function createHTMLNode(html) {
+				var range = document.createRange();
+				range.selectNode(document.getElementById("Chat"));
+				return range.createContextualFragment(html);
+			}
+
+			// removes first insert node from the internal fragment.
+			function rmInsertNode() {
+				var insert = self.fragment.querySelector("#insert");
+				if(insert)
+					insert.parentNode.removeChild(insert);
+			}
+
+			function setShouldScroll(flag) {
+				if(flag && undefined === self.shouldScroll)
+					self.shouldScroll = flag;
+			}
+
+			// hook in a custom method to append new data
+			// to the chat.
+			this.setAppendElementMethod = function (func) {
+				if(typeof func === 'function')
+					appendElement = func;
+			}
+
+			// (re)start the coalescing timer.
+			//   we wait 25ms for a new message to come in.
+			//   If we get one, restart the timer and wait another 10ms.
+			//   If not, run outputHTML()
+			//  We do this a maximum of 400 times, for 10s max that can be spent
+			//  coalescing input, since this will block display.
+			this.coalesce = function() {
+				window.clearTimeout(self.timeoutID);
+				self.timeoutID = window.setTimeout(outputHTML, 25);
+				self.isCoalescing = true;
+				self.coalesceRounds += 1;
+				if(400 < self.coalesceRounds)
+					self.cancel();
+			}
+
+			// if we need to append content into an insertion div,
+			// we need to clear the buffer and cancel the timeout.
+			this.cancel = function() {
+				if(self.isCoalescing) {
+					window.clearTimeout(self.timeoutID);
+					outputHTML();
+				}
+			}
+
+
+			// coalased analogs to the global functions
+
+			this.append = function(html, shouldScroll) {
+				// if we started this fragment with a consecuative message,
+				// cancel and output before we continue
+				if(self.isConsecutive) {
+					self.cancel();
+				}
+				self.isConsecutive = false;
+				rmInsertNode();
+				var node = createHTMLNode(html);
+				self.fragment.appendChild(node);
+
+				node = null;
+
+				setShouldScroll(shouldScroll);
+				self.coalesce();
+			}
+
+			this.appendNext = function(html, shouldScroll) {
+				if(undefined === self.isConsecutive)
+					self.isConsecutive = true;
+				var node = createHTMLNode(html);
+				var insert = self.fragment.querySelector("#insert");
+				if(insert) {
+					insert.parentNode.replaceChild(node, insert);
+				} else {
+					self.fragment.appendChild(node);
+				}
+				node = null;
+				setShouldScroll(shouldScroll);
+				self.coalesce();
+			}
+
+			this.replaceLast = function (html, shouldScroll) {
+				rmInsertNode();
+				var node = createHTMLNode(html);
+				var lastMessage = self.fragment.lastChild;
+				lastMessage.parentNode.replaceChild(node, lastMessage);
+				node = null;
+				setShouldScroll(shouldScroll);
+			}
+		}
+		var coalescedHTML;
+
 		//Appending new content to the message view
 		function appendMessage(html) {
-			shouldScroll = nearBottom();
-		
-			//Remove any existing insertion point
-			insert = document.getElementById("insert");
-			if(insert) insert.parentNode.removeChild(insert);
+			var shouldScroll;
 
-			//Append the new message to the bottom of our chat block
-			chat = document.getElementById("Chat");
-			range = document.createRange();
-			range.selectNode(chat);
-			documentFragment = range.createContextualFragment(html);
-			chat.appendChild(documentFragment);
-			
-			alignChat(shouldScroll);
+			// Only call nearBottom() if should scroll is undefined.
+			if(undefined === coalescedHTML.shouldScroll) {
+				shouldScroll = nearBottom();
+			} else {
+				shouldScroll = coalescedHTML.shouldScroll;
+			}
+			appendMessageNoScroll(html, shouldScroll);
 		}
-		function appendMessageNoScroll(html) {
-			//Remove any existing insertion point
-			insert = document.getElementById("insert");
-			if(insert) insert.parentNode.removeChild(insert);
 
-			//Append the new message to the bottom of our chat block
-			chat = document.getElementById("Chat");
-			range = document.createRange();
-			range.selectNode(chat);
-			documentFragment = range.createContextualFragment(html);
-			chat.appendChild(documentFragment);
+		function appendMessageNoScroll(html, shouldScroll) {
+			shouldScroll = shouldScroll || false;
+			// always try to coalesce new, non-griuped, messages
+			coalescedHTML.append(html, shouldScroll)
 		}
+
 		function appendNextMessage(html){
-			shouldScroll = nearBottom();
+			var shouldScroll;
+			if(undefined === coalescedHTML.shouldScroll) {
+				shouldScroll = nearBottom();
+			} else {
+				shouldScroll = coalescedHTML.shouldScroll;
+			}
+			appendNextMessageNoScroll(html, shouldScroll);
+		}
 
-			//Locate the insertion point
-			insert = document.getElementById("insert");
-		
-			//make new node
-			range = document.createRange();
-			range.selectNode(insert.parentNode);
-			newNode = range.createContextualFragment(html);
+		function appendNextMessageNoScroll(html, shouldScroll){
+			shouldScroll = shouldScroll || false;
+			// only group next messages if we're already coalescing input
+			coalescedHTML.appendNext(html, shouldScroll);
+		}
 
-			//swap
-			insert.parentNode.replaceChild(newNode,insert);
-			
-			alignChat(shouldScroll);
+		function replaceLastMessage(html){
+			var shouldScroll;
+			// only replace messages if we're already coalescing
+			if(coalescedHTML.isCoalescing){
+				if(undefined === coalescedHTML.shouldScroll) {
+					shouldScroll = nearBottom();
+				} else {
+					shouldScroll = coalescedHTML.shouldScroll;
+				}
+				coalescedHTML.replaceLast(html, shouldScroll);
+			} else {
+				shouldScroll = nearBottom();
+				//Retrieve the current insertion point, then remove it
+				//This requires that there have been an insertion point... is there a better way to retrieve the last element? -evands
+				var insert = document.getElementById("insert");
+				if(insert){
+					var parentNode = insert.parentNode;
+					parentNode.removeChild(insert);
+					var lastMessage = document.getElementById("Chat").lastChild;
+					document.getElementById("Chat").removeChild(lastMessage);
+				}
+
+				//Now append the message itself
+				appendHTML(html);
+
+				alignChat(shouldScroll);
+			}
 		}
-		function appendNextMessageNoScroll(html){
-			//Locate the insertion point
-			insert = document.getElementById("insert");
-		
-			//make new node
-			range = document.createRange();
-			range.selectNode(insert.parentNode);
-			newNode = range.createContextualFragment(html);
 
-			//swap
-			insert.parentNode.replaceChild(newNode,insert);
-		}
-		
 		//Auto-scroll to bottom.  Use nearBottom to determine if a scrollToBottom is desired.
 		function nearBottom() {
 			return ( document.body.scrollTop >= ( document.body.offsetHeight - ( window.innerHeight * 1.2 ) ) );
@@ -73,89 +222,119 @@
 
 		//Dynamically exchange the active stylesheet
 		function setStylesheet( id, url ) {
-			code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">";
-			if( url.length ) code += "@import url( \"" + url + "\" );";
+			var code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">";
+			if( url.length )
+				code += "@import url( \"" + url + "\" );";
 			code += "</style>";
-			range = document.createRange();
-			head = document.getElementsByTagName( "head" ).item(0);
+			var range = document.createRange();
+			var head = document.getElementsByTagName( "head" ).item(0);
 			range.selectNode( head );
-			documentFragment = range.createContextualFragment( code );
+			var documentFragment = range.createContextualFragment( code );
 			head.removeChild( document.getElementById( id ) );
 			head.appendChild( documentFragment );
 		}
-		
-		//Swap an image with its alt-tag text on click, or expand/unexpand an attached image
-		document.onclick = imageCheck;
-		function imageCheck() {		
-			node = event.target;
-			if(node.tagName == 'IMG' && !client.zoomImage(node) && node.alt) {
-				a = document.createElement('a');
-				a.setAttribute('onclick', 'imageSwap(this)');
-				a.setAttribute('src', node.getAttribute('src'));
-				a.className = node.className;
-				text = document.createTextNode(node.alt);
-				a.appendChild(text);
-				node.parentNode.replaceChild(a, node);
-			}
+
+		/* Converts emoticon images to textual emoticons; all emoticons in message if alt is held */
+		document.onclick = function imageCheck() {
+			var node = event.target;
+			if (node.tagName.toLowerCase() != 'img')
+				return;
+
+			imageSwap(node, false);
 		}
 
-		function imageSwap(node) {
-			shouldScroll = nearBottom();
+		/* Converts textual emoticons to images if textToImagesFlag is true, otherwise vice versa */
+		function imageSwap(node, textToImagesFlag) {
+			var shouldScroll = nearBottom();
+
+			var images = [node];
+			if (event.altKey) {
+				while (node.id != "Chat" && node.parentNode.id != "Chat")
+					node = node.parentNode;
+				images = node.querySelectorAll(textToImagesFlag ? "a" : "img");
+			}
 
+			for (var i = 0; i < images.length; i++) {
+				textToImagesFlag ? textToImage(images[i]) : imageToText(images[i]);
+			}
+
+			alignChat(shouldScroll);
+		}
+
+		function textToImage(node) {
+			if (!node.getAttribute("isEmoticon"))
+				return;
 			//Swap the image/text
-			img = document.createElement('img');
+			var img = document.createElement('img');
 			img.setAttribute('src', node.getAttribute('src'));
 			img.setAttribute('alt', node.firstChild.nodeValue);
 			img.className = node.className;
 			node.parentNode.replaceChild(img, node);
-			
-			alignChat(shouldScroll);
 		}
-		
+
+		function imageToText(node)
+		{
+			if (client.zoomImage(node) || !node.alt)
+				return;
+			var a = document.createElement('a');
+			a.setAttribute('onclick', 'imageSwap(this, true)');
+			a.setAttribute('src', node.getAttribute('src'));
+			a.setAttribute('isEmoticon', true);
+			a.className = node.className;
+			var text = document.createTextNode(node.alt);
+			a.appendChild(text);
+			node.parentNode.replaceChild(a, node);
+		}
+
 		//Align our chat to the bottom of the window.  If true is passed, view will also be scrolled down
 		function alignChat(shouldScroll) {
 			var windowHeight = window.innerHeight;
-			
+
 			if (windowHeight > 0) {
 				var contentElement = document.getElementById('Chat');
-				var contentHeight = contentElement.offsetHeight;
-				if (windowHeight - contentHeight > 0) {
+				var heightDifference = (windowHeight - contentElement.offsetHeight);
+				if (heightDifference > 0) {
 					contentElement.style.position = 'relative';
-					contentElement.style.top = (windowHeight - contentHeight) + 'px';
+					contentElement.style.top = heightDifference + 'px';
 				} else {
 					contentElement.style.position = 'static';
 				}
 			}
-			
+
 			if (shouldScroll) scrollToBottom();
 		}
-		
-		function windowDidResize(){
+
+		window.onresize = function windowDidResize(){
 			alignChat(true/*nearBottom()*/); //nearBottom buggy with inactive tabs
 		}
-		
-		window.onresize = windowDidResize;
+
+		function initStyle() {
+			alignChat(true);
+			if(!coalescedHTML)
+				coalescedHTML = new CoalescedHTML();
+		}
 	</script>
-	
+
 	<style type="text/css">
-		.actionMessageUserName:before { content:"*"; }
+		.actionMessageUserName { display:none; }
+		.actionMessageBody:before { content:"*"; }
 		.actionMessageBody:after { content:"*"; }
-		*{ word-wrap:break-word; }
-		img.scaledToFitImage { height:auto; width:100%; }
+		* { word-wrap:break-word; text-rendering: optimizelegibility; }
+		img.scaledToFitImage { height: auto; max-width: 100%%; }
 	</style>
-	
+
 	<!-- This style is shared by all variants. !-->
-	<style id="baseStyle" type="text/css" media="screen,print">	
+	<style id="baseStyle" type="text/css" media="screen,print">
 		%@
 	</style>
-	
+
 	<!-- Although we call this mainStyle for legacy reasons, it's actually the variant style !-->
-	<style id="mainStyle" type="text/css" media="screen,print">	
+	<style id="mainStyle" type="text/css" media="screen,print">
 		@import url( "%@" );
 	</style>
 
 </head>
-<body onload="alignChat(true);" style="==bodyBackground==">
+<body onload="initStyle();" style="==bodyBackground==">
 %@
 <div id="Chat">
 </div>