Mercurial > pidgin
changeset 32438:f789627c4f0d
Update Template.html from Adium's hg repo.
author | Elliott Sales de Andrade <qulogic@pidgin.im> |
---|---|
date | Wed, 04 Jan 2012 00:44:36 +0000 |
parents | 5dea9a26078d |
children | 5022cf604d6e |
files | pidgin/themes/Template.html |
diffstat | 1 files changed, 271 insertions(+), 92 deletions(-) [+] |
line wrap: on
line diff
--- a/pidgin/themes/Template.html Wed Jan 04 00:14:51 2012 +0000 +++ b/pidgin/themes/Template.html Wed Jan 04 00:44:36 2012 +0000 @@ -3,66 +3,215 @@ <head> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <base href="%@"> - <script type="text/ecmascript" defer="defer"> - + <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) { - shouldScroll = nearBottom(); - - //Remove any existing insertion point - insert = document.getElementById("insert"); - if(insert) insert.parentNode.removeChild(insert); + var shouldScroll; - //Append the new message to the bottom of our chat block - chat = document.getElementById("Chat"); - range = document.createRange(); - range.selectNode(chat); - documentFragment = range.createContextualFragment(html); - chat.appendChild(documentFragment); - - alignChat(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) { - //Remove any existing insertion point - insert = document.getElementById("insert"); - if(insert) insert.parentNode.removeChild(insert); - //Append the new message to the bottom of our chat block - chat = document.getElementById("Chat"); - range = document.createRange(); - range.selectNode(chat); - documentFragment = range.createContextualFragment(html); - chat.appendChild(documentFragment); + function appendMessageNoScroll(html, shouldScroll) { + shouldScroll = shouldScroll || false; + // always try to coalesce new, non-griuped, messages + coalescedHTML.append(html, shouldScroll) } + function appendNextMessage(html){ - shouldScroll = nearBottom(); + var shouldScroll; + if(undefined === coalescedHTML.shouldScroll) { + shouldScroll = nearBottom(); + } else { + shouldScroll = coalescedHTML.shouldScroll; + } + appendNextMessageNoScroll(html, shouldScroll); + } - //Locate the insertion point - insert = document.getElementById("insert"); - - //make new node - range = document.createRange(); - range.selectNode(insert.parentNode); - newNode = range.createContextualFragment(html); + function appendNextMessageNoScroll(html, shouldScroll){ + shouldScroll = shouldScroll || false; + // only group next messages if we're already coalescing input + coalescedHTML.appendNext(html, shouldScroll); + } - //swap - insert.parentNode.replaceChild(newNode,insert); - - alignChat(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); + } } - function appendNextMessageNoScroll(html){ - //Locate the insertion point - insert = document.getElementById("insert"); - - //make new node - range = document.createRange(); - range.selectNode(insert.parentNode); - newNode = range.createContextualFragment(html); - //swap - insert.parentNode.replaceChild(newNode,insert); - } - //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 ) ) ); @@ -73,89 +222,119 @@ //Dynamically exchange the active stylesheet function setStylesheet( id, url ) { - code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">"; - if( url.length ) code += "@import url( \"" + url + "\" );"; + var code = "<style id=\"" + id + "\" type=\"text/css\" media=\"screen,print\">"; + if( url.length ) + code += "@import url( \"" + url + "\" );"; code += "</style>"; - range = document.createRange(); - head = document.getElementsByTagName( "head" ).item(0); + var range = document.createRange(); + var head = document.getElementsByTagName( "head" ).item(0); range.selectNode( head ); - documentFragment = range.createContextualFragment( code ); + var documentFragment = range.createContextualFragment( code ); head.removeChild( document.getElementById( id ) ); head.appendChild( documentFragment ); } - - //Swap an image with its alt-tag text on click, or expand/unexpand an attached image - document.onclick = imageCheck; - function imageCheck() { - node = event.target; - if(node.tagName == 'IMG' && !client.zoomImage(node) && node.alt) { - a = document.createElement('a'); - a.setAttribute('onclick', 'imageSwap(this)'); - a.setAttribute('src', node.getAttribute('src')); - a.className = node.className; - text = document.createTextNode(node.alt); - a.appendChild(text); - node.parentNode.replaceChild(a, node); - } + + /* 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); } - function imageSwap(node) { - shouldScroll = nearBottom(); + /* 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 - img = document.createElement('img'); + 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); - - alignChat(shouldScroll); } - + + 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) { + var heightDifference = (windowHeight - contentElement.offsetHeight); + if (heightDifference > 0) { contentElement.style.position = 'relative'; - contentElement.style.top = (windowHeight - contentHeight) + 'px'; + contentElement.style.top = heightDifference + 'px'; } else { contentElement.style.position = 'static'; } } - + if (shouldScroll) scrollToBottom(); } - - function windowDidResize(){ + + window.onresize = function windowDidResize(){ alignChat(true/*nearBottom()*/); //nearBottom buggy with inactive tabs } - - window.onresize = windowDidResize; + + function initStyle() { + alignChat(true); + if(!coalescedHTML) + coalescedHTML = new CoalescedHTML(); + } </script> - + <style type="text/css"> - .actionMessageUserName:before { content:"*"; } + .actionMessageUserName { display:none; } + .actionMessageBody:before { content:"*"; } .actionMessageBody:after { content:"*"; } - *{ word-wrap:break-word; } - img.scaledToFitImage { height:auto; width:100%; } + * { 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 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"> + <style id="mainStyle" type="text/css" media="screen,print"> @import url( "%@" ); </style> </head> -<body onload="alignChat(true);" style="==bodyBackground=="> +<body onload="initStyle();" style="==bodyBackground=="> %@ <div id="Chat"> </div>