 core/modules/edit/css/edit.css                     |  410 +++++++++++++++
 core/modules/edit/edit.info                        |    6 +
 core/modules/edit/edit.module                      |  149 ++++++
 core/modules/edit/edit.routing.yml                 |   13 +
 core/modules/edit/images/attention.png             |    4 +
 core/modules/edit/images/close.png                 |    4 +
 core/modules/edit/images/icon-edit-active.png      |    3 +
 core/modules/edit/images/icon-edit.png             |    5 +
 core/modules/edit/images/throbber.gif              |    6 +
 core/modules/edit/js/app.js                        |  528 ++++++++++++++++++++
 core/modules/edit/js/backbone.drupalform.js        |  164 ++++++
 core/modules/edit/js/createjs/editable.js          |   43 ++
 .../editingWidgets/drupalcontenteditablewidget.js  |  110 ++++
 .../edit/js/createjs/editingWidgets/formwidget.js  |  150 ++++++
 core/modules/edit/js/createjs/storage.js           |   11 +
 core/modules/edit/js/edit.js                       |   84 ++++
 core/modules/edit/js/models/edit-app-model.js      |   22 +
 core/modules/edit/js/routers/edit-router.js        |   59 +++
 core/modules/edit/js/theme.js                      |  175 +++++++
 core/modules/edit/js/util.js                       |  142 ++++++
 core/modules/edit/js/viejs/EditService.js          |  297 +++++++++++
 core/modules/edit/js/views/menu-view.js            |   82 +++
 core/modules/edit/js/views/modal-view.js           |  107 ++++
 core/modules/edit/js/views/overlay-view.js         |   86 ++++
 .../edit/js/views/propertyeditordecoration-view.js |  324 ++++++++++++
 core/modules/edit/js/views/toolbar-view.js         |  465 +++++++++++++++++
 .../edit/Access/EditEntityFieldAccessCheck.php     |   78 +++
 .../Access/EditEntityFieldAccessCheckInterface.php |   22 +
 .../edit/lib/Drupal/edit/Ajax/BaseCommand.php      |   52 ++
 .../edit/lib/Drupal/edit/Ajax/FieldFormCommand.php |   27 +
 .../lib/Drupal/edit/Ajax/FieldFormSavedCommand.php |   28 ++
 .../edit/Ajax/FieldFormValidationErrorsCommand.php |   28 ++
 ...RenderedWithoutTransformationFiltersCommand.php |   28 ++
 core/modules/edit/lib/Drupal/edit/EditBundle.php   |   37 ++
 .../edit/lib/Drupal/edit/EditController.php        |  102 ++++
 .../edit/lib/Drupal/edit/EditorAttacher.php        |   83 +++
 .../lib/Drupal/edit/EditorAttacherInterface.php    |   24 +
 .../edit/lib/Drupal/edit/EditorSelector.php        |  152 ++++++
 .../lib/Drupal/edit/EditorSelectorInterface.php    |   46 ++
 .../edit/lib/Drupal/edit/Form/EditFieldForm.php    |  150 ++++++
 .../Drupal/edit/Plugin/ProcessedTextEditorBase.php |   29 ++
 .../edit/Plugin/ProcessedTextEditorInterface.php   |   35 ++
 .../edit/Plugin/ProcessedTextEditorManager.php     |   31 ++
 .../lib/Drupal/edit/Tests/EditorSelectionTest.php  |  240 +++++++++
 core/modules/edit/tests/modules/edit_test.info     |    6 +
 core/modules/edit/tests/modules/edit_test.module   |    6 +
 .../processed_text_editor/TestProcessedEditor.php  |   31 ++
 .../field/formatter/TextDefaultFormatter.php       |    3 +
 .../Plugin/field/formatter/TextPlainFormatter.php  |    3 +
 .../formatter/TextSummaryOrTrimmedFormatter.php    |    3 +
 .../field/formatter/TextTrimmedFormatter.php       |    3 +
 51 files changed, 4696 insertions(+)

diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css
new file mode 100644
index 0000000..65c7f38
--- /dev/null
+++ b/core/modules/edit/css/edit.css
@@ -0,0 +1,410 @@
+/**
+ * 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;
+}
+
+
+
+/**
+ * Toolbar.
+ */
+.icon-edit:before {
+  background-image: url("../images/icon-edit.png");
+}
+.icon-edit:active:before,
+.active .icon-edit:before {
+  background-image: url("../images/icon-edit-active.png");
+}
+.toolbar .tray.edit.active {
+  z-index: 340;
+}
+.toolbar .icon-edit.edit-nothing-editable-hidden {
+  display: none;
+}
+/* In-place editing doesn't work in the overlay, so always hide the tab. */
+.overlay-open .toolbar .icon-edit {
+  display: none;
+}
+
+
+
+/**
+ * Edit mode: overlay + candidate editables + editables being edited.
+ *
+ * Note: every class is prefixed with "edit-" to prevent collisions with modules
+ * or themes. In IPE-specific DOM subtrees, this is not necessary.
+ */
+
+#edit_overlay {
+  position: fixed;
+  z-index: 250;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  background-color: rgba(255,255,255,.5);
+  top: 0;
+  left: 0;
+}
+
+/* Editable. */
+.edit-editable {
+  z-index: 300;
+  position: relative;
+}
+.edit-editable:focus {
+  outline: none;
+}
+.edit-field.edit-editable,
+.edit-field.edit-type-direct .edit-editable {
+  box-shadow: 0 0 1px 1px #4d9de9;
+}
+
+/* Highlighted (hovered) editable. */
+.edit-editable.edit-highlighted {
+  min-width: 200px;
+}
+.edit-field.edit-editable.edit-highlighted,
+.edit-form.edit-editable.edit-highlighted,
+.edit-field.edit-type-direct .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-type-direct .edit-editable.edit-highlighted.edit-validation-error {
+  box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
+}
+.edit-form.edit-editable .form-item .error {
+  border: 1px solid #eea0a0;
+}
+
+
+/* Editing (focused) editable. */
+.edit-form.edit-editable.edit-editing,
+.edit-field.edit-type-direct .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 mode: modal.
+ */
+#edit_modal {
+  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;
+}
+
+/* Modal active: prevent user from interacting with toolbar & editables. */
+.edit-form-container.edit-belowoverlay,
+.edit-toolbar-container.edit-belowoverlay,
+.edit-validation-errors.edit-belowoverlay {
+  z-index: 210;
+}
+.edit-editable.edit-belowoverlay {
+  z-index: 200;
+}
+
+
+
+
+/**
+ * Edit mode: type=direct.
+ */
+.edit-validation-errors {
+  z-index: 300;
+  position: relative;
+}
+
+.edit-validation-errors .messages.error {
+  position: absolute;
+  top: 6px;
+  left: -5px;
+  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;
+}
+
+.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
+ */
+
+/* Trick: wrap statically positioned elements in relatively positioned element
+   without changing its location. This allows us to absolutely position the
+   toolbar.
+*/
+.edit-toolbar-container,
+.edit-form-container {
+  position: relative;
+  padding: 0;
+  border: 0;
+  margin: 0;
+  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;
+}
+
+/* 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 used for a directly WYSIWYG editable field that is actively
+   being edited. */
+.edit-type-direct-with-wysiwyg .edit-editing .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-tabs {
+  float: right;
+}
+.edit-toolgroup.wysiwyg {
+  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);
+}
diff --git a/core/modules/edit/edit.info b/core/modules/edit/edit.info
new file mode 100644
index 0000000..5298534
--- /dev/null
+++ b/core/modules/edit/edit.info
@@ -0,0 +1,6 @@
+name = Edit
+description = In-place content editing.
+package = Core
+core = 8.x
+
+dependencies[] = field
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
new file mode 100644
index 0000000..ecb498e
--- /dev/null
+++ b/core/modules/edit/edit.module
@@ -0,0 +1,149 @@
+<?php
+
+/**
+ * @file
+ * Provides in-place content editing functionality for fields.
+ *
+ * The Edit module makes content editable in-place. Rather than having to visit
+ * a separate page to edit content, it may be edited in-place.
+ *
+ * Technically, this module adds classes and data- attributes to fields and
+ * entities, enabling them for in-place editing.
+ */
+
+use Drupal\edit\Form\EditFieldForm;
+
+/**
+ * Implements hook_custom_theme().
+ *
+ * @todo Add an event subscriber to the Ajax system to automatically set the
+ *   base page theme for all Ajax requests, and then remove this one off.
+ */
+function edit_custom_theme() {
+  if (substr(current_path(), 0, 5) === 'edit/') {
+    return ajax_base_page_theme();
+  }
+}
+
+/**
+ * Implements hook_permission().
+ */
+function edit_permission() {
+  return array(
+    'access in-place editing' => array(
+      'title' => t('Access in-place editing'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function edit_toolbar() {
+  $tab['edit'] = array(
+    'tab' => array(
+      'title' => t('Edit'),
+      'href' => '',
+      'html' => FALSE,
+      'attributes' => array(
+        'class' => array('icon', 'icon-edit', 'edit-nothing-editable-hidden'),
+      ),
+    ),
+    'tray' => array(
+      '#attached' => array(
+        'library' => array(
+          array('edit', 'edit'),
+        ),
+      ),
+    ),
+  );
+
+  return $tab;
+}
+
+/**
+ * Implements hook_library().
+ */
+function edit_library_info() {
+  $path = drupal_get_path('module', 'edit');
+  $options = array(
+    'scope' => 'footer',
+    'attributes' => array('defer' => TRUE),
+  );
+  $libraries['edit'] = array(
+    'title' => 'Edit: in-place editing',
+    'website' => 'http://drupal.org/project/edit',
+    'version' => VERSION,
+    'js' => array(
+      // Core.
+      $path . '/js/edit.js' => $options,
+      $path . '/js/app.js' => $options,
+      // Routers.
+      $path . '/js/routers/edit-router.js' => $options,
+      // Models.
+      $path . '/js/models/edit-app-model.js' => $options,
+      // Views.
+      $path . '/js/views/propertyeditordecoration-view.js' => $options,
+      $path . '/js/views/menu-view.js' => $options,
+      $path . '/js/views/modal-view.js' => $options,
+      $path . '/js/views/overlay-view.js' => $options,
+      $path . '/js/views/toolbar-view.js' => $options,
+      // Backbone.sync implementation on top of Drupal forms.
+      $path . '/js/backbone.drupalform.js' => $options,
+      // VIE service.
+      $path . '/js/viejs/EditService.js' => $options,
+      // Create.js subclasses.
+      $path . '/js/createjs/editable.js' => $options,
+      $path . '/js/createjs/storage.js' => $options,
+      $path . '/js/createjs/editingWidgets/formwidget.js' => $options,
+      $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options,
+      // Other.
+      $path . '/js/util.js' => $options,
+      $path . '/js/theme.js' => $options,
+      // Basic settings.
+      array(
+        'data' => array('edit' => array(
+          'fieldFormURL' => url('edit/form/!entity_type/!id/!field_name/!langcode/!view_mode'),
+          'rerenderProcessedTextURL' => url('edit/text/!entity_type/!id/!field_name/!langcode/!view_mode'),
+          'context' => 'body',
+        )),
+        'type' => 'setting',
+      ),
+    ),
+    'css' => array(
+      $path . '/css/edit.css' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'underscore'),
+      array('system', 'backbone'),
+      array('system', 'vie.core'),
+      array('system', 'create.editonly'),
+      array('system', 'jquery.form'),
+      array('system', 'drupal.form'),
+      array('system', 'drupal.ajax'),
+      array('system', 'drupalSettings'),
+    ),
+  );
+
+  return $libraries;
+}
+
+/**
+ * Implements hook_preprocess_HOOK() for field.tpl.php.
+ */
+function edit_preprocess_field(&$variables) {
+  if (user_access('access in-place editing')) {
+    drupal_container()->get('edit.editor.attacher')->preprocessField($variables);
+  }
+}
+
+/**
+ * Form constructor for the field editing form.
+ *
+ * @ingroup forms
+ */
+function edit_field_form(array $form, array &$form_state) {
+  $form_handler = new EditFieldForm();
+  return $form_handler->build($form, $form_state);
+}
diff --git a/core/modules/edit/edit.routing.yml b/core/modules/edit/edit.routing.yml
new file mode 100644
index 0000000..23011e6
--- /dev/null
+++ b/core/modules/edit/edit.routing.yml
@@ -0,0 +1,13 @@
+edit_field_form:
+  pattern: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}'
+  defaults:
+    _controller: '\Drupal\edit\EditController::fieldForm'
+  requirements:
+    _access_edit_entity_field: 'TRUE'
+
+edit_text:
+  pattern: '/edit/text/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}'
+  defaults:
+    _controller: '\Drupal\edit\EditController::getUntransformedText'
+  requirements:
+    _access_edit_entity_field: 'TRUE'
diff --git a/core/modules/edit/images/attention.png b/core/modules/edit/images/attention.png
new file mode 100644
index 0000000..6a35d1d
--- /dev/null
+++ b/core/modules/edit/images/attention.png
@@ -0,0 +1,4 @@
+PNG
+
+   IHDR          *}   `PLTEl՟FZݱ|В8   ʂx՜nϏ۫@EN;[cgUH7   tRNS =g-Su -4_   IDATx^}ʇ @Qzn /g!ul6;	0!f>>Ǐ 	 kν_΁j㜻!0-@>8,i vҤrlWn?B(ijk*yT%Pv s=b_v>@?k&
+a|NciKBFUD^']d`5+5P:    IENDB`
\ No newline at end of file
diff --git a/core/modules/edit/images/close.png b/core/modules/edit/images/close.png
new file mode 100644
index 0000000..e3f98b8
--- /dev/null
+++ b/core/modules/edit/images/close.png
@@ -0,0 +1,4 @@
+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-edit-active.png b/core/modules/edit/images/icon-edit-active.png
new file mode 100644
index 0000000..ad84761
--- /dev/null
+++ b/core/modules/edit/images/icon-edit-active.png
@@ -0,0 +1,3 @@
+PNG
+
+   IHDR         j	   `PLTE   [   tRNS@ P00p`ϟ Dƙ   IDATxe DQ8Ϩ/BDU9xV+D\?x@qWcF8wicS B}?v;Vf.V$JgX=Kضp0XS"iRw\:LL\~;Z5wu 5E)L    IENDB`
\ No newline at end of file
diff --git a/core/modules/edit/images/icon-edit.png b/core/modules/edit/images/icon-edit.png
new file mode 100644
index 0000000..4f0dcc2
--- /dev/null
+++ b/core/modules/edit/images/icon-edit.png
@@ -0,0 +1,5 @@
+PNG
+
+   IHDR         j	   PLTE̻ʪ̡̜ˠ̣̽¼Ƿ   ʨªZ(e   +tRNSϟ `π@`0p0p`0Pϟϟ c   IDATxeW0 {W
+H"ʵ,y{Hpyo?mf,RBRxBvL;&LPJaRb\(Tbn(1wϔJ)ԈkS
+58äT^4 P6c}[i	<ާ'-+HP>K    IENDB`
\ No newline at end of file
diff --git a/core/modules/edit/images/throbber.gif b/core/modules/edit/images/throbber.gif
new file mode 100644
index 0000000..f2603e8
--- /dev/null
+++ b/core/modules/edit/images/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/js/app.js b/core/modules/edit/js/app.js
new file mode 100644
index 0000000..00bba20
--- /dev/null
+++ b/core/modules/edit/js/app.js
@@ -0,0 +1,528 @@
+/**
+ * @file
+ * A Backbone View that is the central app controller.
+ */
+(function ($, _, Backbone, Drupal, VIE) {
+
+"use strict";
+
+  Drupal.edit = Drupal.edit || {};
+  Drupal.edit.EditAppView = Backbone.View.extend({
+    vie: null,
+    domService: null,
+
+    // Configuration for state handling.
+    states: [],
+    activeEditorStates: [],
+    singleEditorStates: [],
+
+    // State.
+    $entityElements: null,
+
+    /**
+     * Implements Backbone Views' initialize() function.
+     */
+    initialize: function() {
+      _.bindAll(this, 'appStateChange', 'acceptEditorStateChange', 'editorStateChange');
+
+      // VIE instance for Edit.
+      this.vie = new VIE();
+      // Use our custom DOM parsing service until RDFa is available.
+      this.vie.use(new this.vie.EditService());
+      this.domService = this.vie.service('edit');
+
+      // Instantiate configuration for state handling.
+      this.states = [
+        null, 'inactive', 'candidate', 'highlighted',
+        'activating', 'active', 'changed', 'saving', 'saved', 'invalid'
+      ];
+      this.activeEditorStates = ['activating', 'active'];
+      this.singleEditorStates = _.union(['highlighted'], this.activeEditorStates);
+
+      this.$entityElements = $([]);
+
+      // Use Create's Storage widget.
+      this.$el.createStorage({
+        vie: this.vie,
+        editableNs: 'createeditable'
+      });
+
+      // Instantiate OverlayView.
+      var overlayView = new Drupal.edit.views.OverlayView({
+        el: (Drupal.theme('editOverlay', {})),
+        model: this.model
+      });
+
+      // Instantiate MenuView.
+      var editMenuView = new Drupal.edit.views.MenuView({
+        el: this.el,
+        model: this.model
+      });
+
+      // When view/edit mode is toggled in the menu, update the editor widgets.
+      this.model.on('change:isViewing', this.appStateChange);
+    },
+
+    /**
+     * Finds editable properties within a given context.
+     *
+     * Finds editable properties, registers them with the app, updates their
+     * state to match the current app state.
+     *
+     * @param $context
+     *   A jQuery-wrapped context DOM element within which will be searched.
+     */
+    findEditableProperties: function($context) {
+      var that = this;
+      var newState = (this.model.get('isViewing')) ? 'inactive' : 'candidate';
+
+      this.domService.findSubjectElements($context).each(function() {
+        var $element = $(this);
+
+        // Ignore editable properties for which we've already set up Create.js.
+        if (that.$entityElements.index($element) !== -1) {
+          return;
+        }
+
+        $element
+          // Instantiate an EditableEntity widget.
+          .createEditable({
+            vie: that.vie,
+            disabled: true,
+            state: 'inactive',
+            acceptStateChange: that.acceptEditorStateChange,
+            statechange: function(event, data) {
+              that.editorStateChange(data.previous, data.current, data.propertyEditor);
+            },
+            decoratePropertyEditor: function(data) {
+              that.decorateEditor(data.propertyEditor);
+            }
+          })
+          // This event is triggered just before Edit removes an EditableEntity
+          // widget, so that we can do proper clean-up.
+          .on('destroyedPropertyEditor.edit', function(event, editor) {
+            that.undecorateEditor(editor);
+            that.$entityElements = that.$entityElements.not($(this));
+
+          })
+          // Transition the new PropertyEditor into the current state.
+          .createEditable('setState', newState);
+
+        // Add this new EditableEntity widget element to the list.
+        that.$entityElements = that.$entityElements.add($element);
+      });
+    },
+
+    /**
+     * Sets the state of PropertyEditor widgets when edit mode begins or ends.
+     *
+     * Should be called whenever EditAppModel's "isViewing" changes.
+     */
+    appStateChange: function() {
+      // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133, https://github.com/bergie/create/issues/140)
+      // We're currently setting the state on EditableEntity widgets instead of
+      // PropertyEditor widgets, because of
+      // https://github.com/bergie/create/issues/133.
+      var newState = (this.model.get('isViewing')) ? 'inactive' : 'candidate';
+      this.$entityElements.each(function() {
+        $(this).createEditable('setState', newState);
+      });
+      // Manage the page's tab indexes.
+      if (newState === 'candidate') {
+        this._manageDocumentFocus();
+        Drupal.edit.setMessage(Drupal.t('In place edit mode is active'), Drupal.t('Page navigation is limited to editable items.'), Drupal.t('Press escape to exit'));
+      }
+      else if (newState === 'inactive') {
+        this._releaseDocumentFocusManagement();
+        Drupal.edit.setMessage(Drupal.t('Edit mode is inactive.'), Drupal.t('Resume normal page navigation'));
+      }
+    },
+
+    /**
+     * Accepts or reject editor (PropertyEditor) state changes.
+     *
+     * This is what ensures that the app is in control of what happens.
+     *
+     * @param from
+     *   The previous state.
+     * @param to
+     *   The new state.
+     * @param predicate
+     *   The predicate of the property for which the state change is happening.
+     * @param context
+     *   The context that is trying to trigger the state change.
+     * @param callback
+     *   The callback function that should receive the state acceptance result.
+     */
+    acceptEditorStateChange: function(from, to, predicate, context, callback) {
+      var accept = true;
+
+      // If the app is in view mode, then reject all state changes except for
+      // those to 'inactive'.
+      if (this.model.get('isViewing')) {
+        if (to !== 'inactive') {
+          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 (_.indexOf(this.states, from) > _.indexOf(this.states, 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 {
+                // The callback will be called from the helper function.
+                this._confirmStopEditing(callback);
+                return;
+              }
+            }
+          }
+        }
+      }
+
+      callback(accept);
+    },
+
+    /**
+     * Asks the user to confirm whether he wants to stop editing via a modal.
+     *
+     * @param acceptCallback
+     *   The callback function as passed to acceptEditorStateChange(). This
+     *   callback function will be called with the user's choice.
+     *
+     * @see acceptEditorStateChange()
+     */
+    _confirmStopEditing: function(acceptCallback) {
+      // Only instantiate if there isn't a modal instance visible yet.
+      if (!this.model.get('activeModal')) {
+        var that = this;
+        var modal = new Drupal.edit.views.ModalView({
+          model: this.model,
+          message: Drupal.t('You have unsaved changes'),
+          buttons: [
+            { action: 'discard', classes: 'gray-button', label: Drupal.t('Discard changes') },
+            { action: 'save', type: 'submit', classes: 'blue-button', label: Drupal.t('Save') }
+          ],
+          callback: function(action) {
+            // The active modal has been removed.
+            that.model.set('activeModal', null);
+            if (action === 'discard') {
+              acceptCallback(true);
+            }
+            else {
+              acceptCallback(false);
+              var editor = that.model.get('activeEditor');
+              editor.options.widget.setState('saving', editor.options.property);
+            }
+          }
+        });
+        this.model.set('activeModal', modal);
+        // The modal will set the activeModal property on the model when rendering
+        // to prevent multiple modals from being instantiated.
+        modal.render();
+      }
+      else {
+        // Reject as there is still an open transition waiting for confirmation.
+        acceptCallback(false);
+      }
+    },
+
+    /**
+     * Reacts to editor (PropertyEditor) state changes; tracks global state.
+     *
+     * @param from
+     *   The previous state.
+     * @param to
+     *   The new state.
+     * @param editor
+     *   The PropertyEditor widget object.
+     */
+    editorStateChange: function(from, to, editor) {
+      // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
+      // Get rid of this once that issue is solved.
+      if (!editor) {
+        return;
+      }
+      else {
+        editor.stateChange(from, to);
+      }
+
+      // Keep track of the highlighted editor in the global state.
+      if (_.indexOf(this.singleEditorStates, to) !== -1 && this.model.get('highlightedEditor') !== editor) {
+        this.model.set('highlightedEditor', editor);
+      }
+      else if (this.model.get('highlightedEditor') === editor && to === 'candidate') {
+        this.model.set('highlightedEditor', null);
+      }
+
+      // Keep track of the active editor in the global state.
+      if (_.indexOf(this.activeEditorStates, to) !== -1 && this.model.get('activeEditor') !== editor) {
+        this.model.set('activeEditor', editor);
+        Drupal.edit.setMessage(Drupal.t('An editor is active'));
+      }
+      else if (this.model.get('activeEditor') === editor && to === 'candidate') {
+        // Discarded if it transitions from a changed state to 'candidate'.
+        if (from === 'changed' || from === 'invalid') {
+          // Retrieve the storage widget from DOM.
+          var createStorageWidget = this.$el.data('createStorage');
+          // Revert changes in the model, this will trigger the direct editable
+          // content to be reset and redrawn.
+          createStorageWidget.revertChanges(editor.options.entity);
+        }
+        this.model.set('activeEditor', null);
+      }
+
+      // Propagate the state change to the decoration and toolbar views.
+      // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
+      // Uncomment this once that issue is solved.
+      // editor.decorationView.stateChange(from, to);
+      // editor.toolbarView.stateChange(from, to);
+    },
+
+    /**
+     * Decorates an editor (PropertyEditor).
+     *
+     * Upon the page load, all appropriate editors are initialized and decorated
+     * (i.e. even before anything of the editing UI becomes visible; even before
+     * edit mode is enabled).
+     *
+     * @param editor
+     *   The PropertyEditor widget object.
+     */
+    decorateEditor: function(editor) {
+      // Toolbars are rendered "on-demand" (highlighting or activating).
+      // They are a sibling element before the editor's DOM element.
+      editor.toolbarView = new Drupal.edit.views.ToolbarView({
+        editor: editor,
+        $storageWidgetEl: this.$el
+      });
+
+      // Decorate the editor's DOM element depending on its state.
+      editor.decorationView = new Drupal.edit.views.PropertyEditorDecorationView({
+        el: editor.element,
+        editor: editor,
+        toolbarId: editor.toolbarView.getId()
+      });
+
+      // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
+      // Get rid of this once that issue is solved.
+      editor.options.widget.element.on('createeditablestatechange', function(event, data) {
+        editor.decorationView.stateChange(data.previous, data.current);
+        editor.toolbarView.stateChange(data.previous, data.current);
+      });
+    },
+
+    /**
+     * Undecorates an editor (PropertyEditor).
+     *
+     * Whenever a property has been updated, the old HTML will be replaced by
+     * the new (re-rendered) HTML. The EditableEntity widget will be destroyed,
+     * as will be the PropertyEditor widget. This method ensures Edit's editor
+     * views also are removed properly.
+     *
+     * @param editor
+     *   The PropertyEditor widget object.
+     */
+    undecorateEditor: function(editor) {
+      editor.toolbarView.undelegateEvents();
+      editor.toolbarView.remove();
+      delete editor.toolbarView;
+      editor.decorationView.undelegateEvents();
+      // Don't call .remove() on the decoration view, because that would remove
+      // a potentially rerendered field.
+      delete editor.decorationView;
+    },
+
+    /**
+     * Makes elements other than the editables unreachable via the tab key.
+     *
+     * @todo refactoring.
+     *
+     * This method is currently overloaded, handling elements of state modeling
+     * and application control. The state of the application is spread between
+     * this view, its model and aspects of the UI widgets in Create.js. In order
+     * to drive focus management from the application state (and have it
+     * influence that state of the application), we need to distall state out
+     * of Create.js components.
+     *
+     * This method introduces behaviors that support accessibility of the edit
+     * application. Although not yet integrated into the application properly,
+     * it does provide us with the opportunity to collect feedback from
+     * users who will interact with edit primarily through keyboard input. We
+     * want this feedback sooner than we can have a refactored application.
+     */
+    _manageDocumentFocus: function () {
+      var editablesSelector = '.edit-candidate.edit-editable';
+      var inputsSelector = 'a:visible, button:visible, input:visible, textarea:visible, select:visible';
+      var $editables = $(editablesSelector)
+        .attr({
+          'tabindex': 0,
+          'role': 'button'
+        });
+      // Instantiate a variable to hold the editable element in the set.
+      var $currentEditable;
+      // We're using simple function scope to manage 'this' for the internal
+      // handler, so save this as that.
+      var that = this;
+      // Turn on focus management.
+      $(document).on('keydown.edit', function (event) {
+        var activeEditor, editableEntity, predicate;
+        // Handle esc key press. Close any active editors.
+        if (event.keyCode === 27) {
+          event.preventDefault();
+          activeEditor = that.model.get('activeEditor');
+          if (activeEditor) {
+            editableEntity = activeEditor.options.widget;
+            predicate = activeEditor.options.property;
+            editableEntity.setState('candidate', predicate, { reason: 'overlay' });
+          }
+          else {
+            $(editablesSelector).trigger('tabOut.edit');
+            // This should move into the state management for the app model.
+            location.hash = "#view";
+            that.model.set('isViewing', true);
+          }
+          return;
+        }
+        // Handle enter or space key presses.
+        if (event.keyCode === 13 || event.keyCode === 32) {
+          if ($currentEditable && $currentEditable.is(editablesSelector)) {
+            $currentEditable.trigger('click');
+            // Squelch additional handlers.
+            event.preventDefault();
+            return;
+          }
+        }
+        // Handle tab key presses.
+        if (event.keyCode === 9) {
+          var context = '';
+          // Include the view mode toggle with the editables selector.
+          var selector = editablesSelector + ', #toolbar-tab-edit';
+          activeEditor = that.model.get('activeEditor');
+          var $confirmDialog = $('#edit_modal');
+          // If the edit modal is active, that is the tabbing context.
+          if ($confirmDialog.length) {
+            context = $confirmDialog;
+            selector = inputsSelector;
+            if (!$currentEditable || $currentEditable.is(editablesSelector)) {
+              $currentEditable = $(selector, context).eq(-1);
+            }
+          }
+          // If an editor is active, then the tabbing context is the editor and
+          // its toolbar.
+          else if (activeEditor) {
+            context = $(activeEditor.$formContainer).add(activeEditor.toolbarView.$el);
+            // Include the view mode toggle with the editables selector.
+            selector = inputsSelector;
+            if (!$currentEditable || $currentEditable.is(editablesSelector)) {
+              $currentEditable = $(selector, context).eq(-1);
+            }
+          }
+          // Otherwise the tabbing context is the list of editable predicates.
+          var $editables = $(selector, context);
+          if (!$currentEditable) {
+            $currentEditable = $editables.eq(-1);
+          }
+          var count = $editables.length - 1;
+          var index = $editables.index($currentEditable);
+          // Navigate backwards.
+          if (event.shiftKey) {
+            // Beginning of the set, loop to the end.
+            if (index === 0) {
+              index = count;
+            }
+            else {
+              index -= 1;
+            }
+          }
+          // Navigate forewards.
+          else {
+            // End of the set, loop to the start.
+            if (index === count) {
+              index = 0;
+            }
+            else {
+              index += 1;
+            }
+          }
+          // Tab out of the current editable.
+          $currentEditable.trigger('tabOut.edit');
+          // Update the current editable.
+          $currentEditable = $editables
+            .eq(index)
+            .focus()
+            .trigger('tabIn.edit');
+          // Squelch additional handlers.
+          event.preventDefault();
+          event.stopPropagation();
+        }
+      });
+      // Set focus on the edit button initially.
+      $('#toolbar-tab-edit').focus();
+    },
+    /**
+     * Removes key management and edit accessibility features from the DOM.
+     */
+    _releaseDocumentFocusManagement: function () {
+      $(document).off('keydown.edit');
+      $('.edit-allowed.edit-field').removeAttr('tabindex role');
+    }
+  });
+
+})(jQuery, _, Backbone, Drupal, VIE);
diff --git a/core/modules/edit/js/backbone.drupalform.js b/core/modules/edit/js/backbone.drupalform.js
new file mode 100644
index 0000000..ba79e76
--- /dev/null
+++ b/core/modules/edit/js/backbone.drupalform.js
@@ -0,0 +1,164 @@
+/**
+ * @file
+ * Backbone.sync implementation for Edit. This is the beating heart.
+ */
+(function (jQuery, Backbone, Drupal) {
+
+"use strict";
+
+Backbone.defaultSync = Backbone.sync;
+Backbone.sync = function(method, model, options) {
+  if (options.editor.options.editorName === 'form') {
+    return Backbone.syncDrupalFormWidget(method, model, options);
+  }
+  else {
+    return Backbone.syncDirect(method, model, options);
+  }
+};
+
+/**
+ * Performs syncing for "form" PredicateEditor widgets.
+ *
+ * Implemented on top of Form API and the AJAX commands framework. Sets up
+ * scoped AJAX command closures specifically for a given PredicateEditor widget
+ * (which contains a pre-existing form). By submitting the form through
+ * Drupal.ajax and leveraging Drupal.ajax' ability to have scoped (per-instance)
+ * command implementations, we are able to update the VIE model, re-render the
+ * form when there are validation errors and ensure no Drupal.ajax memory leaks.
+ *
+ * @see Drupal.edit.util.form
+ */
+Backbone.syncDrupalFormWidget = function(method, model, options) {
+  if (method === 'update') {
+    var predicate = options.editor.options.property;
+
+    var $formContainer = options.editor.$formContainer;
+    var $submit = $formContainer.find('.edit-form-submit');
+    var base = $submit.attr('id');
+
+    // Successfully saved.
+    Drupal.ajax[base].commands.editFieldFormSaved = function(ajax, response, status) {
+      Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
+
+      // Call Backbone.sync's success callback with the rerendered field.
+      var changedAttributes = {};
+      // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216)
+      // Once full JSON-LD support in Drupal core lands, we can ensure that the
+      // models that VIE maintains are properly updated.
+      changedAttributes[predicate] = undefined;
+      changedAttributes[predicate + '/rendered'] = response.data;
+      options.success(changedAttributes);
+    };
+
+    // Unsuccessfully saved; validation errors.
+    Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) {
+      // Call Backbone.sync's error callback with the validation error messages.
+      options.error(response.data);
+    };
+
+    // The edit_field_form 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. 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(jQuery(ajax.element));
+
+      Drupal.ajax.prototype.commands.insert(ajax, {
+        data: response.data,
+        selector: '#' + $formContainer.attr('id') + ' form'
+      });
+
+      // Create a Drupa.ajax instance for the re-rendered ("new") form.
+      var $newSubmit = $formContainer.find('.edit-form-submit');
+      Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit);
+    };
+
+    // Click the form's submit button; the scoped AJAX commands above will
+    // handle the server's response.
+    $submit.trigger('click.edit');
+  }
+};
+
+/**
+* Performs syncing for "direct" PredicateEditor widgets.
+ *
+ * @see Backbone.syncDrupalFormWidget()
+ * @see Drupal.edit.util.form
+ */
+Backbone.syncDirect = function(method, model, options) {
+  if (method === 'update') {
+    var fillAndSubmitForm = function(value) {
+      jQuery('#edit_backstage form')
+        // Fill in the value in any <input> that isn't hidden or a submit button.
+        .find(':input[type!="hidden"][type!="submit"]:not(select)').val(value).end()
+        // Submit the form.
+        .find('.edit-form-submit').trigger('click.edit');
+    };
+    var entity = options.editor.options.entity;
+    var predicate = options.editor.options.property;
+    var value = model.get(predicate);
+
+    // If form doesn't already exist, load it and then submit.
+    if (jQuery('#edit_backstage form').length === 0) {
+      var formOptions = {
+        propertyID: Drupal.edit.util.calcPropertyID(entity, predicate),
+        $editorElement: options.editor.element,
+        nocssjs: true
+      };
+      Drupal.edit.util.form.load(formOptions, function(form, ajax) {
+        // Create a backstage area for storing forms that are hidden from view
+        // (hence "backstage" — since the editing doesn't happen in the form, it
+        // happens "directly" in the content, the form is only used for saving).
+        jQuery(Drupal.theme('editBackstage', { id: 'edit_backstage' })).appendTo('body');
+        // Direct forms are stuffed into #edit_backstage, apparently.
+        jQuery('#edit_backstage').append(form);
+        // Disable the browser's HTML5 validation; we only care about server-
+        // side validation. (Not disabling this will actually cause problems
+        // because browsers don't like to set HTML5 validation errors on hidden
+        // forms.)
+        jQuery('#edit_backstage form').attr('novalidate', true);
+        var $submit = jQuery('#edit_backstage form .edit-form-submit');
+        var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
+
+        // Successfully saved.
+        Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) {
+          Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
+          jQuery('#edit_backstage form').remove();
+
+          // Call Backbone.sync's success callback with the rerendered field.
+          var changedAttributes = {};
+          // @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216)
+          // Once full JSON-LD support in Drupal core lands, we can ensure that the
+          // models that VIE maintains are properly updated.
+          changedAttributes[predicate] = jQuery(response.data).find('.field-item').html();
+          changedAttributes[predicate + '/rendered'] = response.data;
+          options.success(changedAttributes);
+        };
+
+        // Unsuccessfully saved; validation errors.
+        Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) {
+          // Call Backbone.sync's error callback with the validation error messages.
+          options.error(response.data);
+        };
+
+        // 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
+        // "form" editors, but pointless for "direct" editors: the form itself
+        // won't be visible at all anyway! Therefor, we ignore the new form and
+        // we continue to use the existing form.
+        Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) {
+          // no-op
+        };
+
+        fillAndSubmitForm(value);
+      });
+    }
+    else {
+      fillAndSubmitForm(value);
+    }
+  }
+};
+
+})(jQuery, Backbone, Drupal);
diff --git a/core/modules/edit/js/createjs/editable.js b/core/modules/edit/js/createjs/editable.js
new file mode 100644
index 0000000..aac1ed2
--- /dev/null
+++ b/core/modules/edit/js/createjs/editable.js
@@ -0,0 +1,43 @@
+/**
+ * @file
+ * Determines which editor to use based on a class attribute.
+ */
+(function (jQuery, drupalSettings) {
+
+"use strict";
+
+  jQuery.widget('Drupal.createEditable', jQuery.Midgard.midgardEditable, {
+    _create: function() {
+      this.vie = this.options.vie;
+
+      this.options.domService = 'edit';
+      this.options.predicateSelector = '*'; //'.edit-field.edit-allowed';
+
+      this.options.editors.direct = {
+        widget: 'drupalContentEditableWidget',
+        options: {}
+      };
+      this.options.editors['direct-with-wysiwyg'] = {
+        widget: drupalSettings.edit.wysiwygEditorWidgetName,
+        options: {}
+      };
+      this.options.editors.form = {
+        widget: 'drupalFormWidget',
+        options: {}
+      };
+
+      jQuery.Midgard.midgardEditable.prototype._create.call(this);
+    },
+
+    _propertyEditorName: function(data) {
+      if (jQuery(this.element).hasClass('edit-type-direct')) {
+        if (jQuery(this.element).hasClass('edit-type-direct-with-wysiwyg')) {
+          return 'direct-with-wysiwyg';
+        }
+        return 'direct';
+      }
+      return 'form';
+    }
+  });
+
+})(jQuery, drupalSettings);
diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js
new file mode 100644
index 0000000..c773e6e
--- /dev/null
+++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js
@@ -0,0 +1,110 @@
+/**
+ * @file
+ * Override of Create.js' default "base" (plain contentEditable) widget.
+ */
+(function (jQuery, Drupal) {
+
+"use strict";
+
+  jQuery.widget('Drupal.drupalContentEditableWidget', jQuery.Create.editWidget, {
+
+    /**
+     * Implements jQuery UI widget factory's _init() method.
+     *
+     * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142)
+     * Get rid of this once that issue is solved.
+     */
+    _init: function() {},
+
+    /**
+     * Implements Create's _initialize() method.
+     */
+    _initialize: function() {
+      var that = this;
+
+      // Sets the state to 'activated' upon clicking the element.
+      this.element.on("click.edit", function(event) {
+        event.stopPropagation();
+        event.preventDefault();
+        that.options.activated();
+      });
+
+      // Sets the state to 'changed' whenever the content has changed.
+      var before = jQuery.trim(this.element.text());
+      this.element.on('keyup paste', function (event) {
+        if (that.options.disabled) {
+          return;
+        }
+        var current = jQuery.trim(that.element.text());
+        if (before !== current) {
+          before = current;
+          that.options.changed(current);
+        }
+      });
+    },
+
+    /**
+     * Makes this PropertyEditor widget react to state changes.
+     */
+    stateChange: function(from, to) {
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          if (from !== 'inactive') {
+            // Removes the "contenteditable" attribute.
+            this.disable();
+            this._removeValidationErrors();
+            this._cleanUp();
+          }
+          break;
+        case 'highlighted':
+          break;
+        case 'activating':
+          break;
+        case 'active':
+          // Sets the "contenteditable" attribute to "true".
+          this.enable();
+          break;
+        case 'changed':
+          break;
+        case 'saving':
+          this._removeValidationErrors();
+          break;
+        case 'saved':
+          break;
+        case 'invalid':
+          break;
+      }
+    },
+
+    /**
+     * Removes validation errors' markup changes, if any.
+     *
+     * Note: this only needs to happen for type=direct, because for type=direct,
+     * the property DOM element itself is modified; this is not the case for
+     * type=form.
+     */
+    _removeValidationErrors: function() {
+      this.element
+        .removeClass('edit-validation-error')
+        .next('.edit-validation-errors').remove();
+    },
+
+    /**
+     * Cleans up after the widget has been saved.
+     *
+     * Note: this is where the Create.Storage and accompanying Backbone.sync
+     * abstractions "leak" implementation details. That is only the case because
+     * we have to use Drupal's Form API as a transport mechanism. It is
+     * unfortunately a stateful transport mechanism, and that's why we have to
+     * clean it up here. This clean-up is only necessary when canceling the
+     * editing of a property after having attempted to save at least once.
+     */
+    _cleanUp: function() {
+      Drupal.edit.util.form.unajaxifySaving(jQuery('#edit_backstage form .edit-form-submit'));
+      jQuery('#edit_backstage form').remove();
+    }
+  });
+
+})(jQuery, Drupal);
diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js
new file mode 100644
index 0000000..f7c77cd
--- /dev/null
+++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js
@@ -0,0 +1,150 @@
+/**
+ * @file
+ * Form-based Create.js widget for structured content in Drupal.
+ */
+(function ($, Drupal) {
+
+"use strict";
+
+  $.widget('Drupal.drupalFormWidget', $.Create.editWidget, {
+
+    id: null,
+    $formContainer: null,
+
+    /**
+     * Implements jQuery UI widget factory's _init() method.
+     *
+     * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142)
+     * Get rid of this once that issue is solved.
+     */
+    _init: function() {},
+
+    /**
+     * Implements Create's _initialize() method.
+     */
+    _initialize: function() {
+      // Sets the state to 'activating' upon clicking the element.
+      var that = this;
+      this.element.on("click.edit", function(event) {
+        event.stopPropagation();
+        event.preventDefault();
+        that.options.activating();
+      });
+    },
+
+    /**
+     * Makes this PropertyEditor widget react to state changes.
+     */
+    stateChange: function(from, to) {
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          if (from !== 'inactive') {
+            this.disable();
+          }
+          break;
+        case 'highlighted':
+          break;
+        case 'activating':
+          this.enable();
+          break;
+        case 'active':
+          break;
+        case 'changed':
+          break;
+        case 'saving':
+          break;
+        case 'saved':
+          break;
+        case 'invalid':
+          break;
+      }
+    },
+
+    /**
+     * Enables the widget.
+     */
+    enable: function () {
+      var $editorElement = $(this.options.widget.element);
+      var propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property);
+
+      // Generate a DOM-compatible ID for the form container DOM element.
+      this.id = 'edit-form-for-' + propertyID.replace(/\//g, '_');
+
+      // Render form container.
+      this.$formContainer = $(Drupal.theme('editFormContainer', {
+        id: this.id,
+        loadingMsg: Drupal.t('Loading…')}
+      ));
+      this.$formContainer
+        .find('.edit-form')
+        .addClass('edit-editable edit-highlighted edit-editing')
+        .attr('role', 'dialog')
+        .css('background-color', $editorElement.css('background-color'));
+
+      // Insert form container in DOM.
+      if ($editorElement.css('display') === 'inline') {
+        // @todo: POSTPONED_ON(Drupal core, title/author/date as Entity Properties)
+        // This is untested in Drupal 8, because in Drupal 8 we don't yet
+        // have the ability to edit the node title/author/date, because they
+        // haven't been converted into Entity Properties yet, and they're the
+        // only examples in core of "display: inline" properties.
+        this.$formContainer.prependTo($editorElement.offsetParent());
+
+        var pos = $editorElement.position();
+        this.$formContainer.css('left', pos.left).css('top', pos.top);
+      }
+      else {
+        this.$formContainer.insertBefore($editorElement);
+      }
+
+      // Load form, insert it into the form container and attach event handlers.
+      var widget = this;
+      var formOptions = {
+        propertyID: propertyID,
+        $editorElement: $editorElement,
+        nocssjs: false
+      };
+      Drupal.edit.util.form.load(formOptions, function(form, ajax) {
+        Drupal.ajax.prototype.commands.insert(ajax, {
+          data: form,
+          selector: '#' + widget.id + ' .placeholder'
+        });
+
+        var $submit = widget.$formContainer.find('.edit-form-submit');
+        Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
+        widget.$formContainer
+          .on('formUpdated.edit', ':input', function () {
+            // Sets the state to 'changed'.
+            widget.options.changed();
+          })
+          .on('keypress.edit', 'input', function (event) {
+            if (event.keyCode === 13) {
+              return false;
+            }
+          });
+
+        // Sets the state to 'activated'.
+        widget.options.activated();
+      });
+    },
+
+    /**
+     * Disables the widget.
+     */
+    disable: function () {
+      if (this.$formContainer === null) {
+        return;
+      }
+
+      Drupal.edit.util.form.unajaxifySaving(this.$formContainer.find('.edit-form-submit'));
+      this.$formContainer
+        .off('change.edit', ':input')
+        .off('keypress.edit', 'input')
+        .remove();
+      this.$formContainer = null;
+    }
+  });
+
+})(jQuery, Drupal);
diff --git a/core/modules/edit/js/createjs/storage.js b/core/modules/edit/js/createjs/storage.js
new file mode 100644
index 0000000..580ff82
--- /dev/null
+++ b/core/modules/edit/js/createjs/storage.js
@@ -0,0 +1,11 @@
+/**
+ * @file
+ * Subclasses jQuery.Midgard.midgardStorage to have consistent namespaces.
+ */
+(function(jQuery) {
+
+"use strict";
+
+  jQuery.widget('Drupal.createStorage', jQuery.Midgard.midgardStorage, {});
+
+})(jQuery);
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
new file mode 100644
index 0000000..d900977
--- /dev/null
+++ b/core/modules/edit/js/edit.js
@@ -0,0 +1,84 @@
+/**
+ * @file
+ * Behaviors for Edit, including the one that initializes Edit's EditAppView.
+ */
+(function ($, Backbone, Drupal) {
+
+"use strict";
+
+/**
+ * The edit ARIA live message area.
+ *
+ * @todo Eventually the messages area should be converted into a Backbone View
+ * that will respond to changes in the application's model. For the initial
+ * implementation, we will call the Drupal.edit.setMessage method when an aural
+ * message should be read by the user agent.
+ */
+var $messages;
+
+Drupal.edit = Drupal.edit || {};
+
+/**
+ * Attach toggling behavior and in-place editing.
+ */
+Drupal.behaviors.edit = {
+  attach: function(context) {
+    var $context = $(context);
+
+    // Initialize the Edit app.
+    $context.find('#toolbar-tab-edit').once('edit-init', Drupal.edit.init);
+
+    // As soon as there is at least one editable property, show the Edit tab in
+    // the toolbar.
+    if ($context.find('.edit-field.edit-allowed').length) {
+      $('.toolbar .icon-edit.edit-nothing-editable-hidden').removeClass('edit-nothing-editable-hidden');
+    }
+
+    // Find editable properties, make them editable.
+    if (Drupal.edit.app) {
+      Drupal.edit.app.findEditableProperties($context);
+    }
+  }
+};
+
+Drupal.edit.init = function() {
+  // Append a messages element for appending interaction updates for screen
+  // readers.
+  $messages = $(Drupal.theme('editMessageBox')).appendTo($(this).parent());
+  // Instantiate EditAppView, which is the controller of it all. EditAppModel
+  // instance tracks global state (viewing/editing in-place).
+  var appModel = new Drupal.edit.models.EditAppModel();
+  var app = new Drupal.edit.EditAppView({
+    el: $('body'),
+    model: appModel
+  });
+
+  // Instantiate EditRouter.
+  var editRouter = new Drupal.edit.routers.EditRouter({
+    appModel: appModel
+  });
+
+  // Start Backbone's history/route handling.
+  Backbone.history.start();
+
+  // For now, we work with a singleton app, because for Drupal.behaviors to be
+  // able to discover new editable properties that get AJAXed in, it must know
+  // with which app instance they should be associated.
+  Drupal.edit.app = app;
+};
+
+/**
+ * Places the message in the edit ARIA live message area.
+ *
+ * The message will be read by speaking User Agents.
+ *
+ * @param {String} message
+ *   A string to be inserted into the message area.
+ */
+Drupal.edit.setMessage = function(message) {
+  var args = Array.prototype.slice.call(arguments);
+  args.unshift('editMessage');
+  $messages.html(Drupal.theme.apply(this, args));
+};
+
+})(jQuery, Backbone, Drupal);
diff --git a/core/modules/edit/js/models/edit-app-model.js b/core/modules/edit/js/models/edit-app-model.js
new file mode 100644
index 0000000..b6ff36f
--- /dev/null
+++ b/core/modules/edit/js/models/edit-app-model.js
@@ -0,0 +1,22 @@
+/**
+ * @file
+ * A Backbone Model that models the current Edit application state.
+ */
+(function(Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.models = Drupal.edit.models || {};
+Drupal.edit.models.EditAppModel = Backbone.Model.extend({
+  defaults: {
+    // We always begin in view mode.
+    isViewing: true,
+    highlightedEditor: null,
+    activeEditor: null,
+    // Reference to a ModalView-instance if a transition requires confirmation.
+    activeModal: null
+  }
+});
+
+})(Backbone, Drupal);
diff --git a/core/modules/edit/js/routers/edit-router.js b/core/modules/edit/js/routers/edit-router.js
new file mode 100644
index 0000000..d160ad4
--- /dev/null
+++ b/core/modules/edit/js/routers/edit-router.js
@@ -0,0 +1,59 @@
+/**
+ * @file
+ * A Backbone Router enabling URLs to make the user enter edit mode directly.
+ */
+(function(Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.routers = {};
+Drupal.edit.routers.EditRouter = Backbone.Router.extend({
+
+  appModel: null,
+
+  routes: {
+    "edit": "edit",
+    "view": "view",
+    "": "view"
+  },
+
+  initialize: function(options) {
+    this.appModel = options.appModel;
+
+    var that = this;
+    this.appModel.on('change:isViewing', function() {
+      that.navigate(that.appModel.get('isViewing') ? '#view' : '#edit');
+    });
+  },
+
+  edit: function() {
+    this.appModel.set('isViewing', false);
+  },
+
+  view: function(query, page) {
+    var that = this;
+
+    // If there's an active editor, attempt to set its state to 'candidate', and
+    // then act according to the user's choice.
+    var activeEditor = this.appModel.get('activeEditor');
+    if (activeEditor) {
+      var editableEntity = activeEditor.options.widget;
+      var predicate = activeEditor.options.property;
+      editableEntity.setState('candidate', predicate, { reason: 'menu' }, function(accepted) {
+        if (accepted) {
+          that.appModel.set('isViewing', true);
+        }
+        else {
+          that.appModel.set('isViewing', false);
+        }
+      });
+    }
+    // Otherwise, we can switch to view mode directly.
+    else {
+      that.appModel.set('isViewing', true);
+    }
+  }
+});
+
+})(Backbone, Drupal);
diff --git a/core/modules/edit/js/theme.js b/core/modules/edit/js/theme.js
new file mode 100644
index 0000000..80dcbef
--- /dev/null
+++ b/core/modules/edit/js/theme.js
@@ -0,0 +1,175 @@
+/**
+ * @file
+ * Provides overridable theme functions for all of Edit's client-side HTML.
+ */
+(function($, Drupal) {
+
+"use strict";
+
+/**
+ * Theme function for the overlay of the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - None.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editOverlay = function(settings) {
+  var html = '';
+  html += '<div id="edit_overlay" />';
+  return html;
+};
+
+/**
+ * Theme function for a "backstage" for the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - id: the id to apply to the backstage.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editBackstage = function(settings) {
+  var html = '';
+  html += '<div id="' + settings.id + '" />';
+  return html;
+};
+
+/**
+ * Theme function for a modal of the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - None.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editModal = function(settings) {
+  var classes = 'edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast';
+  var html = '';
+  html += '<div id="edit_modal" class="' + classes + '" role="dialog">';
+  html += '  <div class="main"><p></p></div>';
+  html += '  <div class="actions"></div>';
+  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.editToolbarContainer = function(settings) {
+  var html = '';
+  html += '<div id="' + settings.id + '" class="edit-toolbar-container edit-animate-invisible edit-animate-only-visibility">';
+  html += '  <div class="edit-toolbar-heightfaker edit-animate-fast">';
+  html += '    <div class="edit-toolbar primary" />';
+  html += '  </div>';
+  html += '</div>';
+  return html;
+};
+
+/**
+ * Theme function for a toolbar toolgroup of the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - classes: the class of the toolgroup.
+ *   - buttons: @see Drupal.theme.prototype.editButtons().
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editToolgroup = function(settings) {
+  var classes = 'edit-toolgroup edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast';
+  var html = '';
+  html += '<div class="' + classes + ' ' + settings.classes + '">';
+  html += Drupal.theme('editButtons', { buttons: settings.buttons });
+  html += '</div>';
+  return html;
+};
+
+/**
+ * Theme function for buttons of the Edit module.
+ *
+ * Can be used for the buttons both in the toolbar toolgroups and in the modal.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - buttons: an array of objects with the following keys:
+ *     - type: the type of the button (defaults to 'button')
+ *     - classes: the classes of the button.
+ *     - label: the label of the button.
+ *     - action: sets a data-edit-modal-action attribute.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editButtons = function(settings) {
+  var html = '';
+  for (var i = 0; i < settings.buttons.length; i++) {
+    var button = settings.buttons[i];
+    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;
+    html += '</button>';
+  }
+  return html;
+};
+
+/**
+ * Theme function for a form container of the Edit module.
+ *
+ * @param settings
+ *   An object with the following keys:
+ *   - id: the id to apply to the toolbar container.
+ *   - loadingMsg: The message to show while loading.
+ * @return
+ *   The corresponding HTML.
+ */
+Drupal.theme.editFormContainer = function(settings) {
+  var html = '';
+  html += '<div id="' + settings.id + '" class="edit-form-container">';
+  html += '  <div class="edit-form">';
+  html += '    <div class="placeholder">';
+  html +=        settings.loadingMsg;
+  html += '    </div>';
+  html += '  </div>';
+  html += '</div>';
+  return html;
+};
+
+/**
+ * A region to post messages that a screen reading UA will announce.
+ *
+ * @return {String}
+ *   A string representing a DOM fragment.
+ */
+Drupal.theme.editMessageBox = function() {
+  return '<div id="edit-messages" class="element-invisible" role="region" aria-live="polite"></div>';
+};
+
+/**
+ * Wrap message strings in p tags.
+ *
+ * @return {String}
+ *   A string representing a DOM fragment.
+ */
+Drupal.theme.editMessage = function() {
+  var messages = Array.prototype.slice.call(arguments);
+  var output = '';
+  for (var i = 0; i < messages.length; i++) {
+   output += '<p>' + messages[i] + '</p>';
+  }
+  return output;
+};
+
+})(jQuery, Drupal);
diff --git a/core/modules/edit/js/util.js b/core/modules/edit/js/util.js
new file mode 100644
index 0000000..8ed9a2b
--- /dev/null
+++ b/core/modules/edit/js/util.js
@@ -0,0 +1,142 @@
+/**
+ * @file
+ * Provides utility functions for Edit.
+ */
+(function($, Drupal, drupalSettings) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.util = Drupal.edit.util || {};
+
+Drupal.edit.util.constants = {};
+Drupal.edit.util.constants.transitionEnd = "transitionEnd.edit webkitTransitionEnd.edit transitionend.edit msTransitionEnd.edit oTransitionEnd.edit";
+
+Drupal.edit.util.calcPropertyID = function(entity, predicate) {
+  return entity.getSubjectUri() + '/' + predicate;
+};
+
+Drupal.edit.util.buildUrl = function(id, urlFormat) {
+  var parts = id.split('/');
+  return Drupal.formatString(decodeURIComponent(urlFormat), {
+    '!entity_type': parts[0],
+    '!id'         : parts[1],
+    '!field_name' : parts[2],
+    '!langcode'   : parts[3],
+    '!view_mode'  : parts[4]
+  });
+};
+
+/**
+ * Loads rerendered processed text for a given property.
+ *
+ * Leverages Drupal.ajax' ability to have scoped (per-instance) command
+ * implementations to be able to call a callback.
+ *
+ * @param options
+ *   An object with the following keys:
+ *    - $editorElement (required): the PredicateEditor DOM element.
+ *    - propertyID (required): the property ID that uniquely identifies the
+ *      property for which this form will be loaded.
+ *    - callback (required: A callback function that will receive the rerendered
+ *      processed text.
+ */
+Drupal.edit.util.loadRerenderedProcessedText = function(options) {
+  // Create a Drupal.ajax instance to load the form.
+  Drupal.ajax[options.propertyID] = new Drupal.ajax(options.propertyID, options.$editorElement, {
+    url: Drupal.edit.util.buildUrl(options.propertyID, drupalSettings.edit.rerenderProcessedTextURL),
+    event: 'edit-internal.edit',
+    submit: { nocssjs : true },
+    progress: { type : null } // No progress indicator.
+  });
+  // Implement a scoped editFieldRenderedWithoutTransformationFilters AJAX
+  // command: calls the callback.
+  Drupal.ajax[options.propertyID].commands.editFieldRenderedWithoutTransformationFilters = function(ajax, response, status) {
+    options.callback(response.data);
+    // Delete the Drupal.ajax instance that called this very function.
+    delete Drupal.ajax[options.propertyID];
+    options.$editorElement.off('edit-internal.edit');
+  };
+  // This will ensure our scoped editFieldRenderedWithoutTransformationFilters
+  // AJAX command gets called.
+  options.$editorElement.trigger('edit-internal.edit');
+};
+
+Drupal.edit.util.form = {
+  /**
+   * Loads a form, calls a callback to inserts.
+   *
+   * Leverages Drupal.ajax' ability to have scoped (per-instance) command
+   * implementations to be able to call a callback.
+   *
+   * @param options
+   *   An object with the following keys:
+   *    - $editorElement (required): the PredicateEditor DOM element.
+   *    - propertyID (required): the property ID that uniquely identifies the
+   *      property for which this form will be loaded.
+   *    - nocssjs (required): boolean indicating whether no CSS and JS should be
+   *      returned (necessary when the form is invisible to the user).
+   * @param 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
+   *   commands.
+   */
+  load: function(options, callback) {
+    // Create a Drupal.ajax instance to load the form.
+    Drupal.ajax[options.propertyID] = new Drupal.ajax(options.propertyID, options.$editorElement, {
+      url: Drupal.edit.util.buildUrl(options.propertyID, drupalSettings.edit.fieldFormURL),
+      event: 'edit-internal.edit',
+      submit: { nocssjs : options.nocssjs },
+      progress: { type : null } // No progress indicator.
+    });
+    // Implement a scoped editFieldForm AJAX command: calls the callback.
+    Drupal.ajax[options.propertyID].commands.editFieldForm = function(ajax, response, status) {
+      callback(response.data, ajax);
+      // Delete the Drupal.ajax instance that called this very function.
+      delete Drupal.ajax[options.propertyID];
+      options.$editorElement.off('edit-internal.edit');
+    };
+    // This will ensure our scoped editFieldForm AJAX command gets called.
+    options.$editorElement.trigger('edit-internal.edit');
+  },
+
+  /**
+   * Creates a Drupal.ajax instance that is used to save a form.
+   *
+   * @param options
+   *   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
+   *   The key of the Drupal.ajax instance.
+   */
+  ajaxifySaving: function(options, $submit) {
+    // Re-wire the form to handle submit.
+    var element_settings = {
+      url: $submit.closest('form').attr('action'),
+      setClick: true,
+      event: 'click.edit',
+      progress: { type:'throbber' },
+      submit: { nocssjs : options.nocssjs }
+    };
+    var base = $submit.attr('id');
+
+    Drupal.ajax[base] = new Drupal.ajax(base, $submit[0], element_settings);
+
+    return base;
+  },
+
+  /**
+   * Cleans up the Drupal.ajax instance that is used to save the form.
+   *
+   * @param $submit
+   *   The jQuery-wrapped submit DOM element that should be unajaxified.
+   */
+  unajaxifySaving: function($submit) {
+    delete Drupal.ajax[$submit.attr('id')];
+    $submit.off('click.edit');
+  }
+};
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/edit/js/viejs/EditService.js b/core/modules/edit/js/viejs/EditService.js
new file mode 100644
index 0000000..f52a6c0
--- /dev/null
+++ b/core/modules/edit/js/viejs/EditService.js
@@ -0,0 +1,297 @@
+/**
+ * @file
+ * VIE DOM parsing service for Edit.
+ */
+(function(jQuery, _, VIE, Drupal, drupalSettings) {
+
+"use strict";
+
+  VIE.prototype.EditService = function (options) {
+    var defaults = {
+      name: 'edit',
+      subjectSelector: '.edit-field.edit-allowed'
+    };
+    this.options = _.extend({}, defaults, options);
+
+    this.views = [];
+    this.vie = null;
+    this.name = this.options.name;
+  };
+
+  VIE.prototype.EditService.prototype = {
+    load: function (loadable) {
+      var correct = loadable instanceof this.vie.Loadable;
+      if (!correct) {
+        throw new Error('Invalid Loadable passed');
+      }
+
+      var element;
+      if (!loadable.options.element) {
+        if (typeof document === 'undefined') {
+          return loadable.resolve([]);
+        } else {
+          element = drupalSettings.edit.context;
+        }
+      } else {
+        element = loadable.options.element;
+      }
+
+      var entities = this.readEntities(element);
+      loadable.resolve(entities);
+    },
+
+    _getViewForElement:function (element, collectionView) {
+      var viewInstance;
+
+      jQuery.each(this.views, function () {
+        if (jQuery(this.el).get(0) === element.get(0)) {
+          if (collectionView && !this.template) {
+            return true;
+          }
+          viewInstance = this;
+          return false;
+        }
+      });
+      return viewInstance;
+    },
+
+    _registerEntityView:function (entity, element, isNew) {
+      if (!element.length) {
+        return;
+      }
+
+      // Let's only have this overhead for direct types. Form-based editors are
+      // handled in backbone.drupalform.js and the PropertyEditor instance.
+      if (!jQuery(element).hasClass('edit-type-direct')) {
+        return;
+      }
+
+      var service = this;
+      var viewInstance = this._getViewForElement(element);
+      if (viewInstance) {
+        return viewInstance;
+      }
+
+      viewInstance = new this.vie.view.Entity({
+        model:entity,
+        el:element,
+        tagName:element.get(0).nodeName,
+        vie:this.vie,
+        service:this.name
+      });
+
+      this.views.push(viewInstance);
+
+      return viewInstance;
+    },
+
+    save: function(saveable) {
+      var correct = saveable instanceof this.vie.Savable;
+      if (!correct) {
+        throw "Invalid Savable passed";
+      }
+
+      if (!saveable.options.element) {
+        // FIXME: we could find element based on subject
+        throw "Unable to write entity to edit.module-markup, no element given";
+      }
+
+      if (!saveable.options.entity) {
+        throw "Unable to write to edit.module-markup, no entity given";
+      }
+
+      var $element = jQuery(saveable.options.element);
+      this._writeEntity(saveable.options.entity, saveable.options.element);
+      saveable.resolve();
+    },
+
+    _writeEntity:function (entity, element) {
+      var service = this;
+      this.findPredicateElements(this.getElementSubject(element), element, true).each(function () {
+        var predicateElement = jQuery(this);
+        var predicate = service.getElementPredicate(predicateElement);
+        if (!entity.has(predicate)) {
+          return true;
+        }
+
+        var value = entity.get(predicate);
+        if (value && value.isCollection) {
+          // Handled by CollectionViews separately
+          return true;
+        }
+        if (value === service.readElementValue(predicate, predicateElement)) {
+          return true;
+        }
+        // Unlike in the VIE's RdfaService no (re-)mapping needed here.
+        predicateElement.html(value);
+      });
+      return true;
+    },
+
+    // The edit-id data attribute contains the full identifier of
+    // each entity element in the format
+    // `<entity type>:<id>:<field name>:<language code>:<view mode>`.
+    _getID: function (element) {
+      var id = jQuery(element).attr('data-edit-id');
+      if (!id) {
+        id = jQuery(element).closest('[data-edit-id]').attr('data-edit-id');
+      }
+      return id;
+    },
+
+    // Returns the "URI" of an entity of an element in format
+    // `<entity type>/<id>`.
+    getElementSubject: function (element) {
+      return this._getID(element).split(':').slice(0, 2).join('/');
+    },
+
+    // Returns the field name for an element in format
+    // `<field name>/<language code>/<view mode>`.
+    // (Slashes instead of colons because the field name is no namespace.)
+    getElementPredicate: function (element) {
+      if (!this._getID(element)) {
+        throw new Error('Could not find predicate for element');
+      }
+      return this._getID(element).split(':').slice(2, 5).join('/');
+    },
+
+    getElementType: function (element) {
+      return this._getID(element).split(':').slice(0, 1)[0];
+    },
+
+    // Reads all editable entities (currently each Drupal field is considered an
+    // entity, in the future Drupal entities should be mapped to VIE entities)
+    // from DOM and returns the VIE enties it found.
+    readEntities: function (element) {
+      var service = this;
+      var entities = [];
+      var entityElements = jQuery(this.options.subjectSelector, element);
+      entityElements = entityElements.add(jQuery(element).filter(this.options.subjectSelector));
+      entityElements.each(function () {
+        var entity = service._readEntity(jQuery(this));
+        if (entity) {
+          entities.push(entity);
+        }
+      });
+      return entities;
+    },
+
+    // Returns a filled VIE Entity instance for a DOM element. The Entity
+    // is also registered in the VIE entities collection.
+    _readEntity: function (element) {
+      var subject = this.getElementSubject(element);
+      var type = this.getElementType(element);
+      var entity = this._readEntityPredicates(subject, element, false);
+      if (jQuery.isEmptyObject(entity)) {
+        return null;
+      }
+      entity['@subject'] = subject;
+      if (type) {
+        entity['@type'] = this._registerType(type, element);
+      }
+
+      var entityInstance = new this.vie.Entity(entity);
+      entityInstance = this.vie.entities.addOrUpdate(entityInstance, {
+        updateOptions: {
+          silent: true,
+          ignoreChanges: true
+        }
+      });
+
+      this._registerEntityView(entityInstance, element);
+      return entityInstance;
+    },
+
+    _registerType: function (typeId, element) {
+      typeId = '<http://viejs.org/ns/' + typeId + '>';
+      var type = this.vie.types.get(typeId);
+      if (!type) {
+        this.vie.types.add(typeId, []);
+        type = this.vie.types.get(typeId);
+      }
+
+      var predicate = this.getElementPredicate(element);
+      if (type.attributes.get(predicate)) {
+        return type;
+      }
+
+      var label = element.data('edit-field-label');
+      var range = 'Form';
+      if (element.hasClass('edit-type-direct')) {
+        range = 'Direct';
+      }
+      if (element.hasClass('edit-type-direct-with-wysiwyg')) {
+        range = 'Wysiwyg';
+      }
+      type.attributes.add(predicate, [range], 0, 1, {
+        label: element.data('edit-field-label')
+      });
+
+      return type;
+    },
+
+    _readEntityPredicates: function (subject, element, emptyValues) {
+      var entityPredicates = {};
+      var service = this;
+      this.findPredicateElements(subject, element, true).each(function () {
+        var predicateElement = jQuery(this);
+        var predicate = service.getElementPredicate(predicateElement);
+        if (!predicate) {
+          return;
+        }
+        var value = service.readElementValue(predicate, predicateElement);
+        if (value === null && !emptyValues) {
+          return;
+        }
+
+        entityPredicates[predicate] = value;
+        entityPredicates[predicate + '/rendered'] = predicateElement[0].outerHTML;
+      });
+      return entityPredicates;
+    },
+
+    readElementValue : function(predicate, element) {
+      // Unlike in RdfaService there is parsing needed here.
+      if (element.hasClass('edit-type-form')) {
+        return undefined;
+      }
+      else {
+        return jQuery.trim(element.html());
+      }
+    },
+
+    // Subject elements are the DOM elements containing a single or multiple
+    // editable fields.
+    findSubjectElements: function (element) {
+      if (!element) {
+        element = drupalSettings.edit.context;
+      }
+      return jQuery(this.options.subjectSelector, element);
+    },
+
+    // Predicate Elements are the actual DOM elements that users will be able
+    // to edit.
+    findPredicateElements: function (subject, element, allowNestedPredicates, stop) {
+      var predicates = jQuery();
+      // Make sure that element is wrapped by jQuery.
+      var $element = jQuery(element);
+
+      // Form-type predicates
+      predicates = predicates.add($element.filter('.edit-type-form'));
+
+      // Direct-type predicates
+      var direct = $element.filter('.edit-type-direct');
+      predicates = predicates.add(direct.find('.field-item'));
+
+      if (!predicates.length && !stop) {
+        var parentElement = $element.parent(this.options.subjectSelector);
+        if (parentElement.length) {
+          return this.findPredicateElements(subject, parentElement, allowNestedPredicates, true);
+        }
+      }
+
+      return predicates;
+    }
+  };
+
+})(jQuery, _, VIE, Drupal, drupalSettings);
diff --git a/core/modules/edit/js/views/menu-view.js b/core/modules/edit/js/views/menu-view.js
new file mode 100644
index 0000000..ac7c4e4
--- /dev/null
+++ b/core/modules/edit/js/views/menu-view.js
@@ -0,0 +1,82 @@
+/**
+ * @file
+ * A Backbone View that provides the app-level interactive menu.
+ */
+(function($, _, Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.views = Drupal.edit.views || {};
+Drupal.edit.views.MenuView = Backbone.View.extend({
+
+  events: {
+    'click #toolbar-tab-edit': 'editClickHandler'
+  },
+
+  /**
+   * Implements Backbone Views' initialize() function.
+   */
+  initialize: function() {
+    _.bindAll(this, 'stateChange');
+    this.model.on('change:isViewing', this.stateChange);
+    // @todo
+    // Re-implement hook_toolbar and the corresponding JavaScript behaviors
+    // once https://drupal.org/node/1847198 is resolved. The toolbar tray is
+    // necessary when the page request is processed because its render element
+    // has an #attached property with the Edit module library code assigned to
+    // it. Currently a toolbar tab is not passed as a renderable array, so
+    // #attached properties are not processed. The toolbar tray DOM element is
+    // unnecessary right now, so it is removed.
+    this.$el.find('#toolbar-tray-edit').remove();
+    // Respond to clicks on other toolbar tabs. This temporary pending
+    // improvements to the toolbar module.
+    $('#toolbar-administration').on('click.edit', '.bar a:not(#toolbar-tab-edit)', _.bind(function (event) {
+      this.model.set('isViewing', true);
+    }, this));
+    // We have to call stateChange() here because URL fragments are not passed
+    // to the server, thus the wrong anchor may be marked as active.
+    this.stateChange();
+  },
+
+  /**
+   * Listens to app state changes.
+   */
+  stateChange: function() {
+    var isViewing = this.model.get('isViewing');
+    // Toggle the state of the Toolbar Edit tab based on the isViewing state.
+    this.$el.find('#toolbar-tab-edit')
+      .toggleClass('active', !isViewing)
+      .attr('aria-pressed', !isViewing);
+    // Manage the toolbar state until
+    // https://drupal.org/node/1847198 is resolved
+    if (!isViewing) {
+      // Remove the 'toolbar-tray-open' class from the body element.
+      this.$el.removeClass('toolbar-tray-open');
+      // Deactivate any other active tabs and trays.
+      this.$el
+        .find('.bar a', '#toolbar-administration')
+        .not('#toolbar-tab-edit')
+        .add('.tray', '#toolbar-administration')
+        .removeClass('active');
+      // Set the height of the toolbar.
+      if ('toolbar' in Drupal) {
+        Drupal.toolbar.setHeight();
+      }
+    }
+  },
+  /**
+   * Handles clicks on the edit tab of the toolbar.
+   *
+   * @param {Object} event
+   */
+  editClickHandler: function (event) {
+    var isViewing = this.model.get('isViewing');
+    // Toggle the href of the Toolbar Edit tab based on the isViewing state. The
+    // href value should represent to state to be entered.
+    this.$el.find('#toolbar-tab-edit').attr('href', (isViewing) ? '#edit' : '#view');
+    this.model.set('isViewing', !isViewing);
+  }
+});
+
+})(jQuery, _, Backbone, Drupal);
diff --git a/core/modules/edit/js/views/modal-view.js b/core/modules/edit/js/views/modal-view.js
new file mode 100644
index 0000000..2e3b49c
--- /dev/null
+++ b/core/modules/edit/js/views/modal-view.js
@@ -0,0 +1,107 @@
+/**
+ * @file
+ * A Backbone View that provides an interactive modal.
+ */
+(function($, Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.views = Drupal.edit.views || {};
+Drupal.edit.views.ModalView = Backbone.View.extend({
+
+  message: null,
+  buttons: null,
+  callback: null,
+  $elementsToHide: null,
+
+  events: {
+    'click button': 'onButtonClick'
+  },
+
+  /**
+   * Implements Backbone Views' initialize() function.
+   *
+   * @param options
+   *   An object with the following keys:
+   *   - message: a message to show in the modal.
+   *   - buttons: a set of buttons with 'action's defined, ready to be passed to
+   *     Drupal.theme.editButtons().
+   *   - callback: a callback that will receive the 'action' of the clicked
+   *     button.
+   *
+   * @see Drupal.theme.editModal()
+   * @see Drupal.theme.editButtons()
+   */
+  initialize: function(options) {
+    this.message = options.message;
+    this.buttons = options.buttons;
+    this.callback = options.callback;
+  },
+
+  /**
+   * Implements Backbone Views' render() function.
+   */
+  render: function() {
+    // Step 1: move certain UI elements below the overlay.
+    var editor = this.model.get('activeEditor');
+    this.$elementsToHide = $([])
+      .add((editor.element.hasClass('edit-belowoverlay')) ? null : editor.element)
+      .add(editor.toolbarView.$el)
+      .add((editor.options.editorName === 'form') ? editor.$formContainer : editor.element.next('.edit-validation-errors'));
+    this.$elementsToHide.addClass('edit-belowoverlay');
+
+    // Step 2: the modal. When the user makes a choice, the UI elements that
+    // were moved below the overlay will be restored, and the callback will be
+    // called.
+    this.setElement(Drupal.theme('editModal', {}));
+    this.$el.appendTo('body');
+    // Template.
+    this.$('.main p').text(this.message);
+    var $actions = $(Drupal.theme('editButtons', { 'buttons' : this.buttons}));
+    this.$('.actions').append($actions);
+
+    // Step 3; show the modal with an animation.
+    var that = this;
+    setTimeout(function() {
+      that.$el.removeClass('edit-animate-invisible');
+    }, 0);
+
+    Drupal.edit.setMessage(Drupal.t('Confirmation dialog open'));
+  },
+
+  /**
+   * When the user clicks on any of the buttons, the modal should be removed
+   * and the result should be passed to the callback.
+   *
+   * @param event
+   */
+  onButtonClick: function(event) {
+    event.stopPropagation();
+    event.preventDefault();
+
+    // Remove after animation.
+    var that = this;
+    this.$el
+      .addClass('edit-animate-invisible')
+      .on(Drupal.edit.util.constants.transitionEnd, function(e) {
+        that.remove();
+      });
+
+    var action = $(event.target).attr('data-edit-modal-action');
+    return this.callback(action);
+  },
+
+  /**
+   * Overrides Backbone Views' remove() function.
+   */
+  remove: function() {
+    // Move the moved UI elements on top of the overlay again.
+    this.$elementsToHide.removeClass('edit-belowoverlay');
+
+    // Remove the modal itself.
+    this.$el.remove();
+  }
+});
+
+})(jQuery, Backbone, Drupal);
diff --git a/core/modules/edit/js/views/overlay-view.js b/core/modules/edit/js/views/overlay-view.js
new file mode 100644
index 0000000..2113ab8
--- /dev/null
+++ b/core/modules/edit/js/views/overlay-view.js
@@ -0,0 +1,86 @@
+/**
+ * @file
+ * A Backbone View that provides the app-level overlay.
+ *
+ * The overlay sits on top of the existing content, the properties that are
+ * candidates for editing sit on top of the overlay.
+ */
+(function ($, _, Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.views = Drupal.edit.views || {};
+Drupal.edit.views.OverlayView = Backbone.View.extend({
+
+  events: {
+    'click': 'onClick'
+  },
+
+  /**
+   * Implements Backbone Views' initialize() function.
+   */
+  initialize: function (options) {
+    _.bindAll(this, 'stateChange');
+    this.model.on('change:isViewing', this.stateChange);
+    // Add the overlay to the page.
+    this.$el
+      .addClass('edit-animate-slow edit-animate-invisible')
+      .hide()
+      .appendTo('body');
+  },
+
+  /**
+   * Listens to app state changes.
+   */
+  stateChange: function () {
+    if (this.model.get('isViewing')) {
+      this.remove();
+      return;
+    }
+    this.render();
+  },
+
+  /**
+   * Equates clicks anywhere on the overlay to clicking the active editor's (if
+   * any) "close" button.
+   *
+   * @param {Object} event
+   */
+  onClick: function (event) {
+    event.preventDefault();
+    var activeEditor = this.model.get('activeEditor');
+    if (activeEditor) {
+      var editableEntity = activeEditor.options.widget;
+      var predicate = activeEditor.options.property;
+      editableEntity.setState('candidate', predicate, { reason: 'overlay' });
+    }
+    else {
+      this.model.set('isViewing', true);
+    }
+  },
+
+  /**
+   * Reveal the overlay element.
+   */
+  render: function () {
+    this.$el
+      .show()
+      .css('top', $('#navbar').outerHeight())
+      .removeClass('edit-animate-invisible');
+  },
+
+  /**
+   * Hide the overlay element.
+   */
+  remove: function () {
+    var that = this;
+    this.$el
+      .addClass('edit-animate-invisible')
+      .on(Drupal.edit.util.constants.transitionEnd, function (event) {
+        that.$el.hide();
+      });
+  }
+});
+
+})(jQuery, _, Backbone, Drupal);
diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js
new file mode 100644
index 0000000..269259a
--- /dev/null
+++ b/core/modules/edit/js/views/propertyeditordecoration-view.js
@@ -0,0 +1,324 @@
+/**
+ * @file
+ * A Backbone View that decorates a Property Editor widget.
+ *
+ * It listens to state changes of the property editor.
+ */
+(function($, Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.views = Drupal.edit.views || {};
+Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
+
+  editor: null,
+  entity: null,
+  predicate : null,
+  editorName: null,
+  toolbarId: null,
+
+  _widthAttributeIsEmpty: null,
+
+  events: {
+    'mouseenter.edit' : 'onMouseEnter',
+    'mouseleave.edit' : 'onMouseLeave',
+    'tabIn.edit': 'onMouseEnter',
+    'tabOut.edit': 'onMouseLeave'
+  },
+
+  /**
+   * Implements Backbone Views' initialize() function.
+   *
+   * @param options
+   *   An object with the following keys:
+   *   - editor: the editor object with an 'options' object that has these keys:
+   *      * entity: the VIE entity for the property.
+   *      * property: the predicate of the property.
+   *      * editorName: the editor name: 'form', 'direct' or
+   *        'direct-with-wysiwyg'.
+   *      * widget: the parent EditableeEntity widget.
+   *   - toolbarId: the ID attribute of the toolbar as rendered in the DOM.
+   */
+  initialize: function(options) {
+    this.editor = options.editor;
+    this.toolbarId = options.toolbarId;
+
+    this.entity = this.editor.options.entity;
+    this.predicate = this.editor.options.property;
+    this.editorName = this.editor.options.editorName;
+
+    this.$el.css('background-color', this._getBgColor(this.$el));
+  },
+
+  /**
+   * Listens to editor state changes.
+   */
+  stateChange: function(from, to) {
+    switch (to) {
+      case 'inactive':
+        if (from !== null) {
+          this.undecorate();
+        }
+        break;
+      case 'candidate':
+        this.decorate();
+        if (from !== 'inactive') {
+          this.stopHighlight();
+          if (from !== 'highlighted') {
+            this.stopEdit(this.editorName);
+          }
+        }
+        break;
+      case 'highlighted':
+        this.startHighlight();
+        break;
+      case 'activating':
+        // NOTE: this step only exists for the 'form' editor! It is skipped by
+        // the 'direct' and 'direct-with-wysiwyg' editors, because no loading is
+        // necessary.
+        this.prepareEdit(this.editorName);
+        break;
+      case 'active':
+        if (this.editorName !== 'form') {
+          this.prepareEdit(this.editorName);
+        }
+        this.startEdit(this.editorName);
+        break;
+      case 'changed':
+        break;
+      case 'saving':
+        break;
+      case 'saved':
+        break;
+      case 'invalid':
+        break;
+    }
+  },
+
+  /**
+   * Starts hover: transition to 'highlight' state.
+   *
+   * @param event
+   */
+  onMouseEnter: function(event) {
+    var that = this;
+    this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
+      var editableEntity = that.editor.options.widget;
+      editableEntity.setState('highlighted', that.predicate);
+      event.stopPropagation();
+    });
+  },
+
+  /**
+   * Stops hover: back to 'candidate' state.
+   *
+   * @param event
+   */
+  onMouseLeave: function(event) {
+    var that = this;
+    this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
+      var editableEntity = that.editor.options.widget;
+      editableEntity.setState('candidate', that.predicate, { reason: 'mouseleave' });
+      event.stopPropagation();
+    });
+  },
+
+  decorate: function () {
+    this.$el.addClass('edit-animate-fast edit-candidate edit-editable');
+  },
+
+  undecorate: function () {
+    this.$el
+      .removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay');
+  },
+
+  startHighlight: function () {
+    // Animations.
+    var that = this;
+    setTimeout(function() {
+      that.$el.addClass('edit-highlighted');
+    }, 0);
+  },
+
+  stopHighlight: function() {
+    this.$el
+      .removeClass('edit-highlighted');
+  },
+
+  prepareEdit: function(editorName) {
+    this.$el.addClass('edit-editing');
+
+    // While editing, don't show *any* other editors.
+    // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
+    // Revisit this.
+    $('.edit-candidate').not('.edit-editing').removeClass('edit-editable');
+
+    if (editorName === 'form') {
+      this.$el.addClass('edit-belowoverlay');
+    }
+  },
+
+  startEdit: function(editorName) {
+    if (editorName !== 'form') {
+      this._pad();
+    }
+  },
+
+  stopEdit: function(editorName) {
+    this.$el.removeClass('edit-highlighted edit-editing');
+
+    // Make the other editors show up again.
+    // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
+    // Revisit this.
+    $('.edit-candidate').addClass('edit-editable');
+
+    if (editorName === 'form') {
+      this.$el.removeClass('edit-belowoverlay');
+    }
+    else {
+      this._unpad();
+    }
+  },
+
+  _pad: function () {
+    var self = this;
+
+    // Add 5px padding for readability. This means we'll freeze the current
+    // width and *then* add 5px padding, hence ensuring the padding is added "on
+    // the outside".
+    // 1) Freeze the width (if it's not already set); don't use animations.
+    if (this.$el[0].style.width === "") {
+      this._widthAttributeIsEmpty = true;
+      this.$el
+        .addClass('edit-animate-disable-width')
+        .css('width', this.$el.width());
+    }
+
+    // 2) Add padding; use animations.
+    var posProp = this._getPositionProperties(this.$el);
+    setTimeout(function() {
+      // Re-enable width animations (padding changes affect width too!).
+      self.$el.removeClass('edit-animate-disable-width');
+
+      // Pad the editable.
+      self.$el
+      .css({
+        'position': 'relative',
+        'top':  posProp.top  - 5 + 'px',
+        'left': posProp.left - 5 + 'px',
+        'padding-top'   : posProp['padding-top']    + 5 + 'px',
+        'padding-left'  : posProp['padding-left']   + 5 + 'px',
+        'padding-right' : posProp['padding-right']  + 5 + 'px',
+        'padding-bottom': posProp['padding-bottom'] + 5 + 'px',
+        'margin-bottom':  posProp['margin-bottom'] - 10 + 'px'
+      });
+    }, 0);
+  },
+
+  _unpad: function () {
+    var self = this;
+
+    // 1) Set the empty width again.
+    if (this._widthAttributeIsEmpty) {
+      this.$el
+        .addClass('edit-animate-disable-width')
+        .css('width', '');
+    }
+
+    // 2) Remove padding; use animations (these will run simultaneously with)
+    // the fading out of the toolbar as its gets removed).
+    var posProp = this._getPositionProperties(this.$el);
+    setTimeout(function() {
+      // Re-enable width animations (padding changes affect width too!).
+      self.$el.removeClass('edit-animate-disable-width');
+
+      // Unpad the editable.
+      self.$el
+      .css({
+        'position': 'relative',
+        'top':  posProp.top  + 5 + 'px',
+        'left': posProp.left + 5 + 'px',
+        'padding-top'   : posProp['padding-top']    - 5 + 'px',
+        'padding-left'  : posProp['padding-left']   - 5 + 'px',
+        'padding-right' : posProp['padding-right']  - 5 + 'px',
+        'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
+        'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
+      });
+    }, 0);
+  },
+
+  /**
+   * Gets the background color of an element (or the inherited one).
+   *
+   * @param $e
+   *   A DOM element.
+   */
+  _getBgColor: function($e) {
+    var c;
+
+    if ($e === null || $e[0].nodeName === 'HTML') {
+      // Fallback to white.
+      return 'rgb(255, 255, 255)';
+    }
+    c = $e.css('background-color');
+    // TRICKY: edge case for Firefox' "transparent" here; this is a
+    // browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724
+    if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') {
+      return this._getBgColor($e.parent());
+    }
+    return c;
+  },
+
+  /**
+   * Gets the top and left properties of an element and convert extraneous
+   * values and information into numbers ready for subtraction.
+   *
+   * @param $e
+   *   A DOM element.
+   */
+  _getPositionProperties: function($e) {
+    var p,
+        r = {},
+        props = [
+          'top', 'left', 'bottom', 'right',
+          'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
+          'margin-bottom'
+        ];
+
+    var propCount = props.length;
+    for (var i = 0; i < propCount; i++) {
+      p = props[i];
+      r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10);
+    }
+    return r;
+  },
+
+  /**
+   * Replaces blank or 'auto' CSS "position: <value>" values with "0px".
+   *
+   * @param pos
+   *   The value for a CSS position declaration.
+   */
+  _replaceBlankPosition: function(pos) {
+    if (pos === 'auto' || !pos) {
+      pos = '0px';
+    }
+    return pos;
+  },
+
+  /**
+   * Ignores hovering to/from the given closest element, but as soon as a hover
+   * occurs to/from *another* element, then call the given callback.
+   */
+  _ignoreHoveringVia: function(event, closest, callback) {
+    if ($(event.relatedTarget).closest(closest).length > 0) {
+      event.stopPropagation();
+    }
+    else {
+      callback();
+    }
+  }
+});
+
+})(jQuery, Backbone, Drupal);
diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js
new file mode 100644
index 0000000..899b9e3
--- /dev/null
+++ b/core/modules/edit/js/views/toolbar-view.js
@@ -0,0 +1,465 @@
+/**
+ * @file
+ * A Backbone View that provides an interactive toolbar (1 per property editor).
+ *
+ * It listens to state changes of the property editor. It also triggers state
+ * changes in response to user interactions with the toolbar, including saving.
+ */
+(function ($, _, Backbone, Drupal) {
+
+"use strict";
+
+Drupal.edit = Drupal.edit || {};
+Drupal.edit.views = Drupal.edit.views || {};
+Drupal.edit.views.ToolbarView = Backbone.View.extend({
+
+  editor: null,
+  $storageWidgetEl: null,
+
+  entity: null,
+  predicate : null,
+  editorName: 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 Views' initialize() function.
+   *
+   * @param options
+   *   An object with the following keys:
+   *   - editor: the editor object with an 'options' object that has these keys:
+   *      * entity: the VIE entity for the property.
+   *      * property: the predicate of the property.
+   *      * editorName: the editor name: 'form', 'direct' or
+   *        'direct-with-wysiwyg'.
+   *      * element: the jQuery-wrapped editor DOM element
+   *   - $storageWidgetEl: the DOM element on which the Create Storage widget is
+   *     initialized.
+   */
+  initialize: function(options) {
+    this.editor = options.editor;
+    this.$storageWidgetEl = options.$storageWidgetEl;
+
+    this.entity = this.editor.options.entity;
+    this.predicate = this.editor.options.property;
+    this.editorName = this.editor.options.editorName;
+
+    this._loader = null;
+    this._loaderVisibleStart = 0;
+
+    // Generate a DOM-compatible ID for the toolbar DOM element.
+    var propertyID = Drupal.edit.util.calcPropertyID(this.entity, this.predicate);
+    this._id = 'edit-toolbar-for-' + propertyID.replace(/\//g, '_');
+  },
+
+  /**
+   * Listens to editor state changes.
+   */
+  stateChange: function(from, to) {
+    switch (to) {
+      case 'inactive':
+        // Nothing happens in this stage.
+        break;
+      case 'candidate':
+        if (from !== 'inactive') {
+          if (from !== 'highlighted' && this.editorName !== 'form') {
+            this._unpad(this.editorName);
+          }
+          this.remove();
+        }
+        break;
+      case 'highlighted':
+        // As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title).
+        this.render();
+        this.startHighlight();
+        break;
+      case 'activating':
+        this.setLoadingIndicator(true);
+        break;
+      case 'active':
+        this.startEdit(this.editorName);
+        this.setLoadingIndicator(false);
+        if (this.editorName !== 'form') {
+          this._pad(this.editorName);
+        }
+        if (this.editorName === 'direct-with-wysiwyg') {
+          this.insertWYSIWYGToolGroups();
+        }
+        break;
+      case 'changed':
+        this.$el
+          .find('button.save')
+          .addClass('blue-button')
+          .removeClass('gray-button');
+        break;
+      case 'saving':
+        this.setLoadingIndicator(true);
+        this.save();
+        break;
+      case 'saved':
+        this.setLoadingIndicator(false);
+        break;
+      case 'invalid':
+        this.setLoadingIndicator(false);
+        break;
+    }
+  },
+
+  /**
+   * Saves a property.
+   *
+   * This method deals with the complexity of the editor-dependent ways of
+   * inserting updated content and showing validation error messages.
+   *
+   * One might argue that this does not belong in a view. However, there is no
+   * actual "save" logic here, that lives in Backbone.sync. This is just some
+   * glue code, along with the logic for inserting updated content as well as
+   * showing validation error messages, the latter of which is certainly okay.
+   */
+  save: function() {
+    var that = this;
+    var editor = this.editor;
+    var editableEntity = editor.options.widget;
+    var entity = editor.options.entity;
+    var predicate = editor.options.property;
+
+    // Use Create.js' Storage widget to handle saving. (Uses Backbone.sync.)
+    this.$storageWidgetEl.createStorage('saveRemote', entity, {
+      editor: editor,
+
+      // Successfully saved without validation errors.
+      success: function (model) {
+        editableEntity.setState('saved', predicate);
+
+        // Now that the changes to this property have been saved, the saved
+        // attributes are now the "original" attributes.
+        entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes);
+
+        // Get data necessary to rerender property before it is unavailable.
+        var updatedProperty = entity.get(predicate + '/rendered');
+        var $propertyWrapper = editor.element.closest('.edit-field');
+        var $context = $propertyWrapper.parent();
+
+        editableEntity.setState('candidate', predicate);
+        // Unset the property, because it will be parsed again from the DOM, iff
+        // its new value causes it to still be rendered.
+        entity.unset(predicate, { silent: true });
+        entity.unset(predicate + '/rendered', { silent: true });
+        // Trigger event to allow for proper clean-up of editor-specific views.
+        editor.element.trigger('destroyedPropertyEditor.edit', editor);
+
+        // Replace the old content with the new content.
+        $propertyWrapper.replaceWith(updatedProperty);
+        Drupal.attachBehaviors($context);
+      },
+
+      // Save attempted but failed due to validation errors.
+      error: function (validationErrorMessages) {
+        editableEntity.setState('invalid', predicate);
+
+        if (that.editorName === 'form') {
+          editor.$formContainer
+            .find('.edit-form')
+            .addClass('edit-validation-error')
+            .find('form')
+            .prepend(validationErrorMessages);
+        }
+        else {
+          var $errors = $('<div class="edit-validation-errors"></div>')
+            .append(validationErrorMessages);
+          editor.element
+            .addClass('edit-validation-error')
+            .after($errors);
+        }
+      }
+    });
+  },
+
+  /**
+   * When the user clicks the info label, nothing should happen.
+   * @note currently redirects the click.edit-event to the editor DOM element.
+   *
+   * @param event
+   */
+  onClickInfoLabel: function(event) {
+    event.stopPropagation();
+    event.preventDefault();
+    // Redirects the event to the editor DOM element.
+    this.editor.element.trigger('click.edit');
+  },
+
+  /**
+   * A mouseleave to the editor doesn't matter; a mouseleave to something else
+   * counts as a mouseleave on the editor itself.
+   *
+   * @param event
+   */
+  onMouseLeave: function(event) {
+    var el = this.editor.element[0];
+    if (event.relatedTarget != el && !$.contains(el, event.relatedTarget)) {
+      this.editor.element.trigger('mouseleave.edit');
+    }
+    event.stopPropagation();
+  },
+
+  /**
+   * Upon clicking "Save", trigger a custom event to save this property.
+   *
+   * @param event
+   */
+  onClickSave: function(event) {
+    event.stopPropagation();
+    event.preventDefault();
+    this.editor.options.widget.setState('saving', this.predicate);
+  },
+
+  /**
+   * Upon clicking "Close", trigger a custom event to stop editing.
+   *
+   * @param event
+   */
+  onClickClose: function(event) {
+    event.stopPropagation();
+    event.preventDefault();
+    this.editor.options.widget.setState('candidate', this.predicate, { 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 bool 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() {
+    // We get the label to show for this property from VIE's type system.
+    var label = this.predicate;
+    var attributeDef = this.entity.get('@type').attributes.get(this.predicate);
+    if (attributeDef && attributeDef.metadata) {
+      label = attributeDef.metadata.label;
+    }
+
+    this.$el
+      .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');
+  },
+
+  /**
+   * Adjusts the toolbar to accomodate padding on the PropertyEditor widget.
+   *
+   * @see PropertyEditorDecorationView._pad().
+   */
+  _pad: function(editorName) {
+      // The whole toolbar must move to the top when the property's DOM element
+      // is displayed inline.
+      if (this.editor.element.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' });
+      // When using a WYSIWYG editor, the width of the toolbar must match the
+      // width of the editable.
+      if (editorName === 'direct-with-wysiwyg') {
+        $hf.css({ width: this.editor.element.width() + 10 });
+      }
+  },
+
+  /**
+   * Undoes the changes made by _pad().
+   *
+   * @see PropertyEditorDecorationView._unpad().
+   */
+  _unpad: function(editorName) {
+      // Move the toolbar back to its original position.
+      var $hf = this.$el.find('.edit-toolbar-heightfaker');
+      $hf.css({ bottom: '1px', left: '' });
+      // When using a WYSIWYG editor, restore the width of the toolbar.
+      if (editorName === 'direct-with-wysiwyg') {
+        $hf.css({ width: '' });
+      }
+  },
+
+  insertWYSIWYGToolGroups: function() {
+    this.$el
+      .find('.edit-toolbar')
+      .append(Drupal.theme('editToolgroup', {
+        classes: 'wysiwyg-tabs',
+        buttons: []
+      }))
+      .append(Drupal.theme('editToolgroup', {
+        classes: 'wysiwyg',
+        buttons: []
+      }));
+
+    // Animate the toolgroups into visibility.
+    var that = this;
+    setTimeout(function () {
+      that.show('wysiwyg-tabs');
+      that.show('wysiwyg');
+    }, 0);
+  },
+
+  /**
+   * Renders the Toolbar's markup into the DOM.
+   *
+   * Note: depending on whether the 'display' property of the $el for which a
+   * toolbar is being inserted into the DOM, it will be inserted differently.
+   */
+  render: function () {
+    // Render toolbar.
+    this.setElement($(Drupal.theme('editToolbarContainer', {
+      id: this.getId()
+    })));
+
+    // Insert in DOM.
+    if (this.$el.css('display') === 'inline') {
+      this.$el.prependTo(this.editor.element.offsetParent());
+      var pos = this.editor.element.position();
+      this.$el.css('left', pos.left).css('top', pos.top);
+    }
+    else {
+      this.$el.insertBefore(this.editor.element);
+    }
+
+    var that = this;
+    // Animate the toolbar into visibility.
+    setTimeout(function () {
+      that.$el.removeClass('edit-animate-invisible');
+    }, 0);
+  },
+
+  remove: function () {
+    if (!this.$el) {
+      return;
+    }
+
+    // Remove after animation.
+    var that = this;
+    var $el = this.$el;
+    this.$el
+      .addClass('edit-animate-invisible')
+      // Prevent this toolbar from being detected *while* it is being removed.
+      .removeAttr('id')
+      .find('.edit-toolbar .edit-toolgroup')
+      .addClass('edit-animate-invisible')
+      .on(Drupal.edit.util.constants.transitionEnd, function (e) {
+        $el.remove();
+      });
+  },
+
+  /**
+   * Calculates the ID for this toolbar container.
+   *
+   * Only used to make sane hovering behavior possible.
+   *
+   * @return string
+   *   A string that can be used as the ID for this toolbar container.
+   */
+  getId: function() {
+    return this._id;
+  },
+
+  /**
+   * 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.
+   *
+   * @param string toolgroup
+   *   A toolgroup name.
+   */
+  removeClass: function (toolgroup, classes) {
+    this._find(toolgroup).removeClass(classes);
+  },
+
+  /**
+   * Finds a toolgroup.
+   *
+   * @param string toolgroup
+   *   A toolgroup name.
+   */
+  _find: function (toolgroup) {
+    return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup);
+  }
+});
+
+})(jQuery, _, Backbone, Drupal);
diff --git a/core/modules/edit/lib/Drupal/edit/Access/EditEntityFieldAccessCheck.php b/core/modules/edit/lib/Drupal/edit/Access/EditEntityFieldAccessCheck.php
new file mode 100644
index 0000000..82726c3
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Access/EditEntityFieldAccessCheck.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\Access\EditEntityFieldAccessCheck.
+ */
+
+namespace Drupal\edit\Access;
+
+use Drupal\Core\Access\AccessCheckInterface;
+use Symfony\Component\Routing\Route;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Access check for editing entity fields.
+ */
+class EditEntityFieldAccessCheck implements AccessCheckInterface, EditEntityFieldAccessCheckInterface {
+
+  /**
+   * Implements AccessCheckInterface::applies().
+   */
+  public function applies(Route $route) {
+    return array_key_exists('_access_edit_entity_field', $route->getRequirements());
+  }
+
+  /**
+   * Implements AccessCheckInterface::access().
+   */
+  public function access(Route $route, Request $request) {
+    // @todo Request argument validation and object loading should happen
+    //   elsewhere in the request processing pipeline:
+    //   http://drupal.org/node/1798214.
+    $this->validateAndUpcastRequestAttributes($request);
+
+    return $this->accessEditEntityField($request->attributes->get('entity'), $request->attributes->get('field_name'));
+  }
+
+  /**
+   * Implements EntityFieldAccessCheckInterface::accessEditEntityField().
+   */
+  public function accessEditEntityField(EntityInterface $entity, $field_name) {
+    $entity_type = $entity->entityType();
+    // @todo Generalize to all entity types: http://drupal.org/node/1839516.
+    return ($entity_type == 'node' && node_access('update', $entity) && field_access('edit', $field_name, $entity_type, $entity));
+  }
+
+  /**
+   * Validates and upcasts request attributes.
+   */
+  protected function validateAndUpcastRequestAttributes(Request $request) {
+    // Load the entity.
+    if (!is_object($entity = $request->attributes->get('entity'))) {
+      $entity_id = $entity;
+      $entity_type = $request->attributes->get('entity_type');
+      if (!$entity_type || !entity_get_info($entity_type)) {
+        throw new NotFoundHttpException();
+      }
+      $entity = entity_load($entity_type, $entity_id);
+      if (!$entity) {
+        throw new NotFoundHttpException();
+      }
+      $request->attributes->set('entity', $entity);
+    }
+
+    // Validate the field name and language.
+    $field_name = $request->attributes->get('field_name');
+    if (!$field_name || !field_info_instance($entity->entityType(), $field_name, $entity->bundle())) {
+      throw new NotFoundHttpException();
+    }
+    $langcode = $request->attributes->get('langcode');
+    if (!$langcode || (field_valid_language($langcode) !== $langcode)) {
+      throw new NotFoundHttpException();
+    }
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Access/EditEntityFieldAccessCheckInterface.php b/core/modules/edit/lib/Drupal/edit/Access/EditEntityFieldAccessCheckInterface.php
new file mode 100644
index 0000000..fe6d918
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Access/EditEntityFieldAccessCheckInterface.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\Access\EditEntityFieldAccessCheckInterface.
+ */
+
+namespace Drupal\edit\Access;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Access check for editing entity fields.
+ */
+interface EditEntityFieldAccessCheckInterface {
+
+  /**
+   * Checks access to edit the requested field of the requested entity.
+   */
+  public function accessEditEntityField(EntityInterface $entity, $field_name);
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php
new file mode 100644
index 0000000..32d325d
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Ajax/BaseCommand.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\Edit\Ajax\BaseCommand.
+ */
+
+namespace Drupal\edit\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * Base AJAX command that only exists simplify Edit's actual AJAX commands.
+ */
+class BaseCommand implements CommandInterface {
+
+  /**
+   * The name of the command.
+   *
+   * @var string
+   */
+  protected $command;
+
+  /**
+   * The data to pass on to the client side.
+   *
+   * @var string
+   */
+  protected $data;
+
+  /**
+   * Constructs a BaseCommand object.
+   *
+   * @param string $data
+   *   The data to pass on to the client side.
+   */
+  public function __construct($command, $data) {
+    $this->command = $command;
+    $this->data = $data;
+  }
+
+  /**
+   * Implements Drupal\Core\Ajax\CommandInterface:render().
+   */
+  public function render() {
+    return array(
+      'command' => $this->command,
+      'data' => $this->data,
+    );
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php
new file mode 100644
index 0000000..76b01c5
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormCommand.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\edit\Ajax\FieldFormCommand.
+ */
+
+namespace Drupal\edit\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * AJAX command for passing a rendered field form to Edit's JavaScript app.
+ */
+class FieldFormCommand extends BaseCommand {
+
+  /**
+   * Constructs a FieldFormCommand object.
+   *
+   * @param string $data
+   *   The data to pass on to the client side.
+   */
+  public function __construct($data) {
+    parent::__construct('editFieldForm', $data);
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php
new file mode 100644
index 0000000..d2a9630
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormSavedCommand.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\edit\Ajax\FieldFormSavedCommand.
+ */
+
+namespace Drupal\edit\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * AJAX command to indicate a field form was saved without validation errors and
+ * pass the rerendered field to Edit's JavaScript app.
+ */
+class FieldFormSavedCommand extends BaseCommand {
+
+  /**
+   * Constructs a FieldFormSavedCommand object.
+   *
+   * @param string $data
+   *   The data to pass on to the client side.
+   */
+  public function __construct($data) {
+    parent::__construct('editFieldFormSaved', $data);
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php
new file mode 100644
index 0000000..a703725
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldFormValidationErrorsCommand.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\edit\Ajax\FieldFormValidationErrorsCommand.
+ */
+
+namespace Drupal\edit\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * AJAX command to indicate a field form was attempted to be saved but failed
+ * validation and pass the validation errors.
+ */
+class FieldFormValidationErrorsCommand extends BaseCommand {
+
+  /**
+   * Constructs a FieldFormValidationErrorsCommand object.
+   *
+   * @param string $data
+   *   The data to pass on to the client side.
+   */
+  public function __construct($data) {
+    parent::__construct('editFieldFormValidationErrors', $data);
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Ajax/FieldRenderedWithoutTransformationFiltersCommand.php b/core/modules/edit/lib/Drupal/edit/Ajax/FieldRenderedWithoutTransformationFiltersCommand.php
new file mode 100644
index 0000000..53a8826
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Ajax/FieldRenderedWithoutTransformationFiltersCommand.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\edit\Ajax\FieldRenderedWithoutTransformationFiltersCommand.
+ */
+
+namespace Drupal\edit\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * AJAX command to rerender a processed text field without any transformation
+ * filters.
+ */
+class FieldRenderedWithoutTransformationFiltersCommand extends BaseCommand {
+
+  /**
+   * Constructs a FieldRenderedWithoutTransformationFiltersCommand object.
+   *
+   * @param string $data
+   *   The data to pass on to the client side.
+   */
+  public function __construct($data) {
+    parent::__construct('editFieldRenderedWithoutTransformationFilters', $data);
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/EditBundle.php b/core/modules/edit/lib/Drupal/edit/EditBundle.php
new file mode 100644
index 0000000..acfa598
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/EditBundle.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\EditBundle.
+ */
+
+namespace Drupal\edit;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Edit dependency injection container.
+ */
+class EditBundle extends Bundle {
+
+  /**
+   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
+   */
+  public function build(ContainerBuilder $container) {
+    // Register the plugin managers for our plugin types with the dependency injection container.
+    $container->register('plugin.manager.edit.processed_text_editor', 'Drupal\edit\Plugin\ProcessedTextEditorManager');
+
+    $container->register('access_check.edit.entity_field', 'Drupal\edit\Access\EditEntityFieldAccessCheck')
+      ->addTag('access_check');
+
+    $container->register('edit.editor.selector', 'Drupal\edit\EditorSelector')
+      ->addArgument(new Reference('plugin.manager.edit.processed_text_editor'));
+
+    $container->register('edit.editor.attacher', 'Drupal\edit\EditorAttacher')
+      ->addArgument(new Reference('access_check.edit.entity_field'))
+      ->addArgument(new Reference('edit.editor.selector'));
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php
new file mode 100644
index 0000000..56f8f84
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/EditController.php
@@ -0,0 +1,102 @@
+<?php
+
+/**
+ * @file
+ * Contains of \Drupal\edit\EditController.
+ */
+
+namespace Drupal\edit;
+
+use Symfony\Component\DependencyInjection\ContainerAware;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\edit\Ajax\FieldFormCommand;
+use Drupal\edit\Ajax\FieldFormSavedCommand;
+use Drupal\edit\Ajax\FieldFormValidationErrorsCommand;
+use Drupal\edit\Ajax\FieldRenderedWithoutTransformationFiltersCommand;
+
+/**
+ * Returns responses for Edit module routes.
+ */
+class EditController extends ContainerAware {
+
+  /**
+   * Returns a single field edit form as an Ajax response.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity being edited.
+   * @param string $field_name
+   *   The name of the field that is being edited.
+   * @param string $langcode
+   *   The name of the language for which the field is being edited.
+   * @param string $view_mode
+   *   The view mode the field should be rerendered in.
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The Ajax response.
+   */
+  public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view_mode) {
+    $response = new AjaxResponse();
+
+    $form_state = array(
+      'entity' => $entity,
+      'field_name' => $field_name,
+      'langcode' => $langcode,
+      'no_redirect' => TRUE,
+      'build_info' => array('args' => array()),
+    );
+    $form = drupal_build_form('edit_field_form', $form_state);
+
+    if (!empty($form_state['executed'])) {
+      // The form submission took care of saving the updated entity. Return the
+      // updated view of the field.
+      $entity = $form_state['entity'];
+      $output = field_view_field($entity->entityType(), $entity, $field_name, $view_mode, $langcode);
+
+      $response->addCommand(new FieldFormSavedCommand(drupal_render($output)));
+    }
+    else {
+      $response->addCommand(new FieldFormCommand(drupal_render($form)));
+
+      $errors = form_get_errors();
+      if (count($errors)) {
+        $response->addCommand(new FieldFormValidationErrorsCommand(theme('status_messages')));
+      }
+    }
+
+    // When working with a hidden form, we don't want any CSS or JS to be loaded.
+    if (isset($_POST['nocssjs']) && $_POST['nocssjs'] === 'true') {
+      drupal_static_reset('drupal_add_css');
+      drupal_static_reset('drupal_add_js');
+    }
+
+    return $response;
+  }
+
+  /**
+   * Returns an Ajax response to render a text field without transformation filters.
+   *
+   * @param int $entity
+   *   The entity of which a processed text field is being rerendered.
+   * @param string $field_name
+   *   The name of the (processed text) field that that is being rerendered
+   * @param string $langcode
+   *   The name of the language for which the processed text field is being
+   *   rererendered.
+   * @param string $view_mode
+   *   The view mode the processed text field should be rerendered in.
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   The Ajax response.
+   */
+  public function getUntransformedText(EntityInterface $entity, $field_name, $langcode, $view_mode) {
+    $response = new AjaxResponse();
+
+    $output = field_view_field($entity->entityType(), $entity, $field_name, $view_mode, $langcode);
+    $langcode = $output['#language'];
+    // Direct text editing is only supported for single-valued fields.
+    $editable_text = check_markup($output['#items'][0]['value'], $output['#items'][0]['format'], $langcode, FALSE, array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE));
+    $response->addCommand(new FieldRenderedWithoutTransformationFiltersCommand($editable_text));
+
+    return $response;
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/EditorAttacher.php b/core/modules/edit/lib/Drupal/edit/EditorAttacher.php
new file mode 100644
index 0000000..82ea749
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/EditorAttacher.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\EditorAttacher.
+ */
+
+namespace Drupal\edit;
+
+use Drupal\edit\Access\EditEntityFieldAccessCheckInterface;
+
+/**
+ * Adds the HTML attributes needed to enable in-place editing.
+ */
+class EditorAttacher implements EditorAttacherInterface {
+
+  /**
+   * An object that checks if a user has access to edit a given entity field.
+   *
+   * @var \Drupal\edit\Access\EditEntityFieldAccessCheckInterface
+   */
+  protected $accessChecker;
+
+  /**
+   * An object that determines which editor to attach to a given field.
+   *
+   * @var \Drupal\edit\EditorSelectorInterface
+   */
+  protected $editorSelector;
+
+  /**
+   * Constructs a new EditorAttacher.
+   *
+   * @param \Drupal\edit\Access\EditEntityFieldAccessCheckInterface $access_checker
+   *   An object that checks if a user has access to edit a given field.
+   *
+   * @param \Drupal\edit\EditorSelectorInterface $editor_selector
+   *   An object that determines which editor to attach to a given field.
+   */
+  public function __construct(EditEntityFieldAccessCheckInterface $access_checker, EditorSelectorInterface $editor_selector) {
+    $this->accessChecker = $access_checker;
+    $this->editorSelector = $editor_selector;
+  }
+
+  /**
+   * Implements \Drupal\edit\EditorAttacherInterface::preprocessField().
+   */
+  public function preprocessField(&$variables) {
+    $element = $variables['element'];
+    $entity = $element['#object'];
+    $field_name = $element['#field_name'];
+    if (!$this->accessChecker->accessEditEntityField($entity, $field_name)) {
+      return;
+    }
+
+    $instance = field_info_instance($entity->entityType(), $field_name, $entity->bundle());
+    if ($editor = $this->editorSelector->getEditor($element['#formatter'], $instance, $element['#items'])) {
+      // Attributes needed to make the element editable.
+      $variables['attributes']['data-edit-field-label'] = $instance['label'];
+      $variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $field_name . ':' . $element['#language'] . ':' . $element['#view_mode'];
+      $variables['attributes']['aria-label'] = t('Entity @type @id, field @field', array('@type' => $entity->entityType(), '@id' => $entity->id(), '@field' => $instance['label']));
+      $variables['attributes']['class'][] = 'edit-field';
+      $variables['attributes']['class'][] = 'edit-allowed';
+      $variables['attributes']['class'][] = 'edit-type-' . $editor;
+
+      // Additional attributes for WYSIWYG editor integration.
+      if ($editor == 'direct-with-wysiwyg') {
+        $variables['attributes']['class'][] = 'edit-type-direct';
+        $format_id = $element['#items'][0]['format'];
+        $variables['attributes']['data-edit-text-format'] = $format_id;
+        $variables['attributes']['class'][] = $this->textFormatHasTransformationFilters($format_id) ? 'edit-text-with-transformation-filters' : 'edit-text-without-transformation-filters';
+      }
+    }
+  }
+
+  /**
+   * Returns whether the text format has transformation filters.
+   */
+  protected function textFormatHasTransformationFilters($format_id) {
+    return (bool) count(array_intersect(array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE), filter_get_filter_types_by_format($format_id)));
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/EditorAttacherInterface.php b/core/modules/edit/lib/Drupal/edit/EditorAttacherInterface.php
new file mode 100644
index 0000000..4605ee3
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/EditorAttacherInterface.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\EditorAttacherInterface.
+ */
+
+namespace Drupal\edit;
+
+/**
+ * Interface for attaching an in-place editor to an entity field.
+ */
+interface EditorAttacherInterface {
+
+  /**
+   * Adds attributes needed for in-place editing the field.
+   *
+   * This method should be called from a theme('field') preprocessor.
+   *
+   * @see edit_preprocess_field()
+   */
+  public function preprocessField(&$variables);
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/EditorSelector.php b/core/modules/edit/lib/Drupal/edit/EditorSelector.php
new file mode 100644
index 0000000..008448b
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/EditorSelector.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\EditorSelector.
+ */
+
+namespace Drupal\edit;
+
+use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\field\FieldInstance;
+
+/**
+ * Selects an in-place editor for a given entity field.
+ */
+class EditorSelector implements EditorSelectorInterface {
+
+  /**
+   * The manager for processed text editor plugins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $processedTextEditorManager;
+
+  /**
+   * The processed text editor plugin selected.
+   *
+   * @var \Drupal\edit\Plugin\ProcessedTextEditorInterface
+   */
+  protected $processedTextEditorPlugin;
+
+  /**
+   * Constructs a new EditorSelector.
+   *
+   * @param \Drupal\Component\Plugin\PluginManagerInterface $processed_text_editor_manager
+   *   The manager for processed text editor plugins.
+   */
+  public function __construct(PluginManagerInterface $processed_text_editor_manager) {
+    $this->processedTextEditorManager = $processed_text_editor_manager;
+  }
+
+  /**
+   * Implements \Drupal\edit\EditorSelectorInterface::getEditor().
+   */
+  public function getEditor($formatter_type, FieldInstance $instance, array $items) {
+    // Check if the formatter defines an appropriate in-place editor. For
+    // example, text formatters displaying untrimmed text can choose to use the
+    // 'direct' editor. If the formatter doesn't specify, fall back to the
+    // 'form' editor, since that can work for any field. Formatter definitions
+    // can use 'disabled' to explicitly opt out of in-place editing.
+    $formatter_info = field_info_formatter_types($formatter_type);
+    $editor = isset($formatter_info['edit']['editor']) ? $formatter_info['edit']['editor'] : 'form';
+    if ($editor == 'disabled') {
+      return;
+    }
+
+    // The same text formatters can be used for single-valued and multivalued
+    // fields and for processed and unprocessed text, so we can't rely on the
+    // formatter definition for the final determination, because:
+    // - The direct editor does not work for multivalued fields.
+    // - Processed text can benefit from a WYSIWYG editor.
+    // - Empty processed text without an already selected format requires a form
+    //   to select one.
+    // @todo The processed text logic is too coupled to text fields. Figure out
+    //   how to generalize to other textual field types.
+    // @todo All of this might hint at formatter *definitions* not being the
+    //   ideal place for editor specification. Moving the determination to
+    //   something that works with instantiated formatters, not just their
+    //   definitions, could alleviate that, but might come with its own
+    //   challenges.
+    if ($editor == 'direct') {
+      $field = field_info_field($instance['field_name']);
+      if ($field['cardinality'] != 1) {
+        // The direct editor does not work for multivalued fields.
+        $editor = 'form';
+      }
+      elseif (!empty($instance['settings']['text_processing'])) {
+        $format_id = $items[0]['format'];
+        if (isset($format_id)) {
+          $wysiwyg_plugin = $this->getProcessedTextEditorPlugin();
+          if (isset($wysiwyg_plugin) && $wysiwyg_plugin->checkFormatCompatibility($format_id)) {
+            // Yay! Even though the text is processed, there's a WYSIWYG editor
+            // that can work with it.
+            $editor = 'direct-with-wysiwyg';
+          }
+          else {
+            // @todo We might not have to downgrade all the way to 'form'. The
+            //   'direct' editor might be appropriate for some kinds of
+            //   processed text.
+            $editor = 'form';
+          }
+        }
+        else {
+          // If a format is not yet selected, a form is needed to select one.
+          $editor = 'form';
+        }
+      }
+    }
+
+    return $editor;
+  }
+
+  /**
+   * Returns the plugin to use for the 'direct-with-wysiwyg' editor.
+   *
+   * @return \Drupal\edit\Plugin\ProcessedTextEditorInterface
+   *   The editor plugin.
+   *
+   * @todo We currently only support one plugin (the first one returned by the
+   *   manager) for the 'direct-with-wysiwyg' editor on any given page. Enhance
+   *   this to allow different ones per element (e.g., Aloha for one text field
+   *   and CKEditor for another one).
+   *
+   * @todo The terminology here is confusing. 'direct-with-wysiwyg' is one of
+   *   several possible "editor"s for processed text. When using it, we need to
+   *   integrate a particular WYSIWYG editor, which in Create.js is called a
+   *   "PropertyEditor widget", but we're not yet including "widget" in the name
+   *   of ProcessedTextEditorInterface to minimize confusion with Field API
+   *   widgets. So, we're currently refering to these as "plugins", which is
+   *   correct in that it's using Drupal's Plugin API, but less informative than
+   *   naming it "widget" or similar.
+   */
+  protected function getProcessedTextEditorPlugin() {
+    if (!isset($this->processedTextEditorPlugin)) {
+      $definitions = $this->processedTextEditorManager->getDefinitions();
+      if (count($definitions)) {
+        $plugin_ids = array_keys($definitions);
+        $plugin_id = $plugin_ids[0];
+        $this->processedTextEditorPlugin = $this->processedTextEditorManager->createInstance($plugin_id);
+
+        // Add JavaScript required by this plugin, including the setting to
+        // register it with Create.js.
+        // @todo For compatibility with render caching, this should be done
+        //   with #attached rather than drupal_add_*() functions. However, this
+        //   is called from the context of edit_preprocess_field(), and
+        //   #attached does not bubble up from theme preprocess functions:
+        //   http://drupal.org/node/495968#comment-3639542.
+        $definition = $this->processedTextEditorPlugin->getDefinition();
+        if (!empty($definition['library'])) {
+          drupal_add_library($definition['library']['module'], $definition['library']['name']);
+        }
+        $this->processedTextEditorPlugin->addJsSettings();
+        if (!empty($definition['propertyEditorName'])) {
+          drupal_add_js(array('edit' => array(
+            'wysiwygEditorWidgetName' => $definition['propertyEditorName'],
+          )), 'setting');
+        }
+      }
+    }
+    return $this->processedTextEditorPlugin;
+  }
+}
diff --git a/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php b/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php
new file mode 100644
index 0000000..c1ce47a
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/EditorSelectorInterface.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\EditorSelectorInterface.
+ */
+
+namespace Drupal\edit;
+
+use Drupal\field\FieldInstance;
+
+/**
+ * Interface for selecting an in-place editor for a given entity field.
+ */
+interface EditorSelectorInterface {
+
+  /**
+   * Returns the in-place editor to use for a given field instance.
+   *
+   * The Edit module includes three in-place 'editors' that integrate with the
+   * Create.js framework:
+   * - direct: A minimal wrapper to simply setting the HTML5 contenteditable
+   *   attribute on the DOM element.
+   * - direct-with-wysiwyg: Binds a complete WYSIWYG editor (such as Aloha) to
+   *   the DOM element.
+   * - form: Fetches a simplified version of the field's edit form (widget) and
+   *   overlays that over the DOM element.
+   *
+   * These three editors are registered in js/createjs/editable.js. Modules may
+   * register additional editors via the Create.js API.
+   *
+   * This function returns the editor to use for a given field instance.
+   *
+   * @param string $formatter_type
+   *   The field's formatter type name.
+   * @param \Drupal\field\FieldInstance $instance
+   *   The field's instance info.
+   * @param array $items
+   *   The field's item values.
+   *
+   * @return string|NULL
+   *   The editor to use, or NULL to not enable in-place editing.
+   */
+  public function getEditor($formatter_type, FieldInstance $instance, array $items);
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Form/EditFieldForm.php b/core/modules/edit/lib/Drupal/edit/Form/EditFieldForm.php
new file mode 100644
index 0000000..442851a
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Form/EditFieldForm.php
@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\edit\Form\EditFieldForm.
+ */
+
+namespace Drupal\edit\Form;
+
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Builds and process a form for editing a single entity field.
+ */
+class EditFieldForm {
+
+  /**
+   * Builds a form for a single entity field.
+   */
+  public function build(array $form, array &$form_state) {
+    // Add the field form.
+    field_attach_form($form_state['entity']->entityType(), $form_state['entity'], $form, $form_state, $form_state['langcode'], array('field_name' =>  $form_state['field_name']));
+
+    // Add a submit button. Give it a class for easy JavaScript targeting.
+    $form['actions'] = array('#type' => 'actions');
+    $form['actions']['submit'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+      '#attributes' => array('class' => array('edit-form-submit')),
+    );
+
+    // Add validation and submission handlers.
+    $form['#validate'][] = array($this, 'validate');
+    $form['#submit'][] = array($this, 'submit');
+
+    // Simplify it for optimal in-place use.
+    $this->simplify($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * Validates the form.
+   */
+  public function validate(array $form, array &$form_state) {
+    $entity = $this->buildEntity($form, $form_state);
+    field_attach_form_validate($entity->entityType(), $entity, $form, $form_state, array('field_name' =>  $form_state['field_name']));
+  }
+
+  /**
+   * Saves the entity with updated values for the edited field.
+   */
+  public function submit(array $form, array &$form_state) {
+    $form_state['entity'] = $this->buildEntity($form, $form_state);
+    $this->applyDefaultRevisioning($form_state['entity'], $form_state['field_name']);
+    $form_state['entity']->save();
+  }
+
+  /**
+   * Applies the default revision setting to an entity.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface &$entity
+   *   The entity to be updated with the default revision setting.
+   * @param string $field_name
+   *   The name of the field that is being edited. For use in a log message.
+   *
+   * @todo Improve when the node module doesn't have any special cases anymore.
+   */
+  protected function applyDefaultRevisioning(EntityInterface &$entity, $field_name) {
+    $create_revision = FALSE;
+
+    switch ($entity->entityType()) {
+      case 'node':
+        $node_options = variable_get('node_options_' . $entity->bundle(), array('status', 'promote'));
+        $create_revision = in_array('revision', $node_options);
+        break;
+
+      default:
+        $entity_info = entity_get_info($entity->entityType());
+        $create_revision = !empty($entity_info['revision table']);
+        break;
+    }
+
+    $entity->setNewRevision($create_revision);
+    $entity->revision = $create_revision;
+    if ($create_revision) {
+      $instance = field_info_instance($entity->entityType(), $field_name, $entity->bundle());
+      $entity->log = t('Updated the %field-name field through in-place editing.', array('%field-name' => $instance['label']));
+    }
+  }
+
+  /**
+   * Returns a cloned entity containing updated field values.
+   *
+   * Calling code may then validate the returned entity, and if valid, transfer
+   * it back to the form state and save it.
+   */
+  protected function buildEntity(array $form, array &$form_state) {
+    $entity = clone $form_state['entity'];
+
+    // @todo field_attach_submit() only "submits" to the in-memory $entity
+    //   object, not to anywhere persistent. Consider renaming it to minimize
+    //   confusion: http://drupal.org/node/1846648.
+    field_attach_submit($entity->entityType(), $entity, $form, $form_state, array('field_name' =>  $form_state['field_name']));
+
+    return $entity;
+  }
+
+  /**
+   * Simplifies the field edit form for in-place editing.
+   *
+   * This function:
+   * - Hides the field label inside the form, because JavaScript displays it
+   *   outside the form.
+   * - Adjusts textarea elements to fit their content.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   */
+  protected function simplify(array &$form, array &$form_state) {
+    $field_name = $form_state['field_name'];
+    $langcode = $form_state['langcode'];
+
+    $widget_element =& $form[$field_name][$langcode];
+
+    // Hide the field label from displaying within the form, because JavaScript
+    // displays the equivalent label that was provided within an HTML data
+    // attribute of the field's display element outside of the form. Do this for
+    // widgets without child elements (like Option widgets) as well as for ones
+    // with per-delta elements. Skip single checkboxes, because their title is
+    // key to their UI. Also skip widgets with multiple subelements, because in
+    // that case, per-element labeling is informative.
+    $num_children = count(element_children($widget_element));
+    if ($num_children == 0 && $widget_element['#type'] != 'checkbox') {
+      $widget_element['#title_display'] = 'invisible';
+    }
+    if ($num_children == 1 && isset($widget_element[0]['value'])) {
+      // @todo While most widgets name their primary element 'value', not all
+      //   do, so generalize this.
+      $widget_element[0]['value']['#title_display'] = 'invisible';
+    }
+
+    // Adjust textarea elements to fit their content.
+    if (isset($widget_element[0]['value']['#type']) && $widget_element[0]['value']['#type'] == 'textarea') {
+      $lines = count(explode("\n", $widget_element[0]['value']['#default_value']));
+      $widget_element[0]['value']['#rows'] = $lines + 1;
+    }
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php
new file mode 100644
index 0000000..6370011
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\ProcessedTextPropertyBase.
+ */
+
+namespace Drupal\edit\Plugin;
+
+use Drupal\Component\Plugin\PluginBase;
+
+/**
+ * Base class for processed text editor plugins.
+ */
+abstract class ProcessedTextEditorBase extends PluginBase implements ProcessedTextEditorInterface {
+
+  /**
+   * Implements \Drupal\edit\Plugin\ProcessedTextEditorInterface::addJsSettings().
+   */
+  public function addJsSettings() {
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\ProcessedTextEditorInterface::checkFormatCompatibility().
+   */
+  public function checkFormatCompatibility($format_id) {
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorInterface.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorInterface.php
new file mode 100644
index 0000000..1b2efb1
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorInterface.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\ProcessedTextEditorInterface.
+ */
+
+namespace Drupal\edit\Plugin;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Defines an interface for PropertyEditor widgets for processed text fields.
+ *
+ * A PropertyEditor widget is a user-facing interface to edit an entity property
+ * through Create.js.
+ */
+interface ProcessedTextEditorInterface extends PluginInspectionInterface {
+
+  /**
+   * Adds JavaScript settings.
+   */
+  public function addJsSettings();
+
+  /**
+   * Checks if the text editor is compatible with a given text format.
+   *
+   * @param $format_id
+   *   A text format ID.
+   *
+   * @return bool
+   *   TRUE if it is compatible, FALSE otherwise.
+   */
+  public function checkFormatCompatibility($format_id);
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorManager.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorManager.php
new file mode 100644
index 0000000..26ee525
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorManager.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\ProcessedTextEditorManager.
+ */
+
+namespace Drupal\edit\Plugin;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Component\Plugin\Factory\DefaultFactory;
+use Drupal\Core\Plugin\Discovery\AlterDecorator;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\CacheDecorator;
+
+/**
+ * ProcessedTextEditor manager.
+ */
+class ProcessedTextEditorManager extends PluginManagerBase {
+
+  /**
+   * Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
+   */
+  public function __construct() {
+    $this->discovery = new AnnotatedClassDiscovery('edit', 'processed_text_editor');
+    $this->discovery = new AlterDecorator($this->discovery, 'edit_wysiwyg');
+    $this->discovery = new CacheDecorator($this->discovery, 'edit:wysiwyg');
+    $this->factory = new DefaultFactory($this->discovery);
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php
new file mode 100644
index 0000000..1aca55d
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php
@@ -0,0 +1,240 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\edit\Tests\EditorSelectionTest.
+ */
+
+namespace Drupal\edit\Tests;
+
+use Drupal\simpletest\DrupalUnitTestBase;
+use Drupal\edit\Plugin\ProcessedTextEditorManager;
+use Drupal\edit\EditorSelector;
+
+/**
+ * Test in-place field editor selection.
+ */
+class EditorSelectionTest extends DrupalUnitTestBase {
+  var $default_storage = 'field_sql_storage';
+
+  /**
+   * The editor selector object to be tested.
+   *
+   * @var \Drupal\edit\EditorSelectorInterface
+   */
+  protected $editorSelector;
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system', 'field_test', 'field', 'number', 'text', 'edit', 'edit_test');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'In-place field editor selection',
+      'description' => 'Tests in-place field editor selection.',
+      'group' => 'Edit',
+    );
+  }
+
+  /**
+   * Sets the default field storage backend for fields created during tests.
+   */
+  function setUp() {
+    parent::setUp();
+
+    $this->installSchema('system', 'variable');
+    $this->enableModules(array('field', 'field_sql_storage', 'field_test'));
+
+    // Set default storage backend.
+    variable_set('field_storage_default', $this->default_storage);
+
+    // @todo Rather than using the real ProcessedTextEditorManager, which can
+    //   find all text editor plugins in the codebase, create a mock one for
+    //   testing that is populated with only the ones we want to test.
+    $text_editor_manager = new ProcessedTextEditorManager();
+
+    $this->editorSelector = new EditorSelector($text_editor_manager);
+  }
+
+  /**
+   * Creates a field and an instance of it.
+   *
+   * @param string $field_name
+   *   The field name.
+   * @param string $type
+   *   The field type.
+   * @param int $cardinality
+   *   The field's cardinality.
+   * @param string $label
+   *   The field's label (used everywhere: widget label, formatter label).
+   * @param array $instance_settings
+   * @param string $widget_type
+   *   The widget type.
+   * @param array $widget_settings
+   *   The widget settings.
+   * @param string $formatter_type
+   *   The formatter type.
+   * @param array $formatter_settings
+   *   The formatter settings.
+   */
+  function createFieldWithInstance($field_name, $type, $cardinality, $label, $instance_settings, $widget_type, $widget_settings, $formatter_type, $formatter_settings) {
+    $field = $field_name . '_field';
+    $this->$field = array(
+      'field_name' => $field_name,
+      'type' => $type,
+      'cardinality' => $cardinality,
+    );
+    $this->$field_name = field_create_field($this->$field);
+
+    $instance = $field_name . '_instance';
+    $this->$instance = array(
+      'field_name' => $field_name,
+      'entity_type' => 'test_entity',
+      'bundle' => 'test_bundle',
+      'label' => $label,
+      'description' => $label,
+      'weight' => mt_rand(0, 127),
+      'settings' => $instance_settings,
+      'widget' => array(
+        'type' => $widget_type,
+        'label' => $label,
+        'settings' => $widget_settings,
+      ),
+      'display' => array(
+        'default' => array(
+          'label' => 'above',
+          'type' => $formatter_type,
+          'settings' => $formatter_settings
+        ),
+      ),
+    );
+    field_create_instance($this->$instance);
+  }
+
+  /**
+   * Retrieves the FieldInstance object for the given field and returns the
+   * editor that Edit selects.
+   */
+  function getSelectedEditor($items, $field_name, $display = 'default') {
+    $field_instance = field_info_instance('test_entity', $field_name, 'test_bundle');
+    return $this->editorSelector->getEditor($field_instance['display'][$display]['type'], $field_instance, $items);
+  }
+
+  /**
+   * Tests a textual field, without/with text processing, with cardinality 1 and
+   * >1, always without a WYSIWYG editor present.
+   */
+  function testText() {
+    $field_name = 'field_text';
+    $this->createFieldWithInstance(
+      $field_name, 'text', 1, 'Simple text field',
+      // Instance settings.
+      array('text_processing' => 0),
+      // Widget type & settings.
+      'text_textfield',
+      array('size' => 42),
+      // 'default' formatter type & settings.
+      'text_default',
+      array()
+    );
+
+    // Pretend there is an entity with these items for the field.
+    $items = array(array('value' => 'Hello, world!', 'format' => 'full_html'));
+
+    // Editor selection without text processing, with cardinality 1.
+    $this->assertEqual('direct', $this->getSelectedEditor($items, $field_name), "Without text processing, cardinality 1, the 'direct' editor is selected.");
+
+    // Editor selection with text processing, cardinality 1.
+    $this->field_text_instance['settings']['text_processing'] = 1;
+    field_update_instance($this->field_text_instance);
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With text processing, cardinality 1, the 'form' editor is selected.");
+
+    // Editor selection without text processing, cardinality 1 (again).
+    $this->field_text_instance['settings']['text_processing'] = 0;
+    field_update_instance($this->field_text_instance);
+    $this->assertEqual('direct', $this->getSelectedEditor($items, $field_name), "Without text processing again, cardinality 1, the 'direct' editor is selected.");
+
+    // Editor selection without text processing, cardinality >1
+    $this->field_text_field['cardinality'] = 2;
+    field_update_field($this->field_text_field);
+    $items[] = array('value' => 'Hallo, wereld!', 'format' => 'full_html');
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "Without text processing, cardinality >1, the 'form' editor is selected.");
+
+    // Editor selection with text processing, cardinality >1
+    $this->field_text_instance['settings']['text_processing'] = 1;
+    field_update_instance($this->field_text_instance);
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With text processing, cardinality >1, the 'form' editor is selected.");
+  }
+
+  /**
+   * Tests a textual field, with text processing, with cardinality 1 and >1,
+   * always with a ProcessedTextEditor plug-in present, but with varying text
+   * format compatibility.
+   */
+  function testTextWysiwyg() {
+    $field_name = 'field_textarea';
+    $this->createFieldWithInstance(
+      $field_name, 'text', 1, 'Long text field',
+      // Instance settings.
+      array('text_processing' => 1),
+      // Widget type & settings.
+      'text_textarea',
+      array('size' => 42),
+      // 'default' formatter type & settings.
+      'text_default',
+      array()
+    );
+
+    // ProcessedTextEditor plug-in compatible with the full_html text format.
+    state()->set('edit_test.compatible_format', 'full_html');
+
+    // Pretend there is an entity with these items for the field.
+    $items = array(array('value' => 'Hello, world!', 'format' => 'filtered_html'));
+
+    // Editor selection with cardinality 1, without compatible text format.
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "Without cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
+
+    // Editor selection with cardinality 1, with compatible text format.
+    $items[0]['format'] = 'full_html';
+    $this->assertEqual('direct-with-wysiwyg', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the full_html text format, the 'direct-with-wysiwyg' editor is selected.");
+
+    // Editor selection with text processing, cardinality >1
+    $this->field_textarea_field['cardinality'] = 2;
+    field_update_field($this->field_textarea_field);
+    $items[] = array('value' => 'Hallo, wereld!', 'format' => 'full_html');
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected.");
+  }
+
+  /**
+   * Tests a number field, with cardinality 1 and >1.
+   */
+  function testNumber() {
+    $field_name = 'field_nr';
+    $this->createFieldWithInstance(
+      $field_name, 'number_integer', 1, 'Simple number field',
+      // Instance settings.
+      array(),
+      // Widget type & settings.
+      'number',
+      array(),
+      // 'default' formatter type & settings.
+      'number_integer',
+      array()
+    );
+
+    // Pretend there is an entity with these items for the field.
+    $items = array(42, 43);
+
+    // Editor selection with cardinality 1.
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality 1, the 'form' editor is selected.");
+
+    // Editor selection with cardinality >1.
+    $this->field_nr_field['cardinality'] = 2;
+    field_update_field($this->field_nr_field);
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality >1, the 'form' editor is selected.");
+  }
+
+}
diff --git a/core/modules/edit/tests/modules/edit_test.info b/core/modules/edit/tests/modules/edit_test.info
new file mode 100644
index 0000000..4df4a3f
--- /dev/null
+++ b/core/modules/edit/tests/modules/edit_test.info
@@ -0,0 +1,6 @@
+name = Edit test
+description = Support module for the Edit module tests.
+core = 8.x
+package = Testing
+version = VERSION
+hidden = TRUE
diff --git a/core/modules/edit/tests/modules/edit_test.module b/core/modules/edit/tests/modules/edit_test.module
new file mode 100644
index 0000000..d74528d
--- /dev/null
+++ b/core/modules/edit/tests/modules/edit_test.module
@@ -0,0 +1,6 @@
+<?php
+
+/**
+ * @file
+ * Helper module for the Edit tests.
+ */
diff --git a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php
new file mode 100644
index 0000000..3848f30
--- /dev/null
+++ b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\edit_test\Plugin\edit\processed_text_editor\TestProcessedEditor.
+ */
+
+namespace Drupal\edit_test\Plugin\edit\processed_text_editor;
+
+use Drupal\edit\Plugin\ProcessedTextEditorBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Defines a test processed text editor plugin.
+ *
+ * @Plugin(
+ *   id = "test_processed_editor",
+ *   title = @Translation("Test Processed Editor")
+ * )
+ */
+class TestProcessedEditor extends ProcessedTextEditorBase {
+
+  /**
+   * Implements Drupal\edit\Plugin\ProcessedTextEditorBase::checkFormatCompatibility().
+   */
+  function checkFormatCompatibility($format_id) {
+    return state()->get('edit_test.compatible_format') == $format_id;
+  }
+
+}
diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php
index 6b34ba9..79bdbc7 100644
--- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php
+++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextDefaultFormatter.php
@@ -23,6 +23,9 @@
  *     "text",
  *     "text_long",
  *     "text_with_summary"
+ *   },
+ *   edit = {
+ *    "editor" = "direct"
  *   }
  * )
  */
diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php
index 0f7b615..2695351 100644
--- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php
+++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextPlainFormatter.php
@@ -23,6 +23,9 @@
  *     "text",
  *     "text_long",
  *     "text_with_summary"
+ *   },
+ *   edit = {
+ *    "editor" = "direct"
  *   }
  * )
  */
diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php
index 11f0c14..b318da1 100644
--- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php
+++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextSummaryOrTrimmedFormatter.php
@@ -22,6 +22,9 @@
  *   },
  *   settings = {
  *     "trim_length" = "600"
+ *   },
+ *   edit = {
+ *    "editor" = "form"
  *   }
  * )
  */
diff --git a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php
index 349cf63..05a830a 100644
--- a/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php
+++ b/core/modules/field/modules/text/lib/Drupal/text/Plugin/field/formatter/TextTrimmedFormatter.php
@@ -31,6 +31,9 @@
  *   },
  *   settings = {
  *     "trim_length" = "600"
+ *   },
+ *   edit = {
+ *    "editor" = "form"
  *   }
  * )
  */
