aboutsummaryrefslogtreecommitdiffstats
path: root/data/Template.html
blob: 295ba8b844f4c522ff55b8db6f1dce0f9d9af104 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8" />
    <base href="%@">
    <script type="text/javascript" defer="defer">
        // NOTE:
        // Any percent signs in this file must be escaped!
        // Use two escape signs (%%) to display it, this is passed through a format call!
        
        function appendHTML(html) {
            var node = document.getElementById("Chat");
            var range = document.createRange();
            range.selectNode(node);
            var documentFragment = range.createContextualFragment(html);
            node.appendChild(documentFragment);
        }
        
        // a coalesced HTML object buffers and outputs DOM objects en masse.
        // saves A LOT of CSS recalculation time when loading many messages.
        // (ex. a long twitter timeline)
        function CoalescedHTML() {
            var self = this;
            this.fragment = document.createDocumentFragment();
            this.timeoutID = 0;
            this.coalesceRounds = 0;
            this.isCoalescing = false;
            this.isConsecutive = undefined;
            this.shouldScroll = undefined;
            
            var appendElement = function (elem) {
                document.getElementById("Chat").appendChild(elem);
            };
            
            function outputHTML() {
                var insert = document.getElementById("insert");
                if(!!insert && self.isConsecutive) {
                    insert.parentNode.replaceChild(self.fragment, insert);
                } else {
                    if(insert)
                        insert.parentNode.removeChild(insert);
                    // insert the documentFragment into the live DOM
                    appendElement(self.fragment);
                }
                alignChat(self.shouldScroll);
                
                // reset state to empty/non-coalescing
                self.shouldScroll = undefined;
                self.isConsecutive = undefined;
                self.isCoalescing = false;
                self.coalesceRounds = 0;
            }
            
            // creates and returns a new documentFragment, containing all content nodes
            // which can be inserted as a single node.
            function createHTMLNode(html) {
                var range = document.createRange();
                range.selectNode(document.getElementById("Chat"));
                return range.createContextualFragment(html);
            }
            
            // removes first insert node from the internal fragment.
            function rmInsertNode() {
                var insert = self.fragment.querySelector("#insert");
                if(insert)
                    insert.parentNode.removeChild(insert);
            }
            
            function setShouldScroll(flag) {
                if(flag && undefined === self.shouldScroll)
                    self.shouldScroll = flag;
            }
            
            // hook in a custom method to append new data
            // to the chat.
            this.setAppendElementMethod = function (func) {
                if(typeof func === 'function')
                    appendElement = func;
            }
                        
            // (re)start the coalescing timer.
            //   we wait 25ms for a new message to come in.
            //   If we get one, restart the timer and wait another 10ms.
            //   If not, run outputHTML()
            //  We do this a maximum of 400 times, for 10s max that can be spent
            //  coalescing input, since this will block display.
            this.coalesce = function() {
                window.clearTimeout(self.timeoutID);
                self.timeoutID = window.setTimeout(outputHTML, 25);
                self.isCoalescing = true;
                self.coalesceRounds += 1;
                if(400 < self.coalesceRounds)
                    self.cancel();
            }
            
            // if we need to append content into an insertion div,
            // we need to clear the buffer and cancel the timeout.
            this.cancel = function() {
                if(self.isCoalescing) {
                    window.clearTimeout(self.timeoutID);
                    outputHTML();
                }
            }
            
            
            // coalased analogs to the global functions
            
            this.append = function(html, shouldScroll) {
                // if we started this fragment with a consecuative message,
                // cancel and output before we continue
                if(self.isConsecutive) {
                    self.cancel();
                }
                self.isConsecutive = false;
                rmInsertNode();
                var node = createHTMLNode(html);
                self.fragment.appendChild(node);
                
                node = null;

                setShouldScroll(shouldScroll);
                self.coalesce();
            }
            
            this.appendNext = function(html, shouldScroll) {
                if(undefined === self.isConsecutive)
                    self.isConsecutive = true;
                var node = createHTMLNode(html);
                var insert = self.fragment.querySelector("#insert");
                if(insert) {
                    insert.parentNode.replaceChild(node, insert);
                } else {
                    self.fragment.appendChild(node);
                }
                node = null;
                setShouldScroll(shouldScroll);
                self.coalesce();
            }
            
            this.replaceLast = function (html, shouldScroll) {
                rmInsertNode();
                var node = createHTMLNode(html);
                var lastMessage = self.fragment.lastChild;
                lastMessage.parentNode.replaceChild(node, lastMessage);
                node = null;
                setShouldScroll(shouldScroll);
            }
        }
        var coalescedHTML;

        //Appending new content to the message view
        function appendMessage(html) {
            var shouldScroll;
            
            // Only call nearBottom() if should scroll is undefined.
            if(undefined === coalescedHTML.shouldScroll) {
                shouldScroll = nearBottom();
            } else {
                shouldScroll = coalescedHTML.shouldScroll;
            }
            appendMessageNoScroll(html, shouldScroll);
        }
        
        function appendMessageNoScroll(html, shouldScroll) {            
            shouldScroll = shouldScroll || false;
            // always try to coalesce new, non-griuped, messages
            coalescedHTML.append(html, shouldScroll)
        }
        
        function appendNextMessage(html){
            var shouldScroll;
            if(undefined === coalescedHTML.shouldScroll) {
                shouldScroll = nearBottom();
            } else {
                shouldScroll = coalescedHTML.shouldScroll;
            }
            appendNextMessageNoScroll(html, shouldScroll);
        }
        
        function appendNextMessageNoScroll(html, shouldScroll){
            shouldScroll = shouldScroll || false;
            // only group next messages if we're already coalescing input
            coalescedHTML.appendNext(html, shouldScroll);
        }

        function replaceLastMessage(html){
            var shouldScroll;
            // only replace messages if we're already coalescing
            if(coalescedHTML.isCoalescing){
                if(undefined === coalescedHTML.shouldScroll) {
                    shouldScroll = nearBottom();
                } else {
                    shouldScroll = coalescedHTML.shouldScroll;
                }
                coalescedHTML.replaceLast(html, shouldScroll);
            } else {
                shouldScroll = nearBottom();
                //Retrieve the current insertion point, then remove it
                //This requires that there have been an insertion point... is there a better way to retrieve the last element? -evands
                var insert = document.getElementById("insert");
                if(insert){
                    var parentNode = insert.parentNode;
                    parentNode.removeChild(insert);
                    var lastMessage = document.getElementById("Chat").lastChild;
                    document.getElementById("Chat").removeChild(lastMessage);
                }

                //Now append the message itself
                appendHTML(html);

                alignChat(shouldScroll);
            }
        }

        //Auto-scroll to bottom.  Use nearBottom to determine if a scrollToBottom is desired.
        function nearBottom() {
            return ( document.body.scrollTop >= ( document.body.offsetHeight - ( window.innerHeight * 1.2 ) ) );
        }
        function scrollToBottom() {
            document.body.scrollTop = document.body.offsetHeight;
        }

        //Dynamically exchange the active stylesheet
        function setStylesheet( id, url ) {
            var code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">";
            if( url.length ) 
                code += "@import url( \"" + url + "\" );";
            code += "</style>";
            var range = document.createRange();
            var head = document.getElementsByTagName( "head" ).item(0);
            range.selectNode( head );
            var documentFragment = range.createContextualFragment( code );
            head.removeChild( document.getElementById( id ) );
            head.appendChild( documentFragment );
        }

        /* Converts emoticon images to textual emoticons; all emoticons in message if alt is held */
        document.onclick = function imageCheck() {
            var node = event.target;
            if (node.tagName.toLowerCase() != 'img')
                return;
                
            imageSwap(node, false);
        }
        
        /* Converts textual emoticons to images if textToImagesFlag is true, otherwise vice versa */
        function imageSwap(node, textToImagesFlag) {
            var shouldScroll = nearBottom();
            
            var images = [node];
            if (event.altKey) {
                while (node.id != "Chat" && node.parentNode.id != "Chat")
                    node = node.parentNode;
                images = node.querySelectorAll(textToImagesFlag ? "a" : "img");
            }
            
            for (var i = 0; i < images.length; i++) {
                textToImagesFlag ? textToImage(images[i]) : imageToText(images[i]);
            }
            
            alignChat(shouldScroll);
        }

        function textToImage(node) {
            if (!node.getAttribute("isEmoticon"))
                return;
            //Swap the image/text
            var img = document.createElement('img');
            img.setAttribute('src', node.getAttribute('src'));
            img.setAttribute('alt', node.firstChild.nodeValue);
            img.className = node.className;
            node.parentNode.replaceChild(img, node);
        }
        
        function imageToText(node)
        {
            if (client.zoomImage(node) || !node.alt)
                return;
            var a = document.createElement('a');
            a.setAttribute('onclick', 'imageSwap(this, true)');
            a.setAttribute('src', node.getAttribute('src'));
            a.setAttribute('isEmoticon', true);
            a.className = node.className;
            var text = document.createTextNode(node.alt);
            a.appendChild(text);
            node.parentNode.replaceChild(a, node);
        }
        
        //Align our chat to the bottom of the window.  If true is passed, view will also be scrolled down
        function alignChat(shouldScroll) {
            var windowHeight = window.innerHeight;

            if (windowHeight > 0) {
                var contentElement = document.getElementById('Chat');
                var contentHeight = contentElement.offsetHeight;
                if (windowHeight - contentHeight > 0) {
                    contentElement.style.position = 'relative';
                    contentElement.style.top = (windowHeight - contentHeight) + 'px';
                } else {
                    contentElement.style.position = 'static';
                }
            }

            if (shouldScroll) scrollToBottom();
        }

        window.onresize = function windowDidResize(){
            alignChat(true/*nearBottom()*/); //nearBottom buggy with inactive tabs
        }
        
        function initStyle() {
            alignChat(true);
            if(!coalescedHTML)
                coalescedHTML = new CoalescedHTML();
        }
    </script>

    <style type="text/css">
        .actionMessageUserName { display:none; }
        .actionMessageBody:before { content:"*"; }
        .actionMessageBody:after { content:"*"; }
        * { word-wrap:break-word; text-rendering: optimizelegibility; }
        img.scaledToFitImage { height: auto; max-width: 100%%; }
    </style>

    <!-- This style is shared by all variants. !-->
    <style id="baseStyle" type="text/css" media="screen,print">
        %@
    </style>

    <!-- Although we call this mainStyle for legacy reasons, it's actually the variant style !-->
    <style id="mainStyle" type="text/css" media="screen,print">
        @import url( "%@" );
    </style>

</head>
<body onload="initStyle();" style="==bodyBackground==">
%@
<div id="Chat">
</div>
%@
</body>
</html>