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