comparison core/modules/quickedit/js/views/FieldDecorationView.es6.js @ 4:a9cd425dd02b

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