script.js revision 4cfe22a8b0fe9fb98df9c82303bacc5c1a79f19b
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 hideUnsafeImages() { 178 var i, bodyCount; 179 var j, imgCount; 180 var body, image; 181 var images; 182 var showImages; 183 var bodies = document.getElementsByClassName("mail-message-content"); 184 for (i = 0, bodyCount = bodies.length; i < bodyCount; i++) { 185 body = bodies[i]; 186 showImages = body.classList.contains("mail-show-images"); 187 188 images = body.getElementsByTagName("img"); 189 for (j = 0, imgCount = images.length; j < imgCount; j++) { 190 image = images[j]; 191 rewriteRelativeImageSrc(image); 192 attachImageLoadListener(image); 193 // TODO: handle inline image attachments for all supported protocols 194 if (!showImages) { 195 blockImage(image); 196 } 197 } 198 } 199} 200 201/** 202 * Changes relative paths to absolute path by pre-pending the account uri 203 * @param {Element} imgElement Image for which the src path will be updated. 204 */ 205function rewriteRelativeImageSrc(imgElement) { 206 var src = imgElement.src; 207 208 // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation 209 if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) { 210 // The conversation specifies a different base uri than the document 211 src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length); 212 imgElement.src = src; 213 } 214}; 215 216 217function attachImageLoadListener(imageElement) { 218 // Reset the src attribute to the empty string because onload will only fire if the src 219 // attribute is set after the onload listener. 220 var originalSrc = imageElement.src; 221 imageElement.src = ''; 222 imageElement.onload = imageOnLoad; 223 imageElement.src = originalSrc; 224} 225 226/** 227 * Handle an onload event for an <img> tag. 228 * The image could be within an elided-text block, or at the top level of a message. 229 * When a new image loads, its new bounds may affect message or elided-text geometry, 230 * so we need to inspect and adjust the enclosing element's zoom level where necessary. 231 * 232 * Because this method can be called really often, and zoom-level adjustment is slow, 233 * we collect the elements to be processed and do them all later in a single deferred pass. 234 */ 235function imageOnLoad(e) { 236 // normalize the quoted text parent if we're in a quoted text block, or else 237 // normalize the parent message content element 238 var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content"); 239 if (!parent) { 240 // sanity check. shouldn't really happen. 241 return; 242 } 243 244 // if there was no previous work, schedule a new deferred job 245 if (gImageLoadElements.length == 0) { 246 window.setTimeout(handleAllImageOnLoads, 0); 247 } 248 249 // enqueue the work if it wasn't already enqueued 250 if (gImageLoadElements.indexOf(parent) == -1) { 251 gImageLoadElements.push(parent); 252 } 253} 254 255// handle all deferred work from image onload events 256function handleAllImageOnLoads() { 257 normalizeElementWidths(gImageLoadElements); 258 measurePositions(); 259 // clear the queue so the next onload event starts a new job 260 gImageLoadElements = []; 261} 262 263function blockImage(imageElement) { 264 var src = imageElement.src; 265 if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 || 266 src.indexOf("content://") == 0) { 267 imageElement.setAttribute(BLOCKED_SRC_ATTR, src); 268 imageElement.src = "data:"; 269 } 270} 271 272function setWideViewport() { 273 var metaViewport = document.getElementById('meta-viewport'); 274 metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH); 275} 276 277function restoreScrollPosition() { 278 var scrollYPercent = window.mail.getScrollYPercent(); 279 if (scrollYPercent && document.body.offsetHeight > window.innerHeight) { 280 document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight); 281 } 282} 283 284function onContentReady(event) { 285 window.mail.onContentReady(); 286} 287 288function setupContentReady() { 289 var signalDiv; 290 291 // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER 292 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op 293 // animation that immediately runs on page load. The app uses this as a signal that the 294 // content is loaded and ready to draw, since WebView delays firing this event until the 295 // layers are composited and everything is ready to draw. 296 // 297 // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag. 298 if (ENABLE_CONTENT_READY) { 299 signalDiv = document.getElementById("initial-load-signal"); 300 signalDiv.addEventListener("webkitAnimationStart", onContentReady, false); 301 signalDiv.classList.add("initial-load"); 302 } 303} 304 305// BEGIN Java->JavaScript handlers 306function measurePositions() { 307 var overlayTops, overlayBottoms; 308 var i; 309 var len; 310 311 var expandedBody, headerSpacer; 312 var prevBodyBottom = 0; 313 var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 314 315 // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be 316 // trusted. 317 318 overlayTops = new Array(expandedBodyDivs.length + 1); 319 overlayBottoms = new Array(expandedBodyDivs.length + 1); 320 for (i = 0, len = expandedBodyDivs.length; i < len; i++) { 321 expandedBody = expandedBodyDivs[i]; 322 headerSpacer = expandedBody.previousElementSibling; 323 // addJavascriptInterface handler only supports string arrays 324 overlayTops[i] = "" + prevBodyBottom; 325 overlayBottoms[i] = "" + (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight); 326 prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top; 327 } 328 // add an extra one to mark the top/bottom of the last message footer spacer 329 overlayTops[i] = "" + prevBodyBottom; 330 overlayBottoms[i] = "" + document.body.offsetHeight; 331 332 window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms); 333} 334 335function unblockImages(messageDomId) { 336 var i, images, imgCount, image, blockedSrc; 337 var msg = document.getElementById(messageDomId); 338 if (!msg) { 339 console.log("can't unblock, no matching message for id: " + messageDomId); 340 return; 341 } 342 images = msg.getElementsByTagName("img"); 343 for (i = 0, imgCount = images.length; i < imgCount; i++) { 344 image = images[i]; 345 blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR); 346 if (blockedSrc) { 347 image.src = blockedSrc; 348 image.removeAttribute(BLOCKED_SRC_ATTR); 349 } 350 } 351} 352 353function setConversationHeaderSpacerHeight(spacerHeight) { 354 var spacer = document.getElementById("conversation-header"); 355 if (!spacer) { 356 console.log("can't set spacer for conversation header"); 357 return; 358 } 359 spacer.style.height = spacerHeight + "px"; 360 measurePositions(); 361} 362 363function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) { 364 var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header"); 365 if (!spacer) { 366 console.log("can't set spacer for message with id: " + messageDomId); 367 return; 368 } 369 spacer.style.height = spacerHeight + "px"; 370 measurePositions(); 371} 372 373function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) { 374 var i, len; 375 var visibility = isVisible ? "block" : "none"; 376 var messageDiv = document.querySelector("#" + messageDomId); 377 var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible"); 378 if (!messageDiv || collapsibleDivs.length == 0) { 379 console.log("can't set body visibility for message with id: " + messageDomId); 380 return; 381 } 382 messageDiv.classList.toggle("expanded"); 383 for (i = 0, len = collapsibleDivs.length; i < len; i++) { 384 collapsibleDivs[i].style.display = visibility; 385 } 386 387 // revealing new content should trigger width normalization, since the initial render 388 // skips collapsed and super-collapsed messages 389 if (isVisible) { 390 normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content")); 391 } 392 393 setMessageHeaderSpacerHeight(messageDomId, spacerHeight); 394} 395 396function replaceSuperCollapsedBlock(startIndex) { 397 var parent, block, header; 398 399 block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']"); 400 if (!block) { 401 console.log("can't expand super collapsed block at index: " + startIndex); 402 return; 403 } 404 parent = block.parentNode; 405 block.innerHTML = window.mail.getTempMessageBodies(); 406 407 header = block.firstChild; 408 while (header) { 409 parent.insertBefore(header, block); 410 header = block.firstChild; 411 } 412 parent.removeChild(block); 413 measurePositions(); 414} 415 416function replaceMessageBodies(messageIds) { 417 var i; 418 var id; 419 var msgContentDiv; 420 421 for (i = 0, len = messageIds.length; i < len; i++) { 422 id = messageIds[i]; 423 msgContentDiv = document.querySelector("#" + id + " > .mail-message-content"); 424 msgContentDiv.innerHTML = window.mail.getMessageBody(id); 425 processQuotedText(msgContentDiv, true /* showElided */); 426 } 427} 428 429// END Java->JavaScript handlers 430 431// Do this first to ensure that the readiness signal comes through, 432// even if a stray exception later occurs. 433setupContentReady(); 434 435collapseAllQuotedText(); 436hideUnsafeImages(); 437normalizeAllMessageWidths(); 438//setWideViewport(); 439restoreScrollPosition(); 440measurePositions(); 441 442