core/modules/edit/css/edit.css | 407 +++++++++++++++
core/modules/edit/edit.info | 7 +
core/modules/edit/edit.module | 153 ++++++
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 | 78 +++
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, 4694 insertions(+)
diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css
new file mode 100644
index 0000000..1108cda
--- /dev/null
+++ b/core/modules/edit/css/edit.css
@@ -0,0 +1,407 @@
+/**
+ * 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;
+}
+
+
+
+
+/**
+ * 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..3328601
--- /dev/null
+++ b/core/modules/edit/edit.info
@@ -0,0 +1,7 @@
+name = Edit
+description = In-place content editing.
+package = Core
+core = 8.x
+
+dependencies[] = field
+dependencies[] = toolbar
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
new file mode 100644
index 0000000..5159384
--- /dev/null
+++ b/core/modules/edit/edit.module
@@ -0,0 +1,153 @@
+ array(
+ 'title' => t('Access in-place editing'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_toolbar().
+ */
+function edit_toolbar() {
+ if (path_is_admin(current_path())) {
+ return;
+ }
+
+ $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!ul6
; 0!f>>Ǐ kν_j㜻!0-@>8,i vҤrlWn?B(ijk*yT%Pv s=b_v>@?k&
+a|NciKBFUD^']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@ҔLO%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Ϩ/BDU9xV+D\?x@qWcF8wicS B}?v;Vf.V$JgX=Kضp0XS"iRw\:LL\~;Z5wu 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 IDATxeW0 {W
+H"ʵ,y{Hpyo?mf,RBRxBvL;&LPJaRb\(Tbn(1wϔJ)ԈkS
+58äT^4 P6c}[i <ާ'-+HP>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~\8L Ǹ0, i{qBC~H'JRĨ`f4&a ! , `sɺ(t34M!-0,l#))9 !q(i<hB 3˥ (`
,9%cm
+b0AY_e ! , dRj:ړtG8$c02P hi, ǑQ0X[LEck5`ڭP2F!
a @q dfz{lQ zK ! , `BjR:$BPFq(ˢ J@0i-2͇ Qck6[G`m:pQ4fqow ! , d1j}MS@S\ H9 #IrPØi8f..r$ł|nl
+*T\![͂l,Q0@(KFMO{
ql{ ! , ^I 3a!P(Ol~`8 LÇ!@$Lgx|f,`"`Jʼn"cUP
+GAw< tz ! , h9C 4kk\
&I&l
)F-P@chH!ш4<
+ x@R`0C"8h0BfgX(rc}Y
+1 ! , aйҚX]mKl[)"aĢ\*"$t	@1pP`,`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 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 += '
';
+ 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 += '';
+ 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 += '
';
+ html += '
';
+ html += ' ';
+ html += '
';
+ 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 += '
';
+ html += '
';
+ html += ' ';
+ html += '
';
+ html += '
';
+ 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 += '
';
+ html += Drupal.theme('editButtons', { buttons: settings.buttons });
+ html += '
';
+ 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 += '';
+ }
+ 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 += '
';
+ html += '
';
+ html += '
';
+ html += settings.loadingMsg;
+ html += '
';
+ html += '
';
+ html += '
';
+ 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 '';
+};
+
+/**
+ * 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 += '
' + messages[i] + '
';
+ }
+ 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
+ // `::::`.
+ _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
+ // `/`.
+ getElementSubject: function (element) {
+ return this._getID(element).split(':').slice(0, 2).join('/');
+ },
+
+ // Returns the field name for an element in format
+ // `//`.
+ // (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 = '';
+ 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..2bcdab9
--- /dev/null
+++ b/core/modules/edit/js/views/menu-view.js
@@ -0,0 +1,78 @@
+/**
+ * @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');
+ }
+ },
+ /**
+ * 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: " 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 = $('')
+ .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: '' + Drupal.t('Close') + '', 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 @@
+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 @@
+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 @@
+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 @@
+ $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 @@
+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 @@
+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 @@
+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 @@
+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 @@
+ '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 @@
+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"
* }
* )
*/