Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
300 lines
10 KiB
JavaScript
Executable file
300 lines
10 KiB
JavaScript
Executable file
/*global Promise, Template, Utils,
|
|
ImageUtils
|
|
*/
|
|
|
|
/* exported AttachmentRenderer */
|
|
|
|
(function(exports) {
|
|
'use strict';
|
|
// do not create thumbnails for too big attachments
|
|
// (see bug 805114 for a similar issue in Gallery)
|
|
const MAX_THUMBNAIL_GENERATION_SIZE = 10 * 1024 * 1024; // 5MB
|
|
|
|
// Actual thumbnails size should be 10 rem (100px) * devicePixelRatio
|
|
const THUMBNAIL_SIZE = 100 * window.devicePixelRatio;
|
|
|
|
/**
|
|
* A <div> container suits most of the cases where we want to display an
|
|
* MMS attachment (= icon + file name + file size). However, drafts are a
|
|
* specific case because they are inside an editable area.
|
|
*
|
|
* A <div contenteditable="false"> container would be fine for drafts but
|
|
* Gecko does not support it at the moment, see bug 685445:
|
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=685445
|
|
*
|
|
* Instead of a bunch of JavaScript to manage where the caret is and what
|
|
* to delete on backspace, the contentEditable region treats the iframe as
|
|
* a simple block. Outside of the Compose area, a <div> container is still
|
|
* fine -- and it's *way* faster.
|
|
*/
|
|
const RENDERERS = {
|
|
draft: {
|
|
createAttachmentContainer: function() {
|
|
var container = document.createElement('iframe');
|
|
|
|
// The attachment's iFrame requires access to the parent document's
|
|
// context so that URIs for Blobs created in the parent may resolve as
|
|
// expected.
|
|
container.setAttribute('sandbox', 'allow-same-origin');
|
|
|
|
return container;
|
|
},
|
|
|
|
/**
|
|
* Renders baseMarkup into container node (in this case iframe).
|
|
* @param baseMarkup Base attachment HTML markup. It should be safely
|
|
* escaped in advance!
|
|
* @param attachmentContainer Attachment container node.
|
|
* @returns {Promise.<Node>} Content container node is the container node
|
|
* for the attachment base HTML markup that allows consumer code to
|
|
* perform post processing DOM operations.
|
|
*/
|
|
renderTo: function(data, attachmentContainer) {
|
|
attachmentContainer.classList.add(data.cssClass);
|
|
|
|
var template = Template('attachment-draft-tmpl');
|
|
var markup = template.interpolate({
|
|
baseURL: location.protocol + '//' + location.host + '/',
|
|
baseHTML: data.markup,
|
|
cssClass: data.cssClass
|
|
}, { safe: ['baseHTML'] });
|
|
|
|
var blob = new Blob([markup], { type: 'text/html' });
|
|
var blobUrl = window.URL.createObjectURL(blob);
|
|
|
|
attachmentContainer.src = blobUrl;
|
|
|
|
return this._whenLoaded(attachmentContainer).then(function onload() {
|
|
// do some postprocessing after it's loaded
|
|
window.URL.revokeObjectURL(blobUrl);
|
|
|
|
var contentDocument = attachmentContainer.contentDocument;
|
|
var documentElement = contentDocument.documentElement;
|
|
documentElement.lang = document.documentElement.getAttribute('lang');
|
|
documentElement.dir = document.documentElement.getAttribute('dir');
|
|
document.l10n.translateFragment(documentElement);
|
|
|
|
// Attach click listeners and fire the callback when rendering is
|
|
// complete: we can't bind `readyCallback' to the `load' event
|
|
// listener because it would break our unit tests.
|
|
// Bubble click events from inside the iframe.
|
|
contentDocument.addEventListener(
|
|
'click',
|
|
() => attachmentContainer.click()
|
|
);
|
|
|
|
// Return actual content container node to allow postprocessing of
|
|
// DOM content without dealing with iframe structure.
|
|
return contentDocument.body;
|
|
});
|
|
},
|
|
|
|
_whenLoaded: function(iframe) {
|
|
var innerDocument = iframe.contentDocument;
|
|
// checking for the class is a way to check that the correct content is
|
|
// loaded. If the IFrame is inserted to the DOM before we change the
|
|
// src, its default src about:blank is loaded and we have a body, albeit
|
|
// a bad one.
|
|
if (innerDocument && innerDocument.body &&
|
|
innerDocument.body.classList.contains('attachment-draft')) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
iframe.addEventListener('load', function onload() {
|
|
this.removeEventListener('load', onload);
|
|
resolve();
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns the inner DOM node.
|
|
*/
|
|
getContentNode: function(attachmentContainer) {
|
|
return this._whenLoaded(attachmentContainer).then(
|
|
() => attachmentContainer.contentDocument.documentElement
|
|
);
|
|
},
|
|
|
|
setL10nAttributes: function(element, l10nId, l10nArgs) {
|
|
document.l10n.setAttributes(
|
|
element, l10nId, l10nArgs
|
|
);
|
|
// l10n library can't see changes inside iframes yet
|
|
document.l10n.translateFragment(element);
|
|
}
|
|
},
|
|
|
|
base: {
|
|
createAttachmentContainer: function() {
|
|
return document.createElement('div');
|
|
},
|
|
|
|
renderTo: function(data, attachmentContainer) {
|
|
attachmentContainer.classList.add(data.cssClass);
|
|
attachmentContainer.innerHTML = data.markup;
|
|
|
|
return Promise.resolve(attachmentContainer);
|
|
},
|
|
|
|
getContentNode: function(attachmentContainer) {
|
|
return Promise.resolve(attachmentContainer);
|
|
},
|
|
|
|
setL10nAttributes: function(element, l10nId, l10nArgs) {
|
|
document.l10n.setAttributes(
|
|
element, l10nId, l10nArgs
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
var AttachmentRenderer = function(attachment) {
|
|
this._attachment = attachment;
|
|
this._renderer = attachment.isDraft ? RENDERERS.draft : RENDERERS.base;
|
|
this._attachmentContainer = null;
|
|
};
|
|
|
|
/**
|
|
* Gets DOM node that will be container for the attachment markup. This method
|
|
* will create new container if it wasn't before.
|
|
* @returns {Node}
|
|
*/
|
|
AttachmentRenderer.prototype.getAttachmentContainer = function() {
|
|
if (!this._attachmentContainer) {
|
|
this._attachmentContainer = this._createAttachmentContainer();
|
|
}
|
|
|
|
return this._attachmentContainer;
|
|
};
|
|
|
|
AttachmentRenderer.prototype.render = function() {
|
|
var attachmentContainer = this.getAttachmentContainer();
|
|
|
|
// Currently we try to extract thumbnail for image only, for the rest of
|
|
// types (audio, video and etc.) we display default attachment placeholder.
|
|
// Video type should be revisited with:
|
|
// Bug 924609 - Video thumbnails previews are not showing in MMS when
|
|
// attaching or receiving a video.
|
|
var thumbnailPromise = this._attachment.type === 'img' &&
|
|
this._attachment.size < MAX_THUMBNAIL_GENERATION_SIZE ?
|
|
this.getThumbnail() : Promise.reject();
|
|
|
|
return thumbnailPromise.
|
|
then((thumbnail) => {
|
|
return {
|
|
markup: this._getBaseMarkup('attachment-preview-tmpl'),
|
|
cssClass: 'preview',
|
|
thumbnail: thumbnail
|
|
};
|
|
}).
|
|
catch((error) => {
|
|
return {
|
|
markup: this._getBaseMarkup('attachment-nopreview-tmpl', !!error),
|
|
cssClass: 'nopreview'
|
|
};
|
|
}).
|
|
then((data) => {
|
|
return this._renderer.renderTo(data, attachmentContainer).
|
|
then((contentContainer) => {
|
|
var thumbnailNode = contentContainer.querySelector('.thumbnail');
|
|
if (thumbnailNode) {
|
|
thumbnailNode.style.backgroundImage =
|
|
'url("' + data.thumbnail.url + data.thumbnail.fragment + '")';
|
|
|
|
// It's essential to remember real image URL to revoke it later
|
|
attachmentContainer.dataset.thumbnail = data.thumbnail.url;
|
|
}
|
|
|
|
var fileNode = contentContainer.querySelector('.file-name');
|
|
if (this._attachment.name) {
|
|
fileNode.removeAttribute('data-l10n-id');
|
|
fileNode.textContent = this._attachment.name.slice(
|
|
this._attachment.name.lastIndexOf('/') + 1
|
|
);
|
|
} else {
|
|
fileNode.setAttribute('data-l10n-id', 'unnamed-attachment');
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
AttachmentRenderer.prototype.updateFileSize = function() {
|
|
var attachmentContainer = this.getAttachmentContainer();
|
|
return this._renderer.getContentNode(attachmentContainer).then(
|
|
(contentNode) => {
|
|
var sizeIndicator = contentNode.querySelector('.js-size-indicator');
|
|
if (!sizeIndicator) {
|
|
throw new Error('updateFileSize should be called after a render().');
|
|
}
|
|
|
|
var sizeL10n = Utils.getSizeForL10n(this._attachment.size);
|
|
this._renderer.setL10nAttributes(
|
|
sizeIndicator, sizeL10n.l10nId, sizeL10n.l10nArgs
|
|
);
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Extracts thumbnail for the image.
|
|
* TODO: As we started to use mozSampleSize for thumbnail generation we need
|
|
* to check if we still want to store this thumbnail data (indexedDB)
|
|
* Bug 876467 - [MMS] generate, store, and reuse image thumbnails
|
|
* @returns {Promise}
|
|
*/
|
|
AttachmentRenderer.prototype.getThumbnail = function() {
|
|
// The thumbnail format matches the blob format.
|
|
var blob = this._attachment.blob;
|
|
|
|
return ImageUtils.getSizeAndType(blob).then(
|
|
function getSizeResolve(data) {
|
|
var fragment = ImageUtils.Downsample.sizeNoMoreThan(
|
|
THUMBNAIL_SIZE / Math.min(data.width, data.height)
|
|
);
|
|
|
|
return {
|
|
url: window.URL.createObjectURL(blob),
|
|
fragment: fragment
|
|
};
|
|
}
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Creates new attachment container.
|
|
* @returns {Node}
|
|
*/
|
|
AttachmentRenderer.prototype._createAttachmentContainer = function() {
|
|
var attachmentContainer = this._renderer.createAttachmentContainer();
|
|
|
|
attachmentContainer.classList.add('attachment-container');
|
|
attachmentContainer.dataset.attachmentType = this._attachment.type;
|
|
|
|
return attachmentContainer;
|
|
};
|
|
|
|
/**
|
|
* Returns HTML string that represents base attachment markup.
|
|
* @param templateId Id of the template to use.
|
|
* @param hasError Indicates whether something is wrong with attachment.
|
|
* @returns {string}
|
|
*/
|
|
AttachmentRenderer.prototype._getBaseMarkup = function(templateId, hasError) {
|
|
// interpolate the #attachment-[no]preview-tmpl template
|
|
var sizeL10n = Utils.getSizeForL10n(this._attachment.size);
|
|
return Template(templateId).interpolate({
|
|
type: this._attachment.type,
|
|
errorClass: hasError ? 'corrupted' : '',
|
|
sizeL10nId: sizeL10n.l10nId,
|
|
sizeL10nArgs: JSON.stringify(sizeL10n.l10nArgs)
|
|
});
|
|
};
|
|
|
|
exports.AttachmentRenderer = {
|
|
for: function(attachment) {
|
|
return new AttachmentRenderer(attachment);
|
|
}
|
|
};
|
|
}(this));
|