drupal/core/modules/edit/js/views/toolbar-view.js

510 lines
15 KiB
JavaScript

/**
* @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.
* * 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.
this._id = Drupal.edit.util.calcPropertyID(this.entity, this.predicate).replace(/\//g, '_');
},
/**
* Listens to editor state changes.
*/
stateChange: function(from, to) {
switch (to) {
case 'inactive':
if (from) {
this.remove();
}
break;
case 'candidate':
if (from === 'inactive') {
this.render();
}
else {
// Remove all toolgroups; they're no longer necessary.
this.$el
.removeClass('edit-highlighted edit-editing')
.find('.edit-toolbar .edit-toolgroup').remove();
if (from !== 'highlighted' && this.getEditUISetting('padding')) {
this._unpad();
}
}
break;
case 'highlighted':
// As soon as we highlight, make sure we have a toolbar in the DOM (with at least a title).
this.startHighlight();
break;
case 'activating':
this.setLoadingIndicator(true);
break;
case 'active':
this.startEdit();
this.setLoadingIndicator(false);
if (this.getEditUISetting('fullWidthToolbar')) {
this.$el.addClass('edit-toolbar-fullwidth');
}
if (this.getEditUISetting('padding')) {
this._pad();
}
if (this.getEditUISetting('unifiedToolbar')) {
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
.addClass('edit-highlighted')
.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');
},
/**
* Retrieves a setting of the editor-specific Edit UI integration.
*
* @see Drupal.edit.util.getEditUISetting().
*/
getEditUISetting: function(setting) {
return Drupal.edit.util.getEditUISetting(this.editor, setting);
},
/**
* Adjusts the toolbar to accomodate padding on the PropertyEditor widget.
*
* @see PropertyEditorDecorationView._pad().
*/
_pad: function() {
// 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' });
if (this.getEditUISetting('fullWidthToolbar')) {
$hf.css({ width: this.editor.element.width() + 10 });
}
},
/**
* Undoes the changes made by _pad().
*
* @see PropertyEditorDecorationView._unpad().
*/
_unpad: function() {
// Move the toolbar back to its original position.
var $hf = this.$el.find('.edit-toolbar-heightfaker');
$hf.css({ bottom: '1px', left: '' });
if (this.getEditUISetting('fullWidthToolbar')) {
$hf.css({ width: '' });
}
},
insertWYSIWYGToolGroups: function() {
this.$el
.find('.edit-toolbar')
.append(Drupal.theme('editToolgroup', {
id: this.getFloatedWysiwygToolgroupId(),
classes: 'wysiwyg-floated',
buttons: []
}))
.append(Drupal.theme('editToolgroup', {
id: this.getMainWysiwygToolgroupId(),
classes: 'wysiwyg-main',
buttons: []
}));
// Animate the toolgroups into visibility.
var that = this;
setTimeout(function () {
that.show('wysiwyg-floated');
that.show('wysiwyg-main');
}, 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();
});
},
/**
* Retrieves the ID for this toolbar's container.
*
* Only used to make sane hovering behavior possible.
*
* @return string
* A string that can be used as the ID for this toolbar's container.
*/
getId: function () {
return 'edit-toolbar-for-' + this._id;
},
/**
* Retrieves the ID for this toolbar's floating WYSIWYG toolgroup.
*
* Used to provide an abstraction for any WYSIWYG editor to plug in.
*
* @return string
* A string that can be used as the ID.
*/
getFloatedWysiwygToolgroupId: function () {
return 'edit-wysiwyg-floated-toolgroup-for-' + this._id;
},
/**
* Retrieves the ID for this toolbar's main WYSIWYG toolgroup.
*
* Used to provide an abstraction for any WYSIWYG editor to plug in.
*
* @return string
* A string that can be used as the ID.
*/
getMainWysiwygToolgroupId: function () {
return 'edit-wysiwyg-main-toolgroup-for-' + 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);