/**
* @file
* A Backbone View that decorates the in-place edited element.
*/
(function ($, Backbone, Drupal) {
Drupal.quickedit.FieldDecorationView = Backbone.View.extend(
/** @lends Drupal.quickedit.FieldDecorationView# */ {
/**
* @type {null}
*/
_widthAttributeIsEmpty: null,
/**
* @type {object}
*/
events: {
'mouseenter.quickedit': 'onMouseEnter',
'mouseleave.quickedit': 'onMouseLeave',
click: 'onClick',
'tabIn.quickedit': 'onMouseEnter',
'tabOut.quickedit': 'onMouseLeave',
},
/**
* @constructs
*
* @augments Backbone.View
*
* @param {object} options
* An object with the following keys:
* @param {Drupal.quickedit.EditorView} options.editorView
* The editor object view.
*/
initialize(options) {
this.editorView = options.editorView;
this.listenTo(this.model, 'change:state', this.stateChange);
this.listenTo(
this.model,
'change:isChanged change:inTempStore',
this.renderChanged,
);
},
/**
* {@inheritdoc}
*/
remove() {
// The el property is the field, which should not be removed. Remove the
// pointer to it, then call Backbone.View.prototype.remove().
this.setElement();
Backbone.View.prototype.remove.call(this);
},
/**
* Determines the actions to take given a change of state.
*
* @param {Drupal.quickedit.FieldModel} model
* The `FieldModel` model.
* @param {string} state
* The state of the associated field. One of
* {@link Drupal.quickedit.FieldModel.states}.
*/
stateChange(model, state) {
const from = model.previous('state');
const to = state;
switch (to) {
case 'inactive':
this.undecorate();
break;
case 'candidate':
this.decorate();
if (from !== 'inactive') {
this.stopHighlight();
if (from !== 'highlighted') {
this.model.set('isChanged', false);
this.stopEdit();
}
}
this._unpad();
break;
case 'highlighted':
this.startHighlight();
break;
case 'activating':
// NOTE: this state is not used by every editor! It's only used by
// those that need to interact with the server.
this.prepareEdit();
break;
case 'active':
if (from !== 'activating') {
this.prepareEdit();
}
if (this.editorView.getQuickEditUISettings().padding) {
this._pad();
}
break;
case 'changed':
this.model.set('isChanged', true);
break;
case 'saving':
break;
case 'saved':
break;
case 'invalid':
break;
}
},
/**
* Adds a class to the edited element that indicates whether the field has
* been changed by the user (i.e. locally) or the field has already been
* changed and stored before by the user (i.e. remotely, stored in
* PrivateTempStore).
*/
renderChanged() {
this.$el.toggleClass(
'quickedit-changed',
this.model.get('isChanged') || this.model.get('inTempStore'),
);
},
/**
* Starts hover; transitions to 'highlight' state.
*
* @param {jQuery.Event} event
* The mouse event.
*/
onMouseEnter(event) {
const that = this;
that.model.set('state', 'highlighted');
event.stopPropagation();
},
/**
* Stops hover; transitions to 'candidate' state.
*
* @param {jQuery.Event} event
* The mouse event.
*/
onMouseLeave(event) {
const that = this;
that.model.set('state', 'candidate', { reason: 'mouseleave' });
event.stopPropagation();
},
/**
* Transition to 'activating' stage.
*
* @param {jQuery.Event} event
* The click event.
*/
onClick(event) {
this.model.set('state', 'activating');
event.preventDefault();
event.stopPropagation();
},
/**
* Adds classes used to indicate an elements editable state.
*/
decorate() {
this.$el.addClass('quickedit-candidate quickedit-editable');
},
/**
* Removes classes used to indicate an elements editable state.
*/
undecorate() {
this.$el.removeClass(
'quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing',
);
},
/**
* Adds that class that indicates that an element is highlighted.
*/
startHighlight() {
// Animations.
const that = this;
// Use a timeout to grab the next available animation frame.
that.$el.addClass('quickedit-highlighted');
},
/**
* Removes the class that indicates that an element is highlighted.
*/
stopHighlight() {
this.$el.removeClass('quickedit-highlighted');
},
/**
* Removes the class that indicates that an element as editable.
*/
prepareEdit() {
this.$el.addClass('quickedit-editing');
// Allow the field to be styled differently while editing in a pop-up
// in-place editor.
if (this.editorView.getQuickEditUISettings().popup) {
this.$el.addClass('quickedit-editor-is-popup');
}
},
/**
* Removes the class that indicates that an element is being edited.
*
* Reapplies the class that indicates that a candidate editable element is
* again available to be edited.
*/
stopEdit() {
this.$el.removeClass('quickedit-highlighted quickedit-editing');
// Done editing in a pop-up in-place editor; remove the class.
if (this.editorView.getQuickEditUISettings().popup) {
this.$el.removeClass('quickedit-editor-is-popup');
}
// Make the other editors show up again.
$('.quickedit-candidate').addClass('quickedit-editable');
},
/**
* Adds padding around the editable element to make it pop visually.
*/
_pad() {
// Early return if the element has already been padded.
if (this.$el.data('quickedit-padded')) {
return;
}
const self = this;
// Add 5px padding for readability. This means we'll freeze the current
// width and *then* add 5px padding, hence ensuring the padding is added
// "on the outside".
// 1) Freeze the width (if it's not already set); don't use animations.
if (this.$el[0].style.width === '') {
this._widthAttributeIsEmpty = true;
this.$el
.addClass('quickedit-animate-disable-width')
.css('width', this.$el.width());
}
// 2) Add padding; use animations.
const posProp = this._getPositionProperties(this.$el);
setTimeout(() => {
// Re-enable width animations (padding changes affect width too!).
self.$el.removeClass('quickedit-animate-disable-width');
// Pad the editable.
self.$el
.css({
position: 'relative',
top: `${posProp.top - 5}px`,
left: `${posProp.left - 5}px`,
'padding-top': `${posProp['padding-top'] + 5}px`,
'padding-left': `${posProp['padding-left'] + 5}px`,
'padding-right': `${posProp['padding-right'] + 5}px`,
'padding-bottom': `${posProp['padding-bottom'] + 5}px`,
'margin-bottom': `${posProp['margin-bottom'] - 10}px`,
})
.data('quickedit-padded', true);
}, 0);
},
/**
* Removes the padding around the element being edited when editing ceases.
*/
_unpad() {
// Early return if the element has not been padded.
if (!this.$el.data('quickedit-padded')) {
return;
}
const self = this;
// 1) Set the empty width again.
if (this._widthAttributeIsEmpty) {
this.$el.addClass('quickedit-animate-disable-width').css('width', '');
}
// 2) Remove padding; use animations (these will run simultaneously with)
// the fading out of the toolbar as its gets removed).
const posProp = this._getPositionProperties(this.$el);
setTimeout(() => {
// Re-enable width animations (padding changes affect width too!).
self.$el.removeClass('quickedit-animate-disable-width');
// Unpad the editable.
self.$el.css({
position: 'relative',
top: `${posProp.top + 5}px`,
left: `${posProp.left + 5}px`,
'padding-top': `${posProp['padding-top'] - 5}px`,
'padding-left': `${posProp['padding-left'] - 5}px`,
'padding-right': `${posProp['padding-right'] - 5}px`,
'padding-bottom': `${posProp['padding-bottom'] - 5}px`,
'margin-bottom': `${posProp['margin-bottom'] + 10}px`,
});
}, 0);
// Remove the marker that indicates that this field has padding. This is
// done outside the timed out function above so that we don't get numerous
// queued functions that will remove padding before the data marker has
// been removed.
this.$el.removeData('quickedit-padded');
},
/**
* Gets the top and left properties of an element.
*
* Convert extraneous values and information into numbers ready for
* subtraction.
*
* @param {jQuery} $e
* The element to get position properties from.
*
* @return {object}
* An object containing css values for the needed properties.
*/
_getPositionProperties($e) {
let p;
const r = {};
const props = [
'top',
'left',
'bottom',
'right',
'padding-top',
'padding-left',
'padding-right',
'padding-bottom',
'margin-bottom',
];
const propCount = props.length;
for (let i = 0; i < propCount; i++) {
p = props[i];
r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10);
}
return r;
},
/**
* Replaces blank or 'auto' CSS `position: <value>` values with "0px".
*
* @param {string} [pos]
* The value for a CSS position declaration.
*
* @return {string}
* A CSS value that is valid for `position`.
*/
_replaceBlankPosition(pos) {
if (pos === 'auto' || !pos) {
pos = '0px';
}
return pos;
},
},
);
})(jQuery, Backbone, Drupal);