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