 core/modules/edit/css/edit.css                     |  240 +++++--------
 core/modules/edit/edit.module                      |    5 +-
 core/modules/edit/edit.routing.yml                 |    2 +-
 core/modules/edit/js/edit.js                       |    3 +-
 core/modules/edit/js/editors/directEditor.js       |    4 +-
 core/modules/edit/js/editors/formEditor.js         |   16 +-
 core/modules/edit/js/models/EntityModel.js         |   80 ++++-
 core/modules/edit/js/theme.js                      |   53 ++-
 core/modules/edit/js/util.js                       |    4 +-
 core/modules/edit/js/views/AppView.js              |  227 +++++++++++--
 core/modules/edit/js/views/EditorDecorationView.js |   57 ++--
 core/modules/edit/js/views/EditorView.js           |   18 +-
 core/modules/edit/js/views/EntityToolbarView.js    |  358 ++++++++++++++++++++
 core/modules/edit/js/views/FieldToolbarView.js     |  284 ++--------------
 core/modules/edit/js/views/ModalView.js            |    2 +-
 .../edit/lib/Drupal/edit/EditController.php        |   29 +-
 .../edit/lib/Drupal/edit/MetadataGenerator.php     |   13 +-
 .../lib/Drupal/edit/MetadataGeneratorInterface.php |   17 +-
 .../editor/js/editor.formattedTextEditor.js        |    4 +-
 19 files changed, 911 insertions(+), 505 deletions(-)

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 17d9906..993a3ed 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -53,7 +53,7 @@ function edit_page_build(&$page) {
     'type' => 'setting',
     'data' => array('edit' => array(
       'metadataURL' => url('edit/metadata'),
-      'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode/!reset_tempstore'),
+      'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode'),
       'entitySaveURL' => url('edit/entity/!entity_type/!id/!langcode'),
       'context' => 'body',
     )),
@@ -83,6 +83,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,
@@ -99,8 +100,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/edit.routing.yml b/core/modules/edit/edit.routing.yml
index f962cf7..ff2d1a5 100644
--- a/core/modules/edit/edit.routing.yml
+++ b/core/modules/edit/edit.routing.yml
@@ -6,7 +6,7 @@ edit_metadata:
     _permission: 'access in-place editing'
 
 edit_field_form:
-  pattern: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}/{reset_tempstore}'
+  pattern: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}'
   defaults:
     _controller: '\Drupal\edit\EditController::fieldForm'
   requirements:
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index 6899125..1d618a6 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -313,7 +313,8 @@ function initializeEntityContextualLink (contextualLink) {
   else if (hasFieldWithPermission(fieldIDs)) {
     var entityModel = new Drupal.edit.EntityModel({
       el: contextualLink.region,
-      id: contextualLink.entityID
+      id: contextualLink.entityID,
+      label: Drupal.edit.metadata.get(contextualLink.entityID, 'label')
     });
     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 aafbca3..5fed240 100644
--- a/core/modules/edit/js/editors/directEditor.js
+++ b/core/modules/edit/js/editors/directEditor.js
@@ -46,7 +46,7 @@ Drupal.edit.editors.direct = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  stateChange: function (fieldModel, state) {
+  stateChange: function (fieldModel, state, options) {
     var from = fieldModel.previous('state');
     var to = state;
     switch (to) {
@@ -78,7 +78,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 e143840..14bc2d2 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -14,11 +14,12 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  stateChange: function (fieldModel, state) {
+  stateChange: function (fieldModel, state, options) {
     var from = fieldModel.previous('state');
     var to = state;
     switch (to) {
       case 'inactive':
+        this.removeForm();
         break;
       case 'candidate':
         if (from !== 'inactive') {
@@ -38,7 +39,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       case 'changed':
         break;
       case 'saving':
-        this.save();
+        this.save(options);
         break;
       case 'saved':
         break;
@@ -82,7 +83,8 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
     var formOptions = {
       fieldID: fieldModel.id,
       $el: this.$el,
-      nocssjs: false
+      nocssjs: false,
+      reset: Drupal.edit.app.changedFieldsInTempstore.length === 0
     };
     Drupal.edit.util.form.load(formOptions, function (form, ajax) {
       Drupal.ajax.prototype.commands.insert(ajax, {
@@ -128,12 +130,13 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  save: function () {
+  save: function (options) {
     var $formContainer = this.$formContainer;
     var $submit = $formContainer.find('.edit-form-submit');
     var base = $submit.attr('id');
     var editorModel = this.model;
     var fieldModel = this.fieldModel;
+    var callback = (options || {}).callback || function () {};
 
     // Successfully saved.
     Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) {
@@ -144,7 +147,10 @@ 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.
       fieldModel.set('html', response.data);
-     };
+
+      // Invoke the optional callback.
+      callback.call();
+    };
 
     // Unsuccessfully saved; validation errors.
     Drupal.ajax[base].commands.editFieldFormValidationErrors = function (ajax, response, status) {
diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js
index e484b35..52b4d22 100644
--- a/core/modules/edit/js/models/EntityModel.js
+++ b/core/modules/edit/js/models/EntityModel.js
@@ -14,6 +14,8 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
     el: null,
     // An entity ID, of the form "<entity type>/<entity ID>", e.g. "node/1".
     id: null,
+    // The label of the entity.
+    label: null,
     // A Drupal.edit.FieldCollection for all fields of this entity.
     fields: null,
 
@@ -22,7 +24,13 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
 
     // Indicates whether this instance of this entity is currently being
     // edited in-place.
-    isActive: false
+    isActive: false,
+    //
+    isDirty: false,
+    // The current processing state of an entity.
+    state: 'inactive',
+    // @see AppView.appStateChange()
+    entityToolbar: null
   },
 
   /**
@@ -30,6 +38,34 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
    */
   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);
   },
 
   /**
@@ -39,12 +75,54 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
     Backbone.Model.prototype.destroy.apply(this, options);
 
     // Destroy all fields of this entity.
+    // @todo that app should be responisble for destroying the fields.
     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);
+  },
+
+  /**
    * {@inheritdoc}
    */
   sync: function () {
diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js
index 3e6e250..ef37ffe 100644
--- a/core/modules/edit/js/theme.js
+++ b/core/modules/edit/js/theme.js
@@ -40,23 +40,37 @@ Drupal.theme.editModal = function () {
 /**
  * Theme function for a toolbar container of the Edit module.
  *
- * @param settings
+ * @param Object settings
  *   An object with the following keys:
  *   - String id: the id to apply to the toolbar container.
  * @return String
  *   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 Object settings
@@ -68,9 +82,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 + '"';
   }
@@ -102,12 +118,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;
 };
@@ -134,4 +158,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 ad7136ec..e8c84bc 100644
--- a/core/modules/edit/js/util.js
+++ b/core/modules/edit/js/util.js
@@ -46,6 +46,8 @@ Drupal.edit.util.form = {
    *      field for which this form will be loaded.
    *    - Boolean nocssjs: (required) boolean indicating whether no CSS and JS
    *      should be returned (necessary when the form is invisible to the user).
+   *    - Boolean reset: (required) boolean indicating whether the TempStore
+   *      entity (if any) should be reset.
    * @param Function callback
    *   A callback function that will receive the form to be inserted, as well as
    *   the ajax object, necessary if the callback wants to perform other AJAX
@@ -59,7 +61,7 @@ Drupal.edit.util.form = {
     Drupal.ajax[fieldID] = new Drupal.ajax(fieldID, $el, {
       url: Drupal.edit.util.buildUrl(fieldID, drupalSettings.edit.fieldFormURL),
       event: 'edit-internal.edit',
-      submit: { nocssjs : options.nocssjs },
+      submit: { nocssjs : options.nocssjs, reset : options.reset },
       progress: { type : null } // No progress indicator.
     });
     // Implement a scoped editFieldForm AJAX command: calls the callback.
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 2e617f6..92ee704 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -1,4 +1,4 @@
-(function ($, _, Backbone, Drupal) {
+(function ($, _, Backbone, Drupal, drupalSettings) {
 
 "use strict";
 
@@ -11,6 +11,10 @@ Drupal.edit.AppView = Backbone.View.extend({
   activeEditorStates: [],
   singleEditorStates: [],
 
+  // Ephemeral storage for changed fields that persists through field
+  // rerendering.
+  changedFieldsInTempstore: [],
+
   /**
    * {@inheritdoc}
    *
@@ -53,6 +57,13 @@ Drupal.edit.AppView = Backbone.View.extend({
   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) {
@@ -63,6 +74,10 @@ Drupal.edit.AppView = Backbone.View.extend({
       });
     }
     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) {
@@ -135,15 +150,9 @@ Drupal.edit.AppView = Backbone.View.extend({
       // 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,7 +174,9 @@ Drupal.edit.AppView = Backbone.View.extend({
               // 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
+              });
             }
           }
         }
@@ -184,6 +195,12 @@ Drupal.edit.AppView = Backbone.View.extend({
    *   The field for which an in-place editor must be set up.
    */
   setupEditor: function (fieldModel) {
+    // Get the corresponding entity toolbar.
+    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 in-place editor.
     var editorName = fieldModel.get('metadata').editor;
     var editorModel = new Drupal.edit.EditorModel();
@@ -196,9 +213,14 @@ Drupal.edit.AppView = Backbone.View.extend({
     // Create in-place editor's toolbar — positions appropriately above the
     // edited element.
     var toolbarView = new Drupal.edit.FieldToolbarView({
+      el: fieldToolbarRoot,
       model: fieldModel,
       $editedElement: $(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
     });
 
     // Create decoration for edited element: padding if necessary, sets classes
@@ -206,8 +228,7 @@ Drupal.edit.AppView = Backbone.View.extend({
     var decorationView = new Drupal.edit.EditorDecorationView({
       el: $(editorView.getEditedElement()),
       model: fieldModel,
-      editorView: editorView,
-      toolbarId: toolbarView.getId()
+      editorView: editorView
     });
 
     // Track these three views in FieldModel so that we can tear them down
@@ -247,27 +268,134 @@ Drupal.edit.AppView = Backbone.View.extend({
   },
 
   /**
+   *
+   */
+  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: drupalSettings.basePath + '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.
+        entityModel.get('fields').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) {
           // 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);
@@ -292,7 +420,7 @@ Drupal.edit.AppView = Backbone.View.extend({
     if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== fieldModel) {
       this.model.set('highlightedEditor', fieldModel);
     }
-    else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate') {
+    else if (this.model.get('highlightedEditor') === fieldModel && to === 'candidate' || to === 'inactive') {
       this.model.set('highlightedEditor', null);
     }
 
@@ -310,6 +438,47 @@ Drupal.edit.AppView = Backbone.View.extend({
   },
 
   /**
+   *
+   */
+  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');
+    }
+  },
+
+
+  /**
    * Render an updated field (a field whose 'html' attribute changed).
    *
    * @param Drupal.edit.FieldModel fieldModel
@@ -363,6 +532,22 @@ Drupal.edit.AppView = Backbone.View.extend({
       this.setupEditor(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');
+        }
+      }
+    }
   },
 
   /**
@@ -389,4 +574,4 @@ Drupal.edit.AppView = Backbone.View.extend({
   }
 });
 
-}(jQuery, _, Backbone, Drupal));
+}(jQuery, _, Backbone, Drupal, drupalSettings));
diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js
index b9efdc2..680e10e 100644
--- a/core/modules/edit/js/views/EditorDecorationView.js
+++ b/core/modules/edit/js/views/EditorDecorationView.js
@@ -7,7 +7,6 @@
 "use strict";
 
 Drupal.edit.EditorDecorationView = Backbone.View.extend({
-  toolbarId: null,
 
   _widthAttributeIsEmpty: null,
 
@@ -25,14 +24,10 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    * @param Object options
    *   An object with the following keys:
    *   - Drupal.edit.EditorView editorView: the editor object view.
-   *   - String 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);
   },
 
@@ -84,6 +79,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
         this.startEdit();
         break;
       case 'changed':
+        this.markChanged(true);
         break;
       case 'saving':
         break;
@@ -101,10 +97,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();
   },
 
   /**
@@ -114,10 +108,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();
   },
 
   /**
@@ -126,7 +118,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();
   },
@@ -169,9 +162,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');
   },
 
   /**
@@ -184,6 +174,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
@@ -214,7 +211,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));
     }
 
@@ -257,6 +253,11 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
     // 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');
 
@@ -332,26 +333,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 221765d..84336fd 100644
--- a/core/modules/edit/js/views/EditorView.js
+++ b/core/modules/edit/js/views/EditorView.js
@@ -86,7 +86,7 @@ Drupal.edit.EditorView = Backbone.View.extend({
    * @param String state
    *   The state of the associated field. One of Drupal.edit.FieldModel.states.
    */
-  stateChange: function (fieldModel, state) {
+  stateChange: function (fieldModel, state, options) {
     var from = fieldModel.previous('state');
     var to = state;
     switch (to) {
@@ -102,6 +102,11 @@ Drupal.edit.EditorView = Backbone.View.extend({
         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
@@ -138,7 +143,7 @@ Drupal.edit.EditorView = Backbone.View.extend({
         if (from === 'invalid') {
           this.removeValidationErrors();
         }
-        this.save();
+        this.save(options);
         break;
       case 'saved':
         // Nothing to do for the typical in-place editor. Immediately after
@@ -168,9 +173,10 @@ Drupal.edit.EditorView = Backbone.View.extend({
   /**
    * Saves the modified value in the in-place editor for this field.
    */
-  save: function () {
+  save: function (options) {
     var fieldModel = this.fieldModel;
     var editorModel = this.model;
+    var callback = (options || {}).callback || function () {};
 
     function fillAndSubmitForm (value) {
       var $form = $('#edit_backstage form');
@@ -186,7 +192,8 @@ Drupal.edit.EditorView = Backbone.View.extend({
     var formOptions = {
       fieldID: this.fieldModel.id,
       $el: this.$el,
-      nocssjs: true
+      nocssjs: true,
+      reset: Drupal.edit.app.changedFieldsInTempstore.length === 0
     };
     Drupal.edit.util.form.load(formOptions, function (form, ajax) {
       // Create a backstage area for storing forms that are hidden from view
@@ -217,6 +224,9 @@ Drupal.edit.EditorView = Backbone.View.extend({
         // Then, set the 'html' attribute on the field model. This will cause
         // the field to be rerendered.
         fieldModel.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..9918d97
--- /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;
+  },
+
+  /**
+   * {@inheritdoc}
+   */
+  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();
+  },
+
+  /**
+   * {@inheritdoc}
+   */
+  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 activeEditorView = activeEditor && activeEditor.editorView;
+    var activeEditedElement = activeEditorView && activeEditorView.getEditedElement();
+
+    // Label of a highlighted field, if it exists.
+    var highlightedEditor = Drupal.edit.app.model.get('highlightedEditor');
+    var highlightedEditorView = highlightedEditor && highlightedEditor.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;
+    }
+  },
+
+  /**
+   * Generates a state-dependent label for the entity toolbar.
+   */
+  label: function () {
+    // The entity label.
+    var label = '"' + this.model.get('label') + '"';
+
+    // Label of an active field, if it exists.
+    var activeEditor = Drupal.edit.app.model.get('activeEditor');
+    var activeFieldLabel = activeEditor && activeEditor.get('metadata').label;
+    activeFieldLabel = activeFieldLabel && activeFieldLabel + ' — ' + label;
+
+    // Label of a highlighted field, if it exists.
+    var highlightedEditor = Drupal.edit.app.model.get('highlightedEditor');
+    var highlightedFieldLabel = highlightedEditor && highlightedEditor.get('metadata').label;
+    highlightedFieldLabel = highlightedFieldLabel && highlightedFieldLabel + ' — ' + label;
+
+    this.$el
+      .find('.edit-toolbar-label')
+      .text(activeFieldLabel || highlightedFieldLabel || label);
+  },
+
+  /**
+   * 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 4cdd53d..56b7c1e 100644
--- a/core/modules/edit/js/views/FieldToolbarView.js
+++ b/core/modules/edit/js/views/FieldToolbarView.js
@@ -14,27 +14,15 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
   // A reference to the in-place editor.
   editorView: 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'
-  },
-
   /**
    * {@inheritdoc}
    */
   initialize: function (options) {
     this.$editedElement = options.$editedElement;
     this.editorView = options.editorView;
-
-    this._loader = null;
-    this._loaderVisibleStart = 0;
+    this.$root = this.$el;
 
     // Generate a DOM-compatible ID for the form container DOM element.
     this._id = 'edit-toolbar-for-' + this.model.id.replace(/\//g, '_');
@@ -46,19 +34,17 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    * {@inheritdoc}
    */
   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.$editedElement.css('display') === 'inline') {
-      this.$el.prependTo(this.$editedElement.offsetParent());
-      var pos = this.$editedElement.position();
-      this.$el.css('left', pos.left).css('top', pos.top);
+      this.$el.prependTo(this.$field);
     }
     else {
-      this.$el.insertBefore(this.$editedElement);
+      this.$el.prependTo(this.$root);
     }
 
     return this;
@@ -71,7 +57,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) {
@@ -81,240 +67,52 @@ 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.editorView.getEditUISettings().padding) {
-            this._unpad();
-          }
-        }
         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.editorView.getEditUISettings().fullWidthToolbar) {
           this.$el.addClass('edit-toolbar-fullwidth');
         }
 
-        if (this.editorView.getEditUISettings().padding) {
-          this._pad();
-        }
         if (this.editorView.getEditUISettings().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.$editedElement.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.$editedElement[0] && !$.contains(this.$editedElement, event.relatedTarget)) {
-      this.$editedElement.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;
-    }
-  },
-
-  /**
-   * Decorate the field with markup to indicate it is highlighted.
-   */
-  startHighlight: function () {
-    // Retrieve the lavel to show for this field.
-    var label = this.model.get('metadata').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);
-  },
-
-  /**
-   * Decorate the field with markup to indicate edit state; append a toolbar.
-   */
-  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');
-  },
-
-  /**
-   * 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.$editedElement.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.editorView.getEditUISettings().fullWidthToolbar) {
-      $hf.css({ width: this.$editedElement.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.editorView.getEditUISettings().fullWidthToolbar) {
-      $hf.css({ width: '' });
-    }
-  },
-
-  /**
    * Insert WYSIWYG markup into the associated toolbar.
    */
   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');
   },
 
   /**
@@ -354,49 +152,39 @@ 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.
-   * @param String classes
-   *   A space delimited list of class names to add to the toolgroup.
-   */
-  addClass: function (toolgroup, classes) {
-    this._find(toolgroup).addClass(classes);
-  },
-
-  /**
-   * Removes classes from a toolgroup.
+   * Finds a toolgroup.
    *
    * @param String toolgroup
    *   A toolgroup name.
-   * @param String classes
-   *   A space delimited list of class names to remove from the toolgroup.
+   * @return jQuery
    */
-  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.
-   * @return jQuery
    */
-  _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) {
+      var entityModel = that.model.get('entity');
+      entityModel.trigger('viewChanged', entityModel);
+      $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);
+   }
 });
 
 })(jQuery, _, Backbone, Drupal);
diff --git a/core/modules/edit/js/views/ModalView.js b/core/modules/edit/js/views/ModalView.js
index 9f096ba..9db2831 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..f5e26e3 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));
@@ -135,19 +138,17 @@ public function metadata(Request $request) {
    *   The name of the language for which the field is being edited.
    * @param string $view_mode
    *   The view mode the field should be rerendered in.
-   * @param bool $reset_tempstore
-   *   Set to FALSE if the existing tempstore version should be kept, or TRUE
-   *   if the existing tempstore version should be removed.
    * @return \Drupal\Core\Ajax\AjaxResponse
    *   The Ajax response.
    */
-  public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view_mode, $reset_tempstore = FALSE) {
+  public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view_mode) {
     $response = new AjaxResponse();
 
-    // Replace entity with tempstore copy if available, init tempstore copy
-    // otherwise.
-    if (!$reset_tempstore && ($temp_entity = Drupal::service('user.tempstore')->get('edit')->get($entity->uuid))) {
-      $entity = $temp_entity;
+    // Replace entity with tempstore copy if available and not resetting, init
+    // tempstore copy otherwise.
+    $tempstore_entity = Drupal::service('user.tempstore')->get('edit')->get($entity->uuid);
+    if ($tempstore_entity && !(isset($_POST['reset']) && $_POST['reset'] === 'true')) {
+      $entity = $tempstore_entity;
     }
     else {
       Drupal::service('user.tempstore')->get('edit')->set($entity->uuid, $entity);
@@ -224,8 +225,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..5492914 100644
--- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
+++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
@@ -56,9 +56,18 @@ public function __construct(EditEntityFieldAccessCheckInterface $access_checker,
   }
 
   /**
-   * Implements \Drupal\edit\MetadataGeneratorInterface::generate().
+   * {@inheritdoc}
    */
-  public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) {
+  public function generateEntity(EntityInterface $entity, $langcode) {
+    return array(
+      'label' => $entity->label($langcode),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function generateField(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) {
     $field_name = $instance['field_name'];
 
     // Early-return if user does not have access.
diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
index 16db770..6d60f33 100644
--- a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
+++ b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
@@ -11,11 +11,24 @@
 use Drupal\field\Plugin\Core\Entity\FieldInstance;
 
 /**
- * Interface for generating in-place editing metadata for an entity field.
+ * Interface for generating in-place editing metadata.
  */
 interface MetadataGeneratorInterface {
 
   /**
+   * Generates in-place editing metadata for an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being edited.
+   * @param string $langcode
+   *   The name of the language for which the field is being edited.
+   * @return array
+   *   An array containing metadata with the following keys:
+   *   - label: the user-visible label for the entity.
+   */
+  public function generateEntity(EntityInterface $entity, $langcode);
+
+  /**
    * Generates in-place editing metadata for an entity field.
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
@@ -34,6 +47,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 91e3521..0b9aa7c 100644
--- a/core/modules/editor/js/editor.formattedTextEditor.js
+++ b/core/modules/editor/js/editor.formattedTextEditor.js
@@ -55,7 +55,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
   /**
    * {@inheritdoc}
    */
-  stateChange: function (fieldModel, state) {
+  stateChange: function (fieldModel, state, options) {
     var editorModel = this.model;
     var from = fieldModel.previous('state');
     var to = state;
@@ -122,7 +122,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
         if (from === 'invalid') {
           this.removeValidationErrors();
         }
-        this.save();
+        this.save(options);
         break;
 
       case 'saved':
