Mercurial > hg > cmmr2012-drupal-site
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)); |