script.js revision eb9a4bdc53269ee05fe11870b9ebf03f18196585
1/* 2 * Copyright (C) 2012 Google Inc. 3 * Licensed to The Android Open Source Project. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18var BLOCKED_SRC_ATTR = "blocked-src"; 19 20// the set of Elements currently scheduled for processing in handleAllImageLoads 21// this is an Array, but we treat it like a Set and only insert unique items 22var gImageLoadElements = []; 23 24/** 25 * Returns the page offset of an element. 26 * 27 * @param {Element} element The element to return the page offset for. 28 * @return {left: number, top: number} A tuple including a left and top value representing 29 * the page offset of the element. 30 */ 31function getTotalOffset(el) { 32 var result = { 33 left: 0, 34 top: 0 35 }; 36 var parent = el; 37 38 while (parent) { 39 result.left += parent.offsetLeft; 40 result.top += parent.offsetTop; 41 parent = parent.offsetParent; 42 } 43 44 return result; 45} 46 47/** 48 * Walks up the DOM starting at a given element, and returns an element that has the 49 * specified class name or null. 50 */ 51function up(el, className) { 52 var parent = el; 53 while (parent) { 54 if (parent.classList && parent.classList.contains(className)) { 55 break; 56 } 57 parent = parent.parentNode; 58 } 59 return parent || null; 60} 61 62function onToggleClick(e) { 63 toggleQuotedText(e.target); 64 measurePositions(); 65} 66 67function toggleQuotedText(toggleElement) { 68 var elidedTextElement = toggleElement.nextSibling; 69 var isHidden = getComputedStyle(elidedTextElement).display == 'none'; 70 toggleElement.innerHTML = isHidden ? MSG_HIDE_ELIDED : MSG_SHOW_ELIDED; 71 elidedTextElement.style.display = isHidden ? 'block' : 'none'; 72 73 // Revealing the elided text should normalize it to fit-width to prevent 74 // this message from blowing out the conversation width. 75 if (isHidden) { 76 normalizeElementWidths([elidedTextElement]); 77 } 78} 79 80function collapseAllQuotedText() { 81 processQuotedText(document.documentElement, false /* showElided */); 82} 83 84function processQuotedText(elt, showElided) { 85 var i; 86 var elements = elt.getElementsByClassName("elided-text"); 87 var elidedElement, toggleElement; 88 for (i = 0; i < elements.length; i++) { 89 elidedElement = elements[i]; 90 toggleElement = document.createElement("div"); 91 toggleElement.className = "mail-elided-text"; 92 toggleElement.innerHTML = MSG_SHOW_ELIDED; 93 toggleElement.onclick = onToggleClick; 94 elidedElement.style.display = 'none'; 95 elidedElement.parentNode.insertBefore(toggleElement, elidedElement); 96 if (showElided) { 97 toggleQuotedText(toggleElement); 98 } 99 } 100} 101 102function isConversationEmpty(bodyDivs) { 103 var i, len; 104 var msgBody; 105 var text; 106 107 // Check if given divs are empty (in appearance), and disable zoom if so. 108 for (i = 0, len = bodyDivs.length; i < len; i++) { 109 msgBody = bodyDivs[i]; 110 // use 'textContent' to exclude markup when determining whether bodies are empty 111 // (fall back to more expensive 'innerText' if 'textContent' isn't implemented) 112 text = msgBody.textContent || msgBody.innerText; 113 if (text.trim().length > 0) { 114 return false; 115 } 116 } 117 return true; 118} 119 120function normalizeAllMessageWidths() { 121 var expandedBodyDivs; 122 var metaViewport; 123 var contentValues; 124 var isEmpty; 125 126 if (!NORMALIZE_MESSAGE_WIDTHS) { 127 return; 128 } 129 130 expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 131 132 isEmpty = isConversationEmpty(expandedBodyDivs); 133 134 normalizeElementWidths(expandedBodyDivs); 135 136 // assemble a working <meta> viewport "content" value from the base value in the 137 // document, plus any dynamically determined options 138 metaViewport = document.getElementById("meta-viewport"); 139 contentValues = [metaViewport.getAttribute("content")]; 140 if (isEmpty) { 141 contentValues.push(metaViewport.getAttribute("data-zoom-off")); 142 } else { 143 contentValues.push(metaViewport.getAttribute("data-zoom-on")); 144 } 145 metaViewport.setAttribute("content", contentValues.join(",")); 146} 147 148/* 149 * Normalizes the width of all elements supplied to the document body's overall width. 150 * Narrower elements are zoomed in, and wider elements are zoomed out. 151 * This method is idempotent. 152 */ 153function normalizeElementWidths(elements) { 154 var i; 155 var el; 156 var documentWidth; 157 var newZoom, oldZoom; 158 159 if (!NORMALIZE_MESSAGE_WIDTHS) { 160 return; 161 } 162 163 documentWidth = document.body.offsetWidth; 164 165 for (i = 0; i < elements.length; i++) { 166 el = elements[i]; 167 oldZoom = el.style.zoom; 168 // reset any existing normalization 169 if (oldZoom) { 170 el.style.zoom = 1; 171 } 172 newZoom = documentWidth / el.scrollWidth; 173 el.style.zoom = newZoom; 174 } 175} 176 177function hideAllUnsafeImages() { 178 hideUnsafeImages(document.getElementsByClassName("mail-message-content")); 179} 180 181function hideUnsafeImages(msgContentDivs) { 182 var i, msgContentCount; 183 var j, imgCount; 184 var msgContentDiv, image; 185 var images; 186 var showImages; 187 for (i = 0, msgContentCount = msgContentDivs.length; i < msgContentCount; i++) { 188 msgContentDiv = msgContentDivs[i]; 189 showImages = msgContentDiv.classList.contains("mail-show-images"); 190 191 images = msgContentDiv.getElementsByTagName("img"); 192 for (j = 0, imgCount = images.length; j < imgCount; j++) { 193 image = images[j]; 194 rewriteRelativeImageSrc(image); 195 attachImageLoadListener(image); 196 // TODO: handle inline image attachments for all supported protocols 197 if (!showImages) { 198 blockImage(image); 199 } 200 } 201 } 202} 203 204/** 205 * Changes relative paths to absolute path by pre-pending the account uri 206 * @param {Element} imgElement Image for which the src path will be updated. 207 */ 208function rewriteRelativeImageSrc(imgElement) { 209 var src = imgElement.src; 210 211 // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation 212 if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) { 213 // The conversation specifies a different base uri than the document 214 src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length); 215 imgElement.src = src; 216 } 217}; 218 219 220function attachImageLoadListener(imageElement) { 221 // Reset the src attribute to the empty string because onload will only fire if the src 222 // attribute is set after the onload listener. 223 var originalSrc = imageElement.src; 224 imageElement.src = ''; 225 imageElement.onload = imageOnLoad; 226 imageElement.src = originalSrc; 227} 228 229/** 230 * Handle an onload event for an <img> tag. 231 * The image could be within an elided-text block, or at the top level of a message. 232 * When a new image loads, its new bounds may affect message or elided-text geometry, 233 * so we need to inspect and adjust the enclosing element's zoom level where necessary. 234 * 235 * Because this method can be called really often, and zoom-level adjustment is slow, 236 * we collect the elements to be processed and do them all later in a single deferred pass. 237 */ 238function imageOnLoad(e) { 239 // normalize the quoted text parent if we're in a quoted text block, or else 240 // normalize the parent message content element 241 var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content"); 242 if (!parent) { 243 // sanity check. shouldn't really happen. 244 return; 245 } 246 247 // if there was no previous work, schedule a new deferred job 248 if (gImageLoadElements.length == 0) { 249 window.setTimeout(handleAllImageOnLoads, 0); 250 } 251 252 // enqueue the work if it wasn't already enqueued 253 if (gImageLoadElements.indexOf(parent) == -1) { 254 gImageLoadElements.push(parent); 255 } 256} 257 258// handle all deferred work from image onload events 259function handleAllImageOnLoads() { 260 normalizeElementWidths(gImageLoadElements); 261 measurePositions(); 262 // clear the queue so the next onload event starts a new job 263 gImageLoadElements = []; 264} 265 266function blockImage(imageElement) { 267 var src = imageElement.src; 268 if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 || 269 src.indexOf("content://") == 0) { 270 imageElement.setAttribute(BLOCKED_SRC_ATTR, src); 271 imageElement.src = "data:"; 272 } 273} 274 275function setWideViewport() { 276 var metaViewport = document.getElementById('meta-viewport'); 277 metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH); 278} 279 280function restoreScrollPosition() { 281 var scrollYPercent = window.mail.getScrollYPercent(); 282 if (scrollYPercent && document.body.offsetHeight > window.innerHeight) { 283 document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight); 284 } 285} 286 287function onContentReady(event) { 288 window.mail.onContentReady(); 289} 290 291function setupContentReady() { 292 var signalDiv; 293 294 // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER 295 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op 296 // animation that immediately runs on page load. The app uses this as a signal that the 297 // content is loaded and ready to draw, since WebView delays firing this event until the 298 // layers are composited and everything is ready to draw. 299 // 300 // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag. 301 if (ENABLE_CONTENT_READY) { 302 signalDiv = document.getElementById("initial-load-signal"); 303 signalDiv.addEventListener("webkitAnimationStart", onContentReady, false); 304 } 305} 306 307// BEGIN Java->JavaScript handlers 308function measurePositions() { 309 var overlayTops, overlayBottoms; 310 var i; 311 var len; 312 313 var expandedBody, headerSpacer; 314 var prevBodyBottom = 0; 315 var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 316 317 // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be 318 // trusted. 319 320 overlayTops = new Array(expandedBodyDivs.length + 1); 321 overlayBottoms = new Array(expandedBodyDivs.length + 1); 322 for (i = 0, len = expandedBodyDivs.length; i < len; i++) { 323 expandedBody = expandedBodyDivs[i]; 324 headerSpacer = expandedBody.previousElementSibling; 325 // addJavascriptInterface handler only supports string arrays 326 overlayTops[i] = "" + prevBodyBottom; 327 overlayBottoms[i] = "" + (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight); 328 prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top; 329 } 330 // add an extra one to mark the top/bottom of the last message footer spacer 331 overlayTops[i] = "" + prevBodyBottom; 332 overlayBottoms[i] = "" + document.body.offsetHeight; 333 334 window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms); 335} 336 337function unblockImages(messageDomIds) { 338 var i, j, images, imgCount, image, blockedSrc; 339 for (j = 0, len = messageDomIds.length; j < len; j++) { 340 var messageDomId = messageDomIds[j]; 341 var msg = document.getElementById(messageDomId); 342 if (!msg) { 343 console.log("can't unblock, no matching message for id: " + messageDomId); 344 continue; 345 } 346 images = msg.getElementsByTagName("img"); 347 for (i = 0, imgCount = images.length; i < imgCount; i++) { 348 image = images[i]; 349 blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR); 350 if (blockedSrc) { 351 image.src = blockedSrc; 352 image.removeAttribute(BLOCKED_SRC_ATTR); 353 } 354 } 355 } 356} 357 358function setConversationHeaderSpacerHeight(spacerHeight) { 359 var spacer = document.getElementById("conversation-header"); 360 if (!spacer) { 361 console.log("can't set spacer for conversation header"); 362 return; 363 } 364 spacer.style.height = spacerHeight + "px"; 365 measurePositions(); 366} 367 368function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) { 369 var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header"); 370 if (!spacer) { 371 console.log("can't set spacer for message with id: " + messageDomId); 372 return; 373 } 374 spacer.style.height = spacerHeight + "px"; 375 measurePositions(); 376} 377 378function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) { 379 var i, len; 380 var visibility = isVisible ? "block" : "none"; 381 var messageDiv = document.querySelector("#" + messageDomId); 382 var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible"); 383 if (!messageDiv || collapsibleDivs.length == 0) { 384 console.log("can't set body visibility for message with id: " + messageDomId); 385 return; 386 } 387 messageDiv.classList.toggle("expanded"); 388 for (i = 0, len = collapsibleDivs.length; i < len; i++) { 389 collapsibleDivs[i].style.display = visibility; 390 } 391 392 // revealing new content should trigger width normalization, since the initial render 393 // skips collapsed and super-collapsed messages 394 if (isVisible) { 395 normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content")); 396 } 397 398 setMessageHeaderSpacerHeight(messageDomId, spacerHeight); 399} 400 401function replaceSuperCollapsedBlock(startIndex) { 402 var parent, block, msg; 403 404 block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']"); 405 if (!block) { 406 console.log("can't expand super collapsed block at index: " + startIndex); 407 return; 408 } 409 parent = block.parentNode; 410 block.innerHTML = window.mail.getTempMessageBodies(); 411 412 // process the new block contents in one go before we pluck them out of the common ancestor 413 processQuotedText(block, false /* showElided */); 414 hideUnsafeImages(block.getElementsByClassName("mail-message-content")); 415 416 msg = block.firstChild; 417 while (msg) { 418 parent.insertBefore(msg, block); 419 msg = block.firstChild; 420 } 421 parent.removeChild(block); 422 measurePositions(); 423} 424 425function replaceMessageBodies(messageIds) { 426 var i; 427 var id; 428 var msgContentDiv; 429 430 for (i = 0, len = messageIds.length; i < len; i++) { 431 id = messageIds[i]; 432 msgContentDiv = document.querySelector("#" + id + " > .mail-message-content"); 433 msgContentDiv.innerHTML = window.mail.getMessageBody(id); 434 processQuotedText(msgContentDiv, true /* showElided */); 435 hideUnsafeImages([msgContentDiv]); 436 } 437 measurePositions(); 438} 439 440// handle the special case of adding a single new message at the end of a conversation 441function appendMessageHtml() { 442 var msg = document.createElement("div"); 443 msg.innerHTML = window.mail.getTempMessageBodies(); 444 msg = msg.children[0]; // toss the outer div, it was just to render innerHTML into 445 document.body.appendChild(msg); 446 processQuotedText(msg, true /* showElided */); 447 hideUnsafeImages(msg.getElementsByClassName("mail-message-content")); 448 measurePositions(); 449} 450 451// END Java->JavaScript handlers 452 453// Do this first to ensure that the readiness signal comes through, 454// even if a stray exception later occurs. 455setupContentReady(); 456 457collapseAllQuotedText(); 458hideAllUnsafeImages(); 459normalizeAllMessageWidths(); 460//setWideViewport(); 461restoreScrollPosition(); 462measurePositions(); 463 464