1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
4 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
6 <script type="text/javascript" defer="defer">
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!
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);
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() {
24 this.fragment = document.createDocumentFragment();
26 this.coalesceRounds = 0;
27 this.isCoalescing = false;
28 this.isConsecutive = undefined;
29 this.shouldScroll = undefined;
31 var appendElement = function (elem) {
32 document.getElementById("Chat").appendChild(elem);
35 function outputHTML() {
36 var insert = document.getElementById("insert");
37 if(!!insert && self.isConsecutive) {
38 insert.parentNode.replaceChild(self.fragment, insert);
41 insert.parentNode.removeChild(insert);
42 // insert the documentFragment into the live DOM
43 appendElement(self.fragment);
45 alignChat(self.shouldScroll);
47 // reset state to empty/non-coalescing
48 self.shouldScroll = undefined;
49 self.isConsecutive = undefined;
50 self.isCoalescing = false;
51 self.coalesceRounds = 0;
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);
62 // removes first insert node from the internal fragment.
63 function rmInsertNode() {
64 var insert = self.fragment.querySelector("#insert");
66 insert.parentNode.removeChild(insert);
69 function setShouldScroll(flag) {
70 if(flag && undefined === self.shouldScroll)
71 self.shouldScroll = flag;
74 // hook in a custom method to append new data
76 this.setAppendElementMethod = function (func) {
77 if(typeof func === 'function')
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)
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);
106 // coalased analogs to the global functions
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) {
114 self.isConsecutive = false;
116 var node = createHTMLNode(html);
117 self.fragment.appendChild(node);
121 setShouldScroll(shouldScroll);
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");
131 insert.parentNode.replaceChild(node, insert);
133 self.fragment.appendChild(node);
136 setShouldScroll(shouldScroll);
140 this.replaceLast = function (html, shouldScroll) {
142 var node = createHTMLNode(html);
143 var lastMessage = self.fragment.lastChild;
144 lastMessage.parentNode.replaceChild(node, lastMessage);
146 setShouldScroll(shouldScroll);
151 //Appending new content to the message view
152 function appendMessage(html) {
155 // Only call nearBottom() if should scroll is undefined.
156 if(undefined === coalescedHTML.shouldScroll) {
157 shouldScroll = nearBottom();
159 shouldScroll = coalescedHTML.shouldScroll;
161 appendMessageNoScroll(html, shouldScroll);
164 function appendMessageNoScroll(html, shouldScroll) {
165 shouldScroll = shouldScroll || false;
166 // always try to coalesce new, non-griuped, messages
167 coalescedHTML.append(html, shouldScroll)
170 function appendNextMessage(html){
172 if(undefined === coalescedHTML.shouldScroll) {
173 shouldScroll = nearBottom();
175 shouldScroll = coalescedHTML.shouldScroll;
177 appendNextMessageNoScroll(html, shouldScroll);
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);
186 function replaceLastMessage(html){
188 // only replace messages if we're already coalescing
189 if(coalescedHTML.isCoalescing){
190 if(undefined === coalescedHTML.shouldScroll) {
191 shouldScroll = nearBottom();
193 shouldScroll = coalescedHTML.shouldScroll;
195 coalescedHTML.replaceLast(html, shouldScroll);
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");
202 var parentNode = insert.parentNode;
203 parentNode.removeChild(insert);
204 var lastMessage = document.getElementById("Chat").lastChild;
205 document.getElementById("Chat").removeChild(lastMessage);
208 //Now append the message itself
211 alignChat(shouldScroll);
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 ) ) );
219 function scrollToBottom() {
220 document.body.scrollTop = document.body.offsetHeight;
223 //Dynamically exchange the active stylesheet
224 function setStylesheet( id, url ) {
225 var code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">";
227 code += "@import url( \"" + url + "\" );";
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 );
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')
243 imageSwap(node, false);
246 /* Converts textual emoticons to images if textToImagesFlag is true, otherwise vice versa */
247 function imageSwap(node, textToImagesFlag) {
248 var shouldScroll = nearBottom();
252 while (node.id != "Chat" && node.parentNode.id != "Chat")
253 node = node.parentNode;
254 images = node.querySelectorAll(textToImagesFlag ? "a" : "img");
257 for (var i = 0; i < images.length; i++) {
258 textToImagesFlag ? textToImage(images[i]) : imageToText(images[i]);
261 alignChat(shouldScroll);
264 function textToImage(node) {
265 if (!node.getAttribute("isEmoticon"))
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);
275 function imageToText(node)
277 if (client.zoomImage(node) || !node.alt)
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);
286 node.parentNode.replaceChild(a, node);
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;
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';
300 contentElement.style.position = 'static';
304 if (shouldScroll) scrollToBottom();
307 window.onresize = function windowDidResize(){
308 alignChat(true/*nearBottom()*/); //nearBottom buggy with inactive tabs
311 function initStyle() {
314 coalescedHTML = new CoalescedHTML();
318 <style type="text/css">
319 .actionMessageUserName { display:none; }
320 .actionMessageBody:before { content:"*"; }
321 .actionMessageBody:after { content:"*"; }
322 * { word-wrap:break-word; text-rendering: optimizelegibility; }
323 img.scaledToFitImage { height: auto; max-width: 100%%; }
326 <!-- This style is shared by all variants. !-->
327 <style id="baseStyle" type="text/css" media="screen,print">
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">
337 <body onload="initStyle();" style="==bodyBackground==">