script.js revision adbf3e8cadb66666f307352b72537fbac57b916f
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 normalizeAllMessageWidths() { 103 normalizeElementWidths(document.querySelectorAll(".expanded > .mail-message-content")); 104} 105 106/* 107 * Normalizes the width of all elements supplied to the document body's overall width. 108 * Narrower elements are zoomed in, and wider elements are zoomed out. 109 * This method is idempotent. 110 */ 111function normalizeElementWidths(elements) { 112 var i; 113 var el; 114 var documentWidth; 115 var newZoom, oldZoom; 116 117 if (!NORMALIZE_MESSAGE_WIDTHS) { 118 return; 119 } 120 121 documentWidth = document.body.offsetWidth; 122 123 for (i = 0; i < elements.length; i++) { 124 el = elements[i]; 125 oldZoom = el.style.zoom; 126 // reset any existing normalization 127 if (oldZoom) { 128 el.style.zoom = 1; 129 } 130 newZoom = documentWidth / el.scrollWidth; 131 el.style.zoom = newZoom; 132 } 133} 134 135function hideUnsafeImages() { 136 var i, bodyCount; 137 var j, imgCount; 138 var body, image; 139 var images; 140 var showImages; 141 var bodies = document.getElementsByClassName("mail-message-content"); 142 for (i = 0, bodyCount = bodies.length; i < bodyCount; i++) { 143 body = bodies[i]; 144 showImages = body.classList.contains("mail-show-images"); 145 146 images = body.getElementsByTagName("img"); 147 for (j = 0, imgCount = images.length; j < imgCount; j++) { 148 image = images[j]; 149 rewriteRelativeImageSrc(image); 150 attachImageLoadListener(image); 151 // TODO: handle inline image attachments for all supported protocols 152 if (!showImages) { 153 blockImage(image); 154 } 155 } 156 } 157} 158 159/** 160 * Changes relative paths to absolute path by pre-pending the account uri 161 * @param {Element} imgElement Image for which the src path will be updated. 162 */ 163function rewriteRelativeImageSrc(imgElement) { 164 var src = imgElement.src; 165 166 // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation 167 if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) { 168 // The conversation specifies a different base uri than the document 169 src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length); 170 imgElement.src = src; 171 } 172}; 173 174 175function attachImageLoadListener(imageElement) { 176 // Reset the src attribute to the empty string because onload will only fire if the src 177 // attribute is set after the onload listener. 178 var originalSrc = imageElement.src; 179 imageElement.src = ''; 180 imageElement.onload = imageOnLoad; 181 imageElement.src = originalSrc; 182} 183 184/** 185 * Handle an onload event for an <img> tag. 186 * The image could be within an elided-text block, or at the top level of a message. 187 * When a new image loads, its new bounds may affect message or elided-text geometry, 188 * so we need to inspect and adjust the enclosing element's zoom level where necessary. 189 * 190 * Because this method can be called really often, and zoom-level adjustment is slow, 191 * we collect the elements to be processed and do them all later in a single deferred pass. 192 */ 193function imageOnLoad(e) { 194 // normalize the quoted text parent if we're in a quoted text block, or else 195 // normalize the parent message content element 196 var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content"); 197 if (!parent) { 198 // sanity check. shouldn't really happen. 199 return; 200 } 201 202 // if there was no previous work, schedule a new deferred job 203 if (gImageLoadElements.length == 0) { 204 window.setTimeout(handleAllImageOnLoads, 0); 205 } 206 207 // enqueue the work if it wasn't already enqueued 208 if (gImageLoadElements.indexOf(parent) == -1) { 209 gImageLoadElements.push(parent); 210 } 211} 212 213// handle all deferred work from image onload events 214function handleAllImageOnLoads() { 215 normalizeElementWidths(gImageLoadElements); 216 measurePositions(); 217 // clear the queue so the next onload event starts a new job 218 gImageLoadElements = []; 219} 220 221function blockImage(imageElement) { 222 var src = imageElement.src; 223 if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 || 224 src.indexOf("content://") == 0) { 225 imageElement.setAttribute(BLOCKED_SRC_ATTR, src); 226 imageElement.src = "data:"; 227 } 228} 229 230function setWideViewport() { 231 var metaViewport = document.getElementById('meta-viewport'); 232 metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH); 233} 234 235function restoreScrollPosition() { 236 var scrollYPercent = window.mail.getScrollYPercent(); 237 if (scrollYPercent && document.body.offsetHeight > window.innerHeight) { 238 document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight); 239 } 240} 241 242function onContentReady(event) { 243 window.mail.onContentReady(); 244} 245 246function setupContentReady() { 247 var signalDiv; 248 249 // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER 250 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op 251 // animation that immediately runs on page load. The app uses this as a signal that the 252 // content is loaded and ready to draw, since WebView delays firing this event until the 253 // layers are composited and everything is ready to draw. 254 // 255 // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag. 256 if (ENABLE_CONTENT_READY) { 257 signalDiv = document.getElementById("initial-load-signal"); 258 signalDiv.addEventListener("webkitAnimationStart", onContentReady, false); 259 signalDiv.classList.add("initial-load"); 260 } 261} 262 263// BEGIN Java->JavaScript handlers 264function measurePositions() { 265 var overlayTops, overlayBottoms; 266 var i; 267 var len; 268 269 var expandedBody, headerSpacer; 270 var prevBodyBottom = 0; 271 var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 272 273 // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be 274 // trusted. 275 276 overlayTops = new Array(expandedBodyDivs.length + 1); 277 overlayBottoms = new Array(expandedBodyDivs.length + 1); 278 for (i = 0, len = expandedBodyDivs.length; i < len; i++) { 279 expandedBody = expandedBodyDivs[i]; 280 headerSpacer = expandedBody.previousElementSibling; 281 // addJavascriptInterface handler only supports string arrays 282 overlayTops[i] = "" + prevBodyBottom; 283 overlayBottoms[i] = "" + (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight); 284 prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top; 285 } 286 // add an extra one to mark the top/bottom of the last message footer spacer 287 overlayTops[i] = "" + prevBodyBottom; 288 overlayBottoms[i] = "" + document.body.offsetHeight; 289 290 window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms); 291} 292 293function unblockImages(messageDomId) { 294 var i, images, imgCount, image, blockedSrc; 295 var msg = document.getElementById(messageDomId); 296 if (!msg) { 297 console.log("can't unblock, no matching message for id: " + messageDomId); 298 return; 299 } 300 images = msg.getElementsByTagName("img"); 301 for (i = 0, imgCount = images.length; i < imgCount; i++) { 302 image = images[i]; 303 blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR); 304 if (blockedSrc) { 305 image.src = blockedSrc; 306 image.removeAttribute(BLOCKED_SRC_ATTR); 307 } 308 } 309} 310 311function setConversationHeaderSpacerHeight(spacerHeight) { 312 var spacer = document.getElementById("conversation-header"); 313 if (!spacer) { 314 console.log("can't set spacer for conversation header"); 315 return; 316 } 317 spacer.style.height = spacerHeight + "px"; 318 measurePositions(); 319} 320 321function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) { 322 var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header"); 323 if (!spacer) { 324 console.log("can't set spacer for message with id: " + messageDomId); 325 return; 326 } 327 spacer.style.height = spacerHeight + "px"; 328 measurePositions(); 329} 330 331function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) { 332 var i, len; 333 var visibility = isVisible ? "block" : "none"; 334 var messageDiv = document.querySelector("#" + messageDomId); 335 var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible"); 336 if (!messageDiv || collapsibleDivs.length == 0) { 337 console.log("can't set body visibility for message with id: " + messageDomId); 338 return; 339 } 340 messageDiv.classList.toggle("expanded"); 341 for (i = 0, len = collapsibleDivs.length; i < len; i++) { 342 collapsibleDivs[i].style.display = visibility; 343 } 344 345 // revealing new content should trigger width normalization, since the initial render 346 // skips collapsed and super-collapsed messages 347 if (isVisible) { 348 normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content")); 349 } 350 351 setMessageHeaderSpacerHeight(messageDomId, spacerHeight); 352} 353 354function replaceSuperCollapsedBlock(startIndex) { 355 var parent, block, header; 356 357 block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']"); 358 if (!block) { 359 console.log("can't expand super collapsed block at index: " + startIndex); 360 return; 361 } 362 parent = block.parentNode; 363 block.innerHTML = window.mail.getTempMessageBodies(); 364 365 header = block.firstChild; 366 while (header) { 367 parent.insertBefore(header, block); 368 header = block.firstChild; 369 } 370 parent.removeChild(block); 371 measurePositions(); 372} 373 374function replaceMessageBodies(messageIds) { 375 var i; 376 var id; 377 var msgContentDiv; 378 379 for (i = 0, len = messageIds.length; i < len; i++) { 380 id = messageIds[i]; 381 msgContentDiv = document.querySelector("#" + id + " > .mail-message-content"); 382 msgContentDiv.innerHTML = window.mail.getMessageBody(id); 383 processQuotedText(msgContentDiv, true /* showElided */); 384 } 385} 386 387// END Java->JavaScript handlers 388 389collapseAllQuotedText(); 390hideUnsafeImages(); 391normalizeAllMessageWidths(); 392//setWideViewport(); 393restoreScrollPosition(); 394measurePositions(); 395setupContentReady(); 396 397