comparison core/modules/quickedit/js/views/FieldDecorationView.es6.js @ 0:c75dbcec494b

Initial commit from drush-created site
author Chris Cannam
date Thu, 05 Jul 2018 14:24:15 +0000
parents
children a9cd425dd02b
comparison
equal deleted inserted replaced
-1:000000000000 0:c75dbcec494b
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));