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