 core/modules/editor/editor.module                  |   14 ++
 .../editor/js/createjs/drupalwysiwygwidget.js      |  158 +++++++++++++++++
 .../Drupal/editor/Plugin/edit/editor/Editor.php    |   97 +++++++++++
 .../Drupal/editor/Tests/EditIntegrationTest.php    |  178 ++++++++++++++++++++
 core/modules/editor/tests/modules/editor_test.info |    6 +
 .../editor/tests/modules/editor_test.module        |    6 +
 .../editor_test/Plugin/editor/editor/Unicorn.php   |   58 +++++++
 7 files changed, 517 insertions(+)

diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 7fd50ae..d10c120 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -78,6 +78,20 @@ function editor_library_info() {
       array('system', 'jquery.once'),
     ),
   );
+  $libraries['edit.editor.editor'] = array(
+    'title' => '"Editor" Create.js PropertyEditor widget',
+    'version' => VERSION,
+    'js' => array(
+      $path . '/js/createjs/drupalwysiwygwidget.js' => array(
+        'scope' => 'footer',
+        'attributes' => array('defer' => TRUE),
+      )
+    ),
+    'dependencies' => array(
+      array('edit', 'edit'),
+      array('editor', 'drupal.editor'),
+    ),
+);
 
   return $libraries;
 }
diff --git a/core/modules/editor/js/createjs/drupalwysiwygwidget.js b/core/modules/editor/js/createjs/drupalwysiwygwidget.js
new file mode 100644
index 0000000..36409ec
--- /dev/null
+++ b/core/modules/editor/js/createjs/drupalwysiwygwidget.js
@@ -0,0 +1,158 @@
+/**
+ * @file
+ * Text editor-based Create.js widget for processed text content in Drupal.
+ *
+ * Depends on Editor.module. Works with any (WYSIWYG) editor that implements the
+ * attachTrueWysiwyg(), detach() and onChange() methods.
+ */
+(function (jQuery, Drupal, drupalSettings) {
+
+"use strict";
+
+  jQuery.widget('Drupal.drupalWysiwygWidget', jQuery.Create.editWidget, {
+
+    textFormat: null,
+    textFormatHasTransformations: null,
+    textEditor: null,
+
+    /**
+     * Implements getEditUiIntegration() method.
+     */
+    getEditUiIntegration: function() {
+      return { padding: true, unifiedToolbar: true, fullWidthToolbar: true };
+    },
+
+    /**
+     * 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 propertyID = Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property);
+      var metadata = Drupal.edit.metadataCache[propertyID].custom;
+
+      this.textFormat = drupalSettings.editor.formats[metadata.format];
+      this.textFormatHasTransformations = metadata.formatHasTransformations;
+      this.textEditor = Drupal.editors[this.textFormat.editor];
+
+      this._bindEvents();
+    },
+
+    /**
+     * Binds to events.
+     */
+    _bindEvents: 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.activating();
+      });
+    },
+
+    /**
+     * Makes this PropertyEditor widget react to state changes.
+     */
+    stateChange: function(from, to) {
+      var that = this;
+      switch (to) {
+        case 'inactive':
+          break;
+        case 'candidate':
+          if (from !== 'inactive') {
+            if (from !== 'highlighted') {
+              this.element.attr('contentEditable', 'false');
+              this.textEditor.detach(this.element.get(0), this.textFormat);
+            }
+
+            this._removeValidationErrors();
+            this._cleanUp();
+            this._bindEvents();
+          }
+          break;
+        case 'highlighted':
+          break;
+        case 'activating':
+          // When transformation filters have been been applied to the processed
+          // text of this field, then we'll need to load a re-rendered version of
+          // it without the transformation filters.
+          if (this.textFormatHasTransformations) {
+            Drupal.edit.util.loadRerenderedProcessedText({
+              $editorElement: this.element,
+              propertyID: Drupal.edit.util.calcPropertyID(this.options.entity, this.options.property),
+              callback: function (rerendered) {
+                that.element.html(rerendered);
+                that.options.activated();
+              }
+            });
+          }
+          // When no transformation filters have been applied: start WYSIWYG
+          // editing immediately!
+          else {
+            this.options.activated();
+          }
+          break;
+        case 'active':
+          this.element.attr('contentEditable', 'true');
+          this.textEditor.attachTrueWysiwyg(
+            this.element.get(0),
+            this.textFormat,
+            this.toolbarView.getMainWysiwygToolgroupId(),
+            this.toolbarView.getFloatingWysiwygToolgroupId()
+          );
+
+          // Sets the state to 'changed' whenever the content has changed.
+          this.textEditor.onChange(this.element.get(0), function (html) {
+            that.options.changed(html);
+          });
+          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, drupalSettings);
diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php b/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php
new file mode 100644
index 0000000..b640eac
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Plugin/edit/editor/Editor.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\editor\Plugin\edit\editor\Editor.
+ */
+
+namespace Drupal\editor\Plugin\edit\editor;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\edit\Plugin\EditorInterface;
+use Drupal\field\FieldInstance;
+
+
+/**
+ * Defines the "editor" Create.js PropertyEditor widget.
+ *
+ * @Plugin(
+ *   id = "editor",
+ *   jsClassName = "drupalWysiwygWidget",
+ *   alternativeTo = {"direct"},
+ *   library = {
+ *     "module" = "editor",
+ *     "name" = "edit.editor.editor"
+ *   },
+ *   module = "editor"
+ * )
+ */
+class Editor extends PluginBase implements EditorInterface {
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::isCompatible().
+   */
+  function isCompatible(FieldInstance $instance, array $items) {
+    $field = field_info_field($instance['field_name']);
+
+    // This editor is incompatible with multivalued fields.
+    if ($field['cardinality'] != 1) {
+      return FALSE;
+    }
+    // This editor is compatible with processed ("rich") text fields; but only
+    // if there is a currently active text format, that text format has an
+    // associated editor and that editor supports "true WYSIWYG".
+    elseif (!empty($instance['settings']['text_processing'])) {
+      $format_id = $items[0]['format'];
+      if (isset($format_id) && $editor = editor_load($format_id)) {
+        $definition = drupal_container()->get('plugin.manager.editor')->getDefinition($editor->editor);
+        if ($definition['supports_true_wysiwyg'] === TRUE) {
+          return TRUE;
+        }
+      }
+
+      return FALSE;
+    }
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getMetadata().
+   */
+  function getMetadata(FieldInstance $instance, array $items) {
+    $format_id = $items[0]['format'];
+    $metadata['format'] = $format_id;
+    $metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id);
+    return $metadata;
+  }
+
+  /**
+   * 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)));
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getDynamicAttachments().
+   */
+  function getDynamicAttachments() {
+    global $user;
+
+    $user_formats = filter_formats($user);
+    $definitions = drupal_container()->get('plugin.manager.editor')->getDefinitions();
+
+    // Filter the current user's text to those that support "true WYSIWYG".
+    $formats = array();
+    foreach ($user_formats as $format_id => $format) {
+      $editor = editor_load($format_id);
+      if ($editor && isset($definitions[$editor->editor]) && $definitions[$editor->editor]['supports_true_wysiwyg'] === TRUE) {
+        $formats[$format_id] = $format;
+      }
+    }
+
+    return editor_get_attachments($formats);
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php
new file mode 100644
index 0000000..ddc8fda
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditIntegrationTest.php
@@ -0,0 +1,178 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\editor\Tests\EditorIntegrationTest.
+ */
+
+namespace Drupal\editor\Tests;
+
+use Drupal\edit\EditorSelector;
+use Drupal\edit\MetadataGenerator;
+use Drupal\edit\Plugin\EditorManager;
+use Drupal\edit\Tests\EditTestBase;
+use Drupal\edit_test\MockEditEntityFieldAccessCheck;
+
+/**
+ * Ensure Edit module uses Editor module's "true WYSIWYG" support.
+ */
+class EditIntegrationTest extends EditTestBase {
+
+  /**
+   * The manager for editor (Create.js PropertyEditor widget) plug-ins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $editorManager;
+
+  /**
+   * The metadata generator object to be tested.
+   *
+   * @var \Drupal\edit\MetadataGeneratorInterface.php
+   */
+  protected $metadataGenerator;
+
+  /**
+   * The editor selector object to be used by the metadata generator object.
+   *
+   * @var \Drupal\edit\EditorSelectorInterface
+   */
+  protected $editorSelector;
+
+  /**
+   * The access checker object to be used by the metadata generator object.
+   *
+   * @var \Drupal\edit\Access\EditEntityFieldAccessCheckInterface
+   */
+  protected $accessChecker;
+
+  /**
+   * The name of the field ued for tests.
+   *
+   * @var string
+   */
+  protected $field_name;
+
+  public static function getInfo() {
+    return array(
+      'name' => 'In-place text editors (Edit module integration)',
+      'description' => 'Ensure Edit module uses Editor module\'s "true WYSIWYG" support.',
+      'group' => 'Text Editor',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+
+    // Install the Filter and Text Editor modules.
+    $this->enableModules(array('filter', 'editor'));
+
+    // Enable the Text Editor Test module.
+    $this->enableModules(array('editor_test'), FALSE);
+
+    // Create a field.
+    $this->field_name = 'field_textarea';
+    $this->createFieldWithInstance(
+      $this->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()
+    );
+
+    // Create text format.
+    $full_html_format = array(
+      'format' => 'full_html',
+      'name' => 'Full HTML',
+      'weight' => 1,
+      'filters' => array(
+        'filter_htmlcorrector' => array('status' => 1),
+      ),
+    );
+    $full_html_format = (object) $full_html_format;
+    filter_format_save($full_html_format);
+
+    // Associate text editor with text format.
+    $editor = entity_create('editor', array(
+      'name' => $full_html_format->name,
+      'format' => $full_html_format->format,
+      'editor' => 'unicorn',
+    ));
+    $editor->save();
+  }
+
+  /**
+   * Retrieves the FieldInstance object for the given field and returns the
+   * editor that Edit selects.
+   */
+  protected function getSelectedEditor($items, $field_name, $view_mode = 'default') {
+    $options = entity_get_display('test_entity', 'test_bundle', $view_mode)->getComponent($field_name);
+    $field_instance = field_info_instance('test_entity', $field_name, 'test_bundle');
+    return $this->editorSelector->getEditor($options['type'], $field_instance, $items);
+  }
+
+  /**
+   * Tests editor selection when the Editor module is present.
+   *
+   * 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 testEditorSelection() {
+    $this->editorManager = new EditorManager();
+    $this->editorSelector = new EditorSelector($this->editorManager);
+
+    // Pretend there is an entity with these items for the field.
+    $items = array(array('value' => 'Hello, world!', 'format' => 'filtered_html'));
+
+    // Editor selection w/ cardinality 1, text format w/o associated text editor.
+    $this->assertEqual('form', $this->getSelectedEditor($items, $this->field_name), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
+
+    // Editor selection w/ cardinality 1, text format w/ associated text editor.
+    $items[0]['format'] = 'full_html';
+    $this->assertEqual('editor', $this->getSelectedEditor($items, $this->field_name), "With cardinality 1, and the full_html text format, the 'editor' 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, $this->field_name), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected.");
+  }
+
+  /**
+   * Tests (custom) metadata when the "Editor" Create.js editor is used.
+   */
+  function testMetadata() {
+    $this->editorManager = new EditorManager();
+    $this->accessChecker = new MockEditEntityFieldAccessCheck();
+    $this->editorSelector = new EditorSelector($this->editorManager);
+    $this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
+
+    // Create an entity with values for the field.
+    $this->entity = field_test_create_entity();
+    $this->is_new = TRUE;
+    $this->entity->{$this->field_name}[LANGUAGE_NOT_SPECIFIED] = array(array('value' => 'Test', 'format' => 'full_html'));
+    field_test_entity_save($this->entity);
+    $entity = entity_load('test_entity', $this->entity->ftid);
+
+    // Verify metadata.
+    $instance = field_info_instance($entity->entityType(), $this->field_name, $entity->bundle());
+    $metadata = $this->metadataGenerator->generate($entity, $instance, LANGUAGE_NOT_SPECIFIED, 'default');
+    $expected = array(
+      'access' => TRUE,
+      'label' => 'Long text field',
+      'editor' => 'editor',
+      'aria' => 'Entity test_entity 1, field Long text field',
+      'custom' => array(
+        'format' => 'full_html',
+        'formatHasTransformations' => FALSE,
+      ),
+    );
+    $this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.');
+  }
+
+}
diff --git a/core/modules/editor/tests/modules/editor_test.info b/core/modules/editor/tests/modules/editor_test.info
new file mode 100644
index 0000000..9745677
--- /dev/null
+++ b/core/modules/editor/tests/modules/editor_test.info
@@ -0,0 +1,6 @@
+name = Text Editor test
+description = Support module for the Text Editor module tests.
+core = 8.x
+package = Testing
+version = VERSION
+hidden = TRUE
diff --git a/core/modules/editor/tests/modules/editor_test.module b/core/modules/editor/tests/modules/editor_test.module
new file mode 100644
index 0000000..7c56259
--- /dev/null
+++ b/core/modules/editor/tests/modules/editor_test.module
@@ -0,0 +1,6 @@
+<?php
+
+/**
+ * @file
+ * Helper module for the Text Editor tests.
+ */
diff --git a/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/Unicorn.php b/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/Unicorn.php
new file mode 100644
index 0000000..58e5766
--- /dev/null
+++ b/core/modules/editor/tests/modules/lib/Drupal/editor_test/Plugin/editor/editor/Unicorn.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\editor_test\Plugin\editor\editor\Unicorn.
+ */
+
+namespace Drupal\editor_test\Plugin\editor\editor;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\editor\Plugin\Core\Entity\Editor;
+use Drupal\editor\Plugin\EditorInterface;
+
+/**
+ * Defines a Unicorn-powered text editor for Drupal.
+ *
+ * @Plugin(
+ *   id = "unicorn",
+ *   title = @Translation("Unicorn"),
+ *   library = {
+ *     "module" = "system",
+ *     "name" = "jquery"
+ *   },
+ *   supports_true_wysiwyg = TRUE,
+ *   module = "editor_test"
+ * )
+ */
+class Unicorn extends PluginBase implements EditorInterface {
+
+  /**
+   * Implements \Drupal\editor\Plugin\EditorInterface::defaultSettings().
+   */
+  function defaultSettings() {
+    return array('ponies too' => TRUE);
+  }
+
+  /**
+   * Implements \Drupal\editor\Plugin\EditorInterface::settingsForm().
+   */
+  function settingsForm(array &$form, array &$form_state, Editor $editor) {
+    return array();
+  }
+
+  /**
+   * Implements \Drupal\editor\Plugin\EditorInterface::settingsFormValidate().
+   */
+  function settingsFormValidate(array $form, array &$form_state) { }
+
+  /**
+   * Implements \Drupal\editor\Plugin\EditorInterface::generateJsSettings().
+   */
+  function generateJsSettings(Editor $editor) {
+    return array();
+  }
+
+}
