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