Plugins/WebKit Message View/Template.html
author Zachary West <zacw@adium.im>
Mon Nov 02 18:30:22 2009 -0500 (2009-11-02)
changeset 2728 bb4935318350
parent 2717 50f1ae85f841
child 2996 e3433b31fbf5
permissions -rw-r--r--
Instead of inserting a <hr/> when we lose focus, which ends up breaking more than you'd expect, add a message class for the next message. Fixes #13300.

This removes the "Show Focus Lines" preference (always a good thing), and always inserts the mark in the scrollbar. It will be up to the style to implement the "focus" class to show the location. All previous messages of class "focus" will have "focus" removed when focus is lost again.
     1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
     2 <html>
     3 <head>
     4 	<meta http-equiv="content-type" content="text/html; charset=utf-8" />
     5 	<base href="%@">
     6 	<script type="text/javascript" defer="defer">
     7 		// NOTE:
     8 		// Any percent signs in this file must be escaped!
     9 		// Use two escape signs (%%) to display it, this is passed through a format call!
    10 		
    11 		function appendHTML(html) {
    12 			var node = document.getElementById("Chat");
    13 			var range = document.createRange();
    14 			range.selectNode(node);
    15 			var documentFragment = range.createContextualFragment(html);
    16 			node.appendChild(documentFragment);
    17 		}
    18 		
    19 		// a coalesced HTML object buffers and outputs DOM objects en masse.
    20 		// saves A LOT of CSS recalculation time when loading many messages.
    21 		// (ex. a long twitter timeline)
    22 		function CoalescedHTML() {
    23 			var self = this;
    24 			this.fragment = document.createDocumentFragment();
    25 			this.timeoutID = 0;
    26 			this.coalesceRounds = 0;
    27 			this.isCoalescing = false;
    28 			this.isConsecutive = undefined;
    29 			this.shouldScroll = undefined;
    30 			
    31 			var appendElement = function (elem) {
    32 				document.getElementById("Chat").appendChild(elem);
    33 			};
    34 			
    35 			function outputHTML() {
    36 				var insert = document.getElementById("insert");
    37 				if(!!insert && self.isConsecutive) {
    38 					insert.parentNode.replaceChild(self.fragment, insert);
    39 				} else {
    40 					if(insert)
    41 						insert.parentNode.removeChild(insert);
    42 					// insert the documentFragment into the live DOM
    43 					appendElement(self.fragment);
    44 				}
    45 				alignChat(self.shouldScroll);
    46 				
    47 				// reset state to empty/non-coalescing
    48 				self.shouldScroll = undefined;
    49 				self.isConsecutive = undefined;
    50 				self.isCoalescing = false;
    51 				self.coalesceRounds = 0;
    52 			}
    53 			
    54 			// creates and returns a new documentFragment, containing all content nodes
    55 			// which can be inserted as a single node.
    56 			function createHTMLNode(html) {
    57 				var range = document.createRange();
    58 				range.selectNode(document.getElementById("Chat"));
    59 				return range.createContextualFragment(html);
    60 			}
    61 			
    62 			// removes first insert node from the internal fragment.
    63 			function rmInsertNode() {
    64 				var insert = self.fragment.querySelector("#insert");
    65 				if(insert)
    66 					insert.parentNode.removeChild(insert);
    67 			}
    68 			
    69 			function setShouldScroll(flag) {
    70 				if(flag && undefined === self.shouldScroll)
    71 					self.shouldScroll = flag;
    72 			}
    73 			
    74 			// hook in a custom method to append new data
    75 			// to the chat.
    76 			this.setAppendElementMethod = function (func) {
    77 				if(typeof func === 'function')
    78 					appendElement = func;
    79 			}
    80 						
    81 			// (re)start the coalescing timer.
    82 			//   we wait 25ms for a new message to come in.
    83 			//   If we get one, restart the timer and wait another 10ms.
    84 			//   If not, run outputHTML()
    85 			//  We do this a maximum of 400 times, for 10s max that can be spent
    86 			//  coalescing input, since this will block display.
    87 			this.coalesce = function() {
    88 				window.clearTimeout(self.timeoutID);
    89 				self.timeoutID = window.setTimeout(outputHTML, 25);
    90 				self.isCoalescing = true;
    91 				self.coalesceRounds += 1;
    92 				if(400 < self.coalesceRounds)
    93 					self.cancel();
    94 			}
    95 			
    96 			// if we need to append content into an insertion div,
    97 			// we need to clear the buffer and cancel the timeout.
    98 			this.cancel = function() {
    99 				if(self.isCoalescing) {
   100 					window.clearTimeout(self.timeoutID);
   101 					outputHTML();
   102 				}
   103 			}
   104 			
   105 			
   106 			// coalased analogs to the global functions
   107 			
   108 			this.append = function(html, shouldScroll) {
   109 				// if we started this fragment with a consecuative message,
   110 				// cancel and output before we continue
   111 				if(self.isConsecutive) {
   112 					self.cancel();
   113 				}
   114 				self.isConsecutive = false;
   115 				rmInsertNode();
   116 				var node = createHTMLNode(html);
   117 				self.fragment.appendChild(node);
   118 				
   119 				node = null;
   120 
   121 				setShouldScroll(shouldScroll);
   122 				self.coalesce();
   123 			}
   124 			
   125 			this.appendNext = function(html, shouldScroll) {
   126 				if(undefined === self.isConsecutive)
   127 					self.isConsecutive = true;
   128 				var node = createHTMLNode(html);
   129 				var insert = self.fragment.querySelector("#insert");
   130 				if(insert) {
   131 					insert.parentNode.replaceChild(node, insert);
   132 				} else {
   133 					self.fragment.appendChild(node);
   134 				}
   135 				node = null;
   136 				setShouldScroll(shouldScroll);
   137 				self.coalesce();
   138 			}
   139 			
   140 			this.replaceLast = function (html, shouldScroll) {
   141 				rmInsertNode();
   142 				var node = createHTMLNode(html);
   143 				var lastMessage = self.fragment.lastChild;
   144 				lastMessage.parentNode.replaceChild(node, lastMessage);
   145 				node = null;
   146 				setShouldScroll(shouldScroll);
   147 			}
   148 		}
   149 		var coalescedHTML;
   150 
   151 		//Appending new content to the message view
   152 		function appendMessage(html) {
   153 			var shouldScroll;
   154 			
   155 			// Only call nearBottom() if should scroll is undefined.
   156 			if(undefined === coalescedHTML.shouldScroll) {
   157 				shouldScroll = nearBottom();
   158 			} else {
   159 				shouldScroll = coalescedHTML.shouldScroll;
   160 			}
   161 			appendMessageNoScroll(html, shouldScroll);
   162 		}
   163 		
   164 		function appendMessageNoScroll(html, shouldScroll) {			
   165 			shouldScroll = shouldScroll || false;
   166 			// always try to coalesce new, non-griuped, messages
   167 			coalescedHTML.append(html, shouldScroll)
   168 		}
   169 		
   170 		function appendNextMessage(html){
   171 			var shouldScroll;
   172 			if(undefined === coalescedHTML.shouldScroll) {
   173 				shouldScroll = nearBottom();
   174 			} else {
   175 				shouldScroll = coalescedHTML.shouldScroll;
   176 			}
   177 			appendNextMessageNoScroll(html, shouldScroll);
   178 		}
   179 		
   180 		function appendNextMessageNoScroll(html, shouldScroll){
   181 			shouldScroll = shouldScroll || false;
   182 			// only group next messages if we're already coalescing input
   183 			coalescedHTML.appendNext(html, shouldScroll);
   184 		}
   185 
   186 		function replaceLastMessage(html){
   187 			var shouldScroll;
   188 			// only replace messages if we're already coalescing
   189 			if(coalescedHTML.isCoalescing){
   190 				if(undefined === coalescedHTML.shouldScroll) {
   191 					shouldScroll = nearBottom();
   192 				} else {
   193 					shouldScroll = coalescedHTML.shouldScroll;
   194 				}
   195 				coalescedHTML.replaceLast(html, shouldScroll);
   196 			} else {
   197 				shouldScroll = nearBottom();
   198 				//Retrieve the current insertion point, then remove it
   199 				//This requires that there have been an insertion point... is there a better way to retrieve the last element? -evands
   200 				var insert = document.getElementById("insert");
   201 				if(insert){
   202 					var parentNode = insert.parentNode;
   203 					parentNode.removeChild(insert);
   204 					var lastMessage = document.getElementById("Chat").lastChild;
   205 					document.getElementById("Chat").removeChild(lastMessage);
   206 				}
   207 
   208 				//Now append the message itself
   209 				appendHTML(html);
   210 
   211 				alignChat(shouldScroll);
   212 			}
   213 		}
   214 
   215 		//Auto-scroll to bottom.  Use nearBottom to determine if a scrollToBottom is desired.
   216 		function nearBottom() {
   217 			return ( document.body.scrollTop >= ( document.body.offsetHeight - ( window.innerHeight * 1.2 ) ) );
   218 		}
   219 		function scrollToBottom() {
   220 			document.body.scrollTop = document.body.offsetHeight;
   221 		}
   222 
   223 		//Dynamically exchange the active stylesheet
   224 		function setStylesheet( id, url ) {
   225 			var code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">";
   226 			if( url.length ) 
   227 				code += "@import url( \"" + url + "\" );";
   228 			code += "</style>";
   229 			var range = document.createRange();
   230 			var head = document.getElementsByTagName( "head" ).item(0);
   231 			range.selectNode( head );
   232 			var documentFragment = range.createContextualFragment( code );
   233 			head.removeChild( document.getElementById( id ) );
   234 			head.appendChild( documentFragment );
   235 		}
   236 
   237 		/* Converts emoticon images to textual emoticons; all emoticons in message if alt is held */
   238 		document.onclick = function imageCheck() {
   239 			var node = event.target;
   240 			if (node.tagName.toLowerCase() != 'img')
   241 				return;
   242 				
   243 			imageSwap(node, false);
   244 		}
   245 		
   246 		/* Converts textual emoticons to images if textToImagesFlag is true, otherwise vice versa */
   247 		function imageSwap(node, textToImagesFlag) {
   248 			var shouldScroll = nearBottom();
   249 			
   250 			var images = [node];
   251 			if (event.altKey) {
   252 				while (node.id != "Chat" && node.parentNode.id != "Chat")
   253 					node = node.parentNode;
   254 				images = node.querySelectorAll(textToImagesFlag ? "a" : "img");
   255 			}
   256 			
   257 			for (var i = 0; i < images.length; i++) {
   258 				textToImagesFlag ? textToImage(images[i]) : imageToText(images[i]);
   259 			}
   260 			
   261 			alignChat(shouldScroll);
   262 		}
   263 
   264 		function textToImage(node) {
   265 			if (!node.getAttribute("isEmoticon"))
   266 				return;
   267 			//Swap the image/text
   268 			var img = document.createElement('img');
   269 			img.setAttribute('src', node.getAttribute('src'));
   270 			img.setAttribute('alt', node.firstChild.nodeValue);
   271 			img.className = node.className;
   272 			node.parentNode.replaceChild(img, node);
   273 		}
   274 		
   275 		function imageToText(node)
   276 		{
   277 			if (client.zoomImage(node) || !node.alt)
   278 				return;
   279 			var a = document.createElement('a');
   280 			a.setAttribute('onclick', 'imageSwap(this, true)');
   281 			a.setAttribute('src', node.getAttribute('src'));
   282 			a.setAttribute('isEmoticon', true);
   283 			a.className = node.className;
   284 			var text = document.createTextNode(node.alt);
   285 			a.appendChild(text);
   286 			node.parentNode.replaceChild(a, node);
   287 		}
   288 		
   289 		//Align our chat to the bottom of the window.  If true is passed, view will also be scrolled down
   290 		function alignChat(shouldScroll) {
   291 			var windowHeight = window.innerHeight;
   292 
   293 			if (windowHeight > 0) {
   294 				var contentElement = document.getElementById('Chat');
   295 				var contentHeight = contentElement.offsetHeight;
   296 				if (windowHeight - contentHeight > 0) {
   297 					contentElement.style.position = 'relative';
   298 					contentElement.style.top = (windowHeight - contentHeight) + 'px';
   299 				} else {
   300 					contentElement.style.position = 'static';
   301 				}
   302 			}
   303 
   304 			if (shouldScroll) scrollToBottom();
   305 		}
   306 
   307 		window.onresize = function windowDidResize(){
   308 			alignChat(true/*nearBottom()*/); //nearBottom buggy with inactive tabs
   309 		}
   310 		
   311 		function initStyle() {
   312 			alignChat(true);
   313 			if(!coalescedHTML)
   314 				coalescedHTML = new CoalescedHTML();
   315 		}
   316 	</script>
   317 
   318 	<style type="text/css">
   319 		.actionMessageUserName { display:none; }
   320 		.actionMessageBody:before { content:"*"; }
   321 		.actionMessageBody:after { content:"*"; }
   322 		* { word-wrap:break-word; }
   323 		img.scaledToFitImage { height: auto; max-width: 100%%; }
   324 	</style>
   325 
   326 	<!-- This style is shared by all variants. !-->
   327 	<style id="baseStyle" type="text/css" media="screen,print">
   328 		%@
   329 	</style>
   330 
   331 	<!-- Although we call this mainStyle for legacy reasons, it's actually the variant style !-->
   332 	<style id="mainStyle" type="text/css" media="screen,print">
   333 		@import url( "%@" );
   334 	</style>
   335 
   336 </head>
   337 <body onload="initStyle();" style="==bodyBackground==">
   338 %@
   339 <div id="Chat">
   340 </div>
   341 %@
   342 </body>
   343 </html>