script.js revision adbf3e8cadb66666f307352b72537fbac57b916f
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
24/**
25 * Returns the page offset of an element.
26 *
27 * @param {Element} element The element to return the page offset for.
28 * @return {left: number, top: number} A tuple including a left and top value representing
29 *     the page offset of the element.
30 */
31function getTotalOffset(el) {
32    var result = {
33        left: 0,
34        top: 0
35    };
36    var parent = el;
37
38    while (parent) {
39        result.left += parent.offsetLeft;
40        result.top += parent.offsetTop;
41        parent = parent.offsetParent;
42    }
43
44    return result;
45}
46
47/**
48 * Walks up the DOM starting at a given element, and returns an element that has the
49 * specified class name or null.
50 */
51function up(el, className) {
52    var parent = el;
53    while (parent) {
54        if (parent.classList && parent.classList.contains(className)) {
55            break;
56        }
57        parent = parent.parentNode;
58    }
59    return parent || null;
60}
61
62function onToggleClick(e) {
63    toggleQuotedText(e.target);
64    measurePositions();
65}
66
67function toggleQuotedText(toggleElement) {
68    var elidedTextElement = toggleElement.nextSibling;
69    var isHidden = getComputedStyle(elidedTextElement).display == 'none';
70    toggleElement.innerHTML = isHidden ? MSG_HIDE_ELIDED : MSG_SHOW_ELIDED;
71    elidedTextElement.style.display = isHidden ? 'block' : 'none';
72
73    // Revealing the elided text should normalize it to fit-width to prevent
74    // this message from blowing out the conversation width.
75    if (isHidden) {
76        normalizeElementWidths([elidedTextElement]);
77    }
78}
79
80function collapseAllQuotedText() {
81    processQuotedText(document.documentElement, false /* showElided */);
82}
83
84function processQuotedText(elt, showElided) {
85    var i;
86    var elements = elt.getElementsByClassName("elided-text");
87    var elidedElement, toggleElement;
88    for (i = 0; i < elements.length; i++) {
89        elidedElement = elements[i];
90        toggleElement = document.createElement("div");
91        toggleElement.className = "mail-elided-text";
92        toggleElement.innerHTML = MSG_SHOW_ELIDED;
93        toggleElement.onclick = onToggleClick;
94        elidedElement.style.display = 'none';
95        elidedElement.parentNode.insertBefore(toggleElement, elidedElement);
96        if (showElided) {
97            toggleQuotedText(toggleElement);
98        }
99    }
100}
101
102function normalizeAllMessageWidths() {
103    normalizeElementWidths(document.querySelectorAll(".expanded > .mail-message-content"));
104}
105
106/*
107 * Normalizes the width of all elements supplied to the document body's overall width.
108 * Narrower elements are zoomed in, and wider elements are zoomed out.
109 * This method is idempotent.
110 */
111function normalizeElementWidths(elements) {
112    var i;
113    var el;
114    var documentWidth;
115    var newZoom, oldZoom;
116
117    if (!NORMALIZE_MESSAGE_WIDTHS) {
118        return;
119    }
120
121    documentWidth = document.body.offsetWidth;
122
123    for (i = 0; i < elements.length; i++) {
124        el = elements[i];
125        oldZoom = el.style.zoom;
126        // reset any existing normalization
127        if (oldZoom) {
128            el.style.zoom = 1;
129        }
130        newZoom = documentWidth / el.scrollWidth;
131        el.style.zoom = newZoom;
132    }
133}
134
135function hideUnsafeImages() {
136    var i, bodyCount;
137    var j, imgCount;
138    var body, image;
139    var images;
140    var showImages;
141    var bodies = document.getElementsByClassName("mail-message-content");
142    for (i = 0, bodyCount = bodies.length; i < bodyCount; i++) {
143        body = bodies[i];
144        showImages = body.classList.contains("mail-show-images");
145
146        images = body.getElementsByTagName("img");
147        for (j = 0, imgCount = images.length; j < imgCount; j++) {
148            image = images[j];
149            rewriteRelativeImageSrc(image);
150            attachImageLoadListener(image);
151            // TODO: handle inline image attachments for all supported protocols
152            if (!showImages) {
153                blockImage(image);
154            }
155        }
156    }
157}
158
159/**
160 * Changes relative paths to absolute path by pre-pending the account uri
161 * @param {Element} imgElement Image for which the src path will be updated.
162 */
163function rewriteRelativeImageSrc(imgElement) {
164    var src = imgElement.src;
165
166    // DOC_BASE_URI will always be a unique x-thread:// uri for this particular conversation
167    if (src.indexOf(DOC_BASE_URI) == 0 && (DOC_BASE_URI != CONVERSATION_BASE_URI)) {
168        // The conversation specifies a different base uri than the document
169        src = CONVERSATION_BASE_URI + src.substring(DOC_BASE_URI.length);
170        imgElement.src = src;
171    }
172};
173
174
175function attachImageLoadListener(imageElement) {
176    // Reset the src attribute to the empty string because onload will only fire if the src
177    // attribute is set after the onload listener.
178    var originalSrc = imageElement.src;
179    imageElement.src = '';
180    imageElement.onload = imageOnLoad;
181    imageElement.src = originalSrc;
182}
183
184/**
185 * Handle an onload event for an <img> tag.
186 * The image could be within an elided-text block, or at the top level of a message.
187 * When a new image loads, its new bounds may affect message or elided-text geometry,
188 * so we need to inspect and adjust the enclosing element's zoom level where necessary.
189 *
190 * Because this method can be called really often, and zoom-level adjustment is slow,
191 * we collect the elements to be processed and do them all later in a single deferred pass.
192 */
193function imageOnLoad(e) {
194    // normalize the quoted text parent if we're in a quoted text block, or else
195    // normalize the parent message content element
196    var parent = up(e.target, "elided-text") || up(e.target, "mail-message-content");
197    if (!parent) {
198        // sanity check. shouldn't really happen.
199        return;
200    }
201
202    // if there was no previous work, schedule a new deferred job
203    if (gImageLoadElements.length == 0) {
204        window.setTimeout(handleAllImageOnLoads, 0);
205    }
206
207    // enqueue the work if it wasn't already enqueued
208    if (gImageLoadElements.indexOf(parent) == -1) {
209        gImageLoadElements.push(parent);
210    }
211}
212
213// handle all deferred work from image onload events
214function handleAllImageOnLoads() {
215    normalizeElementWidths(gImageLoadElements);
216    measurePositions();
217    // clear the queue so the next onload event starts a new job
218    gImageLoadElements = [];
219}
220
221function blockImage(imageElement) {
222    var src = imageElement.src;
223    if (src.indexOf("http://") == 0 || src.indexOf("https://") == 0 ||
224            src.indexOf("content://") == 0) {
225        imageElement.setAttribute(BLOCKED_SRC_ATTR, src);
226        imageElement.src = "data:";
227    }
228}
229
230function setWideViewport() {
231    var metaViewport = document.getElementById('meta-viewport');
232    metaViewport.setAttribute('content', 'width=' + WIDE_VIEWPORT_WIDTH);
233}
234
235function restoreScrollPosition() {
236    var scrollYPercent = window.mail.getScrollYPercent();
237    if (scrollYPercent && document.body.offsetHeight > window.innerHeight) {
238        document.body.scrollTop = Math.floor(scrollYPercent * document.body.offsetHeight);
239    }
240}
241
242function onContentReady(event) {
243    window.mail.onContentReady();
244}
245
246function setupContentReady() {
247    var signalDiv;
248
249    // PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
250    // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
251    // animation that immediately runs on page load. The app uses this as a signal that the
252    // content is loaded and ready to draw, since WebView delays firing this event until the
253    // layers are composited and everything is ready to draw.
254    //
255    // This code is conditionally enabled on JB+ by setting the ENABLE_CONTENT_READY flag.
256    if (ENABLE_CONTENT_READY) {
257        signalDiv = document.getElementById("initial-load-signal");
258        signalDiv.addEventListener("webkitAnimationStart", onContentReady, false);
259        signalDiv.classList.add("initial-load");
260    }
261}
262
263// BEGIN Java->JavaScript handlers
264function measurePositions() {
265    var overlayTops, overlayBottoms;
266    var i;
267    var len;
268
269    var expandedBody, headerSpacer;
270    var prevBodyBottom = 0;
271    var expandedBodyDivs = document.querySelectorAll(".expanded > .mail-message-content");
272
273    // N.B. offsetTop and offsetHeight of an element with the "zoom:" style applied cannot be
274    // trusted.
275
276    overlayTops = new Array(expandedBodyDivs.length + 1);
277    overlayBottoms = new Array(expandedBodyDivs.length + 1);
278    for (i = 0, len = expandedBodyDivs.length; i < len; i++) {
279        expandedBody = expandedBodyDivs[i];
280        headerSpacer = expandedBody.previousElementSibling;
281        // addJavascriptInterface handler only supports string arrays
282        overlayTops[i] = "" + prevBodyBottom;
283        overlayBottoms[i] = "" + (getTotalOffset(headerSpacer).top + headerSpacer.offsetHeight);
284        prevBodyBottom = getTotalOffset(expandedBody.nextElementSibling).top;
285    }
286    // add an extra one to mark the top/bottom of the last message footer spacer
287    overlayTops[i] = "" + prevBodyBottom;
288    overlayBottoms[i] = "" + document.body.offsetHeight;
289
290    window.mail.onWebContentGeometryChange(overlayTops, overlayBottoms);
291}
292
293function unblockImages(messageDomId) {
294    var i, images, imgCount, image, blockedSrc;
295    var msg = document.getElementById(messageDomId);
296    if (!msg) {
297        console.log("can't unblock, no matching message for id: " + messageDomId);
298        return;
299    }
300    images = msg.getElementsByTagName("img");
301    for (i = 0, imgCount = images.length; i < imgCount; i++) {
302        image = images[i];
303        blockedSrc = image.getAttribute(BLOCKED_SRC_ATTR);
304        if (blockedSrc) {
305            image.src = blockedSrc;
306            image.removeAttribute(BLOCKED_SRC_ATTR);
307        }
308    }
309}
310
311function setConversationHeaderSpacerHeight(spacerHeight) {
312    var spacer = document.getElementById("conversation-header");
313    if (!spacer) {
314        console.log("can't set spacer for conversation header");
315        return;
316    }
317    spacer.style.height = spacerHeight + "px";
318    measurePositions();
319}
320
321function setMessageHeaderSpacerHeight(messageDomId, spacerHeight) {
322    var spacer = document.querySelector("#" + messageDomId + " > .mail-message-header");
323    if (!spacer) {
324        console.log("can't set spacer for message with id: " + messageDomId);
325        return;
326    }
327    spacer.style.height = spacerHeight + "px";
328    measurePositions();
329}
330
331function setMessageBodyVisible(messageDomId, isVisible, spacerHeight) {
332    var i, len;
333    var visibility = isVisible ? "block" : "none";
334    var messageDiv = document.querySelector("#" + messageDomId);
335    var collapsibleDivs = document.querySelectorAll("#" + messageDomId + " > .collapsible");
336    if (!messageDiv || collapsibleDivs.length == 0) {
337        console.log("can't set body visibility for message with id: " + messageDomId);
338        return;
339    }
340    messageDiv.classList.toggle("expanded");
341    for (i = 0, len = collapsibleDivs.length; i < len; i++) {
342        collapsibleDivs[i].style.display = visibility;
343    }
344
345    // revealing new content should trigger width normalization, since the initial render
346    // skips collapsed and super-collapsed messages
347    if (isVisible) {
348        normalizeElementWidths(messageDiv.getElementsByClassName("mail-message-content"));
349    }
350
351    setMessageHeaderSpacerHeight(messageDomId, spacerHeight);
352}
353
354function replaceSuperCollapsedBlock(startIndex) {
355    var parent, block, header;
356
357    block = document.querySelector(".mail-super-collapsed-block[index='" + startIndex + "']");
358    if (!block) {
359        console.log("can't expand super collapsed block at index: " + startIndex);
360        return;
361    }
362    parent = block.parentNode;
363    block.innerHTML = window.mail.getTempMessageBodies();
364
365    header = block.firstChild;
366    while (header) {
367        parent.insertBefore(header, block);
368        header = block.firstChild;
369    }
370    parent.removeChild(block);
371    measurePositions();
372}
373
374function replaceMessageBodies(messageIds) {
375    var i;
376    var id;
377    var msgContentDiv;
378
379    for (i = 0, len = messageIds.length; i < len; i++) {
380        id = messageIds[i];
381        msgContentDiv = document.querySelector("#" + id + " > .mail-message-content");
382        msgContentDiv.innerHTML = window.mail.getMessageBody(id);
383        processQuotedText(msgContentDiv, true /* showElided */);
384    }
385}
386
387// END Java->JavaScript handlers
388
389collapseAllQuotedText();
390hideUnsafeImages();
391normalizeAllMessageWidths();
392//setWideViewport();
393restoreScrollPosition();
394measurePositions();
395setupContentReady();
396
397