script.js revision 04e53a2e6e71615ddcee39eeb0014a4cd4baf956
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 24var gScaleInfo; 25 26/** 27 * Only revert transforms that do an imperfect job of shrinking content if they fail 28 * to shrink by this much. Expressed as a ratio of: 29 * (original width difference : width difference after transforms); 30 */ 31var TRANSFORM_MINIMUM_EFFECTIVE_RATIO = 0.7; 32 33// Don't ship with this on. 34var DEBUG_DISPLAY_TRANSFORMS = false; 35 36var gTransformText = {}; 37 38/** 39 * Returns the page offset of an element. 40 * 41 * @param {Element} element The element to return the page offset for. 42 * @return {left: number, top: number} A tuple including a left and top value representing 43 * the page offset of the element. 44 */ 45function getTotalOffset(el) { 46 var result = { 47 left: 0, 48 top: 0 49 }; 50 var parent = el; 51 52 while (parent) { 53 result.left += parent.offsetLeft; 54 result.top += parent.offsetTop; 55 parent = parent.offsetParent; 56 } 57 58 return result; 59} 60 61/** 62 * Walks up the DOM starting at a given element, and returns an element that has the 63 * specified class name or null. 64 */ 65function up(el, className) { 66 var parent = el; 67 while (parent) { 68 if (parent.classList && parent.classList.contains(className)) { 69 break; 70 } 71 parent = parent.parentNode; 72 } 73 return parent || null; 74} 75 76function getCachedValue(div, property, attrName) { 77 var value; 78 if (div.hasAttribute(attrName)) { 79 value = div.getAttribute(attrName); 80 } else { 81 value = div[property]; 82 div.setAttribute(attrName, value); 83 } 84 return value; 85} 86 87function onToggleClick(e) { 88 toggleQuotedText(e.target); 89 measurePositions(); 90} 91 92function toggleQuotedText(toggleElement) { 93 var elidedTextElement = toggleElement.nextSibling; 94 var isHidden = getComputedStyle(elidedTextElement).display == 'none'; 95 toggleElement.innerHTML = isHidden ? MSG_HIDE_ELIDED : MSG_SHOW_ELIDED; 96 elidedTextElement.style.display = isHidden ? 'block' : 'none'; 97 98 // Revealing the elided text should normalize it to fit-width to prevent 99 // this message from blowing out the conversation width. 100 if (isHidden) { 101 normalizeElementWidths([elidedTextElement]); 102 } 103} 104 105function collapseAllQuotedText() { 106 processQuotedText(document.documentElement, false /* showElided */); 107} 108 109function processQuotedText(elt, showElided) { 110 var i; 111 var elements = elt.getElementsByClassName("elided-text"); 112 var elidedElement, toggleElement; 113 for (i = 0; i < elements.length; i++) { 114 elidedElement = elements[i]; 115 toggleElement = document.createElement("div"); 116 toggleElement.className = "mail-elided-text"; 117 toggleElement.innerHTML = MSG_SHOW_ELIDED; 118 toggleElement.onclick = onToggleClick; 119 elidedElement.style.display = 'none'; 120 elidedElement.parentNode.insertBefore(toggleElement, elidedElement); 121 if (showElided) { 122 toggleQuotedText(toggleElement); 123 } 124 } 125} 126 127function isConversationEmpty(bodyDivs) { 128 var i, len; 129 var msgBody; 130 var text; 131 132 // Check if given divs are empty (in appearance), and disable zoom if so. 133 for (i = 0, len = bodyDivs.length; i < len; i++) { 134 msgBody = bodyDivs[i]; 135 // use 'textContent' to exclude markup when determining whether bodies are empty 136 // (fall back to more expensive 'innerText' if 'textContent' isn't implemented) 137 text = msgBody.textContent || msgBody.innerText; 138 if (text.trim().length > 0) { 139 return false; 140 } 141 } 142 return true; 143} 144 145function normalizeAllMessageWidths() { 146 var expandedBodyDivs; 147 var metaViewport; 148 var contentValues; 149 var isEmpty; 150 151 expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 152 153 isEmpty = isConversationEmpty(expandedBodyDivs); 154 155 normalizeElementWidths(expandedBodyDivs); 156 157 // assemble a working <meta> viewport "content" value from the base value in the 158 // document, plus any dynamically determined options 159 metaViewport = document.getElementById("meta-viewport"); 160 contentValues = [metaViewport.getAttribute("content")]; 161 if (isEmpty) { 162 contentValues.push(metaViewport.getAttribute("data-zoom-off")); 163 } else { 164 contentValues.push(metaViewport.getAttribute("data-zoom-on")); 165 } 166 metaViewport.setAttribute("content", contentValues.join(",")); 167} 168 169/* 170 * Normalizes the width of all elements supplied to the document body's overall width. 171 * Narrower elements are zoomed in, and wider elements are zoomed out. 172 * This method is idempotent. 173 */ 174function normalizeElementWidths(elements) { 175 var i; 176 var el; 177 var documentWidth; 178 var newZoom, oldZoom; 179 180 documentWidth = document.body.offsetWidth; 181 182 for (i = 0; i < elements.length; i++) { 183 el = elements[i]; 184 oldZoom = el.style.zoom; 185 // reset any existing normalization 186 if (oldZoom) { 187 el.style.zoom = 1; 188 } 189 newZoom = documentWidth / el.scrollWidth; 190 transformContent(el, documentWidth, el.scrollWidth); 191 newZoom = documentWidth / el.scrollWidth; 192 if (NORMALIZE_MESSAGE_WIDTHS) { 193 el.style.zoom = newZoom; 194 } 195 } 196} 197 198function transformContent(el, docWidth, elWidth) { 199 var nodes; 200 var i, len; 201 var index; 202 var newWidth = elWidth; 203 var wStr; 204 var touched; 205 // the format of entries in this array is: 206 // entry := [ undoFunction, undoFunctionThis, undoFunctionParamArray ] 207 var actionLog = []; 208 var node; 209 var done = false; 210 var msgId; 211 var transformText; 212 var existingText; 213 var textElement; 214 var start; 215 if (elWidth <= docWidth) { 216 return; 217 } 218 219 start = Date.now(); 220 221 if (el.parentElement.classList.contains("mail-message")) { 222 msgId = el.parentElement.id; 223 transformText = "[origW=" + elWidth + "/" + docWidth; 224 } 225 226 // Try munging all divs or textareas with inline styles where the width 227 // is wider than docWidth, and change it to be a max-width. 228 touched = false; 229 nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("div[style], textarea[style]") : []; 230 touched = transformBlockElements(nodes, docWidth, actionLog); 231 if (touched) { 232 newWidth = el.scrollWidth; 233 console.log("ran div-width munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 234 + " docW=" + docWidth); 235 if (msgId) { 236 transformText += " DIV:newW=" + newWidth; 237 } 238 if (newWidth <= docWidth) { 239 done = true; 240 } 241 } 242 243 if (!done) { 244 // OK, that wasn't enough. Find images with widths and override their widths. 245 nodes = ENABLE_MUNGE_IMAGES ? el.querySelectorAll("img") : []; 246 touched = transformImages(nodes, docWidth, actionLog); 247 if (touched) { 248 newWidth = el.scrollWidth; 249 console.log("ran img munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 250 + " docW=" + docWidth); 251 if (msgId) { 252 transformText += " IMG:newW=" + newWidth; 253 } 254 if (newWidth <= docWidth) { 255 done = true; 256 } 257 } 258 } 259 260 if (!done) { 261 // OK, that wasn't enough. Find tables with widths and override their widths. 262 nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("table") : []; 263 touched = addClassToElements(nodes, shouldMungeTable, "munged", 264 actionLog); 265 if (touched) { 266 newWidth = el.scrollWidth; 267 console.log("ran table munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 268 + " docW=" + docWidth); 269 if (msgId) { 270 transformText += " TABLE:newW=" + newWidth; 271 } 272 if (newWidth <= docWidth) { 273 done = true; 274 } 275 } 276 } 277 278 if (!done) { 279 // OK, that wasn't enough. Try munging all <td> to override any width and nowrap set. 280 nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("td") : []; 281 touched = addClassToElements(nodes, null /* mungeAll */, "munged", 282 actionLog); 283 if (touched) { 284 newWidth = el.scrollWidth; 285 console.log("ran td munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 286 + " docW=" + docWidth); 287 if (msgId) { 288 transformText += " TD:newW=" + newWidth; 289 } 290 if (newWidth <= docWidth) { 291 done = true; 292 } 293 } 294 } 295 296 // If the transformations shrank the width significantly enough, leave them in place. 297 // We figure that in those cases, the benefits outweight the risk of rendering artifacts. 298 if (!done && (elWidth - newWidth) / (elWidth - docWidth) > 299 TRANSFORM_MINIMUM_EFFECTIVE_RATIO) { 300 console.log("transform(s) deemed effective enough"); 301 done = true; 302 } 303 304 if (done) { 305 if (msgId) { 306 transformText += "]"; 307 existingText = gTransformText[msgId]; 308 if (!existingText) { 309 transformText = "Message transforms: " + transformText; 310 } else { 311 transformText = existingText + " " + transformText; 312 } 313 gTransformText[msgId] = transformText; 314 window.mail.onMessageTransform(msgId, transformText); 315 if (DEBUG_DISPLAY_TRANSFORMS) { 316 textElement = el.firstChild; 317 if (!textElement.classList || !textElement.classList.contains("transform-text")) { 318 textElement = document.createElement("div"); 319 textElement.classList.add("transform-text"); 320 textElement.style.fontSize = "10px"; 321 textElement.style.color = "#ccc"; 322 el.insertBefore(textElement, el.firstChild); 323 } 324 textElement.innerHTML = transformText + "<br>"; 325 } 326 } 327 console.log("munger(s) succeeded, elapsed time=" + (Date.now() - start)); 328 return; 329 } 330 331 // reverse all changes if the width is STILL not narrow enough 332 // (except the width->maxWidth change, which is not particularly destructive) 333 for (i = 0, len = actionLog.length; i < len; i++) { 334 actionLog[i][0].apply(actionLog[i][1], actionLog[i][2]); 335 } 336 if (actionLog.length > 0) { 337 console.log("all mungers failed, changes reversed. elapsed time=" + (Date.now() - start)); 338 } 339} 340 341function addClassToElements(nodes, conditionFn, classToAdd, actionLog) { 342 var i, len; 343 var node; 344 var added = false; 345 for (i = 0, len = nodes.length; i < len; i++) { 346 node = nodes[i]; 347 if (!conditionFn || conditionFn(node)) { 348 if (node.classList.contains(classToAdd)) { 349 continue; 350 } 351 node.classList.add(classToAdd); 352 added = true; 353 actionLog.push([node.classList.remove, node.classList, [classToAdd]]); 354 } 355 } 356 return added; 357} 358 359function transformBlockElements(nodes, docWidth, actionLog) { 360 var i, len; 361 var node; 362 var wStr; 363 var index; 364 var touched = false; 365 366 for (i = 0, len = nodes.length; i < len; i++) { 367 node = nodes[i]; 368 wStr = node.style.width || node.style.minWidth; 369 index = wStr ? wStr.indexOf("px") : -1; 370 if (index >= 0 && wStr.slice(0, index) > docWidth) { 371 saveStyleProperty(node, "width", actionLog); 372 saveStyleProperty(node, "minWidth", actionLog); 373 saveStyleProperty(node, "maxWidth", actionLog); 374 node.style.width = "100%"; 375 node.style.minWidth = ""; 376 node.style.maxWidth = wStr; 377 touched = true; 378 } 379 } 380 return touched; 381} 382 383function transformImages(nodes, docWidth, actionLog) { 384 var i, len; 385 var node; 386 var w, h; 387 var touched = false; 388 389 for (i = 0, len = nodes.length; i < len; i++) { 390 node = nodes[i]; 391 w = node.offsetWidth; 392 h = node.offsetHeight; 393 // shrink w/h proportionally if the img is wider than available width 394 if (w > docWidth) { 395 saveStyleProperty(node, "maxWidth", actionLog); 396 saveStyleProperty(node, "width", actionLog); 397 saveStyleProperty(node, "height", actionLog); 398 node.style.maxWidth = docWidth + "px"; 399 node.style.width = "100%"; 400 node.style.height = "auto"; 401 touched = true; 402 } 403 } 404 return touched; 405} 406 407function saveStyleProperty(node, property, actionLog) { 408 var savedName = "data-" + property; 409 node.setAttribute(savedName, node.style[property]); 410 actionLog.push([undoSetProperty, node, [property, savedName]]); 411} 412 413function undoSetProperty(property, savedProperty) { 414 this.style[property] = savedProperty ? this.getAttribute(savedProperty) : ""; 415} 416 417function shouldMungeTable(table) { 418 return table.hasAttribute("width") || table.style.width; 419} 420 421function hideAllUnsafeImages() { 422 hideUnsafeImages(document.getElementsByClassName("mail-message-content")); 423} 424 425function hideUnsafeImages(msgContentDivs) { 426 var i, msgContentCount; 427 var j, imgCount; 428 var msgContentDiv, image; 429 var images; 430 var showImages; 431 for (i = 0, msgContentCount = msgContentDivs.length; i < msgContentCount; i++) { 432 msgContentDiv = msgContentDivs[i]; 433 showImages = msgContentDiv.classList.contains("mail-show-images"); 434 435 images = msgContentDiv.getElementsByTagName("img"); 436 for (j = 0, imgCount = images.length; j < imgCount; j++) { 437 image = images[j]; 438 rewriteRelativeImageSrc(image); 439 attachImageLoadListener(image); 440 // TODO: handle inline image attachments for all supported protocols 441 if (!showImages) { 442 blockImage(image); 443 } 444 } 445 } 446} 447 448/** 449 * Changes relative paths to absolute path by pre-pending the account uri 450 * @param {Element} imgElement Image for which the src path will be updated. 451 */ 452function rewriteRelativeImageSrc(imgElement) { 453 var src = imgElement.src; 454 455 // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation 456 if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) { 457 // The conversation specifies a different base uri than the document 458 src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length); 459 imgElement.src = src; 460 } 461}; 462 463 464function attachImageLoadListener(imageElement) { 465 // Reset the src attribute to the empty string because onload will only fire if the src 466 // attribute is set after the onload listener. 467 var originalSrc = imageElement.src; 468 imageElement.src = ''; 469 imageElement.onload = imageOnLoad; 470 imageElement.src = originalSrc; 471} 472 473/** 474 * Handle an onload event for an <img> tag. 475 * The image could be within an elided-text block, or at the top level of a message. 476 * When a new image loads, its new bounds may affect message or elided-text geometry, 477 * so we need to inspect and adjust the enclosing element's zoom level where necessary. 478 * 479 * Because this method can be called really often, and zoom-level adjustment is slow, 480 * we collect the elements to be processed and do them all later in a single deferred pass. 481 */ 482function imageOnLoad(e) { 483 // normalize the quoted text parent if we're in a quoted text block, or else 484 // normalize the parent message content element 485 var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content"); 486 if (!parent) { 487 // sanity check. shouldn't really happen. 488 return; 489 } 490 491 // if there was no previous work, schedule a new deferred job 492 if (gImageLoadElements.length == 0) { 493 window.setTimeout(handleAllImageOnLoads, 0); 494 } 495 496 // enqueue the work if it wasn't already enqueued 497 if (gImageLoadElements.indexOf(parent) == -1) { 498 gImageLoadElements.push(parent); 499 } 500} 501 502// handle all deferred work from image onload events 503function handleAllImageOnLoads() { 504 normalizeElementWidths(gImageLoadElements); 505 measurePositions(); 506 // clear the queue so the next onload event starts a new job 507 gImageLoadElements = []; 508} 509 510function blockImage(imageElement) { 511 var src = imageElement.src; 512 if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 || 513 src.indexOf("content://") == 0) { 514 imageElement.setAttribute(BLOCKED_SRC_ATTR, src); 515 imageElement.src = "data:"; 516 } 517} 518 519function setWideViewport() { 520 var metaViewport = document.getElementById('meta-viewport'); 521 metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH); 522} 523 524function restoreScrollPosition() { 525 var scrollYPercent = window.mail.getScrollYPercent(); 526 if (scrollYPercent && document.body.offsetHeight > window.innerHeight) { 527 document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight); 528 } 529} 530 531function onContentReady(event) { 532 window.mail.onContentReady(); 533} 534 535function setupContentReady() { 536 var signalDiv; 537 538 // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER 539 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op 540 // animation that immediately runs on page load. The app uses this as a signal that the 541 // content is loaded and ready to draw, since WebView delays firing this event until the 542 // layers are composited and everything is ready to draw. 543 // 544 // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag. 545 if (ENABLE_CONTENT_READY) { 546 signalDiv = document.getElementById("initial-load-signal"); 547 signalDiv.addEventListener("webkitAnimationStart", onContentReady, false); 548 } 549} 550 551// BEGIN Java->JavaScript handlers 552function measurePositions() { 553 var overlayTops, overlayBottoms; 554 var i; 555 var len; 556 557 var expandedBody, headerSpacer; 558 var prevBodyBottom = 0; 559 var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 560 561 // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be 562 // trusted. 563 564 overlayTops = new Array(expandedBodyDivs.length + 1); 565 overlayBottoms = new Array(expandedBodyDivs.length + 1); 566 for (i = 0, len = expandedBodyDivs.length; i < len; i++) { 567 expandedBody = expandedBodyDivs[i]; 568 headerSpacer = expandedBody.previousElementSibling; 569 // addJavascriptInterface handler only supports string arrays 570 overlayTops[i] = "" + prevBodyBottom; 571 overlayBottoms[i] = "" + (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight); 572 prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top; 573 } 574 // add an extra one to mark the top/bottom of the last message footer spacer 575 overlayTops[i] = "" + prevBodyBottom; 576 overlayBottoms[i] = "" + document.body.offsetHeight; 577 578 window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms); 579} 580 581function unblockImages(messageDomIds) { 582 var i, j, images, imgCount, image, blockedSrc; 583 for (j = 0, len = messageDomIds.length; j < len; j++) { 584 var messageDomId = messageDomIds[j]; 585 var msg = document.getElementById(messageDomId); 586 if (!msg) { 587 console.log("can't unblock, no matching message for id: " + messageDomId); 588 continue; 589 } 590 images = msg.getElementsByTagName("img"); 591 for (i = 0, imgCount = images.length; i < imgCount; i++) { 592 image = images[i]; 593 blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR); 594 if (blockedSrc) { 595 image.src = blockedSrc; 596 image.removeAttribute(BLOCKED_SRC_ATTR); 597 } 598 } 599 } 600} 601 602function setConversationHeaderSpacerHeight(spacerHeight) { 603 var spacer = document.getElementById("conversation-header"); 604 if (!spacer) { 605 console.log("can't set spacer for conversation header"); 606 return; 607 } 608 spacer.style.height = spacerHeight + "px"; 609 measurePositions(); 610} 611 612function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) { 613 var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header"); 614 if (!spacer) { 615 console.log("can't set spacer for message with id: " + messageDomId); 616 return; 617 } 618 spacer.style.height = spacerHeight + "px"; 619 measurePositions(); 620} 621 622function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) { 623 var i, len; 624 var visibility = isVisible ? "block" : "none"; 625 var messageDiv = document.querySelector("#" + messageDomId); 626 var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible"); 627 if (!messageDiv || collapsibleDivs.length == 0) { 628 console.log("can't set body visibility for message with id: " + messageDomId); 629 return; 630 } 631 messageDiv.classList.toggle("expanded"); 632 for (i = 0, len = collapsibleDivs.length; i < len; i++) { 633 collapsibleDivs[i].style.display = visibility; 634 } 635 636 // revealing new content should trigger width normalization, since the initial render 637 // skips collapsed and super-collapsed messages 638 if (isVisible) { 639 normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content")); 640 } 641 642 setMessageHeaderSpacerHeight(messageDomId, spacerHeight); 643} 644 645function replaceSuperCollapsedBlock(startIndex) { 646 var parent, block, msg; 647 648 block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']"); 649 if (!block) { 650 console.log("can't expand super collapsed block at index: " + startIndex); 651 return; 652 } 653 parent = block.parentNode; 654 block.innerHTML = window.mail.getTempMessageBodies(); 655 656 // process the new block contents in one go before we pluck them out of the common ancestor 657 processQuotedText(block, false /* showElided */); 658 hideUnsafeImages(block.getElementsByClassName("mail-message-content")); 659 660 msg = block.firstChild; 661 while (msg) { 662 parent.insertBefore(msg, block); 663 msg = block.firstChild; 664 } 665 parent.removeChild(block); 666 measurePositions(); 667} 668 669function replaceMessageBodies(messageIds) { 670 var i; 671 var id; 672 var msgContentDiv; 673 674 for (i = 0, len = messageIds.length; i < len; i++) { 675 id = messageIds[i]; 676 msgContentDiv = document.querySelector("#" + id + " > .mail-message-content"); 677 msgContentDiv.innerHTML = window.mail.getMessageBody(id); 678 processQuotedText(msgContentDiv, true /* showElided */); 679 hideUnsafeImages([msgContentDiv]); 680 } 681 measurePositions(); 682} 683 684// handle the special case of adding a single new message at the end of a conversation 685function appendMessageHtml() { 686 var msg = document.createElement("div"); 687 msg.innerHTML = window.mail.getTempMessageBodies(); 688 msg = msg.children[0]; // toss the outer div, it was just to render innerHTML into 689 document.body.appendChild(msg); 690 processQuotedText(msg, true /* showElided */); 691 hideUnsafeImages(msg.getElementsByClassName("mail-message-content")); 692 measurePositions(); 693} 694 695function onScaleBegin(screenX, screenY) { 696// console.log("JS got scaleBegin x/y=" + screenX + "/" + screenY); 697 var focusX = screenX + document.body.scrollLeft; 698 var focusY = screenY + document.body.scrollTop; 699 var i, len; 700 var msgDivs = document.getElementsByClassName("mail-message"); 701 var msgDiv, msgBodyDiv; 702 var msgTop, msgDivTop, nextMsgTop; 703 var initialH; 704 var initialScale; 705 var scaledOriginX, scaledOriginY; 706 var translateX, translateY; 707 var origin; 708 709 gScaleInfo = undefined; 710 711 for (i = 0, len = msgDivs.length; i < len; i++) { 712 msgDiv = msgDivs[i]; 713 msgTop = nextMsgTop ? nextMsgTop : getTotalOffset(msgDiv).top; 714 nextMsgTop = (i < len-1) ? getTotalOffset(msgDivs[i+1]).top : document.body.offsetHeight; 715 if (focusY >= msgTop && focusY < nextMsgTop) { 716 msgBodyDiv = msgDiv.children[1]; 717 initialScale = msgBodyDiv.getAttribute("data-initial-scale") || 1.0; 718 719 msgDivTop = getTotalOffset(msgBodyDiv).top; 720 721 // TODO: correct only for no initial translation 722 // FIXME: wrong for initialScale > 1.0 723 scaledOriginX = focusX / initialScale; 724 scaledOriginY = (focusY - msgDivTop) / initialScale; 725 726 // TODO: is this still needed? 727 translateX = 0; 728 translateY = 0; 729 730 gScaleInfo = { 731 div: msgBodyDiv, 732 initialScale: initialScale, 733 initialScreenX: screenX, 734 initialScreenY: screenY, 735 originX: scaledOriginX, 736 originY: scaledOriginY, 737 translateX: translateX, 738 translateY: translateY, 739 initialH: getCachedValue(msgBodyDiv, "offsetHeight", "data-initial-height"), 740 minScale: Math.min(document.body.offsetWidth / msgBodyDiv.scrollWidth, 1.0), 741 currScale: initialScale, 742 currTranslateX: 0, 743 currTranslateY: 0 744 }; 745 746 origin = scaledOriginX + "px " + scaledOriginY + "px"; 747 msgBodyDiv.classList.add("zooming-focused"); 748 msgBodyDiv.style.webkitTransformOrigin = origin; 749 msgBodyDiv.style.webkitTransform = "scale3d(" + initialScale + "," + initialScale 750 + ",1) translate3d(" + translateX + "px," + translateY + "px,0)"; 751// console.log("scaleBegin, h=" + gScaleInfo.initialH + " origin='" + origin + "'"); 752 break; 753 } 754 } 755} 756 757function onScaleEnd(screenX, screenY) { 758 var msgBodyDiv; 759 var scale; 760 var h; 761 if (!gScaleInfo) { 762 return; 763 } 764 765// console.log("JS got scaleEnd x/y=" + screenX + "/" + screenY); 766 msgBodyDiv = gScaleInfo.div; 767 scale = gScaleInfo.currScale; 768 msgBodyDiv.style.webkitTransformOrigin = "0 0"; 769 // clear any translate 770 // switching to a 2D transform here re-renders the fonts more clearly, but introduces 771 // texture upload lag to any subsequent scale operation 772 // TODO: conditionalize this based on device GPU performance and/or body size/complexity? 773 if (true) { 774 msgBodyDiv.style.webkitTransform = "scale(" + gScaleInfo.currScale + ")"; 775 } else { 776 msgBodyDiv.style.webkitTransform = "scale3d(" + scale + "," + scale + ",1)"; 777 } 778 h = gScaleInfo.initialH * scale; 779// console.log("onScaleEnd set h=" + h); 780 msgBodyDiv.style.height = h + "px"; 781 782 // Use saved translateX/Y rather than calculating from screenX/Y because screenX/Y values 783 // from onScaleEnd only track focus of remaining pointers, which is not useful and leads 784 // to a perceived jump. 785 var deltaScrollX = (scale - 1) * gScaleInfo.originX - gScaleInfo.currTranslateX; 786 var deltaScrollY = (scale - 1) * gScaleInfo.originY - gScaleInfo.currTranslateY; 787// console.log("JS adjusting scroll by x/y=" + deltaScrollX + "/" + deltaScrollY); 788 789 msgBodyDiv.classList.remove("zooming-focused"); 790 msgBodyDiv.setAttribute("data-initial-scale", scale); 791 792 // TODO: is there a better way to make this more reliable? 793 window.setTimeout(function() { 794 window.scrollBy(deltaScrollX, deltaScrollY); 795 }, 10); 796} 797 798function onScale(relativeScale, screenX, screenY) { 799 var scale; 800 var translateX, translateY; 801 var transform; 802 803 if (!gScaleInfo) { 804 return; 805 } 806 807 scale = Math.max(gScaleInfo.initialScale * relativeScale, gScaleInfo.minScale); 808 if (scale > 4.0) { 809 scale = 4.0; 810 } 811 translateX = screenX - gScaleInfo.initialScreenX; 812 translateY = screenY - gScaleInfo.initialScreenY; 813 // TODO: clamp translation to prevent going beyond body edges 814 gScaleInfo.currScale = scale; 815 gScaleInfo.currTranslateX = translateX; 816 gScaleInfo.currTranslateY = translateY; 817 transform = "translate3d(" + translateX + "px," + translateY + "px,0) scale3d(" 818 + scale + "," + scale + ",1) translate3d(" + gScaleInfo.translateX + "px," 819 + gScaleInfo.translateY + "px,0)"; 820 gScaleInfo.div.style.webkitTransform = transform; 821// console.log("JS got scale=" + scale + " x/y=" + screenX + "/" + screenY 822// + " transform='" + transform + "'"); 823} 824 825// END Java->JavaScript handlers 826 827// Do this first to ensure that the readiness signal comes through, 828// even if a stray exception later occurs. 829setupContentReady(); 830 831collapseAllQuotedText(); 832hideAllUnsafeImages(); 833normalizeAllMessageWidths(); 834//setWideViewport(); 835restoreScrollPosition(); 836measurePositions(); 837 838