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">
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; }
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==">