[geary/bug/728002-webkit2: 138/140] Split composer web view up into multiple parts.



commit 96ffcdb6085c7112c344df4b969479383d0aeb8e
Author: Michael James Gratton <mike vee net>
Date:   Tue Jan 31 23:55:44 2017 +1100

    Split composer web view up into multiple parts.
    
    This lets us implement changing signatures and deleting bottom-quoted
    messages without having to reload the whole view, and makes it possible
    to target only the user's content when modifying for send, etc.
    
    * src/client/composer/composer-web-view.vala (ComposerWebView): Move
      composer CSS into composer-web-view.css resource file, load it when
      loading JS resource and add it to the view's user content manager.
      (ComposerWebView::load_html): Split up body, signature and quote into a
      DIV container for each.
      (ComposerWebView::linkify_content): Replaced with ::clean_content,
      which will also tidy up internal markup before sending. Update call
      site and unit test.
    
    * src/engine/rfc822/rfc822-utils.vala (Geary.RFC822.Utils): Remove some
      more obtrusive white space when sending replies/forwards.
    
    * test/client/composer/composer-web-view-test.vala,
      test/js/composer-page-state-test.vala: Update tests to expect new HTML
      and text output from ComposerWebView and use of individual parts for
      composer markup.
    
    * ui/composer-web-view.js (ComposerPageState): Replace messageBody
      property and uses with bodyPart, signaturePart and quotePart. Set these
      content-editable on load. Move listeners from messageBody back to the
      document.body so they also listen for events on the additional
      parts. Keep track of text cursor location within the parts and set a
      class if so, to work around the lack of :focus-inside support.
      (ComposerPageState::updateSignature): Implement by updating the inner
      content of the signature part.
      (ComposerPageState::deleteQuotedMessage): Implement by removing the
      quote part from the DOM tree.
      (ComposerPageState::containsAttachmentKeyword): Consider only the
      bodyPart when scanning for attachments, remove hacks for ignoring the
      signature any any quoted message.
      (ComposerPageState::linkifyContent): Mirror ClientWebView change and
      replace with ::cleanContent. Ensure existing parts have contenteditable
      and focus class removed, remove signature and quote parts if empty.
      (ComposerPageState::getHtml): Generate HTML using clones of the three
      parts, so we can rmeove contenteditable and focus classes without
      modifying the actual DOM.
      (ComposerPageState::selectionChanged): Update focus class on parts as
      needed.

 src/client/composer/composer-web-view.vala       |  103 +++++--------
 src/client/composer/composer-widget.vala         |    2 +-
 src/engine/rfc822/rfc822-utils.vala              |    7 +-
 test/client/composer/composer-web-view-test.vala |   14 ++-
 test/js/composer-page-state-test.vala            |   48 ++++---
 ui/CMakeLists.txt                                |    1 +
 ui/composer-web-view.css                         |   60 ++++++++
 ui/composer-web-view.js                          |  176 ++++++++++++++-------
 8 files changed, 259 insertions(+), 152 deletions(-)
---
diff --git a/src/client/composer/composer-web-view.vala b/src/client/composer/composer-web-view.vala
index f86b0e4..67bdf64 100644
--- a/src/client/composer/composer-web-view.vala
+++ b/src/client/composer/composer-web-view.vala
@@ -16,53 +16,6 @@ public class ComposerWebView : ClientWebView {
     private const string CURSOR_CONTEXT_CHANGED = "cursorContextChanged";
     private const string DOCUMENT_MODIFIED = "documentModified";
 
-    private const string HTML_BODY = """
-        <html><head><title></title>
-        <style>
-        body {
-            margin: 0px !important;
-            padding: 0 !important;
-            background-color: white !important;
-            font-size: medium !important;
-        }
-        body.plain, body.plain * {
-            font-family: monospace !important;
-            font-weight: normal;
-            font-style: normal;
-            font-size: medium !important;
-            color: black;
-            text-decoration: none;
-        }
-        body.plain a {
-            cursor: text;
-        }
-        #message-body {
-            box-sizing: border-box;
-            padding: 10px;
-            outline: 0px solid transparent;
-            min-height: 100%;
-        }
-        blockquote {
-            margin-top: 0px;
-            margin-bottom: 0px;
-            margin-left: 10px;
-            margin-right: 10px;
-            padding-left: 5px;
-            padding-right: 5px;
-            background-color: white;
-            border: 0;
-            border-left: 3px #aaa solid;
-        }
-        pre {
-            white-space: pre-wrap;
-            margin: 0;
-        }
-        </style>
-        </head><body>
-        <div id="message-body" dir="auto">%s</div>
-        </body></html>""";
-
-
     /**
      * Encapsulates editing-related state for a specific DOM node.
      *
@@ -125,10 +78,14 @@ public class ComposerWebView : ClientWebView {
     }
 
 
+    private static WebKit.UserStyleSheet? app_style = null;
     private static WebKit.UserScript? app_script = null;
 
     public static void load_resources()
         throws Error {
+        ComposerWebView.app_style = ClientWebView.load_app_stylesheet(
+            "composer-web-view.css"
+        );
         ComposerWebView.app_script = ClientWebView.load_app_script(
             "composer-web-view.js"
         );
@@ -171,8 +128,8 @@ public class ComposerWebView : ClientWebView {
 
         add_events(Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK);
 
+        this.user_content_manager.add_style_sheet(ComposerWebView.app_style);
         this.user_content_manager.add_script(ComposerWebView.app_script);
-        // this.should_insert_text.connect(on_should_insert_text);
 
         register_message_handler(COMMAND_STACK_CHANGED, on_command_stack_changed);
         register_message_handler(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
@@ -187,39 +144,54 @@ public class ComposerWebView : ClientWebView {
                               string quote,
                               bool top_posting,
                               bool is_draft) {
-        const string CURSOR = "<span id=\"cursormarker\"></span>";
-        const string SPACER = "<br />";
+        const string HTML_PRE = """<html><body dir="auto">""";
+        const string HTML_POST = """</body></html>""";
+        const string BODY_PRE = """
+<div id="geary-body">""";
+        const string BODY_POST = """</div>
+""";
+        const string SIGNATURE = """
+<div id="geary-signature">%s</div>
+""";
+        const string QUOTE = """
+<div id="geary-quote"><br />%s</div>
+""";
+        const string CURSOR = "<div><span id=\"cursormarker\"></span><br /></div>";
+        const string SPACER = "<div><br /></div>";
 
         StringBuilder html = new StringBuilder();
+        html.append(HTML_PRE);
         if (!is_draft) {
-            if (!Geary.String.is_empty(body)) {
+            html.append(BODY_PRE);
+            bool have_body = !Geary.String.is_empty(body);
+            if (have_body) {
                 html.append(body);
-                html.append(SPACER);
-                html.append(SPACER);
             }
 
             if (!top_posting && !Geary.String.is_empty(quote)) {
+                if (have_body) {
+                    html.append(SPACER);
+                }
                 html.append(quote);
-                html.append(SPACER);
             }
 
+            html.append(SPACER);
             html.append(CURSOR);
+            html.append(SPACER);
+            html.append(BODY_POST);
 
             if (!Geary.String.is_empty(signature)) {
-                html.append(SPACER);
-                html.append(signature);
+                html.append_printf(SIGNATURE, signature);
             }
 
             if (top_posting && !Geary.String.is_empty(quote)) {
-                html.append(SPACER);
-                html.append(SPACER);
-                html.append(quote);
+                html.append_printf(QUOTE, quote);
             }
         } else {
             html.append(quote);
         }
-
-        base.load_html(HTML_BODY.printf(html.data));
+        html.append(HTML_POST);
+        base.load_html((string) html.data);
     }
 
     /**
@@ -440,10 +412,13 @@ public class ComposerWebView : ClientWebView {
     }
 
     /**
-     * Converts plain text URLs in the editor content into links.
+     * Cleans the editor content ready for sending.
+     *
+     * This modifies the DOM, so there's no going back after calling
+     * this.
      */
-    public async void linkify_content() throws Error {
-        this.call.begin(Geary.JS.callable("geary.linkifyContent"), null);
+    public async void clean_content() throws Error {
+        this.call.begin(Geary.JS.callable("geary.cleanContent"), null);
     }
 
     /**
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 94c4c50..0746c28 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -1268,7 +1268,7 @@ public class ComposerWidget : Gtk.EventBox {
 
         // Perform send.
         try {
-            yield this.editor.linkify_content();
+            yield this.editor.clean_content();
             yield this.account.send_email_async(yield get_composed_email());
         } catch (Error e) {
             GLib.message("Error sending email: %s", e.message);
diff --git a/src/engine/rfc822/rfc822-utils.vala b/src/engine/rfc822/rfc822-utils.vala
index 2b57333..8a50b28 100644
--- a/src/engine/rfc822/rfc822-utils.vala
+++ b/src/engine/rfc822/rfc822-utils.vala
@@ -257,7 +257,7 @@ public string quote_email_for_reply(Geary.Email email, string? quote, TextFormat
     }
 
     quoted += "<br />";
-    quoted += "\n" + quote_body(email, quote, true, format);
+    quoted += quote_body(email, quote, true, format);
 
     return quoted;
 }
@@ -276,7 +276,7 @@ public string quote_email_for_forward(Geary.Email email, string? quote, TextForm
         return "";
 
     string quoted = _("---------- Forwarded message ----------");
-    quoted += "\n\n";
+    quoted += "\n";
     string from_line = email_addresses_for_reply(email.from, format);
     if (!String.is_empty_or_whitespace(from_line))
         quoted += _("From: %s\n").printf(from_line);
@@ -289,11 +289,8 @@ public string quote_email_for_forward(Geary.Email email, string? quote, TextForm
     if (!String.is_empty_or_whitespace(cc_line))
         quoted += _("Cc: %s\n").printf(cc_line);
     quoted += "\n";  // A blank line between headers and body
-    
     quoted = quoted.replace("\n", "<br />");
-    
     quoted += quote_body(email, quote, false, format);
-    
     return quoted;
 }
 
diff --git a/test/client/composer/composer-web-view-test.vala 
b/test/client/composer/composer-web-view-test.vala
index 6fe7e79..f72a844 100644
--- a/test/client/composer/composer-web-view-test.vala
+++ b/test/client/composer/composer-web-view-test.vala
@@ -7,6 +7,8 @@
 
 public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
 
+    private const string BODY_TEMPLATE = """<div 
id="geary-body">%s<div><br></div><div><br></div><div><br></div></div>""";
+
     public ComposerWebViewTest() {
         base("ComposerWebViewTest");
         add_test("load_resources", load_resources);
@@ -45,7 +47,8 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
         load_body_fixture(html);
         this.test_view.get_html.begin((obj, ret) => { async_complete(ret); });
         try {
-            assert(this.test_view.get_html.end(async_result()) == html + "<br><br>");
+            assert(this.test_view.get_html.end(async_result()) ==
+                   BODY_TEMPLATE.printf(html));
         } catch (Error err) {
             print("Error: %s\n", err.message);
             assert_not_reached();
@@ -56,7 +59,7 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
         load_body_fixture("<p>para</p>");
         this.test_view.get_text.begin((obj, ret) => { async_complete(ret); });
         try {
-            assert(this.test_view.get_text.end(async_result()) == "para\n\n\n\n\n");
+            assert(this.test_view.get_text.end(async_result()) == "para\n\n\n\n\n\n");
         } catch (Error err) {
             print("Error: %s\n", err.message);
             assert_not_reached();
@@ -68,7 +71,7 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
         this.test_view.get_text.begin((obj, ret) => { async_complete(ret); });
         try {
             assert(this.test_view.get_text.end(async_result()) ==
-                   "pre\n\n> quote\n> \npost\n\n\n\n\n");
+                   "pre\n\n> quote\n> \npost\n\n\n\n\n\n");
         } catch (Error err) {
             print("Error: %s\n", err.message);
             assert_not_reached();
@@ -80,7 +83,7 @@ public class ComposerWebViewTest : ClientWebViewTestCase<ComposerWebView> {
         this.test_view.get_text.begin((obj, ret) => { async_complete(ret); });
         try {
             assert(this.test_view.get_text.end(async_result()) ==
-                   "pre\n\n> quote1\n> \n>> quote2\n>> \npost\n\n\n\n\n");
+                   "pre\n\n> quote1\n> \n>> quote2\n>> \npost\n\n\n\n\n\n");
         } catch (Error err) {
             print("Error: %s\n", err.message);
             assert_not_reached();
@@ -102,6 +105,7 @@ long long, long long, long.
 
 
 
+
 """);
         } catch (Error err) {
             print("Error: %s\n", err.message);
@@ -128,6 +132,7 @@ long long, long long, long.
 
 
 
+
 """);
         } catch (Error err) {
             print("Error: %s\n", err.message);
@@ -161,6 +166,7 @@ long, long, long, long, long, long, long, long, long, long,
 
 
 
+
 """);
         } catch (Error err) {
             print("Error: %s\n", err.message);
diff --git a/test/js/composer-page-state-test.vala b/test/js/composer-page-state-test.vala
index 9e34591..f931b6c 100644
--- a/test/js/composer-page-state-test.vala
+++ b/test/js/composer-page-state-test.vala
@@ -7,13 +7,16 @@
 
 class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
 
+    private const string COMPLETE_BODY_TEMPLATE = """<div 
id="geary-body">%s<div><br></div><div><br></div><div><br></div></div>""";
+    private const string CLEAN_BODY_TEMPLATE = "%s<div><br></div><div><br></div><div><br></div>";
+
     public ComposerPageStateTest() {
         base("ComposerPageStateTest");
         add_test("edit_context_font", edit_context_font);
         add_test("edit_context_link", edit_context_link);
         add_test("indent_line", indent_line);
         add_test("contains_attachment_keywords", contains_attachment_keywords);
-        add_test("linkify_content", linkify_content);
+        add_test("clean_content", clean_content);
         add_test("get_html", get_html);
         add_test("get_text", get_text);
         add_test("get_text_with_quote", get_text_with_quote);
@@ -81,17 +84,17 @@ class ComposerPageStateTest : ClientWebViewTestCase<ComposerWebView> {
     }
 
     public void contains_attachment_keywords() {
-        load_body_fixture("""
-<blockquote>inner quote</blockquote>
+        load_body_fixture_full("""
+<blockquote>innerquote</blockquote>
 
 <p>some text</p>
 
 some text
-
--- <br>sig
-
-<p>outerquote text<p>
-""");
+""",
+                          "-- <br>sig",
+                          "<p>outerquote text</p>",
+                          true
+            );
         try {
             assert(WebKitUtil.to_bool(run_javascript(
                 @"geary.containsAttachmentKeyword(\"some\", \"subject text\");"
@@ -117,7 +120,7 @@ some text
         }
     }
 
-    public void linkify_content() {
+    public void clean_content() {
         // XXX split these up into multiple tests
         load_body_fixture("""
 http://example1.com
@@ -141,12 +144,12 @@ unknown://example6.com
 <a href="blarg">http://example5.com</a>
 
 unknown://example6.com
-<br><br>""";
+""";
 
         try {
-            run_javascript("geary.linkifyContent();");
-            assert(WebKitUtil.to_string(run_javascript("geary.messageBody.innerHTML;")) ==
-                   expected);
+            run_javascript("geary.cleanContent();");
+            assert(WebKitUtil.to_string(run_javascript("geary.bodyPart.innerHTML;")) ==
+                   CLEAN_BODY_TEMPLATE.printf(expected));
         } catch (Geary.JS.Error err) {
             print("Geary.JS.Error: %s\n", err.message);
             assert_not_reached();
@@ -161,7 +164,7 @@ unknown://example6.com
         load_body_fixture(html);
         try {
             assert(WebKitUtil.to_string(run_javascript(@"window.geary.getHtml();")) ==
-                   html + "<br><br>");
+                   COMPLETE_BODY_TEMPLATE.printf(html));
         } catch (Geary.JS.Error err) {
             print("Geary.JS.Error: %s\n", err.message);
             assert_not_reached();
@@ -175,7 +178,7 @@ unknown://example6.com
         load_body_fixture("<p>para</p>");
         try {
             assert(WebKitUtil.to_string(run_javascript(@"window.geary.getText();")) ==
-                   "para\n\n\n\n");
+                   "para\n\n\n\n\n");
         } catch (Geary.JS.Error err) {
             print("Geary.JS.Error: %s\n", err.message);
             assert_not_reached();
@@ -190,7 +193,7 @@ unknown://example6.com
         load_body_fixture("<p>pre</p> <blockquote><p>quote</p></blockquote> <p>post</p>");
         try {
             assert(WebKitUtil.to_string(run_javascript(@"window.geary.getText();")) ==
-                   @"pre\n\n$(q_marker)quote\n$(q_marker)\npost\n\n\n\n");
+                   @"pre\n\n$(q_marker)quote\n$(q_marker)\npost\n\n\n\n\n");
         } catch (Geary.JS.Error err) {
             print("Geary.JS.Error: %s", err.message);
             assert_not_reached();
@@ -205,7 +208,7 @@ unknown://example6.com
         load_body_fixture("<p>pre</p> <blockquote><p>quote1</p> 
<blockquote><p>quote2</p></blockquote></blockquote> <p>post</p>");
         try {
             assert(WebKitUtil.to_string(run_javascript(@"window.geary.getText();")) ==
-                   
@"pre\n\n$(q_marker)quote1\n$(q_marker)\n$(q_marker)$(q_marker)quote2\n$(q_marker)$(q_marker)\npost\n\n\n\n");
+                   
@"pre\n\n$(q_marker)quote1\n$(q_marker)\n$(q_marker)$(q_marker)quote2\n$(q_marker)$(q_marker)\npost\n\n\n\n\n");
         } catch (Geary.JS.Error err) {
             print("Geary.JS.Error: %s\n", err.message);
             assert_not_reached();
@@ -343,8 +346,15 @@ unknown://example6.com
         return new ComposerWebView(this.config);
     }
 
-    protected override void load_body_fixture(string html = "") {
-        this.test_view.load_html(html, "", "", false, false);
+    protected override void load_body_fixture(string body = "") {
+        load_body_fixture_full(body, "", "", true);
+    }
+
+    protected void load_body_fixture_full(string body,
+                                          string sig,
+                                          string quote,
+                                          bool top_posting) {
+        this.test_view.load_html(body, sig, quote, top_posting, false);
         while (this.test_view.is_loading) {
             Gtk.main_iteration();
         }
diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt
index bc5c57d..dc41091 100644
--- a/ui/CMakeLists.txt
+++ b/ui/CMakeLists.txt
@@ -11,6 +11,7 @@ set(RESOURCE_LIST
   STRIPBLANKS "composer-link-popover.ui"
   STRIPBLANKS "composer-menus.ui"
   STRIPBLANKS "composer-widget.ui"
+              "composer-web-view.css"
               "composer-web-view.js"
   STRIPBLANKS "conversation-email.ui"
   STRIPBLANKS "conversation-email-attachment-view.ui"
diff --git a/ui/composer-web-view.css b/ui/composer-web-view.css
new file mode 100644
index 0000000..83c36e9
--- /dev/null
+++ b/ui/composer-web-view.css
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2017 Michael Gratton <mike vee net>
+ */
+
+body {
+  margin: 0 !important;
+  border: 0 !important;
+  padding: 0 !important;
+  background-color: #f9f9f9 !important;
+  font-size: medium !important;
+}
+
+body.plain, body.plain * {
+  font-family: monospace !important;
+  font-weight: normal;
+  font-style: normal;
+  font-size: medium !important;
+  color: black;
+  text-decoration: none;
+}
+
+body.plain a {
+  cursor: text;
+}
+
+body > div#geary-body {
+  margin: 0 !important;
+  border: 0 !important;
+  padding: 12px !important;
+  outline: 0px !important;
+}
+
+body > div#geary-signature,
+body > div#geary-quote {
+  margin: 0 6px !important;
+  border: 0 !important;
+  padding: 6px !important;
+}
+
+body > div.geary-focus {
+  background-color: white !important;
+}
+
+body > div#geary-signature.geary-focus,
+body > div#geary-quote.geary-focus {
+  outline: 1px dashed #ccc !important;
+}
+
+blockquote {
+  margin: 0 10px;
+  border: 0;
+  border-left: 3px #aaa solid;
+  padding: 0 5px;
+}
+
+pre {
+  white-space: pre-wrap;
+  margin: 0;
+}
diff --git a/ui/composer-web-view.js b/ui/composer-web-view.js
index e52ec38..a0ab097 100644
--- a/ui/composer-web-view.js
+++ b/ui/composer-web-view.js
@@ -12,7 +12,6 @@
 let ComposerPageState = function() {
     this.init.apply(this, arguments);
 };
-ComposerPageState.BODY_ID = "message-body";
 ComposerPageState.KEYWORD_SPLIT_REGEX = /[\s]+/g;
 ComposerPageState.QUOTE_START = "\x91";  // private use one
 ComposerPageState.QUOTE_END = "\x92";    // private use two
@@ -26,7 +25,11 @@ ComposerPageState.prototype = {
     __proto__: PageState.prototype,
     init: function() {
         PageState.prototype.init.apply(this, []);
-        this.messageBody = null;
+        this.bodyPart = null;
+        this.signaturePart = null;
+        this.quotePart = null;
+        this.focusedPart = null;
+
         this.undoEnabled = false;
         this.redoEnabled = false;
         this.selections = new Map();
@@ -55,12 +58,19 @@ ComposerPageState.prototype = {
     loaded: function() {
         let state = this;
 
-        this.messageBody = document.getElementById(ComposerPageState.BODY_ID);
+        this.bodyPart = document.getElementById("geary-body");
+        if (this.bodyPart == null) {
+            this.bodyPart = document.body;
+        }
+
+        this.signaturePart = document.getElementById("geary-signature");
+        this.quotePart = document.getElementById("geary-quote");
+
         // Should be using 'e.key' in listeners below instead of
         // keyIdentifier, but that was only fixed in WK in Oct 2016
         // (WK Bug 36267). Migrate to that when we can rely on it
         // being in WebKitGTK.
-        this.messageBody.addEventListener("keydown", function(e) {
+        document.body.addEventListener("keydown", function(e) {
             if (e.keyIdentifier == "U+0009" &&// Tab
                 !e.ctrlKey && !e.altKey && !e.metaKey) {
                 if (!e.shiftKey) {
@@ -74,7 +84,7 @@ ComposerPageState.prototype = {
         // We can't use keydown for this, captured or bubbled, since
         // that will also cause the line that the cursor is currenty
         // positioned on when Enter is pressed to also be outdented.
-        this.messageBody.addEventListener("keyup", function(e) {
+        document.body.addEventListener("keyup", function(e) {
             if (e.keyIdentifier == "Enter" && !e.shiftKey) {
                 // XXX WebKit seems to support both InsertNewline and
                 // InsertNewlineInQuotedContent arguments for
@@ -101,6 +111,7 @@ ComposerPageState.prototype = {
 
         // Focus within the HTML document
         document.body.focus();
+        this.updateFocusClass(this.bodyPart);
 
         // Set text cursor at appropriate position
         let cursor = document.getElementById("cursormarker");
@@ -117,14 +128,20 @@ ComposerPageState.prototype = {
 
         // Enable editing and observation machinery only after
         // modifying the body above.
-        this.messageBody.contentEditable = true;
+        this.bodyPart.contentEditable = true;
+        if (this.signaturePart != null) {
+            this.signaturePart.contentEditable = true;
+        }
+        if (this.quotePart != null) {
+            this.quotePart.contentEditable = true;
+        }
         let config = {
             attributes: true,
             childList: true,
             characterData: true,
             subtree: true
         };
-        this.bodyObserver.observe(this.messageBody, config);
+        this.bodyObserver.observe(document.body, config);
 
         // Chain up
         PageState.prototype.loaded.apply(this, []);
@@ -184,20 +201,21 @@ ComposerPageState.prototype = {
         }
     },
     updateSignature: function(signature) {
-        // XXX need mark the sig somehow so we can find it, select
-        // it and replace it using execCommand
+        if (this.signaturePart != null) {
+            console.log(signature);
+            this.signaturePart.innerHTML = signature;
+        }
     },
     deleteQuotedMessage: function() {
-        // XXX need mark the quote somehow so we can find it, select
-        // it and delete it using execCommand
+        if (this.quotePart != null) {
+            this.quotePart.parentNode.removeChild(this.quotePart);
+            this.quotePart = null;
+        }
     },
     /**
      * Determines if subject or body content refers to attachments.
      */
     containsAttachmentKeyword: function(keywordSpec, subject) {
-        // XXX this could also use a structured representation of the
-        // message body so we don't need to text to check
-
         let ATTACHMENT_KEYWORDS_SUFFIX = "doc|pdf|xls|ppt|rtf|pps";
 
         let completeKeys = new Set(keywordSpec.toLocaleLowerCase().split("|"));
@@ -209,57 +227,30 @@ ComposerPageState.prototype = {
         }
 
         // Check interesting body text
+        let node = this.bodyPart.firstChild;
+        let content = [];
         let breakingElements = new Set([
             "BR", "P", "DIV", "BLOCKQUOTE", "TABLE", "OL", "UL", "HR"
         ]);
-        let content = this.messageBody.firstChild;
-        let found = false;
-        let done = false;
-        let textContent = [];
-        while (content != null && !done) {
-            if (content.nodeType == Node.TEXT_NODE) {
-                textContent.push(content.textContent);
+        while (node != null) {
+            if (node.nodeType == Node.TEXT_NODE) {
+                content.push(node.textContent);
             } else if (content.nodeType == Node.ELEMENT_NODE) {
-                let isBreaking = breakingElements.has(content.nodeName);
+                let isBreaking = breakingElements.has(node.nodeName);
                 if (isBreaking) {
-                    textContent.push("\n");
+                    content.push("\n");
                 }
 
-                // Always exclude quoted text
+                // Only include non-quoted text
                 if (content.nodeName != "BLOCKQUOTE") {
-                    textContent.push(content.innerText);
-                }
-
-                if (isBreaking || content.nextSibling == null) {
-                    for (let line of textContent.join("").split("\n")) {
-                        // Ignore everything after a sig or a
-                        // forwarded message.
-                        // XXX This breaks if the user types this at
-                        // the start of a line, also, WK's innerText
-                        // impl strips trailing whitespace, so can't
-                        // test for 'line == "-- "' :(
-                        if (line.startsWith("--")) {
-                            done = true;
-                            break;
-                        }
-
-                        line = line.trim();
-                        if (line != "") {
-                            if (ComposerPageState.containsKeywords(line, completeKeys, suffixKeys)) {
-                                found = true;
-                                done = true;
-                                break;
-                            }
-                        }
-                    }
-                    textContent = [];
+                    content.push(content.textContent);
                 }
             }
-
-            content = content.nextSibling;
+            node = node.nextSibling;
         }
-
-        return found;
+        return ComposerPageState.containsKeywords(
+            content.join(""), completeKeys, suffixKeys
+        );
     },
     tabOut: function() {
         document.execCommand(
@@ -284,7 +275,7 @@ ComposerPageState.prototype = {
         // execCommand affecting the DOM srtcuture
         let count = 0;
         let node = SelectionUtil.getCursorElement();
-        while (node != this.messageBody) {
+        while (node != document.body) {
             if (node.nodeName == "BLOCKQUOTE") {
                 count++;
             }
@@ -295,14 +286,39 @@ ComposerPageState.prototype = {
             count--;
         }
     },
-    linkifyContent: function() {
-        ComposerPageState.linkify(this.messageBody);
+    cleanContent: function() {
+        ComposerPageState.cleanPart(this.bodyPart, false);
+        ComposerPageState.linkify(this.bodyPart);
+
+        this.signaturePart = ComposerPageState.cleanPart(this.signaturePart, true);
+        this.quotePart = ComposerPageState.cleanPart(this.quotePart, true);
     },
     getHtml: function() {
-        return this.messageBody.innerHTML;
+        // Clone the message parts so we can clean them without
+        // modifiying the DOM, needed when saving drafts. In contrast
+        // with cleanContent above, we don't remove empty elements so
+        // they still exist when restoring from draft
+        let parent = document.createElement("DIV");
+        parent.appendChild(
+            ComposerPageState.cleanPart(this.bodyPart.cloneNode(true), false)
+        );
+
+        if (this.signaturePart != null) {
+            parent.appendChild(
+                ComposerPageState.cleanPart(this.signaturePart.cloneNode(true), false)
+            );
+        }
+
+        if (this.quotePart != null) {
+            parent.appendChild(
+                ComposerPageState.cleanPart(this.quotePart.cloneNode(true), false)
+            );
+        }
+
+        return parent.innerHTML;
     },
     getText: function() {
-        return ComposerPageState.htmlToQuotedText(this.messageBody);
+        return ComposerPageState.htmlToQuotedText(document.body);
     },
     setRichText: function(enabled) {
         if (enabled) {
@@ -330,6 +346,8 @@ ComposerPageState.prototype = {
         PageState.prototype.selectionChanged.apply(this, []);
 
         let cursor = SelectionUtil.getCursorElement();
+
+        // Update cursor context
         if (cursor != null) {
             let newContext = new EditContext(cursor);
             if (!newContext.equals(this.cursorContext)) {
@@ -339,6 +357,30 @@ ComposerPageState.prototype = {
                 );
             }
         }
+
+        while (cursor != null) {
+            let parent = cursor.parentNode;
+            if (parent == document.body) {
+                this.updateFocusClass(cursor);
+                break;
+            }
+            cursor = parent;
+        }
+    },
+    /**
+     * Work around WebKit note yet supporting :focus-inside pseudoclass.
+     */
+    updateFocusClass: function(newFocus) {
+        if (this.focusedPart != null) {
+            this.focusedPart.classList.remove("geary-focus");
+            this.focusedPart = null;
+        }
+        if (newFocus == this.bodyPart ||
+            newFocus == this.signaturePart ||
+            newFocus == this.quotePart) {
+            this.focusedPart = newFocus;
+            this.focusedPart.classList.add("geary-focus");
+        }
     }
 };
 
@@ -376,6 +418,22 @@ ComposerPageState.containsKeywords = function(line, completeKeys, suffixKeys) {
 };
 
 /**
+ * Removes internal attributes from a composer part..
+ */
+ComposerPageState.cleanPart = function(part, removeIfEmpty) {
+    if (part != null) {
+        part.removeAttribute("class");
+        part.removeAttribute("contenteditable");
+
+        if (removeIfEmpty && part.innerText.trim() == "") {
+            part.parentNode.removeChild(part);
+            part = null;
+        }
+    }
+    return part;
+};
+
+/**
  * Convert a HTML DOM tree to plain text with delineated quotes.
  *
  * Lines are delinated using LF. Quoted lines are prefixed with


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]