|
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> |