Issue #1824500 by Wim Leers, tkoleary, frega, jessebeach, henribergius, effulgentsia, nod_, yched: In-place editing for Fields.
parent
b5ac4a523f
commit
c85c994db2
|
@ -0,0 +1,410 @@
|
|||
/**
|
||||
* Animations.
|
||||
*/
|
||||
.edit-animate-invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.edit-animate-fast {
|
||||
-webkit-transition: all .2s ease;
|
||||
-moz-transition: all .2s ease;
|
||||
-ms-transition: all .2s ease;
|
||||
-o-transition: all .2s ease;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.edit-animate-default {
|
||||
-webkit-transition: all .4s ease;
|
||||
-moz-transition: all .4s ease;
|
||||
-ms-transition: all .4s ease;
|
||||
-o-transition: all .4s ease;
|
||||
transition: all .4s ease;
|
||||
}
|
||||
|
||||
.edit-animate-slow {
|
||||
-webkit-transition: all .6s ease;
|
||||
-moz-transition: all .6s ease;
|
||||
-ms-transition: all .6s ease;
|
||||
-o-transition: all .6s ease;
|
||||
transition: all .6s ease;
|
||||
}
|
||||
|
||||
.edit-animate-delay-veryfast {
|
||||
-webkit-transition-delay: .05s;
|
||||
-moz-transition-delay: .05s;
|
||||
-ms-transition-delay: .05s;
|
||||
-o-transition-delay: .05s;
|
||||
transition-delay: .05s;
|
||||
}
|
||||
|
||||
.edit-animate-delay-fast {
|
||||
-webkit-transition-delay: .2s;
|
||||
-moz-transition-delay: .2s;
|
||||
-ms-transition-delay: .2s;
|
||||
-o-transition-delay: .2s;
|
||||
transition-delay: .2s;
|
||||
}
|
||||
|
||||
.edit-animate-disable-width {
|
||||
-webkit-transition: width 0s;
|
||||
-moz-transition: width 0s;
|
||||
-ms-transition: width 0s;
|
||||
-o-transition: width 0s;
|
||||
transition: width 0s;
|
||||
}
|
||||
|
||||
.edit-animate-only-visibility {
|
||||
-webkit-transition: opacity .2s ease;
|
||||
-moz-transition: opacity .2s ease;
|
||||
-ms-transition: opacity .2s ease;
|
||||
-o-transition: opacity .2s ease;
|
||||
transition: opacity .2s ease;
|
||||
}
|
||||
|
||||
.edit-animate-only-background-and-padding {
|
||||
-webkit-transition: background, padding .2s ease;
|
||||
-moz-transition: background, padding .2s ease;
|
||||
-ms-transition: background, padding .2s ease;
|
||||
-o-transition: background, padding .2s ease;
|
||||
transition: background, padding .2s ease;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Toolbar.
|
||||
*/
|
||||
.icon-edit:before {
|
||||
background-image: url("../images/icon-edit.png");
|
||||
}
|
||||
.icon-edit:active:before,
|
||||
.active .icon-edit:before {
|
||||
background-image: url("../images/icon-edit-active.png");
|
||||
}
|
||||
.toolbar .tray.edit.active {
|
||||
z-index: 340;
|
||||
}
|
||||
.toolbar .icon-edit.edit-nothing-editable-hidden {
|
||||
display: none;
|
||||
}
|
||||
/* In-place editing doesn't work in the overlay, so always hide the tab. */
|
||||
.overlay-open .toolbar .icon-edit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edit mode: overlay + candidate editables + editables being edited.
|
||||
*
|
||||
* Note: every class is prefixed with "edit-" to prevent collisions with modules
|
||||
* or themes. In IPE-specific DOM subtrees, this is not necessary.
|
||||
*/
|
||||
|
||||
#edit_overlay {
|
||||
position: fixed;
|
||||
z-index: 250;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
background-color: rgba(255,255,255,.5);
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* Editable. */
|
||||
.edit-editable {
|
||||
z-index: 300;
|
||||
position: relative;
|
||||
}
|
||||
.edit-editable:focus {
|
||||
outline: none;
|
||||
}
|
||||
.edit-field.edit-editable,
|
||||
.edit-field.edit-type-direct .edit-editable {
|
||||
box-shadow: 0 0 1px 1px #4d9de9;
|
||||
}
|
||||
|
||||
/* Highlighted (hovered) editable. */
|
||||
.edit-editable.edit-highlighted {
|
||||
min-width: 200px;
|
||||
}
|
||||
.edit-field.edit-editable.edit-highlighted,
|
||||
.edit-form.edit-editable.edit-highlighted,
|
||||
.edit-field.edit-type-direct .edit-editable.edit-highlighted {
|
||||
box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
|
||||
}
|
||||
.edit-field.edit-editable.edit-highlighted.edit-validation-error,
|
||||
.edit-form.edit-editable.edit-highlighted.edit-validation-error,
|
||||
.edit-field.edit-type-direct .edit-editable.edit-highlighted.edit-validation-error {
|
||||
box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
|
||||
}
|
||||
.edit-form.edit-editable .form-item .error {
|
||||
border: 1px solid #eea0a0;
|
||||
}
|
||||
|
||||
|
||||
/* Editing (focused) editable. */
|
||||
.edit-form.edit-editable.edit-editing,
|
||||
.edit-field.edit-type-direct .edit-editable.edit-editing {
|
||||
/* In the latest design, there's no special styling when editing as opposed to
|
||||
* just hovering.
|
||||
* This will be necessary again for http://drupal.org/node/1844220.
|
||||
*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edit mode: modal.
|
||||
*/
|
||||
#edit_modal {
|
||||
z-index: 350;
|
||||
position: fixed;
|
||||
top: 40%;
|
||||
left: 40%;
|
||||
box-shadow: 3px 3px 5px #333;
|
||||
background-color: white;
|
||||
border: 1px solid #0199ff;
|
||||
font-family: 'Droid sans', 'Lucida Grande', sans-serif;
|
||||
}
|
||||
|
||||
#edit_modal .main {
|
||||
font-size: 130%;
|
||||
margin: 25px;
|
||||
padding-left: 40px;
|
||||
background: transparent url('../images/attention.png') no-repeat;
|
||||
}
|
||||
|
||||
#edit_modal .actions {
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 3px inherit;
|
||||
text-align: right;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Modal active: prevent user from interacting with toolbar & editables. */
|
||||
.edit-form-container.edit-belowoverlay,
|
||||
.edit-toolbar-container.edit-belowoverlay,
|
||||
.edit-validation-errors.edit-belowoverlay {
|
||||
z-index: 210;
|
||||
}
|
||||
.edit-editable.edit-belowoverlay {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edit mode: type=direct.
|
||||
*/
|
||||
.edit-validation-errors {
|
||||
z-index: 300;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.edit-validation-errors .messages.error {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: -5px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edit mode: type=form.
|
||||
*/
|
||||
#edit_backstage {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-form {
|
||||
position: absolute;
|
||||
z-index: 300;
|
||||
box-shadow: 0 0 30px 4px #4f4f4f;
|
||||
max-width: 35em;
|
||||
}
|
||||
|
||||
.edit-form .placeholder {
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
/* Default form styling overrides. */
|
||||
.edit-form form { padding: 1em; }
|
||||
.edit-form .form-item { margin: 0; }
|
||||
.edit-form .form-wrapper { margin: .5em; }
|
||||
.edit-form .form-wrapper .form-wrapper { margin: inherit; }
|
||||
.edit-form .form-actions { display: none; }
|
||||
.edit-form input { max-width: 100%; }
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edit mode: toolbars
|
||||
*/
|
||||
|
||||
/* Trick: wrap statically positioned elements in relatively positioned element
|
||||
without changing its location. This allows us to absolutely position the
|
||||
toolbar.
|
||||
*/
|
||||
.edit-toolbar-container,
|
||||
.edit-form-container {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
vertical-align: baseline;
|
||||
z-index: 310;
|
||||
}
|
||||
.edit-toolbar-container {
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
-o-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.edit-toolbar-heightfaker {
|
||||
height: auto;
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* The toolbar; these are not necessarily visible. */
|
||||
.edit-toolbar {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
font-family: 'Droid sans', 'Lucida Grande', sans-serif;
|
||||
}
|
||||
.edit-toolbar-heightfaker {
|
||||
clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */
|
||||
}
|
||||
/* Exception: when used for a directly WYSIWYG editable field that is actively
|
||||
being edited. */
|
||||
.edit-type-direct-with-wysiwyg .edit-editing .edit-toolbar-heightfaker {
|
||||
width: 100%;
|
||||
clip: auto;
|
||||
}
|
||||
|
||||
|
||||
/* The toolbar contains toolgroups; these are visible. */
|
||||
.edit-toolgroup {
|
||||
float: left; /* LTR */
|
||||
}
|
||||
|
||||
/* Info toolgroup. */
|
||||
.edit-toolgroup.info {
|
||||
float: left; /* LTR */
|
||||
font-weight: bolder;
|
||||
padding: 0 5px;
|
||||
background: #fff url('../images/throbber.gif') no-repeat -60px 60px;
|
||||
}
|
||||
.edit-toolgroup.info.loading {
|
||||
padding-right: 35px;
|
||||
background-position: 90% 50%;
|
||||
}
|
||||
|
||||
/* Operations toolgroup. */
|
||||
.edit-toolgroup.ops {
|
||||
float: right; /* LTR */
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.edit-toolgroup.wysiwyg-tabs {
|
||||
float: right;
|
||||
}
|
||||
.edit-toolgroup.wysiwyg {
|
||||
clear: left;
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edit mode: buttons (in both modal and toolbar).
|
||||
*/
|
||||
#edit_modal button,
|
||||
.edit-toolbar button {
|
||||
float: left; /* LTR */
|
||||
display: block;
|
||||
height: 29px;
|
||||
min-width: 29px;
|
||||
padding: 3px 6px 6px 6px;
|
||||
margin: 4px 5px 1px 0;
|
||||
border: 1px solid #fff;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#edit_modal button {
|
||||
float: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Button with icons. */
|
||||
#edit_modal button span,
|
||||
.edit-toolbar button span {
|
||||
width: 22px;
|
||||
height: 19px;
|
||||
display: block;
|
||||
float: left;
|
||||
}
|
||||
.edit-toolbar span.close {
|
||||
background: url('../images/close.png') no-repeat 3px 2px;
|
||||
text-indent: -999em;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.edit-toolbar button.blank-button {
|
||||
color: black;
|
||||
background-color: #fff;
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
#edit_modal button.blue-button,
|
||||
.edit-toolbar button.blue-button {
|
||||
color: white;
|
||||
background-image: -webkit-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
|
||||
background-image: -moz-linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
|
||||
background-image: linear-gradient(top, #6fc2f2 0%, #4e97c0 100%);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#edit_modal button.gray-button,
|
||||
.edit-toolbar button.gray-button {
|
||||
color: #666;
|
||||
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
|
||||
background-image: -moz-linear-gradient(top, #f5f5f5 0%, #ccc 100%);
|
||||
background-image: linear-gradient(top, #f5f5f5 0%, #ccc 100%);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#edit_modal button.blue-button:hover,
|
||||
.edit-toolbar button.blue-button:hover,
|
||||
#edit_modal button.blue-button:active,
|
||||
.edit-toolbar button.blue-button:active {
|
||||
border: 1px solid #55a5d3;
|
||||
box-shadow: 0 2px 1px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
#edit_modal button.gray-button:hover,
|
||||
.edit-toolbar button.gray-button:hover,
|
||||
#edit_modal button.gray-button:active,
|
||||
.edit-toolbar button.gray-button:active {
|
||||
border: 1px solid #cdcdcd;
|
||||
box-shadow: 0 2px 1px rgba(0,0,0,0.1);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name = Edit
|
||||
description = In-place content editing.
|
||||
package = Core
|
||||
core = 8.x
|
||||
|
||||
dependencies[] = field
|
|
@ -0,0 +1,159 @@
|
|||
<?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);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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'
|
Binary file not shown.
After Width: | Height: | Size: 409 B |
Binary file not shown.
After Width: | Height: | Size: 333 B |
Binary file not shown.
After Width: | Height: | Size: 358 B |
Binary file not shown.
After Width: | Height: | Size: 498 B |
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
|
@ -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);
|
|
@ -0,0 +1,164 @@
|
|||
/**
|
||||
* @file
|
||||
* Backbone.sync implementation for Edit. This is the beating heart.
|
||||
*/
|
||||
(function (jQuery, Backbone, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Backbone.defaultSync = Backbone.sync;
|
||||
Backbone.sync = function(method, model, options) {
|
||||
if (options.editor.options.editorName === 'form') {
|
||||
return Backbone.syncDrupalFormWidget(method, model, options);
|
||||
}
|
||||
else {
|
||||
return Backbone.syncDirect(method, model, options);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs syncing for "form" PredicateEditor widgets.
|
||||
*
|
||||
* Implemented on top of Form API and the AJAX commands framework. Sets up
|
||||
* scoped AJAX command closures specifically for a given PredicateEditor widget
|
||||
* (which contains a pre-existing form). By submitting the form through
|
||||
* Drupal.ajax and leveraging Drupal.ajax' ability to have scoped (per-instance)
|
||||
* command implementations, we are able to update the VIE model, re-render the
|
||||
* form when there are validation errors and ensure no Drupal.ajax memory leaks.
|
||||
*
|
||||
* @see Drupal.edit.util.form
|
||||
*/
|
||||
Backbone.syncDrupalFormWidget = function(method, model, options) {
|
||||
if (method === 'update') {
|
||||
var predicate = options.editor.options.property;
|
||||
|
||||
var $formContainer = options.editor.$formContainer;
|
||||
var $submit = $formContainer.find('.edit-form-submit');
|
||||
var base = $submit.attr('id');
|
||||
|
||||
// Successfully saved.
|
||||
Drupal.ajax[base].commands.editFieldFormSaved = function(ajax, response, status) {
|
||||
Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
|
||||
|
||||
// Call Backbone.sync's success callback with the rerendered field.
|
||||
var changedAttributes = {};
|
||||
// @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216)
|
||||
// Once full JSON-LD support in Drupal core lands, we can ensure that the
|
||||
// models that VIE maintains are properly updated.
|
||||
changedAttributes[predicate] = undefined;
|
||||
changedAttributes[predicate + '/rendered'] = response.data;
|
||||
options.success(changedAttributes);
|
||||
};
|
||||
|
||||
// Unsuccessfully saved; validation errors.
|
||||
Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) {
|
||||
// Call Backbone.sync's error callback with the validation error messages.
|
||||
options.error(response.data);
|
||||
};
|
||||
|
||||
// The edit_field_form AJAX command is only called upon loading the form for
|
||||
// the first time, and when there are validation errors in the form; Form
|
||||
// API then marks which form items have errors. Therefor, we have to replace
|
||||
// the existing form, unbind the existing Drupal.ajax instance and create a
|
||||
// new Drupal.ajax instance.
|
||||
Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) {
|
||||
Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
|
||||
|
||||
Drupal.ajax.prototype.commands.insert(ajax, {
|
||||
data: response.data,
|
||||
selector: '#' + $formContainer.attr('id') + ' form'
|
||||
});
|
||||
|
||||
// Create a Drupa.ajax instance for the re-rendered ("new") form.
|
||||
var $newSubmit = $formContainer.find('.edit-form-submit');
|
||||
Drupal.edit.util.form.ajaxifySaving({ nocssjs: false }, $newSubmit);
|
||||
};
|
||||
|
||||
// Click the form's submit button; the scoped AJAX commands above will
|
||||
// handle the server's response.
|
||||
$submit.trigger('click.edit');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Performs syncing for "direct" PredicateEditor widgets.
|
||||
*
|
||||
* @see Backbone.syncDrupalFormWidget()
|
||||
* @see Drupal.edit.util.form
|
||||
*/
|
||||
Backbone.syncDirect = function(method, model, options) {
|
||||
if (method === 'update') {
|
||||
var fillAndSubmitForm = function(value) {
|
||||
jQuery('#edit_backstage form')
|
||||
// Fill in the value in any <input> that isn't hidden or a submit button.
|
||||
.find(':input[type!="hidden"][type!="submit"]:not(select)').val(value).end()
|
||||
// Submit the form.
|
||||
.find('.edit-form-submit').trigger('click.edit');
|
||||
};
|
||||
var entity = options.editor.options.entity;
|
||||
var predicate = options.editor.options.property;
|
||||
var value = model.get(predicate);
|
||||
|
||||
// If form doesn't already exist, load it and then submit.
|
||||
if (jQuery('#edit_backstage form').length === 0) {
|
||||
var formOptions = {
|
||||
propertyID: Drupal.edit.util.calcPropertyID(entity, predicate),
|
||||
$editorElement: options.editor.element,
|
||||
nocssjs: true
|
||||
};
|
||||
Drupal.edit.util.form.load(formOptions, function(form, ajax) {
|
||||
// Create a backstage area for storing forms that are hidden from view
|
||||
// (hence "backstage" — since the editing doesn't happen in the form, it
|
||||
// happens "directly" in the content, the form is only used for saving).
|
||||
jQuery(Drupal.theme('editBackstage', { id: 'edit_backstage' })).appendTo('body');
|
||||
// Direct forms are stuffed into #edit_backstage, apparently.
|
||||
jQuery('#edit_backstage').append(form);
|
||||
// Disable the browser's HTML5 validation; we only care about server-
|
||||
// side validation. (Not disabling this will actually cause problems
|
||||
// because browsers don't like to set HTML5 validation errors on hidden
|
||||
// forms.)
|
||||
jQuery('#edit_backstage form').attr('novalidate', true);
|
||||
var $submit = jQuery('#edit_backstage form .edit-form-submit');
|
||||
var base = Drupal.edit.util.form.ajaxifySaving(formOptions, $submit);
|
||||
|
||||
// Successfully saved.
|
||||
Drupal.ajax[base].commands.editFieldFormSaved = function (ajax, response, status) {
|
||||
Drupal.edit.util.form.unajaxifySaving(jQuery(ajax.element));
|
||||
jQuery('#edit_backstage form').remove();
|
||||
|
||||
// Call Backbone.sync's success callback with the rerendered field.
|
||||
var changedAttributes = {};
|
||||
// @todo: POSTPONED_ON(Drupal core, http://drupal.org/node/1784216)
|
||||
// Once full JSON-LD support in Drupal core lands, we can ensure that the
|
||||
// models that VIE maintains are properly updated.
|
||||
changedAttributes[predicate] = jQuery(response.data).find('.field-item').html();
|
||||
changedAttributes[predicate + '/rendered'] = response.data;
|
||||
options.success(changedAttributes);
|
||||
};
|
||||
|
||||
// Unsuccessfully saved; validation errors.
|
||||
Drupal.ajax[base].commands.editFieldFormValidationErrors = function(ajax, response, status) {
|
||||
// Call Backbone.sync's error callback with the validation error messages.
|
||||
options.error(response.data);
|
||||
};
|
||||
|
||||
// The editFieldForm AJAX command is only called upon loading the form
|
||||
// for the first time, and when there are validation errors in the form;
|
||||
// Form API then marks which form items have errors. This is useful for
|
||||
// "form" editors, but pointless for "direct" editors: the form itself
|
||||
// won't be visible at all anyway! Therefor, we ignore the new form and
|
||||
// we continue to use the existing form.
|
||||
Drupal.ajax[base].commands.editFieldForm = function(ajax, response, status) {
|
||||
// no-op
|
||||
};
|
||||
|
||||
fillAndSubmitForm(value);
|
||||
});
|
||||
}
|
||||
else {
|
||||
fillAndSubmitForm(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, Backbone, Drupal);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* @file
|
||||
* Behaviors for Edit, including the one that initializes Edit's EditAppView.
|
||||
*/
|
||||
(function ($, _, Backbone, Drupal, drupalSettings) {
|
||||
|
||||
"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 || {};
|
||||
Drupal.edit.metadataCache = Drupal.edit.metadataCache || {};
|
||||
|
||||
/**
|
||||
* Attach toggling behavior and in-place editing.
|
||||
*/
|
||||
Drupal.behaviors.edit = {
|
||||
attach: function(context) {
|
||||
var $context = $(context);
|
||||
var $fields = $context.find('[data-edit-id]');
|
||||
|
||||
// Initialize the Edit app.
|
||||
$context.find('#toolbar-tab-edit').once('edit-init', Drupal.edit.init);
|
||||
|
||||
var annotateField = function(field) {
|
||||
if (_.has(Drupal.edit.metadataCache, field.editID)) {
|
||||
var meta = Drupal.edit.metadataCache[field.editID];
|
||||
|
||||
field.$el.addClass((meta.access) ? 'edit-allowed' : 'edit-disallowed');
|
||||
if (meta.access) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Find all fields in the context without metadata.
|
||||
var fieldsToAnnotate = _.map($fields.not('.edit-allowed, .edit-disallowed'), function(el) {
|
||||
var $el = $(el);
|
||||
return { $el: $el, editID: $el.attr('data-edit-id') };
|
||||
});
|
||||
|
||||
// Fields whose metadata is known (typically when they were just modified)
|
||||
// can be annotated immediately, those remaining must be requested.
|
||||
var remainingFieldsToAnnotate = _.reduce(fieldsToAnnotate, function(result, field) {
|
||||
if (!annotateField(field)) {
|
||||
result.push(field);
|
||||
}
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
// Make fields that could be annotated immediately available for editing.
|
||||
Drupal.edit.app.findEditableProperties($context);
|
||||
|
||||
if (remainingFieldsToAnnotate.length) {
|
||||
$(window).ready(function() {
|
||||
$.ajax({
|
||||
url: drupalSettings.edit.metadataURL,
|
||||
type: 'POST',
|
||||
data: { 'fields[]' : _.pluck(remainingFieldsToAnnotate, 'editID') },
|
||||
dataType: 'json',
|
||||
success: function(results) {
|
||||
// Update the metadata cache.
|
||||
_.each(results, function(metadata, editID) {
|
||||
Drupal.edit.metadataCache[editID] = metadata;
|
||||
});
|
||||
|
||||
// Annotate the remaining fields based on the updated access cache.
|
||||
_.each(remainingFieldsToAnnotate, annotateField);
|
||||
|
||||
// As soon as there is at least one editable field, show the Edit
|
||||
// tab in the toolbar.
|
||||
if ($fields.filter('.edit-allowed').length) {
|
||||
$('.toolbar .icon-edit.edit-nothing-editable-hidden')
|
||||
.removeClass('edit-nothing-editable-hidden');
|
||||
}
|
||||
|
||||
// Find editable fields, make them editable.
|
||||
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, drupalSettings);
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* @file
|
||||
* Provides overridable theme functions for all of Edit's client-side HTML.
|
||||
*/
|
||||
(function($, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Theme function for the overlay of the Edit module.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - None.
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editOverlay = function(settings) {
|
||||
var html = '';
|
||||
html += '<div id="edit_overlay" />';
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a "backstage" for the Edit module.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - id: the id to apply to the backstage.
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editBackstage = function(settings) {
|
||||
var html = '';
|
||||
html += '<div id="' + settings.id + '" />';
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a modal of the Edit module.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - None.
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editModal = function(settings) {
|
||||
var classes = 'edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast';
|
||||
var html = '';
|
||||
html += '<div id="edit_modal" class="' + classes + '" role="dialog">';
|
||||
html += ' <div class="main"><p></p></div>';
|
||||
html += ' <div class="actions"></div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a toolbar container of the Edit module.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - id: the id to apply to the toolbar container.
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editToolbarContainer = function(settings) {
|
||||
var html = '';
|
||||
html += '<div id="' + settings.id + '" class="edit-toolbar-container edit-animate-invisible edit-animate-only-visibility">';
|
||||
html += ' <div class="edit-toolbar-heightfaker edit-animate-fast">';
|
||||
html += ' <div class="edit-toolbar primary" />';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a toolbar toolgroup of the Edit module.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - classes: the class of the toolgroup.
|
||||
* - buttons: @see Drupal.theme.prototype.editButtons().
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editToolgroup = function(settings) {
|
||||
var classes = 'edit-toolgroup edit-animate-slow edit-animate-invisible edit-animate-delay-veryfast';
|
||||
var html = '';
|
||||
html += '<div class="' + classes + ' ' + settings.classes + '">';
|
||||
html += Drupal.theme('editButtons', { buttons: settings.buttons });
|
||||
html += '</div>';
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for buttons of the Edit module.
|
||||
*
|
||||
* Can be used for the buttons both in the toolbar toolgroups and in the modal.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - buttons: an array of objects with the following keys:
|
||||
* - type: the type of the button (defaults to 'button')
|
||||
* - classes: the classes of the button.
|
||||
* - label: the label of the button.
|
||||
* - action: sets a data-edit-modal-action attribute.
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editButtons = function(settings) {
|
||||
var html = '';
|
||||
for (var i = 0; i < settings.buttons.length; i++) {
|
||||
var button = settings.buttons[i];
|
||||
if (!button.hasOwnProperty('type')) {
|
||||
button.type = 'button';
|
||||
}
|
||||
|
||||
html += '<button type="' + button.type + '" class="' + button.classes + '"';
|
||||
html += (button.action) ? ' data-edit-modal-action="' + button.action + '"' : '';
|
||||
html += '>';
|
||||
html += button.label;
|
||||
html += '</button>';
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* Theme function for a form container of the Edit module.
|
||||
*
|
||||
* @param settings
|
||||
* An object with the following keys:
|
||||
* - id: the id to apply to the toolbar container.
|
||||
* - loadingMsg: The message to show while loading.
|
||||
* @return
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.editFormContainer = function(settings) {
|
||||
var html = '';
|
||||
html += '<div id="' + settings.id + '" class="edit-form-container">';
|
||||
html += ' <div class="edit-form">';
|
||||
html += ' <div class="placeholder">';
|
||||
html += settings.loadingMsg;
|
||||
html += ' </div>';
|
||||
html += ' </div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
};
|
||||
|
||||
/**
|
||||
* A region to post messages that a screen reading UA will announce.
|
||||
*
|
||||
* @return {String}
|
||||
* A string representing a DOM fragment.
|
||||
*/
|
||||
Drupal.theme.editMessageBox = function() {
|
||||
return '<div id="edit-messages" class="element-invisible" role="region" aria-live="polite"></div>';
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrap message strings in p tags.
|
||||
*
|
||||
* @return {String}
|
||||
* A string representing a DOM fragment.
|
||||
*/
|
||||
Drupal.theme.editMessage = function() {
|
||||
var messages = Array.prototype.slice.call(arguments);
|
||||
var output = '';
|
||||
for (var i = 0; i < messages.length; i++) {
|
||||
output += '<p>' + messages[i] + '</p>';
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
|
@ -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);
|
|
@ -0,0 +1,297 @@
|
|||
/**
|
||||
* @file
|
||||
* VIE DOM parsing service for Edit.
|
||||
*/
|
||||
(function(jQuery, _, VIE, Drupal, drupalSettings) {
|
||||
|
||||
"use strict";
|
||||
|
||||
VIE.prototype.EditService = function (options) {
|
||||
var defaults = {
|
||||
name: 'edit',
|
||||
subjectSelector: '.edit-field.edit-allowed'
|
||||
};
|
||||
this.options = _.extend({}, defaults, options);
|
||||
|
||||
this.views = [];
|
||||
this.vie = null;
|
||||
this.name = this.options.name;
|
||||
};
|
||||
|
||||
VIE.prototype.EditService.prototype = {
|
||||
load: function (loadable) {
|
||||
var correct = loadable instanceof this.vie.Loadable;
|
||||
if (!correct) {
|
||||
throw new Error('Invalid Loadable passed');
|
||||
}
|
||||
|
||||
var element;
|
||||
if (!loadable.options.element) {
|
||||
if (typeof document === 'undefined') {
|
||||
return loadable.resolve([]);
|
||||
} else {
|
||||
element = drupalSettings.edit.context;
|
||||
}
|
||||
} else {
|
||||
element = loadable.options.element;
|
||||
}
|
||||
|
||||
var entities = this.readEntities(element);
|
||||
loadable.resolve(entities);
|
||||
},
|
||||
|
||||
_getViewForElement:function (element, collectionView) {
|
||||
var viewInstance;
|
||||
|
||||
jQuery.each(this.views, function () {
|
||||
if (jQuery(this.el).get(0) === element.get(0)) {
|
||||
if (collectionView && !this.template) {
|
||||
return true;
|
||||
}
|
||||
viewInstance = this;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return viewInstance;
|
||||
},
|
||||
|
||||
_registerEntityView:function (entity, element, isNew) {
|
||||
if (!element.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's only have this overhead for direct types. Form-based editors are
|
||||
// handled in backbone.drupalform.js and the PropertyEditor instance.
|
||||
if (!jQuery(element).hasClass('edit-type-direct')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var service = this;
|
||||
var viewInstance = this._getViewForElement(element);
|
||||
if (viewInstance) {
|
||||
return viewInstance;
|
||||
}
|
||||
|
||||
viewInstance = new this.vie.view.Entity({
|
||||
model:entity,
|
||||
el:element,
|
||||
tagName:element.get(0).nodeName,
|
||||
vie:this.vie,
|
||||
service:this.name
|
||||
});
|
||||
|
||||
this.views.push(viewInstance);
|
||||
|
||||
return viewInstance;
|
||||
},
|
||||
|
||||
save: function(saveable) {
|
||||
var correct = saveable instanceof this.vie.Savable;
|
||||
if (!correct) {
|
||||
throw "Invalid Savable passed";
|
||||
}
|
||||
|
||||
if (!saveable.options.element) {
|
||||
// FIXME: we could find element based on subject
|
||||
throw "Unable to write entity to edit.module-markup, no element given";
|
||||
}
|
||||
|
||||
if (!saveable.options.entity) {
|
||||
throw "Unable to write to edit.module-markup, no entity given";
|
||||
}
|
||||
|
||||
var $element = jQuery(saveable.options.element);
|
||||
this._writeEntity(saveable.options.entity, saveable.options.element);
|
||||
saveable.resolve();
|
||||
},
|
||||
|
||||
_writeEntity:function (entity, element) {
|
||||
var service = this;
|
||||
this.findPredicateElements(this.getElementSubject(element), element, true).each(function () {
|
||||
var predicateElement = jQuery(this);
|
||||
var predicate = service.getElementPredicate(predicateElement);
|
||||
if (!entity.has(predicate)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var value = entity.get(predicate);
|
||||
if (value && value.isCollection) {
|
||||
// Handled by CollectionViews separately
|
||||
return true;
|
||||
}
|
||||
if (value === service.readElementValue(predicate, predicateElement)) {
|
||||
return true;
|
||||
}
|
||||
// Unlike in the VIE's RdfaService no (re-)mapping needed here.
|
||||
predicateElement.html(value);
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
||||
// The edit-id data attribute contains the full identifier of
|
||||
// each entity element in the format
|
||||
// `<entity type>:<id>:<field name>:<language code>:<view mode>`.
|
||||
_getID: function (element) {
|
||||
var id = jQuery(element).attr('data-edit-id');
|
||||
if (!id) {
|
||||
id = jQuery(element).closest('[data-edit-id]').attr('data-edit-id');
|
||||
}
|
||||
return id;
|
||||
},
|
||||
|
||||
// Returns the "URI" of an entity of an element in format
|
||||
// `<entity type>/<id>`.
|
||||
getElementSubject: function (element) {
|
||||
return this._getID(element).split(':').slice(0, 2).join('/');
|
||||
},
|
||||
|
||||
// Returns the field name for an element in format
|
||||
// `<field name>/<language code>/<view mode>`.
|
||||
// (Slashes instead of colons because the field name is no namespace.)
|
||||
getElementPredicate: function (element) {
|
||||
if (!this._getID(element)) {
|
||||
throw new Error('Could not find predicate for element');
|
||||
}
|
||||
return this._getID(element).split(':').slice(2, 5).join('/');
|
||||
},
|
||||
|
||||
getElementType: function (element) {
|
||||
return this._getID(element).split(':').slice(0, 1)[0];
|
||||
},
|
||||
|
||||
// Reads all editable entities (currently each Drupal field is considered an
|
||||
// entity, in the future Drupal entities should be mapped to VIE entities)
|
||||
// from DOM and returns the VIE enties it found.
|
||||
readEntities: function (element) {
|
||||
var service = this;
|
||||
var entities = [];
|
||||
var entityElements = jQuery(this.options.subjectSelector, element);
|
||||
entityElements = entityElements.add(jQuery(element).filter(this.options.subjectSelector));
|
||||
entityElements.each(function () {
|
||||
var entity = service._readEntity(jQuery(this));
|
||||
if (entity) {
|
||||
entities.push(entity);
|
||||
}
|
||||
});
|
||||
return entities;
|
||||
},
|
||||
|
||||
// Returns a filled VIE Entity instance for a DOM element. The Entity
|
||||
// is also registered in the VIE entities collection.
|
||||
_readEntity: function (element) {
|
||||
var subject = this.getElementSubject(element);
|
||||
var type = this.getElementType(element);
|
||||
var entity = this._readEntityPredicates(subject, element, false);
|
||||
if (jQuery.isEmptyObject(entity)) {
|
||||
return null;
|
||||
}
|
||||
entity['@subject'] = subject;
|
||||
if (type) {
|
||||
entity['@type'] = this._registerType(type, element);
|
||||
}
|
||||
|
||||
var entityInstance = new this.vie.Entity(entity);
|
||||
entityInstance = this.vie.entities.addOrUpdate(entityInstance, {
|
||||
updateOptions: {
|
||||
silent: true,
|
||||
ignoreChanges: true
|
||||
}
|
||||
});
|
||||
|
||||
this._registerEntityView(entityInstance, element);
|
||||
return entityInstance;
|
||||
},
|
||||
|
||||
_registerType: function (typeId, element) {
|
||||
typeId = '<http://viejs.org/ns/' + typeId + '>';
|
||||
var type = this.vie.types.get(typeId);
|
||||
if (!type) {
|
||||
this.vie.types.add(typeId, []);
|
||||
type = this.vie.types.get(typeId);
|
||||
}
|
||||
|
||||
var predicate = this.getElementPredicate(element);
|
||||
if (type.attributes.get(predicate)) {
|
||||
return type;
|
||||
}
|
||||
|
||||
var label = element.data('edit-field-label');
|
||||
var range = 'Form';
|
||||
if (element.hasClass('edit-type-direct')) {
|
||||
range = 'Direct';
|
||||
}
|
||||
if (element.hasClass('edit-type-direct-with-wysiwyg')) {
|
||||
range = 'Wysiwyg';
|
||||
}
|
||||
type.attributes.add(predicate, [range], 0, 1, {
|
||||
label: element.data('edit-field-label')
|
||||
});
|
||||
|
||||
return type;
|
||||
},
|
||||
|
||||
_readEntityPredicates: function (subject, element, emptyValues) {
|
||||
var entityPredicates = {};
|
||||
var service = this;
|
||||
this.findPredicateElements(subject, element, true).each(function () {
|
||||
var predicateElement = jQuery(this);
|
||||
var predicate = service.getElementPredicate(predicateElement);
|
||||
if (!predicate) {
|
||||
return;
|
||||
}
|
||||
var value = service.readElementValue(predicate, predicateElement);
|
||||
if (value === null && !emptyValues) {
|
||||
return;
|
||||
}
|
||||
|
||||
entityPredicates[predicate] = value;
|
||||
entityPredicates[predicate + '/rendered'] = predicateElement[0].outerHTML;
|
||||
});
|
||||
return entityPredicates;
|
||||
},
|
||||
|
||||
readElementValue : function(predicate, element) {
|
||||
// Unlike in RdfaService there is parsing needed here.
|
||||
if (element.hasClass('edit-type-form')) {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
return jQuery.trim(element.html());
|
||||
}
|
||||
},
|
||||
|
||||
// Subject elements are the DOM elements containing a single or multiple
|
||||
// editable fields.
|
||||
findSubjectElements: function (element) {
|
||||
if (!element) {
|
||||
element = drupalSettings.edit.context;
|
||||
}
|
||||
return jQuery(this.options.subjectSelector, element);
|
||||
},
|
||||
|
||||
// Predicate Elements are the actual DOM elements that users will be able
|
||||
// to edit.
|
||||
findPredicateElements: function (subject, element, allowNestedPredicates, stop) {
|
||||
var predicates = jQuery();
|
||||
// Make sure that element is wrapped by jQuery.
|
||||
var $element = jQuery(element);
|
||||
|
||||
// Form-type predicates
|
||||
predicates = predicates.add($element.filter('.edit-type-form'));
|
||||
|
||||
// Direct-type predicates
|
||||
var direct = $element.filter('.edit-type-direct');
|
||||
predicates = predicates.add(direct.find('.field-item'));
|
||||
|
||||
if (!predicates.length && !stop) {
|
||||
var parentElement = $element.parent(this.options.subjectSelector);
|
||||
if (parentElement.length) {
|
||||
return this.findPredicateElements(subject, parentElement, allowNestedPredicates, true);
|
||||
}
|
||||
}
|
||||
|
||||
return predicates;
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, _, VIE, Drupal, drupalSettings);
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides the app-level interactive menu.
|
||||
*/
|
||||
(function($, _, Backbone, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.edit = Drupal.edit || {};
|
||||
Drupal.edit.views = Drupal.edit.views || {};
|
||||
Drupal.edit.views.MenuView = Backbone.View.extend({
|
||||
|
||||
events: {
|
||||
'click #toolbar-tab-edit': 'editClickHandler'
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements Backbone Views' initialize() function.
|
||||
*/
|
||||
initialize: function() {
|
||||
_.bindAll(this, 'stateChange');
|
||||
this.model.on('change:isViewing', this.stateChange);
|
||||
// @todo
|
||||
// Re-implement hook_toolbar and the corresponding JavaScript behaviors
|
||||
// once https://drupal.org/node/1847198 is resolved. The toolbar tray is
|
||||
// necessary when the page request is processed because its render element
|
||||
// has an #attached property with the Edit module library code assigned to
|
||||
// it. Currently a toolbar tab is not passed as a renderable array, so
|
||||
// #attached properties are not processed. The toolbar tray DOM element is
|
||||
// unnecessary right now, so it is removed.
|
||||
this.$el.find('#toolbar-tray-edit').remove();
|
||||
// Respond to clicks on other toolbar tabs. This temporary pending
|
||||
// improvements to the toolbar module.
|
||||
$('#toolbar-administration').on('click.edit', '.bar a:not(#toolbar-tab-edit)', _.bind(function (event) {
|
||||
this.model.set('isViewing', true);
|
||||
}, this));
|
||||
// We have to call stateChange() here because URL fragments are not passed
|
||||
// to the server, thus the wrong anchor may be marked as active.
|
||||
this.stateChange();
|
||||
},
|
||||
|
||||
/**
|
||||
* Listens to app state changes.
|
||||
*/
|
||||
stateChange: function() {
|
||||
var isViewing = this.model.get('isViewing');
|
||||
// Toggle the state of the Toolbar Edit tab based on the isViewing state.
|
||||
this.$el.find('#toolbar-tab-edit')
|
||||
.toggleClass('active', !isViewing)
|
||||
.attr('aria-pressed', !isViewing);
|
||||
// Manage the toolbar state until
|
||||
// https://drupal.org/node/1847198 is resolved
|
||||
if (!isViewing) {
|
||||
// Remove the 'toolbar-tray-open' class from the body element.
|
||||
this.$el.removeClass('toolbar-tray-open');
|
||||
// Deactivate any other active tabs and trays.
|
||||
this.$el
|
||||
.find('.bar a', '#toolbar-administration')
|
||||
.not('#toolbar-tab-edit')
|
||||
.add('.tray', '#toolbar-administration')
|
||||
.removeClass('active');
|
||||
// Set the height of the toolbar.
|
||||
if ('toolbar' in Drupal) {
|
||||
Drupal.toolbar.setHeight();
|
||||
}
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Handles clicks on the edit tab of the toolbar.
|
||||
*
|
||||
* @param {Object} event
|
||||
*/
|
||||
editClickHandler: function (event) {
|
||||
var isViewing = this.model.get('isViewing');
|
||||
// Toggle the href of the Toolbar Edit tab based on the isViewing state. The
|
||||
// href value should represent to state to be entered.
|
||||
this.$el.find('#toolbar-tab-edit').attr('href', (isViewing) ? '#edit' : '#view');
|
||||
this.model.set('isViewing', !isViewing);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, _, Backbone, Drupal);
|
|
@ -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);
|
|
@ -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);
|
|
@ -0,0 +1,324 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that decorates a Property Editor widget.
|
||||
*
|
||||
* It listens to state changes of the property editor.
|
||||
*/
|
||||
(function($, Backbone, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.edit = Drupal.edit || {};
|
||||
Drupal.edit.views = Drupal.edit.views || {};
|
||||
Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
|
||||
|
||||
editor: null,
|
||||
entity: null,
|
||||
predicate : null,
|
||||
editorName: null,
|
||||
toolbarId: null,
|
||||
|
||||
_widthAttributeIsEmpty: null,
|
||||
|
||||
events: {
|
||||
'mouseenter.edit' : 'onMouseEnter',
|
||||
'mouseleave.edit' : 'onMouseLeave',
|
||||
'tabIn.edit': 'onMouseEnter',
|
||||
'tabOut.edit': 'onMouseLeave'
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements Backbone Views' initialize() function.
|
||||
*
|
||||
* @param options
|
||||
* An object with the following keys:
|
||||
* - editor: the editor object with an 'options' object that has these keys:
|
||||
* * entity: the VIE entity for the property.
|
||||
* * property: the predicate of the property.
|
||||
* * editorName: the editor name: 'form', 'direct' or
|
||||
* 'direct-with-wysiwyg'.
|
||||
* * widget: the parent EditableeEntity widget.
|
||||
* - toolbarId: the ID attribute of the toolbar as rendered in the DOM.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
this.editor = options.editor;
|
||||
this.toolbarId = options.toolbarId;
|
||||
|
||||
this.entity = this.editor.options.entity;
|
||||
this.predicate = this.editor.options.property;
|
||||
this.editorName = this.editor.options.editorName;
|
||||
|
||||
this.$el.css('background-color', this._getBgColor(this.$el));
|
||||
},
|
||||
|
||||
/**
|
||||
* Listens to editor state changes.
|
||||
*/
|
||||
stateChange: function(from, to) {
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
if (from !== null) {
|
||||
this.undecorate();
|
||||
}
|
||||
break;
|
||||
case 'candidate':
|
||||
this.decorate();
|
||||
if (from !== 'inactive') {
|
||||
this.stopHighlight();
|
||||
if (from !== 'highlighted') {
|
||||
this.stopEdit(this.editorName);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'highlighted':
|
||||
this.startHighlight();
|
||||
break;
|
||||
case 'activating':
|
||||
// NOTE: this step only exists for the 'form' editor! It is skipped by
|
||||
// the 'direct' and 'direct-with-wysiwyg' editors, because no loading is
|
||||
// necessary.
|
||||
this.prepareEdit(this.editorName);
|
||||
break;
|
||||
case 'active':
|
||||
if (this.editorName !== 'form') {
|
||||
this.prepareEdit(this.editorName);
|
||||
}
|
||||
this.startEdit(this.editorName);
|
||||
break;
|
||||
case 'changed':
|
||||
break;
|
||||
case 'saving':
|
||||
break;
|
||||
case 'saved':
|
||||
break;
|
||||
case 'invalid':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts hover: transition to 'highlight' state.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onMouseEnter: function(event) {
|
||||
var that = this;
|
||||
this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
|
||||
var editableEntity = that.editor.options.widget;
|
||||
editableEntity.setState('highlighted', that.predicate);
|
||||
event.stopPropagation();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops hover: back to 'candidate' state.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onMouseLeave: function(event) {
|
||||
var that = this;
|
||||
this._ignoreHoveringVia(event, '#' + this.toolbarId, function () {
|
||||
var editableEntity = that.editor.options.widget;
|
||||
editableEntity.setState('candidate', that.predicate, { reason: 'mouseleave' });
|
||||
event.stopPropagation();
|
||||
});
|
||||
},
|
||||
|
||||
decorate: function () {
|
||||
this.$el.addClass('edit-animate-fast edit-candidate edit-editable');
|
||||
},
|
||||
|
||||
undecorate: function () {
|
||||
this.$el
|
||||
.removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay');
|
||||
},
|
||||
|
||||
startHighlight: function () {
|
||||
// Animations.
|
||||
var that = this;
|
||||
setTimeout(function() {
|
||||
that.$el.addClass('edit-highlighted');
|
||||
}, 0);
|
||||
},
|
||||
|
||||
stopHighlight: function() {
|
||||
this.$el
|
||||
.removeClass('edit-highlighted');
|
||||
},
|
||||
|
||||
prepareEdit: function(editorName) {
|
||||
this.$el.addClass('edit-editing');
|
||||
|
||||
// While editing, don't show *any* other editors.
|
||||
// @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
|
||||
// Revisit this.
|
||||
$('.edit-candidate').not('.edit-editing').removeClass('edit-editable');
|
||||
|
||||
if (editorName === 'form') {
|
||||
this.$el.addClass('edit-belowoverlay');
|
||||
}
|
||||
},
|
||||
|
||||
startEdit: function(editorName) {
|
||||
if (editorName !== 'form') {
|
||||
this._pad();
|
||||
}
|
||||
},
|
||||
|
||||
stopEdit: function(editorName) {
|
||||
this.$el.removeClass('edit-highlighted edit-editing');
|
||||
|
||||
// Make the other editors show up again.
|
||||
// @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
|
||||
// Revisit this.
|
||||
$('.edit-candidate').addClass('edit-editable');
|
||||
|
||||
if (editorName === 'form') {
|
||||
this.$el.removeClass('edit-belowoverlay');
|
||||
}
|
||||
else {
|
||||
this._unpad();
|
||||
}
|
||||
},
|
||||
|
||||
_pad: function () {
|
||||
var self = this;
|
||||
|
||||
// Add 5px padding for readability. This means we'll freeze the current
|
||||
// width and *then* add 5px padding, hence ensuring the padding is added "on
|
||||
// the outside".
|
||||
// 1) Freeze the width (if it's not already set); don't use animations.
|
||||
if (this.$el[0].style.width === "") {
|
||||
this._widthAttributeIsEmpty = true;
|
||||
this.$el
|
||||
.addClass('edit-animate-disable-width')
|
||||
.css('width', this.$el.width());
|
||||
}
|
||||
|
||||
// 2) Add padding; use animations.
|
||||
var posProp = this._getPositionProperties(this.$el);
|
||||
setTimeout(function() {
|
||||
// Re-enable width animations (padding changes affect width too!).
|
||||
self.$el.removeClass('edit-animate-disable-width');
|
||||
|
||||
// Pad the editable.
|
||||
self.$el
|
||||
.css({
|
||||
'position': 'relative',
|
||||
'top': posProp.top - 5 + 'px',
|
||||
'left': posProp.left - 5 + 'px',
|
||||
'padding-top' : posProp['padding-top'] + 5 + 'px',
|
||||
'padding-left' : posProp['padding-left'] + 5 + 'px',
|
||||
'padding-right' : posProp['padding-right'] + 5 + 'px',
|
||||
'padding-bottom': posProp['padding-bottom'] + 5 + 'px',
|
||||
'margin-bottom': posProp['margin-bottom'] - 10 + 'px'
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
|
||||
_unpad: function () {
|
||||
var self = this;
|
||||
|
||||
// 1) Set the empty width again.
|
||||
if (this._widthAttributeIsEmpty) {
|
||||
this.$el
|
||||
.addClass('edit-animate-disable-width')
|
||||
.css('width', '');
|
||||
}
|
||||
|
||||
// 2) Remove padding; use animations (these will run simultaneously with)
|
||||
// the fading out of the toolbar as its gets removed).
|
||||
var posProp = this._getPositionProperties(this.$el);
|
||||
setTimeout(function() {
|
||||
// Re-enable width animations (padding changes affect width too!).
|
||||
self.$el.removeClass('edit-animate-disable-width');
|
||||
|
||||
// Unpad the editable.
|
||||
self.$el
|
||||
.css({
|
||||
'position': 'relative',
|
||||
'top': posProp.top + 5 + 'px',
|
||||
'left': posProp.left + 5 + 'px',
|
||||
'padding-top' : posProp['padding-top'] - 5 + 'px',
|
||||
'padding-left' : posProp['padding-left'] - 5 + 'px',
|
||||
'padding-right' : posProp['padding-right'] - 5 + 'px',
|
||||
'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
|
||||
'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the background color of an element (or the inherited one).
|
||||
*
|
||||
* @param $e
|
||||
* A DOM element.
|
||||
*/
|
||||
_getBgColor: function($e) {
|
||||
var c;
|
||||
|
||||
if ($e === null || $e[0].nodeName === 'HTML') {
|
||||
// Fallback to white.
|
||||
return 'rgb(255, 255, 255)';
|
||||
}
|
||||
c = $e.css('background-color');
|
||||
// TRICKY: edge case for Firefox' "transparent" here; this is a
|
||||
// browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724
|
||||
if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') {
|
||||
return this._getBgColor($e.parent());
|
||||
}
|
||||
return c;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the top and left properties of an element and convert extraneous
|
||||
* values and information into numbers ready for subtraction.
|
||||
*
|
||||
* @param $e
|
||||
* A DOM element.
|
||||
*/
|
||||
_getPositionProperties: function($e) {
|
||||
var p,
|
||||
r = {},
|
||||
props = [
|
||||
'top', 'left', 'bottom', 'right',
|
||||
'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
|
||||
'margin-bottom'
|
||||
];
|
||||
|
||||
var propCount = props.length;
|
||||
for (var i = 0; i < propCount; i++) {
|
||||
p = props[i];
|
||||
r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10);
|
||||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces blank or 'auto' CSS "position: <value>" values with "0px".
|
||||
*
|
||||
* @param pos
|
||||
* The value for a CSS position declaration.
|
||||
*/
|
||||
_replaceBlankPosition: function(pos) {
|
||||
if (pos === 'auto' || !pos) {
|
||||
pos = '0px';
|
||||
}
|
||||
return pos;
|
||||
},
|
||||
|
||||
/**
|
||||
* Ignores hovering to/from the given closest element, but as soon as a hover
|
||||
* occurs to/from *another* element, then call the given callback.
|
||||
*/
|
||||
_ignoreHoveringVia: function(event, closest, callback) {
|
||||
if ($(event.relatedTarget).closest(closest).length > 0) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, Backbone, Drupal);
|
|
@ -0,0 +1,465 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides an interactive toolbar (1 per property editor).
|
||||
*
|
||||
* It listens to state changes of the property editor. It also triggers state
|
||||
* changes in response to user interactions with the toolbar, including saving.
|
||||
*/
|
||||
(function ($, _, Backbone, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.edit = Drupal.edit || {};
|
||||
Drupal.edit.views = Drupal.edit.views || {};
|
||||
Drupal.edit.views.ToolbarView = Backbone.View.extend({
|
||||
|
||||
editor: null,
|
||||
$storageWidgetEl: null,
|
||||
|
||||
entity: null,
|
||||
predicate : null,
|
||||
editorName: null,
|
||||
|
||||
_loader: null,
|
||||
_loaderVisibleStart: 0,
|
||||
|
||||
_id: null,
|
||||
|
||||
events: {
|
||||
'click.edit button.label': 'onClickInfoLabel',
|
||||
'mouseleave.edit': 'onMouseLeave',
|
||||
'click.edit button.field-save': 'onClickSave',
|
||||
'click.edit button.field-close': 'onClickClose'
|
||||
},
|
||||
|
||||
/**
|
||||
* Implements Backbone Views' initialize() function.
|
||||
*
|
||||
* @param options
|
||||
* An object with the following keys:
|
||||
* - editor: the editor object with an 'options' object that has these keys:
|
||||
* * entity: the VIE entity for the property.
|
||||
* * property: the predicate of the property.
|
||||
* * editorName: the editor name: 'form', 'direct' or
|
||||
* 'direct-with-wysiwyg'.
|
||||
* * element: the jQuery-wrapped editor DOM element
|
||||
* - $storageWidgetEl: the DOM element on which the Create Storage widget is
|
||||
* initialized.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
this.editor = options.editor;
|
||||
this.$storageWidgetEl = options.$storageWidgetEl;
|
||||
|
||||
this.entity = this.editor.options.entity;
|
||||
this.predicate = this.editor.options.property;
|
||||
this.editorName = this.editor.options.editorName;
|
||||
|
||||
this._loader = null;
|
||||
this._loaderVisibleStart = 0;
|
||||
|
||||
// Generate a DOM-compatible ID for the toolbar DOM element.
|
||||
var propertyID = Drupal.edit.util.calcPropertyID(this.entity, this.predicate);
|
||||
this._id = 'edit-toolbar-for-' + propertyID.replace(/\//g, '_');
|
||||
},
|
||||
|
||||
/**
|
||||
* Listens to editor state changes.
|
||||
*/
|
||||
stateChange: function(from, to) {
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
// Nothing happens in this stage.
|
||||
break;
|
||||
case 'candidate':
|
||||
if (from !== 'inactive') {
|
||||
if (from !== 'highlighted' && this.editorName !== 'form') {
|
||||
this._unpad(this.editorName);
|
||||
}
|
||||
this.remove();
|
||||
}
|
||||
break;
|
||||
case 'highlighted':
|
||||
// As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title).
|
||||
this.render();
|
||||
this.startHighlight();
|
||||
break;
|
||||
case 'activating':
|
||||
this.setLoadingIndicator(true);
|
||||
break;
|
||||
case 'active':
|
||||
this.startEdit(this.editorName);
|
||||
this.setLoadingIndicator(false);
|
||||
if (this.editorName !== 'form') {
|
||||
this._pad(this.editorName);
|
||||
}
|
||||
if (this.editorName === 'direct-with-wysiwyg') {
|
||||
this.insertWYSIWYGToolGroups();
|
||||
}
|
||||
break;
|
||||
case 'changed':
|
||||
this.$el
|
||||
.find('button.save')
|
||||
.addClass('blue-button')
|
||||
.removeClass('gray-button');
|
||||
break;
|
||||
case 'saving':
|
||||
this.setLoadingIndicator(true);
|
||||
this.save();
|
||||
break;
|
||||
case 'saved':
|
||||
this.setLoadingIndicator(false);
|
||||
break;
|
||||
case 'invalid':
|
||||
this.setLoadingIndicator(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves a property.
|
||||
*
|
||||
* This method deals with the complexity of the editor-dependent ways of
|
||||
* inserting updated content and showing validation error messages.
|
||||
*
|
||||
* One might argue that this does not belong in a view. However, there is no
|
||||
* actual "save" logic here, that lives in Backbone.sync. This is just some
|
||||
* glue code, along with the logic for inserting updated content as well as
|
||||
* showing validation error messages, the latter of which is certainly okay.
|
||||
*/
|
||||
save: function() {
|
||||
var that = this;
|
||||
var editor = this.editor;
|
||||
var editableEntity = editor.options.widget;
|
||||
var entity = editor.options.entity;
|
||||
var predicate = editor.options.property;
|
||||
|
||||
// Use Create.js' Storage widget to handle saving. (Uses Backbone.sync.)
|
||||
this.$storageWidgetEl.createStorage('saveRemote', entity, {
|
||||
editor: editor,
|
||||
|
||||
// Successfully saved without validation errors.
|
||||
success: function (model) {
|
||||
editableEntity.setState('saved', predicate);
|
||||
|
||||
// Now that the changes to this property have been saved, the saved
|
||||
// attributes are now the "original" attributes.
|
||||
entity._originalAttributes = entity._previousAttributes = _.clone(entity.attributes);
|
||||
|
||||
// Get data necessary to rerender property before it is unavailable.
|
||||
var updatedProperty = entity.get(predicate + '/rendered');
|
||||
var $propertyWrapper = editor.element.closest('.edit-field');
|
||||
var $context = $propertyWrapper.parent();
|
||||
|
||||
editableEntity.setState('candidate', predicate);
|
||||
// Unset the property, because it will be parsed again from the DOM, iff
|
||||
// its new value causes it to still be rendered.
|
||||
entity.unset(predicate, { silent: true });
|
||||
entity.unset(predicate + '/rendered', { silent: true });
|
||||
// Trigger event to allow for proper clean-up of editor-specific views.
|
||||
editor.element.trigger('destroyedPropertyEditor.edit', editor);
|
||||
|
||||
// Replace the old content with the new content.
|
||||
$propertyWrapper.replaceWith(updatedProperty);
|
||||
Drupal.attachBehaviors($context);
|
||||
},
|
||||
|
||||
// Save attempted but failed due to validation errors.
|
||||
error: function (validationErrorMessages) {
|
||||
editableEntity.setState('invalid', predicate);
|
||||
|
||||
if (that.editorName === 'form') {
|
||||
editor.$formContainer
|
||||
.find('.edit-form')
|
||||
.addClass('edit-validation-error')
|
||||
.find('form')
|
||||
.prepend(validationErrorMessages);
|
||||
}
|
||||
else {
|
||||
var $errors = $('<div class="edit-validation-errors"></div>')
|
||||
.append(validationErrorMessages);
|
||||
editor.element
|
||||
.addClass('edit-validation-error')
|
||||
.after($errors);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* When the user clicks the info label, nothing should happen.
|
||||
* @note currently redirects the click.edit-event to the editor DOM element.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onClickInfoLabel: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// Redirects the event to the editor DOM element.
|
||||
this.editor.element.trigger('click.edit');
|
||||
},
|
||||
|
||||
/**
|
||||
* A mouseleave to the editor doesn't matter; a mouseleave to something else
|
||||
* counts as a mouseleave on the editor itself.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onMouseLeave: function(event) {
|
||||
var el = this.editor.element[0];
|
||||
if (event.relatedTarget != el && !$.contains(el, event.relatedTarget)) {
|
||||
this.editor.element.trigger('mouseleave.edit');
|
||||
}
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Upon clicking "Save", trigger a custom event to save this property.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onClickSave: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.editor.options.widget.setState('saving', this.predicate);
|
||||
},
|
||||
|
||||
/**
|
||||
* Upon clicking "Close", trigger a custom event to stop editing.
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onClickClose: function(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.editor.options.widget.setState('candidate', this.predicate, { reason: 'cancel' });
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates in the 'info' toolgroup that we're waiting for a server reponse.
|
||||
*
|
||||
* Prevents flickering loading indicator by only showing it after 0.6 seconds
|
||||
* and if it is shown, only hiding it after another 0.6 seconds.
|
||||
*
|
||||
* @param bool enabled
|
||||
* Whether the loading indicator should be displayed or not.
|
||||
*/
|
||||
setLoadingIndicator: function(enabled) {
|
||||
var that = this;
|
||||
if (enabled) {
|
||||
this._loader = setTimeout(function() {
|
||||
that.addClass('info', 'loading');
|
||||
that._loaderVisibleStart = new Date().getTime();
|
||||
}, 600);
|
||||
}
|
||||
else {
|
||||
var currentTime = new Date().getTime();
|
||||
clearTimeout(this._loader);
|
||||
if (this._loaderVisibleStart) {
|
||||
setTimeout(function() {
|
||||
that.removeClass('info', 'loading');
|
||||
}, this._loaderVisibleStart + 600 - currentTime);
|
||||
}
|
||||
this._loader = null;
|
||||
this._loaderVisibleStart = 0;
|
||||
}
|
||||
},
|
||||
|
||||
startHighlight: function() {
|
||||
// We get the label to show for this property from VIE's type system.
|
||||
var label = this.predicate;
|
||||
var attributeDef = this.entity.get('@type').attributes.get(this.predicate);
|
||||
if (attributeDef && attributeDef.metadata) {
|
||||
label = attributeDef.metadata.label;
|
||||
}
|
||||
|
||||
this.$el
|
||||
.find('.edit-toolbar')
|
||||
// Append the "info" toolgroup into the toolbar.
|
||||
.append(Drupal.theme('editToolgroup', {
|
||||
classes: 'info edit-animate-only-background-and-padding',
|
||||
buttons: [
|
||||
{ label: label, classes: 'blank-button label' }
|
||||
]
|
||||
}));
|
||||
|
||||
// Animations.
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.show('info');
|
||||
}, 0);
|
||||
},
|
||||
|
||||
startEdit: function() {
|
||||
this.$el
|
||||
.addClass('edit-editing')
|
||||
.find('.edit-toolbar')
|
||||
// Append the "ops" toolgroup into the toolbar.
|
||||
.append(Drupal.theme('editToolgroup', {
|
||||
classes: 'ops',
|
||||
buttons: [
|
||||
{ label: Drupal.t('Save'), type: 'submit', classes: 'field-save save gray-button' },
|
||||
{ label: '<span class="close">' + Drupal.t('Close') + '</span>', classes: 'field-close close gray-button' }
|
||||
]
|
||||
}));
|
||||
this.show('ops');
|
||||
},
|
||||
|
||||
/**
|
||||
* Adjusts the toolbar to accomodate padding on the PropertyEditor widget.
|
||||
*
|
||||
* @see PropertyEditorDecorationView._pad().
|
||||
*/
|
||||
_pad: function(editorName) {
|
||||
// The whole toolbar must move to the top when the property's DOM element
|
||||
// is displayed inline.
|
||||
if (this.editor.element.css('display') === 'inline') {
|
||||
this.$el.css('top', parseInt(this.$el.css('top'), 10) - 5 + 'px');
|
||||
}
|
||||
|
||||
// The toolbar must move to the top and the left.
|
||||
var $hf = this.$el.find('.edit-toolbar-heightfaker');
|
||||
$hf.css({ bottom: '6px', left: '-5px' });
|
||||
// When using a WYSIWYG editor, the width of the toolbar must match the
|
||||
// width of the editable.
|
||||
if (editorName === 'direct-with-wysiwyg') {
|
||||
$hf.css({ width: this.editor.element.width() + 10 });
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Undoes the changes made by _pad().
|
||||
*
|
||||
* @see PropertyEditorDecorationView._unpad().
|
||||
*/
|
||||
_unpad: function(editorName) {
|
||||
// Move the toolbar back to its original position.
|
||||
var $hf = this.$el.find('.edit-toolbar-heightfaker');
|
||||
$hf.css({ bottom: '1px', left: '' });
|
||||
// When using a WYSIWYG editor, restore the width of the toolbar.
|
||||
if (editorName === 'direct-with-wysiwyg') {
|
||||
$hf.css({ width: '' });
|
||||
}
|
||||
},
|
||||
|
||||
insertWYSIWYGToolGroups: function() {
|
||||
this.$el
|
||||
.find('.edit-toolbar')
|
||||
.append(Drupal.theme('editToolgroup', {
|
||||
classes: 'wysiwyg-tabs',
|
||||
buttons: []
|
||||
}))
|
||||
.append(Drupal.theme('editToolgroup', {
|
||||
classes: 'wysiwyg',
|
||||
buttons: []
|
||||
}));
|
||||
|
||||
// Animate the toolgroups into visibility.
|
||||
var that = this;
|
||||
setTimeout(function () {
|
||||
that.show('wysiwyg-tabs');
|
||||
that.show('wysiwyg');
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the Toolbar's markup into the DOM.
|
||||
*
|
||||
* Note: depending on whether the 'display' property of the $el for which a
|
||||
* toolbar is being inserted into the DOM, it will be inserted differently.
|
||||
*/
|
||||
render: function () {
|
||||
// Render toolbar.
|
||||
this.setElement($(Drupal.theme('editToolbarContainer', {
|
||||
id: this.getId()
|
||||
})));
|
||||
|
||||
// Insert in DOM.
|
||||
if (this.$el.css('display') === 'inline') {
|
||||
this.$el.prependTo(this.editor.element.offsetParent());
|
||||
var pos = this.editor.element.position();
|
||||
this.$el.css('left', pos.left).css('top', pos.top);
|
||||
}
|
||||
else {
|
||||
this.$el.insertBefore(this.editor.element);
|
||||
}
|
||||
|
||||
var that = this;
|
||||
// Animate the toolbar into visibility.
|
||||
setTimeout(function () {
|
||||
that.$el.removeClass('edit-animate-invisible');
|
||||
}, 0);
|
||||
},
|
||||
|
||||
remove: function () {
|
||||
if (!this.$el) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove after animation.
|
||||
var that = this;
|
||||
var $el = this.$el;
|
||||
this.$el
|
||||
.addClass('edit-animate-invisible')
|
||||
// Prevent this toolbar from being detected *while* it is being removed.
|
||||
.removeAttr('id')
|
||||
.find('.edit-toolbar .edit-toolgroup')
|
||||
.addClass('edit-animate-invisible')
|
||||
.on(Drupal.edit.util.constants.transitionEnd, function (e) {
|
||||
$el.remove();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculates the ID for this toolbar container.
|
||||
*
|
||||
* Only used to make sane hovering behavior possible.
|
||||
*
|
||||
* @return string
|
||||
* A string that can be used as the ID for this toolbar container.
|
||||
*/
|
||||
getId: function() {
|
||||
return this._id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a toolgroup.
|
||||
*
|
||||
* @param string toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
show: function (toolgroup) {
|
||||
this._find(toolgroup).removeClass('edit-animate-invisible');
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes to a toolgroup.
|
||||
*
|
||||
* @param string toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
addClass: function (toolgroup, classes) {
|
||||
this._find(toolgroup).addClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes from a toolgroup.
|
||||
*
|
||||
* @param string toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
removeClass: function (toolgroup, classes) {
|
||||
this._find(toolgroup).removeClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a toolgroup.
|
||||
*
|
||||
* @param string toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
_find: function (toolgroup) {
|
||||
return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery, _, Backbone, Drupal);
|
|
@ -0,0 +1,78 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\Access\EditEntityFieldAccessCheck.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessCheckInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Access check for editing entity fields.
|
||||
*/
|
||||
class EditEntityFieldAccessCheck implements AccessCheckInterface, EditEntityFieldAccessCheckInterface {
|
||||
|
||||
/**
|
||||
* Implements AccessCheckInterface::applies().
|
||||
*/
|
||||
public function applies(Route $route) {
|
||||
return array_key_exists('_access_edit_entity_field', $route->getRequirements());
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements AccessCheckInterface::access().
|
||||
*/
|
||||
public function access(Route $route, Request $request) {
|
||||
// @todo Request argument validation and object loading should happen
|
||||
// elsewhere in the request processing pipeline:
|
||||
// http://drupal.org/node/1798214.
|
||||
$this->validateAndUpcastRequestAttributes($request);
|
||||
|
||||
return $this->accessEditEntityField($request->attributes->get('entity'), $request->attributes->get('field_name'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements EntityFieldAccessCheckInterface::accessEditEntityField().
|
||||
*/
|
||||
public function accessEditEntityField(EntityInterface $entity, $field_name) {
|
||||
$entity_type = $entity->entityType();
|
||||
// @todo Generalize to all entity types: http://drupal.org/node/1839516.
|
||||
return ($entity_type == 'node' && node_access('update', $entity) && field_access('edit', $field_name, $entity_type, $entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and upcasts request attributes.
|
||||
*/
|
||||
protected function validateAndUpcastRequestAttributes(Request $request) {
|
||||
// Load the entity.
|
||||
if (!is_object($entity = $request->attributes->get('entity'))) {
|
||||
$entity_id = $entity;
|
||||
$entity_type = $request->attributes->get('entity_type');
|
||||
if (!$entity_type || !entity_get_info($entity_type)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
$entity = entity_load($entity_type, $entity_id);
|
||||
if (!$entity) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
$request->attributes->set('entity', $entity);
|
||||
}
|
||||
|
||||
// Validate the field name and language.
|
||||
$field_name = $request->attributes->get('field_name');
|
||||
if (!$field_name || !field_info_instance($entity->entityType(), $field_name, $entity->bundle())) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
$langcode = $request->attributes->get('langcode');
|
||||
if (!$langcode || (field_valid_language($langcode) !== $langcode)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\Access\EditEntityFieldAccessCheckInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Access;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Access check for editing entity fields.
|
||||
*/
|
||||
interface EditEntityFieldAccessCheckInterface {
|
||||
|
||||
/**
|
||||
* Checks access to edit the requested field of the requested entity.
|
||||
*/
|
||||
public function accessEditEntityField(EntityInterface $entity, $field_name);
|
||||
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\Edit\Ajax\BaseCommand.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* Base AJAX command that only exists simplify Edit's actual AJAX commands.
|
||||
*/
|
||||
class BaseCommand implements CommandInterface {
|
||||
|
||||
/**
|
||||
* The name of the command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $command;
|
||||
|
||||
/**
|
||||
* The data to pass on to the client side.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Constructs a BaseCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($command, $data) {
|
||||
$this->command = $command;
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements Drupal\Core\Ajax\CommandInterface:render().
|
||||
*/
|
||||
public function render() {
|
||||
return array(
|
||||
'command' => $this->command,
|
||||
'data' => $this->data,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\edit\Ajax\FieldFormCommand.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* AJAX command for passing a rendered field form to Edit's JavaScript app.
|
||||
*/
|
||||
class FieldFormCommand extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Constructs a FieldFormCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($data) {
|
||||
parent::__construct('editFieldForm', $data);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\edit\Ajax\FieldFormSavedCommand.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* AJAX command to indicate a field form was saved without validation errors and
|
||||
* pass the rerendered field to Edit's JavaScript app.
|
||||
*/
|
||||
class FieldFormSavedCommand extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Constructs a FieldFormSavedCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($data) {
|
||||
parent::__construct('editFieldFormSaved', $data);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\edit\Ajax\FieldFormValidationErrorsCommand.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* AJAX command to indicate a field form was attempted to be saved but failed
|
||||
* validation and pass the validation errors.
|
||||
*/
|
||||
class FieldFormValidationErrorsCommand extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Constructs a FieldFormValidationErrorsCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($data) {
|
||||
parent::__construct('editFieldFormValidationErrors', $data);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\edit\Ajax\FieldRenderedWithoutTransformationFiltersCommand.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* AJAX command to rerender a processed text field without any transformation
|
||||
* filters.
|
||||
*/
|
||||
class FieldRenderedWithoutTransformationFiltersCommand extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Constructs a FieldRenderedWithoutTransformationFiltersCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($data) {
|
||||
parent::__construct('editFieldRenderedWithoutTransformationFilters', $data);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\EditBundle.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
/**
|
||||
* Edit dependency injection container.
|
||||
*/
|
||||
class EditBundle extends Bundle {
|
||||
|
||||
/**
|
||||
* Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
|
||||
*/
|
||||
public function build(ContainerBuilder $container) {
|
||||
// Register the plugin managers for our plugin types with the dependency injection container.
|
||||
$container->register('plugin.manager.edit.processed_text_editor', 'Drupal\edit\Plugin\ProcessedTextEditorManager');
|
||||
|
||||
$container->register('access_check.edit.entity_field', 'Drupal\edit\Access\EditEntityFieldAccessCheck')
|
||||
->addTag('access_check');
|
||||
|
||||
$container->register('edit.editor.selector', 'Drupal\edit\EditorSelector')
|
||||
->addArgument(new Reference('plugin.manager.edit.processed_text_editor'));
|
||||
|
||||
$container->register('edit.metadata.generator', 'Drupal\edit\MetadataGenerator')
|
||||
->addArgument(new Reference('access_check.edit.entity_field'))
|
||||
->addArgument(new Reference('edit.editor.selector'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains of \Drupal\edit\EditController.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerAware;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\edit\Ajax\FieldFormCommand;
|
||||
use Drupal\edit\Ajax\FieldFormSavedCommand;
|
||||
use Drupal\edit\Ajax\FieldFormValidationErrorsCommand;
|
||||
use Drupal\edit\Ajax\FieldRenderedWithoutTransformationFiltersCommand;
|
||||
|
||||
/**
|
||||
* Returns responses for Edit module routes.
|
||||
*/
|
||||
class EditController extends ContainerAware {
|
||||
|
||||
/**
|
||||
* Returns the metadata for a set of fields.
|
||||
*
|
||||
* Given a list of field edit IDs as POST parameters, run access checks on the
|
||||
* entity and field level to determine whether the current user may edit them.
|
||||
* Also retrieves other metadata.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse
|
||||
* The JSON response.
|
||||
*/
|
||||
public function metadata(Request $request) {
|
||||
$fields = $request->request->get('fields');
|
||||
if (!isset($fields)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
$metadataGenerator = $this->container->get('edit.metadata.generator');
|
||||
|
||||
$metadata = array();
|
||||
foreach ($fields as $field) {
|
||||
list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode(':', $field);
|
||||
|
||||
// Load the entity.
|
||||
if (!$entity_type || !entity_get_info($entity_type)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
$entity = entity_load($entity_type, $entity_id);
|
||||
if (!$entity) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
// Validate the field name and language.
|
||||
if (!$field_name || !($instance = field_info_instance($entity->entityType(), $field_name, $entity->bundle()))) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
if (!$langcode || (field_valid_language($langcode) !== $langcode)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$metadata[$field] = $metadataGenerator->generate($entity, $instance, $langcode, $view_mode);
|
||||
}
|
||||
|
||||
return new JsonResponse($metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single field edit form as an Ajax response.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being edited.
|
||||
* @param string $field_name
|
||||
* The name of the field that is being edited.
|
||||
* @param string $langcode
|
||||
* The name of the language for which the field is being edited.
|
||||
* @param string $view_mode
|
||||
* The view mode the field should be rerendered in.
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* The Ajax response.
|
||||
*/
|
||||
public function fieldForm(EntityInterface $entity, $field_name, $langcode, $view_mode) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
$form_state = array(
|
||||
'langcode' => $langcode,
|
||||
'no_redirect' => TRUE,
|
||||
'build_info' => array('args' => array($entity, $field_name)),
|
||||
);
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\EditorSelector.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\field\FieldInstance;
|
||||
|
||||
/**
|
||||
* Selects an in-place editor for a given entity field.
|
||||
*/
|
||||
class EditorSelector implements EditorSelectorInterface {
|
||||
|
||||
/**
|
||||
* The manager for processed text editor plugins.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $processedTextEditorManager;
|
||||
|
||||
/**
|
||||
* The processed text editor plugin selected.
|
||||
*
|
||||
* @var \Drupal\edit\Plugin\ProcessedTextEditorInterface
|
||||
*/
|
||||
protected $processedTextEditorPlugin;
|
||||
|
||||
/**
|
||||
* Constructs a new EditorSelector.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface $processed_text_editor_manager
|
||||
* The manager for processed text editor plugins.
|
||||
*/
|
||||
public function __construct(PluginManagerInterface $processed_text_editor_manager) {
|
||||
$this->processedTextEditorManager = $processed_text_editor_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\edit\EditorSelectorInterface::getEditor().
|
||||
*/
|
||||
public function getEditor($formatter_type, FieldInstance $instance, array $items) {
|
||||
// Check if the formatter defines an appropriate in-place editor. For
|
||||
// example, text formatters displaying untrimmed text can choose to use the
|
||||
// 'direct' editor. If the formatter doesn't specify, fall back to the
|
||||
// 'form' editor, since that can work for any field. Formatter definitions
|
||||
// can use 'disabled' to explicitly opt out of in-place editing.
|
||||
$formatter_info = field_info_formatter_types($formatter_type);
|
||||
$editor = isset($formatter_info['edit']['editor']) ? $formatter_info['edit']['editor'] : 'form';
|
||||
if ($editor == 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
// The same text formatters can be used for single-valued and multivalued
|
||||
// fields and for processed and unprocessed text, so we can't rely on the
|
||||
// formatter definition for the final determination, because:
|
||||
// - The direct editor does not work for multivalued fields.
|
||||
// - Processed text can benefit from a WYSIWYG editor.
|
||||
// - Empty processed text without an already selected format requires a form
|
||||
// to select one.
|
||||
// @todo The processed text logic is too coupled to text fields. Figure out
|
||||
// how to generalize to other textual field types.
|
||||
// @todo All of this might hint at formatter *definitions* not being the
|
||||
// ideal place for editor specification. Moving the determination to
|
||||
// something that works with instantiated formatters, not just their
|
||||
// definitions, could alleviate that, but might come with its own
|
||||
// challenges.
|
||||
if ($editor == 'direct') {
|
||||
$field = field_info_field($instance['field_name']);
|
||||
if ($field['cardinality'] != 1) {
|
||||
// The direct editor does not work for multivalued fields.
|
||||
$editor = 'form';
|
||||
}
|
||||
elseif (!empty($instance['settings']['text_processing'])) {
|
||||
$format_id = $items[0]['format'];
|
||||
if (isset($format_id)) {
|
||||
$wysiwyg_plugin = $this->getProcessedTextEditorPlugin();
|
||||
if (isset($wysiwyg_plugin) && $wysiwyg_plugin->checkFormatCompatibility($format_id)) {
|
||||
// Yay! Even though the text is processed, there's a WYSIWYG editor
|
||||
// that can work with it.
|
||||
$editor = 'direct-with-wysiwyg';
|
||||
}
|
||||
else {
|
||||
// @todo We might not have to downgrade all the way to 'form'. The
|
||||
// 'direct' editor might be appropriate for some kinds of
|
||||
// processed text.
|
||||
$editor = 'form';
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If a format is not yet selected, a form is needed to select one.
|
||||
$editor = 'form';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\edit\EditorSelectorInterface::getAllEditorAttachments().
|
||||
*/
|
||||
public function getAllEditorAttachments() {
|
||||
$this->getProcessedTextEditorPlugin();
|
||||
if (!isset($this->processedTextEditorPlugin)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$js = array();
|
||||
|
||||
// Add library and settings for the selected processed text editor plugin.
|
||||
$definition = $this->processedTextEditorPlugin->getDefinition();
|
||||
if (!empty($definition['library'])) {
|
||||
$js['library'][] = array($definition['library']['module'], $definition['library']['name']);
|
||||
}
|
||||
$this->processedTextEditorPlugin->addJsSettings();
|
||||
|
||||
// Also add the setting to register it with Create.js
|
||||
if (!empty($definition['propertyEditorName'])) {
|
||||
$js['js'][] = array(
|
||||
'data' => array(
|
||||
'edit' => array(
|
||||
'wysiwygEditorWidgetName' => $definition['propertyEditorName'],
|
||||
),
|
||||
),
|
||||
'type' => 'setting'
|
||||
);
|
||||
}
|
||||
|
||||
return $js;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
return $this->processedTextEditorPlugin;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\EditorSelectorInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit;
|
||||
|
||||
use Drupal\field\FieldInstance;
|
||||
|
||||
/**
|
||||
* Interface for selecting an in-place editor for a given entity field.
|
||||
*/
|
||||
interface EditorSelectorInterface {
|
||||
|
||||
/**
|
||||
* Returns the in-place editor to use for a given field instance.
|
||||
*
|
||||
* The Edit module includes three in-place 'editors' that integrate with the
|
||||
* Create.js framework:
|
||||
* - direct: A minimal wrapper to simply setting the HTML5 contenteditable
|
||||
* attribute on the DOM element.
|
||||
* - direct-with-wysiwyg: Binds a complete WYSIWYG editor (such as Aloha) to
|
||||
* the DOM element.
|
||||
* - form: Fetches a simplified version of the field's edit form (widget) and
|
||||
* overlays that over the DOM element.
|
||||
*
|
||||
* These three editors are registered in js/createjs/editable.js. Modules may
|
||||
* register additional editors via the Create.js API.
|
||||
*
|
||||
* This function returns the editor to use for a given field instance.
|
||||
*
|
||||
* @param string $formatter_type
|
||||
* The field's formatter type name.
|
||||
* @param \Drupal\field\FieldInstance $instance
|
||||
* The field's instance info.
|
||||
* @param array $items
|
||||
* The field's item values.
|
||||
*
|
||||
* @return string|NULL
|
||||
* The editor to use, or NULL to not enable in-place editing.
|
||||
*/
|
||||
public function getEditor($formatter_type, FieldInstance $instance, array $items);
|
||||
|
||||
/**
|
||||
* Returns the attachments for all editors.
|
||||
*
|
||||
* @return array
|
||||
* An array of attachments, for use with #attached.
|
||||
*
|
||||
* @see drupal_process_attached()
|
||||
*/
|
||||
public function getAllEditorAttachments();
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\Form\EditFieldForm.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
/**
|
||||
* Builds and process a form for editing a single entity field.
|
||||
*/
|
||||
class EditFieldForm {
|
||||
|
||||
/**
|
||||
* Builds a form for a single entity field.
|
||||
*/
|
||||
public function build(array $form, array &$form_state, EntityInterface $entity, $field_name) {
|
||||
if (!isset($form_state['entity'])) {
|
||||
$this->init($form_state, $entity, $field_name);
|
||||
}
|
||||
|
||||
// Add the field form.
|
||||
field_attach_form($form_state['entity']->entityType(), $form_state['entity'], $form, $form_state, $form_state['langcode'], array('field_name' => $form_state['field_name']));
|
||||
|
||||
// Add a submit button. Give it a class for easy JavaScript targeting.
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Save'),
|
||||
'#attributes' => array('class' => array('edit-form-submit')),
|
||||
);
|
||||
|
||||
// Add validation and submission handlers.
|
||||
$form['#validate'][] = array($this, 'validate');
|
||||
$form['#submit'][] = array($this, 'submit');
|
||||
|
||||
// Simplify it for optimal in-place use.
|
||||
$this->simplify($form, $form_state);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the form state and the entity before the first form build.
|
||||
*/
|
||||
protected function init(array &$form_state, EntityInterface $entity, $field_name) {
|
||||
// @todo Rather than special-casing $node->revision, invoke prepareEdit()
|
||||
// once http://drupal.org/node/1863258 lands.
|
||||
if ($entity->entityType() == 'node') {
|
||||
$entity->setNewRevision(in_array('revision', variable_get('node_options_' . $entity->bundle(), array())));
|
||||
$entity->log = NULL;
|
||||
}
|
||||
|
||||
$form_state['entity'] = $entity;
|
||||
$form_state['field_name'] = $field_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
$form_state['entity']->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']));
|
||||
|
||||
// @todo Refine automated log messages and abstract them to all entity
|
||||
// types: http://drupal.org/node/1678002.
|
||||
if ($entity->entityType() == 'node' && $entity->isNewRevision() && !isset($entity->log)) {
|
||||
$instance = field_info_instance($entity->entityType(), $form_state['field_name'], $entity->bundle());
|
||||
$entity->log = t('Updated the %field-name field through in-place editing.', array('%field-name' => $instance['label']));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\MetadataGenerator.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\field\FieldInstance;
|
||||
|
||||
use Drupal\edit\Access\EditEntityFieldAccessCheckInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Generates in-place editing metadata for an entity field.
|
||||
*/
|
||||
class MetadataGenerator implements MetadataGeneratorInterface {
|
||||
|
||||
/**
|
||||
* An object that checks if a user has access to edit a given entity field.
|
||||
*
|
||||
* @var \Drupal\edit\Access\EditEntityFieldAccessCheckInterface
|
||||
*/
|
||||
protected $accessChecker;
|
||||
|
||||
/**
|
||||
* An object that determines which editor to attach to a given field.
|
||||
*
|
||||
* @var \Drupal\edit\EditorSelectorInterface
|
||||
*/
|
||||
protected $editorSelector;
|
||||
|
||||
/**
|
||||
* Constructs a new MetadataGenerator.
|
||||
*
|
||||
* @param \Drupal\edit\Access\EditEntityFieldAccessCheckInterface $access_checker
|
||||
* An object that checks if a user has access to edit a given field.
|
||||
* @param \Drupal\edit\EditorSelectorInterface $editor_selector
|
||||
* An object that determines which editor to attach to a given field.
|
||||
*/
|
||||
public function __construct(EditEntityFieldAccessCheckInterface $access_checker, EditorSelectorInterface $editor_selector) {
|
||||
$this->accessChecker = $access_checker;
|
||||
$this->editorSelector = $editor_selector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements \Drupal\edit\MetadataGeneratorInterface::generate().
|
||||
*/
|
||||
public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode) {
|
||||
$field_name = $instance['field_name'];
|
||||
|
||||
// Early-return if user does not have access.
|
||||
$access = $this->accessChecker->accessEditEntityField($entity, $field_name);
|
||||
if (!$access) {
|
||||
return array('access' => FALSE);
|
||||
}
|
||||
|
||||
$label = $instance['label'];
|
||||
$formatter_id = $instance->getFormatter($view_mode)->getPluginId();
|
||||
$items = $entity->get($field_name);
|
||||
$items = $items[$langcode];
|
||||
$editor = $this->editorSelector->getEditor($formatter_id, $instance, $items);
|
||||
$metadata = array(
|
||||
'label' => $label,
|
||||
'access' => TRUE,
|
||||
'editor' => $editor,
|
||||
'aria' => t('Entity @type @id, field @field', array('@type' => $entity->entityType(), '@id' => $entity->id(), '@field' => $label)),
|
||||
);
|
||||
// Additional metadata for WYSIWYG editor integration.
|
||||
if ($editor === 'direct-with-wysiwyg') {
|
||||
$format_id = $items[0]['format'];
|
||||
$metadata['format'] = $format_id;
|
||||
$metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id);
|
||||
}
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the text format has transformation filters.
|
||||
*/
|
||||
protected function textFormatHasTransformationFilters($format_id) {
|
||||
return (bool) count(array_intersect(array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE), filter_get_filter_types_by_format($format_id)));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\edit\MetadataGeneratorInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\field\FieldInstance;
|
||||
|
||||
/**
|
||||
* Interface for generating in-place editing metadata for an entity field.
|
||||
*/
|
||||
interface MetadataGeneratorInterface {
|
||||
|
||||
/**
|
||||
* Generates in-place editing metadata for an entity field.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity being edited.
|
||||
* @param Drupal\field\FieldInstance $instance
|
||||
* The field instance of the field being edited.
|
||||
* @param string $langcode
|
||||
* The name of the language for which the field is being edited.
|
||||
* @param string $view_mode
|
||||
* The view mode the field should be rerendered in.
|
||||
* @return array
|
||||
* An array containing metadata with the following keys:
|
||||
* - label: the user-visible label for the field.
|
||||
* - access: whether the current user may edit the field or not.
|
||||
* - editor: which editor should be used for the field.
|
||||
* - aria: the ARIA label.
|
||||
* - format: (optional) the text format ID of the field.
|
||||
* - formatHasTransformations: (optional) whether the text format uses any
|
||||
* transformation filters or not.
|
||||
*/
|
||||
public function generate(EntityInterface $entity, FieldInstance $instance, $langcode, $view_mode);
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of \Drupal\edit\Plugin\ProcessedTextPropertyBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
|
||||
/**
|
||||
* Base class for processed text editor plugins.
|
||||
*/
|
||||
abstract class ProcessedTextEditorBase extends PluginBase implements ProcessedTextEditorInterface {
|
||||
|
||||
/**
|
||||
* Implements \Drupal\edit\Plugin\ProcessedTextEditorInterface::addJsSettings().
|
||||
*
|
||||
* This base class provides an empty implementation for text editors that
|
||||
* do not need to add JavaScript settings besides those added by the library.
|
||||
*/
|
||||
public function addJsSettings() {
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of \Drupal\edit\Plugin\ProcessedTextEditorInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for PropertyEditor widgets for processed text fields.
|
||||
*
|
||||
* A PropertyEditor widget is a user-facing interface to edit an entity property
|
||||
* through Create.js.
|
||||
*/
|
||||
interface ProcessedTextEditorInterface extends PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Adds JavaScript settings.
|
||||
*/
|
||||
public function addJsSettings();
|
||||
|
||||
/**
|
||||
* Checks if the text editor is compatible with a given text format.
|
||||
*
|
||||
* @param $format_id
|
||||
* A text format ID.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if it is compatible, FALSE otherwise.
|
||||
*/
|
||||
public function checkFormatCompatibility($format_id);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of \Drupal\edit\Plugin\ProcessedTextEditorManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerBase;
|
||||
use Drupal\Component\Plugin\Factory\DefaultFactory;
|
||||
use Drupal\Core\Plugin\Discovery\AlterDecorator;
|
||||
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
|
||||
use Drupal\Core\Plugin\Discovery\CacheDecorator;
|
||||
|
||||
/**
|
||||
* ProcessedTextEditor manager.
|
||||
*/
|
||||
class ProcessedTextEditorManager extends PluginManagerBase {
|
||||
|
||||
/**
|
||||
* Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->discovery = new AnnotatedClassDiscovery('edit', 'processed_text_editor');
|
||||
$this->discovery = new AlterDecorator($this->discovery, 'edit_wysiwyg');
|
||||
$this->discovery = new CacheDecorator($this->discovery, 'edit:wysiwyg');
|
||||
$this->factory = new DefaultFactory($this->discovery);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\edit\Tests\EditorSelectionTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit\Tests;
|
||||
|
||||
use Drupal\simpletest\DrupalUnitTestBase;
|
||||
use Drupal\edit\Plugin\ProcessedTextEditorManager;
|
||||
use Drupal\edit\EditorSelector;
|
||||
|
||||
/**
|
||||
* Test in-place field editor selection.
|
||||
*/
|
||||
class EditorSelectionTest extends DrupalUnitTestBase {
|
||||
var $default_storage = 'field_sql_storage';
|
||||
|
||||
/**
|
||||
* The editor selector object to be tested.
|
||||
*
|
||||
* @var \Drupal\edit\EditorSelectorInterface
|
||||
*/
|
||||
protected $editorSelector;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('system', 'field_test', 'field', 'number', 'text', 'edit', 'edit_test');
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'In-place field editor selection',
|
||||
'description' => 'Tests in-place field editor selection.',
|
||||
'group' => 'Edit',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default field storage backend for fields created during tests.
|
||||
*/
|
||||
function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('system', 'variable');
|
||||
$this->enableModules(array('field', 'field_sql_storage', 'field_test'));
|
||||
|
||||
// Set default storage backend.
|
||||
variable_set('field_storage_default', $this->default_storage);
|
||||
|
||||
// @todo Rather than using the real ProcessedTextEditorManager, which can
|
||||
// find all text editor plugins in the codebase, create a mock one for
|
||||
// testing that is populated with only the ones we want to test.
|
||||
$text_editor_manager = new ProcessedTextEditorManager();
|
||||
|
||||
$this->editorSelector = new EditorSelector($text_editor_manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a field and an instance of it.
|
||||
*
|
||||
* @param string $field_name
|
||||
* The field name.
|
||||
* @param string $type
|
||||
* The field type.
|
||||
* @param int $cardinality
|
||||
* The field's cardinality.
|
||||
* @param string $label
|
||||
* The field's label (used everywhere: widget label, formatter label).
|
||||
* @param array $instance_settings
|
||||
* @param string $widget_type
|
||||
* The widget type.
|
||||
* @param array $widget_settings
|
||||
* The widget settings.
|
||||
* @param string $formatter_type
|
||||
* The formatter type.
|
||||
* @param array $formatter_settings
|
||||
* The formatter settings.
|
||||
*/
|
||||
function createFieldWithInstance($field_name, $type, $cardinality, $label, $instance_settings, $widget_type, $widget_settings, $formatter_type, $formatter_settings) {
|
||||
$field = $field_name . '_field';
|
||||
$this->$field = array(
|
||||
'field_name' => $field_name,
|
||||
'type' => $type,
|
||||
'cardinality' => $cardinality,
|
||||
);
|
||||
$this->$field_name = field_create_field($this->$field);
|
||||
|
||||
$instance = $field_name . '_instance';
|
||||
$this->$instance = array(
|
||||
'field_name' => $field_name,
|
||||
'entity_type' => 'test_entity',
|
||||
'bundle' => 'test_bundle',
|
||||
'label' => $label,
|
||||
'description' => $label,
|
||||
'weight' => mt_rand(0, 127),
|
||||
'settings' => $instance_settings,
|
||||
'widget' => array(
|
||||
'type' => $widget_type,
|
||||
'label' => $label,
|
||||
'settings' => $widget_settings,
|
||||
),
|
||||
'display' => array(
|
||||
'default' => array(
|
||||
'label' => 'above',
|
||||
'type' => $formatter_type,
|
||||
'settings' => $formatter_settings
|
||||
),
|
||||
),
|
||||
);
|
||||
field_create_instance($this->$instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the FieldInstance object for the given field and returns the
|
||||
* editor that Edit selects.
|
||||
*/
|
||||
function getSelectedEditor($items, $field_name, $display = 'default') {
|
||||
$field_instance = field_info_instance('test_entity', $field_name, 'test_bundle');
|
||||
return $this->editorSelector->getEditor($field_instance['display'][$display]['type'], $field_instance, $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a textual field, without/with text processing, with cardinality 1 and
|
||||
* >1, always without a WYSIWYG editor present.
|
||||
*/
|
||||
function testText() {
|
||||
$field_name = 'field_text';
|
||||
$this->createFieldWithInstance(
|
||||
$field_name, 'text', 1, 'Simple text field',
|
||||
// Instance settings.
|
||||
array('text_processing' => 0),
|
||||
// Widget type & settings.
|
||||
'text_textfield',
|
||||
array('size' => 42),
|
||||
// 'default' formatter type & settings.
|
||||
'text_default',
|
||||
array()
|
||||
);
|
||||
|
||||
// Pretend there is an entity with these items for the field.
|
||||
$items = array(array('value' => 'Hello, world!', 'format' => 'full_html'));
|
||||
|
||||
// Editor selection without text processing, with cardinality 1.
|
||||
$this->assertEqual('direct', $this->getSelectedEditor($items, $field_name), "Without text processing, cardinality 1, the 'direct' editor is selected.");
|
||||
|
||||
// Editor selection with text processing, cardinality 1.
|
||||
$this->field_text_instance['settings']['text_processing'] = 1;
|
||||
field_update_instance($this->field_text_instance);
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With text processing, cardinality 1, the 'form' editor is selected.");
|
||||
|
||||
// Editor selection without text processing, cardinality 1 (again).
|
||||
$this->field_text_instance['settings']['text_processing'] = 0;
|
||||
field_update_instance($this->field_text_instance);
|
||||
$this->assertEqual('direct', $this->getSelectedEditor($items, $field_name), "Without text processing again, cardinality 1, the 'direct' editor is selected.");
|
||||
|
||||
// Editor selection without text processing, cardinality >1
|
||||
$this->field_text_field['cardinality'] = 2;
|
||||
field_update_field($this->field_text_field);
|
||||
$items[] = array('value' => 'Hallo, wereld!', 'format' => 'full_html');
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "Without text processing, cardinality >1, the 'form' editor is selected.");
|
||||
|
||||
// Editor selection with text processing, cardinality >1
|
||||
$this->field_text_instance['settings']['text_processing'] = 1;
|
||||
field_update_instance($this->field_text_instance);
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With text processing, cardinality >1, the 'form' editor is selected.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a textual field, with text processing, with cardinality 1 and >1,
|
||||
* always with a ProcessedTextEditor plug-in present, but with varying text
|
||||
* format compatibility.
|
||||
*/
|
||||
function testTextWysiwyg() {
|
||||
$field_name = 'field_textarea';
|
||||
$this->createFieldWithInstance(
|
||||
$field_name, 'text', 1, 'Long text field',
|
||||
// Instance settings.
|
||||
array('text_processing' => 1),
|
||||
// Widget type & settings.
|
||||
'text_textarea',
|
||||
array('size' => 42),
|
||||
// 'default' formatter type & settings.
|
||||
'text_default',
|
||||
array()
|
||||
);
|
||||
|
||||
// ProcessedTextEditor plug-in compatible with the full_html text format.
|
||||
state()->set('edit_test.compatible_format', 'full_html');
|
||||
|
||||
// Pretend there is an entity with these items for the field.
|
||||
$items = array(array('value' => 'Hello, world!', 'format' => 'filtered_html'));
|
||||
|
||||
// Editor selection with cardinality 1, without compatible text format.
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "Without cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
|
||||
|
||||
// Editor selection with cardinality 1, with compatible text format.
|
||||
$items[0]['format'] = 'full_html';
|
||||
$this->assertEqual('direct-with-wysiwyg', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the full_html text format, the 'direct-with-wysiwyg' editor is selected.");
|
||||
|
||||
// Editor selection with text processing, cardinality >1
|
||||
$this->field_textarea_field['cardinality'] = 2;
|
||||
field_update_field($this->field_textarea_field);
|
||||
$items[] = array('value' => 'Hallo, wereld!', 'format' => 'full_html');
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests a number field, with cardinality 1 and >1.
|
||||
*/
|
||||
function testNumber() {
|
||||
$field_name = 'field_nr';
|
||||
$this->createFieldWithInstance(
|
||||
$field_name, 'number_integer', 1, 'Simple number field',
|
||||
// Instance settings.
|
||||
array(),
|
||||
// Widget type & settings.
|
||||
'number',
|
||||
array(),
|
||||
// 'default' formatter type & settings.
|
||||
'number_integer',
|
||||
array()
|
||||
);
|
||||
|
||||
// Pretend there is an entity with these items for the field.
|
||||
$items = array(42, 43);
|
||||
|
||||
// Editor selection with cardinality 1.
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality 1, the 'form' editor is selected.");
|
||||
|
||||
// Editor selection with cardinality >1.
|
||||
$this->field_nr_field['cardinality'] = 2;
|
||||
field_update_field($this->field_nr_field);
|
||||
$this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality >1, the 'form' editor is selected.");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
name = Edit test
|
||||
description = Support module for the Edit module tests.
|
||||
core = 8.x
|
||||
package = Testing
|
||||
version = VERSION
|
||||
hidden = TRUE
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Helper module for the Edit tests.
|
||||
*/
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\edit_test\Plugin\edit\processed_text_editor\TestProcessedEditor.
|
||||
*/
|
||||
|
||||
namespace Drupal\edit_test\Plugin\edit\processed_text_editor;
|
||||
|
||||
use Drupal\edit\Plugin\ProcessedTextEditorBase;
|
||||
use Drupal\Core\Annotation\Plugin;
|
||||
use Drupal\Core\Annotation\Translation;
|
||||
|
||||
/**
|
||||
* Defines a test processed text editor plugin.
|
||||
*
|
||||
* @Plugin(
|
||||
* id = "test_processed_editor",
|
||||
* title = @Translation("Test Processed Editor")
|
||||
* )
|
||||
*/
|
||||
class TestProcessedEditor extends ProcessedTextEditorBase {
|
||||
|
||||
/**
|
||||
* Implements Drupal\edit\Plugin\ProcessedTextEditorInterface::checkFormatCompatibility().
|
||||
*/
|
||||
function checkFormatCompatibility($format_id) {
|
||||
return state()->get('edit_test.compatible_format') == $format_id;
|
||||
}
|
||||
|
||||
}
|
|
@ -23,6 +23,9 @@ use Drupal\Core\Entity\EntityInterface;
|
|||
* "text",
|
||||
* "text_long",
|
||||
* "text_with_summary"
|
||||
* },
|
||||
* edit = {
|
||||
* "editor" = "direct"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -23,6 +23,9 @@ use Drupal\Core\Entity\EntityInterface;
|
|||
* "text",
|
||||
* "text_long",
|
||||
* "text_with_summary"
|
||||
* },
|
||||
* edit = {
|
||||
* "editor" = "direct"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -22,6 +22,9 @@ use Drupal\Core\Annotation\Translation;
|
|||
* },
|
||||
* settings = {
|
||||
* "trim_length" = "600"
|
||||
* },
|
||||
* edit = {
|
||||
* "editor" = "form"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -31,6 +31,9 @@ use Drupal\Core\Entity\EntityInterface;
|
|||
* },
|
||||
* settings = {
|
||||
* "trim_length" = "600"
|
||||
* },
|
||||
* edit = {
|
||||
* "editor" = "form"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
|
|
|
@ -11,6 +11,7 @@ dependencies[] = config
|
|||
dependencies[] = comment
|
||||
dependencies[] = contextual
|
||||
dependencies[] = contact
|
||||
dependencies[] = edit
|
||||
dependencies[] = help
|
||||
dependencies[] = image
|
||||
dependencies[] = menu
|
||||
|
|
Loading…
Reference in New Issue