Home | History | Annotate | Download | only in assets
      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.setAttribute("dir", "auto");
    119         toggleElement.onclick = onToggleClick;
    120         elidedElement.style.display = 'none';
    121         elidedElement.parentNode.insertBefore(toggleElement, elidedElement);
    122         if (showElided) {
    123             toggleQuotedText(toggleElement);
    124         }
    125     }
    126 }
    127 
    128 function isConversationEmpty(bodyDivs) {
    129     var i, len;
    130     var msgBody;
    131     var text;
    132 
    133     // Check if given divs are empty (in appearance), and disable zoom if so.
    134     for (i = 0, len = bodyDivs.length; i < len; i++) {
    135         msgBody = bodyDivs[i];
    136         // use 'textContent' to exclude markup when determining whether bodies are empty
    137         // (fall back to more expensive 'innerText' if 'textContent' isn't implemented)
    138         text = msgBody.textContent || msgBody.innerText;
    139         if (text.trim().length > 0) {
    140             return false;
    141         }
    142     }
    143     return true;
    144 }
    145 
    146 function normalizeAllMessageWidths() {
    147     var expandedBodyDivs;
    148     var metaViewport;
    149     var contentValues;
    150     var isEmpty;
    151 
    152     expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content");
    153 
    154     isEmpty = isConversationEmpty(expandedBodyDivs);
    155 
    156     normalizeElementWidths(expandedBodyDivs);
    157 
    158     // assemble a working <meta> viewport "content" value from the base value in the
    159     // document, plus any dynamically determined options
    160     metaViewport = document.getElementById("meta-viewport");
    161     contentValues = [metaViewport.getAttribute("content")];
    162     if (isEmpty) {
    163         contentValues.push(metaViewport.getAttribute("data-zoom-off"));
    164     } else {
    165         contentValues.push(metaViewport.getAttribute("data-zoom-on"));
    166     }
    167     metaViewport.setAttribute("content", contentValues.join(","));
    168 }
    169 
    170 /*
    171  * Normalizes the width of all elements supplied to the document body's overall width.
    172  * Narrower elements are zoomed in, and wider elements are zoomed out.
    173  * This method is idempotent.
    174  */
    175 function normalizeElementWidths(elements) {
    176     var i;
    177     var el;
    178     var documentWidth;
    179     var goalWidth;
    180     var origWidth;
    181     var newZoom, oldZoom;
    182     var outerZoom;
    183     var outerDiv;
    184 
    185     documentWidth = document.body.offsetWidth;
    186     goalWidth = WEBVIEW_WIDTH;
    187 
    188     for (i = 0; i < elements.length; i++) {
    189         el = elements[i];
    190         oldZoom = el.style.zoom;
    191         // reset any existing normalization
    192         if (oldZoom) {
    193             el.style.zoom = 1;
    194         }
    195         origWidth = el.style.width;
    196         el.style.width = goalWidth + "px";
    197         transformContent(el, goalWidth, el.scrollWidth);
    198         newZoom = documentWidth / el.scrollWidth;
    199         if (NORMALIZE_MESSAGE_WIDTHS) {
    200             if (el.classList.contains("mail-message-content")) {
    201                 outerZoom = 1;
    202             } else {
    203                 outerDiv = up(el, "mail-message-content");
    204                 outerZoom = outerDiv ? outerDiv.style.zoom : 1;
    205             }
    206             el.style.zoom = newZoom / outerZoom;
    207         }
    208         el.style.width = origWidth;
    209     }
    210 }
    211 
    212 function transformContent(el, docWidth, elWidth) {
    213     var nodes;
    214     var i, len;
    215     var index;
    216     var newWidth = elWidth;
    217     var wStr;
    218     var touched;
    219     // the format of entries in this array is:
    220     // entry := [ undoFunction, undoFunctionThis, undoFunctionParamArray ]
    221     var actionLog = [];
    222     var node;
    223     var done = false;
    224     var msgId;
    225     var transformText;
    226     var existingText;
    227     var textElement;
    228     var start;
    229     var beforeWidth;
    230     var tmpActionLog = [];
    231     if (elWidth <= docWidth) {
    232         return;
    233     }
    234 
    235     start = Date.now();
    236 
    237     if (el.parentElement.classList.contains("mail-message")) {
    238         msgId = el.parentElement.id;
    239         transformText = "[origW=" + elWidth + "/" + docWidth;
    240     }
    241 
    242     // Try munging all divs or textareas with inline styles where the width
    243     // is wider than docWidth, and change it to be a max-width.
    244     touched = false;
    245     nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("div[style], textarea[style]") : [];
    246     touched = transformBlockElements(nodes, docWidth, actionLog);
    247     if (touched) {
    248         newWidth = el.scrollWidth;
    249         console.log("ran div-width munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
    250             + " docW=" + docWidth);
    251         if (msgId) {
    252             transformText += " DIV:newW=" + newWidth;
    253         }
    254         if (newWidth <= docWidth) {
    255             done = true;
    256         }
    257     }
    258 
    259     if (!done) {
    260         // OK, that wasn't enough. Find images with widths and override their widths.
    261         nodes = ENABLE_MUNGE_IMAGES ? el.querySelectorAll("img") : [];
    262         touched = transformImages(nodes, docWidth, actionLog);
    263         if (touched) {
    264             newWidth = el.scrollWidth;
    265             console.log("ran img munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
    266                 + " docW=" + docWidth);
    267             if (msgId) {
    268                 transformText += " IMG:newW=" + newWidth;
    269             }
    270             if (newWidth <= docWidth) {
    271                 done = true;
    272             }
    273         }
    274     }
    275 
    276     if (!done) {
    277         // OK, that wasn't enough. Find tables with widths and override their widths.
    278         // Also ensure that any use of 'table-layout: fixed' is negated, since using
    279         // that with 'width: auto' causes erratic table width.
    280         nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("table") : [];
    281         touched = addClassToElements(nodes, shouldMungeTable, "munged",
    282             actionLog);
    283         if (touched) {
    284             newWidth = el.scrollWidth;
    285             console.log("ran table munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
    286                 + " docW=" + docWidth);
    287             if (msgId) {
    288                 transformText += " TABLE:newW=" + newWidth;
    289             }
    290             if (newWidth <= docWidth) {
    291                 done = true;
    292             }
    293         }
    294     }
    295 
    296     if (!done) {
    297         // OK, that wasn't enough. Try munging all <td> to override any width and nowrap set.
    298         beforeWidth = newWidth;
    299         nodes = ENABLE_MUNGE_TABLES ? el.querySelectorAll("td") : [];
    300         touched = addClassToElements(nodes, null /* mungeAll */, "munged",
    301             tmpActionLog);
    302         if (touched) {
    303             newWidth = el.scrollWidth;
    304             console.log("ran td munger on el=" + el + " oldW=" + elWidth + " newW=" + newWidth
    305                 + " docW=" + docWidth);
    306             if (msgId) {
    307                 transformText += " TD:newW=" + newWidth;
    308             }
    309             if (newWidth <= docWidth) {
    310                 done = true;
    311             } else if (newWidth == beforeWidth) {
    312                 // this transform did not improve things, and it is somewhat risky.
    313                 // back it out, since it's the last transform and we gained nothing.
    314                 undoActions(tmpActionLog);
    315             } else {
    316                 // the transform WAS effective (although not 100%)
    317                 // copy the temporary action log entries over as normal
    318                 for (i = 0, len = tmpActionLog.length; i < len; i++) {
    319                     actionLog.push(tmpActionLog[i]);
    320                 }
    321             }
    322         }
    323     }
    324 
    325     // If the transformations shrank the width significantly enough, leave them in place.
    326     // We figure that in those cases, the benefits outweight the risk of rendering artifacts.
    327     if (!done && (elWidth - newWidth) / (elWidth - docWidth) >
    328             TRANSFORM_MINIMUM_EFFECTIVE_RATIO) {
    329         console.log("transform(s) deemed effective enough");
    330         done = true;
    331     }
    332 
    333     if (done) {
    334         if (msgId) {
    335             transformText += "]";
    336             existingText = gTransformText[msgId];
    337             if (!existingText) {
    338                 transformText = "Message transforms: " + transformText;
    339             } else {
    340                 transformText = existingText + " " + transformText;
    341             }
    342             gTransformText[msgId] = transformText;
    343             window.mail.onMessageTransform(msgId, transformText);
    344             if (DEBUG_DISPLAY_TRANSFORMS) {
    345                 textElement = el.firstChild;
    346                 if (!textElement.classList || !textElement.classList.contains("transform-text")) {
    347                     textElement = document.createElement("div");
    348                     textElement.classList.add("transform-text");
    349                     textElement.style.fontSize = "10px";
    350                     textElement.style.color = "#ccc";
    351                     el.insertBefore(textElement, el.firstChild);
    352                 }
    353                 textElement.innerHTML = transformText + "<br>";
    354             }
    355         }
    356         console.log("munger(s) succeeded, elapsed time=" + (Date.now() - start));
    357         return;
    358     }
    359 
    360     // reverse all changes if the width is STILL not narrow enough
    361     // (except the width->maxWidth change, which is not particularly destructive)
    362     undoActions(actionLog);
    363     if (actionLog.length > 0) {
    364         console.log("all mungers failed, changes reversed. elapsed time=" + (Date.now() - start));
    365     }
    366 }
    367 
    368 function undoActions(actionLog) {
    369     for (i = 0, len = actionLog.length; i < len; i++) {
    370         actionLog[i][0].apply(actionLog[i][1], actionLog[i][2]);
    371     }
    372 }
    373 
    374 function addClassToElements(nodes, conditionFn, classToAdd, actionLog) {
    375     var i, len;
    376     var node;
    377     var added = false;
    378     for (i = 0, len = nodes.length; i < len; i++) {
    379         node = nodes[i];
    380         if (!conditionFn || conditionFn(node)) {
    381             if (node.classList.contains(classToAdd)) {
    382                 continue;
    383             }
    384             node.classList.add(classToAdd);
    385             added = true;
    386             actionLog.push([node.classList.remove, node.classList, [classToAdd]]);
    387         }
    388     }
    389     return added;
    390 }
    391 
    392 function transformBlockElements(nodes, docWidth, actionLog) {
    393     var i, len;
    394     var node;
    395     var wStr;
    396     var index;
    397     var touched = false;
    398 
    399     for (i = 0, len = nodes.length; i < len; i++) {
    400         node = nodes[i];
    401         wStr = node.style.width || node.style.minWidth;
    402         index = wStr ? wStr.indexOf("px") : -1;
    403         if (index >= 0 && wStr.slice(0, index) > docWidth) {
    404             saveStyleProperty(node, "width", actionLog);
    405             saveStyleProperty(node, "minWidth", actionLog);
    406             saveStyleProperty(node, "maxWidth", actionLog);
    407             node.style.width = "100%";
    408             node.style.minWidth = "";
    409             node.style.maxWidth = wStr;
    410             touched = true;
    411         }
    412     }
    413     return touched;
    414 }
    415 
    416 function transformImages(nodes, docWidth, actionLog) {
    417     var i, len;
    418     var node;
    419     var w, h;
    420     var touched = false;
    421 
    422     for (i = 0, len = nodes.length; i < len; i++) {
    423         node = nodes[i];
    424         w = node.offsetWidth;
    425         h = node.offsetHeight;
    426         // shrink w/h proportionally if the img is wider than available width
    427         if (w > docWidth) {
    428             saveStyleProperty(node, "maxWidth", actionLog);
    429             saveStyleProperty(node, "width", actionLog);
    430             saveStyleProperty(node, "height", actionLog);
    431             node.style.maxWidth = docWidth + "px";
    432             node.style.width = "100%";
    433             node.style.height = "auto";
    434             touched = true;
    435         }
    436     }
    437     return touched;
    438 }
    439 
    440 function saveStyleProperty(node, property, actionLog) {
    441     var savedName = "data-" + property;
    442     node.setAttribute(savedName, node.style[property]);
    443     actionLog.push([undoSetProperty, node, [property, savedName]]);
    444 }
    445 
    446 function undoSetProperty(property, savedProperty) {
    447     this.style[property] = savedProperty ? this.getAttribute(savedProperty) : "";
    448 }
    449 
    450 function shouldMungeTable(table) {
    451     return table.hasAttribute("width") || table.style.width;
    452 }
    453 
    454 function hideAllUnsafeImages() {
    455     hideUnsafeImages(document.getElementsByClassName("mail-message-content"));
    456 }
    457 
    458 function hideUnsafeImages(msgContentDivs) {
    459     var i, msgContentCount;
    460     var j, imgCount;
    461     var msgContentDiv, image;
    462     var images;
    463     var showImages;
    464     var k = 0;
    465     var urls = new Array();
    466     var messageIds = new Array();
    467     for (i = 0, msgContentCount = msgContentDivs.length; i < msgContentCount; i++) {
    468         msgContentDiv = msgContentDivs[i];
    469         showImages = msgContentDiv.classList.contains("mail-show-images");
    470 
    471         images = msgContentDiv.getElementsByTagName("img");
    472         for (j = 0, imgCount = images.length; j < imgCount; j++) {
    473             image = images[j];
    474             var src = rewriteRelativeImageSrc(image);
    475             if (src) {
    476                 urls[k] = src;
    477                 messageIds[k] = msgContentDiv.parentNode.id;
    478                 k++;
    479             }
    480             attachImageLoadListener(image);
    481             // TODO: handle inline image attachments for all supported protocols
    482             if (!showImages) {
    483                 blockImage(image);
    484             }
    485         }
    486     }
    487 
    488     window.mail.onInlineAttachmentsParsed(urls, messageIds);
    489 }
    490 
    491 /**
    492  * Changes relative paths to absolute path by pre-pending the account uri.
    493  * @param {Element} imgElement Image for which the src path will be updated.
    494  * @returns the rewritten image src string or null if the imgElement was not rewritten.
    495  */
    496 function rewriteRelativeImageSrc(imgElement) {
    497     var src = imgElement.src;
    498 
    499     // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation
    500     if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) {
    501         // The conversation specifies a different base uri than the document
    502         src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length);
    503         imgElement.src = src;
    504         return src;
    505     }
    506 
    507     // preserve cid urls as is
    508     if (src.substring(0, 4) == "cid:") {
    509         return src;
    510     }
    511 
    512     return null;
    513 };
    514 
    515 
    516 function attachImageLoadListener(imageElement) {
    517     // Reset the src attribute to the empty string because onload will only fire if the src
    518     // attribute is set after the onload listener.
    519     var originalSrc = imageElement.src;
    520     imageElement.src = '';
    521     imageElement.onload = imageOnLoad;
    522     imageElement.src = originalSrc;
    523 }
    524 
    525 /**
    526  * Handle an onload event for an <img> tag.
    527  * The image could be within an elided-text block, or at the top level of a message.
    528  * When a new image loads, its new bounds may affect message or elided-text geometry,
    529  * so we need to inspect and adjust the enclosing element's zoom level where necessary.
    530  *
    531  * Because this method can be called really often, and zoom-level adjustment is slow,
    532  * we collect the elements to be processed and do them all later in a single deferred pass.
    533  */
    534 function imageOnLoad(e) {
    535     // normalize the quoted text parent if we're in a quoted text block, or else
    536     // normalize the parent message content element
    537     var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content");
    538     if (!parent) {
    539         // sanity check. shouldn't really happen.
    540         return;
    541     }
    542 
    543     // if there was no previous work, schedule a new deferred job
    544     if (gImageLoadElements.length == 0) {
    545         window.setTimeout(handleAllImageOnLoads, 0);
    546     }
    547 
    548     // enqueue the work if it wasn't already enqueued
    549     if (gImageLoadElements.indexOf(parent) == -1) {
    550         gImageLoadElements.push(parent);
    551     }
    552 }
    553 
    554 // handle all deferred work from image onload events
    555 function handleAllImageOnLoads() {
    556     normalizeElementWidths(gImageLoadElements);
    557     measurePositions();
    558     // clear the queue so the next onload event starts a new job
    559     gImageLoadElements = [];
    560 }
    561 
    562 function blockImage(imageElement) {
    563     var src = imageElement.src;
    564     if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 ||
    565             src.indexOf("content://") == 0 || src.indexOf("cid:") == 0) {
    566         imageElement.setAttribute(BLOCKED_SRC_ATTR, src);
    567         imageElement.src = "data:";
    568     }
    569 }
    570 
    571 function setWideViewport() {
    572     var metaViewport = document.getElementById('meta-viewport');
    573     metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH);
    574 }
    575 
    576 function restoreScrollPosition() {
    577     var scrollYPercent = window.mail.getScrollYPercent();
    578     if (scrollYPercent && document.body.offsetHeight > window.innerHeight) {
    579         document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight);
    580     }
    581 }
    582 
    583 function onContentReady(event) {
    584     // hack for b/1333356
    585     if (RUNNING_KITKAT_OR_LATER) {
    586         restoreScrollPosition();
    587     }
    588     window.mail.onContentReady();
    589 }
    590 
    591 function setupContentReady() {
    592     var signalDiv;
    593 
    594     // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
    595     // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
    596     // animation that immediately runs on page load. The app uses this as a signal that the
    597     // content is loaded and ready to draw, since WebView delays firing this event until the
    598     // layers are composited and everything is ready to draw.
    599     //
    600     // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag.
    601     if (ENABLE_CONTENT_READY) {
    602         signalDiv = document.getElementById("initial-load-signal");
    603         signalDiv.addEventListener("webkitAnimationStart", onContentReady, false);
    604     }
    605 }
    606 
    607 // BEGIN Java->JavaScript handlers
    608 function measurePositions() {
    609     var overlayTops, overlayBottoms;
    610     var i;
    611     var len;
    612 
    613     var expandedBody, headerSpacer;
    614     var prevBodyBottom = 0;
    615     var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content");
    616 
    617     // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be
    618     // trusted.
    619 
    620     overlayTops = new Array(expandedBodyDivs.length + 1);
    621     overlayBottoms = new Array(expandedBodyDivs.length + 1);
    622     for (i = 0, len = expandedBodyDivs.length; i < len; i++) {
    623         expandedBody = expandedBodyDivs[i];
    624         headerSpacer = expandedBody.previousElementSibling;
    625         // addJavascriptInterface handler only supports string arrays
    626         overlayTops[i] = prevBodyBottom;
    627         overlayBottoms[i] = (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight);
    628         prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top;
    629     }
    630     // add an extra one to mark the top/bottom of the last message footer spacer
    631     overlayTops[i] = prevBodyBottom;
    632     overlayBottoms[i] = document.documentElement.scrollHeight;
    633 
    634     window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms);
    635 }
    636 
    637 function unblockImages(messageDomIds) {
    638     var i, j, images, imgCount, image, blockedSrc;
    639     for (j = 0, len = messageDomIds.length; j < len; j++) {
    640         var messageDomId = messageDomIds[j];
    641         var msg = document.getElementById(messageDomId);
    642         if (!msg) {
    643             console.log("can't unblock, no matching message for id: " + messageDomId);
    644             continue;
    645         }
    646         images = msg.getElementsByTagName("img");
    647         for (i = 0, imgCount = images.length; i < imgCount; i++) {
    648             image = images[i];
    649             blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR);
    650             if (blockedSrc) {
    651                 image.src = blockedSrc;
    652                 image.removeAttribute(BLOCKED_SRC_ATTR);
    653             }
    654         }
    655     }
    656 }
    657 
    658 function setConversationHeaderSpacerHeight(spacerHeight) {
    659     var spacer = document.getElementById("conversation-header");
    660     if (!spacer) {
    661         console.log("can't set spacer for conversation header");
    662         return;
    663     }
    664     spacer.style.height = spacerHeight + "px";
    665     measurePositions();
    666 }
    667 
    668 function setConversationFooterSpacerHeight(spacerHeight) {
    669     var spacer = document.getElementById("conversation-footer");
    670     if (!spacer) {
    671         console.log("can't set spacer for conversation footer");
    672         return;
    673     }
    674     spacer.style.height = spacerHeight + "px";
    675     measurePositions();
    676 }
    677 
    678 function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) {
    679     var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header");
    680     setSpacerHeight(spacer, spacerHeight);
    681 }
    682 
    683 function setSpacerHeight(spacer, spacerHeight) {
    684     if (!spacer) {
    685         console.log("can't set spacer for message with id: " + messageDomId);
    686         return;
    687     }
    688     spacer.style.height = spacerHeight + "px";
    689     measurePositions();
    690 }
    691 
    692 function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) {
    693     var i, len;
    694     var visibility = isVisible ? "block" : "none";
    695     var messageDiv = document.querySelector("#" + messageDomId);
    696     var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible");
    697     if (!messageDiv || collapsibleDivs.length == 0) {
    698         console.log("can't set body visibility for message with id: " + messageDomId);
    699         return;
    700     }
    701 
    702     messageDiv.classList.toggle("expanded");
    703     for (i = 0, len = collapsibleDivs.length; i < len; i++) {
    704         collapsibleDivs[i].style.display = visibility;
    705     }
    706 
    707     // revealing new content should trigger width normalization, since the initial render
    708     // skips collapsed and super-collapsed messages
    709     if (isVisible) {
    710         normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content"));
    711     }
    712 
    713     setMessageHeaderSpacerHeight(messageDomId, spacerHeight);
    714 }
    715 
    716 function replaceSuperCollapsedBlock(startIndex) {
    717     var parent, block, msg;
    718 
    719     block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']");
    720     if (!block) {
    721         console.log("can't expand super collapsed block at index: " + startIndex);
    722         return;
    723     }
    724     parent = block.parentNode;
    725     block.innerHTML = window.mail.getTempMessageBodies();
    726 
    727     // process the new block contents in one go before we pluck them out of the common ancestor
    728     processQuotedText(block, false /* showElided */);
    729     hideUnsafeImages(block.getElementsByClassName("mail-message-content"));
    730 
    731     msg = block.firstChild;
    732     while (msg) {
    733         parent.insertBefore(msg, block);
    734         msg = block.firstChild;
    735     }
    736     parent.removeChild(block);
    737     disablePostForms();
    738     measurePositions();
    739 }
    740 
    741 function processNewMessageBody(msgContentDiv) {
    742     processQuotedText(msgContentDiv, true /* showElided */);
    743     hideUnsafeImages([msgContentDiv]);
    744     if (up(msgContentDiv, "mail-message").classList.contains("expanded")) {
    745         normalizeElementWidths([msgContentDiv]);
    746     }
    747 }
    748 
    749 function replaceMessageBodies(messageIds) {
    750     var i;
    751     var id;
    752     var msgContentDiv;
    753 
    754     for (i = 0, len = messageIds.length; i < len; i++) {
    755         id = messageIds[i];
    756         msgContentDiv = document.querySelector("#" + id + " > .mail-message-content");
    757         // Check if we actually have a div before trying to replace this message body.
    758         if (msgContentDiv) {
    759             msgContentDiv.innerHTML = window.mail.getMessageBody(id);
    760             processNewMessageBody(msgContentDiv);
    761         } else {
    762             // There's no message div, just skip it. We're in a really busted state.
    763             console.log("Mail message content for msg " + id + " to replace not found.");
    764         }
    765     }
    766     disablePostForms();
    767     measurePositions();
    768 }
    769 
    770 // handle the special case of adding a single new message at the end of a conversation
    771 function appendMessageHtml() {
    772     var msg = document.createElement("div");
    773     msg.innerHTML = window.mail.getTempMessageBodies();
    774     var body = msg.children[0];  // toss the outer div, it was just to render innerHTML into
    775     document.body.insertBefore(body, document.getElementById("conversation-footer"));
    776     processNewMessageBody(body.querySelector(".mail-message-content"));
    777     disablePostForms();
    778     measurePositions();
    779 }
    780 
    781 function disablePostForms() {
    782     var forms = document.getElementsByTagName('FORM');
    783     var i;
    784     var j;
    785     var elements;
    786 
    787     for (i = 0; i < forms.length; ++i) {
    788         if (forms[i].method.toUpperCase() === 'POST') {
    789             forms[i].onsubmit = function() {
    790                 alert(MSG_FORMS_ARE_DISABLED);
    791                 return false;
    792             }
    793             elements = forms[i].elements;
    794             for (j = 0; j < elements.length; ++j) {
    795                 if (elements[j].type != 'submit') {
    796                     elements[j].disabled = true;
    797                 }
    798             }
    799         }
    800     }
    801 }
    802 // END Java->JavaScript handlers
    803 
    804 // Do this first to ensure that the readiness signal comes through,
    805 // even if a stray exception later occurs.
    806 setupContentReady();
    807 
    808 collapseAllQuotedText();
    809 hideAllUnsafeImages();
    810 normalizeAllMessageWidths();
    811 //setWideViewport();
    812 // hack for b/1333356
    813 if (!RUNNING_KITKAT_OR_LATER) {
    814     restoreScrollPosition();
    815 }
    816 disablePostForms();
    817 measurePositions();
    818