Version 1
[yaffs-website] / web / core / modules / quickedit / js / views / FieldDecorationView.js
1 /**
2  * @file
3  * A Backbone View that decorates the in-place edited element.
4  */
5
6 (function ($, Backbone, Drupal) {
7
8   'use strict';
9
10   Drupal.quickedit.FieldDecorationView = Backbone.View.extend(/** @lends Drupal.quickedit.FieldDecorationView# */{
11
12     /**
13      * @type {null}
14      */
15     _widthAttributeIsEmpty: null,
16
17     /**
18      * @type {object}
19      */
20     events: {
21       'mouseenter.quickedit': 'onMouseEnter',
22       'mouseleave.quickedit': 'onMouseLeave',
23       'click': 'onClick',
24       'tabIn.quickedit': 'onMouseEnter',
25       'tabOut.quickedit': 'onMouseLeave'
26     },
27
28     /**
29      * @constructs
30      *
31      * @augments Backbone.View
32      *
33      * @param {object} options
34      *   An object with the following keys:
35      * @param {Drupal.quickedit.EditorView} options.editorView
36      *   The editor object view.
37      */
38     initialize: function (options) {
39       this.editorView = options.editorView;
40
41       this.listenTo(this.model, 'change:state', this.stateChange);
42       this.listenTo(this.model, 'change:isChanged change:inTempStore', this.renderChanged);
43     },
44
45     /**
46      * @inheritdoc
47      */
48     remove: function () {
49       // The el property is the field, which should not be removed. Remove the
50       // pointer to it, then call Backbone.View.prototype.remove().
51       this.setElement();
52       Backbone.View.prototype.remove.call(this);
53     },
54
55     /**
56      * Determines the actions to take given a change of state.
57      *
58      * @param {Drupal.quickedit.FieldModel} model
59      *   The `FieldModel` model.
60      * @param {string} state
61      *   The state of the associated field. One of
62      *   {@link Drupal.quickedit.FieldModel.states}.
63      */
64     stateChange: function (model, state) {
65       var from = model.previous('state');
66       var to = state;
67       switch (to) {
68         case 'inactive':
69           this.undecorate();
70           break;
71
72         case 'candidate':
73           this.decorate();
74           if (from !== 'inactive') {
75             this.stopHighlight();
76             if (from !== 'highlighted') {
77               this.model.set('isChanged', false);
78               this.stopEdit();
79             }
80           }
81           this._unpad();
82           break;
83
84         case 'highlighted':
85           this.startHighlight();
86           break;
87
88         case 'activating':
89           // NOTE: this state is not used by every editor! It's only used by
90           // those that need to interact with the server.
91           this.prepareEdit();
92           break;
93
94         case 'active':
95           if (from !== 'activating') {
96             this.prepareEdit();
97           }
98           if (this.editorView.getQuickEditUISettings().padding) {
99             this._pad();
100           }
101           break;
102
103         case 'changed':
104           this.model.set('isChanged', true);
105           break;
106
107         case 'saving':
108           break;
109
110         case 'saved':
111           break;
112
113         case 'invalid':
114           break;
115       }
116     },
117
118     /**
119      * Adds a class to the edited element that indicates whether the field has
120      * been changed by the user (i.e. locally) or the field has already been
121      * changed and stored before by the user (i.e. remotely, stored in
122      * PrivateTempStore).
123      */
124     renderChanged: function () {
125       this.$el.toggleClass('quickedit-changed', this.model.get('isChanged') || this.model.get('inTempStore'));
126     },
127
128     /**
129      * Starts hover; transitions to 'highlight' state.
130      *
131      * @param {jQuery.Event} event
132      *   The mouse event.
133      */
134     onMouseEnter: function (event) {
135       var that = this;
136       that.model.set('state', 'highlighted');
137       event.stopPropagation();
138     },
139
140     /**
141      * Stops hover; transitions to 'candidate' state.
142      *
143      * @param {jQuery.Event} event
144      *   The mouse event.
145      */
146     onMouseLeave: function (event) {
147       var that = this;
148       that.model.set('state', 'candidate', {reason: 'mouseleave'});
149       event.stopPropagation();
150     },
151
152     /**
153      * Transition to 'activating' stage.
154      *
155      * @param {jQuery.Event} event
156      *   The click event.
157      */
158     onClick: function (event) {
159       this.model.set('state', 'activating');
160       event.preventDefault();
161       event.stopPropagation();
162     },
163
164     /**
165      * Adds classes used to indicate an elements editable state.
166      */
167     decorate: function () {
168       this.$el.addClass('quickedit-candidate quickedit-editable');
169     },
170
171     /**
172      * Removes classes used to indicate an elements editable state.
173      */
174     undecorate: function () {
175       this.$el.removeClass('quickedit-candidate quickedit-editable quickedit-highlighted quickedit-editing');
176     },
177
178     /**
179      * Adds that class that indicates that an element is highlighted.
180      */
181     startHighlight: function () {
182       // Animations.
183       var that = this;
184       // Use a timeout to grab the next available animation frame.
185       that.$el.addClass('quickedit-highlighted');
186     },
187
188     /**
189      * Removes the class that indicates that an element is highlighted.
190      */
191     stopHighlight: function () {
192       this.$el.removeClass('quickedit-highlighted');
193     },
194
195     /**
196      * Removes the class that indicates that an element as editable.
197      */
198     prepareEdit: function () {
199       this.$el.addClass('quickedit-editing');
200
201       // Allow the field to be styled differently while editing in a pop-up
202       // in-place editor.
203       if (this.editorView.getQuickEditUISettings().popup) {
204         this.$el.addClass('quickedit-editor-is-popup');
205       }
206     },
207
208     /**
209      * Removes the class that indicates that an element is being edited.
210      *
211      * Reapplies the class that indicates that a candidate editable element is
212      * again available to be edited.
213      */
214     stopEdit: function () {
215       this.$el.removeClass('quickedit-highlighted quickedit-editing');
216
217       // Done editing in a pop-up in-place editor; remove the class.
218       if (this.editorView.getQuickEditUISettings().popup) {
219         this.$el.removeClass('quickedit-editor-is-popup');
220       }
221
222       // Make the other editors show up again.
223       $('.quickedit-candidate').addClass('quickedit-editable');
224     },
225
226     /**
227      * Adds padding around the editable element to make it pop visually.
228      */
229     _pad: function () {
230       // Early return if the element has already been padded.
231       if (this.$el.data('quickedit-padded')) {
232         return;
233       }
234       var self = this;
235
236       // Add 5px padding for readability. This means we'll freeze the current
237       // width and *then* add 5px padding, hence ensuring the padding is added
238       // "on the outside".
239       // 1) Freeze the width (if it's not already set); don't use animations.
240       if (this.$el[0].style.width === '') {
241         this._widthAttributeIsEmpty = true;
242         this.$el
243           .addClass('quickedit-animate-disable-width')
244           .css('width', this.$el.width());
245       }
246
247       // 2) Add padding; use animations.
248       var posProp = this._getPositionProperties(this.$el);
249       setTimeout(function () {
250         // Re-enable width animations (padding changes affect width too!).
251         self.$el.removeClass('quickedit-animate-disable-width');
252
253         // Pad the editable.
254         self.$el
255           .css({
256             'position': 'relative',
257             'top': posProp.top - 5 + 'px',
258             'left': posProp.left - 5 + 'px',
259             'padding-top': posProp['padding-top'] + 5 + 'px',
260             'padding-left': posProp['padding-left'] + 5 + 'px',
261             'padding-right': posProp['padding-right'] + 5 + 'px',
262             'padding-bottom': posProp['padding-bottom'] + 5 + 'px',
263             'margin-bottom': posProp['margin-bottom'] - 10 + 'px'
264           })
265           .data('quickedit-padded', true);
266       }, 0);
267     },
268
269     /**
270      * Removes the padding around the element being edited when editing ceases.
271      */
272     _unpad: function () {
273       // Early return if the element has not been padded.
274       if (!this.$el.data('quickedit-padded')) {
275         return;
276       }
277       var self = this;
278
279       // 1) Set the empty width again.
280       if (this._widthAttributeIsEmpty) {
281         this.$el
282           .addClass('quickedit-animate-disable-width')
283           .css('width', '');
284       }
285
286       // 2) Remove padding; use animations (these will run simultaneously with)
287       // the fading out of the toolbar as its gets removed).
288       var posProp = this._getPositionProperties(this.$el);
289       setTimeout(function () {
290         // Re-enable width animations (padding changes affect width too!).
291         self.$el.removeClass('quickedit-animate-disable-width');
292
293         // Unpad the editable.
294         self.$el
295           .css({
296             'position': 'relative',
297             'top': posProp.top + 5 + 'px',
298             'left': posProp.left + 5 + 'px',
299             'padding-top': posProp['padding-top'] - 5 + 'px',
300             'padding-left': posProp['padding-left'] - 5 + 'px',
301             'padding-right': posProp['padding-right'] - 5 + 'px',
302             'padding-bottom': posProp['padding-bottom'] - 5 + 'px',
303             'margin-bottom': posProp['margin-bottom'] + 10 + 'px'
304           });
305       }, 0);
306       // Remove the marker that indicates that this field has padding. This is
307       // done outside the timed out function above so that we don't get numerous
308       // queued functions that will remove padding before the data marker has
309       // been removed.
310       this.$el.removeData('quickedit-padded');
311     },
312
313     /**
314      * Gets the top and left properties of an element.
315      *
316      * Convert extraneous values and information into numbers ready for
317      * subtraction.
318      *
319      * @param {jQuery} $e
320      *   The element to get position properties from.
321      *
322      * @return {object}
323      *   An object containing css values for the needed properties.
324      */
325     _getPositionProperties: function ($e) {
326       var p;
327       var r = {};
328       var props = [
329         'top', 'left', 'bottom', 'right',
330         'padding-top', 'padding-left', 'padding-right', 'padding-bottom',
331         'margin-bottom'
332       ];
333
334       var propCount = props.length;
335       for (var i = 0; i < propCount; i++) {
336         p = props[i];
337         r[p] = parseInt(this._replaceBlankPosition($e.css(p)), 10);
338       }
339       return r;
340     },
341
342     /**
343      * Replaces blank or 'auto' CSS `position: <value>` values with "0px".
344      *
345      * @param {string} [pos]
346      *   The value for a CSS position declaration.
347      *
348      * @return {string}
349      *   A CSS value that is valid for `position`.
350      */
351     _replaceBlankPosition: function (pos) {
352       if (pos === 'auto' || !pos) {
353         pos = '0px';
354       }
355       return pos;
356     }
357
358   });
359
360 })(jQuery, Backbone, Drupal);