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