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 18 var 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 22 var gImageLoadElements = []; 23 24 var 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 */ 31 var TRANSFORM_MINIMUM_EFFECTIVE_RATIO = 0.7; 32 33 // Don't ship with this on. 34 var DEBUG_DISPLAY_TRANSFORMS = false; 35 36 var 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 */ 45 function 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 */ 65 function 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 76 function 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 87 function onToggleClick(e) { 88 toggleQuotedText(e.target); 89 measurePositions(); 90 } 91 92 function 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 105 function collapseAllQuotedText() { 106 processQuotedText(document.documentElement, false /* showElided */); 107 } 108 109 function 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 127 function 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 145 function 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 */ 174 function 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 198 function 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 var beforeWidth; 216 var tmpActionLog = []; 217 if (elWidth <= docWidth) { 218 return; 219 } 220 221 start = Date.now(); 222 223 if (el.parentElement.classList.contains("mail-message")) { 224 msgId = el.parentElement.id; 225 transformText = "[origW=" + elWidth + "/" + docWidth; 226 } 227 228 // Try munging all divs or textareas with inline styles where the width 229 // is wider than docWidth, and change it to be a max-width. 230 touched = false; 231 nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("div[style], textarea[style]") : []; 232 touched = transformBlockElements(nodes, docWidth, actionLog); 233 if (touched) { 234 newWidth = el.scrollWidth; 235 console.log("ran div-width munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 236 + " docW=" + docWidth); 237 if (msgId) { 238 transformText += " DIV:newW=" + newWidth; 239 } 240 if (newWidth <= docWidth) { 241 done = true; 242 } 243 } 244 245 if (!done) { 246 // OK, that wasn't enough. Find images with widths and override their widths. 247 nodes = ENABLE_MUNGE_IMAGES ? el.querySelectorAll("img") : []; 248 touched = transformImages(nodes, docWidth, actionLog); 249 if (touched) { 250 newWidth = el.scrollWidth; 251 console.log("ran img munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 252 + " docW=" + docWidth); 253 if (msgId) { 254 transformText += " IMG:newW=" + newWidth; 255 } 256 if (newWidth <= docWidth) { 257 done = true; 258 } 259 } 260 } 261 262 if (!done) { 263 // OK, that wasn't enough. Find tables with widths and override their widths. 264 // Also ensure that any use of 'table-layout: fixed' is negated, since using 265 // that with 'width: auto' causes erratic table width. 266 nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("table") : []; 267 touched = addClassToElements(nodes, shouldMungeTable, "munged", 268 actionLog); 269 if (touched) { 270 newWidth = el.scrollWidth; 271 console.log("ran table munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 272 + " docW=" + docWidth); 273 if (msgId) { 274 transformText += " TABLE:newW=" + newWidth; 275 } 276 if (newWidth <= docWidth) { 277 done = true; 278 } 279 } 280 } 281 282 if (!done) { 283 // OK, that wasn't enough. Try munging all <td> to override any width and nowrap set. 284 beforeWidth = newWidth; 285 nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("td") : []; 286 touched = addClassToElements(nodes, null /* mungeAll */, "munged", 287 tmpActionLog); 288 if (touched) { 289 newWidth = el.scrollWidth; 290 console.log("ran td munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth 291 + " docW=" + docWidth); 292 if (msgId) { 293 transformText += " TD:newW=" + newWidth; 294 } 295 if (newWidth <= docWidth) { 296 done = true; 297 } else if (newWidth == beforeWidth) { 298 // this transform did not improve things, and it is somewhat risky. 299 // back it out, since it's the last transform and we gained nothing. 300 undoActions(tmpActionLog); 301 } else { 302 // the transform WAS effective (although not 100%) 303 // copy the temporary action log entries over as normal 304 for (i = 0, len = tmpActionLog.length; i < len; i++) { 305 actionLog.push(tmpActionLog[i]); 306 } 307 } 308 } 309 } 310 311 // If the transformations shrank the width significantly enough, leave them in place. 312 // We figure that in those cases, the benefits outweight the risk of rendering artifacts. 313 if (!done && (elWidth - newWidth) / (elWidth - docWidth) > 314 TRANSFORM_MINIMUM_EFFECTIVE_RATIO) { 315 console.log("transform(s) deemed effective enough"); 316 done = true; 317 } 318 319 if (done) { 320 if (msgId) { 321 transformText += "]"; 322 existingText = gTransformText[msgId]; 323 if (!existingText) { 324 transformText = "Message transforms: " + transformText; 325 } else { 326 transformText = existingText + " " + transformText; 327 } 328 gTransformText[msgId] = transformText; 329 window.mail.onMessageTransform(msgId, transformText); 330 if (DEBUG_DISPLAY_TRANSFORMS) { 331 textElement = el.firstChild; 332 if (!textElement.classList || !textElement.classList.contains("transform-text")) { 333 textElement = document.createElement("div"); 334 textElement.classList.add("transform-text"); 335 textElement.style.fontSize = "10px"; 336 textElement.style.color = "#ccc"; 337 el.insertBefore(textElement, el.firstChild); 338 } 339 textElement.innerHTML = transformText + "<br>"; 340 } 341 } 342 console.log("munger(s) succeeded, elapsed time=" + (Date.now() - start)); 343 return; 344 } 345 346 // reverse all changes if the width is STILL not narrow enough 347 // (except the width->maxWidth change, which is not particularly destructive) 348 undoActions(actionLog); 349 if (actionLog.length > 0) { 350 console.log("all mungers failed, changes reversed. elapsed time=" + (Date.now() - start)); 351 } 352 } 353 354 function undoActions(actionLog) { 355 for (i = 0, len = actionLog.length; i < len; i++) { 356 actionLog[i][0].apply(actionLog[i][1], actionLog[i][2]); 357 } 358 } 359 360 function addClassToElements(nodes, conditionFn, classToAdd, actionLog) { 361 var i, len; 362 var node; 363 var added = false; 364 for (i = 0, len = nodes.length; i < len; i++) { 365 node = nodes[i]; 366 if (!conditionFn || conditionFn(node)) { 367 if (node.classList.contains(classToAdd)) { 368 continue; 369 } 370 node.classList.add(classToAdd); 371 added = true; 372 actionLog.push([node.classList.remove, node.classList, [classToAdd]]); 373 } 374 } 375 return added; 376 } 377 378 function transformBlockElements(nodes, docWidth, actionLog) { 379 var i, len; 380 var node; 381 var wStr; 382 var index; 383 var touched = false; 384 385 for (i = 0, len = nodes.length; i < len; i++) { 386 node = nodes[i]; 387 wStr = node.style.width || node.style.minWidth; 388 index = wStr ? wStr.indexOf("px") : -1; 389 if (index >= 0 && wStr.slice(0, index) > docWidth) { 390 saveStyleProperty(node, "width", actionLog); 391 saveStyleProperty(node, "minWidth", actionLog); 392 saveStyleProperty(node, "maxWidth", actionLog); 393 node.style.width = "100%"; 394 node.style.minWidth = ""; 395 node.style.maxWidth = wStr; 396 touched = true; 397 } 398 } 399 return touched; 400 } 401 402 function transformImages(nodes, docWidth, actionLog) { 403 var i, len; 404 var node; 405 var w, h; 406 var touched = false; 407 408 for (i = 0, len = nodes.length; i < len; i++) { 409 node = nodes[i]; 410 w = node.offsetWidth; 411 h = node.offsetHeight; 412 // shrink w/h proportionally if the img is wider than available width 413 if (w > docWidth) { 414 saveStyleProperty(node, "maxWidth", actionLog); 415 saveStyleProperty(node, "width", actionLog); 416 saveStyleProperty(node, "height", actionLog); 417 node.style.maxWidth = docWidth + "px"; 418 node.style.width = "100%"; 419 node.style.height = "auto"; 420 touched = true; 421 } 422 } 423 return touched; 424 } 425 426 function saveStyleProperty(node, property, actionLog) { 427 var savedName = "data-" + property; 428 node.setAttribute(savedName, node.style[property]); 429 actionLog.push([undoSetProperty, node, [property, savedName]]); 430 } 431 432 function undoSetProperty(property, savedProperty) { 433 this.style[property] = savedProperty ? this.getAttribute(savedProperty) : ""; 434 } 435 436 function shouldMungeTable(table) { 437 return table.hasAttribute("width") || table.style.width; 438 } 439 440 function hideAllUnsafeImages() { 441 hideUnsafeImages(document.getElementsByClassName("mail-message-content")); 442 } 443 444 function hideUnsafeImages(msgContentDivs) { 445 var i, msgContentCount; 446 var j, imgCount; 447 var msgContentDiv, image; 448 var images; 449 var showImages; 450 for (i = 0, msgContentCount = msgContentDivs.length; i < msgContentCount; i++) { 451 msgContentDiv = msgContentDivs[i]; 452 showImages = msgContentDiv.classList.contains("mail-show-images"); 453 454 images = msgContentDiv.getElementsByTagName("img"); 455 for (j = 0, imgCount = images.length; j < imgCount; j++) { 456 image = images[j]; 457 rewriteRelativeImageSrc(image); 458 attachImageLoadListener(image); 459 // TODO: handle inline image attachments for all supported protocols 460 if (!showImages) { 461 blockImage(image); 462 } 463 } 464 } 465 } 466 467 /** 468 * Changes relative paths to absolute path by pre-pending the account uri 469 * @param {Element} imgElement Image for which the src path will be updated. 470 */ 471 function rewriteRelativeImageSrc(imgElement) { 472 var src = imgElement.src; 473 474 // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation 475 if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) { 476 // The conversation specifies a different base uri than the document 477 src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length); 478 imgElement.src = src; 479 } 480 }; 481 482 483 function attachImageLoadListener(imageElement) { 484 // Reset the src attribute to the empty string because onload will only fire if the src 485 // attribute is set after the onload listener. 486 var originalSrc = imageElement.src; 487 imageElement.src = ''; 488 imageElement.onload = imageOnLoad; 489 imageElement.src = originalSrc; 490 } 491 492 /** 493 * Handle an onload event for an <img> tag. 494 * The image could be within an elided-text block, or at the top level of a message. 495 * When a new image loads, its new bounds may affect message or elided-text geometry, 496 * so we need to inspect and adjust the enclosing element's zoom level where necessary. 497 * 498 * Because this method can be called really often, and zoom-level adjustment is slow, 499 * we collect the elements to be processed and do them all later in a single deferred pass. 500 */ 501 function imageOnLoad(e) { 502 // normalize the quoted text parent if we're in a quoted text block, or else 503 // normalize the parent message content element 504 var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content"); 505 if (!parent) { 506 // sanity check. shouldn't really happen. 507 return; 508 } 509 510 // if there was no previous work, schedule a new deferred job 511 if (gImageLoadElements.length == 0) { 512 window.setTimeout(handleAllImageOnLoads, 0); 513 } 514 515 // enqueue the work if it wasn't already enqueued 516 if (gImageLoadElements.indexOf(parent) == -1) { 517 gImageLoadElements.push(parent); 518 } 519 } 520 521 // handle all deferred work from image onload events 522 function handleAllImageOnLoads() { 523 normalizeElementWidths(gImageLoadElements); 524 measurePositions(); 525 // clear the queue so the next onload event starts a new job 526 gImageLoadElements = []; 527 } 528 529 function blockImage(imageElement) { 530 var src = imageElement.src; 531 if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 || 532 src.indexOf("content://") == 0) { 533 imageElement.setAttribute(BLOCKED_SRC_ATTR, src); 534 imageElement.src = "data:"; 535 } 536 } 537 538 function setWideViewport() { 539 var metaViewport = document.getElementById('meta-viewport'); 540 metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH); 541 } 542 543 function restoreScrollPosition() { 544 var scrollYPercent = window.mail.getScrollYPercent(); 545 if (scrollYPercent && document.body.offsetHeight > window.innerHeight) { 546 document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight); 547 } 548 } 549 550 function onContentReady(event) { 551 window.mail.onContentReady(); 552 } 553 554 function setupContentReady() { 555 var signalDiv; 556 557 // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER 558 // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op 559 // animation that immediately runs on page load. The app uses this as a signal that the 560 // content is loaded and ready to draw, since WebView delays firing this event until the 561 // layers are composited and everything is ready to draw. 562 // 563 // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag. 564 if (ENABLE_CONTENT_READY) { 565 signalDiv = document.getElementById("initial-load-signal"); 566 signalDiv.addEventListener("webkitAnimationStart", onContentReady, false); 567 } 568 } 569 570 // BEGIN Java->JavaScript handlers 571 function measurePositions() { 572 var overlayTops, overlayBottoms; 573 var i; 574 var len; 575 576 var expandedBody, headerSpacer; 577 var prevBodyBottom = 0; 578 var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content"); 579 580 // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be 581 // trusted. 582 583 overlayTops = new Array(expandedBodyDivs.length + 1); 584 overlayBottoms = new Array(expandedBodyDivs.length + 1); 585 for (i = 0, len = expandedBodyDivs.length; i < len; i++) { 586 expandedBody = expandedBodyDivs[i]; 587 headerSpacer = expandedBody.previousElementSibling; 588 // addJavascriptInterface handler only supports string arrays 589 overlayTops[i] = "" + prevBodyBottom; 590 overlayBottoms[i] = "" + (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight); 591 prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top; 592 } 593 // add an extra one to mark the top/bottom of the last message footer spacer 594 overlayTops[i] = "" + prevBodyBottom; 595 overlayBottoms[i] = "" + document.documentElement.scrollHeight; 596 597 window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms); 598 } 599 600 function unblockImages(messageDomIds) { 601 var i, j, images, imgCount, image, blockedSrc; 602 for (j = 0, len = messageDomIds.length; j < len; j++) { 603 var messageDomId = messageDomIds[j]; 604 var msg = document.getElementById(messageDomId); 605 if (!msg) { 606 console.log("can't unblock, no matching message for id: " + messageDomId); 607 continue; 608 } 609 images = msg.getElementsByTagName("img"); 610 for (i = 0, imgCount = images.length; i < imgCount; i++) { 611 image = images[i]; 612 blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR); 613 if (blockedSrc) { 614 image.src = blockedSrc; 615 image.removeAttribute(BLOCKED_SRC_ATTR); 616 } 617 } 618 } 619 } 620 621 function setConversationHeaderSpacerHeight(spacerHeight) { 622 var spacer = document.getElementById("conversation-header"); 623 if (!spacer) { 624 console.log("can't set spacer for conversation header"); 625 return; 626 } 627 spacer.style.height = spacerHeight + "px"; 628 measurePositions(); 629 } 630 631 function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) { 632 var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header"); 633 if (!spacer) { 634 console.log("can't set spacer for message with id: " + messageDomId); 635 return; 636 } 637 spacer.style.height = spacerHeight + "px"; 638 measurePositions(); 639 } 640 641 function setMessageBodyVisible(messageDomId, isVisible, spacerHeight, 642 topBorderHeight, bottomBorderHeight) { 643 var i, len; 644 var visibility = isVisible ? "block" : "none"; 645 var messageDiv = document.querySelector("#" + messageDomId); 646 var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible"); 647 if (!messageDiv || collapsibleDivs.length == 0) { 648 console.log("can't set body visibility for message with id: " + messageDomId); 649 return; 650 } 651 652 // if the top border has changed, update the height of its spacer 653 if (topBorderHeight > 0) { 654 var border = messageDiv.previousElementSibling; 655 if (!border) { 656 console.log("can't set spacer for top border"); 657 return; 658 } 659 border.style.height = topBorderHeight + "px"; 660 } 661 662 // if the bottom border has changed, update the height of its spacer 663 if (bottomBorderHeight > 0) { 664 var border = messageDiv.nextElementSibling; 665 if (!border) { 666 console.log("can't set spacer for bottom border"); 667 return; 668 } 669 border.style.height = bottomBorderHeight + "px"; 670 } 671 672 messageDiv.classList.toggle("expanded"); 673 for (i = 0, len = collapsibleDivs.length; i < len; i++) { 674 collapsibleDivs[i].style.display = visibility; 675 } 676 677 // revealing new content should trigger width normalization, since the initial render 678 // skips collapsed and super-collapsed messages 679 if (isVisible) { 680 normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content")); 681 } 682 683 setMessageHeaderSpacerHeight(messageDomId, spacerHeight); 684 } 685 686 function replaceSuperCollapsedBlock(startIndex) { 687 var parent, block, msg; 688 689 block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']"); 690 if (!block) { 691 console.log("can't expand super collapsed block at index: " + startIndex); 692 return; 693 } 694 parent = block.parentNode; 695 block.innerHTML = window.mail.getTempMessageBodies(); 696 697 // process the new block contents in one go before we pluck them out of the common ancestor 698 processQuotedText(block, false /* showElided */); 699 hideUnsafeImages(block.getElementsByClassName("mail-message-content")); 700 701 msg = block.firstChild; 702 while (msg) { 703 parent.insertBefore(msg, block); 704 msg = block.firstChild; 705 } 706 parent.removeChild(block); 707 measurePositions(); 708 } 709 710 function replaceMessageBodies(messageIds) { 711 var i; 712 var id; 713 var msgContentDiv; 714 715 for (i = 0, len = messageIds.length; i < len; i++) { 716 id = messageIds[i]; 717 msgContentDiv = document.querySelector("#" + id + " > .mail-message-content"); 718 msgContentDiv.innerHTML = window.mail.getMessageBody(id); 719 processQuotedText(msgContentDiv, true /* showElided */); 720 hideUnsafeImages([msgContentDiv]); 721 } 722 measurePositions(); 723 } 724 725 // handle the special case of adding a single new message at the end of a conversation 726 function appendMessageHtml() { 727 var msg = document.createElement("div"); 728 msg.innerHTML = window.mail.getTempMessageBodies(); 729 var body = msg.children[0]; // toss the outer div, it was just to render innerHTML into 730 var border = msg.children[1]; // get the border spacer as well 731 document.body.appendChild(body); 732 document.body.appendChild(border); 733 processQuotedText(msg, true /* showElided */); 734 hideUnsafeImages(msg.getElementsByClassName("mail-message-content")); 735 measurePositions(); 736 } 737 738 function onScaleBegin(screenX, screenY) { 739 // console.log("JS got scaleBegin x/y=" + screenX + "/" + screenY); 740 var focusX = screenX + document.body.scrollLeft; 741 var focusY = screenY + document.body.scrollTop; 742 var i, len; 743 var msgDivs = document.getElementsByClassName("mail-message"); 744 var msgDiv, msgBodyDiv; 745 var msgTop, msgDivTop, nextMsgTop; 746 var initialH; 747 var initialScale; 748 var scaledOriginX, scaledOriginY; 749 var translateX, translateY; 750 var origin; 751 752 gScaleInfo = undefined; 753 754 for (i = 0, len = msgDivs.length; i < len; i++) { 755 msgDiv = msgDivs[i]; 756 msgTop = nextMsgTop ? nextMsgTop : getTotalOffset(msgDiv).top; 757 nextMsgTop = (i < len-1) ? getTotalOffset(msgDivs[i+1]).top : document.body.offsetHeight; 758 if (focusY >= msgTop && focusY < nextMsgTop) { 759 msgBodyDiv = msgDiv.children[1]; 760 initialScale = msgBodyDiv.getAttribute("data-initial-scale") || 1.0; 761 762 msgDivTop = getTotalOffset(msgBodyDiv).top; 763 764 // TODO: correct only for no initial translation 765 // FIXME: wrong for initialScale > 1.0 766 scaledOriginX = focusX / initialScale; 767 scaledOriginY = (focusY - msgDivTop) / initialScale; 768 769 // TODO: is this still needed? 770 translateX = 0; 771 translateY = 0; 772 773 gScaleInfo = { 774 div: msgBodyDiv, 775 initialScale: initialScale, 776 initialScreenX: screenX, 777 initialScreenY: screenY, 778 originX: scaledOriginX, 779 originY: scaledOriginY, 780 translateX: translateX, 781 translateY: translateY, 782 initialH: getCachedValue(msgBodyDiv, "offsetHeight", "data-initial-height"), 783 minScale: Math.min(document.body.offsetWidth / msgBodyDiv.scrollWidth, 1.0), 784 currScale: initialScale, 785 currTranslateX: 0, 786 currTranslateY: 0 787 }; 788 789 origin = scaledOriginX + "px " + scaledOriginY + "px"; 790 msgBodyDiv.classList.add("zooming-focused"); 791 msgBodyDiv.style.webkitTransformOrigin = origin; 792 msgBodyDiv.style.webkitTransform = "scale3d(" + initialScale + "," + initialScale 793 + ",1) translate3d(" + translateX + "px," + translateY + "px,0)"; 794 // console.log("scaleBegin, h=" + gScaleInfo.initialH + " origin='" + origin + "'"); 795 break; 796 } 797 } 798 } 799 800 function onScaleEnd(screenX, screenY) { 801 var msgBodyDiv; 802 var scale; 803 var h; 804 if (!gScaleInfo) { 805 return; 806 } 807 808 // console.log("JS got scaleEnd x/y=" + screenX + "/" + screenY); 809 msgBodyDiv = gScaleInfo.div; 810 scale = gScaleInfo.currScale; 811 msgBodyDiv.style.webkitTransformOrigin = "0 0"; 812 // clear any translate 813 // switching to a 2D transform here re-renders the fonts more clearly, but introduces 814 // texture upload lag to any subsequent scale operation 815 // TODO: conditionalize this based on device GPU performance and/or body size/complexity? 816 if (true) { 817 msgBodyDiv.style.webkitTransform = "scale(" + gScaleInfo.currScale + ")"; 818 } else { 819 msgBodyDiv.style.webkitTransform = "scale3d(" + scale + "," + scale + ",1)"; 820 } 821 h = gScaleInfo.initialH * scale; 822 // console.log("onScaleEnd set h=" + h); 823 msgBodyDiv.style.height = h + "px"; 824 825 // Use saved translateX/Y rather than calculating from screenX/Y because screenX/Y values 826 // from onScaleEnd only track focus of remaining pointers, which is not useful and leads 827 // to a perceived jump. 828 var deltaScrollX = (scale - 1) * gScaleInfo.originX - gScaleInfo.currTranslateX; 829 var deltaScrollY = (scale - 1) * gScaleInfo.originY - gScaleInfo.currTranslateY; 830 // console.log("JS adjusting scroll by x/y=" + deltaScrollX + "/" + deltaScrollY); 831 832 msgBodyDiv.classList.remove("zooming-focused"); 833 msgBodyDiv.setAttribute("data-initial-scale", scale); 834 835 // TODO: is there a better way to make this more reliable? 836 window.setTimeout(function() { 837 window.scrollBy(deltaScrollX, deltaScrollY); 838 }, 10); 839 } 840 841 function onScale(relativeScale, screenX, screenY) { 842 var scale; 843 var translateX, translateY; 844 var transform; 845 846 if (!gScaleInfo) { 847 return; 848 } 849 850 scale = Math.max(gScaleInfo.initialScale * relativeScale, gScaleInfo.minScale); 851 if (scale > 4.0) { 852 scale = 4.0; 853 } 854 translateX = screenX - gScaleInfo.initialScreenX; 855 translateY = screenY - gScaleInfo.initialScreenY; 856 // TODO: clamp translation to prevent going beyond body edges 857 gScaleInfo.currScale = scale; 858 gScaleInfo.currTranslateX = translateX; 859 gScaleInfo.currTranslateY = translateY; 860 transform = "translate3d(" + translateX + "px," + translateY + "px,0) scale3d(" 861 + scale + "," + scale + ",1) translate3d(" + gScaleInfo.translateX + "px," 862 + gScaleInfo.translateY + "px,0)"; 863 gScaleInfo.div.style.webkitTransform = transform; 864 // console.log("JS got scale=" + scale + " x/y=" + screenX + "/" + screenY 865 // + " transform='" + transform + "'"); 866 } 867 868 // END Java->JavaScript handlers 869 870 // Do this first to ensure that the readiness signal comes through, 871 // even if a stray exception later occurs. 872 setupContentReady(); 873 874 collapseAllQuotedText(); 875 hideAllUnsafeImages(); 876 normalizeAllMessageWidths(); 877 //setWideViewport(); 878 restoreScrollPosition(); 879 measurePositions(); 880 881