Commit 65e54edb authored by webchick's avatar webchick

Issue #1874936 by frega, Wim Leers: Pluggable in-place editors (allow modules...

Issue #1874936 by frega, Wim Leers: Pluggable in-place editors (allow modules to define new Create.js PropertyEditor widgets).
parent 189108a1
......@@ -121,7 +121,7 @@
outline: none;
}
.edit-field.edit-editable,
.edit-field.edit-type-direct .edit-editable {
.edit-field .edit-editable {
box-shadow: 0 0 1px 1px #4d9de9;
}
......@@ -131,12 +131,12 @@
}
.edit-field.edit-editable.edit-highlighted,
.edit-form.edit-editable.edit-highlighted,
.edit-field.edit-type-direct .edit-editable.edit-highlighted {
.edit-field .edit-editable.edit-highlighted {
box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
}
.edit-field.edit-editable.edit-highlighted.edit-validation-error,
.edit-form.edit-editable.edit-highlighted.edit-validation-error,
.edit-field.edit-type-direct .edit-editable.edit-highlighted.edit-validation-error {
.edit-field .edit-editable.edit-highlighted.edit-validation-error {
box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
}
.edit-form.edit-editable .form-item .error {
......@@ -146,7 +146,7 @@
/* Editing (focused) editable. */
.edit-form.edit-editable.edit-editing,
.edit-field.edit-type-direct .edit-editable.edit-editing {
.edit-field .edit-editable.edit-editing {
/* In the latest design, there's no special styling when editing as opposed to
* just hovering.
* This will be necessary again for http://drupal.org/node/1844220.
......@@ -290,9 +290,8 @@
.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 {
/* Exception: when the toolbar is instructed to be "full width". */
.edit-toolbar-fullwidth .edit-toolbar-heightfaker {
width: 100%;
clip: auto;
}
......
......@@ -105,8 +105,6 @@ function edit_library_info() {
// 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,
......@@ -136,6 +134,26 @@ function edit_library_info() {
array('system', 'drupalSettings'),
),
);
$libraries['edit.editor.form'] = array(
'title' => '"Form" Create.js PropertyEditor widget',
'version' => VERSION,
'js' => array(
$path . '/js/createjs/editingWidgets/formwidget.js' => $options,
),
'dependencies' => array(
array('edit', 'edit'),
),
);
$libraries['edit.editor.direct'] = array(
'title' => '"Direct" Create.js PropertyEditor widget',
'version' => VERSION,
'js' => array(
$path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options,
),
'dependencies' => array(
array('edit', 'edit'),
),
);
return $libraries;
}
......@@ -146,7 +164,7 @@ function edit_library_info() {
function edit_preprocess_field(&$variables) {
$element = $variables['element'];
$entity = $element['#object'];
$variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $element['#field_name'] . ':' . $element['#language'] . ':' . $element['#view_mode'];
$variables['attributes']['data-edit-id'] = $entity->entityType() . '/' . $entity->id() . '/' . $element['#field_name'] . '/' . $element['#language'] . '/' . $element['#view_mode'];
}
/**
......
/**
* @file
* Determines which editor to use based on a class attribute.
* Determines which editor (Create.js PropertyEditor widget) to use.
*/
(function (jQuery, drupalSettings) {
(function (jQuery, Drupal, drupalSettings) {
"use strict";
......@@ -13,31 +13,18 @@
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: {}
};
// The Create.js PropertyEditor widget configuration is not hardcoded; it
// is generated by the server.
this.options.propertyEditorWidgetsConfiguration = drupalSettings.edit.editors;
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';
// Pick a PropertyEditor widget for a property depending on its metadata.
var propertyID = Drupal.edit.util.calcPropertyID(data.entity, data.property);
return Drupal.edit.metadataCache[propertyID].editor;
}
});
})(jQuery, drupalSettings);
})(jQuery, Drupal, drupalSettings);
......@@ -8,6 +8,13 @@
jQuery.widget('Drupal.drupalContentEditableWidget', jQuery.Create.editWidget, {
/**
* Implements getEditUISettings() method.
*/
getEditUISettings: function() {
return { padding: true, unifiedToolbar: false, fullWidthToolbar: false };
},
/**
* Implements jQuery UI widget factory's _init() method.
*
......
......@@ -11,6 +11,13 @@
id: null,
$formContainer: null,
/**
* Implements getEditUISettings() method.
*/
getEditUISettings: function() {
return { padding: false, unifiedToolbar: false, fullWidthToolbar: false };
},
/**
* Implements jQuery UI widget factory's _init() method.
*
......@@ -42,11 +49,15 @@
case 'candidate':
if (from !== 'inactive') {
this.disable();
if (from !== 'highlighted') {
this.element.removeClass('edit-belowoverlay');
}
}
break;
case 'highlighted':
break;
case 'activating':
this.element.addClass('edit-belowoverlay');
this.enable();
break;
case 'active':
......
......@@ -39,14 +39,7 @@ Drupal.behaviors.edit = {
field.$el
.attr('data-edit-field-label', meta.label)
.attr('aria-label', meta.aria)
.addClass('edit-field edit-type-' + meta.editor);
if (meta.editor === 'direct-with-wysiwyg') {
field.$el
// This editor also uses the Backbone.syncDirect saving mechanism.
.addClass('edit-type-direct')
.attr('data-edit-text-format', meta.format)
.addClass((meta.formatHasTransformations) ? 'edit-text-with-transformation-filters' : 'edit-text-without-transformation-filters');
}
.addClass('edit-field edit-type-' + ((meta.editor === 'form') ? 'form' : 'direct'));
}
return true;
......
......@@ -2,7 +2,7 @@
* @file
* Provides utility functions for Edit.
*/
(function($, Drupal, drupalSettings) {
(function($, _, Drupal, drupalSettings) {
"use strict";
......@@ -16,6 +16,33 @@ Drupal.edit.util.calcPropertyID = function(entity, predicate) {
return entity.getSubjectUri() + '/' + predicate;
};
/**
* Retrieves a setting of the editor-specific Edit UI integration.
*
* If the editor does not implement the optional getEditUISettings() method, or
* if it doesn't set a value for a certain setting, then the default value will
* be used.
*
* @param editor
* A Create.js PropertyEditor widget instance.
* @param setting
* Name of the Edit UI integration setting.
*
* @return {*}
*/
Drupal.edit.util.getEditUISetting = function(editor, setting) {
var settings = {};
var defaultSettings = {
padding: false,
unifiedToolbar: false,
fullWidthToolbar: false
};
if (typeof editor.getEditUISettings === 'function') {
settings = editor.getEditUISettings();
}
return _.extend(defaultSettings, settings)[setting];
};
Drupal.edit.util.buildUrl = function(id, urlFormat) {
var parts = id.split('/');
return Drupal.formatString(decodeURIComponent(urlFormat), {
......@@ -148,4 +175,4 @@ Drupal.edit.util.form = {
}
};
})(jQuery, Drupal, drupalSettings);
})(jQuery, _, Drupal, drupalSettings);
......@@ -62,7 +62,7 @@
// 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')) {
if (jQuery(element).hasClass('edit-type-form')) {
return;
}
......@@ -142,7 +142,7 @@
// Returns the "URI" of an entity of an element in format
// `<entity type>/<id>`.
getElementSubject: function (element) {
return this._getID(element).split(':').slice(0, 2).join('/');
return this._getID(element).split('/').slice(0, 2).join('/');
},
// Returns the field name for an element in format
......@@ -152,11 +152,11 @@
if (!this._getID(element)) {
throw new Error('Could not find predicate for element');
}
return this._getID(element).split(':').slice(2, 5).join('/');
return this._getID(element).split('/').slice(2, 5).join('/');
},
getElementType: function (element) {
return this._getID(element).split(':').slice(0, 1)[0];
return this._getID(element).split('/').slice(0, 1)[0];
},
// Reads all editable entities (currently each Drupal field is considered an
......@@ -214,15 +214,7 @@
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';
}
var range = predicate.split('/')[0];
type.attributes.add(predicate, [range], 0, 1, {
label: element.data('edit-field-label')
});
......
......@@ -12,10 +12,6 @@ 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,
......@@ -35,8 +31,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
* - 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.
*/
......@@ -44,10 +38,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
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));
},
......@@ -66,7 +56,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
if (from !== 'inactive') {
this.stopHighlight();
if (from !== 'highlighted') {
this.stopEdit(this.editorName);
this.stopEdit();
}
}
break;
......@@ -74,16 +64,15 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
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);
// NOTE: this state is not used by every editor! It's only used by those
// that need to interact with the server.
this.prepareEdit();
break;
case 'active':
if (this.editorName !== 'form') {
this.prepareEdit(this.editorName);
if (from !== 'activating') {
this.prepareEdit();
}
this.startEdit(this.editorName);
this.startEdit();
break;
case 'changed':
break;
......@@ -130,7 +119,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
undecorate: function () {
this.$el
.removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay');
.removeClass('edit-candidate edit-editable edit-highlighted edit-editing');
},
startHighlight: function () {
......@@ -146,26 +135,22 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
.removeClass('edit-highlighted');
},
prepareEdit: function(editorName) {
prepareEdit: function() {
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') {
startEdit: function() {
if (this.getEditUISetting('padding')) {
this._pad();
}
},
stopEdit: function(editorName) {
stopEdit: function() {
this.$el.removeClass('edit-highlighted edit-editing');
// Make the other editors show up again.
......@@ -173,14 +158,20 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
// Revisit this.
$('.edit-candidate').addClass('edit-editable');
if (editorName === 'form') {
this.$el.removeClass('edit-belowoverlay');
}
else {
if (this.getEditUISetting('padding')) {
this._unpad();
}
},
/**
* Retrieves a setting of the editor-specific Edit UI integration.
*
* @see Drupal.edit.util.getEditUISetting().
*/
getEditUISetting: function(setting) {
return Drupal.edit.util.getEditUISetting(this.editor, setting);
},
_pad: function () {
var self = this;
......
......@@ -40,8 +40,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
* - 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'.
* * editorName: the editor name.
* * element: the jQuery-wrapped editor DOM element
* - $storageWidgetEl: the DOM element on which the Create Storage widget is
* initialized.
......@@ -71,8 +70,8 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
break;
case 'candidate':
if (from !== 'inactive') {
if (from !== 'highlighted' && this.editorName !== 'form') {
this._unpad(this.editorName);
if (from !== 'highlighted' && this.getEditUISetting('padding')) {
this._unpad();
}
this.remove();
}
......@@ -86,12 +85,16 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
this.setLoadingIndicator(true);
break;
case 'active':
this.startEdit(this.editorName);
this.startEdit();
this.setLoadingIndicator(false);
if (this.editorName !== 'form') {
this._pad(this.editorName);
if (this.getEditUISetting('fullWidthToolbar')) {
this.$el.addClass('edit-toolbar-fullwidth');
}
if (this.editorName === 'direct-with-wysiwyg') {
if (this.getEditUISetting('padding')) {
this._pad();
}
if (this.getEditUISetting('unifiedToolbar')) {
this.insertWYSIWYGToolGroups();
}
break;
......@@ -303,26 +306,34 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
this.show('ops');
},
/**
* Retrieves a setting of the editor-specific Edit UI integration.
*
* @see Drupal.edit.util.getEditUISetting().
*/
getEditUISetting: function(setting) {
return Drupal.edit.util.getEditUISetting(this.editor, setting);
},
/**
* 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');
}
_pad: function() {
// 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 });
}
// The toolbar must move to the top and the left.
var $hf = this.$el.find('.edit-toolbar-heightfaker');
$hf.css({ bottom: '6px', left: '-5px' });
if (this.getEditUISetting('fullWidthToolbar')) {
$hf.css({ width: this.editor.element.width() + 10 });
}
},
/**
......@@ -330,14 +341,14 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
*
* @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: '' });
}
_unpad: function() {
// Move the toolbar back to its original position.
var $hf = this.$el.find('.edit-toolbar-heightfaker');
$hf.css({ bottom: '1px', left: '' });
if (this.getEditUISetting('fullWidthToolbar')) {
$hf.css({ width: '' });
}
},
insertWYSIWYGToolGroups: function() {
......
......@@ -20,18 +20,18 @@ class EditBundle extends Bundle {
* Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
*/
public function build(ContainerBuilder $container) {
// Register the plugin managers for our plugin types with the dependency injection container.
$container->register('plugin.manager.edit.processed_text_editor', 'Drupal\edit\Plugin\ProcessedTextEditorManager');
$container->register('plugin.manager.edit.editor', 'Drupal\edit\Plugin\EditorManager');
$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'));
->addArgument(new Reference('plugin.manager.edit.editor'));
$container->register('edit.metadata.generator', 'Drupal\edit\MetadataGenerator')
->addArgument(new Reference('access_check.edit.entity_field'))
->addArgument(new Reference('edit.editor.selector'));
->addArgument(new Reference('edit.editor.selector'))
->addArgument(new Reference('plugin.manager.edit.editor'));
}
}
......@@ -42,7 +42,7 @@ public function metadata(Request $request) {
$metadata = array();
foreach ($fields as $field) {
list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode(':', $field);
list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode('/', $field);
// Load the entity.
if (!$entity_type || !entity_get_info($entity_type)) {
......
<?php
/**
* @file
* Contains \Drupal\edit\EditorBase.
*/
namespace Drupal\edit;
use Drupal\Component\Plugin\PluginBase;
use Drupal\edit\EditorInterface;
use Drupal\field\FieldInstance;
/**
* Defines a base editor (Create.js PropertyEditor widget) implementation.
*/
abstract class EditorBase extends PluginBase implements EditorInterface {
/**
* Implements \Drupal\edit\EditorInterface::getMetadata().
*/
function getMetadata(FieldInstance $instance, array $items) {
return array();
}
}
<?php
/**
* @file
* Contains \Drupal\edit\EditorInterface.
*/
namespace Drupal\edit;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\field\FieldInstance;
/**
* Defines an interface for in-place editors (Create.js PropertyEditor widgets).
*
* A PropertyEditor widget is a user-facing interface to edit an entity property
* through Create.js.
*/
interface EditorInterface extends PluginInspectionInterface {
/**
* Checks whether this editor is compatible with a given field instance.
*
* @param \Drupal\field\FieldInstance $instance
* The field instance of the field being edited.
* @param array $items
* The field's item values.
*
* @return bool
* TRUE if it is compatible, FALSE otherwise.
*/
public function isCompatible(FieldInstance $instance, array $items);
/**
* Generates metadata that is needed specifically for this editor.
*
* Will only be called by \Drupal\edit\MetadataGeneratorInterface::generate()
* when the passed in field instance & item values will use this editor.
*
* @param \Drupal\field\FieldInstance $instance
* The field instance of the field being edited.
* @param array $items
* The field's item values.
*
* @return array
* A keyed array with metadata. Each key should be prefixed with the plugin
* ID of the editor.
*/
public function getMetadata(FieldInstance $instance, array $items);
/**
* Returns the attachments for this editor.
*
* @return array
* An array of attachments, for use with #attached.
*
* @see drupal_process_attached()
*/
public function getAttachments();
}
......@@ -8,160 +8,120 @@
namespace Drupal\edit;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\field\FieldInstance;
/**
* Selects an in-place editor for a given entity field.
* Selects an in-place editor (an Editor plugin) for a field.
*/
class EditorSelector implements EditorSelectorInterface {
/**
* The manager for processed text editor plugins.
* The manager for editor (Create.js PropertyEditor widget) plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $processedTextEditorManager;
protected $editorManager;
/**
* The processed text editor plugin selected.
* A list of alternative editor plugin IDs, keyed by editor plugin ID.
*
* @var \Drupal\edit\Plugin\ProcessedTextEditorInterface
* @var array
*/
protected $processedTextEditorPlugin;
protected $alternatives;
/**
* Constructs a new EditorSelector.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $processed_text_editor_manager
* The manager for processed text editor plugins.
* @param \Drupal\Component\Plugin\PluginManagerInterface
* The manager for Create.js PropertyEditor widget plugins.
*/
public function __construct(PluginManagerInterface $processed_text_editor_manager) {
$this->processedTextEditorManager = $processed_text_editor_manager;
public function __construct(PluginManagerInterface $editor_manager) {
$this->editorManager = $editor_manager;
}
/**
* Implements \Drupal\edit\EditorSelectorInterface::getEditor().
*/