From b6a325205ed0893255682b3b8f0a99e74964dcd6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Sat, 18 May 2013 18:43:59 -0700
Subject: [PATCH] Issue #1678002-130
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

commit 55e74964677fbb701706c90d14e74fcabee1de3b
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 18:40:51 2013 -0700

    I think saving and quit saving works now.

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

commit 23ba7ebcfbd94fd02a2f08646cfcd0a0ea1f7adc
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 17:33:30 2013 -0700

    cancel/confirm/save is working.

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

commit 8719d71527cf55cb4a396ad55c14393d639f464d
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 17:08:42 2013 -0700

    pass AJAX error object to the error handler.

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

commit 6a3a7cea5696215cba57f5e3286fa89335ecb375
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 16:57:48 2013 -0700

    saving works again.

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

commit 6ab99652771c9a70c3ae41b525a8e622e469578b
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 15:45:46 2013 -0700

    Removed a worthless callback

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

commit d1f000dd902e26b7054775244b3bd49dd50f05c4
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 15:20:53 2013 -0700

    Get rid of the edit-changed on cancel or change.

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

commit e7209149c62376fad381453cf83fd8963eb63f88
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 15:14:23 2013 -0700

    wim's patch

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

commit 0cede712947c8567639e94aa303f44a87540c7a6
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sat May 18 15:13:17 2013 -0700

    1678002-129

    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                     |  244 +++++---------
 core/modules/edit/edit.module                      |    3 +
 core/modules/edit/js/edit.js                       |   24 +-
 core/modules/edit/js/editors/directEditor.js       |    4 +-
 core/modules/edit/js/editors/formEditor.js         |   16 +-
 core/modules/edit/js/models/EntityModel.js         |  275 ++++++++++++++-
 core/modules/edit/js/theme.js                      |   53 ++-
 core/modules/edit/js/views/AppView.js              |  178 +++++++---
 core/modules/edit/js/views/ContextualLinkView.js   |   28 +-
 core/modules/edit/js/views/EditorDecorationView.js |   53 +--
 core/modules/edit/js/views/EditorView.js           |   19 +-
 core/modules/edit/js/views/EntityToolbarView.js    |  352 ++++++++++++++++++++
 core/modules/edit/js/views/FieldToolbarView.js     |  284 ++--------------
 core/modules/edit/js/views/ModalView.js            |    4 +-
 .../edit/lib/Drupal/edit/EditController.php        |   10 +-
 .../edit/lib/Drupal/edit/MetadataGenerator.php     |   13 +-
 .../lib/Drupal/edit/MetadataGeneratorInterface.php |   17 +-
 .../editor/js/editor.formattedTextEditor.js        |    4 +-
 18 files changed, 1033 insertions(+), 548 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..80a9480 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,75 @@
   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-toolbar span.close {
+#edit_modal .action-cancel span,
+.edit-toolbar .action-cancel span {
   background: url('../images/close.png') no-repeat 3px 2px;
-  text-indent: -999em;
   direction: ltr;
+  display: inline-block;
+  width: 18px;
+  text-indent: -999em;
 }
-
-.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 5ac9e44..6ce3e17 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -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/js/edit.js b/core/modules/edit/js/edit.js
index 6899125..c2a50df 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -22,8 +22,7 @@
 
 var options = $.extend({
   strings: {
-    quickEdit: Drupal.t('Quick edit'),
-    stopQuickEdit: Drupal.t('Stop quick edit')
+    quickEdit: Drupal.t('Quick edit')
   }
 }, drupalSettings.edit);
 
@@ -57,6 +56,9 @@ Drupal.behaviors.edit = {
     $('body').once('edit-init', initEdit);
 
     // Process each field element: queue to be used or to fetch metadata.
+    // When a field is being rerender after editing, it will process
+    // immediately. New fields will fail to process. They are queued to have
+    // their metadata fetched, which occurs below in fetchMissingMetaData().
     $(context).find('[data-edit-id]').once('edit').each(function (index, fieldElement) {
       processField(fieldElement);
     });
@@ -167,10 +169,15 @@ function initEdit (bodyElement) {
 function processField (fieldElement) {
   var metadata = Drupal.edit.metadata;
   var fieldID = fieldElement.getAttribute('data-edit-id');
+  var entityID = extractEntityID(fieldID);
 
-  // Early-return if metadata for this field is mising.
+  // Early-return if metadata for this field is missing.
   if (!metadata.has(fieldID)) {
-    fieldsMetadataQueue.push({ el: fieldElement, fieldID: fieldID });
+    fieldsMetadataQueue.push({
+      el: fieldElement,
+      fieldID: fieldID,
+      entityID: entityID
+    });
     return;
   }
   // Early-return if the user is not allowed to in-place edit this field.
@@ -232,6 +239,7 @@ function fetchMissingMetadata (callback) {
   if (fieldsMetadataQueue.length) {
     var fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
     var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
+    var entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
     fieldsMetadataQueue = [];
 
     $(window).ready(function () {
@@ -242,7 +250,10 @@ function fetchMissingMetadata (callback) {
       Drupal.ajax[id] = new Drupal.ajax(id, $el, {
         url: drupalSettings.edit.metadataURL,
         event: 'edit-internal.edit',
-        submit: { 'fields[]': fieldIDs },
+        submit: {
+          'fields[]': fieldIDs,
+          'entities[]': entityIDs
+        },
         // No progress indicator.
         progress: { type: null }
       });
@@ -313,7 +324,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..cc9aa97 100644
--- a/core/modules/edit/js/models/EntityModel.js
+++ b/core/modules/edit/js/models/EntityModel.js
@@ -1,4 +1,4 @@
-(function (Backbone, Drupal) {
+(function (_, $, Backbone, Drupal) {
 
 "use strict";
 
@@ -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,259 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
    */
   initialize: function () {
     this.set('fields', new Drupal.edit.FieldCollection());
+
+    // Respond to state changes.
+    this.on('change:state', this.stateChange, this);
+
+    // 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);
+  },
+
+  /**
+   * @todo We need to restrict the state progression of an entity.
+   * @todo We need a special exception to go from anything to deactivating.
+   */
+  stateChange: function (entityModel, state, options) {
+    var from = entityModel.previous('state');
+    var to = state;
+    switch (to) {
+      case 'deactivating':
+        // Return the fields to candidate state. A changed field may have to go
+        // through confirmation first.
+        entityModel.get('fields').each(function (fieldModel) {
+          // If the field is already in the candidate state, trigger a change
+          // event so that the entityModel can move to the next state in
+          // deactivation.
+          if (_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
+            fieldModel.trigger('change:state', fieldModel, fieldModel.get('state'), options);
+          }
+          else {
+            fieldModel.set('state', 'candidate', options);
+          }
+        });
+        break;
+      case 'closing':
+        _.extend(options, {
+            reason: 'stop'
+          });
+        this.get('fields').each(function (fieldModel) {
+          fieldModel.set('state', 'inactive', options);
+        });
+        break;
+      case 'closed':
+        this.set('isActive', false);
+        this.get('fields').each(function (fieldModel) {
+          // fieldModel.destroy();
+        });
+        break;
+      case 'launching':
+        break;
+      case 'opening':
+        // Set the fields to candidate state.
+        entityModel.get('fields').each(function (fieldModel) {
+          fieldModel.set('state', 'candidate', options);
+        });
+        break;
+      case 'opened':
+        this.set('isActive', true);
+        break;
+      case 'committing':
+        this.get('fields').chain()
+          .filter(function (fieldModel) {
+            return _.intersection([fieldModel.get('state')], Drupal.edit.app.changedEditorStates).length;
+          })
+          .each(function (fieldModel) {
+            fieldModel.set('state', 'saving', options);
+          });
+        break;
+      case 'quitcommitting':
+        this.get('fields').chain()
+          .filter(function (fieldModel) {
+            return _.intersection([fieldModel.get('state')], Drupal.edit.app.changedEditorStates).length;
+          })
+          .each(function (fieldModel) {
+            fieldModel.set('state', 'saving', options);
+          });
+        break;
+      case 'activating':
+        break;
+      case 'active':
+        break;
+      case 'changed':
+        break;
+      case 'saving':
+        break;
+      case 'saved':
+        break;
+      case 'invalid':
+        break;
+    }
+  },
+
+  /**
+   *
+   */
+  fieldStateChange: function (fieldModel, state, options) {
+    var from = fieldModel.previous('state');
+    var to = state;
+    var entityModel = this;
+    // Switch on the entityModel state.
+    switch (this.get('state')) {
+      case 'deactivating':
+        // If the entity is set to closing, it must first confirm that all of its
+        // fieldModels have returned to the candidate state before it can start
+        // deactivating.
+        var mayDeactivate = true;
+        this.get('fields').each(function (fieldModel) {
+          if (!_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
+            mayDeactivate = false;
+          }
+        });
+        if (mayDeactivate) {
+          _.defer(function () {
+            entityModel.set('state', 'closing')
+          });
+        }
+        break;
+      case 'closing':
+        // If the entity is set to deactivating, it must first confirm that all of
+        // its fieldModels have returned to inactive before it can transition to
+        // inactive.
+        var mayClose = true;
+        this.get('fields').each(function (fieldModel) {
+          if (fieldModel.get('state') !== 'inactive') {
+            mayClose = false;
+          }
+        });
+        if (mayClose) {
+          _.defer(function () {
+            entityModel.set('state', 'closed');
+          })
+
+        }
+        break;
+      case 'opening':
+        // If the entity is set to opening, it must first confirm that all of
+        // its fieldModels have transitioned to the candidate state before it
+        // can declare that it is open.
+        var mayOpen = true;
+        this.get('fields').each(function (fieldModel) {
+        if (!_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
+            mayOpen = false;
+          }
+        });
+        if (mayOpen) {
+          _.defer(function () {
+            entityModel.set('state', 'opened');
+          });
+        }
+        break;
+      case 'committing':
+        var mayCommit = true;
+        this.get('fields').each(function (fieldModel) {
+          if (!_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
+            mayCommit = false;
+          }
+        });
+        if (mayCommit) {
+          _.defer(function () {
+            entityModel.save({
+              callback: function () {
+                entityModel.set('state', 'opened');
+              }
+            });
+          })
+        }
+        break;
+      case 'quitcommitting':
+        var mayCommit = true;
+        this.get('fields').each(function (fieldModel) {
+          if (!_.intersection([fieldModel.get('state')], ['candidate', 'highlighted']).length) {
+            mayCommit = false;
+          }
+        });
+        if (mayCommit) {
+          _.defer(function () {
+            entityModel.save({
+              callback: function () {
+                entityModel.set('state', 'deactivating');
+              }
+            });
+          })
+        }
+        break;
+    }
+
+    // Switch on the fieldModel state.
+    switch (to) {
+      case 'candidate':
+        this.set('isDirty', false);
+        break;
+      case 'activating':
+        break;
+      case 'active':
+        break;
+      case 'changed':
+        // The EntityToolbarView is using the isDirty attribute to reposition
+        // the toolbar. This is a legacy holdout.
+        this.set('isDirty', true);
+        break;
+      case 'saving':
+        break;
+      case 'saved':
+        this.set('isDirty', false);
+        break;
+      case 'invalid':
+        break;
+    }
+  },
+
+  /**
+   * Fires an AJAX request to the REST save URL for an entity.
+   */
+  save: function (options) {
+    var entityModel = this;
+    var id = 'edit-save-entity';
+    // Create a temporary element to be able to use Drupal.ajax.
+    var $el = $('#edit-entity-toolbar').find('.action-save'); // 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 (e) {
+        // Clean up.
+        $el.unbind('edit-save.edit');
+        throw new Error(e);
+      }
+    });
+    // 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.
+      entityModel.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');
+
+      if ('callback' in options && typeof options.callback === 'function') {
+        options.callback.call(entityModel);
+      }
+    };
+    $el.trigger('edit-save.edit');
   },
 
   /**
@@ -39,12 +300,20 @@ 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();
     });
   },
 
   /**
+   *
+   */
+  viewChange: function (view) {
+    this.trigger('fieldViewChange', view);
+  },
+
+  /**
    * {@inheritdoc}
    */
   sync: function () {
@@ -58,4 +327,4 @@ Drupal.edit.EntityCollection = Backbone.Collection.extend({
   model: Drupal.edit.EntityModel
 });
 
-}(Backbone, Drupal));
+}(_, jQuery, Backbone, Drupal));
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/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 2e617f6..4c836ea 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}
    *
@@ -25,11 +29,12 @@ Drupal.edit.AppView = Backbone.View.extend({
     // @see Drupal.edit.FieldModel.states
     this.activeEditorStates = ['activating', 'active'];
     this.singleEditorStates = ['highlighted', 'activating', 'active'];
+    this.changedEditorStates = ['saving', 'changed', 'invalid'];
 
     options.entitiesCollection
       // Track app state.
-      .on('change:isActive', this.appStateChange, this)
-      .on('change:isActive', this.enforceSingleActiveEntity, this);
+      .on('change:state', this.appStateChange, this);
+      //.on('change:isActive', this.enforceSingleActiveEntity, this);
 
     options.fieldsCollection
       // Track app state.
@@ -50,27 +55,39 @@ Drupal.edit.AppView = Backbone.View.extend({
    * @param Boolean isActive
    *   A boolean that represents the changed active state of the entityModel.
    */
-  appStateChange: function (entityModel, isActive) {
+  appStateChange: function (entityModel, state, options) {
     var app = this;
-    if (isActive) {
-      // Move all fields of this entity from the 'inactive' state to the
-      // 'candidate' state.
-      entityModel.get('fields').each(function (fieldModel) {
-        // First, set up editors; they must be notified of state changes.
-        app.setupEditor(fieldModel);
-        // Second, change the field's state.
-        fieldModel.set('state', 'candidate');
-      });
-    }
-    else {
-      // Move all fields of this entity from whatever state they are in to
-      // the 'inactive' state.
-      entityModel.get('fields').each(function (fieldModel) {
-        // First, change the field's state.
-        fieldModel.set('state', 'inactive', { reason: 'stop' });
-        // Second, tear down editors.
-        app.teardownEditor(fieldModel);
-      });
+    switch (state) {
+      case 'launching':
+          // 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) {
+          // Set up editors; they must be notified of state changes.
+          app.setupEditor(fieldModel);
+        });
+        _.defer(function () {
+          entityModel.set('state', 'opening');;
+        });
+        break;
+      case 'closed':
+        var entityToolbar = entityModel.get('entityToolbar');
+        if (entityToolbar) {
+          entityModel.get('entityToolbar').remove();
+          entityModel.set('entityToolbar', null);
+        }
+        // Teardown the editors.
+        entityModel.get('fields').each(function (fieldModel) {
+          // Second, tear down editors.
+          app.teardownEditor(fieldModel);
+        });
+        break;
     }
   },
 
@@ -88,7 +105,7 @@ Drupal.edit.AppView = Backbone.View.extend({
    * @param Function callback
    *   The callback function that should receive the state acceptance result.
    */
-  acceptEditorStateChange: function (from, to, context, callback) {
+  acceptEditorStateChange: function (from, to, context) {
     var accept = true;
 
     // If the app is in view mode, then reject all state changes except for
@@ -135,15 +152,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 +176,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: (context || {}).callback || function () {}
+              });
             }
           }
         }
@@ -184,6 +197,11 @@ 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 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 +214,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 +229,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
@@ -251,7 +273,10 @@ Drupal.edit.AppView = Backbone.View.extend({
    *
    * @see acceptEditorStateChange()
    */
-  _confirmStopEditing: function () {
+  _confirmStopEditing: function (options) {
+    var activeEntity = Drupal.edit.collections.entities.where({ isActive: true })[0];
+    // Set the active entity to opened while we confirm the field changes.
+    activeEntity.set('state', 'opened');
     // Only instantiate if there isn't a modal instance visible yet.
     if (!this.model.get('activeModal')) {
       var that = this;
@@ -259,15 +284,22 @@ Drupal.edit.AppView = Backbone.View.extend({
         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: 'save', type: 'submit', classes: 'action-save edit-button', label: Drupal.t('Save') },
+          { action: 'discard', classes: 'action-cancel edit-button', label: Drupal.t('Discard changes') }
         ],
-        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') {
+            activeEntity.set('state', 'quitcommitting');
+          }
+          else {
+            activeEntity.set('state', 'deactivating', {
+              confirmed: true
+            });
+          }
         }
       });
       this.model.set('activeModal', modal);
@@ -292,7 +324,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);
     }
 
@@ -301,7 +333,6 @@ Drupal.edit.AppView = Backbone.View.extend({
       this.model.set('activeEditor', fieldModel);
     }
     else if (this.model.get('activeEditor') === fieldModel && to === 'candidate') {
-      // Discarded if it transitions from a changed state to 'candidate'.
       if (from === 'changed' || from === 'invalid') {
         fieldModel.editorView.revert();
       }
@@ -310,6 +341,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.id);
+        // 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 +435,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.id) {
+        var $field = $(fieldModel.get('el'));
+        if ($field.is('.edit-editable')) {
+          $field.addClass('edit-changed');
+        }
+        else {
+          $field.find('.edit-editable').addClass('edit-changed');
+        }
+      }
+    }
   },
 
   /**
@@ -389,4 +477,4 @@ Drupal.edit.AppView = Backbone.View.extend({
   }
 });
 
-}(jQuery, _, Backbone, Drupal));
+}(jQuery, _, Backbone, Drupal, drupalSettings));
diff --git a/core/modules/edit/js/views/ContextualLinkView.js b/core/modules/edit/js/views/ContextualLinkView.js
index 4b4d075..61199e2 100644
--- a/core/modules/edit/js/views/ContextualLinkView.js
+++ b/core/modules/edit/js/views/ContextualLinkView.js
@@ -17,7 +17,7 @@ Drupal.edit.ContextualLinkView = Backbone.View.extend({
     return {
       'click a': function (event) {
         event.preventDefault();
-        this.model.set('isActive', !this.model.get('isActive'));
+        this.model.set('state', 'launching');
       },
       'touchEnd a': touchEndToClick
     };
@@ -33,36 +33,22 @@ Drupal.edit.ContextualLinkView = Backbone.View.extend({
    *   - strings: the strings for the "Quick edit" link
    */
   initialize: function (options) {
+    // Insert the text of the quick edit toggle.
+    this.$el.find('a').text(this.options.strings.quickEdit);
     // Initial render.
     this.render();
-
     // Re-render whenever this entity's isActive attribute changes.
     this.model.on('change:isActive', this.render, this);
-
-    // Hide the contextual links whenever an in-place editor is active.
-    this.options.appModel.on('change:activeEditor', this.toggleContextualLinksVisibility, this);
   },
 
   /**
    * {@inheritdoc}
    */
-  render: function () {
-    var strings = this.options.strings;
-    var text = !this.model.get('isActive') ? strings.quickEdit : strings.stopQuickEdit;
-    this.$el.find('a').text(text);
-    return this;
-  },
+  render: function (entityModel, isActive) {
+    // Hides the contextual links if an in-place editor is active.
+    this.$el.parents('.contextual').toggle(!isActive);
 
-  /**
-   * Hides the contextual links if an in-place editor is active.
-   *
-   * @param Drupal.edit.AppModel appModel
-   *   The application state model.
-   * @param null|Drupal.edit.FieldModel activeEditor
-   *   The model of the field that is currently being edited, or, if none, null.
-   */
-  toggleContextualLinksVisibility: function (appModel, activeEditor) {
-    this.$el.parents('.contextual').toggle(activeEditor === null);
+    return this;
   }
 
 });
diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js
index b9efdc2..0526136 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);
   },
 
@@ -61,6 +56,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
         this.undecorate();
         break;
       case 'candidate':
+        this.markChanged(false);
         this.decorate();
         if (from !== 'inactive') {
           this.stopHighlight();
@@ -84,6 +80,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
         this.startEdit();
         break;
       case 'changed':
+        this.markChanged(true);
         break;
       case 'saving':
         break;
@@ -101,10 +98,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 +109,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 +119,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 +163,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 +175,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 +212,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));
     }
 
@@ -332,26 +329,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..5eba8bd 100644
--- a/core/modules/edit/js/views/EditorView.js
+++ b/core/modules/edit/js/views/EditorView.js
@@ -43,6 +43,7 @@ Drupal.edit.EditorView = Backbone.View.extend({
     // The el property is the field, which should not be removed. Remove the
     // pointer to it, then call Backbone.View.prototype.remove().
     this.setElement();
+    this.fieldModel.off(null, null, this);
     Backbone.View.prototype.remove.call(this);
   },
 
@@ -86,7 +87,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 +103,14 @@ Drupal.edit.EditorView = Backbone.View.extend({
         if (from === 'invalid') {
           this.removeValidationErrors();
         }
+
+        // Attempt to save if the field was previously in the changed state.
+        // WIM: this code makes zero sense to me, and looks utterly evil!
+        if (from === 'changed') {
+          _.defer(function () {
+            fieldModel.set('state', 'saving');
+          });
+        }
         break;
       case 'highlighted':
         // Nothing to do for the typical in-place editor: it should not be
@@ -138,7 +147,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 +177,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 +196,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
diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
new file mode 100644
index 0000000..4559a82
--- /dev/null
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -0,0 +1,352 @@
+/**
+ * @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': 'onClickCancel',
+      '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();
+    }
+
+    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 'opened':
+          this.position();
+          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());
+    // Save the model.
+    this.model.set('state', 'committing');
+  },
+
+  /**
+   * Sets the model state to candidate when the cancel button is clicked.
+   *
+   * @param jQuery event
+   */
+  onClickCancel: function (event) {
+    event.preventDefault();
+    this.model.set('state', 'deactivating');
+  },
+
+  /**
+   *
+   */
+  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: Drupal.t('Close'), 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..d115ed4 100644
--- a/core/modules/edit/js/views/ModalView.js
+++ b/core/modules/edit/js/views/ModalView.js
@@ -72,8 +72,8 @@ Drupal.edit.ModalView = Backbone.View.extend({
         that.remove();
       });
 
-    var action = $(event.target).attr('data-edit-modal-action');
-    return this.callback(action);
+    var action = $(event.target).parent().attr('data-edit-modal-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 76c859b..bcfc2fa 100644
--- a/core/modules/edit/lib/Drupal/edit/EditController.php
+++ b/core/modules/edit/lib/Drupal/edit/EditController.php
@@ -60,6 +60,8 @@ public function metadata(Request $request) {
     if (!isset($fields)) {
       throw new NotFoundHttpException();
     }
+    $entities = $request->request->get('entities');
+
     $metadataGenerator = $this->container->get('edit.metadata.generator');
 
     $metadata = array();
@@ -82,8 +84,12 @@ public function metadata(Request $request) {
       if (!$langcode || (field_valid_language($langcode) !== $langcode)) {
         throw new NotFoundHttpException();
       }
-
-      $metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode);
+      // If the entity information for this field is requested, include it.
+      $entity_id = $entity->entityType() . '/' . $entity_id;
+      if (in_array($entity_id, $entities) && !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));
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':
-- 
1.7.10.4

