Newer
Older
/**
* @file
Dries Buytaert
committed
* A Backbone View that decorates the in-place edited element.
*/
Dries Buytaert
committed
(function ($, Backbone, Drupal) {
"use strict";
Drupal.quickedit.FieldDecorationView = Backbone.View.extend({
_widthAttributeIsEmpty: null,
events: {
'mouseenter.quickedit': 'onMouseEnter',
'mouseleave.quickedit': 'onMouseLeave',
'click': 'onClick',
'tabIn.quickedit': 'onMouseEnter',
'tabOut.quickedit': 'onMouseLeave'
},
/**
* {@inheritdoc}
*
* @param Object options
* An object with the following keys:
* - Drupal.quickedit.EditorView editorView: the editor object view.
*/
initialize: function (options) {
this.editorView = options.editorView;
this.listenTo(this.model, 'change:state', this.stateChange);
this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged);
},
/**
* {@inheritdoc}
*/
remove: function () {
// The el property is the field, which should not be removed. Remove the
// pointer to it, then call Backbone.View.prototype.remove().
this.setElement();
Backbone.View.prototype.remove.call(this);
},
/**
* Determines the actions to take given a change of state.
*
* @param Drupal.quickedit.FieldModel model
* @param String state
* The state of the associated field. One of Drupal.quickedit.FieldModel.states.
*/
stateChange: function (model, state) {
var from = model.previous('state');
var 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.
Angie Byron
committed
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 TempStore).
*/
renderChanged: function () {
this.$el.toggleClass('quickedit-changed', this.model.get('isChanged') || this.model.get('inTempStore'));
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
},
/**
* Starts hover; transitions to 'highlight' state.
*
* @param jQuery event
*/
onMouseEnter: function (event) {
var that = this;
that.model.set('state', 'highlighted');
event.stopPropagation();
},
/**
* Stops hover; transitions to 'candidate' state.
*
* @param jQuery event
*/
onMouseLeave: function (event) {
var that = this;
that.model.set('state', 'candidate', { reason: 'mouseleave' });
event.stopPropagation();
},
/**
* Transition to 'activating' stage.
*
* @param jQuery event
*/
onClick: function (event) {
this.model.set('state', 'activating');
event.preventDefault();
event.stopPropagation();
},
/**
* Adds classes used to indicate an elements editable state.
*/
decorate: function () {
this.$el.addClass('quickedit-candidate quickedit-editable');
},
/**
* Removes classes used to indicate an elements editable state.
*/
undecorate: function () {
this.$el.removeClass('quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing');
},
/**
* Adds that class that indicates that an element is highlighted.
*/
startHighlight: function () {
// Animations.
var that = this;
// Use a timeout to grab the next available animation frame.
that.$el.addClass('quickedit-highlighted');
},
/**
* Removes the class that indicates that an element is highlighted.
*/
stopHighlight: function () {
this.$el.removeClass('quickedit-highlighted');
},
/**
* Removes the class that indicates that an element as editable.
*/
prepareEdit: function () {
this.$el.addClass('quickedit-editing');
// Allow the field to be styled differently while editing in a pop-up
// in-place editor.
if (this.editorView.getQuickEditUISettings().popup) {
this.$el.addClass('quickedit-editor-is-popup');
}
},
/**
* Removes the class that indicates that an element is being edited.
*
* Reapplies the class that indicates that a candidate editable element is
* again available to be edited.
*/
stopEdit: function () {
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 in order to make it pop visually.
*/
_pad: function () {
// Early return if the element has already been padded.
if (this.$el.data('quickedit-padded')) {
return;
}
var self = this;
// Add 5px padding for readability. This means we'll freeze the current
// width and *then* add 5px padding, hence ensuring the padding is added "on
// the outside".
// 1) Freeze the width (if it's not already set); don't use animations.
if (this.$el[0].style.width === "") {
this._widthAttributeIsEmpty = true;
this.$el
.addClass('quickedit-animate-disable-width')
.css('width', this.$el.width())
.css('background-color', this._getBgColor(this.$el));
}
// 2) Add padding; use animations.
var posProp = this._getPositionProperties(this.$el);
setTimeout(function () {
// Re-enable width animations (padding changes affect width too!).
self.$el.removeClass('quickedit-animate-disable-width');
// Pad the editable.
self.$el
.css({
'position': 'relative',
'top': posProp.top - 5 + 'px',
'left': posProp.left - 5 + 'px',
'padding-top': posProp['padding-top'] + 5 + 'px',
'padding-left': posProp['padding-left'] + 5 + 'px',
'padding-right': posProp['padding-right'] + 5 + 'px',
'padding-bottom': posProp['padding-bottom'] + 5 + 'px',
'margin-bottom': posProp['margin-bottom'] - 10 + 'px'
})
.data('quickedit-padded', true);
}, 0);
},
/**
* Removes the padding around the element being edited when editing ceases.
*/
_unpad: function () {
// Early return if the element has not been padded.
if (!this.$el.data('quickedit-padded')) {
return;
}
var self = this;
// 1) Set the empty width again.
if (this._widthAttributeIsEmpty) {
this.$el
.addClass('quickedit-animate-disable-width')
.css('width', '')
.css('background-color', '');
}
// 2) Remove padding; use animations (these will run simultaneously with)
// the fading out of the toolbar as its gets removed).
var posProp = this._getPositionProperties(this.$el);
setTimeout(function () {
// Re-enable width animations (padding changes affect width too!).
self.$el.removeClass('quickedit-animate-disable-width');
// Unpad the editable.
self.$el
.css({
'position': 'relative',
'top': posProp.top + 5 + 'px',
'left': posProp.left + 5 + 'px',
'padding-top': posProp['padding-top'] - 5 + 'px',
'padding-left': posProp['padding-left'] - 5 + 'px',
'padding-right': posProp['padding-right'] - 5 + 'px',
'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
});
}, 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');
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
},
/**
* Gets the background color of an element (or the inherited one).
*
* @param DOM $e
*/
_getBgColor: function ($e) {
var c;
if ($e === null || $e[0].nodeName === 'HTML') {
// Fallback to white.
return 'rgb(255, 255, 255)';
}
c = $e.css('background-color');
// TRICKY: edge case for Firefox' "transparent" here; this is a
// browser bug: https://bugzilla.mozilla.org/show_bug.cgi?id=635724
if (c === 'rgba(0, 0, 0, 0)' || c === 'transparent') {
return this._getBgColor($e.parent());
}
return c;
},
/**
* Gets the top and left properties of an element.
*
* Convert extraneous values and information into numbers ready for
* subtraction.
*
* @param DOM $e
*/
_getPositionProperties: function ($e) {
var p,
r = {},
props = [
'top', 'left', 'bottom', 'right',
'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
'margin-bottom'
];
var propCount = props.length;
for (var i = 0; i < propCount; i++) {
p = props[i];
r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10);
}
return r;
},
/**
* Replaces blank or 'auto' CSS "position: <value>" values with "0px".
*
* @param String pos
* (optional) The value for a CSS position declaration.
*/
_replaceBlankPosition: function (pos) {
if (pos === 'auto' || !pos) {
pos = '0px';
}
return pos;
}
});
})(jQuery, Backbone, Drupal);