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