Commit c85c994d authored by Dries's avatar Dries

Issue #1824500 by Wim Leers, tkoleary, frega, jessebeach, henribergius,...

Issue #1824500 by Wim Leers, tkoleary, frega, jessebeach, henribergius, effulgentsia, nod_, yched: In-place editing for Fields.
parent b5ac4a52
/**
* Animations.
*/
.edit-animate-invisible {
opacity: 0;
}
.edit-animate-fast {
-webkit-transition: all .2s ease;
-moz-transition: all .2s ease;
-ms-transition: all .2s ease;
-o-transition: all .2s ease;
transition: all .2s ease;
}
.edit-animate-default {
-webkit-transition: all .4s ease;
-moz-transition: all .4s ease;
-ms-transition: all .4s ease;
-o-transition: all .4s ease;
transition: all .4s ease;
}
.edit-animate-slow {
-webkit-transition: all .6s ease;
-moz-transition: all .6s ease;
-ms-transition: all .6s ease;
-o-transition: all .6s ease;
transition: all .6s ease;
}
.edit-animate-delay-veryfast {
-webkit-transition-delay: .05s;
-moz-transition-delay: .05s;
-ms-transition-delay: .05s;
-o-transition-delay: .05s;
transition-delay: .05s;
}
.edit-animate-delay-fast {
-webkit-transition-delay: .2s;
-moz-transition-delay: .2s;
-ms-transition-delay: .2s;
-o-transition-delay: .2s;
transition-delay: .2s;
}
.edit-animate-disable-width {
-webkit-transition: width 0s;
-moz-transition: width 0s;
-ms-transition: width 0s;
-o-transition: width 0s;
transition: width 0s;
}
.edit-animate-only-visibility {
-webkit-transition: opacity .2s ease;
-moz-transition: opacity .2s ease;
-ms-transition: opacity .2s ease;
-o-transition: opacity .2s ease;
transition: opacity .2s ease;
}
.edit-animate-only-background-and-padding {
-webkit-transition: background, padding .2s ease;
-moz-transition: background, padding .2s ease;
-ms-transition: background, padding .2s ease;
-o-transition: background, padding .2s ease;
transition: background, padding .2s ease;
}
/**
* Toolbar.
*/
.icon-edit:before {
background-image: url("../images/icon-edit.png");
}
.icon-edit:active:before,
.active .icon-edit:before {
background-image: url("../images/icon-edit-active.png");
}
.toolbar .tray.edit.active {
z-index: 340;
}
.toolbar .icon-edit.edit-nothing-editable-hidden {
display: none;
}
/* In-place editing doesn't work in the overlay, so always hide the tab. */
.overlay-open .toolbar .icon-edit {
display: none;
}
/**
* Edit mode: overlay + candidate editables + editables being edited.
*
* Note: every class is prefixed with "edit-" to prevent collisions with modules
* or themes. In IPE-specific DOM subtrees, this is not necessary.
*/
#edit_overlay {
position: fixed;
z-index: 250;
width: 100%;
height: 100%;
background-color: #fff;
background-color: rgba(255,255,255,.5);
top: 0;
left: 0;
}
/* Editable. */
.edit-editable {
z-index: 300;
position: relative;
}
.edit-editable:focus {
outline: none;
}
.edit-field.edit-editable,
.edit-field.edit-type-direct .edit-editable {
box-shadow: 0 0 1px 1px #4d9de9;
}
/* Highlighted (hovered) editable. */
.edit-editable.edit-highlighted {
min-width: 200px;
}
.edit-field.edit-editable.edit-highlighted,
.edit-form.edit-editable.edit-highlighted,
.edit-field.edit-type-direct .edit-editable.edit-highlighted {
box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
}
.edit-field.edit-editable.edit-highlighted.edit-validation-error,
.edit-form.edit-editable.edit-highlighted.edit-validation-error,
.edit-field.edit-type-direct .edit-editable.edit-highlighted.edit-validation-error {
box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
}
.edit-form.edit-editable .form-item .error {
border: 1px solid #eea0a0;
}
/* Editing (focused) editable. */
.edit-form.edit-editable.edit-editing,
.edit-field.edit-type-direct .edit-editable.edit-editing {
/* In the latest design, there's no special styling when editing as opposed to
* just hovering.
* This will be necessary again for http://drupal.org/node/1844220.
*/
}
/**
* Edit mode: modal.
*/
#edit_modal {
z-index: 350;
position: fixed;
top: 40%;
left: 40%;
box-shadow: 3px 3px 5px #333;
background-color: white;
border: 1px solid #0199ff;
font-family: 'Droid sans', 'Lucida Grande', sans-serif;
}
#edit_modal .main {
font-size: 130%;
margin: 25px;
padding-left: 40px;
background: transparent url('../images/attention.png') no-repeat;
}
#edit_modal .actions {
border-top: 1px solid #ddd;
padding: 3px inherit;
text-align: right;
background: #f5f5f5;
}
/* Modal active: prevent user from interacting with toolbar & editables. */
.edit-form-container.edit-belowoverlay,
.edit-toolbar-container.edit-belowoverlay,
.edit-validation-errors.edit-belowoverlay {
z-index: 210;
}
.edit-editable.edit-belowoverlay {
z-index: 200;
}
/**
* Edit mode: type=direct.
*/
.edit-validation-errors {
z-index: 300;
position: relative;
}
.edit-validation-errors .messages.error {
position: absolute;
top: 6px;
left: -5px;
margin: 0;
border: none;
box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
background-color: white;
}
/**
* Edit mode: type=form.
*/
#edit_backstage {
display: none;
}
.edit-form {
position: absolute;
z-index: 300;
box-shadow: 0 0 30px 4px #4f4f4f;
max-width: 35em;
}
.edit-form .placeholder {
min-height: 22px;
}
/* Default form styling overrides. */
.edit-form form { padding: 1em; }
.edit-form .form-item { margin: 0; }
.edit-form .form-wrapper { margin: .5em; }
.edit-form .form-wrapper .form-wrapper { margin: inherit; }
.edit-form .form-actions { display: none; }
.edit-form input { max-width: 100%; }
/**
* Edit mode: toolbars
*/
/* Trick: wrap statically positioned elements in relatively positioned element
without changing its location. This allows us to absolutely position the
toolbar.
*/
.edit-toolbar-container,
.edit-form-container {
position: relative;
padding: 0;
border: 0;
margin: 0;
vertical-align: baseline;
z-index: 310;
}
.edit-toolbar-container {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
.edit-toolbar-heightfaker {
height: auto;
position: absolute;
bottom: 1px;
box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
background: #fff;
}
/* The toolbar; these are not necessarily visible. */
.edit-toolbar {
position: relative;
height: 100%;
font-family: 'Droid sans', 'Lucida Grande', sans-serif;
}
.edit-toolbar-heightfaker {
clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */
}
/* Exception: when used for a directly WYSIWYG editable field that is actively
being edited. */
.edit-type-direct-with-wysiwyg .edit-editing .edit-toolbar-heightfaker {
width: 100%;
clip: auto;
}
/* The toolbar contains toolgroups; these are visible. */
.edit-toolgroup {
float: left; /* LTR */
}
/* Info toolgroup. */
.edit-toolgroup.info {
float: left; /* LTR */
font-weight: bolder;
padding: 0 5px;
background: #fff url('../images/throbber.gif') no-repeat -60px 60px;
}
.edit-toolgroup.info.loading {
padding-right: 35px;
background-position: 90% 50%;
}
/* Operations toolgroup. */
.edit-toolgroup.ops {
float: right; /* LTR */
margin-left: 5px;
}
.edit-toolgroup.wysiwyg-tabs {
float: right;
}
.edit-toolgroup.wysiwyg {
clear: left;
width: 100%;
padding-left: 0;
}
/**
* Edit mode: buttons (in both modal and toolbar).
*/
#edit_modal button,
.edit-toolbar button {
float: left; /* LTR */
display: block;
height: 29px;
min-width: 29px;
padding: 3px 6px 6px 6px;
margin: 4px 5px 1px 0;
border: 1px solid #fff;
border-radius: 3px;
color: white;
text-decoration: none;
font-size: 13px;
cursor: pointer;
}
#edit_modal button {
float: none;
display: inline-block;
}
/* Button with icons. */
#edit_modal button span,
.edit-toolbar button span {
width: 22px;
height: 19px;
display: block;
float: left;
}
.edit-toolbar span.close {
background: url('../images/close.png') no-repeat 3px 2px;
text-indent: -999em;
direction: ltr;
}
.edit-toolbar button.blank-button {
color: black;
background-color: #fff;
font-weight: bolder;
}
#edit_modal button.blue-button,
.edit-toolbar button.blue-button {
color: white;
background-image: -webkit-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
background-image: -moz-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
background-image: linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
border-radius: 5px;
}
#edit_modal button.gray-button,
.edit-toolbar button.gray-button {
color: #666;
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
background-image: -moz-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
background-image: linear-gradient(top, #f5f5f5 0%, #ccc 100%);
border-radius: 5px;
}
#edit_modal button.blue-button:hover,
.edit-toolbar button.blue-button:hover,
#edit_modal button.blue-button:active,
.edit-toolbar button.blue-button:active {
border: 1px solid #55a5d3;
box-shadow: 0 2px 1px rgba(0,0,0,0.2);
}
#edit_modal button.gray-button:hover,
.edit-toolbar button.gray-button:hover,
#edit_modal button.gray-button:active,
.edit-toolbar button.gray-button:active {
border: 1px solid #cdcdcd;
box-shadow: 0 2px 1px rgba(0,0,0,0.1);
}
name = Edit
description = In-place content editing.
package = Core
core = 8.x
dependencies[] = field
<?php
/**
* @file
* Provides in-place content editing functionality for fields.
*
* The Edit module makes content editable in-place. Rather than having to visit
* a separate page to edit content, it may be edited in-place.
*
* Technically, this module adds classes and data- attributes to fields and
* entities, enabling them for in-place editing.
*/
use Drupal\Core\Entity\EntityInterface;
use Drupal\edit\Form\EditFieldForm;
/**
* Implements hook_custom_theme().
*
* @todo Add an event subscriber to the Ajax system to automatically set the
* base page theme for all Ajax requests, and then remove this one off.
*/
function edit_custom_theme() {
if (substr(current_path(), 0, 5) === 'edit/') {
return ajax_base_page_theme();
}
}
/**
* Implements hook_permission().
*/
function edit_permission() {
return array(
'access in-place editing' => array(
'title' => t('Access in-place editing'),
),
);
}
/**
* Implements hook_toolbar().
*/
function edit_toolbar() {
if (!user_access('access in-place editing')) {
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'),
),
),
),
);
// Include the attachments and settings for all available editors.
$attachments = drupal_container()->get('edit.editor.selector')->getAllEditorAttachments();
$tab['edit']['tray']['#attached'] = array_merge_recursive($tab['edit']['tray']['#attached'], $attachments);
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(
'metadataURL' => url('edit/metadata'),
'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) {
$element = $variables['element'];
$entity = $element['#object'];
$variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $element['#field_name'] . ':' . $element['#language'] . ':' . $element['#view_mode'];
}
/**
* Form constructor for the field editing form.
*
* @ingroup forms
*/
function edit_field_form(array $form, array &$form_state, EntityInterface $entity, $field_name) {
$form_handler = new EditFieldForm();
return $form_handler->build($form, $form_state, $entity, $field_name);
}
edit_metadata:
pattern: '/edit/metadata'
defaults:
_controller: '\Drupal\edit\EditController::metadata'
requirements:
_permission: 'access in-place editing'
edit_field_form:
pattern: '/edit/form/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode}'
defaults:
_controller: '\Drupal\edit\EditController::fieldForm'
requirements:
_permission: 'access in-place editing'
_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:
_permission: 'access in-place editing'
_access_edit_entity_field: 'TRUE'
This diff is collapsed.
/**
* @file
* Backbone.sync implementation for Edit. This is the beating heart.
*/
(function (jQuery, Backbone, Drupal) {
"use strict";
Backbone.defaultSync = Backbone.sync;
Backbone.sync = function(method, model, options) {
if (options.editor.options.editorName === 'form') {
return Backbone.syncDrupalFormWidget(method, model, options);
}
else {
return Backbone.syncDirect(method, model, options);
}
};
/**
* Performs syncing for "form" PredicateEditor widgets.
*
* Implemented on top of Form API and the AJAX commands framework. Sets up
* scoped AJAX command closures specifically for a given PredicateEditor widget
* (which contains a pre-existing form). By submitting the form through
* Drupal.ajax and leveraging Drupal.ajax' ability to have scoped (per-instance)
* command implementations, we are able to update the VIE model, re-render the
* form when there are validation errors and ensure no Drupal.ajax memory leaks.
*
* @see Drupal.edit.util.form
*/
Backbone.syncDrupalFormWidget = function(method, model, options) {
if (method === 'update') {
var predicate = options.editor.options.property;
var $formContainer = options.editor.$formContainer;
var $submit = $formContainer.find('.edit-form-submit');
var base = $submit.attr('id');
// Successfully saved.
Drupal.ajax[base].commands.editFieldFormSaved = function(ajax, response, status) {
Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
// Call Backbone.sync's success callback with the rerendered field.
var changedAttributes = {};
// @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216)
// Once full JSON-LD support in Drupal core lands, we can ensure that the
// models that VIE maintains are properly updated.
changedAttributes[predicate] = undefined;
changedAttributes[predicate + '/rendered'] = response.data;
options.success(changedAttributes);
};
// Unsuccessfully saved; validation errors.
Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) {
// Call Backbone.sync's error callback with the validation error messages.
options.error(response.data);
};
// The edit_field_form AJAX command is only called upon loading the form for
// the first time, and when there are validation errors in the form; Form
// API then marks which form items have errors. Therefor, we have to replace
// the existing form, unbind the existing Drupal.ajax instance and create a
// new Drupal.ajax instance.
Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) {
Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
Drupal.ajax.prototype.commands.insert(ajax, {
data: response.data,
selector: '#' + $formContainer.attr('id') + ' form'
});
// Create a Drupa.ajax instance for the re-rendered ("new") form.
var $newSubmit = $formContainer.find('.edit-form-submit');
Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit);
};
// Click the form's submit button; the scoped AJAX commands above will
// handle the server's response.
$submit.trigger('click.edit');
}
};
/**
* Performs syncing for "direct" PredicateEditor widgets.
*
* @see Backbone.syncDrupalFormWidget()
* @see Drupal.edit.util.form
*/
Backbone.syncDirect = function(method, model, options) {
if (method === 'update') {
var fillAndSubmitForm = function(value) {
jQuery('#edit_backstage form')
// Fill in the value in any <input> that isn't hidden or a submit button.
.find(':input[type!="hidden"][type!="submit"]:not(select)').val(value).end()
// Submit the form.
.find('.edit-form-submit').trigger('click.edit');
};
var entity = options.editor.options.entity;
var predicate = options.editor.options.property;
var value = model.get(predicate);