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

commit 162f6e2056f1fb6b5a23a9f33231dbd2b990405d
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 18:20:48 2013 -0400

    Fixed positioning of the entity toolbar when a field is in the 'changed' state.

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

commit 3c1a0635f456ac7113435c896b9461c361c3746d
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 18:09:40 2013 -0400

    Moved acceptStateChange to the FieldModel.

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

commit 32c72a6dd359b93f69cba485a376793e74bc8787
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 17:09:33 2013 -0400

    Removed ignoreHoveringVia

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

commit 859f79c770b9ce893f43179eb7ea1b5a745dde9a
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 16:49:14 2013 -0400

    don't need this mouseleave handler any more.

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

commit 4f670a6d562823c6ef787dfc31ef144001b5be49
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 16:30:58 2013 -0400

    cleaning up the close functions.

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

commit 0d6c0f1bb8ed416dc17a3df9caf8aea5f362cc00
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 16:09:16 2013 -0400

    Moved saving to the EntityModel.

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

commit 3ff4c70f51c00fe167f3d03faaed75df11cdbdaa
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 15:41:03 2013 -0400

    So rough and dirty, you'll need two showers. But it works.

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

commit f2f95939cd00f3751550ee260a0dbd95feaea3d6
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 15:20:57 2013 -0400

    Attach label on highlight.

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

commit ff011a58f992e7e2383bb2436dab98e52e4b2bc2
Author: J. Renée Beach <splendidnoise@gmail.com>
Date:   Sun May 5 14:57:26 2013 -0400

    Issue #1678002-89

    commit f21dd7a7663723d477db8febd0edd91dbee04b74
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Fri May 3 16:50:34 2013 -0400

        Entity Toolbar now follows on hover.

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

    commit b31e8e966bdfde2bfb6eb70c4e87c9d2243076bd
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Fri May 3 15:17:12 2013 -0400

        Moved field state handling out of EntityToolbarView

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

    commit 38378dfd9299e9948dfacc2132299fe7853957ac
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Fri May 3 14:24:21 2013 -0400

        Introduced state to the EntityModel.

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

    commit 48cdae96253e83ad6fd3cbe6314404a904cf1456
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Fri May 3 14:06:00 2013 -0400

        Trying to make save work

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

    commit 23ac70132f6351b25d587c3fce2493779b1ccf62
    Author: J. Renée Beach <splendidnoise@gmail.com>
    Date:   Fri May 3 11:38:58 2013 -0400

        Squashed commit of the following:

        commit c1d27c949c5f4519a2cdfe46934d1e90e07f2e33
        Author: J. Renée Beach <splendidnoise@gmail.com>
        Date:   Thu May 2 15:04:36 2013 -0400

            Position the toolbar after a field closes.

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

        commit 39d7b9bccee5aff877120fe964fcdb226f516e7d
        Author: J. Renée Beach <splendidnoise@gmail.com>
        Date:   Thu May 2 15:00:02 2013 -0400

            The field label now attaches correctly.

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

        commit 4b1d1c33196f5ca784cd5e16146d16e1334a1c5c
        Author: J. Renée Beach <splendidnoise@gmail.com>
        Date:   Thu May 2 14:35:42 2013 -0400

            entity toolbar

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

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

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

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/modules/edit/css/edit.css                     |   48 +--
 core/modules/edit/edit.module                      |    3 +
 core/modules/edit/js/edit.js                       |   18 +-
 core/modules/edit/js/models/EntityModel.js         |  159 ++++++++++
 core/modules/edit/js/models/FieldModel.js          |  116 ++++++-
 core/modules/edit/js/theme.js                      |   29 +-
 core/modules/edit/js/views/AppView.js              |  137 ++------
 core/modules/edit/js/views/EditorDecorationView.js |   81 +++--
 core/modules/edit/js/views/EntityToolbarView.js    |  331 ++++++++++++++++++++
 core/modules/edit/js/views/FieldToolbarView.js     |  247 +++------------
 .../editor/js/editor.formattedTextEditor.js        |    3 +-
 11 files changed, 767 insertions(+), 405 deletions(-)
 create mode 100644 core/modules/edit/js/views/EntityToolbarView.js

diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css
index 37e7578..b10d11f 100644
--- a/core/modules/edit/css/edit.css
+++ b/core/modules/edit/css/edit.css
@@ -61,17 +61,19 @@
           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;
+/**
+ * Entity toolbar.
+ */
+.edit-toolbar-container {
+  background-color: white;
+  border: 1px solid #ababab;
+  position: absolute;
+  -webkit-transition: all 0.2s;
+  transition: all 0.2s;
+  width: 20em;
+  z-index: 350;
 }
 
-
-
-
 /**
  * Candidate editables + editables being edited.
  *
@@ -214,8 +216,6 @@
 .edit-toolbar-container,
 .edit-form-container {
   position: relative;
-  padding: 0;
-  border: 0;
   margin: 0;
   vertical-align: baseline;
   z-index: 310;
@@ -229,38 +229,12 @@
           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 {
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index 2de3b37..c7fa4fb 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -78,6 +78,7 @@ function edit_library_info() {
       // Views.
       $path . '/js/views/AppView.js' => $options,
       $path . '/js/views/EditorDecorationView.js' => $options,
+      $path . '/js/views/EntityToolbarView.js' => $options,
       $path . '/js/views/ContextualLinkView.js' => $options,
       $path . '/js/views/ModalView.js' => $options,
       $path . '/js/views/FieldToolbarView.js' => $options,
@@ -106,8 +107,10 @@ function edit_library_info() {
       array('system', 'underscore'),
       array('system', 'backbone'),
       array('system', 'jquery.form'),
+      array('system', 'jquery.ui.position'),
       array('system', 'drupal.form'),
       array('system', 'drupal.ajax'),
+      array('system', 'drupal.debounce'),
       array('system', 'drupalSettings'),
     ),
   );
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index 0720e6e..a963ddf 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -193,7 +193,8 @@ Drupal.behaviors.edit = {
       var id = $this.data('edit-entity');
 
       entityModel = new Drupal.edit.EntityModel({
-        id: id
+        id: id,
+        el: this
       });
       that.collections.entities.add(entityModel);
 
@@ -278,10 +279,11 @@ Drupal.behaviors.edit = {
         // trigger an "update" method rather than a "create" method.
         id: editID || null,
         editID: editID || null,
+        // Store the field in a collection in its entity's model.
+        collection: entity.get('fields'),
         label: Drupal.edit.metadataCache[editID].label,
         editor: Drupal.edit.metadataCache[editID].editor,
-        html: $element[0].outerHTML,
-        acceptStateChange: _.bind(Drupal.edit.app.acceptEditorStateChange, Drupal.edit.app)
+        html: $element[0].outerHTML
       });
 
       // Track all fields on the page.
@@ -291,7 +293,15 @@ Drupal.behaviors.edit = {
       // being edited, then transition it to the 'candidate' state.
       // (This happens when a field was modified and is re-rendered.)
       if (entity === activeEntity) {
-        Drupal.edit.app.decorate(field);
+        // @todo The signature to app.decorate has evovled into a poor design.
+        // It needs to many arguments to function.
+        var entityToolbar = entity.get('entityToolbar');
+
+        // Get the field toolbar DOM root from the entity toolbar.
+        var fieldToolbarRoot = entityToolbar.getToolbarRoot();
+        var fieldLabelRoot = entityToolbar.getLabelRoot();
+
+        Drupal.edit.app.decorate(entity, field, {toolbar: fieldToolbarRoot, label: fieldLabelRoot});
         field.set('state', 'candidate');
       }
     });
diff --git a/core/modules/edit/js/models/EntityModel.js b/core/modules/edit/js/models/EntityModel.js
index 047ffe7..54701ce 100644
--- a/core/modules/edit/js/models/EntityModel.js
+++ b/core/modules/edit/js/models/EntityModel.js
@@ -16,11 +16,170 @@ $.extend(Drupal.edit, {
       // Indicates whether this instance of this entity is currently being
       // edited.
       isActive: false,
+      // The current processing state of an entity.
+      state: 'inactive',
       // A Drupal.edit.FieldCollection for all fields of this entity.
       fields: null
     },
+
+    /**
+     *
+     */
     initialize: function () {
+
       this.set('fields', new Drupal.edit.FieldCollection());
+
+      // Instantiate configuration for state handling.
+      // @see Drupal.edit.FieldModel.states
+      // @todo, these shouldn't be defined here. currently the position method
+      // is using them to find an active field. The FieldCollection should have a
+      // method that returns this.
+      this.activeEditorStates = ['activating', 'active', 'changed'];
+      this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates);
+
+      // Respond to field view changes.
+      this.on('viewChanged', this.viewChange, this);
+
+      // The state of the entity is largely dependent on the state of its
+      // fields.
+      this.get('fields').on('change:state', this.fieldStateChange, this);
+
+      // The entity keeps its own state progression.
+      this.on('change:state', this.stateChange, this);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    save: function () {
+      var changedFields = this.get('fields').where({'state': 'changed'});
+      // If fields are dirty, we just do an incremental save to get the changes
+      // into TempStore.
+      if (changedFields.length) {
+        _.each(changedFields, function (model) {
+          model.set('state', 'saving');
+        });
+      }
+      // If no fields are dirty, save the entity.
+      else {
+        $.ajax({
+          url: '/edit/entity/' + this.id,
+          success: function (data) {
+            console.log(data);
+          },
+          error: function (data) {
+            console.log(data);
+          }
+        });
+      }
+    },
+
+    /**
+     *
+     */
+    close: function () {
+      var fields = this.get('fields');
+      var activeFields = fields.where({'state': 'active'});
+      // If a field is active, then set it back to candidate state. If not,
+      // close the editing of this entity.
+      if (activeFields.length) {
+        fields.each(function (model) {
+          model.set('state', 'candidate');
+        });
+      }
+      else {
+        this.set('isActive', false);
+        // Set all the fields back to candidate status.
+        fields.each(function (model) {
+          model.set('state', 'inactive');
+        });
+      }
+    },
+
+    /**
+     *
+     */
+    fieldStateChange: function (model, state, options) {
+      var from = model.previous('state');
+      var to = state;
+      var fields = this.get('fields');
+      switch (to) {
+        case 'inactive':
+          this.set('state', to);
+          break;
+        case 'candidate':
+          if (fields.where({'state': 'changed'}).length) {
+            this.set('state', 'changed');
+          }
+          else {
+            this.set('state', to);
+          }
+          break;
+        case 'highlighted':
+          this.set('state', to);
+          break;
+        case 'activating':
+          this.set('state', to);
+          break;
+        case 'active':
+          this.set('state', to);
+          break;
+        case 'changed':
+          this.set('state', to);
+          break;
+        case 'saving':
+          this.set('state', to);
+          break;
+        case 'saved':
+          this.set('state', to);
+          break;
+        case 'invalid':
+          this.set('state', to);
+          break;
+        default:
+          break;
+      }
+    },
+
+    /**
+     * Listens to FieldModel editor state changes.
+     *
+     * @param Drupal.edit.FieldModel model
+     * @param String state
+     *   The state of an editable element. Used to determine display and behavior.
+     */
+    stateChange: function (model, state, options) {
+      var from = model.previous('state');
+      var to = state;
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          break;
+        case 'highlighted':
+          break;
+        case 'activating':
+          break;
+        case 'active':
+          break;
+        case 'changed':
+          break;
+        case 'saving':
+          break;
+        case 'saved':
+          break;
+        case 'invalid':
+          break;
+        default:
+          break;
+      }
+    },
+
+    /**
+     *
+     */
+    viewChange: function (view) {
+      this.trigger('fieldViewChange', view);
     }
   })
 });
diff --git a/core/modules/edit/js/models/FieldModel.js b/core/modules/edit/js/models/FieldModel.js
index 0181b9b..bee0dd9 100644
--- a/core/modules/edit/js/models/FieldModel.js
+++ b/core/modules/edit/js/models/FieldModel.js
@@ -98,7 +98,7 @@ $.extend(Drupal.edit, {
           return '"' + next + '" is an invalid state';
         }
         // Check if the acceptStateChange callback accepts it.
-        if (!this.get('acceptStateChange')(current, next, options)) {
+        if (!this.acceptStateChange(current, next, options)) {
           return 'state change not accepted';
         }
       }
@@ -110,12 +110,122 @@ $.extend(Drupal.edit, {
     // Parses the `<field name>/<language code>/<view mode>` part from the edit ID.
     getFieldID: function() {
       return this.get('editID').split('/').slice(2, 5).join('/');
+    },
+    /**
+     * Accepts or rejects state changes.
+     *
+     * @param String from
+     *   The previous state.
+     * @param String to
+     *   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.
+     */
+    acceptStateChange: function(from, to, context, callback) {
+      var that = this;
+      var otherFields = this.collection.reject(function (model) {
+        return _.isEqual(model, that);
+      });
+      var accept = true;
+
+      console.log("accept? %s → %s (reason: %s)", from, to, (context && context.reason) ? context.reason : 'NONE');
+
+      // If the app is in view mode, then reject all state changes except for
+      // those to 'inactive'.
+      if (context && (context.reason === 'stop' || context.reason === 'rerender')) {
+        if (from === 'candidate' && to === 'inactive') {
+          accept = true;
+        }
+      }
+
+      // Ensure only one editor (field) at a time may be higlighted or active.
+      else if (from === 'candidate' && otherFields.some(function (model) {
+            // If any of the other fields are in a single editor state, reject
+            // this state change.
+            return _.indexOf(that.constructor.singleEditorStates, model.get('state')) !== -1
+        })) {
+        accept = false;
+      }
+      // Handling of edit mode state changes is more granular.
+      else {
+        // 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: activating/active -> candidate.
+          // Necessary to stop editing a property.
+          if (_.indexOf(this.constructor.activeEditorStates, from) !== -1 && to === 'candidate') {
+            accept = true;
+          }
+          // Allow: changed/invalid -> candidate.
+          // Necessary to stop editing a property 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.
+          else if (from === 'highlighted' && to === 'candidate') {
+            accept = true;
+          }
+          // Allow: saved -> candidate.
+          // Necessary when successfully saved a property.
+          else if (from === 'saved' && to === 'candidate') {
+            accept = true;
+          }
+          // Allow: invalid -> saving.
+          // Necessary to be able to save a corrected, invalid property.
+          else if (from === 'invalid' && to === 'saving') {
+            accept = true;
+          }
+        }
+
+        // If it's not against the general principle, then here are more
+        // disallowed cases to check.
+        if (accept) {
+          // Reject going from activating/active to candidate because of a
+          // mouseleave.
+          if (_.indexOf(this.constructor.activeEditorStates, from) !== -1 && to === 'candidate') {
+            if (context && context.reason === 'mouseleave') {
+              accept = false;
+            }
+          }
+          // When attempting to stop editing a changed/invalid property, ask for
+          // confirmation.
+          else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
+            if (context && context.reason === 'mouseleave') {
+              accept = false;
+            }
+            else {
+              // Check whether the transition has been confirmed?
+              if (context && context.confirmed) {
+                accept = true;
+              }
+              // Confirm this transition.
+              else {
+                // Do not accept this change right now, instead open a modal
+                // that will ask the user to confirm his choice.
+                accept = false;
+                // The callback will be called from the helper function.
+                this._confirmStopEditing(callback);
+              }
+            }
+          }
+        }
+      }
+
+      return accept;
     }
   }, {
     states: [
-        'inactive', 'candidate', 'highlighted',
-        'activating', 'active', 'changed', 'saving', 'saved', 'invalid'
+      'inactive', 'candidate', 'highlighted',
+      'activating', 'active', 'changed', 'saving', 'saved', 'invalid'
     ],
+    // Instantiate configuration for state handling.
+    activeEditorStates: ['activating', 'active', 'changed'],
+    singleEditorStates: ['activating', 'active', 'changed', 'highlighted'],
     followsStateSequence: function (from, to) {
       return _.indexOf(this.states, from) < _.indexOf(this.states, to);
     }
diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js
index 7bef553..46d5a49 100644
--- a/core/modules/edit/js/theme.js
+++ b/core/modules/edit/js/theme.js
@@ -64,17 +64,31 @@ Drupal.theme.editModal = function(settings) {
  * @return
  *   The corresponding HTML.
  */
-Drupal.theme.editToolbarContainer = function(settings) {
+Drupal.theme.editEntityToolbar = function(settings) {
   var html = '';
-  html += '<div id="' + settings.id + '" class="edit-toolbar-container">';
-  html += '  <div class="edit-toolbar-heightfaker edit-animate-fast">';
-  html += '    <div class="edit-toolbar primary" />';
-  html += '  </div>';
+  html += '<div id="' + settings.id + '" class="edit-toolbar-container clearfix">';
+  html += '<div class="edit-toolbar edit-toolbar-entity clearfix">';
+  html += '<div class="edit-toolbar-label" />';
+  html += '</div>';
+  html += '<div class="edit-toolbar edit-toolbar-field clearfix" />';
   html += '</div>';
   return html;
 };
 
 /**
+ * Theme function for a toolbar container of the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - id: the id to apply to the toolbar container.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editFieldToolbar = function(settings) {
+  return '<div id="' + settings.id + '" class="edit-toolbar" />';
+};
+
+/**
  * Theme function for a toolbar toolgroup of the Edit module.
  *
  * @param settings
@@ -86,9 +100,10 @@ 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';
+  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 + '"';
   }
diff --git a/core/modules/edit/js/views/AppView.js b/core/modules/edit/js/views/AppView.js
index 742dcc5..69fc276 100644
--- a/core/modules/edit/js/views/AppView.js
+++ b/core/modules/edit/js/views/AppView.js
@@ -22,12 +22,7 @@ $.extend(Drupal.edit, {
       this.entitiesCollection = options.entitiesCollection;
       this.fieldsCollection = options.fieldsCollection;
 
-      _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange');
-
-      // Instantiate configuration for state handling.
-      // @see Drupal.edit.FieldModel.states
-      this.activeEditorStates = ['activating', 'active'];
-      this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates);
+      _.bindAll(this, 'appStateChange', 'editorStateChange');
 
       this.entitiesCollection
         // Track app state.
@@ -50,16 +45,32 @@ $.extend(Drupal.edit, {
     appStateChange: function (entityModel, isActive) {
       var app = this;
       if (isActive) {
+        // Create an entity toolbar.
+        var entityToolbar = new Drupal.edit.EntityToolbarView({
+          el: entityModel.get('el'),
+          model: entityModel
+        });
+        entityModel.set('entityToolbar', entityToolbar);
+
+        // Get the field toolbar DOM root from the entity toolbar.
+        var fieldToolbarRoot = entityToolbar.getToolbarRoot();
+        var fieldLabelRoot = entityToolbar.getLabelRoot();
+
         // Move all fields of this entity from the 'inactive' state to the
         // 'candidate' state.
         entityModel.get('fields').each(function (fieldModel) {
           // First, set up decoration views.
-          app.decorate(fieldModel);
+          app.decorate(entityModel, fieldModel, {toolbar: fieldToolbarRoot, label: fieldLabelRoot});
           // Second, change the field's state.
           fieldModel.set('state', 'candidate');
         });
       }
       else {
+        // Remove the entity toolbar.
+        var entityToolbar = entityModel.get('entityToolbar');
+        entityToolbar.remove();
+        entityModel.unset('entityToolbar');
+
         // Move all fields of this entity from whatever state they are in to
         // the 'inactive' state.
         entityModel.get('fields').each(function (fieldModel) {
@@ -71,111 +82,8 @@ $.extend(Drupal.edit, {
       }
     },
 
-    /**
-     * Accepts or reject editor (Editor) state changes.
-     *
-     * This is what ensures that the app is in control of what happens.
-     *
-     * @param String from
-     *   The previous state.
-     * @param String to
-     *   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.
-     */
-    acceptEditorStateChange: function(from, to, context, callback) {
-      var accept = true;
-
-      console.log("accept? %s → %s (reason: %s)", from, to, (context && context.reason) ? context.reason : 'NONE');
-
-      // If the app is in view mode, then reject all state changes except for
-      // those to 'inactive'.
-      if (context && (context.reason === 'stop' || context.reason === 'rerender')) {
-        if (from === 'candidate' && to === 'inactive') {
-          accept = true;
-        }
-      }
-      // Handling of edit mode state changes is more granular.
-      else {
-        // In general, enforce the states sequence. Disallow going back from a
-        // "later" state to an "earlier" state, except in explicitly allowed
-        // cases.
-        if (!Drupal.edit.FieldModel.followsStateSequence(from, to)) {
-          accept = false;
-          // Allow: activating/active -> candidate.
-          // Necessary to stop editing a property.
-          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.
-          else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
-            accept = true;
-          }
-          // Allow: highlighted -> candidate.
-          // Necessary to stop highlighting a property.
-          else if (from === 'highlighted' && to === 'candidate') {
-            accept = true;
-          }
-          // Allow: saved -> candidate.
-          // Necessary when successfully saved a property.
-          else if (from === 'saved' && to === 'candidate') {
-            accept = true;
-          }
-          // Allow: invalid -> saving.
-          // Necessary to be able to save a corrected, invalid property.
-          else if (from === 'invalid' && to === 'saving') {
-            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;
-            }
-          }
-          // Reject going from activating/active to candidate because of a
-          // mouseleave.
-          else if (_.indexOf(this.activeEditorStates, from) !== -1 && to === 'candidate') {
-            if (context && context.reason === 'mouseleave') {
-              accept = false;
-            }
-          }
-          // When attempting to stop editing a changed/invalid property, ask for
-          // confirmation.
-          else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
-            if (context && context.reason === 'mouseleave') {
-              accept = false;
-            }
-            else {
-              // Check whether the transition has been confirmed?
-              if (context && context.confirmed) {
-                accept = true;
-              }
-              // Confirm this transition.
-              else {
-                // Do not accept this change right now, instead open a modal
-                // that will ask the user to confirm his choice.
-                accept = false;
-                // The callback will be called from the helper function.
-                this._confirmStopEditing(callback);
-              }
-            }
-          }
-        }
-      }
-
-      return accept;
-    },
-
     // @todo rename to decorateField
-    decorate: function (fieldModel) {
+    decorate: function (entityModel, fieldModel, attachmentPoints) {
       var editID = fieldModel.get('editID');
       var $el = fieldModel.get('$el');
 
@@ -191,8 +99,10 @@ $.extend(Drupal.edit, {
       // They are a sibling element before the editor's DOM element.
       var toolbarView = new Drupal.edit.FieldToolbarView({
         model: fieldModel,
+        el: attachmentPoints.toolbar,
         $field: $el,
-        editorView: editorView
+        editorView: editorView,
+        entityModel: entityModel
       });
 
       // Decorate the editor's DOM element depending on its state.
@@ -200,7 +110,8 @@ $.extend(Drupal.edit, {
         el: $el,
         model: fieldModel,
         editorView: editorView,
-        toolbarId: toolbarView.getId()
+        toolbarId: toolbarView.getId(),
+        labelEl: attachmentPoints.label
       });
 
       // Create references in the field model; necessary for undecorate() and
diff --git a/core/modules/edit/js/views/EditorDecorationView.js b/core/modules/edit/js/views/EditorDecorationView.js
index 41647df..6409def 100644
--- a/core/modules/edit/js/views/EditorDecorationView.js
+++ b/core/modules/edit/js/views/EditorDecorationView.js
@@ -36,9 +36,15 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
 
     this.toolbarId = options.toolbarId;
 
+    this.$labelEl = $(options.labelEl);
+
     this.model.on('change:state', this.stateChange, this);
   },
 
+  remove: function () {
+    console.log('@todo, EditorDecorationView.remove is not yet defined.');
+  },
+
   /**
    * Determines the actions to take given a change of state.
    *
@@ -52,6 +58,12 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
     switch (to) {
       case 'inactive':
         this.undecorate();
+        if (from) {
+          this.remove();
+          if (this.model.get('editor') !== 'form') {
+            Backbone.syncDirectCleanUp();
+          }
+        }
         break;
       case 'candidate':
         this.decorate();
@@ -61,9 +73,22 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
             this.stopEdit();
           }
         }
+        this.detachLabel();
+
+        if (this.model.get('editor') !== 'form') {
+          Backbone.syncDirectCleanUp();
+        }
+        // Remove all toolgroups; they're no longer necessary.
+        this.$el
+          .removeClass('edit-highlighted edit-editing')
+          .find('.edit-toolbar .edit-toolgroup').remove();
+        if (from !== 'highlighted' && this.getEditUISetting('padding')) {
+          this._unpad();
+        }
         break;
       case 'highlighted':
         this.startHighlight();
+        this.attachLabel();
         break;
       case 'activating':
         // NOTE: this state is not used by every editor! It's only used by those
@@ -94,10 +119,8 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
    */
   onMouseEnter: function (event) {
     var that = this;
-    this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
-      that.model.set('state', 'highlighted');
-      event.stopPropagation();
-    });
+    that.model.set('state', 'highlighted');
+    event.stopPropagation();
   },
 
   /**
@@ -107,10 +130,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();
   },
 
   /**
@@ -194,6 +215,30 @@ Drupal.edit.EditorDecorationView = Backbone.View.extend({
   },
 
   /**
+   *
+   */
+  attachLabel: function () {
+    // Retrieve the label to show for this field.
+    var label = this.model.get('label');
+
+    this.$labelEl
+      // Append the "info" toolgroup into the toolbar.
+      .append(Drupal.theme('editToolgroup', {
+        classes: ['info'],
+        buttons: [
+          { label: label, classes: 'blank-button label' }
+        ]
+      }));
+  },
+
+  /**
+   *
+   */
+  detachLabel: function () {
+    this.$labelEl.find('.edit-toolgroup.info').remove();
+  },
+
+  /**
    * Retrieves a setting of the editor-specific Edit UI integration.
    *
    * @param String setting
@@ -336,26 +381,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/EntityToolbarView.js b/core/modules/edit/js/views/EntityToolbarView.js
new file mode 100644
index 0000000..7a56367
--- /dev/null
+++ b/core/modules/edit/js/views/EntityToolbarView.js
@@ -0,0 +1,331 @@
+/**
+ * @file
+ * A Backbone View that provides an entity level toolbar.
+ */
+(function ($, Backbone, Drupal, debounce) {
+
+"use strict";
+
+Drupal.edit.EntityToolbarView = Backbone.View.extend({
+
+  _loader: null,
+  _loaderVisibleStart: 0,
+  _fieldToolbarRoot: null,
+  _fieldLabelRoot: null,
+
+  events: function () {
+    var map = {
+      'click.edit button.field-save': 'onClickSave',
+      'click.edit button.field-close': 'onClickClose',
+      'click.edit button.label': 'onClickInfoLabel'
+    }
+    return map;
+  },
+
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function (options) {
+    var that = this;
+
+    this.model.on('change:isActive', this.render, this);
+    this.model.on('change:state', this.stateChange, this);
+    this.model.on('fieldViewChange', this.fieldViewChangeHandler, this);
+
+    $(window).on('resize.edit scroll.edit', debounce($.proxy(this.windowChangeHandler, this), 150));
+
+    // Set the el into its own property. Eventually the el property will be
+    // replaced with the rendered toolbar.
+    this.$entity = this.$el;
+
+    // Set the toolbar container to this view's el property.
+    this.buildToolbarEl();
+    this._fieldToolbarRoot = this.$el.find('.edit-toolbar-field').get(0);
+    this._fieldLabelRoot = this.$el.find('.edit-toolbar-label').get(0);
+
+    this._loader = null;
+    this._loaderVisibleStart = 0;
+
+    this.render();
+  },
+
+  /**
+   * Implements Backbone.View.prototype.render().
+   */
+  render: function (model, changeValue) {
+
+    if (this.model.get('isActive')) {
+      // If the toolbar container doesn't exist, create it.
+      if ($('body').children('#edit-entity-toolbar').length === 0) {
+        $('body').append(this.$el);
+      }
+      // If render is being called and the toolbar is already visible, just
+      // reposition it.
+      this.position();
+    }
+    else {
+      this.remove();
+    }
+
+    return this;
+  },
+
+  /**
+   *
+   */
+  windowChangeHandler: function (event) {
+    this.position();
+  },
+
+  /**
+   *
+   */
+  fieldViewChangeHandler: function (view) {
+    this.render(this, view);
+  },
+
+  /**
+   * Uses the jQuery.ui.position() method to position the entity toolbar.
+   */
+  position: function (element) {
+    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 field = this.model.get('fields').find(function (field) {
+        return _.indexOf(that.model.singleEditorStates, field.get('state')) !== -1;
+    });
+    var $fieldElement = field && field.get('$el');
+    // Prefer the specified element from the parameters, then the acive field
+    // and finally the entity itself to determine the position of the toolbar.
+    var of = element || $fieldElement || this.$entity;
+    // Uses the jQuery.ui.position() method.
+    this.$el
+      .position({
+        my: edge + ' bottom',
+        at: edge + ' top',
+        of: of
+      });
+  },
+
+  /**
+   * Determines the actions to take given a change of state.
+   *
+   * @param Drupal.edit.EntityModel model
+   * @param String state
+   *   The state of the associated field. One of Drupal.edit.EntityModel.states.
+   */
+  stateChange: function (model, state, options) {
+      var from = model.previous('state');
+      var to = state;
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          // Position the toolbar against the entity.
+          this.position();
+          break;
+        case 'highlighted':
+          this.position();
+          break;
+        case 'activating':
+          this.setLoadingIndicator(true);
+          break;
+        case 'active':
+          this.setLoadingIndicator(false);
+          this.startEdit();
+          // Position the toolbar against the active field.
+          this.position();
+          this.show('ops');
+          break;
+        case 'changed':
+          this.$el
+            .find('button.save')
+            .addClass('blue-button')
+            .removeClass('gray-button');
+          this.show('ops');
+          break;
+        case 'saving':
+          this.setLoadingIndicator(true);
+          break;
+        case 'saved':
+          this.setLoadingIndicator(false);
+          break;
+        case 'invalid':
+          this.setLoadingIndicator(false);
+          break;
+        default:
+          break;
+      }
+  },
+
+  /**
+   * Set the model state to 'saving' when the save button is clicked.
+   *
+   * @param jQuery event
+   */
+  onClickSave: function (event) {
+    event.stopPropagation();
+    event.preventDefault();
+    this.model.save();
+  },
+
+  /**
+   * Sets the model state to candidate when the cancel button is clicked.
+   *
+   * @param jQuery event
+   */
+  onClickClose: function (event) {
+    event.stopPropagation();
+    event.preventDefault();
+    this.model.close();
+  },
+
+
+  /**
+   * Redirects the click.edit-event to the editor DOM element.
+   *
+   * @param jQuery event
+   */
+  onClickInfoLabel: function (event) {
+    event.stopPropagation();
+    event.preventDefault();
+    // Redirects the event to the editor DOM element.
+    this.$field.trigger('click.edit');
+  },
+
+  /**
+   *
+   */
+  buildToolbarEl: function () {
+    var $toolbar;
+    $toolbar = $(Drupal.theme('editEntityToolbar', {
+      id: 'edit-entity-toolbar'
+    }));
+
+    $toolbar
+      .find('.edit-toolbar-entity')
+      // 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' }
+        ]
+      }));
+
+    // Give the toolbar a sensible starting position so that it doesn't
+    // animiate on to the screen from a far off corner.
+    $toolbar
+      .css({
+        left: this.$entity.offset().left,
+        top: this.$entity.offset().top
+      });
+
+    this.setElement($toolbar);
+  },
+
+  /**
+   *
+   */
+  getToolbarRoot: function () {
+    return this._fieldToolbarRoot;
+  },
+
+  /**
+   *
+   */
+  getLabelRoot: function () {
+    return this._fieldLabelRoot;
+  },
+
+  /**
+   *
+   */
+  startEdit: function () {
+    this.$el.addClass('edit-editing');
+  },
+
+  /**
+   * 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;
+    }
+  },
+
+  /**
+   * Adds classes to a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  addClass: function (toolgroup, classes) {
+    this._find(toolgroup).addClass(classes);
+  },
+
+  /**
+   * Removes classes from a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  removeClass: function (toolgroup, classes) {
+    this._find(toolgroup).removeClass(classes);
+  },
+
+  /**
+   * Finds a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  _find: function (toolgroup) {
+    return this.$el.find('.edit-toolgroup.' + toolgroup);
+  },
+
+  /**
+   * Shows a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  show: function (toolgroup) {
+    this._find(toolgroup).removeClass('edit-animate-invisible');
+  },
+
+  /**
+   * Hides a toolgroup.
+   *
+   * @param String toolgroup
+   *   A toolgroup name.
+   */
+  hide: function (toolgroup) {
+    this._find(toolgroup).addClass('edit-animate-invisible');
+  }
+});
+
+})(jQuery, Backbone, Drupal, Drupal.debounce);
diff --git a/core/modules/edit/js/views/FieldToolbarView.js b/core/modules/edit/js/views/FieldToolbarView.js
index a854312..20e7ae5 100644
--- a/core/modules/edit/js/views/FieldToolbarView.js
+++ b/core/modules/edit/js/views/FieldToolbarView.js
@@ -12,32 +12,20 @@
 Drupal.edit.FieldToolbarView = Backbone.View.extend({
   $field: null,
 
-  _loader: null,
-  _loaderVisibleStart: 0,
-
   _id: null,
 
-  events: {
-    'click.edit button.label': 'onClickInfoLabel',
-    'mouseleave.edit': 'onMouseLeave',
-    'click.edit button.field-save': 'onClickSave',
-    'click.edit button.field-close': 'onClickClose'
-  },
-
   /**
    * Implements Backbone.View.prototype.initialize().
    */
   initialize: function (options) {
     this.$field = options.$field;
     this.editorView = options.editorView;
+    this.entityModel = options.entityModel;
 
-    this._loader = null;
-    this._loaderVisibleStart = 0;
+    this.model.on('change:state', this.stateChange, this);
 
     // Generate a DOM-compatible ID for the form container DOM element.
     this._id = 'edit-toolbar-for-' + this.model.get('editID').replace(/\//g, '_');
-
-    this.model.on('change:state', this.stateChange, this);
   },
 
   /**
@@ -48,18 +36,16 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    */
   render: function () {
     // Render toolbar.
-    this.setElement($(Drupal.theme('editToolbarContainer', {
+    this.$toolbar = $(Drupal.theme('editFieldToolbar', {
       id: this._id
-    })));
+    }));
 
     // Insert in DOM.
     if (this.$field.css('display') === 'inline') {
-      this.$el.prependTo(this.$field.offsetParent());
-      var pos = this.$field.position();
-      this.$el.css('left', pos.left).css('top', pos.top);
+      this.$toolbar.prependTo(this.el);
     }
     else {
-      this.$el.insertBefore(this.$field);
+      this.$toolbar.insertBefore(this.$field);
     }
 
     return this;
@@ -72,45 +58,17 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    * @param String state
    *   The state of the associated field. One of Drupal.edit.FieldModel.states.
    */
-  stateChange: function (model, state) {
+  stateChange: function (model, state, options) {
     var from = model.previous('state');
     var to = state;
     switch (to) {
       case 'inactive':
-        if (from) {
-          this.remove();
-          if (this.model.get('editor') !== 'form') {
-            Backbone.syncDirectCleanUp();
-          }
-        }
         break;
       case 'candidate':
-        if (from === 'inactive') {
-          this.render();
-        }
-        else {
-          if (this.model.get('editor') !== 'form') {
-            Backbone.syncDirectCleanUp();
-          }
-          // Remove all toolgroups; they're no longer necessary.
-          this.$el
-            .removeClass('edit-highlighted edit-editing')
-            .find('.edit-toolbar .edit-toolgroup').remove();
-          if (from !== 'highlighted' && this.getEditUISetting('padding')) {
-            this._unpad();
-          }
-        }
         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);
         if (this.getEditUISetting('fullWidthToolbar')) {
           this.$el.addClass('edit-toolbar-fullwidth');
         }
@@ -122,147 +80,20 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
           this.insertWYSIWYGToolGroups();
         }
         break;
+      case 'active':
+        break;
       case 'changed':
-        this.$el
-          .find('button.save')
-          .addClass('blue-button')
-          .removeClass('gray-button');
         break;
       case 'saving':
-        this.setLoadingIndicator(true);
         break;
       case 'saved':
-        this.setLoadingIndicator(false);
         break;
       case 'invalid':
-        this.setLoadingIndicator(false);
         break;
     }
   },
 
   /**
-   * Redirects the click.edit-event to the editor DOM element.
-   *
-   * @param jQuery event
-   */
-  onClickInfoLabel: function (event) {
-    event.stopPropagation();
-    event.preventDefault();
-    // Redirects the event to the editor DOM element.
-    this.$field.trigger('click.edit');
-  },
-
-  /**
-   * Controls mouseleave events.
-   *
-   * A mouseleave to the editor doesn't matter; a mouseleave to something else
-   * counts as a mouseleave on the editor itself.
-   *
-   * @param jQuery event
-   */
-  onMouseLeave: function (event) {
-    if (event.relatedTarget !== this.$field[0] && !$.contains(this.$field, event.relatedTarget)) {
-      this.$field.trigger('mouseleave.edit');
-    }
-    event.stopPropagation();
-  },
-
-  /**
-   * Set the model state to 'saving' when the save button is clicked.
-   *
-   * @param jQuery event
-   */
-  onClickSave: function (event) {
-    event.stopPropagation();
-    event.preventDefault();
-    this.model.set('state', 'saving');
-  },
-
-  /**
-   * Sets the model state to candidate when the cancel button is clicked.
-   *
-   * @param jQuery event
-   */
-  onClickClose: function (event) {
-    event.stopPropagation();
-    event.preventDefault();
-    this.model.set('state', 'candidate', { reason: 'cancel' });
-  },
-
-  /**
-   * Indicates in the 'info' toolgroup that we're waiting for a server reponse.
-   *
-   * Prevents flickering loading indicator by only showing it after 0.6 seconds
-   * and if it is shown, only hiding it after another 0.6 seconds.
-   *
-   * @param Boolean enabled
-   *   Whether the loading indicator should be displayed or not.
-   */
-  setLoadingIndicator: function (enabled) {
-    var that = this;
-    if (enabled) {
-      this._loader = setTimeout(function() {
-        that.addClass('info', 'loading');
-        that._loaderVisibleStart = new Date().getTime();
-      }, 600);
-    }
-    else {
-      var currentTime = new Date().getTime();
-      clearTimeout(this._loader);
-      if (this._loaderVisibleStart) {
-        setTimeout(function() {
-          that.removeClass('info', 'loading');
-        }, this._loaderVisibleStart + 600 - currentTime);
-      }
-      this._loader = null;
-      this._loaderVisibleStart = 0;
-    }
-  },
-
-  /**
-   *
-   */
-  startHighlight: function () {
-    // Retrieve the lavel to show for this field.
-    var label = this.model.get('label');
-
-    this.$el
-      .addClass('edit-highlighted')
-      .find('.edit-toolbar')
-      // Append the "info" toolgroup into the toolbar.
-      .append(Drupal.theme('editToolgroup', {
-        classes: 'info edit-animate-only-background-and-padding',
-        buttons: [
-          { label: label, classes: 'blank-button label' }
-        ]
-      }));
-
-    // Animations.
-    var that = this;
-    setTimeout(function () {
-      that.show('info');
-    }, 0);
-  },
-
-  /**
-   *
-   */
-  startEdit: function () {
-    this.$el
-      .addClass('edit-editing')
-      .find('.edit-toolbar')
-      // Append the "ops" toolgroup into the toolbar.
-      .append(Drupal.theme('editToolgroup', {
-        classes: 'ops',
-        buttons: [
-          { label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' },
-          { label: '<span class="close">' + Drupal.t('Close') + '</span>', classes: 'field-close close gray-button' }
-        ]
-      }));
-    this.show('ops');
-  },
-
-  /**
    * Retrieves a setting of the editor-specific Edit UI integration.
    *
    * @param String setting
@@ -279,6 +110,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    * @see EditorDecorationView._pad().
    */
   _pad: function () {
+    return;
     // The whole toolbar must move to the top when the property's DOM element
     // is displayed inline.
     if (this.$field.css('display') === 'inline') {
@@ -300,6 +132,7 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    * @see EditorDecorationView._unpad().
    */
   _unpad: function () {
+    return;
     // Move the toolbar back to its original position.
     var $hf = this.$el.find('.edit-toolbar-heightfaker');
     $hf.css({ bottom: '1px', left: '' });
@@ -314,24 +147,20 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
    */
   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');
   },
 
   /**
@@ -371,43 +200,37 @@ Drupal.edit.FieldToolbarView = Backbone.View.extend({
   },
 
   /**
-   * Shows a toolgroup.
-   *
-   * @param String toolgroup
-   *   A toolgroup name.
-   */
-  show: function (toolgroup) {
-    this._find(toolgroup).removeClass('edit-animate-invisible');
-  },
-
-  /**
-   * Adds classes to a toolgroup.
-   *
-   * @param String toolgroup
-   *   A toolgroup name.
-   */
-  addClass: function (toolgroup, classes) {
-    this._find(toolgroup).addClass(classes);
-  },
-
-  /**
-   * Removes classes from a toolgroup.
+   * Finds a toolgroup.
    *
    * @param String toolgroup
    *   A toolgroup name.
    */
-  removeClass: function (toolgroup, classes) {
-    this._find(toolgroup).removeClass(classes);
+  _find: function (toolgroup) {
+    return this.$el.find('.edit-toolgroup.' + toolgroup);
   },
 
   /**
-   * Finds a toolgroup.
+   * Shows a toolgroup.
    *
    * @param String toolgroup
    *   A toolgroup name.
    */
-  _find: function (toolgroup) {
-    return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup);
+  show: function (toolgroup) {
+    var that = this;
+    var $group = this._find(toolgroup);
+    // Attach a transitionEnd event handler to the toolbar group so that update
+    // events can be triggered after the animations have ended.
+    $group
+      .on(Drupal.edit.util.constants.transitionEnd, function (event) {
+          that.entityModel.trigger('viewChanged', that);
+          $group.off(Drupal.edit.util.constants.transitionEnd);
+      });
+    // The call to remove the class and start the animation must be started in
+    // the next animation frame or the event handler attached above won't be
+    // triggered.
+    window.setTimeout(function () {
+      $group.removeClass('edit-animate-invisible');
+    }, 0);
   }
 });
 
diff --git a/core/modules/editor/js/editor.formattedTextEditor.js b/core/modules/editor/js/editor.formattedTextEditor.js
index 2dcdf92..016310f 100644
--- a/core/modules/editor/js/editor.formattedTextEditor.js
+++ b/core/modules/editor/js/editor.formattedTextEditor.js
@@ -51,8 +51,8 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
     var to = state;
     switch (to) {
       case 'inactive':
+        this.model.off();
         break;
-
       case 'candidate':
         // Detach the text editor when entering the 'candidate' state from one
         // of the states where it could have been attached.
@@ -68,6 +68,7 @@ Drupal.edit.editors.editor = Drupal.edit.EditorView.extend({
         break;
 
       case 'activating':
+        console.log(this.cid);
         // When transformation filters have been been applied to the processed
         // text of this field, then we'll need to load a re-processed version of
         // it without the transformation filters.
-- 
1.7.10.4

