Plugins/WebKit Message View/Template.html
author Zachary West <zacw@adium.im>
Mon Nov 02 18:30:22 2009 -0500 (2009-11-02)
changeset 2852 7ff6b3f336d6
parent 2841 b0e9a4466823
child 3252 b6246b272433
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@2738
    18
		
sholt@2738
    19
		// a coalesced HTML object buffers and outputs DOM objects en masse.
sholt@2738
    20
		// saves A LOT of CSS recalculation time when loading many messages.
sholt@2738
    21
		// (ex. a long twitter timeline)
sholt@2738
    22
		function CoalescedHTML() {
sholt@2738
    23
			var self = this;
sholt@2759
    24
			this.fragment = document.createDocumentFragment();
sholt@2738
    25
			this.timeoutID = 0;
sholt@2749
    26
			this.coalesceRounds = 0;
sholt@2738
    27
			this.isCoalescing = false;
sholt@2762
    28
			this.isConsecutive = undefined;
sholt@2760
    29
			this.shouldScroll = undefined;
sholt@2745
    30
			
sholt@2841
    31
			var appendElement = function (elem) {
sholt@2824
    32
				document.getElementById("Chat").appendChild(elem);
sholt@2841
    33
			};
sholt@2824
    34
			
sholt@2738
    35
			function outputHTML() {
sholt@2738
    36
				var insert = document.getElementById("insert");
sholt@2765
    37
				if(!!insert && self.isConsecutive) {
sholt@2764
    38
					insert.parentNode.replaceChild(self.fragment, insert);
sholt@2764
    39
				} else {
sholt@2764
    40
					if(insert)
sholt@2764
    41
						insert.parentNode.removeChild(insert);
sholt@2764
    42
					// insert the documentFragment into the live DOM
sholt@2824
    43
					appendElement(self.fragment);
sholt@2764
    44
				}
sholt@2738
    45
				alignChat(self.shouldScroll);
sholt@2738
    46
				
sholt@2753
    47
				// reset state to empty/non-coalescing
sholt@2760
    48
				self.shouldScroll = undefined;
sholt@2762
    49
				self.isConsecutive = undefined;
sholt@2738
    50
				self.isCoalescing = false;
sholt@2749
    51
				self.coalesceRounds = 0;
sholt@2738
    52
			}
sholt@2738
    53
			
sholt@2757
    54
			// creates and returns a new documentFragment, containing all content nodes
sholt@2757
    55
			// which can be inserted as a single node.
sholt@2748
    56
			function createHTMLNode(html) {
sholt@2773
    57
				var range = document.createRange();
sholt@2773
    58
				range.selectNode(document.getElementById("Chat"));
sholt@2773
    59
				return range.createContextualFragment(html);
sholt@2746
    60
			}
sholt@2746
    61
			
sholt@2756
    62
			// removes first insert node from the internal fragment.
sholt@2756
    63
			function rmInsertNode() {
sholt@2756
    64
				var insert = self.fragment.querySelector("#insert");
sholt@2756
    65
				if(insert)
sholt@2756
    66
					insert.parentNode.removeChild(insert);
sholt@2756
    67
			}
sholt@2756
    68
			
sholt@2841
    69
			function setShouldScroll(flag) {
sholt@2841
    70
				if(flag && undefined === self.shouldScroll)
sholt@2841
    71
					self.shouldScroll = flag;
sholt@2841
    72
			}
sholt@2841
    73
			
sholt@2841
    74
			// hook in a custom method to append new data
sholt@2841
    75
			// to the chat.
sholt@2841
    76
			this.setAppendElementMethod = function (func) {
sholt@2841
    77
				if(typeof func === 'function')
sholt@2841
    78
					appendElement = func;
sholt@2841
    79
			}
sholt@2841
    80
						
sholt@2757
    81
			// (re)start the coalescing timer.
sholt@2761
    82
			//   we wait 25ms for a new message to come in.
sholt@2757
    83
			//   If we get one, restart the timer and wait another 10ms.
sholt@2757
    84
			//   If not, run outputHTML()
sholt@2761
    85
			//  We do this a maximum of 400 times, for 10s max that can be spent
sholt@2757
    86
			//  coalescing input, since this will block display.
sholt@2738
    87
			this.coalesce = function() {
sholt@2749
    88
				window.clearTimeout(self.timeoutID);
sholt@2761
    89
				self.timeoutID = window.setTimeout(outputHTML, 25);
sholt@2749
    90
				self.isCoalescing = true;
sholt@2750
    91
				self.coalesceRounds += 1;
sholt@2761
    92
				if(400 < self.coalesceRounds)
sholt@2749
    93
					self.cancel();
sholt@2738
    94
			}
sholt@2738
    95
			
sholt@2738
    96
			// if we need to append content into an insertion div,
sholt@2738
    97
			// we need to clear the buffer and cancel the timeout.
sholt@2738
    98
			this.cancel = function() {
sholt@2738
    99
				if(self.isCoalescing) {
sholt@2738
   100
					window.clearTimeout(self.timeoutID);
sholt@2738
   101
					outputHTML();
sholt@2738
   102
				}
sholt@2738
   103
			}
sholt@2738
   104
			
sholt@2757
   105
			
sholt@2757
   106
			// coalased analogs to the global functions
sholt@2757
   107
			
sholt@2738
   108
			this.append = function(html, shouldScroll) {
sholt@2762
   109
				// if we started this fragment with a consecuative message,
sholt@2762
   110
				// cancel and output before we continue
sholt@2765
   111
				if(self.isConsecutive) {
sholt@2762
   112
					self.cancel();
sholt@2762
   113
				}
sholt@2762
   114
				self.isConsecutive = false;
sholt@2756
   115
				rmInsertNode();
sholt@2750
   116
				var node = createHTMLNode(html);
sholt@2750
   117
				self.fragment.appendChild(node);
sholt@2758
   118
				
sholt@2758
   119
				node = null;
sholt@2746
   120
sholt@2841
   121
				setShouldScroll(shouldScroll);
sholt@2746
   122
				self.coalesce();
sholt@2746
   123
			}
sholt@2746
   124
			
sholt@2746
   125
			this.appendNext = function(html, shouldScroll) {
sholt@2765
   126
				if(undefined === self.isConsecutive)
sholt@2765
   127
					self.isConsecutive = true;
sholt@2765
   128
				var node = createHTMLNode(html);
sholt@2750
   129
				var insert = self.fragment.querySelector("#insert");
sholt@2746
   130
				if(insert) {
sholt@2751
   131
					insert.parentNode.replaceChild(node, insert);
sholt@2746
   132
				} else {
sholt@2765
   133
					self.fragment.appendChild(node);
sholt@2738
   134
				}
sholt@2765
   135
				node = null;
sholt@2841
   136
				setShouldScroll(shouldScroll);
sholt@2738
   137
				self.coalesce();
sholt@2738
   138
			}
sholt@2754
   139
			
sholt@2754
   140
			this.replaceLast = function (html, shouldScroll) {
sholt@2756
   141
				rmInsertNode();
sholt@2754
   142
				var node = createHTMLNode(html);
sholt@2754
   143
				var lastMessage = self.fragment.lastChild;
sholt@2754
   144
				lastMessage.parentNode.replaceChild(node, lastMessage);
sholt@2758
   145
				node = null;
sholt@2841
   146
				setShouldScroll(shouldScroll);
sholt@2754
   147
			}
sholt@2738
   148
		}
sholt@2738
   149
		var coalescedHTML;
David@685
   150
David@0
   151
		//Appending new content to the message view
David@0
   152
		function appendMessage(html) {
sholt@2760
   153
			var shouldScroll;
sholt@2760
   154
			
sholt@2760
   155
			// Only call nearBottom() if should scroll is undefined.
sholt@2760
   156
			if(undefined === coalescedHTML.shouldScroll) {
sholt@2760
   157
				shouldScroll = nearBottom();
sholt@2760
   158
			} else {
sholt@2760
   159
				shouldScroll = coalescedHTML.shouldScroll;
sholt@2760
   160
			}
sholt@2738
   161
			appendMessageNoScroll(html, shouldScroll);
catfish@2525
   162
		}
catfish@2525
   163
		
sholt@2738
   164
		function appendMessageNoScroll(html, shouldScroll) {			
sholt@2738
   165
			shouldScroll = shouldScroll || false;
sholt@2757
   166
			// always try to coalesce new, non-griuped, messages
sholt@2738
   167
			coalescedHTML.append(html, shouldScroll)
catfish@2525
   168
		}
catfish@2525
   169
		
catfish@2525
   170
		function appendNextMessage(html){
sholt@2760
   171
			var shouldScroll;
sholt@2760
   172
			if(undefined === coalescedHTML.shouldScroll) {
sholt@2760
   173
				shouldScroll = nearBottom();
sholt@2760
   174
			} else {
sholt@2760
   175
				shouldScroll = coalescedHTML.shouldScroll;
sholt@2760
   176
			}
sholt@2746
   177
			appendNextMessageNoScroll(html, shouldScroll);
David@0
   178
		}
catfish@2525
   179
		
sholt@2746
   180
		function appendNextMessageNoScroll(html, shouldScroll){
sholt@2746
   181
			shouldScroll = shouldScroll || false;
sholt@2757
   182
			// only group next messages if we're already coalescing input
sholt@2762
   183
			coalescedHTML.appendNext(html, shouldScroll);
David@0
   184
		}
catfish@2514
   185
David@0
   186
		function replaceLastMessage(html){
sholt@2760
   187
			var shouldScroll;
sholt@2757
   188
			// only replace messages if we're already coalescing
sholt@2754
   189
			if(coalescedHTML.isCoalescing){
sholt@2760
   190
				if(undefined === coalescedHTML.shouldScroll) {
sholt@2760
   191
					shouldScroll = nearBottom();
sholt@2760
   192
				} else {
sholt@2760
   193
					shouldScroll = coalescedHTML.shouldScroll;
sholt@2760
   194
				}
sholt@2754
   195
				coalescedHTML.replaceLast(html, shouldScroll);
sholt@2754
   196
			} else {
sholt@2760
   197
				shouldScroll = nearBottom();
sholt@2754
   198
				//Retrieve the current insertion point, then remove it
sholt@2754
   199
				//This requires that there have been an insertion point... is there a better way to retrieve the last element? -evands
sholt@2754
   200
				var insert = document.getElementById("insert");
sholt@2754
   201
				if(insert){
sholt@2754
   202
					var parentNode = insert.parentNode;
sholt@2754
   203
					parentNode.removeChild(insert);
sholt@2754
   204
					var lastMessage = document.getElementById("Chat").lastChild;
sholt@2754
   205
					document.getElementById("Chat").removeChild(lastMessage);
sholt@2754
   206
				}
David@0
   207
sholt@2754
   208
				//Now append the message itself
sholt@2754
   209
				appendHTML(html);
sholt@2754
   210
sholt@2754
   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@2738
   310
		
sholt@2824
   311
		function initStyle() {
sholt@2738
   312
			alignChat(true);
sholt@2824
   313
			if(!coalescedHTML)
sholt@2824
   314
				coalescedHTML = new CoalescedHTML();
sholt@2738
   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@2809
   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@2824
   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>