Loading apps/email/js/cards/compose.html +1 −1 Changes for apps/email/js/cards/compose.html: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -64,7 +64,7 @@ <ul class="cmp-attachment-container"> </ul> </div> <textarea class="cmp-body-text" rows="1"></textarea> <div class="cmp-body-text" contenteditable="true"></div> <div class="cmp-body-html"> </div> </div> Loading apps/email/js/cards/compose.js +99 −26 Changes for apps/email/js/cards/compose.js: 99 added lines, 26 removed lines. Original line number Diff line number Diff line Loading @@ -44,6 +44,8 @@ function focusInputAndPositionCursorFromContainerClick(event, input) { // because under Gecko originalTarget may contain anonymous content. if (event.explicitOriginalTarget === input) return; // Stop bubbling to avoid our other focus-handlers! event.stopPropagation(); // coordinates are relative to the viewport origin var bounds = input.getBoundingClientRect(); Loading Loading @@ -81,10 +83,6 @@ function ComposeCard(domNode, mode, args) { this.bccNode = domNode.getElementsByClassName('cmp-bcc-text')[0]; this.subjectNode = domNode.getElementsByClassName('cmp-subject-text')[0]; this.textBodyNode = domNode.getElementsByClassName('cmp-body-text')[0]; this.textBodyNode.addEventListener('input', this.onTextBodyDelta.bind(this)); this.textBodyNode.addEventListener('change', this.onTextBodyDelta.bind(this)); this.htmlBodyContainer = domNode.getElementsByClassName('cmp-body-html')[0]; this.htmlIframeNode = null; Loading Loading @@ -124,6 +122,21 @@ function ComposeCard(domNode, mode, args) { evt, subjectContainer.querySelector('input')); }); // Likewise, clicking on the empty space below our contenteditable region // or on our immutable HTML quoting box should result in us positioning // the cursor in our contenteditable region. this.scrollContainer.addEventListener( 'click', function(event) { // Only do this if the click is BELOW the text area. var bounds = this.textBodyNode.getBoundingClientRect(); if (event.clientY > bounds.bottom) { this._focusEditorWithCursorAtEnd(event); } }.bind(this)); this.htmlBodyContainer.addEventListener( 'click', this._focusEditorWithCursorAtEnd.bind(this)); // Sent sound init this.sentAudioKey = 'mail.sent-sound.enabled'; this.sentAudio = new Audio('/sounds/sent.opus'); Loading @@ -142,6 +155,85 @@ function ComposeCard(domNode, mode, args) { } } ComposeCard.prototype = { /** * Focus our contenteditable region and position the cursor at the last * valid editing cursor position. * * The intent is so that if the user taps below our editing region that we * still correctly position the cursor. We previously relied on min-height * to do this for us, but that results in ugly problems when we have quoted * HTML that follows and our editable region is not big enough to satisfy * the height. * * Note: When we are quoting HTML, the "Bob wrote:" stuff does go in the * contenteditable text area, so we may actually want to get smarter and * position the cursor before that node instead. */ _focusEditorWithCursorAtEnd: function(event) { if (event) event.stopPropagation(); // Selection/range manipulation is the easiest way to force the cursor // to a specific location. // // Note: Once the user has pressed return once, the editor will create a // bogus <br type="_moz"> that is always the last element. Even though this // bogus node will be the last child, nothing tremendously bad happens. // // Note: This technique does result in our new text existing in its own, // new text node. So don't make any assumptions about how text nodes are // arranged. var insertAfter = this.textBodyNode.lastChild; var range = document.createRange(); range.setStartAfter(insertAfter); range.setEndAfter(insertAfter); this.textBodyNode.focus(); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); }, /** * Inserts an email into the contenteditable element */ populateEditor: function(value) { var lines = value.split('\n'); var frag = document.createDocumentFragment(); for (var i = 0, len = lines.length; i < len; i++) { if (i) { frag.appendChild(document.createElement('br')); } frag.appendChild(document.createTextNode(lines[i])); } this.textBodyNode.appendChild(frag); }, /** * Gets the raw value from a contenteditable div */ fromEditor: function(value) { var content = ''; var len = this.textBodyNode.childNodes.length; for (var i = 0; i < len; i++) { var node = this.textBodyNode.childNodes[i]; if (node.nodeName === 'BR' && // Gecko's contenteditable implementation likes to create a synthetic // trailing BR with type="_moz". We do not like/need this synthetic // BR, so we filter it out. Check out // nsTextEditRules::CreateTrailingBRIfNeeded to find out where it // comes from. node.getAttribute('type') !== '_moz') { content += '\n'; } else { content += node.textContent; } } return content; }, postInsert: function() { // the HTML bit needs us linked into the DOM so the iframe can be // linked in, hence this happens in postInsert. Loading Loading @@ -197,9 +289,7 @@ ComposeCard.prototype = { this.insertAttachments(); this.subjectNode.value = this.composer.subject; this.textBodyNode.value = this.composer.body.text; // force the textarea to be sized. this.onTextBodyDelta(); this.populateEditor(this.composer.body.text); if (this.composer.body.html) { // Although (still) sanitized, this is still HTML we did not create and so Loading Loading @@ -234,7 +324,7 @@ ComposeCard.prototype = { this.composer.cc = frobAddressNode(this.ccNode); this.composer.bcc = frobAddressNode(this.bccNode); this.composer.subject = this.subjectNode.value; this.composer.body.text = this.textBodyNode.value; this.composer.body.text = this.fromEditor(); // The HTML representation cannot currently change in our UI, so no // need to save it. However, what we send to the back-end is what gets // sent, so if you want to implement editing UI and change this here, Loading @@ -261,7 +351,7 @@ ComposeCard.prototype = { // body, there are attachments, or we already created a draft for this // guy in which case we really want to provide the option to delete the // draft. return (this.subjectNode.value || this.textBodyNode.value || return (this.subjectNode.value || this.textBodyNode.textContent || !checkAddressEmpty() || this.composer.attachments.length || this.composer.hasDraft); }, Loading Loading @@ -442,23 +532,6 @@ ComposeCard.prototype = { focusInputAndPositionCursorFromContainerClick(evt, input); }, /** * Make our textarea grow as new lines are added... */ onTextBodyDelta: function() { var value = this.textBodyNode.value, newlines = 0, idx = -1; while (true) { idx = value.indexOf('\n', idx + 1); if (idx === -1) break; newlines++; } // the last line won't have a newline var neededRows = newlines + 1; if (this.textBodyNode.rows !== neededRows) this.textBodyNode.rows = neededRows; }, insertAttachments: function() { var attachmentsContainer = this.domNode.getElementsByClassName('cmp-attachment-container')[0]; Loading apps/email/js/cards/message_reader.js +4 −7 Changes for apps/email/js/cards/message_reader.js: 4 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -299,11 +299,13 @@ MessageReaderCard.prototype = { }); var otherAddresses = (this.header.to || []).concat(this.header.cc || []); if (this.header.replyTo) { if (this.header.replyTo && this.header.replyTo.author) { otherAddresses.push(this.header.replyTo.author); } for (var i = 0; i < otherAddresses.length; i++) { if (myAddresses.indexOf(otherAddresses[i].address) == -1) { var otherAddress = otherAddresses[i]; if (otherAddress.address && myAddresses.indexOf(otherAddress.address) == -1) { return true; } } Loading Loading @@ -335,11 +337,6 @@ MessageReaderCard.prototype = { }).bind(this); contents.addEventListener('submit', formSubmit); if (!this.canForward()) { contents.querySelector('.msg-reply-menu-forward') .classList.add('collapsed'); } if (!this.canReplyAll()) { contents.querySelector('.msg-reply-menu-reply-all') .classList.add('collapsed'); Loading apps/email/js/iframe_shims.js +8 −3 Changes for apps/email/js/iframe_shims.js: 8 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -185,9 +185,6 @@ function createAndInsertIframeForContent(htmlStr, scrollContainer, var viewportWidth = parentNode.offsetWidth - scrollPad; var viewport = document.createElement('div'); var title = document.getElementsByClassName('msg-reader-header')[0]; var header = document.getElementsByClassName('msg-envelope-bar')[0]; var extraHeight = title.clientHeight + header.clientHeight; viewport.setAttribute( 'style', 'overflow: hidden; position: relative; ' + Loading Loading @@ -329,6 +326,14 @@ function createAndInsertIframeForContent(htmlStr, scrollContainer, if (!newsletterMode || interactiveMode !== 'interactive') { return iframeShims; } // XXX these live down here now because these hardcoded hacks will // explode when we use this logic in the composer (when there isn't // a message reader on the stack). This is okay / not any more horrible // because the compose iframe is entirely noninteractive so these values // aren't used anyways. var title = document.getElementsByClassName('msg-reader-header')[0]; var header = document.getElementsByClassName('msg-envelope-bar')[0]; var extraHeight = title.clientHeight + header.clientHeight; detectorTarget.addEventListener('dbltap', function(e) { var newScale = scale; if (lastScale === scale) { Loading apps/email/style/common.css +0 −1 Changes for apps/email/style/common.css: 0 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -141,7 +141,6 @@ body { overflow-x: hidden; } .scrollregion-below-header { overflow-y: auto; overflow-x: hidden; height: calc(100% - 5.8rem + 0.5rem); will-change: scroll-position; Loading Loading
apps/email/js/cards/compose.html +1 −1 Changes for apps/email/js/cards/compose.html: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -64,7 +64,7 @@ <ul class="cmp-attachment-container"> </ul> </div> <textarea class="cmp-body-text" rows="1"></textarea> <div class="cmp-body-text" contenteditable="true"></div> <div class="cmp-body-html"> </div> </div> Loading
apps/email/js/cards/compose.js +99 −26 Changes for apps/email/js/cards/compose.js: 99 added lines, 26 removed lines. Original line number Diff line number Diff line Loading @@ -44,6 +44,8 @@ function focusInputAndPositionCursorFromContainerClick(event, input) { // because under Gecko originalTarget may contain anonymous content. if (event.explicitOriginalTarget === input) return; // Stop bubbling to avoid our other focus-handlers! event.stopPropagation(); // coordinates are relative to the viewport origin var bounds = input.getBoundingClientRect(); Loading Loading @@ -81,10 +83,6 @@ function ComposeCard(domNode, mode, args) { this.bccNode = domNode.getElementsByClassName('cmp-bcc-text')[0]; this.subjectNode = domNode.getElementsByClassName('cmp-subject-text')[0]; this.textBodyNode = domNode.getElementsByClassName('cmp-body-text')[0]; this.textBodyNode.addEventListener('input', this.onTextBodyDelta.bind(this)); this.textBodyNode.addEventListener('change', this.onTextBodyDelta.bind(this)); this.htmlBodyContainer = domNode.getElementsByClassName('cmp-body-html')[0]; this.htmlIframeNode = null; Loading Loading @@ -124,6 +122,21 @@ function ComposeCard(domNode, mode, args) { evt, subjectContainer.querySelector('input')); }); // Likewise, clicking on the empty space below our contenteditable region // or on our immutable HTML quoting box should result in us positioning // the cursor in our contenteditable region. this.scrollContainer.addEventListener( 'click', function(event) { // Only do this if the click is BELOW the text area. var bounds = this.textBodyNode.getBoundingClientRect(); if (event.clientY > bounds.bottom) { this._focusEditorWithCursorAtEnd(event); } }.bind(this)); this.htmlBodyContainer.addEventListener( 'click', this._focusEditorWithCursorAtEnd.bind(this)); // Sent sound init this.sentAudioKey = 'mail.sent-sound.enabled'; this.sentAudio = new Audio('/sounds/sent.opus'); Loading @@ -142,6 +155,85 @@ function ComposeCard(domNode, mode, args) { } } ComposeCard.prototype = { /** * Focus our contenteditable region and position the cursor at the last * valid editing cursor position. * * The intent is so that if the user taps below our editing region that we * still correctly position the cursor. We previously relied on min-height * to do this for us, but that results in ugly problems when we have quoted * HTML that follows and our editable region is not big enough to satisfy * the height. * * Note: When we are quoting HTML, the "Bob wrote:" stuff does go in the * contenteditable text area, so we may actually want to get smarter and * position the cursor before that node instead. */ _focusEditorWithCursorAtEnd: function(event) { if (event) event.stopPropagation(); // Selection/range manipulation is the easiest way to force the cursor // to a specific location. // // Note: Once the user has pressed return once, the editor will create a // bogus <br type="_moz"> that is always the last element. Even though this // bogus node will be the last child, nothing tremendously bad happens. // // Note: This technique does result in our new text existing in its own, // new text node. So don't make any assumptions about how text nodes are // arranged. var insertAfter = this.textBodyNode.lastChild; var range = document.createRange(); range.setStartAfter(insertAfter); range.setEndAfter(insertAfter); this.textBodyNode.focus(); var selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range); }, /** * Inserts an email into the contenteditable element */ populateEditor: function(value) { var lines = value.split('\n'); var frag = document.createDocumentFragment(); for (var i = 0, len = lines.length; i < len; i++) { if (i) { frag.appendChild(document.createElement('br')); } frag.appendChild(document.createTextNode(lines[i])); } this.textBodyNode.appendChild(frag); }, /** * Gets the raw value from a contenteditable div */ fromEditor: function(value) { var content = ''; var len = this.textBodyNode.childNodes.length; for (var i = 0; i < len; i++) { var node = this.textBodyNode.childNodes[i]; if (node.nodeName === 'BR' && // Gecko's contenteditable implementation likes to create a synthetic // trailing BR with type="_moz". We do not like/need this synthetic // BR, so we filter it out. Check out // nsTextEditRules::CreateTrailingBRIfNeeded to find out where it // comes from. node.getAttribute('type') !== '_moz') { content += '\n'; } else { content += node.textContent; } } return content; }, postInsert: function() { // the HTML bit needs us linked into the DOM so the iframe can be // linked in, hence this happens in postInsert. Loading Loading @@ -197,9 +289,7 @@ ComposeCard.prototype = { this.insertAttachments(); this.subjectNode.value = this.composer.subject; this.textBodyNode.value = this.composer.body.text; // force the textarea to be sized. this.onTextBodyDelta(); this.populateEditor(this.composer.body.text); if (this.composer.body.html) { // Although (still) sanitized, this is still HTML we did not create and so Loading Loading @@ -234,7 +324,7 @@ ComposeCard.prototype = { this.composer.cc = frobAddressNode(this.ccNode); this.composer.bcc = frobAddressNode(this.bccNode); this.composer.subject = this.subjectNode.value; this.composer.body.text = this.textBodyNode.value; this.composer.body.text = this.fromEditor(); // The HTML representation cannot currently change in our UI, so no // need to save it. However, what we send to the back-end is what gets // sent, so if you want to implement editing UI and change this here, Loading @@ -261,7 +351,7 @@ ComposeCard.prototype = { // body, there are attachments, or we already created a draft for this // guy in which case we really want to provide the option to delete the // draft. return (this.subjectNode.value || this.textBodyNode.value || return (this.subjectNode.value || this.textBodyNode.textContent || !checkAddressEmpty() || this.composer.attachments.length || this.composer.hasDraft); }, Loading Loading @@ -442,23 +532,6 @@ ComposeCard.prototype = { focusInputAndPositionCursorFromContainerClick(evt, input); }, /** * Make our textarea grow as new lines are added... */ onTextBodyDelta: function() { var value = this.textBodyNode.value, newlines = 0, idx = -1; while (true) { idx = value.indexOf('\n', idx + 1); if (idx === -1) break; newlines++; } // the last line won't have a newline var neededRows = newlines + 1; if (this.textBodyNode.rows !== neededRows) this.textBodyNode.rows = neededRows; }, insertAttachments: function() { var attachmentsContainer = this.domNode.getElementsByClassName('cmp-attachment-container')[0]; Loading
apps/email/js/cards/message_reader.js +4 −7 Changes for apps/email/js/cards/message_reader.js: 4 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -299,11 +299,13 @@ MessageReaderCard.prototype = { }); var otherAddresses = (this.header.to || []).concat(this.header.cc || []); if (this.header.replyTo) { if (this.header.replyTo && this.header.replyTo.author) { otherAddresses.push(this.header.replyTo.author); } for (var i = 0; i < otherAddresses.length; i++) { if (myAddresses.indexOf(otherAddresses[i].address) == -1) { var otherAddress = otherAddresses[i]; if (otherAddress.address && myAddresses.indexOf(otherAddress.address) == -1) { return true; } } Loading Loading @@ -335,11 +337,6 @@ MessageReaderCard.prototype = { }).bind(this); contents.addEventListener('submit', formSubmit); if (!this.canForward()) { contents.querySelector('.msg-reply-menu-forward') .classList.add('collapsed'); } if (!this.canReplyAll()) { contents.querySelector('.msg-reply-menu-reply-all') .classList.add('collapsed'); Loading
apps/email/js/iframe_shims.js +8 −3 Changes for apps/email/js/iframe_shims.js: 8 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -185,9 +185,6 @@ function createAndInsertIframeForContent(htmlStr, scrollContainer, var viewportWidth = parentNode.offsetWidth - scrollPad; var viewport = document.createElement('div'); var title = document.getElementsByClassName('msg-reader-header')[0]; var header = document.getElementsByClassName('msg-envelope-bar')[0]; var extraHeight = title.clientHeight + header.clientHeight; viewport.setAttribute( 'style', 'overflow: hidden; position: relative; ' + Loading Loading @@ -329,6 +326,14 @@ function createAndInsertIframeForContent(htmlStr, scrollContainer, if (!newsletterMode || interactiveMode !== 'interactive') { return iframeShims; } // XXX these live down here now because these hardcoded hacks will // explode when we use this logic in the composer (when there isn't // a message reader on the stack). This is okay / not any more horrible // because the compose iframe is entirely noninteractive so these values // aren't used anyways. var title = document.getElementsByClassName('msg-reader-header')[0]; var header = document.getElementsByClassName('msg-envelope-bar')[0]; var extraHeight = title.clientHeight + header.clientHeight; detectorTarget.addEventListener('dbltap', function(e) { var newScale = scale; if (lastScale === scale) { Loading
apps/email/style/common.css +0 −1 Changes for apps/email/style/common.css: 0 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -141,7 +141,6 @@ body { overflow-x: hidden; } .scrollregion-below-header { overflow-y: auto; overflow-x: hidden; height: calc(100% - 5.8rem + 0.5rem); will-change: scroll-position; Loading