From b4c4a49206b9819bdb6d5b9f74a9287188da9ee2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Fri, 10 May 2013 22:45:21 -0400
Subject: [PATCH] Issue #1678002-110
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

commit 618773184ff131ec3740bd297e686df3b5078dee
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri May 10 22:42:45 2013 -0400

    I think that's it!

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 983e987251b573f3709fea6889585bd8465cf8f2
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri May 10 19:07:59 2013 -0400

    Saving works better now.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 216beb85311f088b0879b4a89bdea2c02d8607e3
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri May 10 18:40:19 2013 -0400

    Saving is mostly ok.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit e1a0de772671ed73aa68d3b360869ca873c1dec1
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Fri May 10 18:22:14 2013 -0400

    continuing to work on saving

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 20428751e5859124d3ad3750225f09f236e46656
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu May 9 16:00:42 2013 -0400

    Cleaned up button styling.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 792e08036a8d99b0417236df2b8f7a749522a720
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu May 9 15:35:32 2013 -0400

    Stop the EntityToolbar from repositioning when it's hovered.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 31ea486e4e89e3c70141ceaa3eaff2f3ce49e7ed
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu May 9 15:24:41 2013 -0400

    Further improvements to entity save. Piped through the editSavedEntity command.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit bed63b61aa0497d1ccc39540ca1185590c124353
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu May 9 14:18:28 2013 -0400

    Changed fields are now marked.

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

commit 0346f8b5c961abfda26cc44631c52fc08c8723ca
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Thu May 9 00:54:34 2013 -0400

    Issue #1678002-107

    commit a61d2300f029625d6863ebd1b053660c7e510d72
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Thu May 9 00:50:43 2013 -0400

        Dealing with marking the fields changed.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit b155a45153e4ba6c919ed68fb18d9272d22fdbfe
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Thu May 9 00:22:25 2013 -0400

        I think it's actually working.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 340750c9d3125a0eb10d6e09d40bf089eda4a6a9
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Thu May 9 00:02:06 2013 -0400

        Almost there.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 3ac4b0fd8d681c58d4109eacc397122975983142
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Wed May 8 23:10:35 2013 -0400

        Switching between fields works.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 444467a8d6bb65cacf0b04aed64fdb5816550b53
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Wed May 8 19:11:24 2013 -0400

        Clicking between fields works.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 39176d5a8e05beec39efcb00f3352403924929a3
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Wed May 8 18:58:49 2013 -0400

        Cleaner positioning.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit cc7c8ade7ab329ff239237eeb261b570734da852
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Wed May 8 18:44:44 2013 -0400

        Moving closer to a nice toolbar.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 3d31f89837f599b5d28c8bfd1f0b41cf5c4da4b7
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Wed May 8 16:03:31 2013 -0400

        it's dirty pants but it does the job. The entity title is now displayed in the entity toolbar.

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 18865e9b71d30d615c8d3c5da429a5858c60206d
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Tue May 7 23:49:33 2013 -0400

        Issue #1678002-98

        commit 4b2902b7f45267b67ef8249edb07723312dcb737
        Author: J. Renée Beach <splendidnoise@gmail.com>
        Date:   Tue May 7 23:38:26 2013 -0400

            Positioning of the toolbar is really smooth now.

            Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

        commit c8f368bca89dd47cd5ca1b6d48cf622d1776ef62
        Author: J. Renée Beach <splendidnoise@gmail.com>
        Date:   Tue May 7 15:56:49 2013 -0400

            1678002-a

            Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    commit 0460f2174e3c5b5db8ba330e946947cbcb24ff0f
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Tue May 7 23:47:08 2013 -0400

        1901100-24

        Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

    Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/modules/edit/css/edit.css                     |  240 +++++--------
 core/modules/edit/edit.module                      |    3 +
 core/modules/edit/js/edit.js                       |    3 +-
 core/modules/edit/js/editors/directEditor.js       |    4 +-
 core/modules/edit/js/editors/formEditor.js         |   11 +-
 core/modules/edit/js/models/EntityModel.js         |   84 ++++-
 core/modules/edit/js/theme.js                      |   51 ++-
 core/modules/edit/js/util.js                       |    4 -
 core/modules/edit/js/views/AppView.js              |  222 ++++++++++--
 core/modules/edit/js/views/EditorDecorationView.js |   83 ++---
 core/modules/edit/js/views/EditorView.js           |   15 +-
 core/modules/edit/js/views/EntityToolbarView.js    |  358 ++++++++++++++++++++
 core/modules/edit/js/views/FieldToolbarView.js     |  282 +++------------
 core/modules/edit/js/views/ModalView.js            |    2 +-
 .../edit/lib/Drupal/edit/EditController.php        |   15 +-
 .../edit/lib/Drupal/edit/MetadataGenerator.php     |   12 +-
 .../lib/Drupal/edit/MetadataGeneratorInterface.php |    4 +-
 .../editor/js/editor.formattedTextEditor.js        |    4 +-
 18 files changed, 893 insertions(+), 504 deletions(-)
 create mode 100644 core/modules/edit/js/views/EntityToolbarView.js

diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css
index 6a5ac83..6a34814 100644
--- a/core/modules/edit/css/edit.css
+++ b/core/modules/edit/css/edit.css
@@ -6,72 +6,40 @@
 }
 
 .edit-animate-fast {
--webkit-transition: all .2s ease;
-   -moz-transition: all .2s ease;
-    -ms-transition: all .2s ease;
-     -o-transition: all .2s ease;
-        transition: all .2s ease;
+  -webkit-transition: all .2s ease;
+  transition: all .2s ease;
 }
 
 .edit-animate-default {
   -webkit-transition: all .4s ease;
-     -moz-transition: all .4s ease;
-      -ms-transition: all .4s ease;
-       -o-transition: all .4s ease;
-          transition: all .4s ease;
+  transition: all .4s ease;
 }
 
 .edit-animate-slow {
--webkit-transition: all .6s ease;
-   -moz-transition: all .6s ease;
-    -ms-transition: all .6s ease;
-     -o-transition: all .6s ease;
-        transition: all .6s ease;
+  -webkit-transition: all .6s ease;
+  transition: all .6s ease;
 }
 
 .edit-animate-delay-veryfast {
   -webkit-transition-delay: .05s;
-     -moz-transition-delay: .05s;
-      -ms-transition-delay: .05s;
-       -o-transition-delay: .05s;
-          transition-delay: .05s;
+  transition-delay: .05s;
 }
 
 .edit-animate-delay-fast {
   -webkit-transition-delay: .2s;
-     -moz-transition-delay: .2s;
-      -ms-transition-delay: .2s;
-       -o-transition-delay: .2s;
-          transition-delay: .2s;
+  transition-delay: .2s;
 }
 
 .edit-animate-disable-width {
   -webkit-transition: width 0s;
-     -moz-transition: width 0s;
-      -ms-transition: width 0s;
-       -o-transition: width 0s;
-          transition: width 0s;
+  transition: width 0s;
 }
 
 .edit-animate-only-visibility {
   -webkit-transition: opacity .2s ease;
-     -moz-transition: opacity .2s ease;
-      -ms-transition: opacity .2s ease;
-       -o-transition: opacity .2s ease;
-          transition: opacity .2s ease;
+  transition: opacity .2s ease;
 }
 
-.edit-animate-only-background-and-padding {
-  -webkit-transition: background, padding .2s ease;
-     -moz-transition: background, padding .2s ease;
-      -ms-transition: background, padding .2s ease;
-       -o-transition: background, padding .2s ease;
-          transition: background, padding .2s ease;
-}
-
-
-
-
 /**
  * Candidate editables + editables being edited.
  *
@@ -89,25 +57,29 @@
 }
 .edit-field.edit-editable,
 .edit-field .edit-editable {
-  box-shadow: 0 0 1px 1px #4d9de9;
+  box-shadow: 0 0 1px 2px #4d9de9;
 }
 
 /* Highlighted (hovered) editable. */
 .edit-editable.edit-highlighted {
   z-index: 305;
-  min-width: 200px;
 }
-.edit-field.edit-editable.edit-highlighted,
-.edit-form.edit-editable.edit-highlighted,
-.edit-field .edit-editable.edit-highlighted {
-  box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
+.edit-field.edit-highlighted,
+.edit-form.edit-highlighted,
+.edit-field .edit-highlighted {
+  box-shadow: 0 0 1px 2px #0199ff, 0 0 3px 5px rgba(153, 153, 153, .5);
 }
-.edit-field.edit-editable.edit-highlighted.edit-validation-error,
-.edit-form.edit-editable.edit-highlighted.edit-validation-error,
-.edit-field .edit-editable.edit-highlighted.edit-validation-error {
-  box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
+.edit-field.edit-changed,
+.edit-form.edit-changed,
+.edit-field .edit-changed {
+  box-shadow: 0 0 1px 2px orange, 0 0 3px 5px rgba(153, 153, 153, .5);
+}
+.edit-field.edit-validation-error,
+.edit-form.edit-validation-error,
+.edit-field .edit-validation-error {
+  box-shadow: 0 0 1px 2px red, 0 0 3px 5px rgba(153, 153, 153, .5);
 }
-.edit-form.edit-editable .form-item .error {
+.edit-form .form-item .error {
   border: 1px solid #eea0a0;
 }
 
@@ -210,11 +182,25 @@
  * Edit mode: toolbars
  */
 
-/* Trick: wrap statically positioned elements in relatively positioned element
-   without changing its location. This allows us to absolutely position the
-   toolbar.
-*/
-.edit-toolbar-container,
+/**
+ * Entity toolbar.
+ */
+.edit-toolbar-container {
+  border: 1px solid #a8a8a8;
+  background-color: white;
+  border: 1px solid #ababab;
+  box-shadow: 2px 2px 4px -2px black, 2px 2px 12px 0px hsla(40, 10%, 70%, 1);
+  max-width: 100%;
+  position: absolute;
+  -webkit-transition: all 0.5s;
+  transition: all 0.5s;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  width: 40em;
+  z-index: 350;
+}
 .edit-form-container {
   position: relative;
   padding: 0;
@@ -223,51 +209,21 @@
   vertical-align: baseline;
   z-index: 310;
 }
-.edit-toolbar-container {
-  -webkit-user-select: none;
-   -khtml-user-select: none;
-     -moz-user-select: none;
-      -ms-user-select: none;
-       -o-user-select: none;
-          user-select: none;
-}
-
-.edit-toolbar-heightfaker {
-  height: auto;
-  position: absolute;
-  bottom: 1px;
-  box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
-  background: #fff;
-  display: none;
+.edit-toolgroup.ops {
+  float: right; /* LTR */
 }
-.edit-highlighted .edit-toolbar-heightfaker {
-  display: block;
+.edit-toolbar-label {
+  overflow: hidden;
+  padding: 0.333em 0.5em;
 }
 
 /* The toolbar; these are not necessarily visible. */
 .edit-toolbar {
-  position: relative;
-  height: 100%;
   font-family: 'Droid sans', 'Lucida Grande', sans-serif;
 }
-.edit-toolbar-heightfaker {
-  clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */
-}
-/* Exception: when the toolbar is instructed to be "full width". */
-.edit-toolbar-fullwidth .edit-toolbar-heightfaker {
-  width: 100%;
-  clip: auto;
-}
-
-
-/* The toolbar contains toolgroups; these are visible. */
-.edit-toolgroup {
-  float: left; /* LTR */
-}
 
 /* Info toolgroup. */
 .edit-toolgroup.info {
-  float: left; /* LTR */
   font-weight: bolder;
   padding: 0 5px;
   background: #fff url('../images/throbber.gif') no-repeat -60px 60px;
@@ -276,97 +232,77 @@
   padding-right: 35px;
   background-position: 90% 50%;
 }
-
-/* Operations toolgroup. */
-.edit-toolgroup.ops {
-  float: right; /* LTR */
-  margin-left: 5px;
+.edit-toolbar-fullwidth {
+  width: 100%;
 }
-
 .edit-toolgroup.wysiwyg-floated {
   float: right;
 }
 .edit-toolgroup.wysiwyg-main {
-  clear: left;
+  clear: both;
   width: 100%;
   padding-left: 0;
 }
 
-
-
 /**
  * Edit mode: buttons (in both modal and toolbar).
  */
-#edit_modal button,
-.edit-toolbar button {
-  float: left; /* LTR */
-  display: block;
-  height: 29px;
+.edit-button {
+  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
+  background-image: -moz-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
+  background-image: linear-gradient(top, #f5f5f5 0%, #ccc 100%);
+  border: 1px solid #fff;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+  font-size: 1em;
+  min-height: 29px;
   min-width: 29px;
+  opacity: 1;
   padding: 3px 6px 6px 6px;
-  margin: 4px 5px 1px 0;
-  border: 1px solid #fff;
-  border-radius: 3px;
-  color: white;
   text-decoration: none;
-  font-size: 13px;
-  cursor: pointer;
+  -webkit-transition: all .1s ease;
+  transition: all .1s ease;
 }
-#edit_modal button {
-  float: none;
-  display: inline-block;
+.edit-button[aria-hidden="true"] {
+  visibility: hidden;
+  opacity: 0;
 }
-
 /* Button with icons. */
-#edit_modal button span,
-.edit-toolbar button span {
-  width: 22px;
-  height: 19px;
-  display: block;
-  float: left;
+#edit_modal .action-cancel span,
+.edit-toolbar .action-cancel span {
+  display: inline-block;
+  min-width: 18px;
 }
 .edit-toolbar span.close {
   background: url('../images/close.png') no-repeat 3px 2px;
   text-indent: -999em;
   direction: ltr;
 }
-
-.edit-toolbar button.blank-button {
-  color: black;
-  background-color: #fff;
-  font-weight: bolder;
-}
-
-#edit_modal button.blue-button,
-.edit-toolbar button.blue-button {
+.edit-button.action-save {
   color: white;
   background-image: -webkit-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
   background-image: -moz-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
   background-image: linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
-  border-radius: 5px;
 }
-
-#edit_modal button.gray-button,
-.edit-toolbar button.gray-button {
-  color: #666;
-  background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
-  background-image: -moz-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
-  background-image: linear-gradient(top, #f5f5f5 0%, #ccc 100%);
-  border-radius: 5px;
+.edit-button.action-saving {
+  background-image: -webkit-linear-gradient(top, #dddddd 0%, #c0c0c0 100%);
+  background-image: -moz-linear-gradient(top, #dddddd 0%, #c0c0c0 100%);
+  background-image: linear-gradient(top, #dddddd 0%, #c0c0c0 100%);
 }
-
-#edit_modal button.blue-button:hover,
-.edit-toolbar button.blue-button:hover,
-#edit_modal button.blue-button:active,
-.edit-toolbar button.blue-button:active {
-  border: 1px solid #55a5d3;
-  box-shadow: 0 2px 1px rgba(0,0,0,0.2);
+.edit-button.action-saving .ajax-progress {
+  padding: 0px 4px 0px 6px;
 }
-
-#edit_modal button.gray-button:hover,
-.edit-toolbar button.gray-button:hover,
-#edit_modal button.gray-button:active,
-.edit-toolbar button.gray-button:active {
+.edit-button.action-saving .ajax-progress .throbber {
+  padding: 0 6px;
+}
+.edit-button:hover,
+.edit-button:active {
   border: 1px solid #cdcdcd;
   box-shadow: 0 2px 1px rgba(0,0,0,0.1);
 }
+.edit-button.action-save:hover,
+.edit-button.action-save:active {
+  border: 1px solid #55a5d3;
+  box-shadow: 0 2px 1px rgba(0,0,0,0.2);
+}
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index 6c80f31..981dc7d 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -78,6 +78,7 @@ function edit_library_info() {
       // Views.
       $path . '/js/views/AppView.js' => $options,
       $path . '/js/views/EditorDecorationView.js' => $options,
+      $path . '/js/views/EntityToolbarView.js' => $options,
       $path . '/js/views/ContextualLinkView.js' => $options,
       $path . '/js/views/ModalView.js' => $options,
       $path . '/js/views/FieldToolbarView.js' => $options,
@@ -104,8 +105,10 @@ function edit_library_info() {
       array('system', 'underscore'),
       array('system', 'backbone'),
       array('system', 'jquery.form'),
+      array('system', 'jquery.ui.position'),
       array('system', 'drupal.form'),
       array('system', 'drupal.ajax'),
+      array('system', 'drupal.debounce'),
       array('system', 'drupalSettings'),
     ),
   );
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index c68ebb3..17a99a7 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -303,7 +303,8 @@ function initializeEntityContextualLink (contextualLink) {
   else if (hasFieldWithPermission(editIDs)) {
     var entityModel = new Drupal.edit.EntityModel({
       id: contextualLink.entityID,
-      el: contextualLink.region
+      el: contextualLink.region,
+      title: Drupal.edit.Metadata.get(contextualLink.entityID, 'title')
     });
     Drupal.edit.collections.entities.add(entityModel);
 
diff --git a/core/modules/edit/js/editors/directEditor.js b/core/modules/edit/js/editors/directEditor.js
index 61fdf95..815f8c1 100644
--- a/core/modules/edit/js/editors/directEditor.js
+++ b/core/modules/edit/js/editors/directEditor.js
@@ -48,7 +48,7 @@ Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  stateChange: function (model, state) {
+  stateChange: function (model, state, options) {
     var from = model.previous('state');
     var to = state;
     switch (to) {
@@ -81,7 +81,7 @@ Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({
         if (from === 'invalid') {
           this.removeValidationErrors();
         }
-        this.save();
+        this.save(options);
         break;
       case 'saved':
         break;
diff --git a/core/modules/edit/js/editors/formEditor.js b/core/modules/edit/js/editors/formEditor.js
index b356135..ac741cd 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -16,11 +16,12 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  stateChange: function (model, state) {
+  stateChange: function (model, state, options) {
     var from = model.previous('state');
     var to = state;
     switch (to) {
       case 'inactive':
+        this.disable();
         break;
       case 'candidate':
         if (from !== 'inactive') {
@@ -40,7 +41,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       case 'changed':
         break;
       case 'saving':
-        this.save();
+        this.save(options);
         break;
       case 'saved':
         break;
@@ -129,10 +130,11 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  save: function () {
+  save: function (options) {
     var $formContainer = this.model.attributes.editorView.$formContainer;
     var $submit = $formContainer.find('.edit-form-submit');
     var base = $submit.attr('id');
+    var callback = (options || {}).callback || function () {};
     var that = this;
 
     // Successfully saved.
@@ -144,6 +146,9 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       // Then, set the 'html' attribute on the field model. This will cause the
       // field to be rerendered.
       that.model.set('html', response.data);
+
+      // Invoke the optional callback.
+      callback.call();
      };
 
     // Unsuccessfully saved; validation errors.
diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js
index 9edfea3..7e8103f 100644
--- a/core/modules/edit/js/models/EntityModel.js
+++ b/core/modules/edit/js/models/EntityModel.js
@@ -21,22 +21,102 @@ $.extend(Drupal.edit, {
       // Indicates whether this instance of this entity is currently being
       // edited.
       isActive: false,
+      //
+      isDirty: false,
+      // The current processing state of an entity.
+      state: 'inactive',
       // A Drupal.edit.FieldCollection for all fields of this entity.
       fields: null
     },
+
+    /**
+     *
+     */
     initialize: function () {
+
       this.set('fields', new Drupal.edit.FieldCollection());
+
+      // Instantiate configuration for state handling.
+      // @see Drupal.edit.FieldModel.states
+      // @todo, these shouldn't be defined here. currently the position method
+      // is using them to find an active field. The FieldCollection should have a
+      // method that returns this.
+      this.activeEditorStates = ['activating', 'active', 'changed'];
+      this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates);
+
+      // Respond to field view changes.
+      this.on('viewChanged', this.viewChange, this);
+
+      // The state of the entity is largely dependent on the state of its
+      // fields.
+      this.get('fields').on('change:state', this.fieldStateChange, this);
+
+      // The entity keeps its own state progression.
+      this.on('change:state', this.stateChange, this);
     },
+
+    /**
+     *
+     */
+    fieldStateChange: function (model, state, options) {
+      if (state === 'changed') {
+        this.set('isDirty', true);
+      }
+      this.set('state', state);
+    },
+
     destroy: function(options) {
       if (this.get('isActive')) {
         throw new Error("EntityModel cannot be destroyed while it is being edited.");
       }
       Backbone.Model.prototype.destroy.apply(this, options);
 
+      // @todo that app should be responisble for destroying the fields.
       // Destroy all fields of this entity.
-      this.get('fields').each(function (fieldModel) {
+      /*this.get('fields').each(function (fieldModel) {
         fieldModel.destroy();
-      });
+      });*/
+    },
+
+    /**
+     * Listens to FieldModel editor state changes.
+     *
+     * @param Drupal.edit.FieldModel model
+     * @param String state
+     *   The state of an editable element. Used to determine display and behavior.
+     */
+    stateChange: function (model, state, options) {
+      var from = model.previous('state');
+      var to = state;
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          break;
+        case 'highlighted':
+          break;
+        case 'activating':
+          break;
+        case 'active':
+          break;
+        case 'changed':
+          break;
+        case 'saving':
+          break;
+        case 'saved':
+          break;
+        case 'invalid':
+          break;
+        default:
+          break;
+      }
+    },
+
+    /**
+     *
+     */
+    viewChange: function (view) {
+      this.trigger('fieldViewChange', view);
     }
   }),
 
diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js
index 7bef553..8d7ec22 100644
--- a/core/modules/edit/js/theme.js
+++ b/core/modules/edit/js/theme.js
@@ -64,17 +64,31 @@ Drupal.theme.editModal = function(settings) {
  * @return
  *   The corresponding HTML.
  */
-Drupal.theme.editToolbarContainer = function(settings) {
+Drupal.theme.editEntityToolbar = function(settings) {
   var html = '';
-  html += '<div id="' + settings.id + '" class="edit-toolbar-container">';
-  html += '  <div class="edit-toolbar-heightfaker edit-animate-fast">';
-  html += '    <div class="edit-toolbar primary" />';
-  html += '  </div>';
+  html += '<div id="' + settings.id + '" class="edit-toolbar-container clearfix">';
+  html += '<div class="edit-toolbar edit-toolbar-entity clearfix">';
+  html += '<div class="edit-toolbar-label" />';
+  html += '</div>';
+  html += '<div class="edit-toolbar edit-toolbar-field clearfix" />';
   html += '</div>';
   return html;
 };
 
 /**
+ * Theme function for a toolbar container of the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - id: the id to apply to the toolbar container.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editFieldToolbar = function(settings) {
+  return '<div id="' + settings.id + '" />';
+};
+
+/**
  * Theme function for a toolbar toolgroup of the Edit module.
  *
  * @param settings
@@ -86,9 +100,11 @@ Drupal.theme.editToolbarContainer = function(settings) {
  *   The corresponding HTML.
  */
 Drupal.theme.editToolgroup = function(settings) {
-  var classes = 'edit-toolgroup edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast';
+  // Classes.
+  var classes = (settings.classes || []);
+  classes.unshift('edit-toolgroup');
   var html = '';
-  html += '<div class="' + classes + ' ' + settings.classes + '"';
+  html += '<div class="' + classes.join(' ') + '"';
   if (settings.id) {
     html += ' id="' + settings.id + '"';
   }
@@ -120,12 +136,20 @@ Drupal.theme.editButtons = function(settings) {
     if (!button.hasOwnProperty('type')) {
       button.type = 'button';
     }
+    // Attributes.
+    var attributes = [];
+    var attrMap  = settings.buttons[i].attributes || {};
+    for (var attr in attrMap) {
+      if (attrMap.hasOwnProperty(attr)) {
+        attributes.push(attr + ((attrMap[attr]) ? '="' + attrMap[attr] + '"' : '' ));
+      }
+    }
 
-    html += '<button type="' + button.type + '" class="' + button.classes + '"';
+    html += '<button type="' + button.type + '" class="' + button.classes + '"'  + (attributes.length && (' ' + attributes.join(' ')));
     html += (button.action) ? ' data-edit-modal-action="' + button.action + '"' : '';
-    html += '>';
+    html += '><span>';
     html +=    button.label;
-    html += '</button>';
+    html += '</span></button>';
   }
   return html;
 };
@@ -152,4 +176,11 @@ Drupal.theme.editFormContainer = function(settings) {
   return html;
 };
 
+/**
+ *
+ */
+Drupal.theme.editThrobber = function (message) {
+  return '<div class="ajax-progress ajax-progress-throbber"><div class="throbber"><span class="element-hidden">' + (message || '') + '</span></div></div>';
+}
+
 })(jQuery, Drupal);
diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js
index 7e1bd57..d287a1d 100644
--- a/core/modules/edit/js/util.js
+++ b/core/modules/edit/js/util.js
@@ -12,10 +12,6 @@ Drupal.edit.util = Drupal.edit.util || {};
 Drupal.edit.util.constants = {};
 Drupal.edit.util.constants.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit";
 
-Drupal.edit.util.calcPropertyID = function(entity, predicate) {
-  return entity.getSubjectUri() + '/' + predicate;
-};
-
 /**
  * Retrieves a setting of the editor-specific Edit UI integration.
  *
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index b3ef03f..ec47986 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -15,6 +15,10 @@ $.extend(Drupal.edit, {
     activeEditorStates: [],
     singleEditorStates: [],
 
+    // Ephemeral storage for changed fields that persists through field
+    // rerendering.
+    changedFieldsInTempstore: [],
+
     /**
      * Implements Backbone Views' initialize() function.
      */
@@ -52,6 +56,14 @@ $.extend(Drupal.edit, {
     appStateChange: function (entityModel, isActive) {
       var app = this;
       if (isActive) {
+        // Create an entity toolbar.
+        var entityToolbar = new Drupal.edit.EntityToolbarView({
+          el: entityModel.get('el'),
+          model: entityModel,
+          appModel: this.model
+        });
+        entityModel.set('entityToolbar', entityToolbar);
+
         // Move all fields of this entity from the 'inactive' state to the
         // 'candidate' state.
         entityModel.get('fields').each(function (fieldModel) {
@@ -62,6 +74,11 @@ $.extend(Drupal.edit, {
         });
       }
       else {
+        // Remove the entity toolbar.
+        var entityToolbar = entityModel.get('entityToolbar');
+        entityToolbar.remove();
+        entityModel.unset('entityToolbar');
+
         // Move all fields of this entity from whatever state they are in to
         // the 'inactive' state.
         entityModel.get('fields').each(function (fieldModel) {
@@ -136,15 +153,9 @@ $.extend(Drupal.edit, {
         // If it's not against the general principle, then here are more
         // disallowed cases to check.
         if (accept) {
-          // Ensure only one editor (field) at a time may be higlighted or active.
-          if (from === 'candidate' && _.indexOf(this.singleEditorStates, to) !== -1) {
-            if (this.model.get('highlightedEditor') || this.model.get('activeEditor')) {
-              accept = false;
-            }
-          }
           // Reject going from activating/active to candidate because of a
           // mouseleave.
-          else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') {
+          if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') {
             if (context && context.reason === 'mouseleave') {
               accept = false;
             }
@@ -165,8 +176,9 @@ $.extend(Drupal.edit, {
                 // Do not accept this change right now, instead open a modal
                 // that will ask the user to confirm his choice.
                 accept = false;
-                // The callback will be called from the helper function.
-                this._confirmStopEditing(callback);
+                this._confirmStopEditing({
+                  callback: callback
+                });
               }
             }
           }
@@ -178,6 +190,11 @@ $.extend(Drupal.edit, {
 
     // @todo rename to decorateField
     decorate: function (fieldModel) {
+      var editID = fieldModel.get('editID');
+      var entityModel = fieldModel.get('entity');
+      var entityToolbar = entityModel.get('entityToolbar');
+      // Get the field toolbar DOM root from the entity toolbar.
+      var fieldToolbarRoot = entityToolbar.getToolbarRoot();
       // Create a new Editor.
       var editorName = fieldModel.get('editor');
       fieldModel.set('editorName', editorName);
@@ -189,9 +206,14 @@ $.extend(Drupal.edit, {
       // Toolbars are rendered "on-demand" (highlighting or activating).
       // They are a sibling element before the editor's DOM element.
       var toolbarView = new Drupal.edit.FieldToolbarView({
+        el: fieldToolbarRoot,
         model: fieldModel,
         $field: $(editorView.getEditedElement()),
-        editorView: editorView
+        // @todo editorView is needed for the getEditUISetting method. Maybe we
+        // can factor out this dependency and put it in the metadata of the
+        // Drupal.edit.Metadata object for this field.
+        editorView: editorView,
+        entityModel: entityModel
       });
 
       // Decorate the editor's DOM element depending on its state.
@@ -199,7 +221,6 @@ $.extend(Drupal.edit, {
         el: $(editorView.getEditedElement()),
         model: fieldModel,
         editorView: editorView,
-        toolbarId: toolbarView.getId()
       });
 
       // Create references in the field model; necessary for undecorate() and
@@ -233,27 +254,134 @@ $.extend(Drupal.edit, {
     },
 
     /**
+     *
+     */
+    save: function (event, entityModel, options) {
+      var that = this;
+      // check if there's an active editor.
+      var activeEditor = this.model.get('activeEditor');
+
+      /**
+       * Fires an AJAX request to the REST save URL for an entity.
+       */
+      var saveEntity = function () {
+        var id = 'edit-save-entity';
+        // Create a temporary element to be able to use Drupal.ajax.
+        var $el = $(event.target); // This is the span element inside the button.
+        // Create a Drupal.ajax instance to load the form.
+        Drupal.ajax[id] = new Drupal.ajax(id, $el, {
+          url: '/edit/entity/' + entityModel.id,
+          event: 'edit-save.edit',
+          progress: {
+            type: 'none'
+          },
+          error: function (data) {
+            // Clean up.
+            $el.unbind('edit-save.edit');
+            throw new Error();
+          }
+        });
+        // Entity saved successfully.
+        Drupal.ajax[id].commands.editEntitySaved = function(ajax, response, status) {
+          // Remove the changed marker from all of the fields.
+          that.fieldsCollection.each(function (fieldModel) {
+            $(fieldModel.get('el')).find('.edit-editable').addBack().removeClass('edit-changed');
+          });
+          // Reset the list tracking changed fields.
+          that.changedFieldsInTempstore = [];
+          // Clear the dirty flag on the entity.
+          var savedEntity = Drupal.edit.collections.entities.get(response.data.entity_type + '/' + response.data.entity_id);
+          if (savedEntity && 'set' in savedEntity) {
+            savedEntity.set('isDirty', false);
+          }
+          // Clean up.
+          $(ajax.element).unbind('edit-save.edit');
+          // Invoke the provided optional callback.
+          if ('callback' in (options || {})) {
+            options.callback.call();
+          }
+        };
+        $el.trigger('edit-save.edit');
+      };
+
+      // If an field is currently in a changed state, save it, then invoke the
+      // save entity function.
+      if (activeEditor && activeEditor.get('state') === 'changed') {
+        activeEditor.set({'state': 'saving'}, {
+          callback: saveEntity
+        });
+      }
+      // Otherwise, just save the save entity.
+      else if (this.changedFieldsInTempstore.length) {
+        saveEntity();
+      }
+    },
+
+    /**
+     *
+     */
+    close: function (entityModel) {
+      var that = this;
+      // check if there's an active editor.
+      var activeEditor = this.model.get('activeEditor');
+      // Sets all fields to inactive.
+      function cleanup () {
+        entityModel.set('isActive', false);
+      }
+
+      if (activeEditor) {
+        var state = activeEditor.get('state');
+        if (state === 'changed') {
+          this._confirmStopEditing({
+            callback: cleanup
+          });
+        }
+        else {
+          cleanup();
+        }
+      }
+      else {
+        cleanup();
+      }
+    },
+
+    /**
      * Asks the user to confirm whether he wants to stop editing via a modal.
      *
      * @see acceptEditorStateChange()
      */
-    _confirmStopEditing: function () {
+    _confirmStopEditing: function (options) {
       // Only instantiate if there isn't a modal instance visible yet.
       if (!this.model.get('activeModal')) {
         var that = this;
+        var activeEntity = Drupal.edit.collections.entities.where({ isActive: true })[0];
         var modal = new Drupal.edit.ModalView({
           model: this.model,
           message: Drupal.t('You have unsaved changes'),
           buttons: [
-            { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') },
-            { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') }
+            { action: 'discard', classes: 'action-cancel edit-button', label: Drupal.t('Discard changes') },
+            { action: 'save', type: 'submit', classes: 'action-save edit-button', label: Drupal.t('Save') }
           ],
-          callback: function(action) {
+          callback: function(event, action) {
+            var args = arguments;
             // The active modal has been removed.
             that.model.set('activeModal', null);
-            // Set the state that matches the user's action.
-            var targetState = (action === 'discard') ? 'candidate' : 'saving';
-            that.model.get('activeEditor').set('state', 'candidate', { confirmed: true });
+            // If the targetState is saving, the field must be saved, then the
+            // entity must be saved.
+            if (action === 'save') {
+              that.save(event, activeEntity, {
+                confirmed: true,
+                callback: (options || {}).callback || function () {}
+              });
+            }
+            else {
+              that.model.get('activeEditor').set('state', 'candidate', {
+                confirmed: true
+              });
+              if ('callback' in options) {
+                options.callback.call();
+              }
+            }
           }
         });
         this.model.set('activeModal', modal);
@@ -264,6 +392,46 @@ $.extend(Drupal.edit, {
     },
 
     /**
+     *
+     */
+    enableEditor: function (fieldModel) {
+      // check if there's an active editor.
+      var activeEditor = this.model.get('activeEditor');
+
+      // Do nothing if the fieldModel is already the active editor.
+      if (fieldModel === activeEditor) {
+        return;
+      }
+      if (activeEditor) {
+        // If there is, check if the model is changed.
+        if (activeEditor.get('state') === 'changed') {
+          // Save a reference to the changed field so it can be marked as
+          // as changed until the tempStore is pushed to permanent storage.
+          this.changedFieldsInTempstore.push(activeEditor.get('editID'));
+          // Attempt to save the field.
+          activeEditor.set({'state': 'saving'}, {
+            // This callback will be invoked if the activeEditor field is
+            // successfully saved.
+            callback: function () {
+              // Set the new fieldModel to activating.
+              fieldModel.set('state', 'activating');
+            }
+          });
+        }
+        // else, set it to a candidate.
+        else {
+          activeEditor.set('state', 'candidate');
+          // Set the new fieldModel to activating.
+          fieldModel.set('state', 'activating');
+        }
+      }
+      else {
+        // Set the new fieldModel to activating.
+        fieldModel.set('state', 'activating');
+      }
+    },
+
+    /**
      * Reacts to field state changes; tracks global state.
      *
      * @param Drupal.edit.FieldModel fieldModel
@@ -288,7 +456,7 @@ $.extend(Drupal.edit, {
       if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== fieldModel) {
         this.model.set('activeEditor', fieldModel);
       }
-      else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') {
+      else if (this.model.get('activeEditor') === fieldModel && to === 'candidate' || to === 'inactive') {
         // Discarded if it transitions from a changed state to 'candidate'.
         if (from === 'changed' || from === 'invalid') {
           fieldModel.get('editorView').revert();
@@ -359,6 +527,22 @@ $.extend(Drupal.edit, {
         this.decorate(fieldModel);
         fieldModel.set('state', 'candidate');
       }
+
+      // If the field change was only saved to tempstore, mark the field as
+      // changed. The changed marker will be cleared when the
+      // Drupal.edit.app.AppView.prototype.save() method is called.
+      for (var i = 0, fields = this.changedFieldsInTempstore; i < fields.length; i++) {
+        var changedFieldModel = fields[i];
+        if (changedFieldModel === fieldModel.get('editID')) {
+          var $field = $(fieldModel.get('el'));
+          if ($field.is('.edit-editable')) {
+            $field.addClass('edit-changed');
+          }
+          else {
+            $field.find('.edit-editable').addClass('edit-changed');
+          }
+        }
+      }
     },
 
     /**
diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js
index 5854a50..895c076 100644
--- a/core/modules/edit/js/views/EditorDecorationView.js
+++ b/core/modules/edit/js/views/EditorDecorationView.js
@@ -9,7 +9,6 @@
 "use strict";
 
 Drupal.edit.EditorDecorationView = Backbone.View.extend({
-  toolbarId: null,
 
   _widthAttributeIsEmpty: null,
 
@@ -29,13 +28,10 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    *   - editorView: the editor object with an 'options' object that has these keys:
    *      * property: the predicate of the property.
    *      * editorName: the name of the Editor.
-   *   - toolbarId: the ID attribute of the toolbar as rendered in the DOM.
    */
   initialize: function (options) {
     this.editorView = options.editorView;
 
-    this.toolbarId = options.toolbarId;
-
     this.model.on('change:state', this.stateChange, this);
   },
 
@@ -87,6 +83,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
         this.startEdit();
         break;
       case 'changed':
+        this.markChanged(true);
         break;
       case 'saving':
         break;
@@ -104,10 +101,8 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    */
   onMouseEnter: function (event) {
     var that = this;
-    this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
-      that.model.set('state', 'highlighted');
-      event.stopPropagation();
-    });
+    that.model.set('state', 'highlighted');
+    event.stopPropagation();
   },
 
   /**
@@ -117,10 +112,8 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    */
   onMouseLeave: function (event) {
     var that = this;
-    this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
-      that.model.set('state', 'candidate', { reason: 'mouseleave' });
-      event.stopPropagation();
-    });
+    that.model.set('state', 'candidate', { reason: 'mouseleave' });
+    event.stopPropagation();
   },
 
   /**
@@ -129,7 +122,8 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    * @param jQuery event
    */
   onClick: function (event) {
-    this.model.set('state', 'activating');
+    var that = this;
+    Drupal.edit.app.enableEditor(this.model);
     event.preventDefault();
     event.stopPropagation();
   },
@@ -172,9 +166,6 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    */
   prepareEdit: function () {
     this.$el.addClass('edit-editing');
-
-    // While editing, do not show any other editors.
-    $('.edit-candidate').not('.edit-editing').removeClass('edit-editable');
   },
 
   /**
@@ -187,6 +178,13 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
   },
 
   /**
+   *
+   */
+  markChanged: function (toggle) {
+    this.$el.toggleClass('edit-changed', toggle);
+  },
+
+  /**
    * Removes the class that indicates that an element is being edited.
    *
    * Reapplies the class that indicates that a candidate editable element is
@@ -228,7 +226,6 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
       this._widthAttributeIsEmpty = true;
       this.$el
         .addClass('edit-animate-disable-width')
-        .css('width', this.$el.width())
         .css('background-color', this._getBgColor(this.$el));
     }
 
@@ -261,30 +258,24 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
 
     // 1) Set the empty width again.
     if (this._widthAttributeIsEmpty) {
-      this.$el
-        .addClass('edit-animate-disable-width')
-        .css('width', '')
-        .css('background-color', '');
+      this.$el.addClass('edit-animate-disable-width');
+      this.el.style.width = null;
+      this.el.style['background-color'] = null;
     }
 
-    // 2) Remove padding; use animations (these will run simultaneously with)
-    // the fading out of the toolbar as its gets removed).
-    var posProp = this._getPositionProperties(this.$el);
     setTimeout(function() {
+      // If the EditorDecorationView has been removed, the el will be undefined.
+      // Exit without taking action.
+      if (!self.el) {
+        return;
+      }
       // Re-enable width animations (padding changes affect width too!).
       self.$el.removeClass('edit-animate-disable-width');
-
-      // Unpad the editable.
-      self.$el
-      .css({
-        'position': 'relative',
-        'top':  posProp.top  + 5 + 'px',
-        'left': posProp.left + 5 + 'px',
-        'padding-top'   : posProp['padding-top']    - 5 + 'px',
-        'padding-left'  : posProp['padding-left']   - 5 + 'px',
-        'padding-right' : posProp['padding-right']  - 5 + 'px',
-        'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
-        'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
+      // Remove padding; use animations (these will run simultaneously with)
+      // the fading out of the toolbar as its gets removed).
+      var posProp = self._getPositionProperties(self.$el);
+      _.each(_.keys(posProp), function (property) {
+        self.el.style[property] = null;
       });
     }, 0);
   },
@@ -346,26 +337,6 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
       pos = '0px';
     }
     return pos;
-  },
-
-  /**
-   * Ignores hovering to/from the given closest element.
-   *
-   * When a hover occurs to/from another element, invoke the callback.
-   *
-   * @param jQuery event
-   * @param jQuery closest
-   *   A jQuery-wrapped DOM element or compatibale jQuery input. The element
-   *   whose mouseenter and mouseleave events should be ignored.
-   * @param Function callback
-   */
-  _ignoreHoveringVia: function (event, closest, callback) {
-    if ($(event.relatedTarget).closest(closest).length > 0) {
-      event.stopPropagation();
-    }
-    else {
-      callback();
-    }
   }
 });
 
diff --git a/core/modules/edit/js/views/EditorView.js b/core/modules/edit/js/views/EditorView.js
index 791549e..7b04beb 100644
--- a/core/modules/edit/js/views/EditorView.js
+++ b/core/modules/edit/js/views/EditorView.js
@@ -76,7 +76,7 @@ $.extend(Drupal.edit, {
      * @param String state
      *   The state of the associated field. One of Drupal.edit.FieldModel.states.
      */
-    stateChange: function (model, state) {
+    stateChange: function (model, state, options) {
       var from = model.previous('state');
       var to = state;
       switch (to) {
@@ -92,6 +92,11 @@ $.extend(Drupal.edit, {
           if (from === 'invalid') {
             this.removeValidationErrors();
           }
+
+          // Attempt to save if the field was previously in the changed state.
+          if (from === 'changed') {
+            this.model.set('state', 'saving');
+          }
           break;
         case 'highlighted':
           // Nothing to do for the typical in-place editor: it should not be
@@ -129,7 +134,7 @@ $.extend(Drupal.edit, {
           if (from === 'invalid') {
             this.removeValidationErrors();
           }
-          this.save();
+          this.save(options);
           break;
         case 'saved':
           // Nothing to do for the typical in-place editor. Immediately after
@@ -159,8 +164,9 @@ $.extend(Drupal.edit, {
     /**
      * Saves the modified value in the in-place editor for this field.
      */
-    save: function () {
+    save: function (options) {
       var model = this.model;
+      var callback = (options || {}).callback || function () {};
 
       function fillAndSubmitForm (value) {
         var $form = jQuery('#edit_backstage form');
@@ -208,6 +214,9 @@ $.extend(Drupal.edit, {
           // Then, set the 'html' attribute on the field model. This will cause the
           // field to be rerendered.
           model.set('html', response.data);
+
+          // Invoke the optional callback.
+          callback.call();
         };
 
         // Unsuccessfully saved; validation errors.
diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
new file mode 100644
index 0000000..bb9f00c
--- /dev/null
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -0,0 +1,358 @@
+/**
+ * @file
+ * A Backbone View that provides an entity level toolbar.
+ */
+(function ($, Backbone, Drupal, debounce) {
+
+"use strict";
+
+Drupal.edit.EntityToolbarView = Backbone.View.extend({
+
+  _loader: null,
+  _loaderVisibleStart: 0,
+  _fieldToolbarRoot: null,
+  _fieldLabelRoot: null,
+
+  events: function () {
+    var map = {
+      'click.edit button.action-save': 'onClickSave',
+      'click.edit button.action-cancel': 'onClickClose',
+      'mouseenter.edit': 'onMouseenter'
+    }
+    return map;
+  },
+
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function (options) {
+    var that = this;
+
+    this.appModel = options.appModel;
+
+    this.model.on('change:isActive change:isDirty', this.render, this);
+    this.model.on('change:state', this.stateChange, this);
+    this.model.on('fieldViewChange', this.fieldViewChangeHandler, this);
+
+    this.appModel.on('change:highlightedEditor', this.render, this);
+    this.appModel.on('change:activeEditor', this.render, this);
+
+    $(window).on('resize.edit scroll.edit', debounce($.proxy(this.windowChangeHandler, this), 150));
+
+    // Set the el into its own property. Eventually the el property will be
+    // replaced with the rendered toolbar.
+    this.$entity = this.$el;
+
+    // Set the toolbar container to this view's el property.
+    this.buildToolbarEl();
+    this._fieldToolbarRoot = this.$el.find('.edit-toolbar-field').get(0);
+
+    this._loader = null;
+    this._loaderVisibleStart = 0;
+
+    this.render();
+  },
+
+  /**
+   * Implements Backbone.View.prototype.render().
+   */
+  render: function (model, changeValue) {
+
+    if (this.model.get('isActive')) {
+      // If the toolbar container doesn't exist, create it.
+      if ($('body').children('#edit-entity-toolbar').length === 0) {
+        $('body').append(this.$el);
+      }
+
+      this.label();
+
+      this.show('ops');
+      // If render is being called and the toolbar is already visible, just
+      // reposition it.
+      this.position();
+    }
+    else {
+      this.remove();
+    }
+
+    var $save = this.$el.find('.edit-button.action-save')
+    $save.attr('aria-hidden', !this.model.get('isDirty'));
+    // The progress spinner will only be set when the save button is clicked.
+    // Remove it on any call to render.
+    $save.find('.ajax-progress').remove();
+    $save.find('span').text(Drupal.t('Save'));
+    $save.removeClass('action-saving');
+
+    return this;
+  },
+
+  /**
+   *
+   */
+  windowChangeHandler: function (event) {
+    this.position();
+  },
+
+  /**
+   *
+   */
+  fieldViewChangeHandler: function (view) {
+    this.render(this, view);
+  },
+
+  /**
+   * Uses the jQuery.ui.position() method to position the entity toolbar.
+   */
+  position: function (element) {
+    clearTimeout(this.timer);
+    var that = this;
+    // Vary the edge of the positioning according to the direction of language
+    // in the document.
+    var edge = (document.documentElement.dir === 'rtl') ? 'right' : 'left';
+    // If a field in this entity is active, position against it.
+    var activeEditor = Drupal.edit.app.model.get('activeEditor');
+    var activeEkditorView = activeEditor && activeEditor.get('editorView');
+    var activeEditedElement = activeEkditorView && activeEkditorView.getEditedElement();
+
+    // Label of a highlighted field, if it exists.
+    var highlightedEditor = Drupal.edit.app.model.get('highlightedEditor');
+    var highlightedEditorView = highlightedEditor && highlightedEditor.get('editorView');
+    var highlightedEditedElement = highlightedEditorView && highlightedEditorView.getEditedElement();
+    // Prefer the specified element from the parameters, then the acive field
+    // and finally the entity itself to determine the position of the toolbar.
+    var of = element || activeEditedElement || highlightedEditedElement || this.$entity;
+    // Uses the jQuery.ui.position() method. Use a timeout to move the toolbar
+    // only after the user has focused on an editable for 250ms. This prevents
+    // the toolbar from jumping around the screen.
+    this.timer = setTimeout(function () {
+      that.$el
+        .position({
+          my: edge + ' bottom',
+          at: edge + ' top',
+          of: of,
+          // Eliminate some of the placement jitteriness by flooring the suggested
+          // values.
+          using: function (suggested, info) {
+            info.element.element.css({
+              left: Math.floor(suggested.left),
+              top: Math.floor(suggested.top)
+            });
+          }
+        })
+        .css({
+          'max-width': $(of).outerWidth(),
+          'width': '100%'
+        });
+      }, 250);
+  },
+
+  /**
+   * Determines the actions to take given a change of state.
+   *
+   * @param Drupal.edit.EntityModel model
+   * @param String state
+   *   The state of the associated field. One of Drupal.edit.EntityModel.states.
+   */
+  stateChange: function (model, state, options) {
+      var from = model.previous('state');
+      var to = state;
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          break;
+        case 'highlighted':
+          break;
+        case 'activating':
+          this.setLoadingIndicator(true);
+          break;
+        case 'active':
+          this.setLoadingIndicator(false);
+          break;
+        case 'changed':
+          this.$el
+            .find('button.save')
+            .addClass('blue-button')
+            .removeClass('gray-button');
+          break;
+        case 'saving':
+          this.setLoadingIndicator(true);
+          break;
+        case 'saved':
+          this.setLoadingIndicator(false);
+          break;
+        case 'invalid':
+          this.setLoadingIndicator(false);
+          break;
+        default:
+          break;
+      }
+  },
+
+  /**
+   * Set the model state to 'saving' when the save button is clicked.
+   *
+   * @param jQuery event
+   */
+  onClickSave: function (event) {
+    event.stopPropagation();
+    event.preventDefault();
+    var $target = $(event.target);
+    $target = ($target.is('.action-save')) ? $target : $target.closest('.action-save');
+    $target.addClass('action-saving');
+    $target.find('span')
+      .text(Drupal.t('Saving@ellipsis', {'@ellipsis': '...'}))
+      .after(Drupal.theme.editThrobber());
+    Drupal.edit.app.save(event, this.model);
+  },
+
+  /**
+   * Sets the model state to candidate when the cancel button is clicked.
+   *
+   * @param jQuery event
+   */
+  onClickClose: function (event) {
+    event.stopPropagation();
+    event.preventDefault();
+    Drupal.edit.app.close(this.model);
+  },
+
+  /**
+   *
+   */
+  onMouseenter: function (event) {
+    clearTimeout(this.timer);
+  },
+
+  /**
+   *
+   */
+  buildToolbarEl: function () {
+    var $toolbar;
+    $toolbar = $(Drupal.theme('editEntityToolbar', {
+      id: 'edit-entity-toolbar'
+    }));
+
+    $toolbar
+      .find('.edit-toolbar-entity')
+      // Append the "ops" toolgroup into the toolbar.
+      .prepend(Drupal.theme('editToolgroup', {
+        classes: ['ops'],
+        buttons: [
+          { label: Drupal.t('Save'), type: 'submit', classes: 'action-save edit-button', attributes: {'aria-hidden': true}},
+          { label: '<span class="close">' + Drupal.t('Close') + '</span>', classes: 'action-cancel edit-button' }
+        ]
+      }));
+
+    // Give the toolbar a sensible starting position so that it doesn't
+    // animiate on to the screen from a far off corner.
+    $toolbar
+      .css({
+        left: this.$entity.offset().left,
+        top: this.$entity.offset().top
+      });
+
+    this.setElement($toolbar);
+  },
+
+  /**
+   *
+   */
+  getToolbarRoot: function () {
+    return this._fieldToolbarRoot;
+  },
+
+  /**
+   * Indicates in the 'info' toolgroup that we're waiting for a server reponse.
+   *
+   * Prevents flickering loading indicator by only showing it after 0.6 seconds
+   * and if it is shown, only hiding it after another 0.6 seconds.
+   *
+   * @param Boolean enabled
+   *   Whether the loading indicator should be displayed or not.
+   */
+  setLoadingIndicator: function (enabled) {
+    var that = this;
+    if (enabled) {
+      this._loader = setTimeout(function() {
+        that.addClass('info', 'loading');
+        that._loaderVisibleStart = new Date().getTime();
+      }, 600);
+    }
+    else {
+      var currentTime = new Date().getTime();
+      clearTimeout(this._loader);
+      if (this._loaderVisibleStart) {
+        setTimeout(function() {
+          that.removeClass('info', 'loading');
+        }, this._loaderVisibleStart + 600 - currentTime);
+      }
+      this._loader = null;
+      this._loaderVisibleStart = 0;
+    }
+  },
+
+  /**
+   *
+   */
+  label: function () {
+    // The entity title.
+    var title = this.model.get('title');
+
+    // Label of an active field, if it exists.
+    var activeEditor = Drupal.edit.app.model.get('activeEditor');
+    var activeFieldLabel = activeEditor && activeEditor.get('label');
+    activeFieldLabel = activeFieldLabel && activeFieldLabel + '::' + title;
+
+    // Label of a highlighted field, if it exists.
+    var highlightedEditor = Drupal.edit.app.model.get('highlightedEditor');
+    var highlightedFieldLabel = highlightedEditor && highlightedEditor.get('label');
+    highlightedFieldLabel = highlightedFieldLabel && highlightedFieldLabel + '::' + title;
+
+    this.$el
+      .find('.edit-toolbar-label')
+      .text(activeFieldLabel || highlightedFieldLabel || title);
+  },
+
+  /**
+   * Adds classes to a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  addClass: function (toolgroup, classes) {
+    this._find(toolgroup).addClass(classes);
+  },
+
+  /**
+   * Removes classes from a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  removeClass: function (toolgroup, classes) {
+    this._find(toolgroup).removeClass(classes);
+  },
+
+  /**
+   * Finds a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  _find: function (toolgroup) {
+    return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup);
+  },
+
+  /**
+   * Shows a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  show: function (toolgroup) {
+    this.$el.removeClass('edit-animate-invisible');
+  }
+});
+
+})(jQuery, Backbone, Drupal, Drupal.debounce);
diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js
index 117debb..741b97f 100644
--- a/core/modules/edit/js/views/FieldToolbarView.js
+++ b/core/modules/edit/js/views/FieldToolbarView.js
@@ -12,32 +12,22 @@
 Drupal.edit.FieldToolbarView = Backbone.View.extend({
   $field: null,
 
-  _loader: null,
-  _loaderVisibleStart: 0,
-
   _id: null,
 
-  events: {
-    'click.edit button.label': 'onClickInfoLabel',
-    'mouseleave.edit': 'onMouseLeave',
-    'click.edit button.field-save': 'onClickSave',
-    'click.edit button.field-close': 'onClickClose'
-  },
-
   /**
    * Implements Backbone.View.prototype.initialize().
    */
   initialize: function (options) {
     this.$field = options.$field;
+    this.$root = this.$el;
+    this.setElement();
     this.editorView = options.editorView;
+    this.entityModel = options.entityModel;
 
-    this._loader = null;
-    this._loaderVisibleStart = 0;
+    this.model.on('change:state', this.stateChange, this);
 
     // Generate a DOM-compatible ID for the form container DOM element.
     this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_');
-
-    this.model.on('change:state', this.stateChange, this);
   },
 
   /**
@@ -47,19 +37,17 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    * toolbar is being inserted into the DOM, it will be inserted differently.
    */
   render: function () {
-    // Render toolbar.
-    this.setElement($(Drupal.theme('editToolbarContainer', {
+    // Render toolbar and set it as the view's element.
+    this.setElement($(Drupal.theme('editFieldToolbar', {
       id: this._id
     })));
 
     // Insert in DOM.
     if (this.$field.css('display') === 'inline') {
-      this.$el.prependTo(this.$field.offsetParent());
-      var pos = this.$field.position();
-      this.$el.css('left', pos.left).css('top', pos.top);
+      this.$el.prependTo(this.$field);
     }
     else {
-      this.$el.insertBefore(this.$field);
+      this.$el.prependTo(this.$root);
     }
 
     return this;
@@ -72,7 +60,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    * @param String state
    *   The state of the associated field. One of Drupal.edit.FieldModel.states.
    */
-  stateChange: function (model, state) {
+  stateChange: function (model, state, options) {
     var from = model.previous('state');
     var to = state;
     switch (to) {
@@ -82,181 +70,35 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
         }
         break;
       case 'candidate':
-        if (from === 'inactive') {
-          this.render();
-        }
-        else {
-          // Remove all toolgroups; they're no longer necessary.
-          this.$el
-            .removeClass('edit-highlighted edit-editing')
-            .find('.edit-toolbar .edit-toolgroup').remove();
-          if (from !== 'highlighted' && this.getEditUISetting('padding')) {
-            this._unpad();
-          }
-        }
+        // Remove the toolbar; it is no longer necessary.
+        this.$el.remove();
         break;
       case 'highlighted':
-        // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title).
-        this.startHighlight();
         break;
       case 'activating':
-        this.setLoadingIndicator(true);
-        break;
-      case 'active':
-        this.startEdit();
-        this.setLoadingIndicator(false);
+        this.render();
+
         if (this.getEditUISetting('fullWidthToolbar')) {
           this.$el.addClass('edit-toolbar-fullwidth');
         }
-
-        if (this.getEditUISetting('padding')) {
-          this._pad();
-        }
         if (this.getEditUISetting('unifiedToolbar')) {
           this.insertWYSIWYGToolGroups();
         }
         break;
+      case 'active':
+        break;
       case 'changed':
-        this.$el
-          .find('button.save')
-          .addClass('blue-button')
-          .removeClass('gray-button');
         break;
       case 'saving':
-        this.setLoadingIndicator(true);
         break;
       case 'saved':
-        this.setLoadingIndicator(false);
         break;
       case 'invalid':
-        this.setLoadingIndicator(false);
         break;
     }
   },
 
   /**
-   * Redirects the click.edit-event to the editor DOM element.
-   *
-   * @param jQuery event
-   */
-  onClickInfoLabel: function (event) {
-    event.stopPropagation();
-    event.preventDefault();
-    // Redirects the event to the editor DOM element.
-    this.$field.trigger('click.edit');
-  },
-
-  /**
-   * Controls mouseleave events.
-   *
-   * A mouseleave to the editor doesn't matter; a mouseleave to something else
-   * counts as a mouseleave on the editor itself.
-   *
-   * @param jQuery event
-   */
-  onMouseLeave: function (event) {
-    if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) {
-      this.$field.trigger('mouseleave.edit');
-    }
-    event.stopPropagation();
-  },
-
-  /**
-   * Set the model state to 'saving' when the save button is clicked.
-   *
-   * @param jQuery event
-   */
-  onClickSave: function (event) {
-    event.stopPropagation();
-    event.preventDefault();
-    this.model.set('state', 'saving');
-  },
-
-  /**
-   * Sets the model state to candidate when the cancel button is clicked.
-   *
-   * @param jQuery event
-   */
-  onClickClose: function (event) {
-    event.stopPropagation();
-    event.preventDefault();
-    this.model.set('state', 'candidate', { reason: 'cancel' });
-  },
-
-  /**
-   * Indicates in the 'info' toolgroup that we're waiting for a server reponse.
-   *
-   * Prevents flickering loading indicator by only showing it after 0.6 seconds
-   * and if it is shown, only hiding it after another 0.6 seconds.
-   *
-   * @param Boolean enabled
-   *   Whether the loading indicator should be displayed or not.
-   */
-  setLoadingIndicator: function (enabled) {
-    var that = this;
-    if (enabled) {
-      this._loader = setTimeout(function() {
-        that.addClass('info', 'loading');
-        that._loaderVisibleStart = new Date().getTime();
-      }, 600);
-    }
-    else {
-      var currentTime = new Date().getTime();
-      clearTimeout(this._loader);
-      if (this._loaderVisibleStart) {
-        setTimeout(function() {
-          that.removeClass('info', 'loading');
-        }, this._loaderVisibleStart + 600 - currentTime);
-      }
-      this._loader = null;
-      this._loaderVisibleStart = 0;
-    }
-  },
-
-  /**
-   *
-   */
-  startHighlight: function () {
-    // Retrieve the lavel to show for this field.
-    var label = this.model.get('label');
-
-    this.$el
-      .addClass('edit-highlighted')
-      .find('.edit-toolbar')
-      // Append the "info" toolgroup into the toolbar.
-      .append(Drupal.theme('editToolgroup', {
-        classes: 'info edit-animate-only-background-and-padding',
-        buttons: [
-          { label: label, classes: 'blank-button label' }
-        ]
-      }));
-
-    // Animations.
-    var that = this;
-    setTimeout(function () {
-      that.show('info');
-    }, 0);
-  },
-
-  /**
-   *
-   */
-  startEdit: function () {
-    this.$el
-      .addClass('edit-editing')
-      .find('.edit-toolbar')
-      // Append the "ops" toolgroup into the toolbar.
-      .append(Drupal.theme('editToolgroup', {
-        classes: 'ops',
-        buttons: [
-          { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' },
-          { label: '<span class="close">' + Drupal.t('Close') + '</span>', classes: 'field-close close gray-button' }
-        ]
-      }));
-    this.show('ops');
-  },
-
-  /**
    * Retrieves a setting of the editor-specific Edit UI integration.
    *
    * @param String setting
@@ -268,64 +110,24 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
   },
 
   /**
-   * Adjusts the toolbar to accomodate padding on the editor.
-   *
-   * @see EditorDecorationView._pad().
-   */
-  _pad: function () {
-    // The whole toolbar must move to the top when the property's DOM element
-    // is displayed inline.
-    if (this.$field.css('display') === 'inline') {
-      this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px');
-    }
-
-    // The toolbar must move to the top and the left.
-    var $hf = this.$el.find('.edit-toolbar-heightfaker');
-    $hf.css({ bottom: '6px', left: '-5px' });
-
-    if (this.getEditUISetting('fullWidthToolbar')) {
-      $hf.css({ width: this.$field.width() + 10 });
-    }
-  },
-
-  /**
-   * Undoes the changes made by _pad().
-   *
-   * @see EditorDecorationView._unpad().
-   */
-  _unpad: function () {
-    // Move the toolbar back to its original position.
-    var $hf = this.$el.find('.edit-toolbar-heightfaker');
-    $hf.css({ bottom: '1px', left: '' });
-
-    if (this.getEditUISetting('fullWidthToolbar')) {
-      $hf.css({ width: '' });
-    }
-  },
-
-  /**
    *
    */
   insertWYSIWYGToolGroups: function () {
     this.$el
-      .find('.edit-toolbar')
       .append(Drupal.theme('editToolgroup', {
         id: this.getFloatedWysiwygToolgroupId(),
-        classes: 'wysiwyg-floated',
+        classes: ['wysiwyg-floated', 'edit-animate-slow', 'edit-animate-invisible', 'edit-animate-delay-veryfast'],
         buttons: []
       }))
       .append(Drupal.theme('editToolgroup', {
         id: this.getMainWysiwygToolgroupId(),
-        classes: 'wysiwyg-main',
+        classes: ['wysiwyg-main', 'edit-animate-slow', 'edit-animate-invisible', 'edit-animate-delay-veryfast'],
         buttons: []
       }));
 
     // Animate the toolgroups into visibility.
-    var that = this;
-    setTimeout(function () {
-      that.show('wysiwyg-floated');
-      that.show('wysiwyg-main');
-    }, 0);
+    this.show('wysiwyg-floated');
+    this.show('wysiwyg-main');
   },
 
   /**
@@ -365,43 +167,37 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
   },
 
   /**
-   * Shows a toolgroup.
-   *
-   * @param String toolgroup
-   *   A toolgroup name.
-   */
-  show: function (toolgroup) {
-    this._find(toolgroup).removeClass('edit-animate-invisible');
-  },
-
-  /**
-   * Adds classes to a toolgroup.
-   *
-   * @param String toolgroup
-   *   A toolgroup name.
-   */
-  addClass: function (toolgroup, classes) {
-    this._find(toolgroup).addClass(classes);
-  },
-
-  /**
-   * Removes classes from a toolgroup.
+   * Finds a toolgroup.
    *
    * @param String toolgroup
    *   A toolgroup name.
    */
-  removeClass: function (toolgroup, classes) {
-    this._find(toolgroup).removeClass(classes);
+  _find: function (toolgroup) {
+    return this.$el.find('.edit-toolgroup.' + toolgroup);
   },
 
   /**
-   * Finds a toolgroup.
+   * Shows a toolgroup.
    *
    * @param String toolgroup
    *   A toolgroup name.
    */
-  _find: function (toolgroup) {
-    return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup);
+  show: function (toolgroup) {
+    var that = this;
+    var $group = this._find(toolgroup);
+    // Attach a transitionEnd event handler to the toolbar group so that update
+    // events can be triggered after the animations have ended.
+    $group
+      .on(Drupal.edit.util.constants.transitionEnd, function (event) {
+          that.entityModel.trigger('viewChanged', that);
+          $group.off(Drupal.edit.util.constants.transitionEnd);
+      });
+    // The call to remove the class and start the animation must be started in
+    // the next animation frame or the event handler attached above won't be
+    // triggered.
+    window.setTimeout(function () {
+      $group.removeClass('edit-animate-invisible');
+    }, 0);
   }
 });
 
diff --git a/core/modules/edit/js/views/ModalView.js b/core/modules/edit/js/views/ModalView.js
index 15497cc..6d58934 100644
--- a/core/modules/edit/js/views/ModalView.js
+++ b/core/modules/edit/js/views/ModalView.js
@@ -73,7 +73,7 @@ Drupal.edit.ModalView = Backbone.View.extend({
       });
 
     var action = $(event.target).attr('data-edit-modal-action');
-    return this.callback(action);
+    return this.callback(event, action);
   }
 });
 
diff --git a/core/modules/edit/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php
index 44e8456..85ba88f 100644
--- a/core/modules/edit/lib/Drupal/edit/EditController.php
+++ b/core/modules/edit/lib/Drupal/edit/EditController.php
@@ -64,8 +64,11 @@ public function metadata(Request $request) {
       if (!$langcode || (field_valid_language($langcode) !== $langcode)) {
         throw new NotFoundHttpException();
       }
-
-      $metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode);
+      $entity_id = $entity->entityType() . '/' . $entity_id;
+      if (!isset($metadata[$entity_id])) {
+        $metadata[$entity_id] = $metadataGenerator->generateEntity($entity, $langcode);
+      }
+      $metadata[$field] = $metadataGenerator->generateField($entity, $instance, $langcode, $view_mode);
     }
 
     $response->addCommand(new MetaDataCommand($metadata));
@@ -224,8 +227,12 @@ public function entitySave(EntityInterface $entity) {
     $tempstore->get($entity->uuid)->save();
     $tempstore->delete($entity->uuid);
 
-    // @todo add response that makes sense.
-    $output = array();
+    // Return information about the entity that allows a front end application
+    // to identify it.
+    $output = array(
+      'entity_type' => $entity->entityType(),
+      'entity_id' => $entity->id()
+    );
 
     // Respond to client that the entity was saved properly.
     $response = new AjaxResponse();
diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
index cecc676..5f7e41b 100644
--- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
+++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
@@ -55,10 +55,19 @@ public function __construct(EditEntityFieldAccessCheckInterface $access_checker,
     $this->editorManager = $editor_manager;
   }
 
+  public function generateEntity(EntityInterface $entity, $langcode) {
+    $title = $entity->getTranslation($langcode, FALSE)->get('title')->getValue();
+    $title = $title[0]['value'];
+    return array(
+      'type' => 'entity',
+      'title' => $title
+    );
+  }
+
   /**
    * Implements \Drupal\edit\MetadataGeneratorInterface::generate().
    */
-  public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) {
+  public function generateField(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) {
     $field_name = $instance['field_name'];
 
     // Early-return if user does not have access.
@@ -79,6 +88,7 @@ public function generate(EntityInterface $entity, FieldInstance $instance, $lang
     $label = $instance['label'];
     $editor = $this->editorManager->createInstance($editor_id);
     $metadata = array(
+      'type' => 'field',
       'label' => check_plain($label),
       'access' => TRUE,
       'editor' => $editor_id,
diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
index 16db770..d785171 100644
--- a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
+++ b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
@@ -15,6 +15,8 @@
  */
 interface MetadataGeneratorInterface {
 
+  public function generateEntity(EntityInterface $entity, $langcode);
+
   /**
    * Generates in-place editing metadata for an entity field.
    *
@@ -34,6 +36,6 @@
    *   - aria: the ARIA label.
    *   - custom: (optional) any additional metadata that the editor provides.
    */
-  public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode);
+  public function generateField(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode);
 
 }
diff --git a/core/modules/editor/js/editor.formattedTextEditor.js b/core/modules/editor/js/editor.formattedTextEditor.js
index 5d3bd01..8534257 100644
--- a/core/modules/editor/js/editor.formattedTextEditor.js
+++ b/core/modules/editor/js/editor.formattedTextEditor.js
@@ -51,7 +51,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  stateChange: function (model, state) {
+  stateChange: function (model, state, options, callback) {
     var that = this;
     var from = model.previous('state');
     var to = state;
@@ -116,7 +116,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
         if (from === 'invalid') {
           this.removeValidationErrors();
         }
-        this.save();
+        this.save(options);
         break;
 
       case 'saved':
-- 
1.7.10.4

