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