updated core to 8.6.1 via composer
This commit is contained in:
@@ -86,7 +86,6 @@
|
||||
* Entity toolbar.
|
||||
*/
|
||||
.quickedit-toolbar-container {
|
||||
max-width: 100%;
|
||||
position: absolute;
|
||||
max-width: 320px;
|
||||
width: 320px;
|
||||
|
||||
@@ -3,250 +3,277 @@
|
||||
* Form-based in-place editor. Works for any field type.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, _) {
|
||||
(function($, Drupal, _) {
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @augments Drupal.quickedit.EditorView
|
||||
*/
|
||||
Drupal.quickedit.editors.form = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.form# */{
|
||||
Drupal.quickedit.editors.form = Drupal.quickedit.EditorView.extend(
|
||||
/** @lends Drupal.quickedit.editors.form# */ {
|
||||
/**
|
||||
* Tracks form container DOM element that is used while in-place editing.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$formContainer: null,
|
||||
|
||||
/**
|
||||
* Tracks form container DOM element that is used while in-place editing.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$formContainer: null,
|
||||
/**
|
||||
* Holds the {@link Drupal.Ajax} object.
|
||||
*
|
||||
* @type {Drupal.Ajax}
|
||||
*/
|
||||
formSaveAjax: null,
|
||||
|
||||
/**
|
||||
* Holds the {@link Drupal.Ajax} object.
|
||||
*
|
||||
* @type {Drupal.Ajax}
|
||||
*/
|
||||
formSaveAjax: null,
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* The field model that holds the state.
|
||||
* @param {string} state
|
||||
* The state to change to.
|
||||
*/
|
||||
stateChange(fieldModel, state) {
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
break;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* The field model that holds the state.
|
||||
* @param {string} state
|
||||
* The state to change to.
|
||||
*/
|
||||
stateChange(fieldModel, state) {
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
break;
|
||||
case 'candidate':
|
||||
if (from !== 'inactive') {
|
||||
this.removeForm();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
if (from !== 'inactive') {
|
||||
this.removeForm();
|
||||
}
|
||||
break;
|
||||
case 'highlighted':
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
break;
|
||||
case 'activating':
|
||||
// If coming from an invalid state, then the form is already loaded.
|
||||
if (from !== 'invalid') {
|
||||
this.loadForm();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// If coming from an invalid state, then the form is already loaded.
|
||||
if (from !== 'invalid') {
|
||||
this.loadForm();
|
||||
}
|
||||
break;
|
||||
case 'active':
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
break;
|
||||
case 'changed':
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
break;
|
||||
case 'saving':
|
||||
this.save();
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
this.save();
|
||||
break;
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
case 'invalid':
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
case 'invalid':
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
* A settings object for the quick edit UI.
|
||||
*/
|
||||
getQuickEditUISettings() {
|
||||
return {
|
||||
padding: true,
|
||||
unifiedToolbar: true,
|
||||
fullWidthToolbar: true,
|
||||
popup: true,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
* A settings object for the quick edit UI.
|
||||
*/
|
||||
getQuickEditUISettings() {
|
||||
return { padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: true };
|
||||
},
|
||||
/**
|
||||
* Loads the form for this field, displays it on top of the actual field.
|
||||
*/
|
||||
loadForm() {
|
||||
const fieldModel = this.fieldModel;
|
||||
|
||||
/**
|
||||
* Loads the form for this field, displays it on top of the actual field.
|
||||
*/
|
||||
loadForm() {
|
||||
const fieldModel = this.fieldModel;
|
||||
|
||||
// Generate a DOM-compatible ID for the form container DOM element.
|
||||
const id = `quickedit-form-for-${fieldModel.id.replace(/[/[\]]/g, '_')}`;
|
||||
|
||||
// Render form container.
|
||||
const $formContainer = $(Drupal.theme('quickeditFormContainer', {
|
||||
id,
|
||||
loadingMsg: Drupal.t('Loading…'),
|
||||
}));
|
||||
this.$formContainer = $formContainer;
|
||||
$formContainer
|
||||
.find('.quickedit-form')
|
||||
.addClass('quickedit-editable quickedit-highlighted quickedit-editing')
|
||||
.attr('role', 'dialog');
|
||||
|
||||
// Insert form container in DOM.
|
||||
if (this.$el.css('display') === 'inline') {
|
||||
$formContainer.prependTo(this.$el.offsetParent());
|
||||
// Position the form container to render on top of the field's element.
|
||||
const pos = this.$el.position();
|
||||
$formContainer.css('left', pos.left).css('top', pos.top);
|
||||
}
|
||||
else {
|
||||
$formContainer.insertBefore(this.$el);
|
||||
}
|
||||
|
||||
// Load form, insert it into the form container and attach event handlers.
|
||||
const formOptions = {
|
||||
fieldID: fieldModel.get('fieldID'),
|
||||
$el: this.$el,
|
||||
nocssjs: false,
|
||||
// Reset an existing entry for this entity in the PrivateTempStore (if
|
||||
// any) when loading 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
|
||||
// loads a first changed field for an entity that this needs to happen:
|
||||
// precisely now!
|
||||
reset: !fieldModel.get('entity').get('inTempStore'),
|
||||
};
|
||||
Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
|
||||
Drupal.AjaxCommands.prototype.insert(ajax, {
|
||||
data: form,
|
||||
selector: `#${id} .placeholder`,
|
||||
});
|
||||
// Generate a DOM-compatible ID for the form container DOM element.
|
||||
const id = `quickedit-form-for-${fieldModel.id.replace(
|
||||
/[/[\]]/g,
|
||||
'_',
|
||||
)}`;
|
||||
|
||||
// Render form container.
|
||||
const $formContainer = $(
|
||||
Drupal.theme('quickeditFormContainer', {
|
||||
id,
|
||||
loadingMsg: Drupal.t('Loading…'),
|
||||
}),
|
||||
);
|
||||
this.$formContainer = $formContainer;
|
||||
$formContainer
|
||||
.on('formUpdated.quickedit', ':input', (event) => {
|
||||
const state = fieldModel.get('state');
|
||||
// If the form is in an invalid state, it will persist on the page.
|
||||
// Set the field to activating so that the user can correct the
|
||||
// invalid value.
|
||||
if (state === 'invalid') {
|
||||
fieldModel.set('state', 'activating');
|
||||
}
|
||||
// Otherwise assume that the fieldModel is in a candidate state and
|
||||
// set it to changed on formUpdate.
|
||||
else {
|
||||
fieldModel.set('state', 'changed');
|
||||
}
|
||||
})
|
||||
.on('keypress.quickedit', 'input', (event) => {
|
||||
if (event.keyCode === 13) {
|
||||
return false;
|
||||
}
|
||||
.find('.quickedit-form')
|
||||
.addClass(
|
||||
'quickedit-editable quickedit-highlighted quickedit-editing',
|
||||
)
|
||||
.attr('role', 'dialog');
|
||||
|
||||
// Insert form container in DOM.
|
||||
if (this.$el.css('display') === 'inline') {
|
||||
$formContainer.prependTo(this.$el.offsetParent());
|
||||
// Position the form container to render on top of the field's element.
|
||||
const pos = this.$el.position();
|
||||
$formContainer.css('left', pos.left).css('top', pos.top);
|
||||
} else {
|
||||
$formContainer.insertBefore(this.$el);
|
||||
}
|
||||
|
||||
// Load form, insert it into the form container and attach event handlers.
|
||||
const formOptions = {
|
||||
fieldID: fieldModel.get('fieldID'),
|
||||
$el: this.$el,
|
||||
nocssjs: false,
|
||||
// Reset an existing entry for this entity in the PrivateTempStore (if
|
||||
// any) when loading 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
|
||||
// loads a first changed field for an entity that this needs to happen:
|
||||
// precisely now!
|
||||
reset: !fieldModel.get('entity').get('inTempStore'),
|
||||
};
|
||||
Drupal.quickedit.util.form.load(formOptions, (form, ajax) => {
|
||||
Drupal.AjaxCommands.prototype.insert(ajax, {
|
||||
data: form,
|
||||
selector: `#${id} .placeholder`,
|
||||
});
|
||||
|
||||
// The in-place editor has loaded; change state to 'active'.
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
},
|
||||
$formContainer
|
||||
.on('formUpdated.quickedit', ':input', event => {
|
||||
const state = fieldModel.get('state');
|
||||
// If the form is in an invalid state, it will persist on the page.
|
||||
// Set the field to activating so that the user can correct the
|
||||
// invalid value.
|
||||
if (state === 'invalid') {
|
||||
fieldModel.set('state', 'activating');
|
||||
}
|
||||
// Otherwise assume that the fieldModel is in a candidate state and
|
||||
// set it to changed on formUpdate.
|
||||
else {
|
||||
fieldModel.set('state', 'changed');
|
||||
}
|
||||
})
|
||||
.on('keypress.quickedit', 'input', event => {
|
||||
if (event.keyCode === 13) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Removes the form for this field, detaches behaviors and event handlers.
|
||||
*/
|
||||
removeForm() {
|
||||
if (this.$formContainer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
delete this.formSaveAjax;
|
||||
// Allow form widgets to detach properly.
|
||||
Drupal.detachBehaviors(this.$formContainer.get(0), null, 'unload');
|
||||
this.$formContainer
|
||||
.off('change.quickedit', ':input')
|
||||
.off('keypress.quickedit', 'input')
|
||||
.remove();
|
||||
this.$formContainer = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
save() {
|
||||
const $formContainer = this.$formContainer;
|
||||
const $submit = $formContainer.find('.quickedit-form-submit');
|
||||
const editorModel = this.model;
|
||||
const fieldModel = this.fieldModel;
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
// Create an AJAX object for the form associated with the field.
|
||||
let formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving({
|
||||
nocssjs: false,
|
||||
other_view_modes: fieldModel.findOtherViewModes(),
|
||||
}, $submit);
|
||||
|
||||
// Successfully saved.
|
||||
formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
|
||||
cleanUpAjax();
|
||||
// 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.
|
||||
_.defer(() => {
|
||||
fieldModel.set('html', response.data);
|
||||
// The in-place editor has loaded; change state to 'active'.
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
// Unsuccessfully saved; validation errors.
|
||||
formSaveAjax.commands.quickeditFieldFormValidationErrors = function (ajax, response, status) {
|
||||
editorModel.set('validationErrors', response.data);
|
||||
fieldModel.set('state', 'invalid');
|
||||
};
|
||||
/**
|
||||
* Removes the form for this field, detaches behaviors and event handlers.
|
||||
*/
|
||||
removeForm() {
|
||||
if (this.$formContainer === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The quickeditFieldForm AJAX command is called upon attempting to save
|
||||
// the form; Form API will mark which form items have errors, if any. This
|
||||
// command is invoked only if validation errors exist and then it runs
|
||||
// before editFieldFormValidationErrors().
|
||||
formSaveAjax.commands.quickeditFieldForm = function (ajax, response, status) {
|
||||
Drupal.AjaxCommands.prototype.insert(ajax, {
|
||||
data: response.data,
|
||||
selector: `#${$formContainer.attr('id')} form`,
|
||||
});
|
||||
};
|
||||
delete this.formSaveAjax;
|
||||
// Allow form widgets to detach properly.
|
||||
Drupal.detachBehaviors(this.$formContainer.get(0), null, 'unload');
|
||||
this.$formContainer
|
||||
.off('change.quickedit', ':input')
|
||||
.off('keypress.quickedit', 'input')
|
||||
.remove();
|
||||
this.$formContainer = null;
|
||||
},
|
||||
|
||||
// Click the form's submit button; the scoped AJAX commands above will
|
||||
// handle the server's response.
|
||||
$submit.trigger('click.quickedit');
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
save() {
|
||||
const $formContainer = this.$formContainer;
|
||||
const $submit = $formContainer.find('.quickedit-form-submit');
|
||||
const editorModel = this.model;
|
||||
const fieldModel = this.fieldModel;
|
||||
|
||||
// Create an AJAX object for the form associated with the field.
|
||||
let formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving(
|
||||
{
|
||||
nocssjs: false,
|
||||
other_view_modes: fieldModel.findOtherViewModes(),
|
||||
},
|
||||
$submit,
|
||||
);
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
// Successfully saved.
|
||||
formSaveAjax.commands.quickeditFieldFormSaved = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
cleanUpAjax();
|
||||
// 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.
|
||||
_.defer(() => {
|
||||
fieldModel.set('html', response.data);
|
||||
});
|
||||
};
|
||||
|
||||
// Unsuccessfully saved; validation errors.
|
||||
formSaveAjax.commands.quickeditFieldFormValidationErrors = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
editorModel.set('validationErrors', response.data);
|
||||
fieldModel.set('state', 'invalid');
|
||||
};
|
||||
|
||||
// The quickeditFieldForm AJAX command is called upon attempting to save
|
||||
// the form; Form API will mark which form items have errors, if any. This
|
||||
// command is invoked only if validation errors exist and then it runs
|
||||
// before editFieldFormValidationErrors().
|
||||
formSaveAjax.commands.quickeditFieldForm = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
Drupal.AjaxCommands.prototype.insert(ajax, {
|
||||
data: response.data,
|
||||
selector: `#${$formContainer.attr('id')} form`,
|
||||
});
|
||||
};
|
||||
|
||||
// Click the form's submit button; the scoped AJAX commands above will
|
||||
// handle the server's response.
|
||||
$submit.trigger('click.quickedit');
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
showValidationErrors() {
|
||||
this.$formContainer
|
||||
.find('.quickedit-form')
|
||||
.addClass('quickedit-validation-error')
|
||||
.find('form')
|
||||
.prepend(this.model.get('validationErrors'));
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
showValidationErrors() {
|
||||
this.$formContainer
|
||||
.find('.quickedit-form')
|
||||
.addClass('quickedit-validation-error')
|
||||
.find('form')
|
||||
.prepend(this.model.get('validationErrors'));
|
||||
},
|
||||
});
|
||||
}(jQuery, Drupal, _));
|
||||
);
|
||||
})(jQuery, Drupal, _);
|
||||
|
||||
@@ -52,7 +52,12 @@
|
||||
}
|
||||
},
|
||||
getQuickEditUISettings: function getQuickEditUISettings() {
|
||||
return { padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: true };
|
||||
return {
|
||||
padding: true,
|
||||
unifiedToolbar: true,
|
||||
fullWidthToolbar: true,
|
||||
popup: true
|
||||
};
|
||||
},
|
||||
loadForm: function loadForm() {
|
||||
var fieldModel = this.fieldModel;
|
||||
@@ -122,16 +127,16 @@
|
||||
var editorModel = this.model;
|
||||
var fieldModel = this.fieldModel;
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
var formSaveAjax = Drupal.quickedit.util.form.ajaxifySaving({
|
||||
nocssjs: false,
|
||||
other_view_modes: fieldModel.findOtherViewModes()
|
||||
}, $submit);
|
||||
|
||||
function cleanUpAjax() {
|
||||
Drupal.quickedit.util.form.unajaxifySaving(formSaveAjax);
|
||||
formSaveAjax = null;
|
||||
}
|
||||
|
||||
formSaveAjax.commands.quickeditFieldFormSaved = function (ajax, response, status) {
|
||||
cleanUpAjax();
|
||||
|
||||
|
||||
@@ -3,133 +3,138 @@
|
||||
* ContentEditable-based in-place editor for plain text content.
|
||||
*/
|
||||
|
||||
(function ($, _, Drupal) {
|
||||
Drupal.quickedit.editors.plain_text = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.plain_text# */{
|
||||
(function($, _, Drupal) {
|
||||
Drupal.quickedit.editors.plain_text = Drupal.quickedit.EditorView.extend(
|
||||
/** @lends Drupal.quickedit.editors.plain_text# */ {
|
||||
/**
|
||||
* Stores the textual DOM element that is being in-place edited.
|
||||
*/
|
||||
$textElement: null,
|
||||
|
||||
/**
|
||||
* Stores the textual DOM element that is being in-place edited.
|
||||
*/
|
||||
$textElement: null,
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Drupal.quickedit.EditorView
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the plain text editor.
|
||||
*/
|
||||
initialize(options) {
|
||||
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Drupal.quickedit.EditorView
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the plain text editor.
|
||||
*/
|
||||
initialize(options) {
|
||||
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
const editorModel = this.model;
|
||||
const fieldModel = this.fieldModel;
|
||||
|
||||
const editorModel = this.model;
|
||||
const fieldModel = this.fieldModel;
|
||||
// Store the original value of this field. Necessary for reverting
|
||||
// changes.
|
||||
const $fieldItems = this.$el.find('.quickedit-field');
|
||||
const $textElement = $fieldItems.length ? $fieldItems.eq(0) : this.$el;
|
||||
this.$textElement = $textElement;
|
||||
editorModel.set('originalValue', $.trim(this.$textElement.text()));
|
||||
|
||||
// Store the original value of this field. Necessary for reverting
|
||||
// changes.
|
||||
const $fieldItems = this.$el.find('.quickedit-field');
|
||||
const $textElement = $fieldItems.length ? $fieldItems.eq(0) : this.$el;
|
||||
this.$textElement = $textElement;
|
||||
editorModel.set('originalValue', $.trim(this.$textElement.text()));
|
||||
// Sets the state to 'changed' whenever the value changes.
|
||||
let previousText = editorModel.get('originalValue');
|
||||
$textElement.on('keyup paste', event => {
|
||||
const currentText = $.trim($textElement.text());
|
||||
if (previousText !== currentText) {
|
||||
previousText = currentText;
|
||||
editorModel.set('currentValue', currentText);
|
||||
fieldModel.set('state', 'changed');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Sets the state to 'changed' whenever the value changes.
|
||||
let previousText = editorModel.get('originalValue');
|
||||
$textElement.on('keyup paste', (event) => {
|
||||
const currentText = $.trim($textElement.text());
|
||||
if (previousText !== currentText) {
|
||||
previousText = currentText;
|
||||
editorModel.set('currentValue', currentText);
|
||||
fieldModel.set('state', 'changed');
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The text element for the plain text editor.
|
||||
*/
|
||||
getEditedElement() {
|
||||
return this.$textElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* The field model that holds the state.
|
||||
* @param {string} state
|
||||
* The state to change to.
|
||||
* @param {object} options
|
||||
* State options, if needed by the state change.
|
||||
*/
|
||||
stateChange(fieldModel, state, options) {
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
if (from !== 'inactive') {
|
||||
this.$textElement.removeAttr('contenteditable');
|
||||
}
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// Defer updating the field model until the current state change has
|
||||
// propagated, to not trigger a nested state change event.
|
||||
_.defer(() => {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
this.$textElement.attr('contenteditable', 'true');
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
this.save(options);
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
* A settings object for the quick edit UI.
|
||||
*/
|
||||
getQuickEditUISettings() {
|
||||
return {
|
||||
padding: true,
|
||||
unifiedToolbar: false,
|
||||
fullWidthToolbar: false,
|
||||
popup: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
revert() {
|
||||
this.$textElement.html(this.model.get('originalValue'));
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {jQuery}
|
||||
* The text element for the plain text editor.
|
||||
*/
|
||||
getEditedElement() {
|
||||
return this.$textElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* The field model that holds the state.
|
||||
* @param {string} state
|
||||
* The state to change to.
|
||||
* @param {object} options
|
||||
* State options, if needed by the state change.
|
||||
*/
|
||||
stateChange(fieldModel, state, options) {
|
||||
const from = fieldModel.previous('state');
|
||||
const to = state;
|
||||
switch (to) {
|
||||
case 'inactive':
|
||||
break;
|
||||
|
||||
case 'candidate':
|
||||
if (from !== 'inactive') {
|
||||
this.$textElement.removeAttr('contenteditable');
|
||||
}
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
break;
|
||||
|
||||
case 'activating':
|
||||
// Defer updating the field model until the current state change has
|
||||
// propagated, to not trigger a nested state change event.
|
||||
_.defer(() => {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
this.$textElement.attr('contenteditable', 'true');
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
this.save(options);
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
* A settings object for the quick edit UI.
|
||||
*/
|
||||
getQuickEditUISettings() {
|
||||
return { padding: true, unifiedToolbar: false, fullWidthToolbar: false, popup: false };
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
revert() {
|
||||
this.$textElement.html(this.model.get('originalValue'));
|
||||
},
|
||||
|
||||
});
|
||||
}(jQuery, _, Drupal));
|
||||
);
|
||||
})(jQuery, _, Drupal);
|
||||
|
||||
@@ -81,7 +81,12 @@
|
||||
}
|
||||
},
|
||||
getQuickEditUISettings: function getQuickEditUISettings() {
|
||||
return { padding: true, unifiedToolbar: false, fullWidthToolbar: false, popup: false };
|
||||
return {
|
||||
padding: true,
|
||||
unifiedToolbar: false,
|
||||
fullWidthToolbar: false,
|
||||
popup: false
|
||||
};
|
||||
},
|
||||
revert: function revert() {
|
||||
this.$textElement.html(this.model.get('originalValue'));
|
||||
|
||||
@@ -5,49 +5,48 @@
|
||||
* @see Drupal.quickedit.AppView
|
||||
*/
|
||||
|
||||
(function (Backbone, Drupal) {
|
||||
(function(Backbone, Drupal) {
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*/
|
||||
Drupal.quickedit.AppModel = Backbone.Model.extend(/** @lends Drupal.quickedit.AppModel# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Drupal.quickedit.FieldModel} highlightedField
|
||||
* @prop {Drupal.quickedit.FieldModel} activeField
|
||||
* @prop {Drupal.dialog~dialogDefinition} activeModal
|
||||
*/
|
||||
defaults: /** @lends Drupal.quickedit.AppModel# */{
|
||||
|
||||
Drupal.quickedit.AppModel = Backbone.Model.extend(
|
||||
/** @lends Drupal.quickedit.AppModel# */ {
|
||||
/**
|
||||
* The currently state='highlighted' Drupal.quickedit.FieldModel, if any.
|
||||
* @type {object}
|
||||
*
|
||||
* @type {Drupal.quickedit.FieldModel}
|
||||
*
|
||||
* @see Drupal.quickedit.FieldModel.states
|
||||
* @prop {Drupal.quickedit.FieldModel} highlightedField
|
||||
* @prop {Drupal.quickedit.FieldModel} activeField
|
||||
* @prop {Drupal.dialog~dialogDefinition} activeModal
|
||||
*/
|
||||
highlightedField: null,
|
||||
defaults: /** @lends Drupal.quickedit.AppModel# */ {
|
||||
/**
|
||||
* The currently state='highlighted' Drupal.quickedit.FieldModel, if any.
|
||||
*
|
||||
* @type {Drupal.quickedit.FieldModel}
|
||||
*
|
||||
* @see Drupal.quickedit.FieldModel.states
|
||||
*/
|
||||
highlightedField: null,
|
||||
|
||||
/**
|
||||
* The currently state = 'active' Drupal.quickedit.FieldModel, if any.
|
||||
*
|
||||
* @type {Drupal.quickedit.FieldModel}
|
||||
*
|
||||
* @see Drupal.quickedit.FieldModel.states
|
||||
*/
|
||||
activeField: null,
|
||||
/**
|
||||
* The currently state = 'active' Drupal.quickedit.FieldModel, if any.
|
||||
*
|
||||
* @type {Drupal.quickedit.FieldModel}
|
||||
*
|
||||
* @see Drupal.quickedit.FieldModel.states
|
||||
*/
|
||||
activeField: null,
|
||||
|
||||
/**
|
||||
* Reference to a {@link Drupal.dialog} instance if a state change
|
||||
* requires confirmation.
|
||||
*
|
||||
* @type {Drupal.dialog~dialogDefinition}
|
||||
*/
|
||||
activeModal: null,
|
||||
/**
|
||||
* Reference to a {@link Drupal.dialog} instance if a state change
|
||||
* requires confirmation.
|
||||
*
|
||||
* @type {Drupal.dialog~dialogDefinition}
|
||||
*/
|
||||
activeModal: null,
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
}(Backbone, Drupal));
|
||||
);
|
||||
})(Backbone, Drupal);
|
||||
|
||||
@@ -14,6 +14,5 @@
|
||||
|
||||
activeModal: null
|
||||
}
|
||||
|
||||
});
|
||||
})(Backbone, Drupal);
|
||||
@@ -3,54 +3,53 @@
|
||||
* A Backbone Model subclass that enforces validation when calling set().
|
||||
*/
|
||||
|
||||
(function (Drupal, Backbone) {
|
||||
Drupal.quickedit.BaseModel = Backbone.Model.extend(/** @lends Drupal.quickedit.BaseModel# */{
|
||||
(function(Drupal, Backbone) {
|
||||
Drupal.quickedit.BaseModel = Backbone.Model.extend(
|
||||
/** @lends Drupal.quickedit.BaseModel# */ {
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the base model-
|
||||
*
|
||||
* @return {Drupal.quickedit.BaseModel}
|
||||
* A quickedit base model.
|
||||
*/
|
||||
initialize(options) {
|
||||
this.__initialized = true;
|
||||
return Backbone.Model.prototype.initialize.call(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the base model-
|
||||
*
|
||||
* @return {Drupal.quickedit.BaseModel}
|
||||
* A quickedit base model.
|
||||
*/
|
||||
initialize(options) {
|
||||
this.__initialized = true;
|
||||
return Backbone.Model.prototype.initialize.call(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a value on the model
|
||||
*
|
||||
* @param {object|string} key
|
||||
* The key to set a value for.
|
||||
* @param {*} val
|
||||
* The value to set.
|
||||
* @param {object} [options]
|
||||
* Options for the model.
|
||||
*
|
||||
* @return {*}
|
||||
* The result of `Backbone.Model.prototype.set` with the specified
|
||||
* parameters.
|
||||
*/
|
||||
set(key, val, options) {
|
||||
if (this.__initialized) {
|
||||
// Deal with both the "key", value and {key:value}-style arguments.
|
||||
if (typeof key === 'object') {
|
||||
key.validate = true;
|
||||
}
|
||||
else {
|
||||
if (!options) {
|
||||
options = {};
|
||||
/**
|
||||
* Set a value on the model
|
||||
*
|
||||
* @param {object|string} key
|
||||
* The key to set a value for.
|
||||
* @param {*} val
|
||||
* The value to set.
|
||||
* @param {object} [options]
|
||||
* Options for the model.
|
||||
*
|
||||
* @return {*}
|
||||
* The result of `Backbone.Model.prototype.set` with the specified
|
||||
* parameters.
|
||||
*/
|
||||
set(key, val, options) {
|
||||
if (this.__initialized) {
|
||||
// Deal with both the "key", value and {key:value}-style arguments.
|
||||
if (typeof key === 'object') {
|
||||
key.validate = true;
|
||||
} else {
|
||||
if (!options) {
|
||||
options = {};
|
||||
}
|
||||
options.validate = true;
|
||||
}
|
||||
options.validate = true;
|
||||
}
|
||||
}
|
||||
return Backbone.Model.prototype.set.call(this, key, val, options);
|
||||
return Backbone.Model.prototype.set.call(this, key, val, options);
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
}(Drupal, Backbone));
|
||||
);
|
||||
})(Drupal, Backbone);
|
||||
|
||||
@@ -5,46 +5,45 @@
|
||||
* @see Drupal.quickedit.EditorView
|
||||
*/
|
||||
|
||||
(function (Backbone, Drupal) {
|
||||
(function(Backbone, Drupal) {
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.Model
|
||||
*/
|
||||
Drupal.quickedit.EditorModel = Backbone.Model.extend(/** @lends Drupal.quickedit.EditorModel# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {string} originalValue
|
||||
* @prop {string} currentValue
|
||||
* @prop {Array} validationErrors
|
||||
*/
|
||||
defaults: /** @lends Drupal.quickedit.EditorModel# */{
|
||||
|
||||
Drupal.quickedit.EditorModel = Backbone.Model.extend(
|
||||
/** @lends Drupal.quickedit.EditorModel# */ {
|
||||
/**
|
||||
* Not the full HTML representation of this field, but the "actual"
|
||||
* original value of the field, stored by the used in-place editor, and
|
||||
* in a representation that can be chosen by the in-place editor.
|
||||
* @type {object}
|
||||
*
|
||||
* @type {string}
|
||||
* @prop {string} originalValue
|
||||
* @prop {string} currentValue
|
||||
* @prop {Array} validationErrors
|
||||
*/
|
||||
originalValue: null,
|
||||
defaults: /** @lends Drupal.quickedit.EditorModel# */ {
|
||||
/**
|
||||
* Not the full HTML representation of this field, but the "actual"
|
||||
* original value of the field, stored by the used in-place editor, and
|
||||
* in a representation that can be chosen by the in-place editor.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
originalValue: null,
|
||||
|
||||
/**
|
||||
* Analogous to originalValue, but the current value.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
currentValue: null,
|
||||
/**
|
||||
* Analogous to originalValue, but the current value.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
currentValue: null,
|
||||
|
||||
/**
|
||||
* Stores any validation errors to be rendered.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
validationErrors: null,
|
||||
/**
|
||||
* Stores any validation errors to be rendered.
|
||||
*
|
||||
* @type {Array}
|
||||
*/
|
||||
validationErrors: null,
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
}(Backbone, Drupal));
|
||||
);
|
||||
})(Backbone, Drupal);
|
||||
|
||||
@@ -14,6 +14,5 @@
|
||||
|
||||
validationErrors: null
|
||||
}
|
||||
|
||||
});
|
||||
})(Backbone, Drupal);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -193,7 +193,9 @@
|
||||
error: function error() {
|
||||
entityModel.set('isCommitting', false);
|
||||
|
||||
entityModel.set('state', 'opened', { reason: 'networkerror' });
|
||||
entityModel.set('state', 'opened', {
|
||||
reason: 'networkerror'
|
||||
});
|
||||
|
||||
var message = Drupal.t('Your changes to <q>@entity-title</q> could not be saved, either due to a website problem or a network connection problem.<br>Please try again.', { '@entity-title': entityModel.get('label') });
|
||||
Drupal.quickedit.util.networkErrorModal(Drupal.t('Network problem!'), message);
|
||||
@@ -256,9 +258,11 @@
|
||||
|
||||
if (!this._acceptStateChange(currentState, nextState, options)) {
|
||||
return 'state change not accepted';
|
||||
} else if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
|
||||
return 'state change not accepted because fields are not in acceptable state';
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._fieldsHaveAcceptableStates(acceptedFieldStates)) {
|
||||
return 'state change not accepted because fields are not in acceptable state';
|
||||
}
|
||||
}
|
||||
|
||||
var currentIsCommitting = this.get('isCommitting');
|
||||
|
||||
@@ -3,335 +3,351 @@
|
||||
* A Backbone Model for the state of an in-place editable field in the DOM.
|
||||
*/
|
||||
|
||||
(function (_, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend(/** @lends Drupal.quickedit.FieldModel# */{
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
defaults: /** @lends Drupal.quickedit.FieldModel# */{
|
||||
|
||||
(function(_, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldModel = Drupal.quickedit.BaseModel.extend(
|
||||
/** @lends Drupal.quickedit.FieldModel# */ {
|
||||
/**
|
||||
* The DOM element that represents this field. It may seem bizarre to have
|
||||
* a DOM element in a Backbone Model, but we need to be able to map fields
|
||||
* in the DOM to FieldModels in memory.
|
||||
* @type {object}
|
||||
*/
|
||||
el: null,
|
||||
defaults: /** @lends Drupal.quickedit.FieldModel# */ {
|
||||
/**
|
||||
* The DOM element that represents this field. It may seem bizarre to have
|
||||
* a DOM element in a Backbone Model, but we need to be able to map fields
|
||||
* in the DOM to FieldModels in memory.
|
||||
*/
|
||||
el: null,
|
||||
|
||||
/**
|
||||
* A field ID, of the form
|
||||
* `<entity type>/<id>/<field name>/<language>/<view mode>`
|
||||
*
|
||||
* @example
|
||||
* "node/1/field_tags/und/full"
|
||||
*/
|
||||
fieldID: null,
|
||||
|
||||
/**
|
||||
* The unique ID of this field within its entity instance on the page, of
|
||||
* the form `<entity type>/<id>/<field name>/<language>/<view
|
||||
* mode>[entity instance ID]`.
|
||||
*
|
||||
* @example
|
||||
* "node/1/field_tags/und/full[0]"
|
||||
*/
|
||||
id: null,
|
||||
|
||||
/**
|
||||
* A {@link Drupal.quickedit.EntityModel}. Its "fields" attribute, which
|
||||
* is a FieldCollection, is automatically updated to include this
|
||||
* FieldModel.
|
||||
*/
|
||||
entity: null,
|
||||
|
||||
/**
|
||||
* This field's metadata as returned by the
|
||||
* QuickEditController::metadata().
|
||||
*/
|
||||
metadata: null,
|
||||
|
||||
/**
|
||||
* Callback function for validating changes between states. Receives the
|
||||
* previous state, new state, context, and a callback.
|
||||
*/
|
||||
acceptStateChange: null,
|
||||
|
||||
/**
|
||||
* A logical field ID, of the form
|
||||
* `<entity type>/<id>/<field name>/<language>`, i.e. the fieldID without
|
||||
* the view mode, to be able to identify other instances of the same
|
||||
* field on the page but rendered in a different view mode.
|
||||
*
|
||||
* @example
|
||||
* "node/1/field_tags/und".
|
||||
*/
|
||||
logicalFieldID: null,
|
||||
|
||||
// The attributes below are stateful. The ones above will never change
|
||||
// during the life of a FieldModel instance.
|
||||
|
||||
/**
|
||||
* In-place editing state of this field. Defaults to the initial state.
|
||||
* Possible values: {@link Drupal.quickedit.FieldModel.states}.
|
||||
*/
|
||||
state: 'inactive',
|
||||
|
||||
/**
|
||||
* The field is currently in the 'changed' state or one of the following
|
||||
* states in which the field is still changed.
|
||||
*/
|
||||
isChanged: false,
|
||||
|
||||
/**
|
||||
* Is tracked by the EntityModel, is mirrored here solely for decorative
|
||||
* purposes: so that FieldDecorationView.renderChanged() can react to it.
|
||||
*/
|
||||
inTempStore: false,
|
||||
|
||||
/**
|
||||
* The full HTML representation of this field (with the element that has
|
||||
* the data-quickedit-field-id as the outer element). Used to propagate
|
||||
* changes from this field to other instances of the same field storage.
|
||||
*/
|
||||
html: null,
|
||||
|
||||
/**
|
||||
* An object containing the full HTML representations (values) of other
|
||||
* view modes (keys) of this field, for other instances of this field
|
||||
* displayed in a different view mode.
|
||||
*/
|
||||
htmlForOtherViewModes: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* A field ID, of the form
|
||||
* `<entity type>/<id>/<field name>/<language>/<view mode>`
|
||||
* State of an in-place editable field in the DOM.
|
||||
*
|
||||
* @example
|
||||
* "node/1/field_tags/und/full"
|
||||
*/
|
||||
fieldID: null,
|
||||
|
||||
/**
|
||||
* The unique ID of this field within its entity instance on the page, of
|
||||
* the form `<entity type>/<id>/<field name>/<language>/<view
|
||||
* mode>[entity instance ID]`.
|
||||
* @constructs
|
||||
*
|
||||
* @example
|
||||
* "node/1/field_tags/und/full[0]"
|
||||
*/
|
||||
id: null,
|
||||
|
||||
/**
|
||||
* A {@link Drupal.quickedit.EntityModel}. Its "fields" attribute, which
|
||||
* is a FieldCollection, is automatically updated to include this
|
||||
* FieldModel.
|
||||
*/
|
||||
entity: null,
|
||||
|
||||
/**
|
||||
* This field's metadata as returned by the
|
||||
* QuickEditController::metadata().
|
||||
*/
|
||||
metadata: null,
|
||||
|
||||
/**
|
||||
* Callback function for validating changes between states. Receives the
|
||||
* previous state, new state, context, and a callback.
|
||||
*/
|
||||
acceptStateChange: null,
|
||||
|
||||
/**
|
||||
* A logical field ID, of the form
|
||||
* `<entity type>/<id>/<field name>/<language>`, i.e. the fieldID without
|
||||
* the view mode, to be able to identify other instances of the same
|
||||
* field on the page but rendered in a different view mode.
|
||||
* @augments Drupal.quickedit.BaseModel
|
||||
*
|
||||
* @example
|
||||
* "node/1/field_tags/und".
|
||||
* @param {object} options
|
||||
* Options for the field model.
|
||||
*/
|
||||
logicalFieldID: null,
|
||||
initialize(options) {
|
||||
// Store the original full HTML representation of this field.
|
||||
this.set('html', options.el.outerHTML);
|
||||
|
||||
// The attributes below are stateful. The ones above will never change
|
||||
// during the life of a FieldModel instance.
|
||||
// Enlist field automatically in the associated entity's field collection.
|
||||
this.get('entity')
|
||||
.get('fields')
|
||||
.add(this);
|
||||
|
||||
// Automatically generate the logical field ID.
|
||||
this.set(
|
||||
'logicalFieldID',
|
||||
this.get('fieldID')
|
||||
.split('/')
|
||||
.slice(0, 4)
|
||||
.join('/'),
|
||||
);
|
||||
|
||||
// Call Drupal.quickedit.BaseModel's initialize() method.
|
||||
Drupal.quickedit.BaseModel.prototype.initialize.call(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* In-place editing state of this field. Defaults to the initial state.
|
||||
* Possible values: {@link Drupal.quickedit.FieldModel.states}.
|
||||
* Destroys the field model.
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the field model.
|
||||
*/
|
||||
state: 'inactive',
|
||||
|
||||
/**
|
||||
* The field is currently in the 'changed' state or one of the following
|
||||
* states in which the field is still changed.
|
||||
*/
|
||||
isChanged: false,
|
||||
|
||||
/**
|
||||
* Is tracked by the EntityModel, is mirrored here solely for decorative
|
||||
* purposes: so that FieldDecorationView.renderChanged() can react to it.
|
||||
*/
|
||||
inTempStore: false,
|
||||
|
||||
/**
|
||||
* The full HTML representation of this field (with the element that has
|
||||
* the data-quickedit-field-id as the outer element). Used to propagate
|
||||
* changes from this field to other instances of the same field storage.
|
||||
*/
|
||||
html: null,
|
||||
|
||||
/**
|
||||
* An object containing the full HTML representations (values) of other
|
||||
* view modes (keys) of this field, for other instances of this field
|
||||
* displayed in a different view mode.
|
||||
*/
|
||||
htmlForOtherViewModes: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* State of an in-place editable field in the DOM.
|
||||
*
|
||||
* @constructs
|
||||
*
|
||||
* @augments Drupal.quickedit.BaseModel
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the field model.
|
||||
*/
|
||||
initialize(options) {
|
||||
// Store the original full HTML representation of this field.
|
||||
this.set('html', options.el.outerHTML);
|
||||
|
||||
// Enlist field automatically in the associated entity's field collection.
|
||||
this.get('entity').get('fields').add(this);
|
||||
|
||||
// Automatically generate the logical field ID.
|
||||
this.set('logicalFieldID', this.get('fieldID').split('/').slice(0, 4).join('/'));
|
||||
|
||||
// Call Drupal.quickedit.BaseModel's initialize() method.
|
||||
Drupal.quickedit.BaseModel.prototype.initialize.call(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Destroys the field model.
|
||||
*
|
||||
* @param {object} options
|
||||
* Options for the field model.
|
||||
*/
|
||||
destroy(options) {
|
||||
if (this.get('state') !== 'inactive') {
|
||||
throw new Error('FieldModel cannot be destroyed if it is not inactive state.');
|
||||
}
|
||||
Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
sync() {
|
||||
// We don't use REST updates to sync.
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate function for the field model.
|
||||
*
|
||||
* @param {object} attrs
|
||||
* The attributes changes in the save or set call.
|
||||
* @param {object} options
|
||||
* An object with the following option:
|
||||
* @param {string} [options.reason]
|
||||
* A string that conveys a particular reason to allow for an exceptional
|
||||
* state change.
|
||||
* @param {Array} options.accept-field-states
|
||||
* An array of strings that represent field states that the entities must
|
||||
* be in to validate. For example, if `accept-field-states` is
|
||||
* `['candidate', 'highlighted']`, then all the fields of the entity must
|
||||
* be in either of these two states for the save or set call to
|
||||
* validate and proceed.
|
||||
*
|
||||
* @return {string}
|
||||
* A string to say something about the state of the field model.
|
||||
*/
|
||||
validate(attrs, options) {
|
||||
const current = this.get('state');
|
||||
const next = attrs.state;
|
||||
if (current !== next) {
|
||||
// Ensure it's a valid state.
|
||||
if (_.indexOf(this.constructor.states, next) === -1) {
|
||||
return `"${next}" is an invalid state`;
|
||||
destroy(options) {
|
||||
if (this.get('state') !== 'inactive') {
|
||||
throw new Error(
|
||||
'FieldModel cannot be destroyed if it is not inactive state.',
|
||||
);
|
||||
}
|
||||
// Check if the acceptStateChange callback accepts it.
|
||||
if (!this.get('acceptStateChange')(current, next, options, this)) {
|
||||
return 'state change not accepted';
|
||||
}
|
||||
}
|
||||
},
|
||||
Drupal.quickedit.BaseModel.prototype.destroy.call(this, options);
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts the entity ID from this field's ID.
|
||||
*
|
||||
* @return {string}
|
||||
* An entity ID: a string of the format `<entity type>/<id>`.
|
||||
*/
|
||||
getEntityID() {
|
||||
return this.get('fieldID').split('/').slice(0, 2).join('/');
|
||||
},
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
sync() {
|
||||
// We don't use REST updates to sync.
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts the view mode ID from this field's ID.
|
||||
*
|
||||
* @return {string}
|
||||
* A view mode ID.
|
||||
*/
|
||||
getViewMode() {
|
||||
return this.get('fieldID').split('/').pop();
|
||||
},
|
||||
|
||||
/**
|
||||
* Find other instances of this field with different view modes.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array containing view mode IDs.
|
||||
*/
|
||||
findOtherViewModes() {
|
||||
const currentField = this;
|
||||
const otherViewModes = [];
|
||||
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: currentField.get('logicalFieldID') })
|
||||
.forEach((field) => {
|
||||
// Ignore the current field and other fields with the same view mode.
|
||||
if (field !== currentField && field.get('fieldID') !== currentField.get('fieldID')) {
|
||||
otherViewModes.push(field.getViewMode());
|
||||
/**
|
||||
* Validate function for the field model.
|
||||
*
|
||||
* @param {object} attrs
|
||||
* The attributes changes in the save or set call.
|
||||
* @param {object} options
|
||||
* An object with the following option:
|
||||
* @param {string} [options.reason]
|
||||
* A string that conveys a particular reason to allow for an exceptional
|
||||
* state change.
|
||||
* @param {Array} options.accept-field-states
|
||||
* An array of strings that represent field states that the entities must
|
||||
* be in to validate. For example, if `accept-field-states` is
|
||||
* `['candidate', 'highlighted']`, then all the fields of the entity must
|
||||
* be in either of these two states for the save or set call to
|
||||
* validate and proceed.
|
||||
*
|
||||
* @return {string}
|
||||
* A string to say something about the state of the field model.
|
||||
*/
|
||||
validate(attrs, options) {
|
||||
const current = this.get('state');
|
||||
const next = attrs.state;
|
||||
if (current !== next) {
|
||||
// Ensure it's a valid state.
|
||||
if (_.indexOf(this.constructor.states, next) === -1) {
|
||||
return `"${next}" is an invalid state`;
|
||||
}
|
||||
});
|
||||
return otherViewModes;
|
||||
// Check if the acceptStateChange callback accepts it.
|
||||
if (!this.get('acceptStateChange')(current, next, options, this)) {
|
||||
return 'state change not accepted';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts the entity ID from this field's ID.
|
||||
*
|
||||
* @return {string}
|
||||
* An entity ID: a string of the format `<entity type>/<id>`.
|
||||
*/
|
||||
getEntityID() {
|
||||
return this.get('fieldID')
|
||||
.split('/')
|
||||
.slice(0, 2)
|
||||
.join('/');
|
||||
},
|
||||
|
||||
/**
|
||||
* Extracts the view mode ID from this field's ID.
|
||||
*
|
||||
* @return {string}
|
||||
* A view mode ID.
|
||||
*/
|
||||
getViewMode() {
|
||||
return this.get('fieldID')
|
||||
.split('/')
|
||||
.pop();
|
||||
},
|
||||
|
||||
/**
|
||||
* Find other instances of this field with different view modes.
|
||||
*
|
||||
* @return {Array}
|
||||
* An array containing view mode IDs.
|
||||
*/
|
||||
findOtherViewModes() {
|
||||
const currentField = this;
|
||||
const otherViewModes = [];
|
||||
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: currentField.get('logicalFieldID') })
|
||||
.forEach(field => {
|
||||
// Ignore the current field and other fields with the same view mode.
|
||||
if (
|
||||
field !== currentField &&
|
||||
field.get('fieldID') !== currentField.get('fieldID')
|
||||
) {
|
||||
otherViewModes.push(field.getViewMode());
|
||||
}
|
||||
});
|
||||
return otherViewModes;
|
||||
},
|
||||
},
|
||||
/** @lends Drupal.quickedit.FieldModel */ {
|
||||
/**
|
||||
* Sequence of all possible states a field can be in during quickediting.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
states: [
|
||||
// The field associated with this FieldModel is linked to an EntityModel;
|
||||
// the user can choose to start in-place editing that entity (and
|
||||
// consequently this field). No in-place editor (EditorView) is associated
|
||||
// with this field, because this field is not being in-place edited.
|
||||
// This is both the initial (not yet in-place editing) and the end state
|
||||
// (finished in-place editing).
|
||||
'inactive',
|
||||
// The user is in-place editing this entity, and this field is a
|
||||
// candidate
|
||||
// for in-place editing. In-place editor should not
|
||||
// - Trigger: user.
|
||||
// - Guarantees: entity is ready, in-place editor (EditorView) is
|
||||
// associated with the field.
|
||||
// - Expected behavior: visual indicators
|
||||
// around the field indicate it is available for in-place editing, no
|
||||
// in-place editor presented yet.
|
||||
'candidate',
|
||||
// User is highlighting this field.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate'.
|
||||
// - Expected behavior: visual indicators to convey highlighting, in-place
|
||||
// editing toolbar shows field's label.
|
||||
'highlighted',
|
||||
// User has activated the in-place editing of this field; in-place editor
|
||||
// is activating.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate'.
|
||||
// - Expected behavior: loading indicator, in-place editor is loading
|
||||
// remote data (e.g. retrieve form from back-end). Upon retrieval of
|
||||
// remote data, the in-place editor transitions the field's state to
|
||||
// 'active'.
|
||||
'activating',
|
||||
// In-place editor has finished loading remote data; ready for use.
|
||||
// - Trigger: in-place editor.
|
||||
// - Guarantees: see 'candidate'.
|
||||
// - Expected behavior: in-place editor for the field is ready for use.
|
||||
'active',
|
||||
// User has modified values in the in-place editor.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate', plus in-place editor is ready for use.
|
||||
// - Expected behavior: visual indicator of change.
|
||||
'changed',
|
||||
// User is saving changed field data in in-place editor to
|
||||
// PrivateTempStore. The save mechanism of the in-place editor is called.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate' and 'active'.
|
||||
// - Expected behavior: saving indicator, in-place editor is saving field
|
||||
// data into PrivateTempStore. Upon successful saving (without
|
||||
// validation errors), the in-place editor transitions the field's state
|
||||
// to 'saved', but to 'invalid' upon failed saving (with validation
|
||||
// errors).
|
||||
'saving',
|
||||
// In-place editor has successfully saved the changed field.
|
||||
// - Trigger: in-place editor.
|
||||
// - Guarantees: see 'candidate' and 'active'.
|
||||
// - Expected behavior: transition back to 'candidate' state because the
|
||||
// deed is done. Then: 1) transition to 'inactive' to allow the field
|
||||
// to be rerendered, 2) destroy the FieldModel (which also destroys
|
||||
// attached views like the EditorView), 3) replace the existing field
|
||||
// HTML with the existing HTML and 4) attach behaviors again so that the
|
||||
// field becomes available again for in-place editing.
|
||||
'saved',
|
||||
// In-place editor has failed to saved the changed field: there were
|
||||
// validation errors.
|
||||
// - Trigger: in-place editor.
|
||||
// - Guarantees: see 'candidate' and 'active'.
|
||||
// - Expected behavior: remain in 'invalid' state, let the user make more
|
||||
// changes so that he can save it again, without validation errors.
|
||||
'invalid',
|
||||
],
|
||||
|
||||
}, /** @lends Drupal.quickedit.FieldModel */{
|
||||
|
||||
/**
|
||||
* Sequence of all possible states a field can be in during quickediting.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
states: [
|
||||
// The field associated with this FieldModel is linked to an EntityModel;
|
||||
// the user can choose to start in-place editing that entity (and
|
||||
// consequently this field). No in-place editor (EditorView) is associated
|
||||
// with this field, because this field is not being in-place edited.
|
||||
// This is both the initial (not yet in-place editing) and the end state
|
||||
// (finished in-place editing).
|
||||
'inactive',
|
||||
// The user is in-place editing this entity, and this field is a
|
||||
// candidate
|
||||
// for in-place editing. In-place editor should not
|
||||
// - Trigger: user.
|
||||
// - Guarantees: entity is ready, in-place editor (EditorView) is
|
||||
// associated with the field.
|
||||
// - Expected behavior: visual indicators
|
||||
// around the field indicate it is available for in-place editing, no
|
||||
// in-place editor presented yet.
|
||||
'candidate',
|
||||
// User is highlighting this field.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate'.
|
||||
// - Expected behavior: visual indicators to convey highlighting, in-place
|
||||
// editing toolbar shows field's label.
|
||||
'highlighted',
|
||||
// User has activated the in-place editing of this field; in-place editor
|
||||
// is activating.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate'.
|
||||
// - Expected behavior: loading indicator, in-place editor is loading
|
||||
// remote data (e.g. retrieve form from back-end). Upon retrieval of
|
||||
// remote data, the in-place editor transitions the field's state to
|
||||
// 'active'.
|
||||
'activating',
|
||||
// In-place editor has finished loading remote data; ready for use.
|
||||
// - Trigger: in-place editor.
|
||||
// - Guarantees: see 'candidate'.
|
||||
// - Expected behavior: in-place editor for the field is ready for use.
|
||||
'active',
|
||||
// User has modified values in the in-place editor.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate', plus in-place editor is ready for use.
|
||||
// - Expected behavior: visual indicator of change.
|
||||
'changed',
|
||||
// User is saving changed field data in in-place editor to
|
||||
// PrivateTempStore. The save mechanism of the in-place editor is called.
|
||||
// - Trigger: user.
|
||||
// - Guarantees: see 'candidate' and 'active'.
|
||||
// - Expected behavior: saving indicator, in-place editor is saving field
|
||||
// data into PrivateTempStore. Upon successful saving (without
|
||||
// validation errors), the in-place editor transitions the field's state
|
||||
// to 'saved', but to 'invalid' upon failed saving (with validation
|
||||
// errors).
|
||||
'saving',
|
||||
// In-place editor has successfully saved the changed field.
|
||||
// - Trigger: in-place editor.
|
||||
// - Guarantees: see 'candidate' and 'active'.
|
||||
// - Expected behavior: transition back to 'candidate' state because the
|
||||
// deed is done. Then: 1) transition to 'inactive' to allow the field
|
||||
// to be rerendered, 2) destroy the FieldModel (which also destroys
|
||||
// attached views like the EditorView), 3) replace the existing field
|
||||
// HTML with the existing HTML and 4) attach behaviors again so that the
|
||||
// field becomes available again for in-place editing.
|
||||
'saved',
|
||||
// In-place editor has failed to saved the changed field: there were
|
||||
// validation errors.
|
||||
// - Trigger: in-place editor.
|
||||
// - Guarantees: see 'candidate' and 'active'.
|
||||
// - Expected behavior: remain in 'invalid' state, let the user make more
|
||||
// changes so that he can save it again, without validation errors.
|
||||
'invalid',
|
||||
],
|
||||
|
||||
/**
|
||||
* Indicates whether the 'from' state comes before the 'to' state.
|
||||
*
|
||||
* @param {string} from
|
||||
* One of {@link Drupal.quickedit.FieldModel.states}.
|
||||
* @param {string} to
|
||||
* One of {@link Drupal.quickedit.FieldModel.states}.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the 'from' state comes before the 'to' state.
|
||||
*/
|
||||
followsStateSequence(from, to) {
|
||||
return _.indexOf(this.states, from) < _.indexOf(this.states, to);
|
||||
/**
|
||||
* Indicates whether the 'from' state comes before the 'to' state.
|
||||
*
|
||||
* @param {string} from
|
||||
* One of {@link Drupal.quickedit.FieldModel.states}.
|
||||
* @param {string} to
|
||||
* One of {@link Drupal.quickedit.FieldModel.states}.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the 'from' state comes before the 'to' state.
|
||||
*/
|
||||
followsStateSequence(from, to) {
|
||||
return _.indexOf(this.states, from) < _.indexOf(this.states, to);
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
);
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @augments Backbone.Collection
|
||||
*/
|
||||
Drupal.quickedit.FieldCollection = Backbone.Collection.extend(/** @lends Drupal.quickedit.FieldCollection */{
|
||||
|
||||
/**
|
||||
* @type {Drupal.quickedit.FieldModel}
|
||||
*/
|
||||
model: Drupal.quickedit.FieldModel,
|
||||
});
|
||||
}(_, Backbone, Drupal));
|
||||
Drupal.quickedit.FieldCollection = Backbone.Collection.extend(
|
||||
/** @lends Drupal.quickedit.FieldCollection */ {
|
||||
/**
|
||||
* @type {Drupal.quickedit.FieldModel}
|
||||
*/
|
||||
model: Drupal.quickedit.FieldModel,
|
||||
},
|
||||
);
|
||||
})(_, Backbone, Drupal);
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
* is not yet known whether the user has permission to edit at >=1 of them.
|
||||
*/
|
||||
|
||||
(function ($, _, Backbone, Drupal, drupalSettings, JSON, storage) {
|
||||
const options = $.extend(drupalSettings.quickedit,
|
||||
(function($, _, Backbone, Drupal, drupalSettings, JSON, storage) {
|
||||
const options = $.extend(
|
||||
drupalSettings.quickedit,
|
||||
// Merge strings on top of drupalSettings so that they are not mutable.
|
||||
{
|
||||
strings: {
|
||||
@@ -60,249 +61,6 @@
|
||||
*/
|
||||
const entityInstancesTracker = {};
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.quickedit = {
|
||||
attach(context) {
|
||||
// Initialize the Quick Edit app once per page load.
|
||||
$('body').once('quickedit-init').each(initQuickEdit);
|
||||
|
||||
// Find all in-place editable fields, if any.
|
||||
const $fields = $(context).find('[data-quickedit-field-id]').once('quickedit');
|
||||
if ($fields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each entity element: identical entities that appear multiple
|
||||
// times will get a numeric identifier, starting at 0.
|
||||
$(context).find('[data-quickedit-entity-id]').once('quickedit').each((index, entityElement) => {
|
||||
processEntity(entityElement);
|
||||
});
|
||||
|
||||
// Process each field element: queue to be used or to fetch metadata.
|
||||
// When a field is being rerendered after editing, it will be processed
|
||||
// immediately. New fields will be unable to be processed immediately,
|
||||
// but will instead be queued to have their metadata fetched, which occurs
|
||||
// below in fetchMissingMetaData().
|
||||
$fields.each((index, fieldElement) => {
|
||||
processField(fieldElement);
|
||||
});
|
||||
|
||||
// Entities and fields on the page have been detected, try to set up the
|
||||
// contextual links for those entities that already have the necessary
|
||||
// meta- data in the client-side cache.
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, contextualLink => !initializeEntityContextualLink(contextualLink));
|
||||
|
||||
// Fetch metadata for any fields that are queued to retrieve it.
|
||||
fetchMissingMetadata((fieldElementsWithFreshMetadata) => {
|
||||
// Metadata has been fetched, reprocess fields whose metadata was
|
||||
// missing.
|
||||
_.each(fieldElementsWithFreshMetadata, processField);
|
||||
|
||||
// Metadata has been fetched, try to set up more contextual links now.
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, contextualLink => !initializeEntityContextualLink(contextualLink));
|
||||
});
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
deleteContainedModelsAndQueues($(context));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.quickedit = {
|
||||
|
||||
/**
|
||||
* A {@link Drupal.quickedit.AppView} instance.
|
||||
*/
|
||||
app: null,
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array.<Drupal.quickedit.EntityModel>} entities
|
||||
* @prop {Array.<Drupal.quickedit.FieldModel>} fields
|
||||
*/
|
||||
collections: {
|
||||
// All in-place editable entities (Drupal.quickedit.EntityModel) on the
|
||||
// page.
|
||||
entities: null,
|
||||
// All in-place editable fields (Drupal.quickedit.FieldModel) on the page.
|
||||
fields: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* In-place editors will register themselves in this object.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
editors: {},
|
||||
|
||||
/**
|
||||
* Per-field metadata that indicates whether in-place editing is allowed,
|
||||
* which in-place editor should be used, etc.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
metadata: {
|
||||
|
||||
/**
|
||||
* Check if a field exists in storage.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field id to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether it was found or not.
|
||||
*/
|
||||
has(fieldID) {
|
||||
return storage.getItem(this._prefixFieldID(fieldID)) !== null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add metadata to a field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field ID to add data to.
|
||||
* @param {object} metadata
|
||||
* Metadata to add.
|
||||
*/
|
||||
add(fieldID, metadata) {
|
||||
storage.setItem(this._prefixFieldID(fieldID), JSON.stringify(metadata));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a key from a field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field ID to check.
|
||||
* @param {string} [key]
|
||||
* The key to check. If empty, will return all metadata.
|
||||
*
|
||||
* @return {object|*}
|
||||
* The value for the key, if defined. Otherwise will return all metadata
|
||||
* for the specified field id.
|
||||
*
|
||||
*/
|
||||
get(fieldID, key) {
|
||||
const metadata = JSON.parse(storage.getItem(this._prefixFieldID(fieldID)));
|
||||
return (typeof key === 'undefined') ? metadata : metadata[key];
|
||||
},
|
||||
|
||||
/**
|
||||
* Prefix the field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field id to prefix.
|
||||
*
|
||||
* @return {string}
|
||||
* A prefixed field id.
|
||||
*/
|
||||
_prefixFieldID(fieldID) {
|
||||
return `Drupal.quickedit.metadata.${fieldID}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unprefix the field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field id to unprefix.
|
||||
*
|
||||
* @return {string}
|
||||
* An unprefixed field id.
|
||||
*/
|
||||
_unprefixFieldID(fieldID) {
|
||||
// Strip "Drupal.quickedit.metadata.", which is 26 characters long.
|
||||
return fieldID.substring(26);
|
||||
},
|
||||
|
||||
/**
|
||||
* Intersection calculation.
|
||||
*
|
||||
* @param {Array} fieldIDs
|
||||
* An array of field ids to compare to prefix field id.
|
||||
*
|
||||
* @return {Array}
|
||||
* The intersection found.
|
||||
*/
|
||||
intersection(fieldIDs) {
|
||||
const prefixedFieldIDs = _.map(fieldIDs, this._prefixFieldID);
|
||||
const intersection = _.intersection(prefixedFieldIDs, _.keys(sessionStorage));
|
||||
return _.map(intersection, this._unprefixFieldID);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Clear the Quick Edit metadata cache whenever the current user's set of
|
||||
// permissions changes.
|
||||
const permissionsHashKey = Drupal.quickedit.metadata._prefixFieldID('permissionsHash');
|
||||
const permissionsHashValue = storage.getItem(permissionsHashKey);
|
||||
const permissionsHash = drupalSettings.user.permissionsHash;
|
||||
if (permissionsHashValue !== permissionsHash) {
|
||||
if (typeof permissionsHash === 'string') {
|
||||
_.chain(storage).keys().each((key) => {
|
||||
if (key.substring(0, 26) === 'Drupal.quickedit.metadata.') {
|
||||
storage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
storage.setItem(permissionsHashKey, permissionsHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect contextual links on entities annotated by quickedit.
|
||||
*
|
||||
* Queue contextual links to be processed.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The `drupalContextualLinkAdded` event.
|
||||
* @param {object} data
|
||||
* An object containing the data relevant to the event.
|
||||
*
|
||||
* @listens event:drupalContextualLinkAdded
|
||||
*/
|
||||
$(document).on('drupalContextualLinkAdded', (event, data) => {
|
||||
if (data.$region.is('[data-quickedit-entity-id]')) {
|
||||
// If the contextual link is cached on the client side, an entity instance
|
||||
// will not yet have been assigned. So assign one.
|
||||
if (!data.$region.is('[data-quickedit-entity-instance-id]')) {
|
||||
data.$region.once('quickedit');
|
||||
processEntity(data.$region.get(0));
|
||||
}
|
||||
const contextualLink = {
|
||||
entityID: data.$region.attr('data-quickedit-entity-id'),
|
||||
entityInstanceID: data.$region.attr('data-quickedit-entity-instance-id'),
|
||||
el: data.$el[0],
|
||||
region: data.$region[0],
|
||||
};
|
||||
// Set up contextual links for this, otherwise queue it to be set up
|
||||
// later.
|
||||
if (!initializeEntityContextualLink(contextualLink)) {
|
||||
contextualLinksQueue.push(contextualLink);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Extracts the entity ID from a field ID.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* A field ID: a string of the format
|
||||
* `<entity type>/<id>/<field name>/<language>/<view mode>`.
|
||||
*
|
||||
* @return {string}
|
||||
* An entity ID: a string of the format `<entity type>/<id>`.
|
||||
*/
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Quick Edit app.
|
||||
*
|
||||
@@ -334,77 +92,16 @@
|
||||
const entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
// Set the calculated entity instance ID for this element.
|
||||
const entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the field's metadata; queue or initialize it (if EntityModel exists).
|
||||
*
|
||||
* @param {HTMLElement} fieldElement
|
||||
* A Drupal Field API field's DOM element with a data-quickedit-field-id
|
||||
* attribute.
|
||||
*/
|
||||
function processField(fieldElement) {
|
||||
const metadata = Drupal.quickedit.metadata;
|
||||
const fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
const entityID = extractEntityID(fieldID);
|
||||
// Figure out the instance ID by looking at the ancestor
|
||||
// [data-quickedit-entity-id] element's data-quickedit-entity-instance-id
|
||||
// attribute.
|
||||
const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`;
|
||||
const $entityElement = $(entityElementSelector);
|
||||
|
||||
// If there are no elements returned from `entityElementSelector`
|
||||
// throw an error. Check the browser console for this message.
|
||||
if (!$entityElement.length) {
|
||||
throw new Error(`Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="${fieldID}"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="${entityID}"]. This is typically caused by the theme's template for this entity type forgetting to print the attributes.`);
|
||||
}
|
||||
let entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
// In the case of a full entity view page, the entity title is rendered
|
||||
// outside of "the entity DOM node": it's rendered as the page title. So in
|
||||
// this case, we find the lowest common parent element (deepest in the tree)
|
||||
// and consider that the entity element.
|
||||
if (entityElement.length === 0) {
|
||||
const $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
const entityInstanceID = entityElement
|
||||
.get(0)
|
||||
.getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
// Early-return if metadata for this field is missing.
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Early-return if the user is not allowed to in-place edit this field.
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an EntityModel for this field already exists (and hence also a "Quick
|
||||
// edit" contextual link), then initialize it immediately.
|
||||
if (Drupal.quickedit.collections.entities.findWhere({ entityID, entityInstanceID })) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
}
|
||||
// Otherwise: queue the field. It is now available to be set up when its
|
||||
// corresponding entity becomes in-place editable.
|
||||
else {
|
||||
fieldsAvailableQueue.push({ el: fieldElement, fieldID, entityID, entityInstanceID });
|
||||
}
|
||||
entityElement.setAttribute(
|
||||
'data-quickedit-entity-instance-id',
|
||||
entityInstanceID,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,51 +131,16 @@
|
||||
id: `${fieldID}[${entity.get('entityInstanceID')}]`,
|
||||
entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app),
|
||||
acceptStateChange: _.bind(
|
||||
Drupal.quickedit.app.acceptEditorStateChange,
|
||||
Drupal.quickedit.app,
|
||||
),
|
||||
});
|
||||
|
||||
// Track all fields on the page.
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches metadata for fields whose metadata is missing.
|
||||
*
|
||||
* Fields whose metadata is missing are tracked at fieldsMetadataQueue.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that receives field elements whose metadata will just
|
||||
* have been fetched.
|
||||
*/
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
// Ensure we only request entityIDs for which we don't have metadata yet.
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
// Store the metadata.
|
||||
_.each(results, (fieldMetadata, fieldID) => {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads missing in-place editor's attachments (JavaScript and CSS files).
|
||||
*
|
||||
@@ -492,7 +154,7 @@
|
||||
function loadMissingEditors(callback) {
|
||||
const loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
let missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each((fieldModel) => {
|
||||
Drupal.quickedit.collections.fields.each(fieldModel => {
|
||||
const metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
@@ -519,7 +181,7 @@
|
||||
// Implement a scoped insert AJAX command: calls the callback after all AJAX
|
||||
// command functions have been executed (hence the deferred calling).
|
||||
const realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
loadEditorsAjax.commands.insert = function(ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
@@ -580,7 +242,7 @@
|
||||
// The entity for the given contextual link contains at least one field that
|
||||
// the current user may edit in-place; instantiate EntityModel,
|
||||
// EntityDecorationView and ContextualLinkView.
|
||||
else if (hasFieldWithPermission(fieldIDs)) {
|
||||
if (hasFieldWithPermission(fieldIDs)) {
|
||||
const entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
@@ -598,8 +260,13 @@
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
// Initialize all queued fields within this entity (creates FieldModels).
|
||||
_.each(fields, (field) => {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
_.each(fields, field => {
|
||||
initializeField(
|
||||
field.el,
|
||||
field.fieldID,
|
||||
contextualLink.entityID,
|
||||
contextualLink.entityInstanceID,
|
||||
);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
@@ -607,11 +274,18 @@
|
||||
// to get a one-time use version of the function.
|
||||
const initContextualLink = _.once(() => {
|
||||
const $links = $(contextualLink.el).find('.contextual-links');
|
||||
const contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model,
|
||||
}, options));
|
||||
const contextualLinkView = new Drupal.quickedit.ContextualLinkView(
|
||||
$.extend(
|
||||
{
|
||||
el: $(
|
||||
'<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>',
|
||||
).prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model,
|
||||
},
|
||||
options,
|
||||
),
|
||||
);
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
@@ -622,13 +296,108 @@
|
||||
}
|
||||
// There was not at least one field that the current user may edit in-place,
|
||||
// even though the metadata for all fields within this entity is available.
|
||||
else if (allMetadataExists(fieldIDs)) {
|
||||
if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the entity ID from a field ID.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* A field ID: a string of the format
|
||||
* `<entity type>/<id>/<field name>/<language>/<view mode>`.
|
||||
*
|
||||
* @return {string}
|
||||
* An entity ID: a string of the format `<entity type>/<id>`.
|
||||
*/
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID
|
||||
.split('/')
|
||||
.slice(0, 2)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the field's metadata; queue or initialize it (if EntityModel exists).
|
||||
*
|
||||
* @param {HTMLElement} fieldElement
|
||||
* A Drupal Field API field's DOM element with a data-quickedit-field-id
|
||||
* attribute.
|
||||
*/
|
||||
function processField(fieldElement) {
|
||||
const metadata = Drupal.quickedit.metadata;
|
||||
const fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
const entityID = extractEntityID(fieldID);
|
||||
// Figure out the instance ID by looking at the ancestor
|
||||
// [data-quickedit-entity-id] element's data-quickedit-entity-instance-id
|
||||
// attribute.
|
||||
const entityElementSelector = `[data-quickedit-entity-id="${entityID}"]`;
|
||||
const $entityElement = $(entityElementSelector);
|
||||
|
||||
// If there are no elements returned from `entityElementSelector`
|
||||
// throw an error. Check the browser console for this message.
|
||||
if (!$entityElement.length) {
|
||||
throw new Error(
|
||||
`Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="${fieldID}"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="${entityID}"]. This is typically caused by the theme's template for this entity type forgetting to print the attributes.`,
|
||||
);
|
||||
}
|
||||
let entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
// In the case of a full entity view page, the entity title is rendered
|
||||
// outside of "the entity DOM node": it's rendered as the page title. So in
|
||||
// this case, we find the lowest common parent element (deepest in the tree)
|
||||
// and consider that the entity element.
|
||||
if (entityElement.length === 0) {
|
||||
const $lowestCommonParent = $entityElement
|
||||
.parents()
|
||||
.has(fieldElement)
|
||||
.first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
const entityInstanceID = entityElement
|
||||
.get(0)
|
||||
.getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
// Early-return if metadata for this field is missing.
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Early-return if the user is not allowed to in-place edit this field.
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an EntityModel for this field already exists (and hence also a "Quick
|
||||
// edit" contextual link), then initialize it immediately.
|
||||
if (
|
||||
Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
})
|
||||
) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
}
|
||||
// Otherwise: queue the field. It is now available to be set up when its
|
||||
// corresponding entity becomes in-place editable.
|
||||
else {
|
||||
fieldsAvailableQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID,
|
||||
entityID,
|
||||
entityInstanceID,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete models and queue items that are contained within a given context.
|
||||
*
|
||||
@@ -647,40 +416,355 @@
|
||||
* The context within which to delete.
|
||||
*/
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each((index, entityElement) => {
|
||||
// Delete entity model.
|
||||
const entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement });
|
||||
if (entityModel) {
|
||||
const contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
// Remove the EntityDecorationView.
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
// Destroy the EntityModel; this will also destroy its FieldModels.
|
||||
entityModel.destroy();
|
||||
}
|
||||
$context
|
||||
.find('[data-quickedit-entity-id]')
|
||||
.addBack('[data-quickedit-entity-id]')
|
||||
.each((index, entityElement) => {
|
||||
// Delete entity model.
|
||||
const entityModel = Drupal.quickedit.collections.entities.findWhere({
|
||||
el: entityElement,
|
||||
});
|
||||
if (entityModel) {
|
||||
const contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
// Remove the EntityDecorationView.
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
// Destroy the EntityModel; this will also destroy its FieldModels.
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
// Filter queue.
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
// Filter queue.
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each((index, fieldElement) => {
|
||||
// Delete field models.
|
||||
Drupal.quickedit.collections.fields.chain()
|
||||
.filter(fieldModel => fieldModel.get('el') === fieldElement)
|
||||
.invoke('destroy');
|
||||
$context
|
||||
.find('[data-quickedit-field-id]')
|
||||
.addBack('[data-quickedit-field-id]')
|
||||
.each((index, fieldElement) => {
|
||||
// Delete field models.
|
||||
Drupal.quickedit.collections.fields
|
||||
.chain()
|
||||
.filter(fieldModel => fieldModel.get('el') === fieldElement)
|
||||
.invoke('destroy');
|
||||
|
||||
// Filter queues.
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
// Filter queues.
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
fieldsMetadataQueue = _.filter(
|
||||
fieldsMetadataQueue,
|
||||
hasOtherFieldElement,
|
||||
);
|
||||
fieldsAvailableQueue = _.filter(
|
||||
fieldsAvailableQueue,
|
||||
hasOtherFieldElement,
|
||||
);
|
||||
});
|
||||
}
|
||||
}(jQuery, _, Backbone, Drupal, drupalSettings, window.JSON, window.sessionStorage));
|
||||
|
||||
/**
|
||||
* Fetches metadata for fields whose metadata is missing.
|
||||
*
|
||||
* Fields whose metadata is missing are tracked at fieldsMetadataQueue.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that receives field elements whose metadata will just
|
||||
* have been fetched.
|
||||
*/
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
const fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
const fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
let entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
// Ensure we only request entityIDs for which we don't have metadata yet.
|
||||
entityIDs = _.difference(
|
||||
entityIDs,
|
||||
Drupal.quickedit.metadata.intersection(entityIDs),
|
||||
);
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs,
|
||||
},
|
||||
dataType: 'json',
|
||||
success(results) {
|
||||
// Store the metadata.
|
||||
_.each(results, (fieldMetadata, fieldID) => {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.quickedit = {
|
||||
attach(context) {
|
||||
// Initialize the Quick Edit app once per page load.
|
||||
$('body')
|
||||
.once('quickedit-init')
|
||||
.each(initQuickEdit);
|
||||
|
||||
// Find all in-place editable fields, if any.
|
||||
const $fields = $(context)
|
||||
.find('[data-quickedit-field-id]')
|
||||
.once('quickedit');
|
||||
if ($fields.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each entity element: identical entities that appear multiple
|
||||
// times will get a numeric identifier, starting at 0.
|
||||
$(context)
|
||||
.find('[data-quickedit-entity-id]')
|
||||
.once('quickedit')
|
||||
.each((index, entityElement) => {
|
||||
processEntity(entityElement);
|
||||
});
|
||||
|
||||
// Process each field element: queue to be used or to fetch metadata.
|
||||
// When a field is being rerendered after editing, it will be processed
|
||||
// immediately. New fields will be unable to be processed immediately,
|
||||
// but will instead be queued to have their metadata fetched, which occurs
|
||||
// below in fetchMissingMetaData().
|
||||
$fields.each((index, fieldElement) => {
|
||||
processField(fieldElement);
|
||||
});
|
||||
|
||||
// Entities and fields on the page have been detected, try to set up the
|
||||
// contextual links for those entities that already have the necessary
|
||||
// meta- data in the client-side cache.
|
||||
contextualLinksQueue = _.filter(
|
||||
contextualLinksQueue,
|
||||
contextualLink => !initializeEntityContextualLink(contextualLink),
|
||||
);
|
||||
|
||||
// Fetch metadata for any fields that are queued to retrieve it.
|
||||
fetchMissingMetadata(fieldElementsWithFreshMetadata => {
|
||||
// Metadata has been fetched, reprocess fields whose metadata was
|
||||
// missing.
|
||||
_.each(fieldElementsWithFreshMetadata, processField);
|
||||
|
||||
// Metadata has been fetched, try to set up more contextual links now.
|
||||
contextualLinksQueue = _.filter(
|
||||
contextualLinksQueue,
|
||||
contextualLink => !initializeEntityContextualLink(contextualLink),
|
||||
);
|
||||
});
|
||||
},
|
||||
detach(context, settings, trigger) {
|
||||
if (trigger === 'unload') {
|
||||
deleteContainedModelsAndQueues($(context));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.quickedit = {
|
||||
/**
|
||||
* A {@link Drupal.quickedit.AppView} instance.
|
||||
*/
|
||||
app: null,
|
||||
|
||||
/**
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array.<Drupal.quickedit.EntityModel>} entities
|
||||
* @prop {Array.<Drupal.quickedit.FieldModel>} fields
|
||||
*/
|
||||
collections: {
|
||||
// All in-place editable entities (Drupal.quickedit.EntityModel) on the
|
||||
// page.
|
||||
entities: null,
|
||||
// All in-place editable fields (Drupal.quickedit.FieldModel) on the page.
|
||||
fields: null,
|
||||
},
|
||||
|
||||
/**
|
||||
* In-place editors will register themselves in this object.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
editors: {},
|
||||
|
||||
/**
|
||||
* Per-field metadata that indicates whether in-place editing is allowed,
|
||||
* which in-place editor should be used, etc.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
metadata: {
|
||||
/**
|
||||
* Check if a field exists in storage.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field id to check.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether it was found or not.
|
||||
*/
|
||||
has(fieldID) {
|
||||
return storage.getItem(this._prefixFieldID(fieldID)) !== null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add metadata to a field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field ID to add data to.
|
||||
* @param {object} metadata
|
||||
* Metadata to add.
|
||||
*/
|
||||
add(fieldID, metadata) {
|
||||
storage.setItem(this._prefixFieldID(fieldID), JSON.stringify(metadata));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a key from a field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field ID to check.
|
||||
* @param {string} [key]
|
||||
* The key to check. If empty, will return all metadata.
|
||||
*
|
||||
* @return {object|*}
|
||||
* The value for the key, if defined. Otherwise will return all metadata
|
||||
* for the specified field id.
|
||||
*
|
||||
*/
|
||||
get(fieldID, key) {
|
||||
const metadata = JSON.parse(
|
||||
storage.getItem(this._prefixFieldID(fieldID)),
|
||||
);
|
||||
return typeof key === 'undefined' ? metadata : metadata[key];
|
||||
},
|
||||
|
||||
/**
|
||||
* Prefix the field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field id to prefix.
|
||||
*
|
||||
* @return {string}
|
||||
* A prefixed field id.
|
||||
*/
|
||||
_prefixFieldID(fieldID) {
|
||||
return `Drupal.quickedit.metadata.${fieldID}`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Unprefix the field id.
|
||||
*
|
||||
* @param {string} fieldID
|
||||
* The field id to unprefix.
|
||||
*
|
||||
* @return {string}
|
||||
* An unprefixed field id.
|
||||
*/
|
||||
_unprefixFieldID(fieldID) {
|
||||
// Strip "Drupal.quickedit.metadata.", which is 26 characters long.
|
||||
return fieldID.substring(26);
|
||||
},
|
||||
|
||||
/**
|
||||
* Intersection calculation.
|
||||
*
|
||||
* @param {Array} fieldIDs
|
||||
* An array of field ids to compare to prefix field id.
|
||||
*
|
||||
* @return {Array}
|
||||
* The intersection found.
|
||||
*/
|
||||
intersection(fieldIDs) {
|
||||
const prefixedFieldIDs = _.map(fieldIDs, this._prefixFieldID);
|
||||
const intersection = _.intersection(
|
||||
prefixedFieldIDs,
|
||||
_.keys(sessionStorage),
|
||||
);
|
||||
return _.map(intersection, this._unprefixFieldID);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Clear the Quick Edit metadata cache whenever the current user's set of
|
||||
// permissions changes.
|
||||
const permissionsHashKey = Drupal.quickedit.metadata._prefixFieldID(
|
||||
'permissionsHash',
|
||||
);
|
||||
const permissionsHashValue = storage.getItem(permissionsHashKey);
|
||||
const permissionsHash = drupalSettings.user.permissionsHash;
|
||||
if (permissionsHashValue !== permissionsHash) {
|
||||
if (typeof permissionsHash === 'string') {
|
||||
_.chain(storage)
|
||||
.keys()
|
||||
.each(key => {
|
||||
if (key.substring(0, 26) === 'Drupal.quickedit.metadata.') {
|
||||
storage.removeItem(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
storage.setItem(permissionsHashKey, permissionsHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect contextual links on entities annotated by quickedit.
|
||||
*
|
||||
* Queue contextual links to be processed.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The `drupalContextualLinkAdded` event.
|
||||
* @param {object} data
|
||||
* An object containing the data relevant to the event.
|
||||
*
|
||||
* @listens event:drupalContextualLinkAdded
|
||||
*/
|
||||
$(document).on('drupalContextualLinkAdded', (event, data) => {
|
||||
if (data.$region.is('[data-quickedit-entity-id]')) {
|
||||
// If the contextual link is cached on the client side, an entity instance
|
||||
// will not yet have been assigned. So assign one.
|
||||
if (!data.$region.is('[data-quickedit-entity-instance-id]')) {
|
||||
data.$region.once('quickedit');
|
||||
processEntity(data.$region.get(0));
|
||||
}
|
||||
const contextualLink = {
|
||||
entityID: data.$region.attr('data-quickedit-entity-id'),
|
||||
entityInstanceID: data.$region.attr(
|
||||
'data-quickedit-entity-instance-id',
|
||||
),
|
||||
el: data.$el[0],
|
||||
region: data.$region[0],
|
||||
};
|
||||
// Set up contextual links for this, otherwise queue it to be set up
|
||||
// later.
|
||||
if (!initializeEntityContextualLink(contextualLink)) {
|
||||
contextualLinksQueue.push(contextualLink);
|
||||
}
|
||||
}
|
||||
});
|
||||
})(
|
||||
jQuery,
|
||||
_,
|
||||
Backbone,
|
||||
Drupal,
|
||||
drupalSettings,
|
||||
window.JSON,
|
||||
window.sessionStorage,
|
||||
);
|
||||
|
||||
@@ -20,6 +20,267 @@
|
||||
|
||||
var entityInstancesTracker = {};
|
||||
|
||||
function initQuickEdit(bodyElement) {
|
||||
Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
|
||||
Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
|
||||
|
||||
Drupal.quickedit.app = new Drupal.quickedit.AppView({
|
||||
el: bodyElement,
|
||||
model: new Drupal.quickedit.AppModel(),
|
||||
entitiesCollection: Drupal.quickedit.collections.entities,
|
||||
fieldsCollection: Drupal.quickedit.collections.fields
|
||||
});
|
||||
}
|
||||
|
||||
function processEntity(entityElement) {
|
||||
var entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
} else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
var entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
|
||||
var entity = Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
|
||||
$(fieldElement).addClass('quickedit-field');
|
||||
|
||||
var field = new Drupal.quickedit.FieldModel({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
id: fieldID + '[' + entity.get('entityInstanceID') + ']',
|
||||
entity: entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app)
|
||||
});
|
||||
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
function loadMissingEditors(callback) {
|
||||
var loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
var missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each(function (fieldModel) {
|
||||
var metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
|
||||
Drupal.quickedit.editors[metadata.editor] = false;
|
||||
}
|
||||
});
|
||||
missingEditors = _.uniq(missingEditors);
|
||||
if (missingEditors.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var loadEditorsAjax = Drupal.ajax({
|
||||
url: Drupal.url('quickedit/attachments'),
|
||||
submit: { 'editors[]': missingEditors }
|
||||
});
|
||||
|
||||
var realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
|
||||
loadEditorsAjax.execute();
|
||||
}
|
||||
|
||||
function initializeEntityContextualLink(contextualLink) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
|
||||
function hasFieldWithPermission(fieldIDs) {
|
||||
for (var i = 0; i < fieldIDs.length; i++) {
|
||||
var fieldID = fieldIDs[i];
|
||||
if (metadata.get(fieldID, 'access') === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function allMetadataExists(fieldIDs) {
|
||||
return fieldIDs.length === metadata.intersection(fieldIDs).length;
|
||||
}
|
||||
|
||||
var fields = _.where(fieldsAvailableQueue, {
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID
|
||||
});
|
||||
var fieldIDs = _.pluck(fields, 'fieldID');
|
||||
|
||||
if (fieldIDs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasFieldWithPermission(fieldIDs)) {
|
||||
var entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']',
|
||||
label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label')
|
||||
});
|
||||
Drupal.quickedit.collections.entities.add(entityModel);
|
||||
|
||||
var entityDecorationView = new Drupal.quickedit.EntityDecorationView({
|
||||
el: contextualLink.region,
|
||||
model: entityModel
|
||||
});
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
_.each(fields, function (field) {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
var initContextualLink = _.once(function () {
|
||||
var $links = $(contextualLink.el).find('.contextual-links');
|
||||
var contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model
|
||||
}, options));
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
loadMissingEditors(initContextualLink);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
function processField(fieldElement) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
var fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
var entityID = extractEntityID(fieldID);
|
||||
|
||||
var entityElementSelector = '[data-quickedit-entity-id="' + entityID + '"]';
|
||||
var $entityElement = $(entityElementSelector);
|
||||
|
||||
if (!$entityElement.length) {
|
||||
throw new Error('Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="' + fieldID + '"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="' + entityID + '"]. This is typically caused by the theme\'s template for this entity type forgetting to print the attributes.');
|
||||
}
|
||||
var entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
if (entityElement.length === 0) {
|
||||
var $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
var entityInstanceID = entityElement.get(0).getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
})) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
} else {
|
||||
fieldsAvailableQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each(function (index, entityElement) {
|
||||
var entityModel = Drupal.quickedit.collections.entities.findWhere({
|
||||
el: entityElement
|
||||
});
|
||||
if (entityModel) {
|
||||
var contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each(function (index, fieldElement) {
|
||||
Drupal.quickedit.collections.fields.chain().filter(function (fieldModel) {
|
||||
return fieldModel.get('el') === fieldElement;
|
||||
}).invoke('destroy');
|
||||
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
}
|
||||
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
var fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
var entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(results) {
|
||||
_.each(results, function (fieldMetadata, fieldID) {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Drupal.behaviors.quickedit = {
|
||||
attach: function attach(context) {
|
||||
$('body').once('quickedit-init').each(initQuickEdit);
|
||||
@@ -124,251 +385,4 @@
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function extractEntityID(fieldID) {
|
||||
return fieldID.split('/').slice(0, 2).join('/');
|
||||
}
|
||||
|
||||
function initQuickEdit(bodyElement) {
|
||||
Drupal.quickedit.collections.entities = new Drupal.quickedit.EntityCollection();
|
||||
Drupal.quickedit.collections.fields = new Drupal.quickedit.FieldCollection();
|
||||
|
||||
Drupal.quickedit.app = new Drupal.quickedit.AppView({
|
||||
el: bodyElement,
|
||||
model: new Drupal.quickedit.AppModel(),
|
||||
entitiesCollection: Drupal.quickedit.collections.entities,
|
||||
fieldsCollection: Drupal.quickedit.collections.fields
|
||||
});
|
||||
}
|
||||
|
||||
function processEntity(entityElement) {
|
||||
var entityID = entityElement.getAttribute('data-quickedit-entity-id');
|
||||
if (!entityInstancesTracker.hasOwnProperty(entityID)) {
|
||||
entityInstancesTracker[entityID] = 0;
|
||||
} else {
|
||||
entityInstancesTracker[entityID]++;
|
||||
}
|
||||
|
||||
var entityInstanceID = entityInstancesTracker[entityID];
|
||||
entityElement.setAttribute('data-quickedit-entity-instance-id', entityInstanceID);
|
||||
}
|
||||
|
||||
function processField(fieldElement) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
var fieldID = fieldElement.getAttribute('data-quickedit-field-id');
|
||||
var entityID = extractEntityID(fieldID);
|
||||
|
||||
var entityElementSelector = '[data-quickedit-entity-id="' + entityID + '"]';
|
||||
var $entityElement = $(entityElementSelector);
|
||||
|
||||
if (!$entityElement.length) {
|
||||
throw new Error('Quick Edit could not associate the rendered entity field markup (with [data-quickedit-field-id="' + fieldID + '"]) with the corresponding rendered entity markup: no parent DOM node found with [data-quickedit-entity-id="' + entityID + '"]. This is typically caused by the theme\'s template for this entity type forgetting to print the attributes.');
|
||||
}
|
||||
var entityElement = $(fieldElement).closest($entityElement);
|
||||
|
||||
if (entityElement.length === 0) {
|
||||
var $lowestCommonParent = $entityElement.parents().has(fieldElement).first();
|
||||
entityElement = $lowestCommonParent.find($entityElement);
|
||||
}
|
||||
var entityInstanceID = entityElement.get(0).getAttribute('data-quickedit-entity-instance-id');
|
||||
|
||||
if (!metadata.has(fieldID)) {
|
||||
fieldsMetadataQueue.push({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (metadata.get(fieldID, 'access') !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Drupal.quickedit.collections.entities.findWhere({ entityID: entityID, entityInstanceID: entityInstanceID })) {
|
||||
initializeField(fieldElement, fieldID, entityID, entityInstanceID);
|
||||
} else {
|
||||
fieldsAvailableQueue.push({ el: fieldElement, fieldID: fieldID, entityID: entityID, entityInstanceID: entityInstanceID });
|
||||
}
|
||||
}
|
||||
|
||||
function initializeField(fieldElement, fieldID, entityID, entityInstanceID) {
|
||||
var entity = Drupal.quickedit.collections.entities.findWhere({
|
||||
entityID: entityID,
|
||||
entityInstanceID: entityInstanceID
|
||||
});
|
||||
|
||||
$(fieldElement).addClass('quickedit-field');
|
||||
|
||||
var field = new Drupal.quickedit.FieldModel({
|
||||
el: fieldElement,
|
||||
fieldID: fieldID,
|
||||
id: fieldID + '[' + entity.get('entityInstanceID') + ']',
|
||||
entity: entity,
|
||||
metadata: Drupal.quickedit.metadata.get(fieldID),
|
||||
acceptStateChange: _.bind(Drupal.quickedit.app.acceptEditorStateChange, Drupal.quickedit.app)
|
||||
});
|
||||
|
||||
Drupal.quickedit.collections.fields.add(field);
|
||||
}
|
||||
|
||||
function fetchMissingMetadata(callback) {
|
||||
if (fieldsMetadataQueue.length) {
|
||||
var fieldIDs = _.pluck(fieldsMetadataQueue, 'fieldID');
|
||||
var fieldElementsWithoutMetadata = _.pluck(fieldsMetadataQueue, 'el');
|
||||
var entityIDs = _.uniq(_.pluck(fieldsMetadataQueue, 'entityID'), true);
|
||||
|
||||
entityIDs = _.difference(entityIDs, Drupal.quickedit.metadata.intersection(entityIDs));
|
||||
fieldsMetadataQueue = [];
|
||||
|
||||
$.ajax({
|
||||
url: Drupal.url('quickedit/metadata'),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'fields[]': fieldIDs,
|
||||
'entities[]': entityIDs
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function success(results) {
|
||||
_.each(results, function (fieldMetadata, fieldID) {
|
||||
Drupal.quickedit.metadata.add(fieldID, fieldMetadata);
|
||||
});
|
||||
|
||||
callback(fieldElementsWithoutMetadata);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function loadMissingEditors(callback) {
|
||||
var loadedEditors = _.keys(Drupal.quickedit.editors);
|
||||
var missingEditors = [];
|
||||
Drupal.quickedit.collections.fields.each(function (fieldModel) {
|
||||
var metadata = Drupal.quickedit.metadata.get(fieldModel.get('fieldID'));
|
||||
if (metadata.access && _.indexOf(loadedEditors, metadata.editor) === -1) {
|
||||
missingEditors.push(metadata.editor);
|
||||
|
||||
Drupal.quickedit.editors[metadata.editor] = false;
|
||||
}
|
||||
});
|
||||
missingEditors = _.uniq(missingEditors);
|
||||
if (missingEditors.length === 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
var loadEditorsAjax = Drupal.ajax({
|
||||
url: Drupal.url('quickedit/attachments'),
|
||||
submit: { 'editors[]': missingEditors }
|
||||
});
|
||||
|
||||
var realInsert = Drupal.AjaxCommands.prototype.insert;
|
||||
loadEditorsAjax.commands.insert = function (ajax, response, status) {
|
||||
_.defer(callback);
|
||||
realInsert(ajax, response, status);
|
||||
};
|
||||
|
||||
loadEditorsAjax.execute();
|
||||
}
|
||||
|
||||
function initializeEntityContextualLink(contextualLink) {
|
||||
var metadata = Drupal.quickedit.metadata;
|
||||
|
||||
function hasFieldWithPermission(fieldIDs) {
|
||||
for (var i = 0; i < fieldIDs.length; i++) {
|
||||
var fieldID = fieldIDs[i];
|
||||
if (metadata.get(fieldID, 'access') === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function allMetadataExists(fieldIDs) {
|
||||
return fieldIDs.length === metadata.intersection(fieldIDs).length;
|
||||
}
|
||||
|
||||
var fields = _.where(fieldsAvailableQueue, {
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID
|
||||
});
|
||||
var fieldIDs = _.pluck(fields, 'fieldID');
|
||||
|
||||
if (fieldIDs.length === 0) {
|
||||
return false;
|
||||
} else if (hasFieldWithPermission(fieldIDs)) {
|
||||
var entityModel = new Drupal.quickedit.EntityModel({
|
||||
el: contextualLink.region,
|
||||
entityID: contextualLink.entityID,
|
||||
entityInstanceID: contextualLink.entityInstanceID,
|
||||
id: contextualLink.entityID + '[' + contextualLink.entityInstanceID + ']',
|
||||
label: Drupal.quickedit.metadata.get(contextualLink.entityID, 'label')
|
||||
});
|
||||
Drupal.quickedit.collections.entities.add(entityModel);
|
||||
|
||||
var entityDecorationView = new Drupal.quickedit.EntityDecorationView({
|
||||
el: contextualLink.region,
|
||||
model: entityModel
|
||||
});
|
||||
entityModel.set('entityDecorationView', entityDecorationView);
|
||||
|
||||
_.each(fields, function (field) {
|
||||
initializeField(field.el, field.fieldID, contextualLink.entityID, contextualLink.entityInstanceID);
|
||||
});
|
||||
fieldsAvailableQueue = _.difference(fieldsAvailableQueue, fields);
|
||||
|
||||
var initContextualLink = _.once(function () {
|
||||
var $links = $(contextualLink.el).find('.contextual-links');
|
||||
var contextualLinkView = new Drupal.quickedit.ContextualLinkView($.extend({
|
||||
el: $('<li class="quickedit"><a href="" role="button" aria-pressed="false"></a></li>').prependTo($links),
|
||||
model: entityModel,
|
||||
appModel: Drupal.quickedit.app.model
|
||||
}, options));
|
||||
entityModel.set('contextualLinkView', contextualLinkView);
|
||||
});
|
||||
|
||||
loadMissingEditors(initContextualLink);
|
||||
|
||||
return true;
|
||||
} else if (allMetadataExists(fieldIDs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function deleteContainedModelsAndQueues($context) {
|
||||
$context.find('[data-quickedit-entity-id]').addBack('[data-quickedit-entity-id]').each(function (index, entityElement) {
|
||||
var entityModel = Drupal.quickedit.collections.entities.findWhere({ el: entityElement });
|
||||
if (entityModel) {
|
||||
var contextualLinkView = entityModel.get('contextualLinkView');
|
||||
contextualLinkView.undelegateEvents();
|
||||
contextualLinkView.remove();
|
||||
|
||||
entityModel.get('entityDecorationView').remove();
|
||||
|
||||
entityModel.destroy();
|
||||
}
|
||||
|
||||
function hasOtherRegion(contextualLink) {
|
||||
return contextualLink.region !== entityElement;
|
||||
}
|
||||
|
||||
contextualLinksQueue = _.filter(contextualLinksQueue, hasOtherRegion);
|
||||
});
|
||||
|
||||
$context.find('[data-quickedit-field-id]').addBack('[data-quickedit-field-id]').each(function (index, fieldElement) {
|
||||
Drupal.quickedit.collections.fields.chain().filter(function (fieldModel) {
|
||||
return fieldModel.get('el') === fieldElement;
|
||||
}).invoke('destroy');
|
||||
|
||||
function hasOtherFieldElement(field) {
|
||||
return field.el !== fieldElement;
|
||||
}
|
||||
|
||||
fieldsMetadataQueue = _.filter(fieldsMetadataQueue, hasOtherFieldElement);
|
||||
fieldsAvailableQueue = _.filter(fieldsAvailableQueue, hasOtherFieldElement);
|
||||
});
|
||||
}
|
||||
})(jQuery, _, Backbone, Drupal, drupalSettings, window.JSON, window.sessionStorage);
|
||||
@@ -3,7 +3,7 @@
|
||||
* Provides theme functions for all of Quick Edit's client-side HTML.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
(function($, Drupal) {
|
||||
/**
|
||||
* Theme function for a "backstage" for the Quick Edit module.
|
||||
*
|
||||
@@ -15,7 +15,7 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditBackstage = function (settings) {
|
||||
Drupal.theme.quickeditBackstage = function(settings) {
|
||||
let html = '';
|
||||
html += `<div id="${settings.id}" />`;
|
||||
return html;
|
||||
@@ -32,15 +32,19 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditEntityToolbar = function (settings) {
|
||||
Drupal.theme.quickeditEntityToolbar = function(settings) {
|
||||
let html = '';
|
||||
html += `<div id="${settings.id}" class="quickedit quickedit-toolbar-container clearfix">`;
|
||||
html += `<div id="${
|
||||
settings.id
|
||||
}" class="quickedit quickedit-toolbar-container clearfix">`;
|
||||
html += '<i class="quickedit-toolbar-pointer"></i>';
|
||||
html += '<div class="quickedit-toolbar-content">';
|
||||
html += '<div class="quickedit-toolbar quickedit-toolbar-entity clearfix icon icon-pencil">';
|
||||
html +=
|
||||
'<div class="quickedit-toolbar quickedit-toolbar-entity clearfix icon icon-pencil">';
|
||||
html += '<div class="quickedit-toolbar-label" />';
|
||||
html += '</div>';
|
||||
html += '<div class="quickedit-toolbar quickedit-toolbar-field clearfix" />';
|
||||
html +=
|
||||
'<div class="quickedit-toolbar quickedit-toolbar-field clearfix" />';
|
||||
html += '</div><div class="quickedit-toolbar-lining"></div></div>';
|
||||
return html;
|
||||
};
|
||||
@@ -58,9 +62,11 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditEntityToolbarLabel = function (settings) {
|
||||
Drupal.theme.quickeditEntityToolbarLabel = function(settings) {
|
||||
// @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437
|
||||
return `<span class="field">${Drupal.checkPlain(settings.fieldLabel)}</span>${Drupal.checkPlain(settings.entityLabel)}`;
|
||||
return `<span class="field">${Drupal.checkPlain(
|
||||
settings.fieldLabel,
|
||||
)}</span>${Drupal.checkPlain(settings.entityLabel)}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,7 +75,7 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditEntityToolbarFence = function () {
|
||||
Drupal.theme.quickeditEntityToolbarFence = function() {
|
||||
return '<div id="quickedit-toolbar-fence" />';
|
||||
};
|
||||
|
||||
@@ -84,7 +90,7 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditFieldToolbar = function (settings) {
|
||||
Drupal.theme.quickeditFieldToolbar = function(settings) {
|
||||
return `<div id="${settings.id}" />`;
|
||||
};
|
||||
|
||||
@@ -103,9 +109,9 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditToolgroup = function (settings) {
|
||||
Drupal.theme.quickeditToolgroup = function(settings) {
|
||||
// Classes.
|
||||
const classes = (settings.classes || []);
|
||||
const classes = settings.classes || [];
|
||||
classes.unshift('quickedit-toolgroup');
|
||||
let html = '';
|
||||
html += `<div class="${classes.join(' ')}"`;
|
||||
@@ -134,7 +140,7 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditButtons = function (settings) {
|
||||
Drupal.theme.quickeditButtons = function(settings) {
|
||||
let html = '';
|
||||
for (let i = 0; i < settings.buttons.length; i++) {
|
||||
const button = settings.buttons[i];
|
||||
@@ -144,10 +150,12 @@
|
||||
// Attributes.
|
||||
const attributes = [];
|
||||
const attrMap = settings.buttons[i].attributes || {};
|
||||
Object.keys(attrMap).forEach((attr) => {
|
||||
attributes.push(attr + ((attrMap[attr]) ? `="${attrMap[attr]}"` : ''));
|
||||
Object.keys(attrMap).forEach(attr => {
|
||||
attributes.push(attr + (attrMap[attr] ? `="${attrMap[attr]}"` : ''));
|
||||
});
|
||||
html += `<button type="${button.type}" class="${button.classes}" ${attributes.join(' ')}>${button.label}</button>`;
|
||||
html += `<button type="${button.type}" class="${
|
||||
button.classes
|
||||
}" ${attributes.join(' ')}>${button.label}</button>`;
|
||||
}
|
||||
return html;
|
||||
};
|
||||
@@ -165,7 +173,7 @@
|
||||
* @return {string}
|
||||
* The corresponding HTML.
|
||||
*/
|
||||
Drupal.theme.quickeditFormContainer = function (settings) {
|
||||
Drupal.theme.quickeditFormContainer = function(settings) {
|
||||
let html = '';
|
||||
html += `<div id="${settings.id}" class="quickedit-form-container">`;
|
||||
html += ' <div class="quickedit-form">';
|
||||
@@ -176,4 +184,4 @@
|
||||
html += '</div>';
|
||||
return html;
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
})(jQuery, Drupal);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Provides utility functions for Quick Edit.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
(function($, Drupal) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
@@ -18,7 +18,8 @@
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
Drupal.quickedit.util.constants.transitionEnd = 'transitionEnd.quickedit webkitTransitionEnd.quickedit transitionend.quickedit msTransitionEnd.quickedit oTransitionEnd.quickedit';
|
||||
Drupal.quickedit.util.constants.transitionEnd =
|
||||
'transitionEnd.quickedit webkitTransitionEnd.quickedit transitionend.quickedit msTransitionEnd.quickedit oTransitionEnd.quickedit';
|
||||
|
||||
/**
|
||||
* Converts a field id into a formatted url path.
|
||||
@@ -37,7 +38,7 @@
|
||||
* @return {string}
|
||||
* The formatted URL.
|
||||
*/
|
||||
Drupal.quickedit.util.buildUrl = function (id, urlFormat) {
|
||||
Drupal.quickedit.util.buildUrl = function(id, urlFormat) {
|
||||
const parts = id.split('/');
|
||||
return Drupal.formatString(decodeURIComponent(urlFormat), {
|
||||
'!entity_type': parts[0],
|
||||
@@ -56,7 +57,7 @@
|
||||
* @param {string} message
|
||||
* The message to use in the modal dialog.
|
||||
*/
|
||||
Drupal.quickedit.util.networkErrorModal = function (title, message) {
|
||||
Drupal.quickedit.util.networkErrorModal = function(title, message) {
|
||||
const $message = $(`<div>${message}</div>`);
|
||||
const networkErrorModal = Drupal.dialog($message.get(0), {
|
||||
title,
|
||||
@@ -71,7 +72,10 @@
|
||||
},
|
||||
],
|
||||
create() {
|
||||
$(this).parent().find('.ui-dialog-titlebar-close').remove();
|
||||
$(this)
|
||||
.parent()
|
||||
.find('.ui-dialog-titlebar-close')
|
||||
.remove();
|
||||
},
|
||||
close(event) {
|
||||
// Automatically destroy the DOM element that was used for the dialog.
|
||||
@@ -85,7 +89,6 @@
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.quickedit.util.form = {
|
||||
|
||||
/**
|
||||
* Loads a form, calls a callback to insert.
|
||||
*
|
||||
@@ -113,7 +116,12 @@
|
||||
|
||||
// Create a Drupal.ajax instance to load the form.
|
||||
const formLoaderAjax = Drupal.ajax({
|
||||
url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/form/!entity_type/!id/!field_name/!langcode/!view_mode')),
|
||||
url: Drupal.quickedit.util.buildUrl(
|
||||
fieldID,
|
||||
Drupal.url(
|
||||
'quickedit/form/!entity_type/!id/!field_name/!langcode/!view_mode',
|
||||
),
|
||||
),
|
||||
submit: {
|
||||
nocssjs: options.nocssjs,
|
||||
reset: options.reset,
|
||||
@@ -121,8 +129,14 @@
|
||||
error(xhr, url) {
|
||||
// Show a modal to inform the user of the network error.
|
||||
const fieldLabel = Drupal.quickedit.metadata.get(fieldID, 'label');
|
||||
const message = Drupal.t('Could not load the form for <q>@field-label</q>, either due to a website problem or a network connection problem.<br>Please try again.', { '@field-label': fieldLabel });
|
||||
Drupal.quickedit.util.networkErrorModal(Drupal.t('Network problem!'), message);
|
||||
const message = Drupal.t(
|
||||
'Could not load the form for <q>@field-label</q>, either due to a website problem or a network connection problem.<br>Please try again.',
|
||||
{ '@field-label': fieldLabel },
|
||||
);
|
||||
Drupal.quickedit.util.networkErrorModal(
|
||||
Drupal.t('Network problem!'),
|
||||
message,
|
||||
);
|
||||
|
||||
// Change the state back to "candidate", to allow the user to start
|
||||
// in-place editing of the field again.
|
||||
@@ -131,7 +145,11 @@
|
||||
},
|
||||
});
|
||||
// Implement a scoped quickeditFieldForm AJAX command: calls the callback.
|
||||
formLoaderAjax.commands.quickeditFieldForm = function (ajax, response, status) {
|
||||
formLoaderAjax.commands.quickeditFieldForm = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
callback(response.data, ajax);
|
||||
Drupal.ajax.instances[this.instanceIndex] = null;
|
||||
};
|
||||
@@ -181,7 +199,7 @@
|
||||
* The HTTP status code.
|
||||
*/
|
||||
success(response, status) {
|
||||
Object.keys(response || {}).forEach((i) => {
|
||||
Object.keys(response || {}).forEach(i => {
|
||||
if (response[i].command && this.commands[response[i].command]) {
|
||||
this.commands[response[i].command](this, response[i], status);
|
||||
}
|
||||
@@ -204,6 +222,5 @@
|
||||
unajaxifySaving(ajax) {
|
||||
$(ajax.element).off('click.quickedit');
|
||||
},
|
||||
|
||||
};
|
||||
}(jQuery, Drupal));
|
||||
})(jQuery, Drupal);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -63,7 +63,7 @@
|
||||
|
||||
if (reload) {
|
||||
reload = false;
|
||||
location.reload();
|
||||
window.location.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -287,12 +287,16 @@
|
||||
if (field === updatedField) {} else if (field.getViewMode() === updatedField.getViewMode()) {
|
||||
field.set('html', updatedField.get('html'));
|
||||
} else if (field.getViewMode() in htmlForOtherViewModes) {
|
||||
field.set('html', htmlForOtherViewModes[field.getViewMode()], { propagation: true });
|
||||
field.set('html', htmlForOtherViewModes[field.getViewMode()], {
|
||||
propagation: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
rerenderedFieldToCandidate: function rerenderedFieldToCandidate(fieldModel) {
|
||||
var activeEntity = Drupal.quickedit.collections.entities.findWhere({ isActive: true });
|
||||
var activeEntity = Drupal.quickedit.collections.entities.findWhere({
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!activeEntity) {
|
||||
return;
|
||||
|
||||
@@ -3,75 +3,75 @@
|
||||
* 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) {
|
||||
(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();
|
||||
this.model.set('state', 'launching');
|
||||
},
|
||||
'touchEnd a': touchEndToClick,
|
||||
};
|
||||
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;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 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));
|
||||
);
|
||||
})(jQuery, Backbone, Drupal);
|
||||
|
||||
@@ -3,299 +3,323 @@
|
||||
* An abstract Backbone View that controls an in-place editor.
|
||||
*/
|
||||
|
||||
(function ($, Backbone, Drupal) {
|
||||
Drupal.quickedit.EditorView = Backbone.View.extend(/** @lends Drupal.quickedit.EditorView# */{
|
||||
(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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
/**
|
||||
* @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;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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 '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();
|
||||
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;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
// Nothing to do for the typical in-place editor: it should not be
|
||||
// visible yet.
|
||||
break;
|
||||
case 'active':
|
||||
// The user can now actually use the in-place editor.
|
||||
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();
|
||||
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);
|
||||
};
|
||||
loadDependencies(() => {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'active':
|
||||
// The user can now actually use the in-place editor.
|
||||
break;
|
||||
// Unsuccessfully saved; validation errors.
|
||||
self.formSaveAjax.commands.quickeditFieldFormValidationErrors = function(
|
||||
ajax,
|
||||
response,
|
||||
status,
|
||||
) {
|
||||
removeHiddenForm();
|
||||
editorModel.set('validationErrors', response.data);
|
||||
fieldModel.set('state', 'invalid');
|
||||
};
|
||||
|
||||
case 'changed':
|
||||
// Nothing to do for the typical in-place editor. The UI will show an
|
||||
// indicator that the field has changed.
|
||||
break;
|
||||
// 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() {};
|
||||
|
||||
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;
|
||||
fillAndSubmitForm(editorModel.get('currentValue'));
|
||||
});
|
||||
},
|
||||
|
||||
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;
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
|
||||
case 'invalid':
|
||||
// The modified field value was attempted to be saved, but there were
|
||||
// validation errors.
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
/**
|
||||
* 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();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 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));
|
||||
);
|
||||
})(jQuery, Backbone, Drupal);
|
||||
|
||||
@@ -19,7 +19,12 @@
|
||||
return this.$el;
|
||||
},
|
||||
getQuickEditUISettings: function getQuickEditUISettings() {
|
||||
return { padding: false, unifiedToolbar: false, fullWidthToolbar: false, popup: false };
|
||||
return {
|
||||
padding: false,
|
||||
unifiedToolbar: false,
|
||||
fullWidthToolbar: false,
|
||||
popup: false
|
||||
};
|
||||
},
|
||||
stateChange: function stateChange(fieldModel, state) {
|
||||
var from = fieldModel.previous('state');
|
||||
|
||||
@@ -3,34 +3,37 @@
|
||||
* A Backbone view that decorates the in-place editable entity.
|
||||
*/
|
||||
|
||||
(function (Drupal, $, Backbone) {
|
||||
Drupal.quickedit.EntityDecorationView = Backbone.View.extend(/** @lends Drupal.quickedit.EntityDecorationView# */{
|
||||
(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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* @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));
|
||||
);
|
||||
})(Drupal, jQuery, Backbone);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,293 +3,299 @@
|
||||
* A Backbone View that decorates the in-place edited element.
|
||||
*/
|
||||
|
||||
(function ($, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldDecorationView = Backbone.View.extend(/** @lends Drupal.quickedit.FieldDecorationView# */{
|
||||
(function($, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldDecorationView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.FieldDecorationView# */ {
|
||||
/**
|
||||
* @type {null}
|
||||
*/
|
||||
_widthAttributeIsEmpty: null,
|
||||
|
||||
/**
|
||||
* @type {null}
|
||||
*/
|
||||
_widthAttributeIsEmpty: null,
|
||||
/**
|
||||
* @type {object}
|
||||
*/
|
||||
events: {
|
||||
'mouseenter.quickedit': 'onMouseEnter',
|
||||
'mouseleave.quickedit': 'onMouseLeave',
|
||||
click: 'onClick',
|
||||
'tabIn.quickedit': 'onMouseEnter',
|
||||
'tabOut.quickedit': 'onMouseLeave',
|
||||
},
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* @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,
|
||||
);
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
case 'candidate':
|
||||
this.decorate();
|
||||
if (from !== 'inactive') {
|
||||
this.stopHighlight();
|
||||
if (from !== 'highlighted') {
|
||||
this.model.set('isChanged', false);
|
||||
this.stopEdit();
|
||||
}
|
||||
}
|
||||
}
|
||||
this._unpad();
|
||||
break;
|
||||
this._unpad();
|
||||
break;
|
||||
|
||||
case 'highlighted':
|
||||
this.startHighlight();
|
||||
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') {
|
||||
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();
|
||||
}
|
||||
if (this.editorView.getQuickEditUISettings().padding) {
|
||||
this._pad();
|
||||
}
|
||||
break;
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
this.model.set('isChanged', true);
|
||||
break;
|
||||
case 'active':
|
||||
if (from !== 'activating') {
|
||||
this.prepareEdit();
|
||||
}
|
||||
if (this.editorView.getQuickEditUISettings().padding) {
|
||||
this._pad();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
break;
|
||||
case 'changed':
|
||||
this.model.set('isChanged', true);
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
case 'saving':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
break;
|
||||
}
|
||||
},
|
||||
case 'saved':
|
||||
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'));
|
||||
},
|
||||
case 'invalid':
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 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();
|
||||
},
|
||||
/**
|
||||
* 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'),
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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();
|
||||
},
|
||||
/**
|
||||
* 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();
|
||||
},
|
||||
|
||||
/**
|
||||
* Transition to 'activating' stage.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClick(event) {
|
||||
this.model.set('state', 'activating');
|
||||
event.preventDefault();
|
||||
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();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds classes used to indicate an elements editable state.
|
||||
*/
|
||||
decorate() {
|
||||
this.$el.addClass('quickedit-candidate quickedit-editable');
|
||||
},
|
||||
/**
|
||||
* Transition to 'activating' stage.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
* The click event.
|
||||
*/
|
||||
onClick(event) {
|
||||
this.model.set('state', 'activating');
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes classes used to indicate an elements editable state.
|
||||
*/
|
||||
undecorate() {
|
||||
this.$el.removeClass('quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing');
|
||||
},
|
||||
/**
|
||||
* Adds classes used to indicate an elements editable state.
|
||||
*/
|
||||
decorate() {
|
||||
this.$el.addClass('quickedit-candidate quickedit-editable');
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 classes used to indicate an elements editable state.
|
||||
*/
|
||||
undecorate() {
|
||||
this.$el.removeClass(
|
||||
'quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing',
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the class that indicates that an element is highlighted.
|
||||
*/
|
||||
stopHighlight() {
|
||||
this.$el.removeClass('quickedit-highlighted');
|
||||
},
|
||||
/**
|
||||
* 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 as editable.
|
||||
*/
|
||||
prepareEdit() {
|
||||
this.$el.addClass('quickedit-editing');
|
||||
/**
|
||||
* Removes the class that indicates that an element is highlighted.
|
||||
*/
|
||||
stopHighlight() {
|
||||
this.$el.removeClass('quickedit-highlighted');
|
||||
},
|
||||
|
||||
// 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 as editable.
|
||||
*/
|
||||
prepareEdit() {
|
||||
this.$el.addClass('quickedit-editing');
|
||||
|
||||
/**
|
||||
* 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');
|
||||
// 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');
|
||||
}
|
||||
},
|
||||
|
||||
// Done editing in a pop-up in-place editor; remove the class.
|
||||
if (this.editorView.getQuickEditUISettings().popup) {
|
||||
this.$el.removeClass('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');
|
||||
|
||||
// Make the other editors show up again.
|
||||
$('.quickedit-candidate').addClass('quickedit-editable');
|
||||
},
|
||||
// Done editing in a pop-up in-place editor; remove the class.
|
||||
if (this.editorView.getQuickEditUISettings().popup) {
|
||||
this.$el.removeClass('quickedit-editor-is-popup');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// Make the other editors show up again.
|
||||
$('.quickedit-candidate').addClass('quickedit-editable');
|
||||
},
|
||||
|
||||
// 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());
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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');
|
||||
// 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());
|
||||
}
|
||||
|
||||
// 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);
|
||||
},
|
||||
// 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');
|
||||
|
||||
/**
|
||||
* 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;
|
||||
// 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);
|
||||
},
|
||||
|
||||
// 1) Set the empty width again.
|
||||
if (this._widthAttributeIsEmpty) {
|
||||
this.$el
|
||||
.addClass('quickedit-animate-disable-width')
|
||||
.css('width', '');
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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');
|
||||
// 1) Set the empty width again.
|
||||
if (this._widthAttributeIsEmpty) {
|
||||
this.$el.addClass('quickedit-animate-disable-width').css('width', '');
|
||||
}
|
||||
|
||||
// Unpad the editable.
|
||||
self.$el
|
||||
.css({
|
||||
// 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`,
|
||||
@@ -299,58 +305,64 @@
|
||||
'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');
|
||||
}, 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;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 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));
|
||||
);
|
||||
})(jQuery, Backbone, Drupal);
|
||||
|
||||
@@ -3,221 +3,242 @@
|
||||
* 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;
|
||||
|
||||
(function($, _, Backbone, Drupal) {
|
||||
Drupal.quickedit.FieldToolbarView = Backbone.View.extend(
|
||||
/** @lends Drupal.quickedit.FieldToolbarView# */ {
|
||||
/**
|
||||
* The edited element, as indicated by EditorView.getEditedElement.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
this.$root = this.$el;
|
||||
$editedElement: null,
|
||||
|
||||
// Generate a DOM-compatible ID for the form container DOM element.
|
||||
this._id = `quickedit-toolbar-for-${this.model.id.replace(/[/[\]]/g, '_')}`;
|
||||
/**
|
||||
* A reference to the in-place editor.
|
||||
*
|
||||
* @type {Drupal.quickedit.EditorView}
|
||||
*/
|
||||
editorView: null,
|
||||
|
||||
this.listenTo(this.model, 'change:state', this.stateChange);
|
||||
/**
|
||||
* @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);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* @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));
|
||||
);
|
||||
})(jQuery, _, Backbone, Drupal);
|
||||
|
||||
@@ -5,6 +5,6 @@ package: Core
|
||||
core: 8.x
|
||||
version: VERSION
|
||||
dependencies:
|
||||
- contextual
|
||||
- field
|
||||
- filter
|
||||
- drupal:contextual
|
||||
- drupal:field
|
||||
- drupal:filter
|
||||
|
||||
@@ -22,7 +22,7 @@ class EditorSelector implements EditorSelectorInterface {
|
||||
/**
|
||||
* The manager for formatter plugins.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FormatterPluginManager.
|
||||
* @var \Drupal\Core\Field\FormatterPluginManager
|
||||
*/
|
||||
protected $formatterManager;
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ class QuickEditController extends ControllerBase {
|
||||
$errors = $form_state->getErrors();
|
||||
if (count($errors)) {
|
||||
$status_messages = [
|
||||
'#type' => 'status_messages'
|
||||
'#type' => 'status_messages',
|
||||
];
|
||||
$response->addCommand(new FieldFormValidationErrorsCommand($this->renderer->renderRoot($status_messages)));
|
||||
}
|
||||
@@ -293,7 +293,7 @@ class QuickEditController extends ControllerBase {
|
||||
// to identify it.
|
||||
$output = [
|
||||
'entity_type' => $entity->getEntityTypeId(),
|
||||
'entity_id' => $entity->id()
|
||||
'entity_id' => $entity->id(),
|
||||
];
|
||||
|
||||
// Respond to client that the entity was saved properly.
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace Drupal\quickedit\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
@@ -87,7 +86,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
0 => [
|
||||
'value' => '<p>How are you?</p>',
|
||||
'format' => 'filtered_html',
|
||||
]
|
||||
],
|
||||
],
|
||||
'revision_log' => $this->randomString(),
|
||||
]);
|
||||
@@ -188,7 +187,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
'label' => 'Body',
|
||||
'access' => TRUE,
|
||||
'editor' => 'form',
|
||||
]
|
||||
],
|
||||
];
|
||||
$this->assertIdentical(Json::decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
|
||||
// Restore drupalSettings to build the next requests; simpletest wipes them
|
||||
@@ -216,7 +215,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
$ajax_commands = Json::decode($response);
|
||||
$this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
|
||||
$this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
|
||||
$this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
$this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
|
||||
// Prepare form values for submission. drupalPostAjaxForm() is not suitable
|
||||
// for handling pages with JSON responses, so we need our own solution here.
|
||||
@@ -286,7 +285,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
$ajax_commands = Json::decode($response);
|
||||
$this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
|
||||
$this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
|
||||
$this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
$this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
|
||||
// Submit field form.
|
||||
preg_match('/\sname="form_token" value="([^"]+)"/', $ajax_commands[0]['data'], $token_match);
|
||||
@@ -374,7 +373,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
'label' => 'Title',
|
||||
'access' => TRUE,
|
||||
'editor' => 'plain_text',
|
||||
]
|
||||
],
|
||||
];
|
||||
$this->assertIdentical(Json::decode($response), $expected, 'The metadata HTTP request answers with the correct JSON response.');
|
||||
// Restore drupalSettings to build the next requests; simpletest wipes them
|
||||
@@ -389,7 +388,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
$ajax_commands = Json::decode($response);
|
||||
$this->assertIdentical(1, count($ajax_commands), 'The field form HTTP request results in one AJAX command.');
|
||||
$this->assertIdentical('quickeditFieldForm', $ajax_commands[0]['command'], 'The first AJAX command is a quickeditFieldForm command.');
|
||||
$this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
$this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
|
||||
// Prepare form values for submission. drupalPostAjaxForm() is not suitable
|
||||
// for handling pages with JSON responses, so we need our own solution
|
||||
@@ -602,7 +601,7 @@ class QuickEditLoadingTest extends WebTestBase {
|
||||
$response = $this->drupalPost('quickedit/form/node/1/field_image/en/full', '', ['nocssjs' => 'true'] + $this->getAjaxPageStatePostData(), ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
|
||||
$this->assertResponse(200);
|
||||
$ajax_commands = Json::decode($response);
|
||||
$this->assertIdentical('<form ', Unicode::substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
$this->assertIdentical('<form ', mb_substr($ajax_commands[0]['data'], 0, 6), 'The quickeditFieldForm command contains a form.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\quickedit\FunctionalJavascript;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\contextual\FunctionalJavascript\ContextualLinkClickTrait;
|
||||
|
||||
/**
|
||||
* Tests quickedit.
|
||||
*
|
||||
* @group quickedit
|
||||
*/
|
||||
class FieldTest extends WebDriverTestBase {
|
||||
|
||||
use ContextualLinkClickTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'ckeditor',
|
||||
'contextual',
|
||||
'quickedit',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a text format and associate CKEditor.
|
||||
$filtered_html_format = FilterFormat::create([
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
]);
|
||||
$filtered_html_format->save();
|
||||
|
||||
Editor::create([
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'ckeditor',
|
||||
])->save();
|
||||
|
||||
// Create note type with body field.
|
||||
$node_type = NodeType::create(['type' => 'page', 'name' => 'Page']);
|
||||
$node_type->save();
|
||||
node_add_body_field($node_type);
|
||||
|
||||
$account = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'administer nodes',
|
||||
'edit any page content',
|
||||
'use text format filtered_html',
|
||||
'access contextual links',
|
||||
'access in-place editing',
|
||||
]);
|
||||
$this->drupalLogin($account);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that quickeditor works correctly for field with CKEditor.
|
||||
*/
|
||||
public function testFieldWithCkeditor() {
|
||||
$body_value = '<p>Sapere aude</p>';
|
||||
$node = Node::create([
|
||||
'type' => 'page',
|
||||
'title' => 'Page node',
|
||||
'body' => [['value' => $body_value, 'format' => 'filtered_html']],
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$page = $this->getSession()->getPage();
|
||||
$assert = $this->assertSession();
|
||||
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
|
||||
// Wait "Quick edit" button for node.
|
||||
$this->assertSession()->waitForElement('css', '[data-quickedit-entity-id="node/' . $node->id() . '"] .contextual .quickedit');
|
||||
// Click by "Quick edit".
|
||||
$this->clickContextualLink('[data-quickedit-entity-id="node/' . $node->id() . '"]', 'Quick edit');
|
||||
// Switch to body field.
|
||||
$page->find('css', '[data-quickedit-field-id="node/' . $node->id() . '/body/en/full"]')->click();
|
||||
// Wait and click by "Blockquote" button from editor for body field.
|
||||
$this->assertSession()->waitForElementVisible('css', '.cke_button.cke_button__blockquote')->click();
|
||||
// Wait and click by "Save" button after body field was changed.
|
||||
$this->assertSession()->waitForElementVisible('css', '.quickedit-toolgroup.ops [type="submit"][aria-hidden="false"]')->click();
|
||||
// Wait until the save occurs and the editor UI disappears.
|
||||
$this->waitForNoElement('.cke_button.cke_button__blockquote');
|
||||
// Ensure that the changes take effect.
|
||||
$assert->responseMatches("|<blockquote>\s*$body_value\s*</blockquote>|");
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for an element to be removed from the page.
|
||||
*
|
||||
* @param string $selector
|
||||
* CSS selector.
|
||||
* @param int $timeout
|
||||
* (optional) Timeout in milliseconds, defaults to 10000.
|
||||
*/
|
||||
protected function waitForNoElement($selector, $timeout = 10000) {
|
||||
$condition = "(typeof jQuery !== 'undefined' && jQuery('$selector').length === 0)";
|
||||
$this->assertJsCondition($condition, $timeout);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\quickedit\FunctionalJavascript;
|
||||
|
||||
use Drupal\block_content\Entity\BlockContent;
|
||||
use Drupal\block_content\Entity\BlockContentType;
|
||||
use Drupal\Core\Field\FieldStorageDefinitionInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\taxonomy\Entity\Term;
|
||||
use Drupal\taxonomy\Entity\Vocabulary;
|
||||
|
||||
/**
|
||||
* @group quickedit
|
||||
*/
|
||||
class QuickEditIntegrationTest extends QuickEditJavascriptTestBase {
|
||||
|
||||
use EntityReferenceTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'editor',
|
||||
'ckeditor',
|
||||
'taxonomy',
|
||||
'block',
|
||||
'block_content',
|
||||
'hold_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* A user with permissions to edit Articles and use Quick Edit.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $contentAuthorUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
// Create text format, associate CKEditor.
|
||||
FilterFormat::create([
|
||||
'format' => 'some_format',
|
||||
'name' => 'Some format',
|
||||
'weight' => 0,
|
||||
'filters' => [
|
||||
'filter_html' => [
|
||||
'status' => 1,
|
||||
'settings' => [
|
||||
'allowed_html' => '<h2 id> <h3> <h4> <h5> <h6> <p> <br> <strong> <a href hreflang>',
|
||||
],
|
||||
],
|
||||
],
|
||||
])->save();
|
||||
Editor::create([
|
||||
'format' => 'some_format',
|
||||
'editor' => 'ckeditor',
|
||||
])->save();
|
||||
|
||||
// Create the Article node type.
|
||||
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
|
||||
|
||||
// Add "tags" vocabulary + field to the Article node type.
|
||||
$vocabulary = Vocabulary::create([
|
||||
'name' => 'Tags',
|
||||
'vid' => 'tags',
|
||||
]);
|
||||
$vocabulary->save();
|
||||
$field_name = 'field_' . $vocabulary->id();
|
||||
$handler_settings = [
|
||||
'target_bundles' => [
|
||||
$vocabulary->id() => $vocabulary->id(),
|
||||
],
|
||||
'auto_create' => TRUE,
|
||||
];
|
||||
$this->createEntityReferenceField('node', 'article', $field_name, 'Tags', 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
|
||||
|
||||
// Add formatter & widget for "tags" field.
|
||||
\Drupal::entityTypeManager()
|
||||
->getStorage('entity_form_display')
|
||||
->load('node.article.default')
|
||||
->setComponent($field_name, ['type' => 'entity_reference_autocomplete_tags'])
|
||||
->save();
|
||||
\Drupal::entityTypeManager()
|
||||
->getStorage('entity_view_display')
|
||||
->load('node.article.default')
|
||||
->setComponent($field_name, ['type' => 'entity_reference_label'])
|
||||
->save();
|
||||
|
||||
$this->drupalPlaceBlock('page_title_block');
|
||||
$this->drupalPlaceBlock('system_main_block');
|
||||
|
||||
// Log in as a content author who can use Quick Edit and edit Articles.
|
||||
$this->contentAuthorUser = $this->drupalCreateUser([
|
||||
'access contextual links',
|
||||
'access toolbar',
|
||||
'access in-place editing',
|
||||
'access content',
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format some_format',
|
||||
'edit terms in tags',
|
||||
'administer blocks',
|
||||
]);
|
||||
$this->drupalLogin($this->contentAuthorUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if an article node can be in-place edited with Quick Edit.
|
||||
*/
|
||||
public function testArticleNode() {
|
||||
$term = Term::create([
|
||||
'name' => 'foo',
|
||||
'vid' => 'tags',
|
||||
]);
|
||||
$term->save();
|
||||
|
||||
$node = $this->drupalCreateNode([
|
||||
'type' => 'article',
|
||||
'title' => t('My Test Node'),
|
||||
'body' => [
|
||||
'value' => '<p>Hello world!</p><p>I do not know what to say…</p><p>I wish I were eloquent.</p>',
|
||||
'format' => 'some_format',
|
||||
],
|
||||
'field_tags' => [
|
||||
['target_id' => $term->id()],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->drupalGet('node/' . $node->id());
|
||||
|
||||
// Initial state.
|
||||
$this->awaitQuickEditForEntity('node', 1);
|
||||
$this->assertEntityInstanceStates([
|
||||
'node/1[0]' => 'closed',
|
||||
]);
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/title/en/full' => 'inactive',
|
||||
'node/1/uid/en/full' => 'inactive',
|
||||
'node/1/created/en/full' => 'inactive',
|
||||
'node/1/body/en/full' => 'inactive',
|
||||
'node/1/field_tags/en/full' => 'inactive',
|
||||
]);
|
||||
|
||||
// Start in-place editing of the article node.
|
||||
$this->startQuickEditViaToolbar('node', 1, 0);
|
||||
$this->assertEntityInstanceStates([
|
||||
'node/1[0]' => 'opened',
|
||||
]);
|
||||
$this->assertQuickEditEntityToolbar((string) $node->label(), NULL);
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/title/en/full' => 'candidate',
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'candidate',
|
||||
]);
|
||||
|
||||
$assert_session = $this->assertSession();
|
||||
|
||||
// Click the title field.
|
||||
$this->click('[data-quickedit-field-id="node/1/title/en/full"].quickedit-candidate');
|
||||
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="title"]');
|
||||
$this->assertQuickEditEntityToolbar((string) $node->label(), 'Title');
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/title/en/full' => 'active',
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'candidate',
|
||||
]);
|
||||
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
|
||||
'node/1/title/en/full' => '[contenteditable="true"]',
|
||||
]);
|
||||
|
||||
// Append something to the title.
|
||||
$this->typeInPlainTextEditor('[data-quickedit-field-id="node/1/title/en/full"].quickedit-candidate', ' Llamas are awesome!');
|
||||
$this->awaitEntityInstanceFieldState('node', 1, 0, 'title', 'en', 'changed');
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/title/en/full' => 'changed',
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'candidate',
|
||||
]);
|
||||
|
||||
// Click the body field.
|
||||
hold_test_response(TRUE);
|
||||
$this->click('[data-quickedit-entity-id="node/1"] .field--name-body');
|
||||
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="body"]');
|
||||
$this->assertQuickEditEntityToolbar((string) $node->label(), 'Body');
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/title/en/full' => 'saving',
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'active',
|
||||
'node/1/field_tags/en/full' => 'candidate',
|
||||
]);
|
||||
hold_test_response(FALSE);
|
||||
|
||||
// Wait for CKEditor to load, then verify it has.
|
||||
$this->assertJsCondition('CKEDITOR.status === "loaded"');
|
||||
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
|
||||
'node/1/body/en/full' => '.cke_editable_inline',
|
||||
'node/1/field_tags/en/full' => ':not(.quickedit-editor-is-popup)',
|
||||
]);
|
||||
$this->assertSession()->elementExists('css', '#quickedit-entity-toolbar .quickedit-toolgroup.wysiwyg-main > .cke_chrome .cke_top[role="presentation"] .cke_toolbar[role="toolbar"] .cke_toolgroup[role="presentation"] > .cke_button[title~="Bold"][role="button"]');
|
||||
|
||||
// Wait for the validating & saving of the title to complete.
|
||||
$this->awaitEntityInstanceFieldState('node', 1, 0, 'title', 'en', 'candidate');
|
||||
|
||||
// Click the tags field.
|
||||
hold_test_response(TRUE);
|
||||
$this->click('[data-quickedit-field-id="node/1/field_tags/en/full"]');
|
||||
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="tags"]');
|
||||
$this->assertQuickEditEntityToolbar((string) $node->label(), 'Tags');
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'activating',
|
||||
'node/1/title/en/full' => 'candidate',
|
||||
]);
|
||||
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
|
||||
'node/1/title/en/full' => '.quickedit-changed',
|
||||
'node/1/field_tags/en/full' => '.quickedit-editor-is-popup',
|
||||
]);
|
||||
// Assert the "Loading…" popup appears.
|
||||
$this->assertSession()->elementExists('css', '.quickedit-form-container > .quickedit-form[role="dialog"] > .placeholder');
|
||||
hold_test_response(FALSE);
|
||||
// Wait for the form to load.
|
||||
$this->assertJsCondition('document.querySelector(\'.quickedit-form-container > .quickedit-form[role="dialog"] > .placeholder\') === null');
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'active',
|
||||
'node/1/title/en/full' => 'candidate',
|
||||
]);
|
||||
|
||||
// Enter an additional tag.
|
||||
$this->typeInFormEditorTextInputField('field_tags[target_id]', 'foo, bar');
|
||||
$this->awaitEntityInstanceFieldState('node', 1, 0, 'field_tags', 'en', 'changed');
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'changed',
|
||||
'node/1/title/en/full' => 'candidate',
|
||||
]);
|
||||
|
||||
// Click 'Save'.
|
||||
hold_test_response(TRUE);
|
||||
$this->saveQuickEdit();
|
||||
$this->assertEntityInstanceStates([
|
||||
'node/1[0]' => 'committing',
|
||||
]);
|
||||
$this->assertEntityInstanceFieldStates('node', 1, 0, [
|
||||
'node/1/uid/en/full' => 'candidate',
|
||||
'node/1/created/en/full' => 'candidate',
|
||||
'node/1/body/en/full' => 'candidate',
|
||||
'node/1/field_tags/en/full' => 'saving',
|
||||
'node/1/title/en/full' => 'candidate',
|
||||
]);
|
||||
hold_test_response(FALSE);
|
||||
$this->assertEntityInstanceFieldMarkup('node', 1, 0, [
|
||||
'node/1/title/en/full' => '.quickedit-changed',
|
||||
'node/1/field_tags/en/full' => '.quickedit-changed',
|
||||
]);
|
||||
|
||||
// Wait for the saving of the tags field to complete.
|
||||
$this->assertJsCondition("Drupal.quickedit.collections.entities.get('node/1[0]').get('state') === 'closed'");
|
||||
$this->assertEntityInstanceStates([
|
||||
'node/1[0]' => 'closed',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if a custom can be in-place edited with Quick Edit.
|
||||
*/
|
||||
public function testCustomBlock() {
|
||||
$block_content_type = BlockContentType::create([
|
||||
'id' => 'basic',
|
||||
'label' => 'basic',
|
||||
'revision' => FALSE,
|
||||
]);
|
||||
$block_content_type->save();
|
||||
block_content_add_body_field($block_content_type->id());
|
||||
|
||||
$block_content = BlockContent::create([
|
||||
'info' => 'Llama',
|
||||
'type' => 'basic',
|
||||
'body' => [
|
||||
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
|
||||
'format' => 'some_format',
|
||||
],
|
||||
]);
|
||||
$block_content->save();
|
||||
$this->drupalPlaceBlock('block_content:' . $block_content->uuid(), [
|
||||
'label' => 'My custom block!',
|
||||
]);
|
||||
|
||||
$this->drupalGet('');
|
||||
|
||||
// Initial state.
|
||||
$this->awaitQuickEditForEntity('block_content', 1);
|
||||
$this->assertEntityInstanceStates([
|
||||
'block_content/1[0]' => 'closed',
|
||||
]);
|
||||
|
||||
// Start in-place editing of the article node.
|
||||
$this->startQuickEditViaToolbar('block_content', 1, 0);
|
||||
$this->assertEntityInstanceStates([
|
||||
'block_content/1[0]' => 'opened',
|
||||
]);
|
||||
$this->assertQuickEditEntityToolbar((string) $block_content->label(), 'Body');
|
||||
$this->assertEntityInstanceFieldStates('block_content', 1, 0, [
|
||||
'block_content/1/body/en/full' => 'highlighted',
|
||||
]);
|
||||
|
||||
// Click the body field.
|
||||
$this->click('[data-quickedit-entity-id="block_content/1"] .field--name-body');
|
||||
$assert_session = $this->assertSession();
|
||||
$assert_session->waitForElement('css', '.quickedit-toolbar-field div[id*="body"]');
|
||||
$this->assertQuickEditEntityToolbar((string) $block_content->label(), 'Body');
|
||||
$this->assertEntityInstanceFieldStates('block_content', 1, 0, [
|
||||
'block_content/1/body/en/full' => 'active',
|
||||
]);
|
||||
|
||||
// Wait for CKEditor to load, then verify it has.
|
||||
$this->assertJsCondition('CKEDITOR.status === "loaded"');
|
||||
$this->assertEntityInstanceFieldMarkup('block_content', 1, 0, [
|
||||
'block_content/1/body/en/full' => '.cke_editable_inline',
|
||||
]);
|
||||
$this->assertSession()->elementExists('css', '#quickedit-entity-toolbar .quickedit-toolgroup.wysiwyg-main > .cke_chrome .cke_top[role="presentation"] .cke_toolbar[role="toolbar"] .cke_toolgroup[role="presentation"] > .cke_button[title~="Bold"][role="button"]');
|
||||
}
|
||||
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\quickedit\FunctionalJavascript;
|
||||
|
||||
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
|
||||
use WebDriver\Key;
|
||||
|
||||
/**
|
||||
* Base class for testing the QuickEdit.
|
||||
*/
|
||||
class QuickEditJavascriptTestBase extends WebDriverTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['contextual', 'quickedit', 'toolbar'];
|
||||
|
||||
/**
|
||||
* A user with permissions to edit Articles and use Quick Edit.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $contentAuthorUser;
|
||||
|
||||
protected static $expectedFieldStateAttributes = [
|
||||
'inactive' => '.quickedit-field:not(.quickedit-editable):not(.quickedit-candidate):not(.quickedit-highlighted):not(.quickedit-editing):not(.quickedit-changed)',
|
||||
// A field in 'candidate' state may still have the .quickedit-changed class
|
||||
// because when its changes were saved to tempstore, it'll still be changed.
|
||||
// It's just not currently being edited, so that's why it is not in the
|
||||
// 'changed' state.
|
||||
'candidate' => '.quickedit-field.quickedit-editable.quickedit-candidate:not(.quickedit-highlighted):not(.quickedit-editing)',
|
||||
'highlighted' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted:not(.quickedit-editing)',
|
||||
'activating' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing:not(.quickedit-changed)',
|
||||
'active' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing:not(.quickedit-changed)',
|
||||
'changed' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing.quickedit-changed',
|
||||
'saving' => '.quickedit-field.quickedit-editable.quickedit-candidate.quickedit-highlighted.quickedit-editing.quickedit-changed',
|
||||
];
|
||||
|
||||
/**
|
||||
* Starts in-place editing of the given entity instance.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
* @param int $entity_instance_id
|
||||
* The entity instance ID. (Instance on the page.)
|
||||
*/
|
||||
protected function startQuickEditViaToolbar($entity_type_id, $entity_id, $entity_instance_id) {
|
||||
$page = $this->getSession()->getPage();
|
||||
|
||||
$toolbar_edit_button_selector = '#toolbar-bar .contextual-toolbar-tab button';
|
||||
$entity_instance_selector = '[data-quickedit-entity-id="' . $entity_type_id . '/' . $entity_id . '"][data-quickedit-entity-instance-id="' . $entity_instance_id . '"]';
|
||||
$contextual_links_trigger_selector = '[data-contextual-id] > .trigger';
|
||||
|
||||
// Assert the original page state does not have the toolbar's "Edit" button
|
||||
// pressed/activated, and hence none of the contextual link triggers should
|
||||
// be visible.
|
||||
$toolbar_edit_button = $page->find('css', $toolbar_edit_button_selector);
|
||||
$this->assertSame('false', $toolbar_edit_button->getAttribute('aria-pressed'), 'The "Edit" button in the toolbar is not yet pressed.');
|
||||
$this->assertFalse($toolbar_edit_button->hasClass('is-active'), 'The "Edit" button in the toolbar is not yet marked as active.');
|
||||
foreach ($page->findAll('css', $contextual_links_trigger_selector) as $dom_node) {
|
||||
/** @var \Behat\Mink\Element\NodeElement $dom_node */
|
||||
$this->assertTrue($dom_node->hasClass('visually-hidden'), 'The contextual links trigger "' . $dom_node->getParent()->getAttribute('data-contextual-id') . '" is hidden.');
|
||||
}
|
||||
$this->assertTrue(TRUE, 'All contextual links triggers are hidden.');
|
||||
|
||||
// Click the "Edit" button in the toolbar.
|
||||
$this->click($toolbar_edit_button_selector);
|
||||
|
||||
// Assert the toolbar's "Edit" button is now pressed/activated, and hence
|
||||
// all of the contextual link triggers should be visible.
|
||||
$this->assertSame('true', $toolbar_edit_button->getAttribute('aria-pressed'), 'The "Edit" button in the toolbar is pressed.');
|
||||
$this->assertTrue($toolbar_edit_button->hasClass('is-active'), 'The "Edit" button in the toolbar is marked as active.');
|
||||
foreach ($page->findAll('css', $contextual_links_trigger_selector) as $dom_node) {
|
||||
/** @var \Behat\Mink\Element\NodeElement $dom_node */
|
||||
$this->assertFalse($dom_node->hasClass('visually-hidden'), 'The contextual links trigger "' . $dom_node->getParent()->getAttribute('data-contextual-id') . '" is visible.');
|
||||
}
|
||||
$this->assertTrue(TRUE, 'All contextual links triggers are visible.');
|
||||
|
||||
// @todo Press tab key to verify that tabbing is now contrained to only
|
||||
// contextual links triggers: https://www.drupal.org/node/2834776
|
||||
|
||||
// Assert that the contextual links associated with the entity's contextual
|
||||
// links trigger are not visible.
|
||||
/** @var \Behat\Mink\Element\NodeElement $entity_contextual_links_container */
|
||||
$entity_contextual_links_container = $page->find('css', $entity_instance_selector)
|
||||
->find('css', $contextual_links_trigger_selector)
|
||||
->getParent();
|
||||
$this->assertFalse($entity_contextual_links_container->hasClass('open'));
|
||||
$this->assertTrue($entity_contextual_links_container->find('css', 'ul.contextual-links')->hasAttribute('hidden'));
|
||||
|
||||
// Click the contextual link trigger for the entity we want to Quick Edit.
|
||||
$this->click($entity_instance_selector . ' ' . $contextual_links_trigger_selector);
|
||||
|
||||
$this->assertTrue($entity_contextual_links_container->hasClass('open'));
|
||||
$this->assertFalse($entity_contextual_links_container->find('css', 'ul.contextual-links')->hasAttribute('hidden'));
|
||||
|
||||
// Click the "Quick edit" contextual link.
|
||||
$this->click($entity_instance_selector . ' [data-contextual-id] ul.contextual-links li.quickedit a');
|
||||
|
||||
// Assert the Quick Edit internal state is correct.
|
||||
$js_condition = <<<JS
|
||||
Drupal.quickedit.collections.entities.where({isActive: true}).length === 1 && Drupal.quickedit.collections.entities.where({isActive: true})[0].get('entityID') === '$entity_type_id/$entity_id';
|
||||
JS;
|
||||
$this->assertJsCondition($js_condition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks the 'Save' button in the Quick Edit entity toolbar.
|
||||
*/
|
||||
protected function saveQuickEdit() {
|
||||
$quickedit_entity_toolbar = $this->getSession()->getPage()->findById('quickedit-entity-toolbar');
|
||||
$save_button = $quickedit_entity_toolbar->find('css', 'button.action-save');
|
||||
$save_button->press();
|
||||
$this->assertSame('Saving', $save_button->getText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits Quick Edit to be initiated for all instances of the given entity.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
*/
|
||||
protected function awaitQuickEditForEntity($entity_type_id, $entity_id) {
|
||||
$entity_selector = '[data-quickedit-entity-id="' . $entity_type_id . '/' . $entity_id . '"]';
|
||||
$condition = "document.querySelectorAll('" . $entity_selector . "').length === document.querySelectorAll('" . $entity_selector . " .quickedit').length";
|
||||
$this->assertJsCondition($condition, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Awaits a particular field instance to reach a particular state.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
* @param int $entity_instance_id
|
||||
* The entity instance ID. (Instance on the page.)
|
||||
* @param string $field_name
|
||||
* The field name.
|
||||
* @param string $langcode
|
||||
* The language code.
|
||||
* @param string $awaited_state
|
||||
* One of the possible field states.
|
||||
*/
|
||||
protected function awaitEntityInstanceFieldState($entity_type_id, $entity_id, $entity_instance_id, $field_name, $langcode, $awaited_state) {
|
||||
$entity_page_id = $entity_type_id . '/' . $entity_id . '[' . $entity_instance_id . ']';
|
||||
$logical_field_id = $entity_type_id . '/' . $entity_id . '/' . $field_name . '/' . $langcode;
|
||||
$this->assertJsCondition("Drupal.quickedit.collections.entities.get('$entity_page_id').get('fields').findWhere({logicalFieldID: '$logical_field_id'}).get('state') === '$awaited_state';");
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts the state of the Quick Edit entity toolbar.
|
||||
*
|
||||
* @param string $expected_entity_label
|
||||
* The expected label in the Quick Edit Entity Toolbar.
|
||||
*/
|
||||
protected function assertQuickEditEntityToolbar($expected_entity_label, $expected_field_label) {
|
||||
$quickedit_entity_toolbar = $this->getSession()->getPage()->findById('quickedit-entity-toolbar');
|
||||
// We cannot use ->getText() because it also returns the text of all child
|
||||
// nodes. We also cannot use XPath to select text node in Selenium. So we
|
||||
// use JS expression to select only the text node.
|
||||
$this->assertSame($expected_entity_label, $this->getSession()->evaluateScript("return window.jQuery('#quickedit-entity-toolbar .quickedit-toolbar-label').clone().children().remove().end().text();"));
|
||||
if ($expected_field_label !== NULL) {
|
||||
$field_label = $quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label > .field');
|
||||
// Only try to find the text content of the element if it was actually
|
||||
// found; otherwise use the returned value for assertion. This helps
|
||||
// us find a more useful stack/error message from testbot instead of the
|
||||
// trimmed partial exception stack.
|
||||
if ($field_label) {
|
||||
$field_label = $field_label->getText();
|
||||
}
|
||||
$this->assertSame($expected_field_label, $field_label);
|
||||
}
|
||||
else {
|
||||
$this->assertFalse($quickedit_entity_toolbar->find('css', '.quickedit-toolbar-label > .field'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts all EntityModels (entity instances) on the page.
|
||||
*
|
||||
* @param array $expected_entity_states
|
||||
* Must describe the expected state of all in-place editable entity
|
||||
* instances on the page.
|
||||
*
|
||||
* @see Drupal.quickedit.EntityModel
|
||||
*/
|
||||
protected function assertEntityInstanceStates(array $expected_entity_states) {
|
||||
$js_get_all_field_states_for_entity = <<<JS
|
||||
function () {
|
||||
Drupal.quickedit.collections.entities.reduce(function (result, fieldModel) { result[fieldModel.get('id')] = fieldModel.get('state'); return result; }, {})
|
||||
var entityCollection = Drupal.quickedit.collections.entities;
|
||||
return entityCollection.reduce(function (result, entityModel) {
|
||||
result[entityModel.id] = entityModel.get('state');
|
||||
return result;
|
||||
}, {});
|
||||
}()
|
||||
JS;
|
||||
$this->assertSame($expected_entity_states, $this->getSession()->evaluateScript($js_get_all_field_states_for_entity));
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts all FieldModels for the given entity instance.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
* @param int $entity_instance_id
|
||||
* The entity instance ID. (Instance on the page.)
|
||||
* @param array $expected_field_states
|
||||
* Must describe the expected state of all in-place editable fields of the
|
||||
* given entity instance.
|
||||
*/
|
||||
protected function assertEntityInstanceFieldStates($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_states) {
|
||||
// Get all FieldModel states for the entity instance being asserted. This
|
||||
// ensures that $expected_field_states must describe the state of all fields
|
||||
// of the entity instance.
|
||||
$entity_page_id = $entity_type_id . '/' . $entity_id . '[' . $entity_instance_id . ']';
|
||||
$js_get_all_field_states_for_entity = <<<JS
|
||||
function () {
|
||||
var entityCollection = Drupal.quickedit.collections.entities;
|
||||
var entityModel = entityCollection.get('$entity_page_id');
|
||||
return entityModel.get('fields').reduce(function (result, fieldModel) {
|
||||
result[fieldModel.get('fieldID')] = fieldModel.get('state');
|
||||
return result;
|
||||
}, {});
|
||||
}()
|
||||
JS;
|
||||
$this->assertEquals($expected_field_states, $this->getSession()->evaluateScript($js_get_all_field_states_for_entity));
|
||||
|
||||
// Assert that those fields also have the appropriate DOM decorations.
|
||||
$expected_field_attributes = [];
|
||||
foreach ($expected_field_states as $quickedit_field_id => $expected_field_state) {
|
||||
$expected_field_attributes[$quickedit_field_id] = static::$expectedFieldStateAttributes[$expected_field_state];
|
||||
}
|
||||
$this->assertEntityInstanceFieldMarkup($entity_type_id, $entity_id, $entity_instance_id, $expected_field_attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts all in-place editable fields with markup expectations.
|
||||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param int $entity_id
|
||||
* The entity ID.
|
||||
* @param int $entity_instance_id
|
||||
* The entity instance ID. (Instance on the page.)
|
||||
* @param array $expected_field_attributes
|
||||
* Must describe the expected markup attributes for all given in-place
|
||||
* editable fields.
|
||||
*/
|
||||
protected function assertEntityInstanceFieldMarkup($entity_type_id, $entity_id, $entity_instance_id, array $expected_field_attributes) {
|
||||
$entity_page_id = $entity_type_id . '/' . $entity_id . '[' . $entity_instance_id . ']';
|
||||
$expected_field_attributes_json = json_encode($expected_field_attributes);
|
||||
$js_match_field_element_attributes = <<<JS
|
||||
function () {
|
||||
var expectations = $expected_field_attributes_json;
|
||||
var entityCollection = Drupal.quickedit.collections.entities;
|
||||
var entityModel = entityCollection.get('$entity_page_id');
|
||||
return entityModel.get('fields').reduce(function (result, fieldModel) {
|
||||
var fieldID = fieldModel.get('fieldID');
|
||||
var element = fieldModel.get('el');
|
||||
var matches = element.webkitMatchesSelector(expectations[fieldID]);
|
||||
result[fieldID] = matches ? matches : element.outerHTML;
|
||||
return result;
|
||||
}, {});
|
||||
}()
|
||||
JS;
|
||||
$result = $this->getSession()->evaluateScript($js_match_field_element_attributes);
|
||||
foreach ($expected_field_attributes as $quickedit_field_id => $expectation) {
|
||||
$this->assertSame(TRUE, $result[$quickedit_field_id], 'Field ' . $quickedit_field_id . ' did not match its expectation selector (' . $expectation . '), actual HTML: ' . $result[$quickedit_field_id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates typing in a 'plain_text' in-place editor.
|
||||
*
|
||||
* @param string $css_selector
|
||||
* The CSS selector to find the DOM element (with the 'contenteditable=true'
|
||||
* attribute set), to type in.
|
||||
* @param string $text
|
||||
* The text to type.
|
||||
*
|
||||
* @see \Drupal\quickedit\Plugin\InPlaceEditor\PlainTextEditor
|
||||
*/
|
||||
protected function typeInPlainTextEditor($css_selector, $text) {
|
||||
$field = $this->getSession()->getPage()->find('css', $css_selector);
|
||||
$field->setValue(Key::END . $text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates typing in an input[type=text] inside a 'form' in-place editor.
|
||||
*
|
||||
* @param string $input_name
|
||||
* The "name" attribute of the input[type=text] to type in.
|
||||
* @param string $text
|
||||
* The text to type.
|
||||
*
|
||||
* @see \Drupal\quickedit\Plugin\InPlaceEditor\FormEditor
|
||||
*/
|
||||
protected function typeInFormEditorTextInputField($input_name, $text) {
|
||||
$input = $this->cssSelect('.quickedit-form-container > .quickedit-form[role="dialog"] form.quickedit-field-form input[type=text][name="' . $input_name . '"]')[0];
|
||||
$input->setValue($text);
|
||||
$js_simulate_user_typing = <<<JS
|
||||
function () {
|
||||
var el = document.querySelector('.quickedit-form-container > .quickedit-form[role="dialog"] form.quickedit-field-form input[name="$input_name"]');
|
||||
window.jQuery(el).trigger('formUpdated');
|
||||
}()
|
||||
JS;
|
||||
$this->getSession()->evaluateScript($js_simulate_user_typing);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class MetadataGeneratorTest extends QuickEditTestBase {
|
||||
/**
|
||||
* The metadata generator object to be tested.
|
||||
*
|
||||
* @var \Drupal\quickedit\MetadataGeneratorInterface.php
|
||||
* @var \Drupal\quickedit\MetadataGeneratorInterface
|
||||
*/
|
||||
protected $metadataGenerator;
|
||||
|
||||
@@ -169,7 +169,7 @@ class MetadataGeneratorTest extends QuickEditTestBase {
|
||||
'label' => 'Rich text field',
|
||||
'editor' => 'wysiwyg',
|
||||
'custom' => [
|
||||
'format' => 'full_html'
|
||||
'format' => 'full_html',
|
||||
],
|
||||
];
|
||||
$this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.');
|
||||
|
||||
@@ -97,7 +97,7 @@ abstract class QuickEditTestBase extends KernelTestBase {
|
||||
->setComponent($field_name, [
|
||||
'label' => 'above',
|
||||
'type' => $formatter_type,
|
||||
'settings' => $formatter_settings
|
||||
'settings' => $formatter_settings,
|
||||
])
|
||||
->save();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user