 core/misc/ajax.js                                  |    3 +
 core/modules/edit/css/edit.icons-rtl.css           |    3 +
 core/modules/edit/css/edit.icons.css               |   46 ++
 core/modules/edit/css/edit.module-rtl.css          |    3 +
 core/modules/edit/css/edit.module.css              |  336 ++------------
 core/modules/edit/css/edit.theme-rtl.css           |    3 +
 core/modules/edit/css/edit.theme.css               |  224 ++++++++++
 core/modules/edit/edit.module                      |    6 +
 core/modules/edit/images/close.png                 |    4 -
 core/modules/edit/images/icon-close.png            |    3 +
 core/modules/edit/images/icon-throbber.gif         |    6 +
 core/modules/edit/images/throbber.gif              |    6 -
 core/modules/edit/js/edit.js                       |   46 +-
 core/modules/edit/js/editors/directEditor.js       |    4 +-
 core/modules/edit/js/editors/formEditor.js         |   35 +-
 core/modules/edit/js/models/EntityModel.js         |  463 +++++++++++++++++++-
 core/modules/edit/js/models/FieldModel.js          |   75 +++-
 core/modules/edit/js/theme.js                      |   48 +-
 core/modules/edit/js/util.js                       |   40 +-
 core/modules/edit/js/views/AppView.js              |  211 ++++++---
 core/modules/edit/js/views/ContextualLinkView.js   |   28 +-
 core/modules/edit/js/views/EditorDecorationView.js |   57 +--
 core/modules/edit/js/views/EditorView.js           |   30 +-
 core/modules/edit/js/views/EntityToolbarView.js    |  310 +++++++++++++
 core/modules/edit/js/views/EntityView.js           |   31 ++
 core/modules/edit/js/views/FieldToolbarView.js     |  292 ++----------
 .../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        |    8 +-
 30 files changed, 1605 insertions(+), 756 deletions(-)

diff --git a/core/misc/ajax.js b/core/misc/ajax.js
index 045cccf..c7a969c 100644
--- a/core/misc/ajax.js
+++ b/core/misc/ajax.js
@@ -136,6 +136,9 @@ Drupal.AjaxError.prototype.constructor = Drupal.AjaxError;
  * @code
  *    Drupal.behaviors.myCustomAJAXStuff = {
  *      attach: function (context, settings) {
+ *        // We don't want to override the original commands!
+ *        Drupal.ajax['edit-submit'].commands = {};
+ *        // Now make just the 'insert' command available.
  *        Drupal.ajax['edit-submit'].commands.insert = function (ajax, response, status) {
  *          new_content = $(response.data);
  *          $('#my-wrapper').append(new_content);
diff --git a/core/modules/edit/css/edit.icons-rtl.css b/core/modules/edit/css/edit.icons-rtl.css
new file mode 100644
index 0000000..7c8de15
--- /dev/null
+++ b/core/modules/edit/css/edit.icons-rtl.css
@@ -0,0 +1,3 @@
+/**
+ * @file edit.icons-rtl.css
+ */
diff --git a/core/modules/edit/css/edit.icons.css b/core/modules/edit/css/edit.icons.css
new file mode 100644
index 0000000..adab959
--- /dev/null
+++ b/core/modules/edit/css/edit.icons.css
@@ -0,0 +1,46 @@
+/**
+ * @file edit.icons.css
+ */
+
+.edit .icon {
+  min-height: 1em;
+  min-width: 2.5em;
+  position: relative;
+}
+.edit .icon.icon-only {
+  text-indent: -9999px; /* LTR */
+}
+.edit .icon.icon-end {
+  padding-right: 2.5em; /* LTR */
+}
+.edit .icon:before {
+  background-attachment: scroll;
+  background-color: transparent;
+  background-position: center center;
+  background-repeat: no-repeat;
+  content: '';
+  display: block;
+  height: 100%;
+  left: 0; /* LTR */
+  position: absolute;
+  top: 0;
+  width: 100%;
+}
+.edit .icon-end:before {
+  left: auto; /* LTR */
+  right: 0.5em; /* LTR */
+  width: 18px;
+}
+.edit button.icon {
+  font-size: 1em;
+}
+
+/**
+ * Images.
+ */
+.edit .icon-close:before {
+  background-image: url("../images/icon-close.png");
+}
+.edit .icon-throbber:before {
+  background-image: url("../images/icon-throbber.gif");
+}
diff --git a/core/modules/edit/css/edit.module-rtl.css b/core/modules/edit/css/edit.module-rtl.css
new file mode 100644
index 0000000..d05c000
--- /dev/null
+++ b/core/modules/edit/css/edit.module-rtl.css
@@ -0,0 +1,3 @@
+/**
+ * @file edit.module-rtl.css
+ */
diff --git a/core/modules/edit/css/edit.module.css b/core/modules/edit/css/edit.module.css
index 6a5ac83..7af5aee 100644
--- a/core/modules/edit/css/edit.module.css
+++ b/core/modules/edit/css/edit.module.css
@@ -1,85 +1,13 @@
 /**
- * Animations.
- */
-.edit-animate-invisible {
-  opacity: 0;
-}
-
-.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;
-}
-
-.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;
-}
-
-.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;
-}
-
-.edit-animate-delay-veryfast {
-  -webkit-transition-delay: .05s;
-     -moz-transition-delay: .05s;
-      -ms-transition-delay: .05s;
-       -o-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;
-}
-
-.edit-animate-disable-width {
-  -webkit-transition: width 0s;
-     -moz-transition: width 0s;
-      -ms-transition: width 0s;
-       -o-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;
-}
-
-.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.
+ * @file edit.module.css
  *
  * Note: every class is prefixed with "edit-" to prevent collisions with modules
  * or themes. In IPE-specific DOM subtrees, this is not necessary.
  */
 
-/* Editable. */
+/**
+ * Editable.
+ */
 .edit-editable {
   z-index: 300;
   position: relative;
@@ -87,43 +15,23 @@
 .edit-editable:focus {
   outline: none;
 }
-.edit-field.edit-editable,
-.edit-field .edit-editable {
-  box-shadow: 0 0 1px 1px #4d9de9;
-}
 
-/* Highlighted (hovered) editable. */
+/**
+ * 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-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-validation-errors > .messages {
+  margin-left: 0;
+  margin-right: 0;
 }
-.edit-form.edit-editable .form-item .error {
-  border: 1px solid #eea0a0;
-}
-
-
-/* Editing (focused) editable. */
-.edit-form.edit-editable.edit-editing,
-.edit-field .edit-editable.edit-editing {
-  /* In the latest design, there's no special styling when editing as opposed to
-   * just hovering.
-   * This will be necessary again for http://drupal.org/node/1844220.
-   */
+.edit-validation-errors > .messages > ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
 }
 
-
-
-
 /**
  * Edit mode: modal.
  */
@@ -131,30 +39,9 @@
   z-index: 350;
   position: fixed;
   top: 40%;
-  left: 40%;
-  box-shadow: 3px 3px 5px #333;
-  background-color: white;
-  border: 1px solid #0199ff;
-  font-family: 'Droid sans', 'Lucida Grande', sans-serif;
-}
-
-#edit_modal .main {
-  font-size: 130%;
-  margin: 25px;
-  padding-left: 40px;
-  background: transparent url('../images/attention.png') no-repeat;
-}
-
-#edit_modal .actions {
-  border-top: 1px solid #ddd;
-  padding: 3px inherit;
-  text-align: right;
-  background: #f5f5f5;
+  left: 40%; /* LTR */
 }
 
-
-
-
 /**
  * Edit mode: type=direct.
  */
@@ -162,59 +49,51 @@
   z-index: 300;
   position: relative;
 }
-
 .edit-validation-errors .messages.error {
   position: absolute;
   top: 6px;
-  left: -5px;
+  left: -5px; /* LTR */
   margin: 0;
   border: none;
-  box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
-  background-color: white;
 }
 
-
-
-
 /**
  * Edit mode: type=form.
  */
 #edit_backstage {
   display: none;
 }
-
 .edit-form {
   position: absolute;
   z-index: 300;
-  box-shadow: 0 0 30px 4px #4f4f4f;
   max-width: 35em;
-  background-color: white;
 }
-
 .edit-form .placeholder {
   min-height: 22px;
 }
 
-/* Default form styling overrides. */
-.edit-form form { padding: 1em; }
-.edit-form .form-item { margin: 0; }
-.edit-form .form-wrapper { margin: .5em; }
-.edit-form .form-wrapper .form-wrapper { margin: inherit; }
-.edit-form .form-actions { display: none; }
-.edit-form input { max-width: 100%; }
-
-
-
-
 /**
- * Edit mode: toolbars
+ * Default form styling overrides.
  */
+.edit-form .form-wrapper .form-wrapper {
+  margin: inherit;
+}
+.edit-form .form-actions {
+  display: none;
+}
+.edit-form input {
+  max-width: 100%;
+}
 
-/* 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 {
+  max-width: 100%;
+  position: absolute;
+  width: 40em;
+  z-index: 350;
+}
 .edit-form-container {
   position: relative;
   padding: 0;
@@ -223,150 +102,9 @@
   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-highlighted .edit-toolbar-heightfaker {
-  display: block;
-}
-
-/* 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;
-}
-.edit-toolgroup.info.loading {
-  padding-right: 35px;
-  background-position: 90% 50%;
-}
-
-/* Operations toolgroup. */
 .edit-toolgroup.ops {
   float: right; /* LTR */
-  margin-left: 5px;
-}
-
-.edit-toolgroup.wysiwyg-floated {
-  float: right;
-}
-.edit-toolgroup.wysiwyg-main {
-  clear: left;
-  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;
-  min-width: 29px;
-  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;
 }
-#edit_modal button {
-  float: none;
-  display: inline-block;
-}
-
-/* Button with icons. */
-#edit_modal button span,
-.edit-toolbar button span {
-  width: 22px;
-  height: 19px;
-  display: block;
-  float: left;
-}
-.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 {
-  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_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_modal button.gray-button:hover,
-.edit-toolbar button.gray-button:hover,
-#edit_modal button.gray-button:active,
-.edit-toolbar button.gray-button:active {
-  border: 1px solid #cdcdcd;
-  box-shadow: 0 2px 1px rgba(0,0,0,0.1);
+.edit-toolbar-label {
+  overflow: hidden;
 }
diff --git a/core/modules/edit/css/edit.theme-rtl.css b/core/modules/edit/css/edit.theme-rtl.css
new file mode 100644
index 0000000..c26378e
--- /dev/null
+++ b/core/modules/edit/css/edit.theme-rtl.css
@@ -0,0 +1,3 @@
+/**
+ * @file edit.theme-rtl.css
+ */
diff --git a/core/modules/edit/css/edit.theme.css b/core/modules/edit/css/edit.theme.css
new file mode 100644
index 0000000..d286925
--- /dev/null
+++ b/core/modules/edit/css/edit.theme.css
@@ -0,0 +1,224 @@
+/**
+ * @file edit.theme.css
+ */
+
+/**
+ * Editable.
+ */
+.edit-field.edit-editable,
+.edit-field .edit-editable {
+  box-shadow: 0 0 1px 2px #4d9de9;
+}
+
+/**
+ * Highlighted (hovered) editable.
+ */
+.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-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-editing.edit-validation-error,
+.edit-form.edit-validation-error {
+  box-shadow: 0 0 1px 2px red, 0 0 3px 5px rgba(153, 153, 153, .5);
+}
+.edit-form .form-item .error {
+  border: 1px solid #eea0a0;
+}
+
+/**
+ * Editing (focused) editable.
+ */
+.edit-form.edit-editable.edit-editing,
+.edit-field .edit-editable.edit-editing {
+  /* In the latest design, there's no special styling when editing as opposed to
+   * just hovering.
+   * This will be necessary again for http://drupal.org/node/1844220.
+   */
+}
+
+/**
+ * Default form styling overrides.
+ */
+.edit-form form {
+  padding: 1em;
+}
+.edit-form .form-item {
+  margin: 0;
+}
+.edit-form .form-wrapper {
+  margin: .5em;
+}
+
+/**
+ * Animations.
+ */
+.edit-animate-invisible {
+  opacity: 0;
+}
+.edit-animate-default {
+  -webkit-transition: all .4s ease;
+  transition: all .4s ease;
+}
+.edit-animate-slow {
+  -webkit-transition: all .6s ease;
+  transition: all .6s ease;
+}
+.edit-animate-delay-veryfast {
+  -webkit-transition-delay: .05s;
+  transition-delay: .05s;
+}
+.edit-animate-delay-fast {
+  -webkit-transition-delay: .2s;
+  transition-delay: .2s;
+}
+.edit-animate-disable-width {
+  -webkit-transition: width 0s;
+  transition: width 0s;
+}
+.edit-animate-only-visibility {
+  -webkit-transition: opacity .2s ease;
+  transition: opacity .2s ease;
+}
+
+/**
+ * Edit mode: modal.
+ */
+#edit_modal {
+  background-color: white;
+  border: 1px solid #0199ff;
+  box-shadow: 3px 3px 5px #333;
+  font-family: 'Droid sans', 'Lucida Grande', sans-serif;
+}
+#edit_modal .main {
+  font-size: 130%;
+  margin: 25px;
+  padding-left: 40px; /* LTR */
+  background: transparent url('../images/attention.png') no-repeat;
+}
+#edit_modal .actions {
+  border-top: 1px solid #ddd;
+  padding: 0.25em 0.2em;
+  text-align: right;
+  background: #f5f5f5;
+}
+
+/**
+ * Edit mode: type=direct.
+ */
+.edit-validation-errors .messages.error {
+  box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
+  background-color: white;
+}
+
+/**
+ * Edit mode: type=form.
+ */
+.edit-form {
+  box-shadow: 0 0 30px 4px #4f4f4f;
+  background-color: white;
+}
+
+/**
+ * Toolbars.
+ */
+.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);
+  -webkit-transition: all 0.5s;
+  transition: all 0.5s;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
+.edit-toolbar-label {
+  overflow: hidden;
+  padding: 0.333em 0.5em;
+}
+/* The toolbar; these are not necessarily visible. */
+.edit-toolbar {
+  font-family: 'Droid sans', 'Lucida Grande', sans-serif;
+}
+.edit-toolbar-entity {
+  padding: 0.1667em 0.2em;
+}
+
+
+ /**
+  * Info toolgroup.
+  */
+.edit-toolgroup.info {
+  font-weight: bolder;
+  padding: 0 5px;
+  background: #fff url('../images/throbber.gif') no-repeat -60px 60px;
+}
+.edit-toolgroup.info.loading {
+  padding-right: 35px; /* LTR */
+  background-position: 90% 50%;
+}
+.edit-toolbar-fullwidth {
+  width: 100%;
+}
+.edit-toolgroup.wysiwyg-floated {
+  float: right; /* LTR */
+}
+.edit-toolgroup.wysiwyg-main {
+  clear: both;
+  width: 100%;
+  padding-left: 0; /* LTR */
+}
+
+/**
+ * Buttons.
+ */
+.edit-button {
+  background-color: #ddd;
+  border: 1px solid transparent;
+  color: #666;
+  cursor: pointer;
+  display: inline-block;
+  margin: 0;
+  opacity: 1;
+  padding: 0.4545em;
+  -webkit-transition: all .1s ease;
+  transition: all .1s ease;
+}
+.edit-button[aria-hidden="true"] {
+  visibility: hidden;
+  opacity: 0;
+}
+.edit-button + .edit-button {
+  margin-left: 0.25em; /* LTR */
+}
+/* Button with icons. */
+#edit_modal .action-cancel,
+.edit-toolbar .action-cancel  {
+}
+.edit-button.action-save {
+  color: white;
+  background-color: #4e97c0;
+}
+.edit-button:hover,
+.edit-button:active {
+  background-color: #e8e8e8;
+  border: 1px solid #cdcdcd;
+}
+.edit-button.action-save:hover,
+.edit-button.action-save:active {
+  background-color: #6fc2f2;
+  border: 1px solid #55a5d3;
+}
+.edit-button.action-saving,
+.edit-button.action-saving:hover,
+.edit-button.action-saving:active {
+  background-color: #dddddd;
+  border-color: #ccc;
+}
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index 7bdece0..9fb0718 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -88,6 +88,8 @@ function edit_library_info() {
       // Views.
       $path . '/js/views/AppView.js' => $options,
       $path . '/js/views/EditorDecorationView.js' => $options,
+      $path . '/js/views/EntityView.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,
@@ -98,14 +100,18 @@ function edit_library_info() {
     ),
     'css' => array(
       $path . '/css/edit.module.css' => array(),
+      $path . '/css/edit.theme.css' => array(),
+      $path . '/css/edit.icons.css' => array(),
     ),
     'dependencies' => array(
       array('system', 'jquery'),
       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/images/close.png b/core/modules/edit/images/close.png
deleted file mode 100644
index e3f98b8..0000000
--- a/core/modules/edit/images/close.png
+++ /dev/null
@@ -1,4 +0,0 @@
-PNG
-
-   IHDR         (-S   `PLTE   >+   tRNS```00 mi   IDATx^=	  H4ͼ!KsfQGx"LCyל׽(ux;zKA.Jo
-E	wy/2cdD@ҔL؅O%8F ?Q    IENDB`
\ No newline at end of file
diff --git a/core/modules/edit/images/icon-close.png b/core/modules/edit/images/icon-close.png
new file mode 100644
index 0000000..3506e56
--- /dev/null
+++ b/core/modules/edit/images/icon-close.png
@@ -0,0 +1,3 @@
+PNG
+
+   IHDR         Vu\   tEXtSoftware Adobe ImageReadyqe<   wIDATxڔ DU=A#FALʇ1s-|ED1\ҹZlx03\&SJ;FA&iX]Mpx(tm.1=x'IW k nB$    IENDB`
\ No newline at end of file
diff --git a/core/modules/edit/images/icon-throbber.gif b/core/modules/edit/images/icon-throbber.gif
new file mode 100644
index 0000000..f2603e8
--- /dev/null
+++ b/core/modules/edit/images/icon-throbber.gif
@@ -0,0 +1,6 @@
+GIF89a   Ž{{{   !NETSCAPE2.0   !	  ,      @`)KkŏA|ad0L9~\8L Ǹ0, i{qBC~H'JRĨ`f4&a !	  ,      `sɺ(t34M!-0,l#))9	!q(i<hB  3˥ (`,9%cm
+b0AY_e !	  ,      dRj:ړtG8$c02P	hi, ǑQ0X[LEck5`ڭP2F!	a	@q dfz{lQ zK !	  ,      `BjR:$BPFq(ˢ	J@0i-2͇ Qck6[G`m:pQ4fqow !	  ,      d1j}MS@S\ H9 #IrPØi8f..r$ł|nl
+*T\![͂l,Q0@(KFMO{ql{ !	  ,      ^I	3a!P(Ol~`8 LÇ!@$Lgx|f,`"`Jŉ"cUP
+GAw<	tz !	  ,       h9C 4kk\ &I&l)F-P@chH!ш4<
+ x@R`0C"8h0BfgX(rc}Y
+1 !  ,     aйҚX]mKl[)"aĢ\*"$t&#9@1pP`,`I,8 S	8U,Q`(d`3-qH$1T{ ;
\ No newline at end of file
diff --git a/core/modules/edit/images/throbber.gif b/core/modules/edit/images/throbber.gif
deleted file mode 100644
index f2603e8..0000000
--- a/core/modules/edit/images/throbber.gif
+++ /dev/null
@@ -1,6 +0,0 @@
-GIF89a   Ž{{{   !NETSCAPE2.0   !	  ,      @`)KkŏA|ad0L9~\8L Ǹ0, i{qBC~H'JRĨ`f4&a !	  ,      `sɺ(t34M!-0,l#))9	!q(i<hB  3˥ (`,9%cm
-b0AY_e !	  ,      dRj:ړtG8$c02P	hi, ǑQ0X[LEck5`ڭP2F!	a	@q dfz{lQ zK !	  ,      `BjR:$BPFq(ˢ	J@0i-2͇ Qck6[G`m:pQ4fqow !	  ,      d1j}MS@S\ H9 #IrPØi8f..r$ł|nl
-*T\![͂l,Q0@(KFMO{ql{ !	  ,      ^I	3a!P(Ol~`8 LÇ!@$Lgx|f,`"`Jŉ"cUP
-GAw<	tz !	  ,       h9C 4kk\ &I&l)F-P@chH!ш4<
- x@R`0C"8h0BfgX(rc}Y
-1 !  ,     aйҚX]mKl[)"aĢ\*"$t&#9@1pP`,`I,8 S	8U,Q`(d`3-qH$1T{ ;
\ No newline at end of file
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index 102fef7..a6b344f 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);
 
@@ -31,6 +30,7 @@ var options = $.extend({
  * Tracks fields without metadata. Contains objects with the following keys:
  *   - DOM el
  *   - String fieldID
+ *   - String entityID
  */
 var fieldsMetadataQueue = [];
 
@@ -57,6 +57,10 @@ 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 rerendered after editing, it will be processed
+    // immediately. New fields will be unable to be processed immediately, but
+    // will instead be 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 +171,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.
@@ -180,7 +189,6 @@ function processField (fieldElement) {
 
   // If an EntityModel for this field already exists (and hence also a "Quick
   // edit" contextual link), then initialize it immediately.
-  var entityID = extractEntityID(fieldID);
   if (Drupal.edit.collections.entities.where({ id: entityID }).length > 0) {
     initializeField(fieldElement, fieldID);
   }
@@ -232,12 +240,18 @@ function fetchMissingMetadata (callback) {
   if (fieldsMetadataQueue.length) {
     var fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
     var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
+    var entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
+    // Ensure we only request entityIDs for which we don't have metadata yet.
+    entityIDs = _.difference(entityIDs, _.keys(Drupal.edit.metadata.data));
     fieldsMetadataQueue = [];
 
     $.ajax({
       url: Drupal.url('edit/metadata'),
       type: 'POST',
-      data: { 'fields[]' : fieldIDs },
+      data: {
+        'fields[]': fieldIDs,
+        'entities[]': entityIDs
+      },
       dataType: 'json',
       success: function(results) {
         // Store the metadata.
@@ -281,7 +295,7 @@ function loadMissingEditors (callback) {
   // Create a temporary element to be able to use Drupal.ajax.
   var $el = $('<div id="' + id + '" class="element-hidden"></div>').appendTo('body');
   // Create a Drupal.ajax instance to load the form.
-  Drupal.ajax[id] = new Drupal.ajax(id, $el, {
+  var loadEditorsAjax = new Drupal.ajax(id, $el, {
     url: Drupal.url('edit/attachments'),
     event: 'edit-internal.edit',
     submit: { 'editors[]': missingEditors },
@@ -291,7 +305,8 @@ function loadMissingEditors (callback) {
   // Implement a scoped insert AJAX command: calls the callback after all AJAX
   // command functions have been executed (hence the deferred calling).
   var realInsert = Drupal.ajax.prototype.commands.insert;
-  Drupal.ajax[id].commands.insert = function (ajax, response, status) {
+  loadEditorsAjax.commands = {};
+  loadEditorsAjax.commands.insert = function (ajax, response, status) {
     _.defer(function() { callback(); });
     realInsert(ajax, response, status);
   };
@@ -344,14 +359,21 @@ function initializeEntityContextualLink (contextualLink) {
     return false;
   }
   // The entity for the given contextual link contains at least one field that
-  // the current user may edit in-place; instantiate EntityModel and
+  // the current user may edit in-place; instantiate EntityModel, EntityView and
   // ContextualLinkView.
   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);
+    // Create an EntityView associated with the root DOM node of the entity.
+    var entityView = new Drupal.edit.EntityView({
+      el: contextualLink.region,
+      model: entityModel
+    });
+    entityModel.set('entityView', entityView);
 
     // Initialize all queued fields within this entity (creates FieldModels).
     _.each(fields, function (field) {
@@ -408,7 +430,9 @@ function deleteContainedModelsAndQueues($context) {
       var contextualLinkView = entityModels[0].get('contextualLinkView');
       contextualLinkView.undelegateEvents();
       contextualLinkView.remove();
-
+      // Remove the EntityView.
+      entityModels[0].get('entityView').remove();
+      // Destroy the EntityModel; this will also destroy its FieldModels.
       entityModels[0].destroy();
     }
 
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..6235579 100644
--- a/core/modules/edit/js/editors/formEditor.js
+++ b/core/modules/edit/js/editors/formEditor.js
@@ -11,6 +11,9 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
   // Tracks the form container DOM element that is used while in-place editing.
   $formContainer: null,
 
+  // Holds the Drupal.ajax object
+  formSaveAjax: null,
+
   /**
    * {@inheritdoc}
    */
@@ -82,8 +85,16 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
     var formOptions = {
       fieldID: fieldModel.id,
       $el: this.$el,
-      nocssjs: false
+      nocssjs: false,
+      // Reset an existing entry for this entity in the TempStore (if any) when
+      // saving the field. Logically speaking, this should happen in a separate
+      // request because this is an entity-level operation, not a field-level
+      // operation. But that would require an additional request, that might not
+      // even be necessary: it is only when a user saves a first changed field
+      // for an entity that this needs to happen: precisely now!
+      reset: !fieldModel.get('entity').get('inTempStore')
     };
+    var self = this;
     Drupal.edit.util.form.load(formOptions, function (form, ajax) {
       Drupal.ajax.prototype.commands.insert(ajax, {
         data: form,
@@ -91,7 +102,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       });
 
       var $submit = $formContainer.find('.edit-form-submit');
-      Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
+      self.formSaveAjax = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
       $formContainer
         .on('formUpdated.edit', ':input', function () {
           fieldModel.set('state', 'changed');
@@ -115,7 +126,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
       return;
     }
 
-    Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit'));
+    delete this.formSaveAjax;
     // Allow form widgets to detach properly.
     Drupal.detachBehaviors(this.$formContainer, null, 'unload');
     this.$formContainer
@@ -131,23 +142,25 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
   save: function () {
     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 self = this;
+
+    this.formSaveAjax.commands = {};
 
     // Successfully saved.
-    Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) {
-      Drupal.edit.util.form.unajaxifySaving($(ajax.element));
+    this.formSaveAjax.commands.editFieldFormSaved = function (ajax, response, status) {
+      delete self.formSaveAjax;
 
       // First, transition the state to 'saved'.
       fieldModel.set('state', 'saved');
       // Then, set the 'html' attribute on the field model. This will cause the
       // field to be rerendered.
       fieldModel.set('html', response.data);
-     };
+    };
 
     // Unsuccessfully saved; validation errors.
-    Drupal.ajax[base].commands.editFieldFormValidationErrors = function (ajax, response, status) {
+    this.formSaveAjax.commands.editFieldFormValidationErrors = function (ajax, response, status) {
       editorModel.set('validationErrors', response.data);
       fieldModel.set('state', 'invalid');
     };
@@ -157,8 +170,8 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
     // API then marks which form items have errors. Therefor, we have to replace
     // the existing form, unbind the existing Drupal.ajax instance and create a
     // new Drupal.ajax instance.
-    Drupal.ajax[base].commands.editFieldForm = function (ajax, response, status) {
-      Drupal.edit.util.form.unajaxifySaving($(ajax.element));
+    this.formSaveAjax.commands.editFieldForm = function (ajax, response, status) {
+      delete self.formSaveAjax;
 
       Drupal.ajax.prototype.commands.insert(ajax, {
         data: response.data,
@@ -167,7 +180,7 @@ Drupal.edit.editors.form = Drupal.edit.EditorView.extend({
 
       // Create a Drupal.ajax instance for the re-rendered ("new") form.
       var $newSubmit = $formContainer.find('.edit-form-submit');
-      Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit);
+      self.formSaveAjax = Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit);
     };
 
     // Click the form's submit button; the scoped AJAX commands above will
diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js
index e484b35..74cd6a8 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,24 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
 
     // Indicates whether this instance of this entity is currently being
     // edited in-place.
-    isActive: false
+    isActive: false,
+    // Whether one or more fields have already been stored in TempStore.
+    inTempStore: false,
+    // Whether one or more fields have already been stored in TempStore *or*
+    // the field that's currently being edited is in the 'changed' or a later
+    // state. In other words, this boolean indicates whether a "Save" button is
+    // necessary or not.
+    isDirty: false,
+    // Whether the request to the server has been made to commit this entity.
+    // Used to prevent multiple such requests.
+    isCommitting: false,
+    // The current processing state of an entity.
+    state: 'closed',
+    // The IDs of the fields whose new values have been stored in TempStore. We
+    // must store this on the EntityModel as well (even though it already is on
+    // the FieldModel) because when a field is rerendered, its FieldModel is
+    // destroyed and this allows us to transition it back to the proper state.
+    fieldsInTempStore: []
   },
 
   /**
@@ -30,6 +49,373 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
    */
   initialize: function () {
     this.set('fields', new Drupal.edit.FieldCollection());
+
+    // Respond to entity state changes.
+    this.on('change:state', this.stateChange, this);
+
+    // The state of the entity is largely dependent on the state of its
+    // fields.
+    this.get('fields').on('change:state', this.fieldStateChange, this);
+  },
+
+  /**
+   * Updates FieldModels' states when an EntityModel change occurs.
+   *
+   * @param Drupal.edit.EntityModel entityModel
+   * @param String state
+   *   The state of the associated entity. One of Drupal.edit.EntityModel.states.
+   * @param Object options
+   */
+  stateChange: function (entityModel, state, options) {
+    var to = state;
+    switch (to) {
+      case 'closed':
+        this.set('isActive', false);
+        this.set('inTempStore', false);
+        this.set('isDirty', false);
+        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':
+        // The entity is now ready for editing!
+        this.set('isActive', true);
+        break;
+
+      case 'committing':
+        // The user indicated he wants to save the entity, but to do that, all
+        // fields for the entity must first be stored in TempStore.
+        this.get('fields').chain()
+          .filter(function (fieldModel) {
+            return _.intersection([fieldModel.get('state')], Drupal.edit.app.changedEditorStates).length;
+          })
+          .each(function (fieldModel) {
+            fieldModel.set('state', 'saving');
+          });
+        break;
+
+      case 'deactivating':
+        // Set all fields to the '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':
+        // Set all fields to the 'inactive' state.
+        options.reason = 'stop';
+        this.get('fields').each(function (fieldModel) {
+          fieldModel.set({
+            'inTempStore': false,
+            'state': 'inactive'
+          }, options);
+        });
+        break;
+    }
+  },
+
+  /**
+   * Reacts to state changes in this entity's fields.
+   *
+   * @param Drupal.edit.FieldModel fieldModel
+   *   The model of the field whose state property changed.
+   * @param String state
+   *   The state of the associated field. One of Drupal.edit.FieldModel.states.
+   */
+  fieldStateChange: function (fieldModel, state) {
+    var entityModel = this;
+    var fieldState = state;
+    // Switch on the entityModel state.
+    // The EntityModel responds to FieldModel state changes as a function of its
+    // state. For example, a field switching back to 'candidate' state when its
+    // entity is in the 'opened' state has no effect on the entity. But that
+    // same switch back to 'candidate' state of a field when the entity is in
+    // the 'committing' state might allow the entity to proceed with the commit
+    // flow.
+    switch (this.get('state')) {
+      case 'closed':
+      case 'launching':
+        // It should be impossible to reach these: fields can't change state
+        // while the entity is closed or still launching.
+        break;
+
+      case 'opening':
+        // We must change the entity to the 'opened' state, but it must first be
+        // confirmed that all of its fieldModels have transitioned to the
+        // 'candidate' state.
+        // We do this here, because this is called every time a fieldModel
+        // changes state, hence each time this is called, we get closer to the
+        // goal of having all fieldModels in the 'candidate' state.
+        // A state change in reaction to another state change must be deferred.
+        _.defer(function() {
+          entityModel.set('state', 'opened', {
+            'accept-field-states': Drupal.edit.app.fieldReadyStates
+          });
+        });
+        break;
+
+      case 'opened':
+        var fieldsInTempStore = this.get('fieldsInTempStore');
+
+        // Set the isDirty attribute when appropriate so that it is known when
+        // to display the "Save" button in the entity toolbar.
+        // Note that once a field has been changed, there's no way to discard
+        // that change, hence it will have to be saved into TempStore, or the
+        // in-place editing of this field will have to be stopped completely.
+        // In other words: once any field enters the 'changed' field, then for
+        // the remainder of the in-place editing session, the entity is by
+        // definition dirty.
+        if (fieldState === 'changed') {
+          entityModel.set('isDirty', true);
+        }
+        // If the fieldModel changed to the 'saved' state: remember that this
+        // field was saved to TempStore.
+        else if (fieldState === 'saved') {
+          // Mark the entity as saved in TempStore, so that we can pass the
+          // proper "reset TempStore" boolean value when communicating with the
+          // server.
+          entityModel.set('inTempStore', true);
+          // Mark the field as saved in TempStore, so that visual indicators
+          // signifying just that may be rendered.
+          fieldModel.set('inTempStore', true);
+          // Remember that this field is in TempStore, restore when rerendered.
+          fieldsInTempStore.push(fieldModel.id);
+          fieldsInTempStore = _.uniq(fieldsInTempStore);
+          entityModel.set('fieldsInTempStore', fieldsInTempStore);
+        }
+        // If the fieldModel changed to the 'candidate' state from the
+        // 'inactive' state, then this is a field for this entity that got
+        // rerendered. Restore its previous 'inTempStore' attribute value.
+        // @todo consider moving this to AppView.rerenderedFieldToCandidate() when all this logic is moved out of EntityModel.
+        else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') {
+          fieldModel.set('inTempStore', _.intersection([fieldModel.id], fieldsInTempStore).length > 0);
+        }
+        break;
+
+      case 'committing':
+        // If the field save returned a validation error, set the state of the
+        // entity back to opened.
+        if (fieldState === 'invalid') {
+          // A state change in reaction to another state change must be deferred.
+          _.defer(function() {
+            entityModel.set('state', 'opened', { reason: 'invalid' });
+          });
+        }
+        // If the fieldModel changed to the 'candidate' state from the
+        // 'inactive' state, then this is a field for this entity that got
+        // rerendered. Restore its previous 'inTempStore' attribute value.
+        // @todo consider moving this to AppView.rerenderedFieldToCandidate() when all this logic is moved out of EntityModel.
+        else if (fieldState === 'candidate' && fieldModel.previous('state') === 'inactive') {
+          fieldModel.set('inTempStore', _.intersection([fieldModel.id], fieldsInTempStore).length > 0);
+        }
+
+        // Attempt to save the entity. If the entity's fields are not yet all in
+        // a ready state, the save will not be processed.
+        var options = {
+          'accept-field-states': Drupal.edit.app.fieldReadyStates
+        };
+        if (entityModel.set('isCommitting', true, options)) {
+          entityModel.save({
+            success: function () {
+              entityModel.set({
+                'state': 'deactivating',
+                'isCommitting' : false
+              });
+            }
+          });
+        }
+        break;
+
+      case 'deactivating':
+        // When setting the entity to 'closing', require that all fieldModels
+        // are in either the 'candidate' or 'highlighted' state.
+        // A state change in reaction to another state change must be deferred.
+        _.defer(function() {
+          entityModel.set('state', 'closing', {
+            'accept-field-states': Drupal.edit.app.fieldReadyStates
+          });
+        });
+        break;
+
+      case 'closing':
+        // When setting the entity to 'closed', require that all fieldModels are
+        // in the 'inactive' state.
+        // A state change in reaction to another state change must be deferred.
+        _.defer(function() {
+          entityModel.set('state', 'closed', {
+            'accept-field-states': ['inactive']
+          });
+        });
+        break;
+    }
+  },
+
+  /**
+   * Fires an AJAX request to the REST save URL for an entity.
+   *
+   * @param options
+   *   An object of options that contains:
+   *     - success: (optional) A function to invoke if the entity is success-
+   *     fully saved.
+   */
+  save: function (options) {
+    var entityModel = this;
+
+    // @todo Simplify this once https://drupal.org/node/1533366 lands.
+    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 save the entity.
+    var entitySaverAjax = new Drupal.ajax(id, $el, {
+      url: Drupal.url('edit/entity/' + entityModel.id),
+      event: 'edit-save.edit',
+      progress: { type: 'none' }
+    });
+    // Entity saved successfully.
+    entitySaverAjax.commands = {};
+    entitySaverAjax.commands.editEntitySaved = function(ajax, response, status) {
+      // All fields have been moved from TempStore to permanent storage, update
+      // the "inTempStore" attribute on FieldModels, on the EntityModel and
+      // clear EntityModel's "fieldInTempStore" attribute.
+      entityModel.get('fields').each(function (fieldModel) {
+        fieldModel.set('inTempStore', false);
+      });
+      entityModel.set('inTempStore', false);
+      entityModel.set('fieldsInTempStore', []);
+
+      // Clean up.
+      $(ajax.element).unbind('edit-save.edit');
+      // Invoke the optional success callback.
+      if (options.success) {
+        options.success.call(entityModel);
+      }
+    };
+    // Trigger the AJAX request, which will will return the editEntitySaved AJAX
+    // command to which we then react.
+    $el.trigger('edit-save.edit');
+  },
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param Object attrs
+   *   The attributes changes in the save or set call.
+   * @param Object options
+   *   An object with the following option:
+   *     - String reason (optional): a string that conveys a particular reason
+   *       to allow for an exceptional state change.
+   *     - Array accept-field-states (optional) An array of strings that
+   *     represent field states that the entities must be in to validate. For
+   *     example, if accept-field-states is ['candidate', 'highlighted'], then
+   *     all the fields of the entity must be in either of these two states
+   *     for the save or set call to validate and proceed.
+   */
+  validate: function (attrs, options) {
+    var acceptedFieldStates = options['accept-field-states'] || [];
+
+    // Validate state change.
+    var currentState = this.get('state');
+    var nextState = attrs.state;
+    if (currentState !== nextState) {
+      // Ensure it's a valid state.
+      if (_.indexOf(this.constructor.states, nextState) === -1) {
+        return '"' + nextState + '" is an invalid state';
+      }
+
+      // Ensure it's a state change that is allowed.
+      // Check if the acceptStateChange function accepts it.
+      if (!this._acceptStateChange(currentState, nextState, options)) {
+        return 'state change not accepted';
+      }
+      // If that function accepts it, then ensure all fields are also in an
+      // acceptable state.
+      else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
+        return 'state change not accepted because fields are not in acceptable state';
+      }
+    }
+
+    // Validate setting isCommitting = true.
+    var currentIsCommitting = this.get('isCommitting');
+    var nextIsCommitting = attrs.isCommitting;
+    if (currentIsCommitting === false && nextIsCommitting === true) {
+      if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
+        return 'isCommitting change not accepted because fields are not in acceptable state';
+      }
+    }
+    else if (currentIsCommitting === true && nextIsCommitting === true) {
+      return "isCommiting is a mutex, hence only changes are allowed";
+    }
+  },
+
+  // Like @see AppView.acceptEditorStateChange()
+  // @todo move this into AppView?
+  _acceptStateChange: function (from, to, context) {
+    var accept = true;
+
+    // In general, enforce the states sequence. Disallow going back from a
+    // "later" state to an "earlier" state, except in explicitly allowed
+    // cases.
+    if (!this.constructor.followsStateSequence(from, to)) {
+      accept = false;
+
+      // Allow: closing -> closed.
+      // Necessary to stop editing an entity.
+      if (from === 'closing' && to === 'closed') {
+        accept = true;
+      }
+      // Allow: committing -> opened.
+      // Necessary to be able to correct an invalid field.
+      else if (from === 'committing' && to === 'opened' && context.reason && context.reason === 'invalid') {
+        accept = true;
+      }
+      // Allow: opened -> deactivating.
+      // Necessary to be able to stop editing.
+      else if (from === 'opened' && to === 'deactivating' && context.confirmed) {
+        accept = true;
+      }
+    }
+
+    return accept;
+  },
+
+  /**
+   * @param Array acceptedFieldStates
+   *   @see validate()
+   * @return Boolean
+   */
+  _fieldsHaveAcceptableStates: function (acceptedFieldStates) {
+    var accept = true;
+
+    // If no acceptable field states are provided, assume all field states are
+    // acceptable. We want to let validation pass as a default and only
+    // check validity on calls to set that explicitly request it.
+    if (acceptedFieldStates.length > 0) {
+      var fieldStates = this.get('fields').pluck('state') || [];
+      // If not all fields are in one of the accepted field states, then we
+      // still can't allow this state change.
+      if (_.difference(fieldStates, acceptedFieldStates).length) {
+        accept = false;
+      }
+    }
+
+    return accept;
   },
 
   /**
@@ -52,10 +438,81 @@ Drupal.edit.EntityModel = Backbone.Model.extend({
     return;
   }
 
+}, {
+
+  /**
+   * A list (sequence) of all possible states an entity can be in during
+   * in-place editing.
+   */
+  states: [
+    // Initial state, like field's 'inactive' OR the user has just finished
+    // in-place editing this entity.
+    // - Trigger: none (initial) or EntityModel (finished).
+    // - Expected behavior: (when not initial state): tear down
+    //   EntityToolbarView, in-place editors and related views.
+    'closed',
+    // User has activated in-place editing of this entity.
+    // - Trigger: user.
+    // - Expected behavior: the EntityToolbarView is gets set up, in-place
+    //   editors (EditorViews) and related views for this entity's fields are
+    //   set up. Upon completion of those, the state is changed to 'opening'.
+    'launching',
+    // Launching has finished.
+    // - Trigger: application.
+    // - Guarantees: in-place editors ready for use, all entity and field views
+    //   have been set up, all fields are in the 'inactive' state.
+    // - Expected behavior: all fields are changed to the 'candidate' state and
+    //   once this is completed, the entity state will be changed to 'opened'.
+    'opening',
+    // Opening has finished.
+    // - Trigger: EntityModel.
+    // - Guarantees: see 'opening', all fields are in the 'candidate' state.
+    // - Expected behavior: the user is able to actually use in-place editing.
+    'opened',
+    // User has clicked the 'Save' button (and has thus changed at least one
+    // field).
+    // - Trigger: user.
+    // - Guarantees: see 'opened', plus: either a changed field is in TempStore,
+    //   or the user has just modified a field without activating (switching to)
+    //   another field.
+    // - Expected behavior: 1) if any of the fields are not yet in TempStore,
+    //   save them to TempStore, 2) if then any of the fields has the 'invalid'
+    //   state, then change the entity state back to 'opened', otherwise: save
+    //   the entity by committing it from TempStore into permanent storage.
+    'committing',
+    // User has clicked the 'Close' button, or has clicked the 'Save' button and
+    // that was successfully completed.
+    // - Trigger: user or EntityModel.
+    // - Guarantees: when having clicked 'Close' hardly any: fields may be in a
+    //   variety of states; when having clicked 'Save': all fields are in the
+    //   'candidate' state.
+    // - Expected behavior: transition all fields to the 'candidate' state,
+    //   possibly requiring confirmation in the case of having clicked 'Close'.
+    'deactivating',
+    // Deactivation has been completed.
+    // - Trigger: EntityModel.
+    // - Guarantees: all fields are in the 'candidate' state.
+    // - Expected behavior: change all fields to the 'inactive' state.
+    'closing'
+  ],
+
+  /**
+   * Indicates whether the 'from' state comes before the 'to' state.
+   *
+   * @param String from
+   *   One of Drupal.edit.EntityModel.states.
+   * @param String to
+   *   One of Drupal.edit.EntityModel.states.
+   * @return Boolean
+   */
+  followsStateSequence: function (from, to) {
+    return _.indexOf(this.states, from) < _.indexOf(this.states, to);
+  }
+
 });
 
 Drupal.edit.EntityCollection = Backbone.Collection.extend({
   model: Drupal.edit.EntityModel
 });
 
-}(Backbone, Drupal));
+}(_, jQuery, Backbone, Drupal));
diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js
index dc02eab..8b06521 100644
--- a/core/modules/edit/js/models/FieldModel.js
+++ b/core/modules/edit/js/models/FieldModel.js
@@ -31,6 +31,12 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
     // In-place editing state of this field. Defaults to the initial state.
     // Possible values: @see Drupal.edit.FieldModel.states.
     state: 'inactive',
+    // The field is currently in the 'changed' state or one of the following
+    // states in which the field is still changed.
+    isChanged: false,
+    // Is tracked by the EntityModel, is mirrored here solely for decorative
+    // purposes: so that EditorDecorationView.renderChanged() can react to it.
+    inTempStore: false,
     // The full HTML representation of this field (with the element that has
     // the data-edit-id as the outer element). Used to propagate changes from
     // this field instance to other instances of the same field.
@@ -83,7 +89,7 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
         return '"' + next + '" is an invalid state';
       }
       // Check if the acceptStateChange callback accepts it.
-      if (!this.get('acceptStateChange')(current, next, options)) {
+      if (!this.get('acceptStateChange')(current, next, options, this)) {
         return 'state change not accepted';
       }
     }
@@ -106,8 +112,71 @@ Drupal.edit.FieldModel = Backbone.Model.extend({
    * editing.
    */
   states: [
-    'inactive', 'candidate', 'highlighted',
-    'activating', 'active', 'changed', 'saving', 'saved', 'invalid'
+    // The field associated with this FieldModel is linked to an EntityModel;
+    // the user can choose to start in-place editing that entity (and
+    // consequently this field). No in-place editor (EditorView) is associated
+    // with this field, because this field is not being in-place edited.
+    // This is both the initial (not yet in-place editing) and the end state (
+    // finished in-place editing).
+    'inactive',
+    // The user is in-place editing this entity, and this field is a candidate
+    // for in-place editing. In-place editor should not
+    // - Trigger: user.
+    // - Guarantees: entity is ready, in-place editor (EditorView) is associated
+    //   with the field.
+    // - Expected behavior: visual indicators around the field indicate it is
+    //   available for in-place editing, no in-place editor presented yet.
+    'candidate',
+    // User is highlighting this field.
+    // - Trigger: user.
+    // - Guarantees: see 'candidate'.
+    // - Expected behavior: visual indicators to convey highlighting, in-place
+    //   editing toolbar shows field's label.
+    'highlighted',
+    // User has activated the in-place editing of this field; in-place editor is
+    // activating.
+    // - Trigger: user.
+    // - Guarantees: see 'candidate'.
+    // - Expected behavior: loading indicator, in-place editor is loading remote
+    //   data (e.g. retrieve form from back-end). Upon retrieval of remote data,
+    //   the in-place editor transitions the field's state to 'active'.
+    'activating',
+    // In-place editor has finished loading remote data; ready for use.
+    // - Trigger: in-place editor.
+    // - Guarantees: see 'candidate'.
+    // - Expected behavior: in-place editor for the field is ready for use.
+    'active',
+    // User has modified values in the in-place editor.
+    // - Trigger: user.
+    // - Guarantees: see 'candidate', plus in-place editor is ready for use.
+    // - Expected behavior: visual indicator of change.
+    'changed',
+    // User is saving changed field data in in-place editor to TempStore. The
+    // save mechanism of the in-place editor is called.
+    // - Trigger: user.
+    // - Guarantees: see 'candidate' and 'active'.
+    // - Expected behavior: saving indicator, in-place editor is saving field
+    //   data into TempStore. Upon succesful saving (without validation errors),
+    //   the in-place editor transitions the field's state to 'saved', but to
+    //   'invalid' upon failed saving (with validation errors).
+    'saving',
+    // In-place editor has successfully saved the changed field.
+    // - Trigger: in-place editor.
+    // - Guarantees: see 'candidate' and 'active'.
+    // - Expected behavior: transition back to 'candidate' state because the
+    //   deed is done. Then: 1) transition to 'inactive' to allow the field to
+    //   be rerendered, 2) destroy the FieldModel (which also destroys attached
+    //   views like the EditorView), 3) replace the existing field HTML with the
+    //   existing HTML and 4) attach behaviors again so that the field becomes
+    //   available again for in-place editing.
+    'saved',
+    // In-place editor has failed to saved the changed field: there were
+    // validation errors.
+    // - Trigger: in-place editor.
+    // - Guarantees: see 'candidate' and 'active'.
+    // - Expected behavior: remain in 'invalid' state, let the user make more
+    //   changes so that he can save it again, without validation errors.
+    'invalid'
   ],
 
   /**
diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js
index 3e6e250..0700e6f 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 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,11 +118,17 @@ Drupal.theme.editButtons = function (settings) {
     if (!button.hasOwnProperty('type')) {
       button.type = 'button';
     }
-
-    html += '<button type="' + button.type + '" class="' + button.classes + '"';
-    html += (button.action) ? ' data-edit-modal-action="' + button.action + '"' : '';
-    html += '>';
-    html +=    button.label;
+    // 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 + '"'  + ' ' + attributes.join(' ');
+    html += ((button.action) ? ' data-edit-modal-action="' + button.action + '"' : '') + '>';
+    html += button.label;
     html += '</button>';
   }
   return html;
diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js
index ad7136ec..9d63b97 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 data stored
+   *      for this field's entity in TempStore should be used or 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
@@ -56,18 +58,23 @@ Drupal.edit.util.form = {
     var fieldID = options.fieldID;
 
     // Create a Drupal.ajax instance to load the form.
-    Drupal.ajax[fieldID] = new Drupal.ajax(fieldID, $el, {
+    var formLoaderAjax = 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.
-    Drupal.ajax[fieldID].commands.editFieldForm = function (ajax, response, status) {
+    formLoaderAjax.commands = {
+      settings: Drupal.ajax.prototype.commands.settings,
+      insert: Drupal.ajax.prototype.commands.insert,
+      add_css: Drupal.ajax.prototype.commands.add_css
+    };
+    formLoaderAjax.commands.editFieldForm = function (ajax, response, status) {
       callback(response.data, ajax);
-      // Delete the Drupal.ajax instance that called this very function.
-      delete Drupal.ajax[fieldID];
-      $el.off('edit-internal.edit');
     };
     // This will ensure our scoped editFieldForm AJAX command gets called.
     $el.trigger('edit-internal.edit');
@@ -80,8 +87,8 @@ Drupal.edit.util.form = {
    *   An object with the following keys:
    *    - nocssjs: (required) boolean indicating whether no CSS and JS should be
    *      returned (necessary when the form is invisible to the user).
-   * @return String
-   *   The key of the Drupal.ajax instance.
+   * @return Drupal.ajax
+   *   A Drupal.ajax instance.
    */
   ajaxifySaving: function (options, $submit) {
     // Re-wire the form to handle submit.
@@ -94,10 +101,10 @@ Drupal.edit.util.form = {
     };
     var base = $submit.attr('id');
 
-    Drupal.ajax[base] = new Drupal.ajax(base, $submit[0], element_settings);
+    var formSaveAjax = new Drupal.ajax(base, $submit[0], element_settings);
     // Reimplement the success handler to ensure Drupal.attachBehaviors() does
     // not get called on the form.
-    Drupal.ajax[base].success = function (response, status) {
+    formSaveAjax.success = function (response, status) {
       for (var i in response) {
         if (response.hasOwnProperty(i) && response[i].command && this.commands[response[i].command]) {
           this.commands[response[i].command](this, response[i], status);
@@ -105,18 +112,7 @@ Drupal.edit.util.form = {
       }
     };
 
-    return base;
-  },
-
-  /**
-   * Cleans up the Drupal.ajax instance that is used to save the form.
-   *
-   * @param jQuery $submit
-   *   The jQuery-wrapped submit DOM element that should be unajaxified.
-   */
-  unajaxifySaving: function ($submit) {
-    delete Drupal.ajax[$submit.attr('id')];
-    $submit.off('click.edit');
+    return formSaveAjax;
   }
 };
 
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 6302356..4ae153b 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";
 
@@ -7,10 +7,6 @@
  */
 Drupal.edit.AppView = Backbone.View.extend({
 
-  // Configuration for state handling.
-  activeEditorStates: [],
-  singleEditorStates: [],
-
   /**
    * {@inheritdoc}
    *
@@ -25,10 +21,12 @@ Drupal.edit.AppView = Backbone.View.extend({
     // @see Drupal.edit.FieldModel.states
     this.activeEditorStates = ['activating', 'active'];
     this.singleEditorStates = ['highlighted', 'activating', 'active'];
+    this.changedEditorStates = ['changed', 'saving', 'saved', 'invalid'];
+    this.fieldReadyStates = ['candidate', 'highlighted'];
 
     options.entitiesCollection
       // Track app state.
-      .on('change:isActive', this.appStateChange, this)
+      .on('change:state', this.appStateChange, this)
       .on('change:isActive', this.enforceSingleActiveEntity, this);
 
     options.fieldsCollection
@@ -47,30 +45,45 @@ Drupal.edit.AppView = Backbone.View.extend({
    *
    * @param Drupal.edit.EntityModel entityModel
    *   An instance of the EntityModel class.
-   * @param Boolean isActive
-   *   A boolean that represents the changed active state of the entityModel.
+   * @param String state
+   *   The state of the associated field. One of Drupal.edit.EntityModel.states.
    */
-  appStateChange: function (entityModel, isActive) {
+  appStateChange: function (entityModel, state) {
     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);
-      });
+    var entityToolbarView;
+    switch (state) {
+      case 'launching':
+        // First, create an entity toolbar view.
+        entityToolbarView = new Drupal.edit.EntityToolbarView({
+          model: entityModel,
+          appModel: this.model
+        });
+        entityModel.toolbarView = entityToolbarView;
+        // Second, set up in-place editors.
+        // They must be notified of state changes, hence this must happen while
+        // the associated fields are still in the 'inactive' state.
+        entityModel.get('fields').each(function (fieldModel) {
+          app.setupEditor(fieldModel);
+        });
+        // Third, transition the entity to the 'opening' state, which will
+        // transition all fields from 'inactive' to 'candidate'.
+        _.defer(function () {
+          entityModel.set('state', 'opening');
+        });
+        break;
+      case 'closed':
+        entityToolbarView = entityModel.toolbarView;
+        // First, tear down the entity toolbar view.
+        // @todo @jesse Why shouldn't this happen last?
+        if (entityToolbarView) {
+          entityToolbarView.remove();
+          delete entityModel.toolbarView;
+        }
+        // Second, tear down the in-place editors.
+        entityModel.get('fields').each(function (fieldModel) {
+          app.teardownEditor(fieldModel);
+        });
+        break;
     }
   },
 
@@ -85,10 +98,10 @@ Drupal.edit.AppView = Backbone.View.extend({
    *   The new state.
    * @param null|Object context
    *   The context that is trying to trigger the state change.
-   * @param Function callback
-   *   The callback function that should receive the state acceptance result.
+   * @param Drupal.edit.FieldModel fieldModel
+   *   The fieldModel to which this change applies.
    */
-  acceptEditorStateChange: function (from, to, context, callback) {
+  acceptEditorStateChange: function (from, to, context, fieldModel) {
     var accept = true;
 
     // If the app is in view mode, then reject all state changes except for
@@ -106,39 +119,79 @@ Drupal.edit.AppView = Backbone.View.extend({
       if (!Drupal.edit.FieldModel.followsStateSequence(from, to)) {
         accept = false;
         // Allow: activating/active -> candidate.
-        // Necessary to stop editing a property.
+        // Necessary to stop editing a field.
         if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') {
           accept = true;
         }
         // Allow: changed/invalid -> candidate.
-        // Necessary to stop editing a property when it is changed or invalid.
+        // Necessary to stop editing a field when it is changed or invalid.
         else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
           accept = true;
         }
         // Allow: highlighted -> candidate.
-        // Necessary to stop highlighting a property.
+        // Necessary to stop highlighting a field.
         else if (from === 'highlighted' && to === 'candidate') {
           accept = true;
         }
         // Allow: saved -> candidate.
-        // Necessary when successfully saved a property.
+        // Necessary when successfully saved a field.
         else if (from === 'saved' && to === 'candidate') {
           accept = true;
         }
         // Allow: invalid -> saving.
-        // Necessary to be able to save a corrected, invalid property.
+        // Necessary to be able to save a corrected, invalid field.
         else if (from === 'invalid' && to === 'saving') {
           accept = true;
         }
+        // Allow: invalid -> activating.
+        // Necessary to be able to correct a field that turned out to be invalid
+        // after the user already had moved on to the next field (which we
+        // explicitly allow to have a fluent UX).
+        else if (from === 'invalid' && to === 'activating') {
+          accept = true;
+        }
       }
 
       // 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;
+        var activeEditor, activeEditorState;
+        // Ensure only one editor (field) at a time is active … but allow a user
+        // to hop from one field to the next, even if we still have to start
+        // saving the field that is currently active: assume it will be valid,
+        // to allow for a fluent UX. (If it turns out to be invalid, this block
+        // of code also handles that.)
+        if ((this.fieldReadyStates.indexOf(from) !== -1 || from === 'invalid') && this.activeEditorStates.indexOf(to) !== -1) {
+          activeEditor = this.model.get('activeEditor');
+          if (activeEditor && activeEditor !== fieldModel) {
+            activeEditorState = activeEditor.get('state');
+            // Allow the state change. If the state of the active editor is:
+            // - 'activating' or 'active': change it to 'candidate'
+            // - 'changed' or 'invalid': change it to 'saving'
+            // - 'saving'or 'saved': don't do anything.
+            if (this.activeEditorStates.indexOf(activeEditorState) !== -1) {
+              activeEditor.set('state', 'candidate');
+            }
+            else if (activeEditorState === 'changed' || activeEditorState === 'invalid') {
+              activeEditor.set('state', 'saving');
+            }
+
+            // If the field that's being activated is in fact already in the
+            // invalid state (which can only happen because above we allowed the
+            // user to move on to another field to allow for a fluent UX; we
+            // assumed it would be saved successfully), then we shouldn't allow
+            // the field to enter the 'activating' state, instead, we simply
+            // change the active editor. All guarantees and assumptions for this
+            // field still hold!
+            if (from === 'invalid') {
+              this.model.set('activeEditor', fieldModel);
+              accept = false;
+            }
+            else {
+              // Do not reject: the field is either in the 'candidate' or
+              // 'highlighted' state and we allow it to enter the 'activating'
+              // state!
+            }
           }
         }
         // Reject going from activating/active to candidate because of a
@@ -165,7 +218,7 @@ 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();
             }
           }
         }
@@ -184,6 +237,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 entityToolbarView = entityModel.toolbarView;
+    // Get the field toolbar DOM root from the entity toolbar.
+    var fieldToolbarRoot = entityToolbarView.getToolbarRoot();
     // Create in-place editor.
     var editorName = fieldModel.get('metadata').editor;
     var editorModel = new Drupal.edit.EditorModel();
@@ -196,9 +254,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 +269,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
@@ -252,6 +314,7 @@ Drupal.edit.AppView = Backbone.View.extend({
    * @see acceptEditorStateChange()
    */
   _confirmStopEditing: function () {
+    var activeEntity = Drupal.edit.collections.entities.where({ isActive: true })[0];
     // Only instantiate if there isn't a modal instance visible yet.
     if (!this.model.get('activeModal')) {
       var that = this;
@@ -259,15 +322,31 @@ 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) {
           // 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', targetState, { confirmed: true });
+          // If the targetState is saving, the field must be saved, then the
+          // entity must be saved.
+          if (action === 'save') {
+            activeEntity.set('state', 'committing');
+          }
+          else {
+            activeEntity.get('fields').each(function (fieldModel) {
+              fieldModel.set('state', 'candidate', { confirmed : true });
+            });
+          }
         }
       });
       this.model.set('activeModal', modal);
@@ -310,6 +389,36 @@ 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') {
+        // Attempt to save the field.
+        activeEditor.set('state', 'saving');
+      }
+      // 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
@@ -351,7 +460,7 @@ Drupal.edit.AppView = Backbone.View.extend({
    *   A field that was just added to the collection of fields.
    */
   rerenderedFieldToCandidate: function (fieldModel) {
-    var activeEntity = Drupal.edit.collections.entities.where({ isActive: true })[0];
+    var activeEntity = Drupal.edit.collections.entities.where({isActive: true})[0];
 
     // Early-return if there is no active entity.
     if (activeEntity === null) {
@@ -389,4 +498,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..3f8e144 100644
--- a/core/modules/edit/js/views/EditorDecorationView.js
+++ b/core/modules/edit/js/views/EditorDecorationView.js
@@ -1,13 +1,15 @@
 /**
  * @file
  * A Backbone View that decorates the in-place edited element.
+ *
+ * @todo  Rename to FieldDecorationView? This is not decorating the in-place
+ * editor, but the in-place edited element, i.e.: the field.
  */
 (function ($, Backbone, Drupal) {
 
 "use strict";
 
 Drupal.edit.EditorDecorationView = Backbone.View.extend({
-  toolbarId: null,
 
   _widthAttributeIsEmpty: null,
 
@@ -25,15 +27,12 @@ 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);
+    this.model.on('change:isChanged change:inTempStore', this.renderChanged, this);
   },
 
   /**
@@ -65,6 +64,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
         if (from !== 'inactive') {
           this.stopHighlight();
           if (from !== 'highlighted') {
+            this.model.set('isChanged', false);
             this.stopEdit();
           }
         }
@@ -84,6 +84,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
         this.startEdit();
         break;
       case 'changed':
+        this.model.set('isChanged', true);
         break;
       case 'saving':
         break;
@@ -95,16 +96,23 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
   },
 
   /**
+   * Adds a class to the edited element that indicates whether the field has
+   * been changed by the user (i.e. locally) or the field has already been
+   * changed and stored before by the user (i.e. remotely, stored in TempStore).
+   */
+  renderChanged: function () {
+    this.$el.toggleClass('edit-changed', this.model.get('isChanged') || this.model.get('inTempStore'));
+  },
+
+  /**
    * Starts hover; transitions to 'highlight' state.
    *
    * @param jQuery event
    */
   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 +122,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();
   },
 
   /**
@@ -135,7 +141,7 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    * Adds classes used to indicate an elements editable state.
    */
   decorate: function () {
-    this.$el.addClass('edit-animate-fast edit-candidate edit-editable');
+    this.$el.addClass('edit-candidate edit-editable');
   },
 
   /**
@@ -169,9 +175,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');
   },
 
   /**
@@ -332,26 +335,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..15408ad 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);
   },
 
@@ -186,7 +187,14 @@ Drupal.edit.EditorView = Backbone.View.extend({
     var formOptions = {
       fieldID: this.fieldModel.id,
       $el: this.$el,
-      nocssjs: true
+      nocssjs: true,
+      // Reset an existing entry for this entity in the TempStore (if any) when
+      // saving the field. Logically speaking, this should happen in a separate
+      // request because this is an entity-level operation, not a field-level
+      // operation. But that would require an additional request, that might not
+      // even be necessary: it is only when a user saves a first changed field
+      // for an entity that this needs to happen: precisely now!
+      reset: !this.fieldModel.get('entity').get('inTempStore')
     };
     Drupal.edit.util.form.load(formOptions, function (form, ajax) {
       // Create a backstage area for storing forms that are hidden from view
@@ -201,15 +209,16 @@ Drupal.edit.EditorView = Backbone.View.extend({
       // forms.)
       $('#edit_backstage form').prop('novalidate', true);
       var $submit = $('#edit_backstage form .edit-form-submit');
-      var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
+      var formSaveAjax = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
 
       function removeHiddenForm () {
-        Drupal.edit.util.form.unajaxifySaving($submit);
         $('#edit_backstage').remove();
       }
 
+      formSaveAjax.commands = {};
+
       // Successfully saved.
-      Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) {
+      formSaveAjax.commands.editFieldFormSaved = function (ajax, response, status) {
         removeHiddenForm();
 
         // First, transition the state to 'saved'.
@@ -220,20 +229,13 @@ Drupal.edit.EditorView = Backbone.View.extend({
       };
 
       // Unsuccessfully saved; validation errors.
-      Drupal.ajax[base].commands.editFieldFormValidationErrors = function (ajax, response, status) {
+      formSaveAjax.commands.editFieldFormValidationErrors = function (ajax, response, status) {
         removeHiddenForm();
 
         editorModel.set('validationErrors', response.data);
         fieldModel.set('state', 'invalid');
       };
 
-      // The editFieldForm AJAX command is only called upon loading the form
-      // for the first time, and when there are validation errors in the form;
-      // Form API then marks which form items have errors. This is useful for
-      // the form-based in-place editor, but pointless for any other: the form
-      // itself won't be visible at all anyway! So, we just ignore it.
-      Drupal.ajax[base].commands.editFieldForm = function () {};
-
       fillAndSubmitForm(editorModel.get('currentValue'));
     });
   },
@@ -246,7 +248,7 @@ Drupal.edit.EditorView = Backbone.View.extend({
   showValidationErrors: function () {
     var $errors = $('<div class="edit-validation-errors"></div>')
       .append(this.model.get('validationErrors'));
-    $(this.fieldModel.get('el'))
+    this.getEditedElement()
       .addClass('edit-validation-error')
       .after($errors);
   },
@@ -260,7 +262,7 @@ Drupal.edit.EditorView = Backbone.View.extend({
    * invalid value was discarded.
    */
   removeValidationErrors: function () {
-    $(this.fieldModel.get('el'))
+    this.getEditedElement()
       .removeClass('edit-validation-error')
       .next('.edit-validation-errors')
       .remove();
diff --git a/core/modules/edit/js/views/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
new file mode 100644
index 0000000..81ba03f
--- /dev/null
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -0,0 +1,310 @@
+/**
+ * @file
+ * A Backbone View that provides an entity level toolbar.
+ */
+(function ($, Backbone, Drupal, debounce) {
+
+"use strict";
+
+Drupal.edit.EntityToolbarView = Backbone.View.extend({
+
+  _fieldToolbarRoot: 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) {
+    this.appModel = options.appModel;
+    this.$entity = $(this.model.get('el'));
+
+    // Rerender whenever the entity state changes.
+    this.model.on('change:isActive change:isDirty change:state', this.render, this);
+    // Also rerender whenever the highlighted or active in-place editor changes.
+    this.appModel.on('change:highlightedEditor change:activeEditor', this.render, this);
+
+    // Reposition the entity toolbar as the viewport and the position within the
+    // viewport changes.
+    $(window).on('resize.edit scroll.edit', debounce($.proxy(this.windowChangeHandler, this), 150));
+
+    // Set the entity toolbar DOM element as the el for this view.
+    var $toolbar = this.buildToolbarEl();
+    this.setElement($toolbar);
+    this._fieldToolbarRoot = $toolbar.find('.edit-toolbar-field').get(0);
+
+    // Initial render.
+    this.render();
+  },
+
+  /**
+   * {@inheritdoc}
+   */
+  render: function () {
+    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);
+      }
+      // Adds the entity title to the toolbar.
+      this.label();
+
+      // Show the save and cancel buttons.
+      this.show('ops');
+      // If render is being called and the toolbar is already visible, just
+      // reposition it.
+      this.position();
+    }
+
+    // The save button text and state varies with the state of the entity model.
+    var $button = this.$el.find('.edit-button.action-save');
+    var isDirty = this.model.get('isDirty');
+    // Adjust the save button according to the state of the model.
+    switch (this.model.get('state')) {
+      // Quick editing is active, but no field is being edited.
+      case 'opened':
+        // The saving throbber is not managed by AJAX system. The
+        // EntityToolbarView manages this visual element.
+        $button
+          .removeClass('action-saving icon-throbber icon-end')
+          .text(Drupal.t('Save'))
+          .removeAttr('disabled')
+          .attr('aria-hidden', !isDirty);
+        break;
+      // The changes to the fields of the entity are being committed.
+      case 'committing':
+        $button
+          .addClass('action-saving icon-throbber icon-end')
+          .text(Drupal.t('Saving'))
+          .attr('disabled', 'disabled');
+        break;
+      default:
+        $button.attr('aria-hidden', true);
+        break;
+    }
+
+    return this;
+  },
+
+  /**
+   * Repositions the entity toolbar on window scroll and resize.
+   *
+   * @param jQuery.Eevent event
+   */
+  windowChangeHandler: function (event) {
+    this.position();
+  },
+
+  /**
+   * Uses the jQuery.ui.position() method to position the entity toolbar.
+   *
+   * @param jQuery|DOM element
+   *   (optional) The element against which the entity toolbar is positioned.
+   */
+  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 active 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 + '-2 bottom-3',
+          at: edge + ' top',
+          of: of,
+          collision: 'flipfit',
+          // 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)
+            });
+          }
+        })
+        // Resize the toolbar to match the dimensions of the field, up to a max
+        // width that is equal to the field's width.
+        .css({
+          'max-width': $(of).outerWidth(),
+          'width': '100%'
+        });
+      }, 250);
+  },
+
+  /**
+   * Set the model state to 'saving' when the save button is clicked.
+   *
+   * @param jQuery event
+   */
+  onClickSave: function (event) {
+    event.stopPropagation();
+    event.preventDefault();
+    // 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');
+  },
+
+  /**
+   * Clears the timeout that will eventually reposition the entity toolbar.
+   *
+   * Without this, it may reposition itself, away from the user's cursor!
+   *
+   * @param jQuery event
+   */
+  onMouseenter: function (event) {
+    clearTimeout(this.timer);
+  },
+
+  /**
+   * Builds the entity toolbar HTML; attaches to DOM; sets starting position.
+   */
+  buildToolbarEl: function () {
+    var $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 icon',
+            attributes: {
+              'aria-hidden': true
+            }
+          },
+          {
+            label: Drupal.t('Close'),
+            classes: 'action-cancel edit-button icon icon-close icon-only'
+          }
+        ]
+      }));
+
+    // Give the toolbar a sensible starting position so that it doesn't animate
+    // on to the screen from a far off corner.
+    $toolbar
+      .css({
+        left: this.$entity.offset().left,
+        top: this.$entity.offset().top
+      });
+
+    return $toolbar;
+  },
+
+  /**
+   * Returns the DOM element that fields will attach their toolbars to.
+   *
+   * @return jQuery
+   *   The DOM element that fields will attach their toolbars to.
+   */
+  getToolbarRoot: function () {
+    return this._fieldToolbarRoot;
+  },
+
+  /**
+   * 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.
+   * @param String classes
+   *   A string of space-delimited class names that will be applied to the
+   *   wrapping element of the toolbar group.
+   */
+  addClass: function (toolgroup, classes) {
+    this._find(toolgroup).addClass(classes);
+  },
+
+  /**
+   * Removes classes from a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   * @param String classes
+   *   A string of space-delimited class names that will be removed from the
+   *   wrapping element of the toolbar group.
+   */
+  removeClass: function (toolgroup, classes) {
+    this._find(toolgroup).removeClass(classes);
+  },
+
+  /**
+   * Finds a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   * @return jQuery
+   *   The toolgroup DOM element.
+   */
+  _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/EntityView.js b/core/modules/edit/js/views/EntityView.js
new file mode 100644
index 0000000..a2e771a
--- /dev/null
+++ b/core/modules/edit/js/views/EntityView.js
@@ -0,0 +1,31 @@
+(function ($, Backbone) {
+
+"use strict";
+
+Drupal.edit.EntityView = Backbone.View.extend({
+  /**
+   * {@inheritdoc}
+   *
+   * Associated with the DOM root node of an editable entity.
+   */
+  initialize: function () {
+    this.model.on('change', this.render, this);
+  },
+
+  /**
+   * {@inheritdoc}
+   */
+  render: function () {
+    this.$el.toggleClass('edit-entity-active', this.model.get('isActive'));
+  },
+
+  /**
+   * {@inheritdoc}
+   */
+  remove: function () {
+    this.setElement(null);
+    Backbone.View.prototype.remove.call(this);
+  }
+});
+
+}(jQuery, Backbone));
diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js
index 4cdd53d..b261a7b 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,22 @@ 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.
+    // @todo @Jesse I think this if statement can be removed, because the entity
+    // toolbar positions its correctly above any element, including
+    // display:inline ones presumably.
     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);
+      // The $root is the DOM element in the entity toolbar that field toolbars
+      // attach to.
+      this.$el.prependTo(this.$root);
     }
 
     return this;
@@ -76,245 +67,61 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
     var to = state;
     switch (to) {
       case 'inactive':
-        if (from) {
-          this.remove();
-        }
         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();
-          }
+        // Remove the view's existing element if we went to the 'activating'
+        // state or later, because it will be recreated. Not doing this would
+        // result in memory leaks.
+        if (from !== 'inactive' && from !== 'highlighted') {
+          this.$el.remove();
+          this.setElement();
         }
         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 +161,36 @@ 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 $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) {
+      $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/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php
index ee9c269..8d2bcb0 100644
--- a/core/modules/edit/lib/Drupal/edit/EditController.php
+++ b/core/modules/edit/lib/Drupal/edit/EditController.php
@@ -58,6 +58,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();
@@ -81,7 +83,13 @@ public function metadata(Request $request) {
         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);
     }
 
     return new JsonResponse($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..7baaf5b 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 in the given language.
+   */
+  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..6db2ecf 100644
--- a/core/modules/editor/js/editor.formattedTextEditor.js
+++ b/core/modules/editor/js/editor.formattedTextEditor.js
@@ -163,7 +163,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
     var fieldID = this.fieldModel.id;
 
     // Create a Drupal.ajax instance to load the form.
-    Drupal.ajax[fieldID] = new Drupal.ajax(fieldID, this.$el, {
+    var textLoaderAjax = new Drupal.ajax(fieldID, this.$el, {
       url: Drupal.edit.util.buildUrl(fieldID, drupalSettings.editor.getUntransformedTextURL),
       event: 'editor-internal.editor',
       submit: { nocssjs : true },
@@ -172,11 +172,9 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
 
     // Implement a scoped editorGetUntransformedText AJAX command: calls the
     // callback.
-    Drupal.ajax[fieldID].commands.editorGetUntransformedText = function (ajax, response, status) {
+    textLoaderAjax.commands = {};
+    textLoaderAjax.commands.editorGetUntransformedText = function (ajax, response, status) {
       callback(response.data);
-      // Delete the Drupal.ajax instance that called this very function.
-      delete Drupal.ajax[fieldID];
-      this.$el.off('editor-internal.editor');
     };
 
     // This will ensure our scoped editorGetUntransformedText AJAX command
