Update Composer, update everything
This commit is contained in:
parent
ea3e94409f
commit
dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions
656
web/core/modules/quickedit/js/views/AppView.es6.js
Normal file
656
web/core/modules/quickedit/js/views/AppView.es6.js
Normal file
|
@ -0,0 +1,656 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that controls the overall "in-place editing application".
|
||||
*
|
||||
* @see Drupal.quickedit.AppModel
|
||||
*/
|
||||
|
||||
(function($, _, Backbone, Drupal) {
|
||||
// Indicates whether the page should be reloaded after in-place editing has
|
||||
// shut down. A page reload is necessary to re-instate the original HTML of
|
||||
// the edited fields if in-place editing has been canceled and one or more of
|
||||
// the entity's fields were saved to PrivateTempStore: one of them may have
|
||||
// been changed to the empty value and hence may have been rerendered as the
|
||||
// empty string, which makes it impossible for Quick Edit to know where to
|
||||
// restore the original HTML.
|
||||
let reload = false;
|
||||
|
||||
Drupal.quickedit.AppView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.AppView# */ {
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.AppModel} options.model
|
||||
* The application state model.
|
||||
* @param {Drupal.quickedit.EntityCollection} options.entitiesCollection
|
||||
* All on-page entities.
|
||||
* @param {Drupal.quickedit.FieldCollection} options.fieldsCollection
|
||||
* All on-page fields
|
||||
*/
|
||||
initialize(options) {
|
||||
// AppView's configuration for handling states.
|
||||
// @see Drupal.quickedit.FieldModel.states
|
||||
this.activeFieldStates = ['activating', 'active'];
|
||||
this.singleFieldStates = ['highlighted', 'activating', 'active'];
|
||||
this.changedFieldStates = ['changed', 'saving', 'saved', 'invalid'];
|
||||
this.readyFieldStates = ['candidate', 'highlighted'];
|
||||
|
||||
// Track app state.
|
||||
this.listenTo(
|
||||
options.entitiesCollection,
|
||||
'change:state',
|
||||
this.appStateChange,
|
||||
);
|
||||
this.listenTo(
|
||||
options.entitiesCollection,
|
||||
'change:isActive',
|
||||
this.enforceSingleActiveEntity,
|
||||
);
|
||||
|
||||
// Track app state.
|
||||
this.listenTo(
|
||||
options.fieldsCollection,
|
||||
'change:state',
|
||||
this.editorStateChange,
|
||||
);
|
||||
// Respond to field model HTML representation change events.
|
||||
this.listenTo(
|
||||
options.fieldsCollection,
|
||||
'change:html',
|
||||
this.renderUpdatedField,
|
||||
);
|
||||
this.listenTo(
|
||||
options.fieldsCollection,
|
||||
'change:html',
|
||||
this.propagateUpdatedField,
|
||||
);
|
||||
// Respond to addition.
|
||||
this.listenTo(
|
||||
options.fieldsCollection,
|
||||
'add',
|
||||
this.rerenderedFieldToCandidate,
|
||||
);
|
||||
// Respond to destruction.
|
||||
this.listenTo(options.fieldsCollection, 'destroy', this.teardownEditor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles setup/teardown and state changes when the active entity changes.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} entityModel
|
||||
* An instance of the EntityModel class.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.EntityModel.states}.
|
||||
*/
|
||||
appStateChange(entityModel, state) {
|
||||
const app = this;
|
||||
let entityToolbarView;
|
||||
switch (state) {
|
||||
case 'launching':
|
||||
reload = false;
|
||||
// First, create an entity toolbar view.
|
||||
entityToolbarView = new Drupal.quickedit.EntityToolbarView({
|
||||
model: entityModel,
|
||||
appModel: this.model,
|
||||
});
|
||||
entityModel.toolbarView = entityToolbarView;
|
||||
// Second, set up in-place editors.
|
||||
// They must be notified of state changes, hence this must happen
|
||||
// while the associated fields are still in the 'inactive' state.
|
||||
entityModel.get('fields').each(fieldModel => {
|
||||
app.setupEditor(fieldModel);
|
||||
});
|
||||
// Third, transition the entity to the 'opening' state, which will
|
||||
// transition all fields from 'inactive' to 'candidate'.
|
||||
_.defer(() => {
|
||||
entityModel.set('state', 'opening');
|
||||
});
|
||||
break;
|
||||
|
||||
case 'closed':
|
||||
entityToolbarView = entityModel.toolbarView;
|
||||
// First, tear down the in-place editors.
|
||||
entityModel.get('fields').each(fieldModel => {
|
||||
app.teardownEditor(fieldModel);
|
||||
});
|
||||
// Second, tear down the entity toolbar view.
|
||||
if (entityToolbarView) {
|
||||
entityToolbarView.remove();
|
||||
delete entityModel.toolbarView;
|
||||
}
|
||||
// A page reload may be necessary to re-instate the original HTML of
|
||||
// the edited fields.
|
||||
if (reload) {
|
||||
reload = false;
|
||||
window.location.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Accepts or reject editor (Editor) state changes.
|
||||
*
|
||||
* This is what ensures that the app is in control of what happens.
|
||||
*
|
||||
* @param {string} from
|
||||
* The previous state.
|
||||
* @param {string} to
|
||||
* The new state.
|
||||
* @param {null|object} context
|
||||
* The context that is trying to trigger the state change.
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The fieldModel to which this change applies.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the editor change was accepted or rejected.
|
||||
*/
|
||||
acceptEditorStateChange(from, to, context, fieldModel) {
|
||||
let accept = true;
|
||||
|
||||
// If the app is in view mode, then reject all state changes except for
|
||||
// those to 'inactive'.
|
||||
if (
|
||||
context &&
|
||||
(context.reason === 'stop' || context.reason === 'rerender')
|
||||
) {
|
||||
if (from === 'candidate' && to === 'inactive') {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
// 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 (!Drupal.quickedit.FieldModel.followsStateSequence(from, to)) {
|
||||
accept = false;
|
||||
// Allow: activating/active -> candidate.
|
||||
// Necessary to stop editing a field.
|
||||
if (
|
||||
_.indexOf(this.activeFieldStates, from) !== -1 &&
|
||||
to === 'candidate'
|
||||
) {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: changed/invalid -> candidate.
|
||||
// Necessary to stop editing a field when it is changed or invalid.
|
||||
else if (
|
||||
(from === 'changed' || from === 'invalid') &&
|
||||
to === 'candidate'
|
||||
) {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: highlighted -> candidate.
|
||||
// Necessary to stop highlighting a field.
|
||||
else if (from === 'highlighted' && to === 'candidate') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: saved -> candidate.
|
||||
// Necessary when successfully saved a field.
|
||||
else if (from === 'saved' && to === 'candidate') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: invalid -> saving.
|
||||
// Necessary to be able to save a corrected, invalid field.
|
||||
else if (from === 'invalid' && to === 'saving') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: invalid -> activating.
|
||||
// Necessary to be able to correct a field that turned out to be
|
||||
// invalid after the user already had moved on to the next field
|
||||
// (which we explicitly allow to have a fluent UX).
|
||||
else if (from === 'invalid' && to === 'activating') {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's not against the general principle, then here are more
|
||||
// disallowed cases to check.
|
||||
if (accept) {
|
||||
let activeField;
|
||||
let activeFieldState;
|
||||
// Ensure only one field (editor) at a time is active … but allow a
|
||||
// user to hop from one field to the next, even if we still have to
|
||||
// start saving the field that is currently active: assume it will be
|
||||
// valid, to allow for a fluent UX. (If it turns out to be invalid,
|
||||
// this block of code also handles that.)
|
||||
if (
|
||||
(this.readyFieldStates.indexOf(from) !== -1 ||
|
||||
from === 'invalid') &&
|
||||
this.activeFieldStates.indexOf(to) !== -1
|
||||
) {
|
||||
activeField = this.model.get('activeField');
|
||||
if (activeField && activeField !== fieldModel) {
|
||||
activeFieldState = activeField.get('state');
|
||||
// Allow the state change. If the state of the active field is:
|
||||
// - 'activating' or 'active': change it to 'candidate'
|
||||
// - 'changed' or 'invalid': change it to 'saving'
|
||||
// - 'saving' or 'saved': don't do anything.
|
||||
if (this.activeFieldStates.indexOf(activeFieldState) !== -1) {
|
||||
activeField.set('state', 'candidate');
|
||||
} else if (
|
||||
activeFieldState === 'changed' ||
|
||||
activeFieldState === 'invalid'
|
||||
) {
|
||||
activeField.set('state', 'saving');
|
||||
}
|
||||
|
||||
// If the field that's being activated is in fact already in the
|
||||
// invalid state (which can only happen because above we allowed
|
||||
// the user to move on to another field to allow for a fluent UX;
|
||||
// we assumed it would be saved successfully), then we shouldn't
|
||||
// allow the field to enter the 'activating' state, instead, we
|
||||
// simply change the active editor. All guarantees and
|
||||
// assumptions for this field still hold!
|
||||
if (from === 'invalid') {
|
||||
this.model.set('activeField', fieldModel);
|
||||
accept = false;
|
||||
}
|
||||
// Do not reject: the field is either in the 'candidate' or
|
||||
// 'highlighted' state and we allow it to enter the 'activating'
|
||||
// state!
|
||||
}
|
||||
}
|
||||
// Reject going from activating/active to candidate because of a
|
||||
// mouseleave.
|
||||
else if (
|
||||
_.indexOf(this.activeFieldStates, 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;
|
||||
}
|
||||
// Check whether the transition has been confirmed?
|
||||
else if (context && context.confirmed) {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accept;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up the in-place editor for the given field.
|
||||
*
|
||||
* Must happen before the fieldModel's state is changed to 'candidate'.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The field for which an in-place editor must be set up.
|
||||
*/
|
||||
setupEditor(fieldModel) {
|
||||
// Get the corresponding entity toolbar.
|
||||
const entityModel = fieldModel.get('entity');
|
||||
const entityToolbarView = entityModel.toolbarView;
|
||||
// Get the field toolbar DOM root from the entity toolbar.
|
||||
const fieldToolbarRoot = entityToolbarView.getToolbarRoot();
|
||||
// Create in-place editor.
|
||||
const editorName = fieldModel.get('metadata').editor;
|
||||
const editorModel = new Drupal.quickedit.EditorModel();
|
||||
const editorView = new Drupal.quickedit.editors[editorName]({
|
||||
el: $(fieldModel.get('el')),
|
||||
model: editorModel,
|
||||
fieldModel,
|
||||
});
|
||||
|
||||
// Create in-place editor's toolbar for this field — stored inside the
|
||||
// entity toolbar, the entity toolbar will position itself appropriately
|
||||
// above (or below) the edited element.
|
||||
const toolbarView = new Drupal.quickedit.FieldToolbarView({
|
||||
el: fieldToolbarRoot,
|
||||
model: fieldModel,
|
||||
$editedElement: $(editorView.getEditedElement()),
|
||||
editorView,
|
||||
entityModel,
|
||||
});
|
||||
|
||||
// Create decoration for edited element: padding if necessary, sets
|
||||
// classes on the element to style it according to the current state.
|
||||
const decorationView = new Drupal.quickedit.FieldDecorationView({
|
||||
el: $(editorView.getEditedElement()),
|
||||
model: fieldModel,
|
||||
editorView,
|
||||
});
|
||||
|
||||
// Track these three views in FieldModel so that we can tear them down
|
||||
// correctly.
|
||||
fieldModel.editorView = editorView;
|
||||
fieldModel.toolbarView = toolbarView;
|
||||
fieldModel.decorationView = decorationView;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tears down the in-place editor for the given field.
|
||||
*
|
||||
* Must happen after the fieldModel's state is changed to 'inactive'.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The field for which an in-place editor must be torn down.
|
||||
*/
|
||||
teardownEditor(fieldModel) {
|
||||
// Early-return if this field was not yet decorated.
|
||||
if (typeof fieldModel.editorView === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unbind event handlers; remove toolbar element; delete toolbar view.
|
||||
fieldModel.toolbarView.remove();
|
||||
delete fieldModel.toolbarView;
|
||||
|
||||
// Unbind event handlers; delete decoration view. Don't remove the element
|
||||
// because that would remove the field itself.
|
||||
fieldModel.decorationView.remove();
|
||||
delete fieldModel.decorationView;
|
||||
|
||||
// Unbind event handlers; delete editor view. Don't remove the element
|
||||
// because that would remove the field itself.
|
||||
fieldModel.editorView.remove();
|
||||
delete fieldModel.editorView;
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the user to confirm whether he wants to stop editing via a modal.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} entityModel
|
||||
* An instance of the EntityModel class.
|
||||
*
|
||||
* @see Drupal.quickedit.AppView#acceptEditorStateChange
|
||||
*/
|
||||
confirmEntityDeactivation(entityModel) {
|
||||
const that = this;
|
||||
let discardDialog;
|
||||
|
||||
function closeDiscardDialog(action) {
|
||||
discardDialog.close(action);
|
||||
// The active modal has been removed.
|
||||
that.model.set('activeModal', null);
|
||||
|
||||
// If the targetState is saving, the field must be saved, then the
|
||||
// entity must be saved.
|
||||
if (action === 'save') {
|
||||
entityModel.set('state', 'committing', { confirmed: true });
|
||||
} else {
|
||||
entityModel.set('state', 'deactivating', { confirmed: true });
|
||||
// Editing has been canceled and the changes will not be saved. Mark
|
||||
// the page for reload if the entityModel declares that it requires
|
||||
// a reload.
|
||||
if (entityModel.get('reload')) {
|
||||
reload = true;
|
||||
entityModel.set('reload', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only instantiate if there isn't a modal instance visible yet.
|
||||
if (!this.model.get('activeModal')) {
|
||||
const $unsavedChanges = $(
|
||||
`<div>${Drupal.t('You have unsaved changes')}</div>`,
|
||||
);
|
||||
discardDialog = Drupal.dialog($unsavedChanges.get(0), {
|
||||
title: Drupal.t('Discard changes?'),
|
||||
dialogClass: 'quickedit-discard-modal',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Save'),
|
||||
click() {
|
||||
closeDiscardDialog('save');
|
||||
},
|
||||
primary: true,
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Discard changes'),
|
||||
click() {
|
||||
closeDiscardDialog('discard');
|
||||
},
|
||||
},
|
||||
],
|
||||
// Prevent this modal from being closed without the user making a
|
||||
// choice as per http://stackoverflow.com/a/5438771.
|
||||
closeOnEscape: false,
|
||||
create() {
|
||||
$(this)
|
||||
.parent()
|
||||
.find('.ui-dialog-titlebar-close')
|
||||
.remove();
|
||||
},
|
||||
beforeClose: false,
|
||||
close(event) {
|
||||
// Automatically destroy the DOM element that was used for the
|
||||
// dialog.
|
||||
$(event.target).remove();
|
||||
},
|
||||
});
|
||||
this.model.set('activeModal', discardDialog);
|
||||
|
||||
discardDialog.showModal();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reacts to field state changes; tracks global state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The `fieldModel` holding the state.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
editorStateChange(fieldModel, state) {
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
|
||||
// Keep track of the highlighted field in the global state.
|
||||
if (
|
||||
_.indexOf(this.singleFieldStates, to) !== -1 &&
|
||||
this.model.get('highlightedField') !== fieldModel
|
||||
) {
|
||||
this.model.set('highlightedField', fieldModel);
|
||||
} else if (
|
||||
this.model.get('highlightedField') === fieldModel &&
|
||||
to === 'candidate'
|
||||
) {
|
||||
this.model.set('highlightedField', null);
|
||||
}
|
||||
|
||||
// Keep track of the active field in the global state.
|
||||
if (
|
||||
_.indexOf(this.activeFieldStates, to) !== -1 &&
|
||||
this.model.get('activeField') !== fieldModel
|
||||
) {
|
||||
this.model.set('activeField', fieldModel);
|
||||
} else if (
|
||||
this.model.get('activeField') === fieldModel &&
|
||||
to === 'candidate'
|
||||
) {
|
||||
// Discarded if it transitions from a changed state to 'candidate'.
|
||||
if (from === 'changed' || from === 'invalid') {
|
||||
fieldModel.editorView.revert();
|
||||
}
|
||||
this.model.set('activeField', null);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render an updated field (a field whose 'html' attribute changed).
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The FieldModel whose 'html' attribute changed.
|
||||
* @param {string} html
|
||||
* The updated 'html' attribute.
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {bool} options.propagation
|
||||
* Whether this change to the 'html' attribute occurred because of the
|
||||
* propagation of changes to another instance of this field.
|
||||
*/
|
||||
renderUpdatedField(fieldModel, html, options) {
|
||||
// Get data necessary to rerender property before it is unavailable.
|
||||
const $fieldWrapper = $(fieldModel.get('el'));
|
||||
const $context = $fieldWrapper.parent();
|
||||
|
||||
const renderField = function() {
|
||||
// Destroy the field model; this will cause all attached views to be
|
||||
// destroyed too, and removal from all collections in which it exists.
|
||||
fieldModel.destroy();
|
||||
|
||||
// Replace the old content with the new content.
|
||||
$fieldWrapper.replaceWith(html);
|
||||
|
||||
// Attach behaviors again to the modified piece of HTML; this will
|
||||
// create a new field model and call rerenderedFieldToCandidate() with
|
||||
// it.
|
||||
Drupal.attachBehaviors($context.get(0));
|
||||
};
|
||||
|
||||
// When propagating the changes of another instance of this field, this
|
||||
// field is not being actively edited and hence no state changes are
|
||||
// necessary. So: only update the state of this field when the rerendering
|
||||
// of this field happens not because of propagation, but because it is
|
||||
// being edited itself.
|
||||
if (!options.propagation) {
|
||||
// Deferred because renderUpdatedField is reacting to a field model
|
||||
// change event, and we want to make sure that event fully propagates
|
||||
// before making another change to the same model.
|
||||
_.defer(() => {
|
||||
// First set the state to 'candidate', to allow all attached views to
|
||||
// clean up all their "active state"-related changes.
|
||||
fieldModel.set('state', 'candidate');
|
||||
|
||||
// Similarly, the above .set() call's change event must fully
|
||||
// propagate before calling it again.
|
||||
_.defer(() => {
|
||||
// Set the field's state to 'inactive', to enable the updating of
|
||||
// its DOM value.
|
||||
fieldModel.set('state', 'inactive', { reason: 'rerender' });
|
||||
|
||||
renderField();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
renderField();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Propagates changes to an updated field to all instances of that field.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} updatedField
|
||||
* The FieldModel whose 'html' attribute changed.
|
||||
* @param {string} html
|
||||
* The updated 'html' attribute.
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {bool} options.propagation
|
||||
* Whether this change to the 'html' attribute occurred because of the
|
||||
* propagation of changes to another instance of this field.
|
||||
*
|
||||
* @see Drupal.quickedit.AppView#renderUpdatedField
|
||||
*/
|
||||
propagateUpdatedField(updatedField, html, options) {
|
||||
// Don't propagate field updates that themselves were caused by
|
||||
// propagation.
|
||||
if (options.propagation) {
|
||||
return;
|
||||
}
|
||||
|
||||
const htmlForOtherViewModes = updatedField.get('htmlForOtherViewModes');
|
||||
Drupal.quickedit.collections.fields
|
||||
// Find all instances of fields that display the same logical field
|
||||
// (same entity, same field, just a different instance and maybe a
|
||||
// different view mode).
|
||||
.where({ logicalFieldID: updatedField.get('logicalFieldID') })
|
||||
.forEach(field => {
|
||||
if (field === updatedField) {
|
||||
// Ignore the field that was already updated.
|
||||
}
|
||||
// If this other instance of the field has the same view mode, we can
|
||||
// update it easily.
|
||||
else if (field.getViewMode() === updatedField.getViewMode()) {
|
||||
field.set('html', updatedField.get('html'));
|
||||
}
|
||||
// If this other instance of the field has a different view mode, and
|
||||
// that is one of the view modes for which a re-rendered version is
|
||||
// available (and that should be the case unless this field was only
|
||||
// added to the page after editing of the updated field began), then
|
||||
// use that view mode's re-rendered version.
|
||||
else if (field.getViewMode() in htmlForOtherViewModes) {
|
||||
field.set('html', htmlForOtherViewModes[field.getViewMode()], {
|
||||
propagation: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* If the new in-place editable field is for the entity that's currently
|
||||
* being edited, then transition it to the 'candidate' state.
|
||||
*
|
||||
* This happens when a field was modified, saved and hence rerendered.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* A field that was just added to the collection of fields.
|
||||
*/
|
||||
rerenderedFieldToCandidate(fieldModel) {
|
||||
const activeEntity = Drupal.quickedit.collections.entities.findWhere({
|
||||
isActive: true,
|
||||
});
|
||||
|
||||
// Early-return if there is no active entity.
|
||||
if (!activeEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the field's entity is the active entity, make it a candidate.
|
||||
if (fieldModel.get('entity') === activeEntity) {
|
||||
this.setupEditor(fieldModel);
|
||||
fieldModel.set('state', 'candidate');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* EntityModel Collection change handler.
|
||||
*
|
||||
* Handler is called `change:isActive` and enforces a single active entity.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} changedEntityModel
|
||||
* The entityModel instance whose active state has changed.
|
||||
*/
|
||||
enforceSingleActiveEntity(changedEntityModel) {
|
||||
// When an entity is deactivated, we don't need to enforce anything.
|
||||
if (changedEntityModel.get('isActive') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This entity was activated; deactivate all other entities.
|
||||
changedEntityModel.collection
|
||||
.chain()
|
||||
.filter(
|
||||
entityModel =>
|
||||
entityModel.get('isActive') === true &&
|
||||
entityModel !== changedEntityModel,
|
||||
)
|
||||
.each(entityModel => {
|
||||
entityModel.set('state', 'deactivating');
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, _, Backbone, Drupal);
|
|
@ -1,91 +1,49 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that controls the overall "in-place editing application".
|
||||
*
|
||||
* @see Drupal.quickedit.AppModel
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, _, Backbone, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
// Indicates whether the page should be reloaded after in-place editing has
|
||||
// shut down. A page reload is necessary to re-instate the original HTML of
|
||||
// the edited fields if in-place editing has been canceled and one or more of
|
||||
// the entity's fields were saved to PrivateTempStore: one of them may have
|
||||
// been changed to the empty value and hence may have been rerendered as the
|
||||
// empty string, which makes it impossible for Quick Edit to know where to
|
||||
// restore the original HTML.
|
||||
var reload = false;
|
||||
|
||||
Drupal.quickedit.AppView = Backbone.View.extend(/** @lends Drupal.quickedit.AppView# */{
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.AppModel} options.model
|
||||
* The application state model.
|
||||
* @param {Drupal.quickedit.EntityCollection} options.entitiesCollection
|
||||
* All on-page entities.
|
||||
* @param {Drupal.quickedit.FieldCollection} options.fieldsCollection
|
||||
* All on-page fields
|
||||
*/
|
||||
initialize: function (options) {
|
||||
// AppView's configuration for handling states.
|
||||
// @see Drupal.quickedit.FieldModel.states
|
||||
Drupal.quickedit.AppView = Backbone.View.extend({
|
||||
initialize: function initialize(options) {
|
||||
this.activeFieldStates = ['activating', 'active'];
|
||||
this.singleFieldStates = ['highlighted', 'activating', 'active'];
|
||||
this.changedFieldStates = ['changed', 'saving', 'saved', 'invalid'];
|
||||
this.readyFieldStates = ['candidate', 'highlighted'];
|
||||
|
||||
// Track app state.
|
||||
this.listenTo(options.entitiesCollection, 'change:state', this.appStateChange);
|
||||
this.listenTo(options.entitiesCollection, 'change:isActive', this.enforceSingleActiveEntity);
|
||||
|
||||
// Track app state.
|
||||
this.listenTo(options.fieldsCollection, 'change:state', this.editorStateChange);
|
||||
// Respond to field model HTML representation change events.
|
||||
|
||||
this.listenTo(options.fieldsCollection, 'change:html', this.renderUpdatedField);
|
||||
this.listenTo(options.fieldsCollection, 'change:html', this.propagateUpdatedField);
|
||||
// Respond to addition.
|
||||
|
||||
this.listenTo(options.fieldsCollection, 'add', this.rerenderedFieldToCandidate);
|
||||
// Respond to destruction.
|
||||
|
||||
this.listenTo(options.fieldsCollection, 'destroy', this.teardownEditor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles setup/teardown and state changes when the active entity changes.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} entityModel
|
||||
* An instance of the EntityModel class.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.EntityModel.states}.
|
||||
*/
|
||||
appStateChange: function (entityModel, state) {
|
||||
appStateChange: function appStateChange(entityModel, state) {
|
||||
var app = this;
|
||||
var entityToolbarView;
|
||||
var entityToolbarView = void 0;
|
||||
switch (state) {
|
||||
case 'launching':
|
||||
reload = false;
|
||||
// First, create an entity toolbar view.
|
||||
|
||||
entityToolbarView = new Drupal.quickedit.EntityToolbarView({
|
||||
model: entityModel,
|
||||
appModel: this.model
|
||||
});
|
||||
entityModel.toolbarView = entityToolbarView;
|
||||
// Second, set up in-place editors.
|
||||
// They must be notified of state changes, hence this must happen
|
||||
// while the associated fields are still in the 'inactive' state.
|
||||
|
||||
entityModel.get('fields').each(function (fieldModel) {
|
||||
app.setupEditor(fieldModel);
|
||||
});
|
||||
// Third, transition the entity to the 'opening' state, which will
|
||||
// transition all fields from 'inactive' to 'candidate'.
|
||||
|
||||
_.defer(function () {
|
||||
entityModel.set('state', 'opening');
|
||||
});
|
||||
|
@ -93,175 +51,91 @@
|
|||
|
||||
case 'closed':
|
||||
entityToolbarView = entityModel.toolbarView;
|
||||
// First, tear down the in-place editors.
|
||||
|
||||
entityModel.get('fields').each(function (fieldModel) {
|
||||
app.teardownEditor(fieldModel);
|
||||
});
|
||||
// Second, tear down the entity toolbar view.
|
||||
|
||||
if (entityToolbarView) {
|
||||
entityToolbarView.remove();
|
||||
delete entityModel.toolbarView;
|
||||
}
|
||||
// A page reload may be necessary to re-instate the original HTML of
|
||||
// the edited fields.
|
||||
|
||||
if (reload) {
|
||||
reload = false;
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Accepts or reject editor (Editor) state changes.
|
||||
*
|
||||
* This is what ensures that the app is in control of what happens.
|
||||
*
|
||||
* @param {string} from
|
||||
* The previous state.
|
||||
* @param {string} to
|
||||
* The new state.
|
||||
* @param {null|object} context
|
||||
* The context that is trying to trigger the state change.
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The fieldModel to which this change applies.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the editor change was accepted or rejected.
|
||||
*/
|
||||
acceptEditorStateChange: function (from, to, context, fieldModel) {
|
||||
acceptEditorStateChange: function acceptEditorStateChange(from, to, context, fieldModel) {
|
||||
var accept = true;
|
||||
|
||||
// If the app is in view mode, then reject all state changes except for
|
||||
// those to 'inactive'.
|
||||
if (context && (context.reason === 'stop' || context.reason === 'rerender')) {
|
||||
if (from === 'candidate' && to === 'inactive') {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
// 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 (!Drupal.quickedit.FieldModel.followsStateSequence(from, to)) {
|
||||
accept = false;
|
||||
// Allow: activating/active -> candidate.
|
||||
// Necessary to stop editing a field.
|
||||
if (_.indexOf(this.activeFieldStates, from) !== -1 && to === 'candidate') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: changed/invalid -> candidate.
|
||||
// Necessary to stop editing a field when it is changed or invalid.
|
||||
else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: highlighted -> candidate.
|
||||
// Necessary to stop highlighting a field.
|
||||
else if (from === 'highlighted' && to === 'candidate') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: saved -> candidate.
|
||||
// Necessary when successfully saved a field.
|
||||
else if (from === 'saved' && to === 'candidate') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: invalid -> saving.
|
||||
// Necessary to be able to save a corrected, invalid field.
|
||||
else if (from === 'invalid' && to === 'saving') {
|
||||
accept = true;
|
||||
}
|
||||
// Allow: invalid -> activating.
|
||||
// Necessary to be able to correct a field that turned out to be
|
||||
// invalid after the user already had moved on to the next field
|
||||
// (which we explicitly allow to have a fluent UX).
|
||||
else if (from === 'invalid' && to === 'activating') {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!Drupal.quickedit.FieldModel.followsStateSequence(from, to)) {
|
||||
accept = false;
|
||||
|
||||
// If it's not against the general principle, then here are more
|
||||
// disallowed cases to check.
|
||||
if (accept) {
|
||||
var activeField;
|
||||
var activeFieldState;
|
||||
// Ensure only one field (editor) at a time is active … but allow a
|
||||
// user to hop from one field to the next, even if we still have to
|
||||
// start saving the field that is currently active: assume it will be
|
||||
// valid, to allow for a fluent UX. (If it turns out to be invalid,
|
||||
// this block of code also handles that.)
|
||||
if ((this.readyFieldStates.indexOf(from) !== -1 || from === 'invalid') && this.activeFieldStates.indexOf(to) !== -1) {
|
||||
activeField = this.model.get('activeField');
|
||||
if (activeField && activeField !== fieldModel) {
|
||||
activeFieldState = activeField.get('state');
|
||||
// Allow the state change. If the state of the active field is:
|
||||
// - 'activating' or 'active': change it to 'candidate'
|
||||
// - 'changed' or 'invalid': change it to 'saving'
|
||||
// - 'saving' or 'saved': don't do anything.
|
||||
if (this.activeFieldStates.indexOf(activeFieldState) !== -1) {
|
||||
activeField.set('state', 'candidate');
|
||||
}
|
||||
else if (activeFieldState === 'changed' || activeFieldState === 'invalid') {
|
||||
activeField.set('state', 'saving');
|
||||
}
|
||||
|
||||
// If the field that's being activated is in fact already in the
|
||||
// invalid state (which can only happen because above we allowed
|
||||
// the user to move on to another field to allow for a fluent UX;
|
||||
// we assumed it would be saved successfully), then we shouldn't
|
||||
// allow the field to enter the 'activating' state, instead, we
|
||||
// simply change the active editor. All guarantees and
|
||||
// assumptions for this field still hold!
|
||||
if (from === 'invalid') {
|
||||
this.model.set('activeField', fieldModel);
|
||||
accept = false;
|
||||
}
|
||||
// Do not reject: the field is either in the 'candidate' or
|
||||
// 'highlighted' state and we allow it to enter the 'activating'
|
||||
// state!
|
||||
}
|
||||
}
|
||||
// Reject going from activating/active to candidate because of a
|
||||
// mouseleave.
|
||||
else if (_.indexOf(this.activeFieldStates, 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) {
|
||||
if (_.indexOf(this.activeFieldStates, from) !== -1 && to === 'candidate') {
|
||||
accept = true;
|
||||
} else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
|
||||
accept = true;
|
||||
} else if (from === 'highlighted' && to === 'candidate') {
|
||||
accept = true;
|
||||
} else if (from === 'saved' && to === 'candidate') {
|
||||
accept = true;
|
||||
} else if (from === 'invalid' && to === 'saving') {
|
||||
accept = true;
|
||||
} else if (from === 'invalid' && to === 'activating') {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (accept) {
|
||||
var activeField = void 0;
|
||||
var activeFieldState = void 0;
|
||||
|
||||
if ((this.readyFieldStates.indexOf(from) !== -1 || from === 'invalid') && this.activeFieldStates.indexOf(to) !== -1) {
|
||||
activeField = this.model.get('activeField');
|
||||
if (activeField && activeField !== fieldModel) {
|
||||
activeFieldState = activeField.get('state');
|
||||
|
||||
if (this.activeFieldStates.indexOf(activeFieldState) !== -1) {
|
||||
activeField.set('state', 'candidate');
|
||||
} else if (activeFieldState === 'changed' || activeFieldState === 'invalid') {
|
||||
activeField.set('state', 'saving');
|
||||
}
|
||||
|
||||
if (from === 'invalid') {
|
||||
this.model.set('activeField', fieldModel);
|
||||
accept = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (_.indexOf(this.activeFieldStates, from) !== -1 && to === 'candidate') {
|
||||
if (context && context.reason === 'mouseleave') {
|
||||
accept = false;
|
||||
}
|
||||
} else if ((from === 'changed' || from === 'invalid') && to === 'candidate') {
|
||||
if (context && context.reason === 'mouseleave') {
|
||||
accept = false;
|
||||
} else if (context && context.confirmed) {
|
||||
accept = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return accept;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets up the in-place editor for the given field.
|
||||
*
|
||||
* Must happen before the fieldModel's state is changed to 'candidate'.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The field for which an in-place editor must be set up.
|
||||
*/
|
||||
setupEditor: function (fieldModel) {
|
||||
// Get the corresponding entity toolbar.
|
||||
setupEditor: function setupEditor(fieldModel) {
|
||||
var entityModel = fieldModel.get('entity');
|
||||
var entityToolbarView = entityModel.toolbarView;
|
||||
// Get the field toolbar DOM root from the entity toolbar.
|
||||
|
||||
var fieldToolbarRoot = entityToolbarView.getToolbarRoot();
|
||||
// Create in-place editor.
|
||||
|
||||
var editorName = fieldModel.get('metadata').editor;
|
||||
var editorModel = new Drupal.quickedit.EditorModel();
|
||||
var editorView = new Drupal.quickedit.editors[editorName]({
|
||||
|
@ -270,9 +144,6 @@
|
|||
fieldModel: fieldModel
|
||||
});
|
||||
|
||||
// Create in-place editor's toolbar for this field — stored inside the
|
||||
// entity toolbar, the entity toolbar will position itself appropriately
|
||||
// above (or below) the edited element.
|
||||
var toolbarView = new Drupal.quickedit.FieldToolbarView({
|
||||
el: fieldToolbarRoot,
|
||||
model: fieldModel,
|
||||
|
@ -281,77 +152,44 @@
|
|||
entityModel: entityModel
|
||||
});
|
||||
|
||||
// Create decoration for edited element: padding if necessary, sets
|
||||
// classes on the element to style it according to the current state.
|
||||
var decorationView = new Drupal.quickedit.FieldDecorationView({
|
||||
el: $(editorView.getEditedElement()),
|
||||
model: fieldModel,
|
||||
editorView: editorView
|
||||
});
|
||||
|
||||
// Track these three views in FieldModel so that we can tear them down
|
||||
// correctly.
|
||||
fieldModel.editorView = editorView;
|
||||
fieldModel.toolbarView = toolbarView;
|
||||
fieldModel.decorationView = decorationView;
|
||||
},
|
||||
|
||||
/**
|
||||
* Tears down the in-place editor for the given field.
|
||||
*
|
||||
* Must happen after the fieldModel's state is changed to 'inactive'.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The field for which an in-place editor must be torn down.
|
||||
*/
|
||||
teardownEditor: function (fieldModel) {
|
||||
// Early-return if this field was not yet decorated.
|
||||
teardownEditor: function teardownEditor(fieldModel) {
|
||||
if (typeof fieldModel.editorView === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unbind event handlers; remove toolbar element; delete toolbar view.
|
||||
fieldModel.toolbarView.remove();
|
||||
delete fieldModel.toolbarView;
|
||||
|
||||
// Unbind event handlers; delete decoration view. Don't remove the element
|
||||
// because that would remove the field itself.
|
||||
fieldModel.decorationView.remove();
|
||||
delete fieldModel.decorationView;
|
||||
|
||||
// Unbind event handlers; delete editor view. Don't remove the element
|
||||
// because that would remove the field itself.
|
||||
fieldModel.editorView.remove();
|
||||
delete fieldModel.editorView;
|
||||
},
|
||||
|
||||
/**
|
||||
* Asks the user to confirm whether he wants to stop editing via a modal.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} entityModel
|
||||
* An instance of the EntityModel class.
|
||||
*
|
||||
* @see Drupal.quickedit.AppView#acceptEditorStateChange
|
||||
*/
|
||||
confirmEntityDeactivation: function (entityModel) {
|
||||
confirmEntityDeactivation: function confirmEntityDeactivation(entityModel) {
|
||||
var that = this;
|
||||
var discardDialog;
|
||||
var discardDialog = void 0;
|
||||
|
||||
function closeDiscardDialog(action) {
|
||||
discardDialog.close(action);
|
||||
// The active modal has been removed.
|
||||
|
||||
that.model.set('activeModal', null);
|
||||
|
||||
// If the targetState is saving, the field must be saved, then the
|
||||
// entity must be saved.
|
||||
if (action === 'save') {
|
||||
entityModel.set('state', 'committing', {confirmed: true});
|
||||
}
|
||||
else {
|
||||
entityModel.set('state', 'deactivating', {confirmed: true});
|
||||
// Editing has been canceled and the changes will not be saved. Mark
|
||||
// the page for reload if the entityModel declares that it requires
|
||||
// a reload.
|
||||
entityModel.set('state', 'committing', { confirmed: true });
|
||||
} else {
|
||||
entityModel.set('state', 'deactivating', { confirmed: true });
|
||||
|
||||
if (entityModel.get('reload')) {
|
||||
reload = true;
|
||||
entityModel.set('reload', false);
|
||||
|
@ -359,38 +197,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Only instantiate if there isn't a modal instance visible yet.
|
||||
if (!this.model.get('activeModal')) {
|
||||
var $unsavedChanges = $('<div>' + Drupal.t('You have unsaved changes') + '</div>');
|
||||
discardDialog = Drupal.dialog($unsavedChanges.get(0), {
|
||||
title: Drupal.t('Discard changes?'),
|
||||
dialogClass: 'quickedit-discard-modal',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Save'),
|
||||
click: function () {
|
||||
closeDiscardDialog('save');
|
||||
},
|
||||
primary: true
|
||||
buttons: [{
|
||||
text: Drupal.t('Save'),
|
||||
click: function click() {
|
||||
closeDiscardDialog('save');
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Discard changes'),
|
||||
click: function () {
|
||||
closeDiscardDialog('discard');
|
||||
}
|
||||
|
||||
primary: true
|
||||
}, {
|
||||
text: Drupal.t('Discard changes'),
|
||||
click: function click() {
|
||||
closeDiscardDialog('discard');
|
||||
}
|
||||
],
|
||||
// Prevent this modal from being closed without the user making a
|
||||
// choice as per http://stackoverflow.com/a/5438771.
|
||||
}],
|
||||
|
||||
closeOnEscape: false,
|
||||
create: function () {
|
||||
create: function create() {
|
||||
$(this).parent().find('.ui-dialog-titlebar-close').remove();
|
||||
},
|
||||
|
||||
beforeClose: false,
|
||||
close: function (event) {
|
||||
// Automatically destroy the DOM element that was used for the
|
||||
// dialog.
|
||||
close: function close(event) {
|
||||
$(event.target).remove();
|
||||
}
|
||||
});
|
||||
|
@ -399,202 +232,91 @@
|
|||
discardDialog.showModal();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reacts to field state changes; tracks global state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The `fieldModel` holding the state.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
editorStateChange: function (fieldModel, state) {
|
||||
editorStateChange: function editorStateChange(fieldModel, state) {
|
||||
var from = fieldModel.previous('state');
|
||||
var to = state;
|
||||
|
||||
// Keep track of the highlighted field in the global state.
|
||||
if (_.indexOf(this.singleFieldStates, to) !== -1 && this.model.get('highlightedField') !== fieldModel) {
|
||||
this.model.set('highlightedField', fieldModel);
|
||||
}
|
||||
else if (this.model.get('highlightedField') === fieldModel && to === 'candidate') {
|
||||
} else if (this.model.get('highlightedField') === fieldModel && to === 'candidate') {
|
||||
this.model.set('highlightedField', null);
|
||||
}
|
||||
|
||||
// Keep track of the active field in the global state.
|
||||
if (_.indexOf(this.activeFieldStates, to) !== -1 && this.model.get('activeField') !== fieldModel) {
|
||||
this.model.set('activeField', fieldModel);
|
||||
}
|
||||
else if (this.model.get('activeField') === fieldModel && to === 'candidate') {
|
||||
// Discarded if it transitions from a changed state to 'candidate'.
|
||||
} else if (this.model.get('activeField') === fieldModel && to === 'candidate') {
|
||||
if (from === 'changed' || from === 'invalid') {
|
||||
fieldModel.editorView.revert();
|
||||
}
|
||||
this.model.set('activeField', null);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Render an updated field (a field whose 'html' attribute changed).
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The FieldModel whose 'html' attribute changed.
|
||||
* @param {string} html
|
||||
* The updated 'html' attribute.
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {bool} options.propagation
|
||||
* Whether this change to the 'html' attribute occurred because of the
|
||||
* propagation of changes to another instance of this field.
|
||||
*/
|
||||
renderUpdatedField: function (fieldModel, html, options) {
|
||||
// Get data necessary to rerender property before it is unavailable.
|
||||
renderUpdatedField: function renderUpdatedField(fieldModel, html, options) {
|
||||
var $fieldWrapper = $(fieldModel.get('el'));
|
||||
var $context = $fieldWrapper.parent();
|
||||
|
||||
var renderField = function () {
|
||||
// Destroy the field model; this will cause all attached views to be
|
||||
// destroyed too, and removal from all collections in which it exists.
|
||||
var renderField = function renderField() {
|
||||
fieldModel.destroy();
|
||||
|
||||
// Replace the old content with the new content.
|
||||
$fieldWrapper.replaceWith(html);
|
||||
|
||||
// Attach behaviors again to the modified piece of HTML; this will
|
||||
// create a new field model and call rerenderedFieldToCandidate() with
|
||||
// it.
|
||||
Drupal.attachBehaviors($context.get(0));
|
||||
};
|
||||
|
||||
// When propagating the changes of another instance of this field, this
|
||||
// field is not being actively edited and hence no state changes are
|
||||
// necessary. So: only update the state of this field when the rerendering
|
||||
// of this field happens not because of propagation, but because it is
|
||||
// being edited itself.
|
||||
if (!options.propagation) {
|
||||
// Deferred because renderUpdatedField is reacting to a field model
|
||||
// change event, and we want to make sure that event fully propagates
|
||||
// before making another change to the same model.
|
||||
_.defer(function () {
|
||||
// First set the state to 'candidate', to allow all attached views to
|
||||
// clean up all their "active state"-related changes.
|
||||
fieldModel.set('state', 'candidate');
|
||||
|
||||
// Similarly, the above .set() call's change event must fully
|
||||
// propagate before calling it again.
|
||||
_.defer(function () {
|
||||
// Set the field's state to 'inactive', to enable the updating of
|
||||
// its DOM value.
|
||||
fieldModel.set('state', 'inactive', {reason: 'rerender'});
|
||||
fieldModel.set('state', 'inactive', { reason: 'rerender' });
|
||||
|
||||
renderField();
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
renderField();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Propagates changes to an updated field to all instances of that field.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} updatedField
|
||||
* The FieldModel whose 'html' attribute changed.
|
||||
* @param {string} html
|
||||
* The updated 'html' attribute.
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {bool} options.propagation
|
||||
* Whether this change to the 'html' attribute occurred because of the
|
||||
* propagation of changes to another instance of this field.
|
||||
*
|
||||
* @see Drupal.quickedit.AppView#renderUpdatedField
|
||||
*/
|
||||
propagateUpdatedField: function (updatedField, html, options) {
|
||||
// Don't propagate field updates that themselves were caused by
|
||||
// propagation.
|
||||
propagateUpdatedField: function propagateUpdatedField(updatedField, html, options) {
|
||||
if (options.propagation) {
|
||||
return;
|
||||
}
|
||||
|
||||
var htmlForOtherViewModes = updatedField.get('htmlForOtherViewModes');
|
||||
Drupal.quickedit.collections.fields
|
||||
// Find all instances of fields that display the same logical field
|
||||
// (same entity, same field, just a different instance and maybe a
|
||||
// different view mode).
|
||||
.where({logicalFieldID: updatedField.get('logicalFieldID')})
|
||||
.forEach(function (field) {
|
||||
// Ignore the field that was already updated.
|
||||
if (field === updatedField) {
|
||||
return;
|
||||
}
|
||||
// If this other instance of the field has the same view mode, we can
|
||||
// update it easily.
|
||||
else if (field.getViewMode() === updatedField.getViewMode()) {
|
||||
Drupal.quickedit.collections.fields.where({ logicalFieldID: updatedField.get('logicalFieldID') }).forEach(function (field) {
|
||||
if (field === updatedField) {} else if (field.getViewMode() === updatedField.getViewMode()) {
|
||||
field.set('html', updatedField.get('html'));
|
||||
}
|
||||
// If this other instance of the field has a different view mode, and
|
||||
// that is one of the view modes for which a re-rendered version is
|
||||
// available (and that should be the case unless this field was only
|
||||
// added to the page after editing of the updated field began), then
|
||||
// use that view mode's re-rendered version.
|
||||
else {
|
||||
if (field.getViewMode() in htmlForOtherViewModes) {
|
||||
field.set('html', htmlForOtherViewModes[field.getViewMode()], {propagation: true});
|
||||
} else if (field.getViewMode() in htmlForOtherViewModes) {
|
||||
field.set('html', htmlForOtherViewModes[field.getViewMode()], {
|
||||
propagation: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
rerenderedFieldToCandidate: function rerenderedFieldToCandidate(fieldModel) {
|
||||
var activeEntity = Drupal.quickedit.collections.entities.findWhere({
|
||||
isActive: true
|
||||
});
|
||||
|
||||
/**
|
||||
* If the new in-place editable field is for the entity that's currently
|
||||
* being edited, then transition it to the 'candidate' state.
|
||||
*
|
||||
* This happens when a field was modified, saved and hence rerendered.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* A field that was just added to the collection of fields.
|
||||
*/
|
||||
rerenderedFieldToCandidate: function (fieldModel) {
|
||||
var activeEntity = Drupal.quickedit.collections.entities.findWhere({isActive: true});
|
||||
|
||||
// Early-return if there is no active entity.
|
||||
if (!activeEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the field's entity is the active entity, make it a candidate.
|
||||
if (fieldModel.get('entity') === activeEntity) {
|
||||
this.setupEditor(fieldModel);
|
||||
fieldModel.set('state', 'candidate');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* EntityModel Collection change handler.
|
||||
*
|
||||
* Handler is called `change:isActive` and enforces a single active entity.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} changedEntityModel
|
||||
* The entityModel instance whose active state has changed.
|
||||
*/
|
||||
enforceSingleActiveEntity: function (changedEntityModel) {
|
||||
// When an entity is deactivated, we don't need to enforce anything.
|
||||
enforceSingleActiveEntity: function enforceSingleActiveEntity(changedEntityModel) {
|
||||
if (changedEntityModel.get('isActive') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This entity was activated; deactivate all other entities.
|
||||
changedEntityModel.collection.chain()
|
||||
.filter(function (entityModel) {
|
||||
return entityModel.get('isActive') === true && entityModel !== changedEntityModel;
|
||||
})
|
||||
.each(function (entityModel) {
|
||||
entityModel.set('state', 'deactivating');
|
||||
});
|
||||
changedEntityModel.collection.chain().filter(function (entityModel) {
|
||||
return entityModel.get('isActive') === true && entityModel !== changedEntityModel;
|
||||
}).each(function (entityModel) {
|
||||
entityModel.set('state', 'deactivating');
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}(jQuery, _, Backbone, Drupal));
|
||||
})(jQuery, _, Backbone, Drupal);
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides a dynamic contextual link.
|
||||
*/
|
||||
|
||||
(function($, Backbone, Drupal) {
|
||||
Drupal.quickedit.ContextualLinkView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.ContextualLinkView# */ {
|
||||
/**
|
||||
* Define all events to listen to.
|
||||
*
|
||||
* @return {object}
|
||||
* A map of events.
|
||||
*/
|
||||
events() {
|
||||
// Prevents delay and simulated mouse events.
|
||||
function touchEndToClick(event) {
|
||||
event.preventDefault();
|
||||
event.target.click();
|
||||
}
|
||||
|
||||
return {
|
||||
'click a': function(event) {
|
||||
event.preventDefault();
|
||||
this.model.set('state', 'launching');
|
||||
},
|
||||
'touchEnd a': touchEndToClick,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new contextual link view.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.EntityModel} options.model
|
||||
* The associated entity's model.
|
||||
* @param {Drupal.quickedit.AppModel} options.appModel
|
||||
* The application state model.
|
||||
* @param {object} options.strings
|
||||
* The strings for the "Quick edit" link.
|
||||
*/
|
||||
initialize(options) {
|
||||
// Insert the text of the quick edit toggle.
|
||||
this.$el.find('a').text(options.strings.quickEdit);
|
||||
// Initial render.
|
||||
this.render();
|
||||
// Re-render whenever this entity's isActive attribute changes.
|
||||
this.listenTo(this.model, 'change:isActive', this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render function for the contextual link view.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} entityModel
|
||||
* The associated `EntityModel`.
|
||||
* @param {bool} isActive
|
||||
* Whether the in-place editor is active or not.
|
||||
*
|
||||
* @return {Drupal.quickedit.ContextualLinkView}
|
||||
* The `ContextualLinkView` in question.
|
||||
*/
|
||||
render(entityModel, isActive) {
|
||||
this.$el.find('a').attr('aria-pressed', isActive);
|
||||
|
||||
// Hides the contextual links if an in-place editor is active.
|
||||
this.$el.closest('.contextual').toggle(!isActive);
|
||||
|
||||
return this;
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, Backbone, Drupal);
|
|
@ -1,81 +1,39 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides a dynamic contextual link.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Backbone, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.ContextualLinkView = Backbone.View.extend(/** @lends Drupal.quickedit.ContextualLinkView# */{
|
||||
|
||||
/**
|
||||
* Define all events to listen to.
|
||||
*
|
||||
* @return {object}
|
||||
* A map of events.
|
||||
*/
|
||||
events: function () {
|
||||
// Prevents delay and simulated mouse events.
|
||||
Drupal.quickedit.ContextualLinkView = Backbone.View.extend({
|
||||
events: function events() {
|
||||
function touchEndToClick(event) {
|
||||
event.preventDefault();
|
||||
event.target.click();
|
||||
}
|
||||
|
||||
return {
|
||||
'click a': function (event) {
|
||||
'click a': function clickA(event) {
|
||||
event.preventDefault();
|
||||
this.model.set('state', 'launching');
|
||||
},
|
||||
'touchEnd a': touchEndToClick
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new contextual link view.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.EntityModel} options.model
|
||||
* The associated entity's model.
|
||||
* @param {Drupal.quickedit.AppModel} options.appModel
|
||||
* The application state model.
|
||||
* @param {object} options.strings
|
||||
* The strings for the "Quick edit" link.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
// Insert the text of the quick edit toggle.
|
||||
initialize: function initialize(options) {
|
||||
this.$el.find('a').text(options.strings.quickEdit);
|
||||
// Initial render.
|
||||
|
||||
this.render();
|
||||
// Re-render whenever this entity's isActive attribute changes.
|
||||
|
||||
this.listenTo(this.model, 'change:isActive', this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render function for the contextual link view.
|
||||
*
|
||||
* @param {Drupal.quickedit.EntityModel} entityModel
|
||||
* The associated `EntityModel`.
|
||||
* @param {bool} isActive
|
||||
* Whether the in-place editor is active or not.
|
||||
*
|
||||
* @return {Drupal.quickedit.ContextualLinkView}
|
||||
* The `ContextualLinkView` in question.
|
||||
*/
|
||||
render: function (entityModel, isActive) {
|
||||
render: function render(entityModel, isActive) {
|
||||
this.$el.find('a').attr('aria-pressed', isActive);
|
||||
|
||||
// Hides the contextual links if an in-place editor is active.
|
||||
this.$el.closest('.contextual').toggle(!isActive);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, Backbone, Drupal);
|
||||
})(jQuery, Backbone, Drupal);
|
325
web/core/modules/quickedit/js/views/EditorView.es6.js
Normal file
325
web/core/modules/quickedit/js/views/EditorView.es6.js
Normal file
|
@ -0,0 +1,325 @@
|
|||
/**
|
||||
* @file
|
||||
* An abstract Backbone View that controls an in-place editor.
|
||||
*/
|
||||
|
||||
(function($, Backbone, Drupal) {
|
||||
Drupal.quickedit.EditorView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.EditorView# */ {
|
||||
/**
|
||||
* A base implementation that outlines the structure for in-place editors.
|
||||
*
|
||||
* Specific in-place editor implementations should subclass (extend) this
|
||||
* View and override whichever method they deem necessary to override.
|
||||
*
|
||||
* Typically you would want to override this method to set the
|
||||
* originalValue attribute in the FieldModel to such a value that your
|
||||
* in-place editor can revert to the original value when necessary.
|
||||
*
|
||||
* @example
|
||||
* <caption>If you override this method, you should call this
|
||||
* method (the parent class' initialize()) first.</caption>
|
||||
* Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.EditorModel} options.model
|
||||
* The in-place editor state model.
|
||||
* @param {Drupal.quickedit.FieldModel} options.fieldModel
|
||||
* The field model.
|
||||
*
|
||||
* @see Drupal.quickedit.EditorModel
|
||||
* @see Drupal.quickedit.editors.plain_text
|
||||
*/
|
||||
initialize(options) {
|
||||
this.fieldModel = options.fieldModel;
|
||||
this.listenTo(this.fieldModel, 'change:state', this.stateChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove() {
|
||||
// The el property is the field, which should not be removed. Remove the
|
||||
// pointer to it, then call Backbone.View.prototype.remove().
|
||||
this.setElement();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the edited element.
|
||||
*
|
||||
* For some single cardinality fields, it may be necessary or useful to
|
||||
* not in-place edit (and hence decorate) the DOM element with the
|
||||
* data-quickedit-field-id attribute (which is the field's wrapper), but a
|
||||
* specific element within the field's wrapper.
|
||||
* e.g. using a WYSIWYG editor on a body field should happen on the DOM
|
||||
* element containing the text itself, not on the field wrapper.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* A jQuery-wrapped DOM element.
|
||||
*
|
||||
* @see Drupal.quickedit.editors.plain_text
|
||||
*/
|
||||
getEditedElement() {
|
||||
return this.$el;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {object}
|
||||
* Returns 3 Quick Edit UI settings that depend on the in-place editor:
|
||||
* - Boolean padding: indicates whether padding should be applied to the
|
||||
* edited element, to guarantee legibility of text.
|
||||
* - Boolean unifiedToolbar: provides the in-place editor with the ability
|
||||
* to insert its own toolbar UI into Quick Edit's tightly integrated
|
||||
* toolbar.
|
||||
* - Boolean fullWidthToolbar: indicates whether Quick Edit's tightly
|
||||
* integrated toolbar should consume the full width of the element,
|
||||
* rather than being just long enough to accommodate a label.
|
||||
*/
|
||||
getQuickEditUISettings() {
|
||||
return {
|
||||
padding: false,
|
||||
unifiedToolbar: false,
|
||||
fullWidthToolbar: false,
|
||||
popup: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The quickedit `FieldModel` that holds the state.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
stateChange(fieldModel, state) {
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
// An in-place editor view will not yet exist in this state, hence
|
||||
// this will never be reached. Listed for sake of completeness.
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
// Nothing to do for the typical in-place editor: it should not be
|
||||
// visible yet. Except when we come from the 'invalid' state, then we
|
||||
// clean up.
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
// Nothing to do for the typical in-place editor: it should not be
|
||||
// visible yet.
|
||||
break;
|
||||
|
||||
case 'activating': {
|
||||
// The user has indicated he wants to do in-place editing: if
|
||||
// something needs to be loaded (CSS/JavaScript/server data/…), then
|
||||
// do so at this stage, and once the in-place editor is ready,
|
||||
// set the 'active' state. A "loading" indicator will be shown in the
|
||||
// UI for as long as the field remains in this state.
|
||||
const loadDependencies = function(callback) {
|
||||
// Do the loading here.
|
||||
callback();
|
||||
};
|
||||
loadDependencies(() => {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'active':
|
||||
// The user can now actually use the in-place editor.
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
// Nothing to do for the typical in-place editor. The UI will show an
|
||||
// indicator that the field has changed.
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
// When the user has indicated he wants to save his changes to this
|
||||
// field, this state will be entered. If the previous saving attempt
|
||||
// resulted in validation errors, the previous state will be
|
||||
// 'invalid'. Clean up those validation errors while the user is
|
||||
// saving.
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
this.save();
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
// Nothing to do for the typical in-place editor. Immediately after
|
||||
// being saved, a field will go to the 'candidate' state, where it
|
||||
// should no longer be visible (after all, the field will then again
|
||||
// just be a *candidate* to be in-place edited).
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
// The modified field value was attempted to be saved, but there were
|
||||
// validation errors.
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reverts the modified value to the original, before editing started.
|
||||
*/
|
||||
revert() {
|
||||
// A no-op by default; each editor should implement reverting itself.
|
||||
// Note that if the in-place editor does not cause the FieldModel's
|
||||
// element to be modified, then nothing needs to happen.
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the modified value in the in-place editor for this field.
|
||||
*/
|
||||
save() {
|
||||
const fieldModel = this.fieldModel;
|
||||
const editorModel = this.model;
|
||||
const backstageId = `quickedit_backstage-${this.fieldModel.id.replace(
|
||||
/[/[\]_\s]/g,
|
||||
'-',
|
||||
)}`;
|
||||
|
||||
function fillAndSubmitForm(value) {
|
||||
const $form = $(`#${backstageId}`).find('form');
|
||||
// Fill in the value in any <input> that isn't hidden or a submit
|
||||
// button.
|
||||
$form
|
||||
.find(':input[type!="hidden"][type!="submit"]:not(select)')
|
||||
// Don't mess with the node summary.
|
||||
.not('[name$="\\[summary\\]"]')
|
||||
.val(value);
|
||||
// Submit the form.
|
||||
$form.find('.quickedit-form-submit').trigger('click.quickedit');
|
||||
}
|
||||
|
||||
const formOptions = {
|
||||
fieldID: this.fieldModel.get('fieldID'),
|
||||
$el: this.$el,
|
||||
nocssjs: true,
|
||||
other_view_modes: fieldModel.findOtherViewModes(),
|
||||
// Reset an existing entry for this entity in the PrivateTempStore (if
|
||||
// any) when saving the field. Logically speaking, this should happen in
|
||||
// a separate request because this is an entity-level operation, not a
|
||||
// field-level operation. But that would require an additional request,
|
||||
// that might not even be necessary: it is only when a user saves a
|
||||
// first changed field for an entity that this needs to happen:
|
||||
// precisely now!
|
||||
reset: !this.fieldModel.get('entity').get('inTempStore'),
|
||||
};
|
||||
|
||||
const self = this;
|
||||
Drupal.quickedit.util.form.load(formOptions, (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).
|
||||
const $backstage = $(
|
||||
Drupal.theme('quickeditBackstage', { id: backstageId }),
|
||||
).appendTo('body');
|
||||
// Hidden forms are stuffed into the backstage container for this field.
|
||||
const $form = $(form).appendTo($backstage);
|
||||
// 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.)
|
||||
$form.prop('novalidate', true);
|
||||
const $submit = $form.find('.quickedit-form-submit');
|
||||
self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
|
||||
formOptions,
|
||||
$submit,
|
||||
);
|
||||
|
||||
function removeHiddenForm() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(self.formSaveAjax);
|
||||
delete self.formSaveAjax;
|
||||
$backstage.remove();
|
||||
}
|
||||
|
||||
// Successfully saved.
|
||||
self.formSaveAjax.commands.quickeditFieldFormSaved = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
removeHiddenForm();
|
||||
// First, transition the state to 'saved'.
|
||||
fieldModel.set('state', 'saved');
|
||||
// Second, set the 'htmlForOtherViewModes' attribute, so that when
|
||||
// this field is rerendered, the change can be propagated to other
|
||||
// instances of this field, which may be displayed in different view
|
||||
// modes.
|
||||
fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
|
||||
// Finally, set the 'html' attribute on the field model. This will
|
||||
// cause the field to be rerendered.
|
||||
fieldModel.set('html', response.data);
|
||||
};
|
||||
|
||||
// Unsuccessfully saved; validation errors.
|
||||
self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
removeHiddenForm();
|
||||
editorModel.set('validationErrors', response.data);
|
||||
fieldModel.set('state', 'invalid');
|
||||
};
|
||||
|
||||
// The quickeditFieldForm 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 the form-based in-place editor, but pointless for any
|
||||
// other: the form itself won't be visible at all anyway! So, we just
|
||||
// ignore it.
|
||||
self.formSaveAjax.commands.quickeditFieldForm = function() {};
|
||||
|
||||
fillAndSubmitForm(editorModel.get('currentValue'));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows validation error messages.
|
||||
*
|
||||
* Should be called when the state is changed to 'invalid'.
|
||||
*/
|
||||
showValidationErrors() {
|
||||
const $errors = $(
|
||||
'<div class="quickedit-validation-errors"></div>',
|
||||
).append(this.model.get('validationErrors'));
|
||||
this.getEditedElement()
|
||||
.addClass('quickedit-validation-error')
|
||||
.after($errors);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleans up validation error messages.
|
||||
*
|
||||
* Should be called when the state is changed to 'candidate' or 'saving'. In
|
||||
* the case of the latter: the user has modified the value in the in-place
|
||||
* editor again to attempt to save again. In the case of the latter: the
|
||||
* invalid value was discarded.
|
||||
*/
|
||||
removeValidationErrors() {
|
||||
this.getEditedElement()
|
||||
.removeClass('quickedit-validation-error')
|
||||
.next('.quickedit-validation-errors')
|
||||
.remove();
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, Backbone, Drupal);
|
|
@ -1,156 +1,65 @@
|
|||
/**
|
||||
* @file
|
||||
* An abstract Backbone View that controls an in-place editor.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Backbone, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.EditorView = Backbone.View.extend(/** @lends Drupal.quickedit.EditorView# */{
|
||||
|
||||
/**
|
||||
* A base implementation that outlines the structure for in-place editors.
|
||||
*
|
||||
* Specific in-place editor implementations should subclass (extend) this
|
||||
* View and override whichever method they deem necessary to override.
|
||||
*
|
||||
* Typically you would want to override this method to set the
|
||||
* originalValue attribute in the FieldModel to such a value that your
|
||||
* in-place editor can revert to the original value when necessary.
|
||||
*
|
||||
* @example
|
||||
* <caption>If you override this method, you should call this
|
||||
* method (the parent class' initialize()) first.</caption>
|
||||
* Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.EditorModel} options.model
|
||||
* The in-place editor state model.
|
||||
* @param {Drupal.quickedit.FieldModel} options.fieldModel
|
||||
* The field model.
|
||||
*
|
||||
* @see Drupal.quickedit.EditorModel
|
||||
* @see Drupal.quickedit.editors.plain_text
|
||||
*/
|
||||
initialize: function (options) {
|
||||
Drupal.quickedit.EditorView = Backbone.View.extend({
|
||||
initialize: function initialize(options) {
|
||||
this.fieldModel = options.fieldModel;
|
||||
this.listenTo(this.fieldModel, 'change:state', this.stateChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove: function () {
|
||||
// The el property is the field, which should not be removed. Remove the
|
||||
// pointer to it, then call Backbone.View.prototype.remove().
|
||||
remove: function remove() {
|
||||
this.setElement();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the edited element.
|
||||
*
|
||||
* For some single cardinality fields, it may be necessary or useful to
|
||||
* not in-place edit (and hence decorate) the DOM element with the
|
||||
* data-quickedit-field-id attribute (which is the field's wrapper), but a
|
||||
* specific element within the field's wrapper.
|
||||
* e.g. using a WYSIWYG editor on a body field should happen on the DOM
|
||||
* element containing the text itself, not on the field wrapper.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* A jQuery-wrapped DOM element.
|
||||
*
|
||||
* @see Drupal.quickedit.editors.plain_text
|
||||
*/
|
||||
getEditedElement: function () {
|
||||
getEditedElement: function getEditedElement() {
|
||||
return this.$el;
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @return {object}
|
||||
* Returns 3 Quick Edit UI settings that depend on the in-place editor:
|
||||
* - Boolean padding: indicates whether padding should be applied to the
|
||||
* edited element, to guarantee legibility of text.
|
||||
* - Boolean unifiedToolbar: provides the in-place editor with the ability
|
||||
* to insert its own toolbar UI into Quick Edit's tightly integrated
|
||||
* toolbar.
|
||||
* - Boolean fullWidthToolbar: indicates whether Quick Edit's tightly
|
||||
* integrated toolbar should consume the full width of the element,
|
||||
* rather than being just long enough to accommodate a label.
|
||||
*/
|
||||
getQuickEditUISettings: function () {
|
||||
return {padding: false, unifiedToolbar: false, fullWidthToolbar: false, popup: false};
|
||||
getQuickEditUISettings: function getQuickEditUISettings() {
|
||||
return {
|
||||
padding: false,
|
||||
unifiedToolbar: false,
|
||||
fullWidthToolbar: false,
|
||||
popup: false
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} fieldModel
|
||||
* The quickedit `FieldModel` that holds the state.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
stateChange: function (fieldModel, state) {
|
||||
stateChange: function stateChange(fieldModel, state) {
|
||||
var from = fieldModel.previous('state');
|
||||
var to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
// An in-place editor view will not yet exist in this state, hence
|
||||
// this will never be reached. Listed for sake of completeness.
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
// Nothing to do for the typical in-place editor: it should not be
|
||||
// visible yet. Except when we come from the 'invalid' state, then we
|
||||
// clean up.
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
// Nothing to do for the typical in-place editor: it should not be
|
||||
// visible yet.
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// The user has indicated he wants to do in-place editing: if
|
||||
// something needs to be loaded (CSS/JavaScript/server data/…), then
|
||||
// do so at this stage, and once the in-place editor is ready,
|
||||
// set the 'active' state. A "loading" indicator will be shown in the
|
||||
// UI for as long as the field remains in this state.
|
||||
var loadDependencies = function (callback) {
|
||||
// Do the loading here.
|
||||
callback();
|
||||
};
|
||||
loadDependencies(function () {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
break;
|
||||
{
|
||||
var loadDependencies = function loadDependencies(callback) {
|
||||
callback();
|
||||
};
|
||||
loadDependencies(function () {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'active':
|
||||
// The user can now actually use the in-place editor.
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
// Nothing to do for the typical in-place editor. The UI will show an
|
||||
// indicator that the field has changed.
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
// When the user has indicated he wants to save his changes to this
|
||||
// field, this state will be entered. If the previous saving attempt
|
||||
// resulted in validation errors, the previous state will be
|
||||
// 'invalid'. Clean up those validation errors while the user is
|
||||
// saving.
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
|
@ -158,45 +67,24 @@
|
|||
break;
|
||||
|
||||
case 'saved':
|
||||
// Nothing to do for the typical in-place editor. Immediately after
|
||||
// being saved, a field will go to the 'candidate' state, where it
|
||||
// should no longer be visible (after all, the field will then again
|
||||
// just be a *candidate* to be in-place edited).
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
// The modified field value was attempted to be saved, but there were
|
||||
// validation errors.
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Reverts the modified value to the original, before editing started.
|
||||
*/
|
||||
revert: function () {
|
||||
// A no-op by default; each editor should implement reverting itself.
|
||||
// Note that if the in-place editor does not cause the FieldModel's
|
||||
// element to be modified, then nothing needs to happen.
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the modified value in the in-place editor for this field.
|
||||
*/
|
||||
save: function () {
|
||||
revert: function revert() {},
|
||||
save: function save() {
|
||||
var fieldModel = this.fieldModel;
|
||||
var editorModel = this.model;
|
||||
var backstageId = 'quickedit_backstage-' + this.fieldModel.id.replace(/[\/\[\]\_\s]/g, '-');
|
||||
var backstageId = 'quickedit_backstage-' + this.fieldModel.id.replace(/[/[\]_\s]/g, '-');
|
||||
|
||||
function fillAndSubmitForm(value) {
|
||||
var $form = $('#' + backstageId).find('form');
|
||||
// Fill in the value in any <input> that isn't hidden or a submit
|
||||
// button.
|
||||
$form.find(':input[type!="hidden"][type!="submit"]:not(select)')
|
||||
// Don't mess with the node summary.
|
||||
.not('[name$="\\[summary\\]"]').val(value);
|
||||
// Submit the form.
|
||||
|
||||
$form.find(':input[type!="hidden"][type!="submit"]:not(select)').not('[name$="\\[summary\\]"]').val(value);
|
||||
|
||||
$form.find('.quickedit-form-submit').trigger('click.quickedit');
|
||||
}
|
||||
|
||||
|
@ -205,28 +93,16 @@
|
|||
$el: this.$el,
|
||||
nocssjs: true,
|
||||
other_view_modes: fieldModel.findOtherViewModes(),
|
||||
// Reset an existing entry for this entity in the PrivateTempStore (if
|
||||
// any) when saving the field. Logically speaking, this should happen in
|
||||
// a separate request because this is an entity-level operation, not a
|
||||
// field-level operation. But that would require an additional request,
|
||||
// that might not even be necessary: it is only when a user saves a
|
||||
// first changed field for an entity that this needs to happen:
|
||||
// precisely now!
|
||||
|
||||
reset: !this.fieldModel.get('entity').get('inTempStore')
|
||||
};
|
||||
|
||||
var self = this;
|
||||
Drupal.quickedit.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).
|
||||
var $backstage = $(Drupal.theme('quickeditBackstage', {id: backstageId})).appendTo('body');
|
||||
// Hidden forms are stuffed into the backstage container for this field.
|
||||
var $backstage = $(Drupal.theme('quickeditBackstage', { id: backstageId })).appendTo('body');
|
||||
|
||||
var $form = $(form).appendTo($backstage);
|
||||
// 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.)
|
||||
|
||||
$form.prop('novalidate', true);
|
||||
var $submit = $form.find('.quickedit-form-submit');
|
||||
self.formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(formOptions, $submit);
|
||||
|
@ -237,68 +113,33 @@
|
|||
$backstage.remove();
|
||||
}
|
||||
|
||||
// Successfully saved.
|
||||
self.formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
|
||||
removeHiddenForm();
|
||||
// First, transition the state to 'saved'.
|
||||
|
||||
fieldModel.set('state', 'saved');
|
||||
// Second, set the 'htmlForOtherViewModes' attribute, so that when
|
||||
// this field is rerendered, the change can be propagated to other
|
||||
// instances of this field, which may be displayed in different view
|
||||
// modes.
|
||||
|
||||
fieldModel.set('htmlForOtherViewModes', response.other_view_modes);
|
||||
// Finally, set the 'html' attribute on the field model. This will
|
||||
// cause the field to be rerendered.
|
||||
|
||||
fieldModel.set('html', response.data);
|
||||
};
|
||||
|
||||
// Unsuccessfully saved; validation errors.
|
||||
self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function (ajax, response, status) {
|
||||
removeHiddenForm();
|
||||
editorModel.set('validationErrors', response.data);
|
||||
fieldModel.set('state', 'invalid');
|
||||
};
|
||||
|
||||
// The quickeditFieldForm 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 the form-based in-place editor, but pointless for any
|
||||
// other: the form itself won't be visible at all anyway! So, we just
|
||||
// ignore it.
|
||||
self.formSaveAjax.commands.quickeditFieldForm = function () {};
|
||||
|
||||
fillAndSubmitForm(editorModel.get('currentValue'));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows validation error messages.
|
||||
*
|
||||
* Should be called when the state is changed to 'invalid'.
|
||||
*/
|
||||
showValidationErrors: function () {
|
||||
var $errors = $('<div class="quickedit-validation-errors"></div>')
|
||||
.append(this.model.get('validationErrors'));
|
||||
this.getEditedElement()
|
||||
.addClass('quickedit-validation-error')
|
||||
.after($errors);
|
||||
showValidationErrors: function showValidationErrors() {
|
||||
var $errors = $('<div class="quickedit-validation-errors"></div>').append(this.model.get('validationErrors'));
|
||||
this.getEditedElement().addClass('quickedit-validation-error').after($errors);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleans up validation error messages.
|
||||
*
|
||||
* Should be called when the state is changed to 'candidate' or 'saving'. In
|
||||
* the case of the latter: the user has modified the value in the in-place
|
||||
* editor again to attempt to save again. In the case of the latter: the
|
||||
* invalid value was discarded.
|
||||
*/
|
||||
removeValidationErrors: function () {
|
||||
this.getEditedElement()
|
||||
.removeClass('quickedit-validation-error')
|
||||
.next('.quickedit-validation-errors')
|
||||
.remove();
|
||||
removeValidationErrors: function removeValidationErrors() {
|
||||
this.getEditedElement().removeClass('quickedit-validation-error').next('.quickedit-validation-errors').remove();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}(jQuery, Backbone, Drupal));
|
||||
})(jQuery, Backbone, Drupal);
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone view that decorates the in-place editable entity.
|
||||
*/
|
||||
|
||||
(function(Drupal, $, Backbone) {
|
||||
Drupal.quickedit.EntityDecorationView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.EntityDecorationView# */ {
|
||||
/**
|
||||
* Associated with the DOM root node of an editable entity.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize() {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
this.$el.toggleClass(
|
||||
'quickedit-entity-active',
|
||||
this.model.get('isActive'),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove() {
|
||||
this.setElement(null);
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
},
|
||||
);
|
||||
})(Drupal, jQuery, Backbone);
|
|
@ -1,40 +1,21 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone view that decorates the in-place editable entity.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal, $, Backbone) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.EntityDecorationView = Backbone.View.extend(/** @lends Drupal.quickedit.EntityDecorationView# */{
|
||||
|
||||
/**
|
||||
* Associated with the DOM root node of an editable entity.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*/
|
||||
initialize: function () {
|
||||
Drupal.quickedit.EntityDecorationView = Backbone.View.extend({
|
||||
initialize: function initialize() {
|
||||
this.listenTo(this.model, 'change', this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
render: function () {
|
||||
render: function render() {
|
||||
this.$el.toggleClass('quickedit-entity-active', this.model.get('isActive'));
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove: function () {
|
||||
remove: function remove() {
|
||||
this.setElement(null);
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}(Drupal, jQuery, Backbone));
|
||||
})(Drupal, jQuery, Backbone);
|
582
web/core/modules/quickedit/js/views/EntityToolbarView.es6.js
Normal file
582
web/core/modules/quickedit/js/views/EntityToolbarView.es6.js
Normal file
|
@ -0,0 +1,582 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides an entity level toolbar.
|
||||
*/
|
||||
|
||||
(function($, _, Backbone, Drupal, debounce) {
|
||||
Drupal.quickedit.EntityToolbarView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.EntityToolbarView# */ {
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
_fieldToolbarRoot: null,
|
||||
|
||||
/**
|
||||
* @return {object}
|
||||
* A map of events.
|
||||
*/
|
||||
events() {
|
||||
const map = {
|
||||
'click button.action-save': 'onClickSave',
|
||||
'click button.action-cancel': 'onClickCancel',
|
||||
mouseenter: 'onMouseenter',
|
||||
};
|
||||
return map;
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* Options to construct the view.
|
||||
* @param {Drupal.quickedit.AppModel} options.appModel
|
||||
* A quickedit `AppModel` to use in the view.
|
||||
*/
|
||||
initialize(options) {
|
||||
const that = this;
|
||||
this.appModel = options.appModel;
|
||||
this.$entity = $(this.model.get('el'));
|
||||
|
||||
// Rerender whenever the entity state changes.
|
||||
this.listenTo(
|
||||
this.model,
|
||||
'change:isActive change:isDirty change:state',
|
||||
this.render,
|
||||
);
|
||||
// Also rerender whenever a different field is highlighted or activated.
|
||||
this.listenTo(
|
||||
this.appModel,
|
||||
'change:highlightedField change:activeField',
|
||||
this.render,
|
||||
);
|
||||
// Rerender when a field of the entity changes state.
|
||||
this.listenTo(
|
||||
this.model.get('fields'),
|
||||
'change:state',
|
||||
this.fieldStateChange,
|
||||
);
|
||||
|
||||
// Reposition the entity toolbar as the viewport and the position within
|
||||
// the viewport changes.
|
||||
$(window).on(
|
||||
'resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit',
|
||||
debounce($.proxy(this.windowChangeHandler, this), 150),
|
||||
);
|
||||
|
||||
// Adjust the fence placement within which the entity toolbar may be
|
||||
// positioned.
|
||||
$(document).on(
|
||||
'drupalViewportOffsetChange.quickedit',
|
||||
(event, offsets) => {
|
||||
if (that.$fence) {
|
||||
that.$fence.css(offsets);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Set the entity toolbar DOM element as the el for this view.
|
||||
const $toolbar = this.buildToolbarEl();
|
||||
this.setElement($toolbar);
|
||||
this._fieldToolbarRoot = $toolbar
|
||||
.find('.quickedit-toolbar-field')
|
||||
.get(0);
|
||||
|
||||
// Initial render.
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.quickedit.EntityToolbarView}
|
||||
* The entity toolbar view.
|
||||
*/
|
||||
render() {
|
||||
if (this.model.get('isActive')) {
|
||||
// If the toolbar container doesn't exist, create it.
|
||||
const $body = $('body');
|
||||
if ($body.children('#quickedit-entity-toolbar').length === 0) {
|
||||
$body.append(this.$el);
|
||||
}
|
||||
// The fence will define a area on the screen that the entity toolbar
|
||||
// will be position within.
|
||||
if ($body.children('#quickedit-toolbar-fence').length === 0) {
|
||||
this.$fence = $(Drupal.theme('quickeditEntityToolbarFence'))
|
||||
.css(Drupal.displace())
|
||||
.appendTo($body);
|
||||
}
|
||||
// Adds the entity title to the toolbar.
|
||||
this.label();
|
||||
|
||||
// Show the save and cancel buttons.
|
||||
this.show('ops');
|
||||
// If render is being called and the toolbar is already visible, just
|
||||
// reposition it.
|
||||
this.position();
|
||||
}
|
||||
|
||||
// The save button text and state varies with the state of the entity
|
||||
// model.
|
||||
const $button = this.$el.find('.quickedit-button.action-save');
|
||||
const isDirty = this.model.get('isDirty');
|
||||
// Adjust the save button according to the state of the model.
|
||||
switch (this.model.get('state')) {
|
||||
// Quick editing is active, but no field is being edited.
|
||||
case 'opened':
|
||||
// The saving throbber is not managed by AJAX system. The
|
||||
// EntityToolbarView manages this visual element.
|
||||
$button
|
||||
.removeClass('action-saving icon-throbber icon-end')
|
||||
.text(Drupal.t('Save'))
|
||||
.removeAttr('disabled')
|
||||
.attr('aria-hidden', !isDirty);
|
||||
break;
|
||||
|
||||
// The changes to the fields of the entity are being committed.
|
||||
case 'committing':
|
||||
$button
|
||||
.addClass('action-saving icon-throbber icon-end')
|
||||
.text(Drupal.t('Saving'))
|
||||
.attr('disabled', 'disabled');
|
||||
break;
|
||||
|
||||
default:
|
||||
$button.attr('aria-hidden', true);
|
||||
break;
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove() {
|
||||
// Remove additional DOM elements controlled by this View.
|
||||
this.$fence.remove();
|
||||
|
||||
// Stop listening to additional events.
|
||||
$(window).off(
|
||||
'resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit',
|
||||
);
|
||||
$(document).off('drupalViewportOffsetChange.quickedit');
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Repositions the entity toolbar on window scroll and resize.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The scroll or resize event.
|
||||
*/
|
||||
windowChangeHandler(event) {
|
||||
this.position();
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* The `FieldModel` model.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
fieldStateChange(model, state) {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
this.render();
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
this.render();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uses the jQuery.ui.position() method to position the entity toolbar.
|
||||
*
|
||||
* @param {HTMLElement} [element]
|
||||
* The element against which the entity toolbar is positioned.
|
||||
*/
|
||||
position(element) {
|
||||
clearTimeout(this.timer);
|
||||
|
||||
const that = this;
|
||||
// Vary the edge of the positioning according to the direction of language
|
||||
// in the document.
|
||||
const edge = document.documentElement.dir === 'rtl' ? 'right' : 'left';
|
||||
// A time unit to wait until the entity toolbar is repositioned.
|
||||
let delay = 0;
|
||||
// Determines what check in the series of checks below should be
|
||||
// evaluated.
|
||||
let check = 0;
|
||||
// When positioned against an active field that has padding, we should
|
||||
// ignore that padding when positioning the toolbar, to not unnecessarily
|
||||
// move the toolbar horizontally, which feels annoying.
|
||||
let horizontalPadding = 0;
|
||||
let of;
|
||||
let activeField;
|
||||
let highlightedField;
|
||||
// There are several elements in the page that the entity toolbar might be
|
||||
// positioned against. They are considered below in a priority order.
|
||||
do {
|
||||
switch (check) {
|
||||
case 0:
|
||||
// Position against a specific element.
|
||||
of = element;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Position against a form container.
|
||||
activeField = Drupal.quickedit.app.model.get('activeField');
|
||||
of =
|
||||
activeField &&
|
||||
activeField.editorView &&
|
||||
activeField.editorView.$formContainer &&
|
||||
activeField.editorView.$formContainer.find('.quickedit-form');
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Position against an active field.
|
||||
of =
|
||||
activeField &&
|
||||
activeField.editorView &&
|
||||
activeField.editorView.getEditedElement();
|
||||
if (
|
||||
activeField &&
|
||||
activeField.editorView &&
|
||||
activeField.editorView.getQuickEditUISettings().padding
|
||||
) {
|
||||
horizontalPadding = 5;
|
||||
}
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Position against a highlighted field.
|
||||
highlightedField = Drupal.quickedit.app.model.get(
|
||||
'highlightedField',
|
||||
);
|
||||
of =
|
||||
highlightedField &&
|
||||
highlightedField.editorView &&
|
||||
highlightedField.editorView.getEditedElement();
|
||||
delay = 250;
|
||||
break;
|
||||
|
||||
default: {
|
||||
const fieldModels = this.model.get('fields').models;
|
||||
let topMostPosition = 1000000;
|
||||
let topMostField = null;
|
||||
// Position against the topmost field.
|
||||
for (let i = 0; i < fieldModels.length; i++) {
|
||||
const pos = fieldModels[i].get('el').getBoundingClientRect()
|
||||
.top;
|
||||
if (pos < topMostPosition) {
|
||||
topMostPosition = pos;
|
||||
topMostField = fieldModels[i];
|
||||
}
|
||||
}
|
||||
of = topMostField.get('el');
|
||||
delay = 50;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Prepare to check the next possible element to position against.
|
||||
check++;
|
||||
} while (!of);
|
||||
|
||||
/**
|
||||
* Refines the positioning algorithm of jquery.ui.position().
|
||||
*
|
||||
* Invoked as the 'using' callback of jquery.ui.position() in
|
||||
* positionToolbar().
|
||||
*
|
||||
* @param {*} view
|
||||
* The view the positions will be calculated from.
|
||||
* @param {object} suggested
|
||||
* A hash of top and left values for the position that should be set. It
|
||||
* can be forwarded to .css() or .animate().
|
||||
* @param {object} info
|
||||
* The position and dimensions of both the 'my' element and the 'of'
|
||||
* elements, as well as calculations to their relative position. This
|
||||
* object contains the following properties:
|
||||
* @param {object} info.element
|
||||
* A hash that contains information about the HTML element that will be
|
||||
* positioned. Also known as the 'my' element.
|
||||
* @param {object} info.target
|
||||
* A hash that contains information about the HTML element that the
|
||||
* 'my' element will be positioned against. Also known as the 'of'
|
||||
* element.
|
||||
*/
|
||||
function refinePosition(view, suggested, info) {
|
||||
// Determine if the pointer should be on the top or bottom.
|
||||
const isBelow = suggested.top > info.target.top;
|
||||
info.element.element.toggleClass(
|
||||
'quickedit-toolbar-pointer-top',
|
||||
isBelow,
|
||||
);
|
||||
// Don't position the toolbar past the first or last editable field if
|
||||
// the entity is the target.
|
||||
if (view.$entity[0] === info.target.element[0]) {
|
||||
// Get the first or last field according to whether the toolbar is
|
||||
// above or below the entity.
|
||||
const $field = view.$entity
|
||||
.find('.quickedit-editable')
|
||||
.eq(isBelow ? -1 : 0);
|
||||
if ($field.length > 0) {
|
||||
suggested.top = isBelow
|
||||
? $field.offset().top + $field.outerHeight(true)
|
||||
: $field.offset().top - info.element.element.outerHeight(true);
|
||||
}
|
||||
}
|
||||
// Don't let the toolbar go outside the fence.
|
||||
const fenceTop = view.$fence.offset().top;
|
||||
const fenceHeight = view.$fence.height();
|
||||
const toolbarHeight = info.element.element.outerHeight(true);
|
||||
if (suggested.top < fenceTop) {
|
||||
suggested.top = fenceTop;
|
||||
} else if (suggested.top + toolbarHeight > fenceTop + fenceHeight) {
|
||||
suggested.top = fenceTop + fenceHeight - toolbarHeight;
|
||||
}
|
||||
// Position the toolbar.
|
||||
info.element.element.css({
|
||||
left: Math.floor(suggested.left),
|
||||
top: Math.floor(suggested.top),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the jquery.ui.position() method on the $el of this view.
|
||||
*/
|
||||
function positionToolbar() {
|
||||
that.$el
|
||||
.position({
|
||||
my: `${edge} bottom`,
|
||||
// Move the toolbar 1px towards the start edge of the 'of' element,
|
||||
// plus any horizontal padding that may have been added to the
|
||||
// element that is being added, to prevent unwanted horizontal
|
||||
// movement.
|
||||
at: `${edge}+${1 + horizontalPadding} top`,
|
||||
of,
|
||||
collision: 'flipfit',
|
||||
using: refinePosition.bind(null, that),
|
||||
within: that.$fence,
|
||||
})
|
||||
// Resize the toolbar to match the dimensions of the field, up to a
|
||||
// maximum width that is equal to 90% of the field's width.
|
||||
.css({
|
||||
'max-width':
|
||||
document.documentElement.clientWidth < 450
|
||||
? document.documentElement.clientWidth
|
||||
: 450,
|
||||
// Set a minimum width of 240px for the entity toolbar, or the width
|
||||
// of the client if it is less than 240px, so that the toolbar
|
||||
// never folds up into a squashed and jumbled mess.
|
||||
'min-width':
|
||||
document.documentElement.clientWidth < 240
|
||||
? document.documentElement.clientWidth
|
||||
: 240,
|
||||
width: '100%',
|
||||
});
|
||||
}
|
||||
|
||||
// Uses the jQuery.ui.position() method. Use a timeout to move the toolbar
|
||||
// only after the user has focused on an editable for 250ms. This prevents
|
||||
// the toolbar from jumping around the screen.
|
||||
this.timer = setTimeout(() => {
|
||||
// Render the position in the next execution cycle, so that animations
|
||||
// on the field have time to process. This is not strictly speaking, a
|
||||
// guarantee that all animations will be finished, but it's a simple
|
||||
// way to get better positioning without too much additional code.
|
||||
_.defer(positionToolbar);
|
||||
}, delay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the model state to 'saving' when the save button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClickSave(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// Save the model.
|
||||
this.model.set('state', 'committing');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the model state to candidate when the cancel button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClickCancel(event) {
|
||||
event.preventDefault();
|
||||
this.model.set('state', 'deactivating');
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the timeout that will eventually reposition the entity toolbar.
|
||||
*
|
||||
* Without this, it may reposition itself, away from the user's cursor!
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The mouse event.
|
||||
*/
|
||||
onMouseenter(event) {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the entity toolbar HTML; attaches to DOM; sets starting position.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolbar element.
|
||||
*/
|
||||
buildToolbarEl() {
|
||||
const $toolbar = $(
|
||||
Drupal.theme('quickeditEntityToolbar', {
|
||||
id: 'quickedit-entity-toolbar',
|
||||
}),
|
||||
);
|
||||
|
||||
$toolbar
|
||||
.find('.quickedit-toolbar-entity')
|
||||
// Append the "ops" toolgroup into the toolbar.
|
||||
.prepend(
|
||||
Drupal.theme('quickeditToolgroup', {
|
||||
classes: ['ops'],
|
||||
buttons: [
|
||||
{
|
||||
label: Drupal.t('Save'),
|
||||
type: 'submit',
|
||||
classes: 'action-save quickedit-button icon',
|
||||
attributes: {
|
||||
'aria-hidden': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
label: Drupal.t('Close'),
|
||||
classes:
|
||||
'action-cancel quickedit-button icon icon-close icon-only',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
// Give the toolbar a sensible starting position so that it doesn't
|
||||
// animate on to the screen from a far off corner.
|
||||
$toolbar.css({
|
||||
left: this.$entity.offset().left,
|
||||
top: this.$entity.offset().top,
|
||||
});
|
||||
|
||||
return $toolbar;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the DOM element that fields will attach their toolbars to.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The DOM element that fields will attach their toolbars to.
|
||||
*/
|
||||
getToolbarRoot() {
|
||||
return this._fieldToolbarRoot;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a state-dependent label for the entity toolbar.
|
||||
*/
|
||||
label() {
|
||||
// The entity label.
|
||||
let label = '';
|
||||
const entityLabel = this.model.get('label');
|
||||
|
||||
// Label of an active field, if it exists.
|
||||
const activeField = Drupal.quickedit.app.model.get('activeField');
|
||||
const activeFieldLabel =
|
||||
activeField && activeField.get('metadata').label;
|
||||
// Label of a highlighted field, if it exists.
|
||||
const highlightedField = Drupal.quickedit.app.model.get(
|
||||
'highlightedField',
|
||||
);
|
||||
const highlightedFieldLabel =
|
||||
highlightedField && highlightedField.get('metadata').label;
|
||||
// The label is constructed in a priority order.
|
||||
if (activeFieldLabel) {
|
||||
label = Drupal.theme('quickeditEntityToolbarLabel', {
|
||||
entityLabel,
|
||||
fieldLabel: activeFieldLabel,
|
||||
});
|
||||
} else if (highlightedFieldLabel) {
|
||||
label = Drupal.theme('quickeditEntityToolbarLabel', {
|
||||
entityLabel,
|
||||
fieldLabel: highlightedFieldLabel,
|
||||
});
|
||||
} else {
|
||||
// @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437
|
||||
label = Drupal.checkPlain(entityLabel);
|
||||
}
|
||||
|
||||
this.$el.find('.quickedit-toolbar-label').html(label);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes to a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
* @param {string} classes
|
||||
* A string of space-delimited class names that will be applied to the
|
||||
* wrapping element of the toolbar group.
|
||||
*/
|
||||
addClass(toolgroup, classes) {
|
||||
this._find(toolgroup).addClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes from a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
* @param {string} classes
|
||||
* A string of space-delimited class names that will be removed from the
|
||||
* wrapping element of the toolbar group.
|
||||
*/
|
||||
removeClass(toolgroup, classes) {
|
||||
this._find(toolgroup).removeClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolgroup DOM element.
|
||||
*/
|
||||
_find(toolgroup) {
|
||||
return this.$el.find(
|
||||
`.quickedit-toolbar .quickedit-toolgroup.${toolgroup}`,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
show(toolgroup) {
|
||||
this.$el.removeClass('quickedit-animate-invisible');
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, _, Backbone, Drupal, Drupal.debounce);
|
|
@ -1,128 +1,75 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides an entity level toolbar.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, _, Backbone, Drupal, debounce) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.EntityToolbarView = Backbone.View.extend(/** @lends Drupal.quickedit.EntityToolbarView# */{
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
Drupal.quickedit.EntityToolbarView = Backbone.View.extend({
|
||||
_fieldToolbarRoot: null,
|
||||
|
||||
/**
|
||||
* @return {object}
|
||||
* A map of events.
|
||||
*/
|
||||
events: function () {
|
||||
events: function events() {
|
||||
var map = {
|
||||
'click button.action-save': 'onClickSave',
|
||||
'click button.action-cancel': 'onClickCancel',
|
||||
'mouseenter': 'onMouseenter'
|
||||
mouseenter: 'onMouseenter'
|
||||
};
|
||||
return map;
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* Options to construct the view.
|
||||
* @param {Drupal.quickedit.AppModel} options.appModel
|
||||
* A quickedit `AppModel` to use in the view.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
initialize: function initialize(options) {
|
||||
var that = this;
|
||||
this.appModel = options.appModel;
|
||||
this.$entity = $(this.model.get('el'));
|
||||
|
||||
// Rerender whenever the entity state changes.
|
||||
this.listenTo(this.model, 'change:isActive change:isDirty change:state', this.render);
|
||||
// Also rerender whenever a different field is highlighted or activated.
|
||||
|
||||
this.listenTo(this.appModel, 'change:highlightedField change:activeField', this.render);
|
||||
// Rerender when a field of the entity changes state.
|
||||
|
||||
this.listenTo(this.model.get('fields'), 'change:state', this.fieldStateChange);
|
||||
|
||||
// Reposition the entity toolbar as the viewport and the position within
|
||||
// the viewport changes.
|
||||
$(window).on('resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit', debounce($.proxy(this.windowChangeHandler, this), 150));
|
||||
|
||||
// Adjust the fence placement within which the entity toolbar may be
|
||||
// positioned.
|
||||
$(document).on('drupalViewportOffsetChange.quickedit', function (event, offsets) {
|
||||
if (that.$fence) {
|
||||
that.$fence.css(offsets);
|
||||
}
|
||||
});
|
||||
|
||||
// Set the entity toolbar DOM element as the el for this view.
|
||||
var $toolbar = this.buildToolbarEl();
|
||||
this.setElement($toolbar);
|
||||
this._fieldToolbarRoot = $toolbar.find('.quickedit-toolbar-field').get(0);
|
||||
|
||||
// Initial render.
|
||||
this.render();
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.quickedit.EntityToolbarView}
|
||||
* The entity toolbar view.
|
||||
*/
|
||||
render: function () {
|
||||
render: function render() {
|
||||
if (this.model.get('isActive')) {
|
||||
// If the toolbar container doesn't exist, create it.
|
||||
var $body = $('body');
|
||||
if ($body.children('#quickedit-entity-toolbar').length === 0) {
|
||||
$body.append(this.$el);
|
||||
}
|
||||
// The fence will define a area on the screen that the entity toolbar
|
||||
// will be position within.
|
||||
|
||||
if ($body.children('#quickedit-toolbar-fence').length === 0) {
|
||||
this.$fence = $(Drupal.theme('quickeditEntityToolbarFence'))
|
||||
.css(Drupal.displace())
|
||||
.appendTo($body);
|
||||
this.$fence = $(Drupal.theme('quickeditEntityToolbarFence')).css(Drupal.displace()).appendTo($body);
|
||||
}
|
||||
// Adds the entity title to the toolbar.
|
||||
|
||||
this.label();
|
||||
|
||||
// Show the save and cancel buttons.
|
||||
this.show('ops');
|
||||
// If render is being called and the toolbar is already visible, just
|
||||
// reposition it.
|
||||
|
||||
this.position();
|
||||
}
|
||||
|
||||
// The save button text and state varies with the state of the entity
|
||||
// model.
|
||||
var $button = this.$el.find('.quickedit-button.action-save');
|
||||
var isDirty = this.model.get('isDirty');
|
||||
// Adjust the save button according to the state of the model.
|
||||
|
||||
switch (this.model.get('state')) {
|
||||
// Quick editing is active, but no field is being edited.
|
||||
case 'opened':
|
||||
// The saving throbber is not managed by AJAX system. The
|
||||
// EntityToolbarView manages this visual element.
|
||||
$button
|
||||
.removeClass('action-saving icon-throbber icon-end')
|
||||
.text(Drupal.t('Save'))
|
||||
.removeAttr('disabled')
|
||||
.attr('aria-hidden', !isDirty);
|
||||
$button.removeClass('action-saving icon-throbber icon-end').text(Drupal.t('Save')).removeAttr('disabled').attr('aria-hidden', !isDirty);
|
||||
break;
|
||||
|
||||
// The changes to the fields of the entity are being committed.
|
||||
case 'committing':
|
||||
$button
|
||||
.addClass('action-saving icon-throbber icon-end')
|
||||
.text(Drupal.t('Saving'))
|
||||
.attr('disabled', 'disabled');
|
||||
$button.addClass('action-saving icon-throbber icon-end').text(Drupal.t('Saving')).attr('disabled', 'disabled');
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -132,41 +79,18 @@
|
|||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove: function () {
|
||||
// Remove additional DOM elements controlled by this View.
|
||||
remove: function remove() {
|
||||
this.$fence.remove();
|
||||
|
||||
// Stop listening to additional events.
|
||||
$(window).off('resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit');
|
||||
$(document).off('drupalViewportOffsetChange.quickedit');
|
||||
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Repositions the entity toolbar on window scroll and resize.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The scroll or resize event.
|
||||
*/
|
||||
windowChangeHandler: function (event) {
|
||||
windowChangeHandler: function windowChangeHandler(event) {
|
||||
this.position();
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* The `FieldModel` model.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
fieldStateChange: function (model, state) {
|
||||
fieldStateChange: function fieldStateChange(model, state) {
|
||||
switch (state) {
|
||||
case 'active':
|
||||
this.render();
|
||||
|
@ -177,49 +101,34 @@
|
|||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uses the jQuery.ui.position() method to position the entity toolbar.
|
||||
*
|
||||
* @param {HTMLElement} [element]
|
||||
* The element against which the entity toolbar is positioned.
|
||||
*/
|
||||
position: function (element) {
|
||||
position: function position(element) {
|
||||
clearTimeout(this.timer);
|
||||
|
||||
var that = this;
|
||||
// Vary the edge of the positioning according to the direction of language
|
||||
// in the document.
|
||||
var edge = (document.documentElement.dir === 'rtl') ? 'right' : 'left';
|
||||
// A time unit to wait until the entity toolbar is repositioned.
|
||||
|
||||
var edge = document.documentElement.dir === 'rtl' ? 'right' : 'left';
|
||||
|
||||
var delay = 0;
|
||||
// Determines what check in the series of checks below should be
|
||||
// evaluated.
|
||||
|
||||
var check = 0;
|
||||
// When positioned against an active field that has padding, we should
|
||||
// ignore that padding when positioning the toolbar, to not unnecessarily
|
||||
// move the toolbar horizontally, which feels annoying.
|
||||
|
||||
var horizontalPadding = 0;
|
||||
var of;
|
||||
var activeField;
|
||||
var highlightedField;
|
||||
// There are several elements in the page that the entity toolbar might be
|
||||
// positioned against. They are considered below in a priority order.
|
||||
var of = void 0;
|
||||
var activeField = void 0;
|
||||
var highlightedField = void 0;
|
||||
|
||||
do {
|
||||
switch (check) {
|
||||
case 0:
|
||||
// Position against a specific element.
|
||||
of = element;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
// Position against a form container.
|
||||
activeField = Drupal.quickedit.app.model.get('activeField');
|
||||
of = activeField && activeField.editorView && activeField.editorView.$formContainer && activeField.editorView.$formContainer.find('.quickedit-form');
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Position against an active field.
|
||||
of = activeField && activeField.editorView && activeField.editorView.getEditedElement();
|
||||
if (activeField && activeField.editorView && activeField.editorView.getQuickEditUISettings().padding) {
|
||||
horizontalPadding = 5;
|
||||
|
@ -227,302 +136,160 @@
|
|||
break;
|
||||
|
||||
case 3:
|
||||
// Position against a highlighted field.
|
||||
highlightedField = Drupal.quickedit.app.model.get('highlightedField');
|
||||
of = highlightedField && highlightedField.editorView && highlightedField.editorView.getEditedElement();
|
||||
delay = 250;
|
||||
break;
|
||||
|
||||
default:
|
||||
var fieldModels = this.model.get('fields').models;
|
||||
var topMostPosition = 1000000;
|
||||
var topMostField = null;
|
||||
// Position against the topmost field.
|
||||
for (var i = 0; i < fieldModels.length; i++) {
|
||||
var pos = fieldModels[i].get('el').getBoundingClientRect().top;
|
||||
if (pos < topMostPosition) {
|
||||
topMostPosition = pos;
|
||||
topMostField = fieldModels[i];
|
||||
{
|
||||
var fieldModels = this.model.get('fields').models;
|
||||
var topMostPosition = 1000000;
|
||||
var topMostField = null;
|
||||
|
||||
for (var i = 0; i < fieldModels.length; i++) {
|
||||
var pos = fieldModels[i].get('el').getBoundingClientRect().top;
|
||||
if (pos < topMostPosition) {
|
||||
topMostPosition = pos;
|
||||
topMostField = fieldModels[i];
|
||||
}
|
||||
}
|
||||
of = topMostField.get('el');
|
||||
delay = 50;
|
||||
break;
|
||||
}
|
||||
of = topMostField.get('el');
|
||||
delay = 50;
|
||||
break;
|
||||
}
|
||||
// Prepare to check the next possible element to position against.
|
||||
|
||||
check++;
|
||||
} while (!of);
|
||||
|
||||
/**
|
||||
* Refines the positioning algorithm of jquery.ui.position().
|
||||
*
|
||||
* Invoked as the 'using' callback of jquery.ui.position() in
|
||||
* positionToolbar().
|
||||
*
|
||||
* @param {*} view
|
||||
* The view the positions will be calculated from.
|
||||
* @param {object} suggested
|
||||
* A hash of top and left values for the position that should be set. It
|
||||
* can be forwarded to .css() or .animate().
|
||||
* @param {object} info
|
||||
* The position and dimensions of both the 'my' element and the 'of'
|
||||
* elements, as well as calculations to their relative position. This
|
||||
* object contains the following properties:
|
||||
* @param {object} info.element
|
||||
* A hash that contains information about the HTML element that will be
|
||||
* positioned. Also known as the 'my' element.
|
||||
* @param {object} info.target
|
||||
* A hash that contains information about the HTML element that the
|
||||
* 'my' element will be positioned against. Also known as the 'of'
|
||||
* element.
|
||||
*/
|
||||
function refinePosition(view, suggested, info) {
|
||||
// Determine if the pointer should be on the top or bottom.
|
||||
var isBelow = suggested.top > info.target.top;
|
||||
info.element.element.toggleClass('quickedit-toolbar-pointer-top', isBelow);
|
||||
// Don't position the toolbar past the first or last editable field if
|
||||
// the entity is the target.
|
||||
|
||||
if (view.$entity[0] === info.target.element[0]) {
|
||||
// Get the first or last field according to whether the toolbar is
|
||||
// above or below the entity.
|
||||
var $field = view.$entity.find('.quickedit-editable').eq((isBelow) ? -1 : 0);
|
||||
var $field = view.$entity.find('.quickedit-editable').eq(isBelow ? -1 : 0);
|
||||
if ($field.length > 0) {
|
||||
suggested.top = (isBelow) ? ($field.offset().top + $field.outerHeight(true)) : $field.offset().top - info.element.element.outerHeight(true);
|
||||
suggested.top = isBelow ? $field.offset().top + $field.outerHeight(true) : $field.offset().top - info.element.element.outerHeight(true);
|
||||
}
|
||||
}
|
||||
// Don't let the toolbar go outside the fence.
|
||||
|
||||
var fenceTop = view.$fence.offset().top;
|
||||
var fenceHeight = view.$fence.height();
|
||||
var toolbarHeight = info.element.element.outerHeight(true);
|
||||
if (suggested.top < fenceTop) {
|
||||
suggested.top = fenceTop;
|
||||
}
|
||||
else if ((suggested.top + toolbarHeight) > (fenceTop + fenceHeight)) {
|
||||
} else if (suggested.top + toolbarHeight > fenceTop + fenceHeight) {
|
||||
suggested.top = fenceTop + fenceHeight - toolbarHeight;
|
||||
}
|
||||
// Position the toolbar.
|
||||
|
||||
info.element.element.css({
|
||||
left: Math.floor(suggested.left),
|
||||
top: Math.floor(suggested.top)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the jquery.ui.position() method on the $el of this view.
|
||||
*/
|
||||
function positionToolbar() {
|
||||
that.$el
|
||||
.position({
|
||||
my: edge + ' bottom',
|
||||
// Move the toolbar 1px towards the start edge of the 'of' element,
|
||||
// plus any horizontal padding that may have been added to the
|
||||
// element that is being added, to prevent unwanted horizontal
|
||||
// movement.
|
||||
at: edge + '+' + (1 + horizontalPadding) + ' top',
|
||||
of: of,
|
||||
collision: 'flipfit',
|
||||
using: refinePosition.bind(null, that),
|
||||
within: that.$fence
|
||||
})
|
||||
// Resize the toolbar to match the dimensions of the field, up to a
|
||||
// maximum width that is equal to 90% of the field's width.
|
||||
.css({
|
||||
'max-width': (document.documentElement.clientWidth < 450) ? document.documentElement.clientWidth : 450,
|
||||
// Set a minimum width of 240px for the entity toolbar, or the width
|
||||
// of the client if it is less than 240px, so that the toolbar
|
||||
// never folds up into a squashed and jumbled mess.
|
||||
'min-width': (document.documentElement.clientWidth < 240) ? document.documentElement.clientWidth : 240,
|
||||
'width': '100%'
|
||||
});
|
||||
that.$el.position({
|
||||
my: edge + ' bottom',
|
||||
|
||||
at: edge + '+' + (1 + horizontalPadding) + ' top',
|
||||
of: of,
|
||||
collision: 'flipfit',
|
||||
using: refinePosition.bind(null, that),
|
||||
within: that.$fence
|
||||
}).css({
|
||||
'max-width': document.documentElement.clientWidth < 450 ? document.documentElement.clientWidth : 450,
|
||||
|
||||
'min-width': document.documentElement.clientWidth < 240 ? document.documentElement.clientWidth : 240,
|
||||
width: '100%'
|
||||
});
|
||||
}
|
||||
|
||||
// Uses the jQuery.ui.position() method. Use a timeout to move the toolbar
|
||||
// only after the user has focused on an editable for 250ms. This prevents
|
||||
// the toolbar from jumping around the screen.
|
||||
this.timer = setTimeout(function () {
|
||||
// Render the position in the next execution cycle, so that animations
|
||||
// on the field have time to process. This is not strictly speaking, a
|
||||
// guarantee that all animations will be finished, but it's a simple
|
||||
// way to get better positioning without too much additional code.
|
||||
_.defer(positionToolbar);
|
||||
}, delay);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the model state to 'saving' when the save button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClickSave: function (event) {
|
||||
onClickSave: function onClickSave(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
// Save the model.
|
||||
|
||||
this.model.set('state', 'committing');
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the model state to candidate when the cancel button is clicked.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClickCancel: function (event) {
|
||||
onClickCancel: function onClickCancel(event) {
|
||||
event.preventDefault();
|
||||
this.model.set('state', 'deactivating');
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the timeout that will eventually reposition the entity toolbar.
|
||||
*
|
||||
* Without this, it may reposition itself, away from the user's cursor!
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The mouse event.
|
||||
*/
|
||||
onMouseenter: function (event) {
|
||||
onMouseenter: function onMouseenter(event) {
|
||||
clearTimeout(this.timer);
|
||||
},
|
||||
|
||||
/**
|
||||
* Builds the entity toolbar HTML; attaches to DOM; sets starting position.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolbar element.
|
||||
*/
|
||||
buildToolbarEl: function () {
|
||||
buildToolbarEl: function buildToolbarEl() {
|
||||
var $toolbar = $(Drupal.theme('quickeditEntityToolbar', {
|
||||
id: 'quickedit-entity-toolbar'
|
||||
}));
|
||||
|
||||
$toolbar
|
||||
.find('.quickedit-toolbar-entity')
|
||||
// Append the "ops" toolgroup into the toolbar.
|
||||
.prepend(Drupal.theme('quickeditToolgroup', {
|
||||
classes: ['ops'],
|
||||
buttons: [
|
||||
{
|
||||
label: Drupal.t('Save'),
|
||||
type: 'submit',
|
||||
classes: 'action-save quickedit-button icon',
|
||||
attributes: {
|
||||
'aria-hidden': true
|
||||
}
|
||||
},
|
||||
{
|
||||
label: Drupal.t('Close'),
|
||||
classes: 'action-cancel quickedit-button icon icon-close icon-only'
|
||||
}
|
||||
]
|
||||
}));
|
||||
$toolbar.find('.quickedit-toolbar-entity').prepend(Drupal.theme('quickeditToolgroup', {
|
||||
classes: ['ops'],
|
||||
buttons: [{
|
||||
label: Drupal.t('Save'),
|
||||
type: 'submit',
|
||||
classes: 'action-save quickedit-button icon',
|
||||
attributes: {
|
||||
'aria-hidden': true
|
||||
}
|
||||
}, {
|
||||
label: Drupal.t('Close'),
|
||||
classes: 'action-cancel quickedit-button icon icon-close icon-only'
|
||||
}]
|
||||
}));
|
||||
|
||||
// Give the toolbar a sensible starting position so that it doesn't
|
||||
// animate on to the screen from a far off corner.
|
||||
$toolbar
|
||||
.css({
|
||||
left: this.$entity.offset().left,
|
||||
top: this.$entity.offset().top
|
||||
});
|
||||
$toolbar.css({
|
||||
left: this.$entity.offset().left,
|
||||
top: this.$entity.offset().top
|
||||
});
|
||||
|
||||
return $toolbar;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the DOM element that fields will attach their toolbars to.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The DOM element that fields will attach their toolbars to.
|
||||
*/
|
||||
getToolbarRoot: function () {
|
||||
getToolbarRoot: function getToolbarRoot() {
|
||||
return this._fieldToolbarRoot;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates a state-dependent label for the entity toolbar.
|
||||
*/
|
||||
label: function () {
|
||||
// The entity label.
|
||||
label: function label() {
|
||||
var label = '';
|
||||
var entityLabel = this.model.get('label');
|
||||
|
||||
// Label of an active field, if it exists.
|
||||
var activeField = Drupal.quickedit.app.model.get('activeField');
|
||||
var activeFieldLabel = activeField && activeField.get('metadata').label;
|
||||
// Label of a highlighted field, if it exists.
|
||||
|
||||
var highlightedField = Drupal.quickedit.app.model.get('highlightedField');
|
||||
var highlightedFieldLabel = highlightedField && highlightedField.get('metadata').label;
|
||||
// The label is constructed in a priority order.
|
||||
|
||||
if (activeFieldLabel) {
|
||||
label = Drupal.theme('quickeditEntityToolbarLabel', {
|
||||
entityLabel: entityLabel,
|
||||
fieldLabel: activeFieldLabel
|
||||
});
|
||||
}
|
||||
else if (highlightedFieldLabel) {
|
||||
} else if (highlightedFieldLabel) {
|
||||
label = Drupal.theme('quickeditEntityToolbarLabel', {
|
||||
entityLabel: entityLabel,
|
||||
fieldLabel: highlightedFieldLabel
|
||||
});
|
||||
}
|
||||
else {
|
||||
// @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437
|
||||
} else {
|
||||
label = Drupal.checkPlain(entityLabel);
|
||||
}
|
||||
|
||||
this.$el
|
||||
.find('.quickedit-toolbar-label')
|
||||
.html(label);
|
||||
this.$el.find('.quickedit-toolbar-label').html(label);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes to a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
* @param {string} classes
|
||||
* A string of space-delimited class names that will be applied to the
|
||||
* wrapping element of the toolbar group.
|
||||
*/
|
||||
addClass: function (toolgroup, classes) {
|
||||
addClass: function addClass(toolgroup, classes) {
|
||||
this._find(toolgroup).addClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes from a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
* @param {string} classes
|
||||
* A string of space-delimited class names that will be removed from the
|
||||
* wrapping element of the toolbar group.
|
||||
*/
|
||||
removeClass: function (toolgroup, classes) {
|
||||
removeClass: function removeClass(toolgroup, classes) {
|
||||
this._find(toolgroup).removeClass(classes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolgroup DOM element.
|
||||
*/
|
||||
_find: function (toolgroup) {
|
||||
_find: function _find(toolgroup) {
|
||||
return this.$el.find('.quickedit-toolbar .quickedit-toolgroup.' + toolgroup);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
show: function (toolgroup) {
|
||||
show: function show(toolgroup) {
|
||||
this.$el.removeClass('quickedit-animate-invisible');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, _, Backbone, Drupal, Drupal.debounce);
|
||||
})(jQuery, _, Backbone, Drupal, Drupal.debounce);
|
368
web/core/modules/quickedit/js/views/FieldDecorationView.es6.js
Normal file
368
web/core/modules/quickedit/js/views/FieldDecorationView.es6.js
Normal file
|
@ -0,0 +1,368 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that decorates the in-place edited element.
|
||||
*/
|
||||
|
||||
(function($, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldDecorationView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.FieldDecorationView# */ {
|
||||
/**
|
||||
* @type {null}
|
||||
*/
|
||||
_widthAttributeIsEmpty: null,
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'mouseenter.quickedit': 'onMouseEnter',
|
||||
'mouseleave.quickedit': 'onMouseLeave',
|
||||
click: 'onClick',
|
||||
'tabIn.quickedit': 'onMouseEnter',
|
||||
'tabOut.quickedit': 'onMouseLeave',
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.EditorView} options.editorView
|
||||
* The editor object view.
|
||||
*/
|
||||
initialize(options) {
|
||||
this.editorView = options.editorView;
|
||||
|
||||
this.listenTo(this.model, 'change:state', this.stateChange);
|
||||
this.listenTo(
|
||||
this.model,
|
||||
'change:isChanged change:inTempStore',
|
||||
this.renderChanged,
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove() {
|
||||
// The el property is the field, which should not be removed. Remove the
|
||||
// pointer to it, then call Backbone.View.prototype.remove().
|
||||
this.setElement();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* The `FieldModel` model.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
stateChange(model, state) {
|
||||
const from = model.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
this.undecorate();
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
this.decorate();
|
||||
if (from !== 'inactive') {
|
||||
this.stopHighlight();
|
||||
if (from !== 'highlighted') {
|
||||
this.model.set('isChanged', false);
|
||||
this.stopEdit();
|
||||
}
|
||||
}
|
||||
this._unpad();
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
this.startHighlight();
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// NOTE: this state is not used by every editor! It's only used by
|
||||
// those that need to interact with the server.
|
||||
this.prepareEdit();
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
if (from !== 'activating') {
|
||||
this.prepareEdit();
|
||||
}
|
||||
if (this.editorView.getQuickEditUISettings().padding) {
|
||||
this._pad();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
this.model.set('isChanged', true);
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a class to the edited element that indicates whether the field has
|
||||
* been changed by the user (i.e. locally) or the field has already been
|
||||
* changed and stored before by the user (i.e. remotely, stored in
|
||||
* PrivateTempStore).
|
||||
*/
|
||||
renderChanged() {
|
||||
this.$el.toggleClass(
|
||||
'quickedit-changed',
|
||||
this.model.get('isChanged') || this.model.get('inTempStore'),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts hover; transitions to 'highlight' state.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The mouse event.
|
||||
*/
|
||||
onMouseEnter(event) {
|
||||
const that = this;
|
||||
that.model.set('state', 'highlighted');
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops hover; transitions to 'candidate' state.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The mouse event.
|
||||
*/
|
||||
onMouseLeave(event) {
|
||||
const that = this;
|
||||
that.model.set('state', 'candidate', { reason: 'mouseleave' });
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition to 'activating' stage.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClick(event) {
|
||||
this.model.set('state', 'activating');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes used to indicate an elements editable state.
|
||||
*/
|
||||
decorate() {
|
||||
this.$el.addClass('quickedit-candidate quickedit-editable');
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes used to indicate an elements editable state.
|
||||
*/
|
||||
undecorate() {
|
||||
this.$el.removeClass(
|
||||
'quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing',
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds that class that indicates that an element is highlighted.
|
||||
*/
|
||||
startHighlight() {
|
||||
// Animations.
|
||||
const that = this;
|
||||
// Use a timeout to grab the next available animation frame.
|
||||
that.$el.addClass('quickedit-highlighted');
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element is highlighted.
|
||||
*/
|
||||
stopHighlight() {
|
||||
this.$el.removeClass('quickedit-highlighted');
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element as editable.
|
||||
*/
|
||||
prepareEdit() {
|
||||
this.$el.addClass('quickedit-editing');
|
||||
|
||||
// Allow the field to be styled differently while editing in a pop-up
|
||||
// in-place editor.
|
||||
if (this.editorView.getQuickEditUISettings().popup) {
|
||||
this.$el.addClass('quickedit-editor-is-popup');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element is being edited.
|
||||
*
|
||||
* Reapplies the class that indicates that a candidate editable element is
|
||||
* again available to be edited.
|
||||
*/
|
||||
stopEdit() {
|
||||
this.$el.removeClass('quickedit-highlighted quickedit-editing');
|
||||
|
||||
// Done editing in a pop-up in-place editor; remove the class.
|
||||
if (this.editorView.getQuickEditUISettings().popup) {
|
||||
this.$el.removeClass('quickedit-editor-is-popup');
|
||||
}
|
||||
|
||||
// Make the other editors show up again.
|
||||
$('.quickedit-candidate').addClass('quickedit-editable');
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds padding around the editable element to make it pop visually.
|
||||
*/
|
||||
_pad() {
|
||||
// Early return if the element has already been padded.
|
||||
if (this.$el.data('quickedit-padded')) {
|
||||
return;
|
||||
}
|
||||
const 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('quickedit-animate-disable-width')
|
||||
.css('width', this.$el.width());
|
||||
}
|
||||
|
||||
// 2) Add padding; use animations.
|
||||
const posProp = this._getPositionProperties(this.$el);
|
||||
setTimeout(() => {
|
||||
// Re-enable width animations (padding changes affect width too!).
|
||||
self.$el.removeClass('quickedit-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`,
|
||||
})
|
||||
.data('quickedit-padded', true);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the padding around the element being edited when editing ceases.
|
||||
*/
|
||||
_unpad() {
|
||||
// Early return if the element has not been padded.
|
||||
if (!this.$el.data('quickedit-padded')) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
|
||||
// 1) Set the empty width again.
|
||||
if (this._widthAttributeIsEmpty) {
|
||||
this.$el.addClass('quickedit-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).
|
||||
const posProp = this._getPositionProperties(this.$el);
|
||||
setTimeout(() => {
|
||||
// Re-enable width animations (padding changes affect width too!).
|
||||
self.$el.removeClass('quickedit-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);
|
||||
// Remove the marker that indicates that this field has padding. This is
|
||||
// done outside the timed out function above so that we don't get numerous
|
||||
// queued functions that will remove padding before the data marker has
|
||||
// been removed.
|
||||
this.$el.removeData('quickedit-padded');
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the top and left properties of an element.
|
||||
*
|
||||
* Convert extraneous values and information into numbers ready for
|
||||
* subtraction.
|
||||
*
|
||||
* @param {jQuery} $e
|
||||
* The element to get position properties from.
|
||||
*
|
||||
* @return {object}
|
||||
* An object containing css values for the needed properties.
|
||||
*/
|
||||
_getPositionProperties($e) {
|
||||
let p;
|
||||
const r = {};
|
||||
const props = [
|
||||
'top',
|
||||
'left',
|
||||
'bottom',
|
||||
'right',
|
||||
'padding-top',
|
||||
'padding-left',
|
||||
'padding-right',
|
||||
'padding-bottom',
|
||||
'margin-bottom',
|
||||
];
|
||||
|
||||
const propCount = props.length;
|
||||
for (let 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 {string} [pos]
|
||||
* The value for a CSS position declaration.
|
||||
*
|
||||
* @return {string}
|
||||
* A CSS value that is valid for `position`.
|
||||
*/
|
||||
_replaceBlankPosition(pos) {
|
||||
if (pos === 'auto' || !pos) {
|
||||
pos = '0px';
|
||||
}
|
||||
return pos;
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, Backbone, Drupal);
|
|
@ -1,67 +1,33 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that decorates the in-place edited element.
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Backbone, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.FieldDecorationView = Backbone.View.extend(/** @lends Drupal.quickedit.FieldDecorationView# */{
|
||||
|
||||
/**
|
||||
* @type {null}
|
||||
*/
|
||||
Drupal.quickedit.FieldDecorationView = Backbone.View.extend({
|
||||
_widthAttributeIsEmpty: null,
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'mouseenter.quickedit': 'onMouseEnter',
|
||||
'mouseleave.quickedit': 'onMouseLeave',
|
||||
'click': 'onClick',
|
||||
click: 'onClick',
|
||||
'tabIn.quickedit': 'onMouseEnter',
|
||||
'tabOut.quickedit': 'onMouseLeave'
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* An object with the following keys:
|
||||
* @param {Drupal.quickedit.EditorView} options.editorView
|
||||
* The editor object view.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
initialize: function initialize(options) {
|
||||
this.editorView = options.editorView;
|
||||
|
||||
this.listenTo(this.model, 'change:state', this.stateChange);
|
||||
this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
remove: function () {
|
||||
// The el property is the field, which should not be removed. Remove the
|
||||
// pointer to it, then call Backbone.View.prototype.remove().
|
||||
remove: function remove() {
|
||||
this.setElement();
|
||||
Backbone.View.prototype.remove.call(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* The `FieldModel` model.
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
stateChange: function (model, state) {
|
||||
stateChange: function stateChange(model, state) {
|
||||
var from = model.previous('state');
|
||||
var to = state;
|
||||
switch (to) {
|
||||
|
@ -86,8 +52,6 @@
|
|||
break;
|
||||
|
||||
case 'activating':
|
||||
// NOTE: this state is not used by every editor! It's only used by
|
||||
// those that need to interact with the server.
|
||||
this.prepareEdit();
|
||||
break;
|
||||
|
||||
|
@ -114,222 +78,113 @@
|
|||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a class to the edited element that indicates whether the field has
|
||||
* been changed by the user (i.e. locally) or the field has already been
|
||||
* changed and stored before by the user (i.e. remotely, stored in
|
||||
* PrivateTempStore).
|
||||
*/
|
||||
renderChanged: function () {
|
||||
renderChanged: function renderChanged() {
|
||||
this.$el.toggleClass('quickedit-changed', this.model.get('isChanged') || this.model.get('inTempStore'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts hover; transitions to 'highlight' state.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The mouse event.
|
||||
*/
|
||||
onMouseEnter: function (event) {
|
||||
onMouseEnter: function onMouseEnter(event) {
|
||||
var that = this;
|
||||
that.model.set('state', 'highlighted');
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stops hover; transitions to 'candidate' state.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The mouse event.
|
||||
*/
|
||||
onMouseLeave: function (event) {
|
||||
onMouseLeave: function onMouseLeave(event) {
|
||||
var that = this;
|
||||
that.model.set('state', 'candidate', {reason: 'mouseleave'});
|
||||
that.model.set('state', 'candidate', { reason: 'mouseleave' });
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition to 'activating' stage.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClick: function (event) {
|
||||
onClick: function onClick(event) {
|
||||
this.model.set('state', 'activating');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes used to indicate an elements editable state.
|
||||
*/
|
||||
decorate: function () {
|
||||
decorate: function decorate() {
|
||||
this.$el.addClass('quickedit-candidate quickedit-editable');
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes used to indicate an elements editable state.
|
||||
*/
|
||||
undecorate: function () {
|
||||
undecorate: function undecorate() {
|
||||
this.$el.removeClass('quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing');
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds that class that indicates that an element is highlighted.
|
||||
*/
|
||||
startHighlight: function () {
|
||||
// Animations.
|
||||
startHighlight: function startHighlight() {
|
||||
var that = this;
|
||||
// Use a timeout to grab the next available animation frame.
|
||||
|
||||
that.$el.addClass('quickedit-highlighted');
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element is highlighted.
|
||||
*/
|
||||
stopHighlight: function () {
|
||||
stopHighlight: function stopHighlight() {
|
||||
this.$el.removeClass('quickedit-highlighted');
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element as editable.
|
||||
*/
|
||||
prepareEdit: function () {
|
||||
prepareEdit: function prepareEdit() {
|
||||
this.$el.addClass('quickedit-editing');
|
||||
|
||||
// Allow the field to be styled differently while editing in a pop-up
|
||||
// in-place editor.
|
||||
if (this.editorView.getQuickEditUISettings().popup) {
|
||||
this.$el.addClass('quickedit-editor-is-popup');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element is being edited.
|
||||
*
|
||||
* Reapplies the class that indicates that a candidate editable element is
|
||||
* again available to be edited.
|
||||
*/
|
||||
stopEdit: function () {
|
||||
stopEdit: function stopEdit() {
|
||||
this.$el.removeClass('quickedit-highlighted quickedit-editing');
|
||||
|
||||
// Done editing in a pop-up in-place editor; remove the class.
|
||||
if (this.editorView.getQuickEditUISettings().popup) {
|
||||
this.$el.removeClass('quickedit-editor-is-popup');
|
||||
}
|
||||
|
||||
// Make the other editors show up again.
|
||||
$('.quickedit-candidate').addClass('quickedit-editable');
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds padding around the editable element to make it pop visually.
|
||||
*/
|
||||
_pad: function () {
|
||||
// Early return if the element has already been padded.
|
||||
_pad: function _pad() {
|
||||
if (this.$el.data('quickedit-padded')) {
|
||||
return;
|
||||
}
|
||||
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('quickedit-animate-disable-width')
|
||||
.css('width', this.$el.width());
|
||||
this.$el.addClass('quickedit-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('quickedit-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'
|
||||
})
|
||||
.data('quickedit-padded', true);
|
||||
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'
|
||||
}).data('quickedit-padded', true);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the padding around the element being edited when editing ceases.
|
||||
*/
|
||||
_unpad: function () {
|
||||
// Early return if the element has not been padded.
|
||||
_unpad: function _unpad() {
|
||||
if (!this.$el.data('quickedit-padded')) {
|
||||
return;
|
||||
}
|
||||
var self = this;
|
||||
|
||||
// 1) Set the empty width again.
|
||||
if (this._widthAttributeIsEmpty) {
|
||||
this.$el
|
||||
.addClass('quickedit-animate-disable-width')
|
||||
.css('width', '');
|
||||
this.$el.addClass('quickedit-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('quickedit-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'
|
||||
});
|
||||
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);
|
||||
// Remove the marker that indicates that this field has padding. This is
|
||||
// done outside the timed out function above so that we don't get numerous
|
||||
// queued functions that will remove padding before the data marker has
|
||||
// been removed.
|
||||
|
||||
this.$el.removeData('quickedit-padded');
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the top and left properties of an element.
|
||||
*
|
||||
* Convert extraneous values and information into numbers ready for
|
||||
* subtraction.
|
||||
*
|
||||
* @param {jQuery} $e
|
||||
* The element to get position properties from.
|
||||
*
|
||||
* @return {object}
|
||||
* An object containing css values for the needed properties.
|
||||
*/
|
||||
_getPositionProperties: function ($e) {
|
||||
var p;
|
||||
_getPositionProperties: function _getPositionProperties($e) {
|
||||
var p = void 0;
|
||||
var r = {};
|
||||
var props = [
|
||||
'top', 'left', 'bottom', 'right',
|
||||
'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
|
||||
'margin-bottom'
|
||||
];
|
||||
var 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++) {
|
||||
|
@ -338,23 +193,11 @@
|
|||
}
|
||||
return r;
|
||||
},
|
||||
|
||||
/**
|
||||
* Replaces blank or 'auto' CSS `position: <value>` values with "0px".
|
||||
*
|
||||
* @param {string} [pos]
|
||||
* The value for a CSS position declaration.
|
||||
*
|
||||
* @return {string}
|
||||
* A CSS value that is valid for `position`.
|
||||
*/
|
||||
_replaceBlankPosition: function (pos) {
|
||||
_replaceBlankPosition: function _replaceBlankPosition(pos) {
|
||||
if (pos === 'auto' || !pos) {
|
||||
pos = '0px';
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, Backbone, Drupal);
|
||||
})(jQuery, Backbone, Drupal);
|
244
web/core/modules/quickedit/js/views/FieldToolbarView.es6.js
Normal file
244
web/core/modules/quickedit/js/views/FieldToolbarView.es6.js
Normal file
|
@ -0,0 +1,244 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides an interactive toolbar (1 per in-place editor).
|
||||
*/
|
||||
|
||||
(function($, _, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldToolbarView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.FieldToolbarView# */ {
|
||||
/**
|
||||
* The edited element, as indicated by EditorView.getEditedElement.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$editedElement: null,
|
||||
|
||||
/**
|
||||
* A reference to the in-place editor.
|
||||
*
|
||||
* @type {Drupal.quickedit.EditorView}
|
||||
*/
|
||||
editorView: null,
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
_id: null,
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object to construct the field toolbar.
|
||||
* @param {jQuery} options.$editedElement
|
||||
* The element being edited.
|
||||
* @param {Drupal.quickedit.EditorView} options.editorView
|
||||
* The EditorView the toolbar belongs to.
|
||||
*/
|
||||
initialize(options) {
|
||||
this.$editedElement = options.$editedElement;
|
||||
this.editorView = options.editorView;
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$root = this.$el;
|
||||
|
||||
// Generate a DOM-compatible ID for the form container DOM element.
|
||||
this._id = `quickedit-toolbar-for-${this.model.id.replace(
|
||||
/[/[\]]/g,
|
||||
'_',
|
||||
)}`;
|
||||
|
||||
this.listenTo(this.model, 'change:state', this.stateChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.quickedit.FieldToolbarView}
|
||||
* The current FieldToolbarView.
|
||||
*/
|
||||
render() {
|
||||
// Render toolbar and set it as the view's element.
|
||||
this.setElement(
|
||||
$(
|
||||
Drupal.theme('quickeditFieldToolbar', {
|
||||
id: this._id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Attach to the field toolbar $root element in the entity toolbar.
|
||||
this.$el.prependTo(this.$root);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* The quickedit FieldModel
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
stateChange(model, state) {
|
||||
const from = model.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
// Remove the view's existing element if we went to the 'activating'
|
||||
// state or later, because it will be recreated. Not doing this would
|
||||
// result in memory leaks.
|
||||
if (from !== 'inactive' && from !== 'highlighted') {
|
||||
this.$el.remove();
|
||||
this.setElement();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
this.render();
|
||||
|
||||
if (this.editorView.getQuickEditUISettings().fullWidthToolbar) {
|
||||
this.$el.addClass('quickedit-toolbar-fullwidth');
|
||||
}
|
||||
|
||||
if (this.editorView.getQuickEditUISettings().unifiedToolbar) {
|
||||
this.insertWYSIWYGToolGroups();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert WYSIWYG markup into the associated toolbar.
|
||||
*/
|
||||
insertWYSIWYGToolGroups() {
|
||||
this.$el
|
||||
.append(
|
||||
Drupal.theme('quickeditToolgroup', {
|
||||
id: this.getFloatedWysiwygToolgroupId(),
|
||||
classes: [
|
||||
'wysiwyg-floated',
|
||||
'quickedit-animate-slow',
|
||||
'quickedit-animate-invisible',
|
||||
'quickedit-animate-delay-veryfast',
|
||||
],
|
||||
buttons: [],
|
||||
}),
|
||||
)
|
||||
.append(
|
||||
Drupal.theme('quickeditToolgroup', {
|
||||
id: this.getMainWysiwygToolgroupId(),
|
||||
classes: [
|
||||
'wysiwyg-main',
|
||||
'quickedit-animate-slow',
|
||||
'quickedit-animate-invisible',
|
||||
'quickedit-animate-delay-veryfast',
|
||||
],
|
||||
buttons: [],
|
||||
}),
|
||||
);
|
||||
|
||||
// Animate the toolgroups into visibility.
|
||||
this.show('wysiwyg-floated');
|
||||
this.show('wysiwyg-main');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
return `quickedit-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() {
|
||||
return `quickedit-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() {
|
||||
return `quickedit-wysiwyg-main-toolgroup-for-${this._id}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolgroup element.
|
||||
*/
|
||||
_find(toolgroup) {
|
||||
return this.$el.find(`.quickedit-toolgroup.${toolgroup}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
show(toolgroup) {
|
||||
const $group = this._find(toolgroup);
|
||||
// Attach a transitionEnd event handler to the toolbar group so that
|
||||
// update events can be triggered after the animations have ended.
|
||||
$group.on(Drupal.quickedit.util.constants.transitionEnd, event => {
|
||||
$group.off(Drupal.quickedit.util.constants.transitionEnd);
|
||||
});
|
||||
// The call to remove the class and start the animation must be started in
|
||||
// the next animation frame or the event handler attached above won't be
|
||||
// triggered.
|
||||
window.setTimeout(() => {
|
||||
$group.removeClass('quickedit-animate-invisible');
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
);
|
||||
})(jQuery, _, Backbone, Drupal);
|
|
@ -1,88 +1,38 @@
|
|||
/**
|
||||
* @file
|
||||
* A Backbone View that provides an interactive toolbar (1 per in-place editor).
|
||||
*/
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, _, Backbone, Drupal) {
|
||||
|
||||
'use strict';
|
||||
|
||||
Drupal.quickedit.FieldToolbarView = Backbone.View.extend(/** @lends Drupal.quickedit.FieldToolbarView# */{
|
||||
|
||||
/**
|
||||
* The edited element, as indicated by EditorView.getEditedElement.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
Drupal.quickedit.FieldToolbarView = Backbone.View.extend({
|
||||
$editedElement: null,
|
||||
|
||||
/**
|
||||
* A reference to the in-place editor.
|
||||
*
|
||||
* @type {Drupal.quickedit.EditorView}
|
||||
*/
|
||||
editorView: null,
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
_id: null,
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.View
|
||||
*
|
||||
* @param {object} options
|
||||
* Options object to construct the field toolbar.
|
||||
* @param {jQuery} options.$editedElement
|
||||
* The element being edited.
|
||||
* @param {Drupal.quickedit.EditorView} options.editorView
|
||||
* The EditorView the toolbar belongs to.
|
||||
*/
|
||||
initialize: function (options) {
|
||||
initialize: function initialize(options) {
|
||||
this.$editedElement = options.$editedElement;
|
||||
this.editorView = options.editorView;
|
||||
|
||||
/**
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$root = this.$el;
|
||||
|
||||
// Generate a DOM-compatible ID for the form container DOM element.
|
||||
this._id = 'quickedit-toolbar-for-' + this.model.id.replace(/[\/\[\]]/g, '_');
|
||||
this._id = 'quickedit-toolbar-for-' + this.model.id.replace(/[/[\]]/g, '_');
|
||||
|
||||
this.listenTo(this.model, 'change:state', this.stateChange);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {Drupal.quickedit.FieldToolbarView}
|
||||
* The current FieldToolbarView.
|
||||
*/
|
||||
render: function () {
|
||||
// Render toolbar and set it as the view's element.
|
||||
render: function render() {
|
||||
this.setElement($(Drupal.theme('quickeditFieldToolbar', {
|
||||
id: this._id
|
||||
})));
|
||||
|
||||
// Attach to the field toolbar $root element in the entity toolbar.
|
||||
this.$el.prependTo(this.$root);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the actions to take given a change of state.
|
||||
*
|
||||
* @param {Drupal.quickedit.FieldModel} model
|
||||
* The quickedit FieldModel
|
||||
* @param {string} state
|
||||
* The state of the associated field. One of
|
||||
* {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
stateChange: function (model, state) {
|
||||
stateChange: function stateChange(model, state) {
|
||||
var from = model.previous('state');
|
||||
var to = state;
|
||||
switch (to) {
|
||||
|
@ -90,9 +40,6 @@
|
|||
break;
|
||||
|
||||
case 'candidate':
|
||||
// Remove the view's existing element if we went to the 'activating'
|
||||
// state or later, because it will be recreated. Not doing this would
|
||||
// result in memory leaks.
|
||||
if (from !== 'inactive' && from !== 'highlighted') {
|
||||
this.$el.remove();
|
||||
this.setElement();
|
||||
|
@ -130,98 +77,42 @@
|
|||
break;
|
||||
}
|
||||
},
|
||||
insertWYSIWYGToolGroups: function insertWYSIWYGToolGroups() {
|
||||
this.$el.append(Drupal.theme('quickeditToolgroup', {
|
||||
id: this.getFloatedWysiwygToolgroupId(),
|
||||
classes: ['wysiwyg-floated', 'quickedit-animate-slow', 'quickedit-animate-invisible', 'quickedit-animate-delay-veryfast'],
|
||||
buttons: []
|
||||
})).append(Drupal.theme('quickeditToolgroup', {
|
||||
id: this.getMainWysiwygToolgroupId(),
|
||||
classes: ['wysiwyg-main', 'quickedit-animate-slow', 'quickedit-animate-invisible', 'quickedit-animate-delay-veryfast'],
|
||||
buttons: []
|
||||
}));
|
||||
|
||||
/**
|
||||
* Insert WYSIWYG markup into the associated toolbar.
|
||||
*/
|
||||
insertWYSIWYGToolGroups: function () {
|
||||
this.$el
|
||||
.append(Drupal.theme('quickeditToolgroup', {
|
||||
id: this.getFloatedWysiwygToolgroupId(),
|
||||
classes: ['wysiwyg-floated', 'quickedit-animate-slow', 'quickedit-animate-invisible', 'quickedit-animate-delay-veryfast'],
|
||||
buttons: []
|
||||
}))
|
||||
.append(Drupal.theme('quickeditToolgroup', {
|
||||
id: this.getMainWysiwygToolgroupId(),
|
||||
classes: ['wysiwyg-main', 'quickedit-animate-slow', 'quickedit-animate-invisible', 'quickedit-animate-delay-veryfast'],
|
||||
buttons: []
|
||||
}));
|
||||
|
||||
// Animate the toolgroups into visibility.
|
||||
this.show('wysiwyg-floated');
|
||||
this.show('wysiwyg-main');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 () {
|
||||
getId: function getId() {
|
||||
return 'quickedit-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 () {
|
||||
getFloatedWysiwygToolgroupId: function getFloatedWysiwygToolgroupId() {
|
||||
return 'quickedit-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 () {
|
||||
getMainWysiwygToolgroupId: function getMainWysiwygToolgroupId() {
|
||||
return 'quickedit-wysiwyg-main-toolgroup-for-' + this._id;
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The toolgroup element.
|
||||
*/
|
||||
_find: function (toolgroup) {
|
||||
_find: function _find(toolgroup) {
|
||||
return this.$el.find('.quickedit-toolgroup.' + toolgroup);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows a toolgroup.
|
||||
*
|
||||
* @param {string} toolgroup
|
||||
* A toolgroup name.
|
||||
*/
|
||||
show: function (toolgroup) {
|
||||
show: function show(toolgroup) {
|
||||
var $group = this._find(toolgroup);
|
||||
// Attach a transitionEnd event handler to the toolbar group so that
|
||||
// update events can be triggered after the animations have ended.
|
||||
|
||||
$group.on(Drupal.quickedit.util.constants.transitionEnd, function (event) {
|
||||
$group.off(Drupal.quickedit.util.constants.transitionEnd);
|
||||
});
|
||||
// The call to remove the class and start the animation must be started in
|
||||
// the next animation frame or the event handler attached above won't be
|
||||
// triggered.
|
||||
|
||||
window.setTimeout(function () {
|
||||
$group.removeClass('quickedit-animate-invisible');
|
||||
}, 0);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
})(jQuery, _, Backbone, Drupal);
|
||||
})(jQuery, _, Backbone, Drupal);
|
Reference in a new issue