comparison core/modules/quickedit/js/views/EntityToolbarView.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 provides an entity level toolbar. 3 * A Backbone View that provides an entity level toolbar.
4 */ 4 */
5 5
6 (function ($, _, Backbone, Drupal, debounce) { 6 (function($, _, Backbone, Drupal, debounce) {
7 Drupal.quickedit.EntityToolbarView = Backbone.View.extend(/** @lends Drupal.quickedit.EntityToolbarView# */{ 7 Drupal.quickedit.EntityToolbarView = Backbone.View.extend(
8 8 /** @lends Drupal.quickedit.EntityToolbarView# */ {
9 /** 9 /**
10 * @type {jQuery} 10 * @type {jQuery}
11 */ 11 */
12 _fieldToolbarRoot: null, 12 _fieldToolbarRoot: null,
13 13
14 /** 14 /**
15 * @return {object} 15 * @return {object}
16 * A map of events. 16 * A map of events.
17 */ 17 */
18 events() { 18 events() {
19 const map = { 19 const map = {
20 'click button.action-save': 'onClickSave', 20 'click button.action-save': 'onClickSave',
21 'click button.action-cancel': 'onClickCancel', 21 'click button.action-cancel': 'onClickCancel',
22 mouseenter: 'onMouseenter', 22 mouseenter: 'onMouseenter',
23 }; 23 };
24 return map; 24 return map;
25 }, 25 },
26 26
27 /** 27 /**
28 * @constructs 28 * @constructs
29 * 29 *
30 * @augments Backbone.View 30 * @augments Backbone.View
31 * 31 *
32 * @param {object} options 32 * @param {object} options
33 * Options to construct the view. 33 * Options to construct the view.
34 * @param {Drupal.quickedit.AppModel} options.appModel 34 * @param {Drupal.quickedit.AppModel} options.appModel
35 * A quickedit `AppModel` to use in the view. 35 * A quickedit `AppModel` to use in the view.
36 */ 36 */
37 initialize(options) { 37 initialize(options) {
38 const that = this; 38 const that = this;
39 this.appModel = options.appModel; 39 this.appModel = options.appModel;
40 this.$entity = $(this.model.get('el')); 40 this.$entity = $(this.model.get('el'));
41 41
42 // Rerender whenever the entity state changes. 42 // Rerender whenever the entity state changes.
43 this.listenTo(this.model, 'change:isActive change:isDirty change:state', this.render); 43 this.listenTo(
44 // Also rerender whenever a different field is highlighted or activated. 44 this.model,
45 this.listenTo(this.appModel, 'change:highlightedField change:activeField', this.render); 45 'change:isActive change:isDirty change:state',
46 // Rerender when a field of the entity changes state. 46 this.render,
47 this.listenTo(this.model.get('fields'), 'change:state', this.fieldStateChange); 47 );
48 48 // Also rerender whenever a different field is highlighted or activated.
49 // Reposition the entity toolbar as the viewport and the position within 49 this.listenTo(
50 // the viewport changes. 50 this.appModel,
51 $(window).on('resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit', debounce($.proxy(this.windowChangeHandler, this), 150)); 51 'change:highlightedField change:activeField',
52 52 this.render,
53 // Adjust the fence placement within which the entity toolbar may be 53 );
54 // positioned. 54 // Rerender when a field of the entity changes state.
55 $(document).on('drupalViewportOffsetChange.quickedit', (event, offsets) => { 55 this.listenTo(
56 if (that.$fence) { 56 this.model.get('fields'),
57 that.$fence.css(offsets); 57 'change:state',
58 this.fieldStateChange,
59 );
60
61 // Reposition the entity toolbar as the viewport and the position within
62 // the viewport changes.
63 $(window).on(
64 'resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit',
65 debounce($.proxy(this.windowChangeHandler, this), 150),
66 );
67
68 // Adjust the fence placement within which the entity toolbar may be
69 // positioned.
70 $(document).on(
71 'drupalViewportOffsetChange.quickedit',
72 (event, offsets) => {
73 if (that.$fence) {
74 that.$fence.css(offsets);
75 }
76 },
77 );
78
79 // Set the entity toolbar DOM element as the el for this view.
80 const $toolbar = this.buildToolbarEl();
81 this.setElement($toolbar);
82 this._fieldToolbarRoot = $toolbar
83 .find('.quickedit-toolbar-field')
84 .get(0);
85
86 // Initial render.
87 this.render();
88 },
89
90 /**
91 * @inheritdoc
92 *
93 * @return {Drupal.quickedit.EntityToolbarView}
94 * The entity toolbar view.
95 */
96 render() {
97 if (this.model.get('isActive')) {
98 // If the toolbar container doesn't exist, create it.
99 const $body = $('body');
100 if ($body.children('#quickedit-entity-toolbar').length === 0) {
101 $body.append(this.$el);
102 }
103 // The fence will define a area on the screen that the entity toolbar
104 // will be position within.
105 if ($body.children('#quickedit-toolbar-fence').length === 0) {
106 this.$fence = $(Drupal.theme('quickeditEntityToolbarFence'))
107 .css(Drupal.displace())
108 .appendTo($body);
109 }
110 // Adds the entity title to the toolbar.
111 this.label();
112
113 // Show the save and cancel buttons.
114 this.show('ops');
115 // If render is being called and the toolbar is already visible, just
116 // reposition it.
117 this.position();
58 } 118 }
59 }); 119
60 120 // The save button text and state varies with the state of the entity
61 // Set the entity toolbar DOM element as the el for this view. 121 // model.
62 const $toolbar = this.buildToolbarEl(); 122 const $button = this.$el.find('.quickedit-button.action-save');
63 this.setElement($toolbar); 123 const isDirty = this.model.get('isDirty');
64 this._fieldToolbarRoot = $toolbar.find('.quickedit-toolbar-field').get(0); 124 // Adjust the save button according to the state of the model.
65 125 switch (this.model.get('state')) {
66 // Initial render. 126 // Quick editing is active, but no field is being edited.
67 this.render(); 127 case 'opened':
68 }, 128 // The saving throbber is not managed by AJAX system. The
69 129 // EntityToolbarView manages this visual element.
70 /** 130 $button
71 * @inheritdoc 131 .removeClass('action-saving icon-throbber icon-end')
72 * 132 .text(Drupal.t('Save'))
73 * @return {Drupal.quickedit.EntityToolbarView} 133 .removeAttr('disabled')
74 * The entity toolbar view. 134 .attr('aria-hidden', !isDirty);
75 */ 135 break;
76 render() { 136
77 if (this.model.get('isActive')) { 137 // The changes to the fields of the entity are being committed.
78 // If the toolbar container doesn't exist, create it. 138 case 'committing':
79 const $body = $('body'); 139 $button
80 if ($body.children('#quickedit-entity-toolbar').length === 0) { 140 .addClass('action-saving icon-throbber icon-end')
81 $body.append(this.$el); 141 .text(Drupal.t('Saving'))
142 .attr('disabled', 'disabled');
143 break;
144
145 default:
146 $button.attr('aria-hidden', true);
147 break;
82 } 148 }
83 // The fence will define a area on the screen that the entity toolbar 149
84 // will be position within. 150 return this;
85 if ($body.children('#quickedit-toolbar-fence').length === 0) { 151 },
86 this.$fence = $(Drupal.theme('quickeditEntityToolbarFence')) 152
87 .css(Drupal.displace()) 153 /**
88 .appendTo($body); 154 * @inheritdoc
155 */
156 remove() {
157 // Remove additional DOM elements controlled by this View.
158 this.$fence.remove();
159
160 // Stop listening to additional events.
161 $(window).off(
162 'resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit',
163 );
164 $(document).off('drupalViewportOffsetChange.quickedit');
165
166 Backbone.View.prototype.remove.call(this);
167 },
168
169 /**
170 * Repositions the entity toolbar on window scroll and resize.
171 *
172 * @param {jQuery.Event} event
173 * The scroll or resize event.
174 */
175 windowChangeHandler(event) {
176 this.position();
177 },
178
179 /**
180 * Determines the actions to take given a change of state.
181 *
182 * @param {Drupal.quickedit.FieldModel} model
183 * The `FieldModel` model.
184 * @param {string} state
185 * The state of the associated field. One of
186 * {@link Drupal.quickedit.FieldModel.states}.
187 */
188 fieldStateChange(model, state) {
189 switch (state) {
190 case 'active':
191 this.render();
192 break;
193
194 case 'invalid':
195 this.render();
196 break;
89 } 197 }
90 // Adds the entity title to the toolbar. 198 },
91 this.label(); 199
92 200 /**
93 // Show the save and cancel buttons. 201 * Uses the jQuery.ui.position() method to position the entity toolbar.
94 this.show('ops'); 202 *
95 // If render is being called and the toolbar is already visible, just 203 * @param {HTMLElement} [element]
96 // reposition it. 204 * The element against which the entity toolbar is positioned.
97 this.position(); 205 */
98 } 206 position(element) {
99 207 clearTimeout(this.timer);
100 // The save button text and state varies with the state of the entity 208
101 // model. 209 const that = this;
102 const $button = this.$el.find('.quickedit-button.action-save'); 210 // Vary the edge of the positioning according to the direction of language
103 const isDirty = this.model.get('isDirty'); 211 // in the document.
104 // Adjust the save button according to the state of the model. 212 const edge = document.documentElement.dir === 'rtl' ? 'right' : 'left';
105 switch (this.model.get('state')) { 213 // A time unit to wait until the entity toolbar is repositioned.
106 // Quick editing is active, but no field is being edited. 214 let delay = 0;
107 case 'opened': 215 // Determines what check in the series of checks below should be
108 // The saving throbber is not managed by AJAX system. The 216 // evaluated.
109 // EntityToolbarView manages this visual element. 217 let check = 0;
110 $button 218 // When positioned against an active field that has padding, we should
111 .removeClass('action-saving icon-throbber icon-end') 219 // ignore that padding when positioning the toolbar, to not unnecessarily
112 .text(Drupal.t('Save')) 220 // move the toolbar horizontally, which feels annoying.
113 .removeAttr('disabled') 221 let horizontalPadding = 0;
114 .attr('aria-hidden', !isDirty); 222 let of;
115 break; 223 let activeField;
116 224 let highlightedField;
117 // The changes to the fields of the entity are being committed. 225 // There are several elements in the page that the entity toolbar might be
118 case 'committing': 226 // positioned against. They are considered below in a priority order.
119 $button 227 do {
120 .addClass('action-saving icon-throbber icon-end') 228 switch (check) {
121 .text(Drupal.t('Saving')) 229 case 0:
122 .attr('disabled', 'disabled'); 230 // Position against a specific element.
123 break; 231 of = element;
124 232 break;
125 default: 233
126 $button.attr('aria-hidden', true); 234 case 1:
127 break; 235 // Position against a form container.
128 } 236 activeField = Drupal.quickedit.app.model.get('activeField');
129 237 of =
130 return this; 238 activeField &&
131 }, 239 activeField.editorView &&
132 240 activeField.editorView.$formContainer &&
133 /** 241 activeField.editorView.$formContainer.find('.quickedit-form');
134 * @inheritdoc 242 break;
135 */ 243
136 remove() { 244 case 2:
137 // Remove additional DOM elements controlled by this View. 245 // Position against an active field.
138 this.$fence.remove(); 246 of =
139 247 activeField &&
140 // Stop listening to additional events. 248 activeField.editorView &&
141 $(window).off('resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit'); 249 activeField.editorView.getEditedElement();
142 $(document).off('drupalViewportOffsetChange.quickedit'); 250 if (
143 251 activeField &&
144 Backbone.View.prototype.remove.call(this); 252 activeField.editorView &&
145 }, 253 activeField.editorView.getQuickEditUISettings().padding
146 254 ) {
147 /** 255 horizontalPadding = 5;
148 * Repositions the entity toolbar on window scroll and resize. 256 }
149 * 257 break;
150 * @param {jQuery.Event} event 258
151 * The scroll or resize event. 259 case 3:
152 */ 260 // Position against a highlighted field.
153 windowChangeHandler(event) { 261 highlightedField = Drupal.quickedit.app.model.get(
154 this.position(); 262 'highlightedField',
155 }, 263 );
156 264 of =
157 /** 265 highlightedField &&
158 * Determines the actions to take given a change of state. 266 highlightedField.editorView &&
159 * 267 highlightedField.editorView.getEditedElement();
160 * @param {Drupal.quickedit.FieldModel} model 268 delay = 250;
161 * The `FieldModel` model. 269 break;
162 * @param {string} state 270
163 * The state of the associated field. One of 271 default: {
164 * {@link Drupal.quickedit.FieldModel.states}. 272 const fieldModels = this.model.get('fields').models;
165 */ 273 let topMostPosition = 1000000;
166 fieldStateChange(model, state) { 274 let topMostField = null;
167 switch (state) { 275 // Position against the topmost field.
168 case 'active': 276 for (let i = 0; i < fieldModels.length; i++) {
169 this.render(); 277 const pos = fieldModels[i].get('el').getBoundingClientRect()
170 break; 278 .top;
171 279 if (pos < topMostPosition) {
172 case 'invalid': 280 topMostPosition = pos;
173 this.render(); 281 topMostField = fieldModels[i];
174 break; 282 }
175 } 283 }
176 }, 284 of = topMostField.get('el');
177 285 delay = 50;
178 /** 286 break;
179 * Uses the jQuery.ui.position() method to position the entity toolbar.
180 *
181 * @param {HTMLElement} [element]
182 * The element against which the entity toolbar is positioned.
183 */
184 position(element) {
185 clearTimeout(this.timer);
186
187 const that = this;
188 // Vary the edge of the positioning according to the direction of language
189 // in the document.
190 const edge = (document.documentElement.dir === 'rtl') ? 'right' : 'left';
191 // A time unit to wait until the entity toolbar is repositioned.
192 let delay = 0;
193 // Determines what check in the series of checks below should be
194 // evaluated.
195 let check = 0;
196 // When positioned against an active field that has padding, we should
197 // ignore that padding when positioning the toolbar, to not unnecessarily
198 // move the toolbar horizontally, which feels annoying.
199 let horizontalPadding = 0;
200 let of;
201 let activeField;
202 let highlightedField;
203 // There are several elements in the page that the entity toolbar might be
204 // positioned against. They are considered below in a priority order.
205 do {
206 switch (check) {
207 case 0:
208 // Position against a specific element.
209 of = element;
210 break;
211
212 case 1:
213 // Position against a form container.
214 activeField = Drupal.quickedit.app.model.get('activeField');
215 of = activeField && activeField.editorView && activeField.editorView.$formContainer && activeField.editorView.$formContainer.find('.quickedit-form');
216 break;
217
218 case 2:
219 // Position against an active field.
220 of = activeField && activeField.editorView && activeField.editorView.getEditedElement();
221 if (activeField && activeField.editorView && activeField.editorView.getQuickEditUISettings().padding) {
222 horizontalPadding = 5;
223 } 287 }
224 break; 288 }
225 289 // Prepare to check the next possible element to position against.
226 case 3: 290 check++;
227 // Position against a highlighted field. 291 } while (!of);
228 highlightedField = Drupal.quickedit.app.model.get('highlightedField'); 292
229 of = highlightedField && highlightedField.editorView && highlightedField.editorView.getEditedElement(); 293 /**
230 delay = 250; 294 * Refines the positioning algorithm of jquery.ui.position().
231 break; 295 *
232 296 * Invoked as the 'using' callback of jquery.ui.position() in
233 default: { 297 * positionToolbar().
234 const fieldModels = this.model.get('fields').models; 298 *
235 let topMostPosition = 1000000; 299 * @param {*} view
236 let topMostField = null; 300 * The view the positions will be calculated from.
237 // Position against the topmost field. 301 * @param {object} suggested
238 for (let i = 0; i < fieldModels.length; i++) { 302 * A hash of top and left values for the position that should be set. It
239 const pos = fieldModels[i].get('el').getBoundingClientRect().top; 303 * can be forwarded to .css() or .animate().
240 if (pos < topMostPosition) { 304 * @param {object} info
241 topMostPosition = pos; 305 * The position and dimensions of both the 'my' element and the 'of'
242 topMostField = fieldModels[i]; 306 * elements, as well as calculations to their relative position. This
243 } 307 * object contains the following properties:
308 * @param {object} info.element
309 * A hash that contains information about the HTML element that will be
310 * positioned. Also known as the 'my' element.
311 * @param {object} info.target
312 * A hash that contains information about the HTML element that the
313 * 'my' element will be positioned against. Also known as the 'of'
314 * element.
315 */
316 function refinePosition(view, suggested, info) {
317 // Determine if the pointer should be on the top or bottom.
318 const isBelow = suggested.top > info.target.top;
319 info.element.element.toggleClass(
320 'quickedit-toolbar-pointer-top',
321 isBelow,
322 );
323 // Don't position the toolbar past the first or last editable field if
324 // the entity is the target.
325 if (view.$entity[0] === info.target.element[0]) {
326 // Get the first or last field according to whether the toolbar is
327 // above or below the entity.
328 const $field = view.$entity
329 .find('.quickedit-editable')
330 .eq(isBelow ? -1 : 0);
331 if ($field.length > 0) {
332 suggested.top = isBelow
333 ? $field.offset().top + $field.outerHeight(true)
334 : $field.offset().top - info.element.element.outerHeight(true);
244 } 335 }
245 of = topMostField.get('el');
246 delay = 50;
247 break;
248 } 336 }
337 // Don't let the toolbar go outside the fence.
338 const fenceTop = view.$fence.offset().top;
339 const fenceHeight = view.$fence.height();
340 const toolbarHeight = info.element.element.outerHeight(true);
341 if (suggested.top < fenceTop) {
342 suggested.top = fenceTop;
343 } else if (suggested.top + toolbarHeight > fenceTop + fenceHeight) {
344 suggested.top = fenceTop + fenceHeight - toolbarHeight;
345 }
346 // Position the toolbar.
347 info.element.element.css({
348 left: Math.floor(suggested.left),
349 top: Math.floor(suggested.top),
350 });
249 } 351 }
250 // Prepare to check the next possible element to position against. 352
251 check++; 353 /**
252 } while (!of); 354 * Calls the jquery.ui.position() method on the $el of this view.
253 355 */
254 /** 356 function positionToolbar() {
255 * Refines the positioning algorithm of jquery.ui.position(). 357 that.$el
256 * 358 .position({
257 * Invoked as the 'using' callback of jquery.ui.position() in 359 my: `${edge} bottom`,
258 * positionToolbar(). 360 // Move the toolbar 1px towards the start edge of the 'of' element,
259 * 361 // plus any horizontal padding that may have been added to the
260 * @param {*} view 362 // element that is being added, to prevent unwanted horizontal
261 * The view the positions will be calculated from. 363 // movement.
262 * @param {object} suggested 364 at: `${edge}+${1 + horizontalPadding} top`,
263 * A hash of top and left values for the position that should be set. It 365 of,
264 * can be forwarded to .css() or .animate(). 366 collision: 'flipfit',
265 * @param {object} info 367 using: refinePosition.bind(null, that),
266 * The position and dimensions of both the 'my' element and the 'of' 368 within: that.$fence,
267 * elements, as well as calculations to their relative position. This 369 })
268 * object contains the following properties: 370 // Resize the toolbar to match the dimensions of the field, up to a
269 * @param {object} info.element 371 // maximum width that is equal to 90% of the field's width.
270 * A hash that contains information about the HTML element that will be 372 .css({
271 * positioned. Also known as the 'my' element. 373 'max-width':
272 * @param {object} info.target 374 document.documentElement.clientWidth < 450
273 * A hash that contains information about the HTML element that the 375 ? document.documentElement.clientWidth
274 * 'my' element will be positioned against. Also known as the 'of' 376 : 450,
275 * element. 377 // Set a minimum width of 240px for the entity toolbar, or the width
276 */ 378 // of the client if it is less than 240px, so that the toolbar
277 function refinePosition(view, suggested, info) { 379 // never folds up into a squashed and jumbled mess.
278 // Determine if the pointer should be on the top or bottom. 380 'min-width':
279 const isBelow = suggested.top > info.target.top; 381 document.documentElement.clientWidth < 240
280 info.element.element.toggleClass('quickedit-toolbar-pointer-top', isBelow); 382 ? document.documentElement.clientWidth
281 // Don't position the toolbar past the first or last editable field if 383 : 240,
282 // the entity is the target. 384 width: '100%',
283 if (view.$entity[0] === info.target.element[0]) { 385 });
284 // Get the first or last field according to whether the toolbar is
285 // above or below the entity.
286 const $field = view.$entity.find('.quickedit-editable').eq((isBelow) ? -1 : 0);
287 if ($field.length > 0) {
288 suggested.top = (isBelow) ? ($field.offset().top + $field.outerHeight(true)) : $field.offset().top - info.element.element.outerHeight(true);
289 }
290 } 386 }
291 // Don't let the toolbar go outside the fence. 387
292 const fenceTop = view.$fence.offset().top; 388 // Uses the jQuery.ui.position() method. Use a timeout to move the toolbar
293 const fenceHeight = view.$fence.height(); 389 // only after the user has focused on an editable for 250ms. This prevents
294 const toolbarHeight = info.element.element.outerHeight(true); 390 // the toolbar from jumping around the screen.
295 if (suggested.top < fenceTop) { 391 this.timer = setTimeout(() => {
296 suggested.top = fenceTop; 392 // Render the position in the next execution cycle, so that animations
297 } 393 // on the field have time to process. This is not strictly speaking, a
298 else if ((suggested.top + toolbarHeight) > (fenceTop + fenceHeight)) { 394 // guarantee that all animations will be finished, but it's a simple
299 suggested.top = (fenceTop + fenceHeight) - toolbarHeight; 395 // way to get better positioning without too much additional code.
300 } 396 _.defer(positionToolbar);
301 // Position the toolbar. 397 }, delay);
302 info.element.element.css({ 398 },
303 left: Math.floor(suggested.left), 399
304 top: Math.floor(suggested.top), 400 /**
305 }); 401 * Set the model state to 'saving' when the save button is clicked.
306 } 402 *
307 403 * @param {jQuery.Event} event
308 /** 404 * The click event.
309 * Calls the jquery.ui.position() method on the $el of this view. 405 */
310 */ 406 onClickSave(event) {
311 function positionToolbar() { 407 event.stopPropagation();
312 that.$el 408 event.preventDefault();
313 .position({ 409 // Save the model.
314 my: `${edge} bottom`, 410 this.model.set('state', 'committing');
315 // Move the toolbar 1px towards the start edge of the 'of' element, 411 },
316 // plus any horizontal padding that may have been added to the 412
317 // element that is being added, to prevent unwanted horizontal 413 /**
318 // movement. 414 * Sets the model state to candidate when the cancel button is clicked.
319 at: `${edge}+${1 + horizontalPadding} top`, 415 *
320 of, 416 * @param {jQuery.Event} event
321 collision: 'flipfit', 417 * The click event.
322 using: refinePosition.bind(null, that), 418 */
323 within: that.$fence, 419 onClickCancel(event) {
324 }) 420 event.preventDefault();
325 // Resize the toolbar to match the dimensions of the field, up to a 421 this.model.set('state', 'deactivating');
326 // maximum width that is equal to 90% of the field's width. 422 },
327 .css({ 423
328 'max-width': (document.documentElement.clientWidth < 450) ? document.documentElement.clientWidth : 450, 424 /**
329 // Set a minimum width of 240px for the entity toolbar, or the width 425 * Clears the timeout that will eventually reposition the entity toolbar.
330 // of the client if it is less than 240px, so that the toolbar 426 *
331 // never folds up into a squashed and jumbled mess. 427 * Without this, it may reposition itself, away from the user's cursor!
332 'min-width': (document.documentElement.clientWidth < 240) ? document.documentElement.clientWidth : 240, 428 *
333 width: '100%', 429 * @param {jQuery.Event} event
334 }); 430 * The mouse event.
335 } 431 */
336 432 onMouseenter(event) {
337 // Uses the jQuery.ui.position() method. Use a timeout to move the toolbar 433 clearTimeout(this.timer);
338 // only after the user has focused on an editable for 250ms. This prevents 434 },
339 // the toolbar from jumping around the screen. 435
340 this.timer = setTimeout(() => { 436 /**
341 // Render the position in the next execution cycle, so that animations 437 * Builds the entity toolbar HTML; attaches to DOM; sets starting position.
342 // on the field have time to process. This is not strictly speaking, a 438 *
343 // guarantee that all animations will be finished, but it's a simple 439 * @return {jQuery}
344 // way to get better positioning without too much additional code. 440 * The toolbar element.
345 _.defer(positionToolbar); 441 */
346 }, delay); 442 buildToolbarEl() {
347 }, 443 const $toolbar = $(
348 444 Drupal.theme('quickeditEntityToolbar', {
349 /** 445 id: 'quickedit-entity-toolbar',
350 * Set the model state to 'saving' when the save button is clicked. 446 }),
351 * 447 );
352 * @param {jQuery.Event} event 448
353 * The click event. 449 $toolbar
354 */ 450 .find('.quickedit-toolbar-entity')
355 onClickSave(event) { 451 // Append the "ops" toolgroup into the toolbar.
356 event.stopPropagation(); 452 .prepend(
357 event.preventDefault(); 453 Drupal.theme('quickeditToolgroup', {
358 // Save the model. 454 classes: ['ops'],
359 this.model.set('state', 'committing'); 455 buttons: [
360 }, 456 {
361 457 label: Drupal.t('Save'),
362 /** 458 type: 'submit',
363 * Sets the model state to candidate when the cancel button is clicked. 459 classes: 'action-save quickedit-button icon',
364 * 460 attributes: {
365 * @param {jQuery.Event} event 461 'aria-hidden': true,
366 * The click event. 462 },
367 */ 463 },
368 onClickCancel(event) { 464 {
369 event.preventDefault(); 465 label: Drupal.t('Close'),
370 this.model.set('state', 'deactivating'); 466 classes:
371 }, 467 'action-cancel quickedit-button icon icon-close icon-only',
372 468 },
373 /** 469 ],
374 * Clears the timeout that will eventually reposition the entity toolbar. 470 }),
375 * 471 );
376 * Without this, it may reposition itself, away from the user's cursor! 472
377 * 473 // Give the toolbar a sensible starting position so that it doesn't
378 * @param {jQuery.Event} event 474 // animate on to the screen from a far off corner.
379 * The mouse event. 475 $toolbar.css({
380 */
381 onMouseenter(event) {
382 clearTimeout(this.timer);
383 },
384
385 /**
386 * Builds the entity toolbar HTML; attaches to DOM; sets starting position.
387 *
388 * @return {jQuery}
389 * The toolbar element.
390 */
391 buildToolbarEl() {
392 const $toolbar = $(Drupal.theme('quickeditEntityToolbar', {
393 id: 'quickedit-entity-toolbar',
394 }));
395
396 $toolbar
397 .find('.quickedit-toolbar-entity')
398 // Append the "ops" toolgroup into the toolbar.
399 .prepend(Drupal.theme('quickeditToolgroup', {
400 classes: ['ops'],
401 buttons: [
402 {
403 label: Drupal.t('Save'),
404 type: 'submit',
405 classes: 'action-save quickedit-button icon',
406 attributes: {
407 'aria-hidden': true,
408 },
409 },
410 {
411 label: Drupal.t('Close'),
412 classes: 'action-cancel quickedit-button icon icon-close icon-only',
413 },
414 ],
415 }));
416
417 // Give the toolbar a sensible starting position so that it doesn't
418 // animate on to the screen from a far off corner.
419 $toolbar
420 .css({
421 left: this.$entity.offset().left, 476 left: this.$entity.offset().left,
422 top: this.$entity.offset().top, 477 top: this.$entity.offset().top,
423 }); 478 });
424 479
425 return $toolbar; 480 return $toolbar;
481 },
482
483 /**
484 * Returns the DOM element that fields will attach their toolbars to.
485 *
486 * @return {jQuery}
487 * The DOM element that fields will attach their toolbars to.
488 */
489 getToolbarRoot() {
490 return this._fieldToolbarRoot;
491 },
492
493 /**
494 * Generates a state-dependent label for the entity toolbar.
495 */
496 label() {
497 // The entity label.
498 let label = '';
499 const entityLabel = this.model.get('label');
500
501 // Label of an active field, if it exists.
502 const activeField = Drupal.quickedit.app.model.get('activeField');
503 const activeFieldLabel =
504 activeField && activeField.get('metadata').label;
505 // Label of a highlighted field, if it exists.
506 const highlightedField = Drupal.quickedit.app.model.get(
507 'highlightedField',
508 );
509 const highlightedFieldLabel =
510 highlightedField && highlightedField.get('metadata').label;
511 // The label is constructed in a priority order.
512 if (activeFieldLabel) {
513 label = Drupal.theme('quickeditEntityToolbarLabel', {
514 entityLabel,
515 fieldLabel: activeFieldLabel,
516 });
517 } else if (highlightedFieldLabel) {
518 label = Drupal.theme('quickeditEntityToolbarLabel', {
519 entityLabel,
520 fieldLabel: highlightedFieldLabel,
521 });
522 } else {
523 // @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437
524 label = Drupal.checkPlain(entityLabel);
525 }
526
527 this.$el.find('.quickedit-toolbar-label').html(label);
528 },
529
530 /**
531 * Adds classes to a toolgroup.
532 *
533 * @param {string} toolgroup
534 * A toolgroup name.
535 * @param {string} classes
536 * A string of space-delimited class names that will be applied to the
537 * wrapping element of the toolbar group.
538 */
539 addClass(toolgroup, classes) {
540 this._find(toolgroup).addClass(classes);
541 },
542
543 /**
544 * Removes classes from a toolgroup.
545 *
546 * @param {string} toolgroup
547 * A toolgroup name.
548 * @param {string} classes
549 * A string of space-delimited class names that will be removed from the
550 * wrapping element of the toolbar group.
551 */
552 removeClass(toolgroup, classes) {
553 this._find(toolgroup).removeClass(classes);
554 },
555
556 /**
557 * Finds a toolgroup.
558 *
559 * @param {string} toolgroup
560 * A toolgroup name.
561 *
562 * @return {jQuery}
563 * The toolgroup DOM element.
564 */
565 _find(toolgroup) {
566 return this.$el.find(
567 `.quickedit-toolbar .quickedit-toolgroup.${toolgroup}`,
568 );
569 },
570
571 /**
572 * Shows a toolgroup.
573 *
574 * @param {string} toolgroup
575 * A toolgroup name.
576 */
577 show(toolgroup) {
578 this.$el.removeClass('quickedit-animate-invisible');
579 },
426 }, 580 },
427 581 );
428 /** 582 })(jQuery, _, Backbone, Drupal, Drupal.debounce);
429 * Returns the DOM element that fields will attach their toolbars to.
430 *
431 * @return {jQuery}
432 * The DOM element that fields will attach their toolbars to.
433 */
434 getToolbarRoot() {
435 return this._fieldToolbarRoot;
436 },
437
438 /**
439 * Generates a state-dependent label for the entity toolbar.
440 */
441 label() {
442 // The entity label.
443 let label = '';
444 const entityLabel = this.model.get('label');
445
446 // Label of an active field, if it exists.
447 const activeField = Drupal.quickedit.app.model.get('activeField');
448 const activeFieldLabel = activeField && activeField.get('metadata').label;
449 // Label of a highlighted field, if it exists.
450 const highlightedField = Drupal.quickedit.app.model.get('highlightedField');
451 const highlightedFieldLabel = highlightedField && highlightedField.get('metadata').label;
452 // The label is constructed in a priority order.
453 if (activeFieldLabel) {
454 label = Drupal.theme('quickeditEntityToolbarLabel', {
455 entityLabel,
456 fieldLabel: activeFieldLabel,
457 });
458 }
459 else if (highlightedFieldLabel) {
460 label = Drupal.theme('quickeditEntityToolbarLabel', {
461 entityLabel,
462 fieldLabel: highlightedFieldLabel,
463 });
464 }
465 else {
466 // @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437
467 label = Drupal.checkPlain(entityLabel);
468 }
469
470 this.$el
471 .find('.quickedit-toolbar-label')
472 .html(label);
473 },
474
475 /**
476 * Adds classes to a toolgroup.
477 *
478 * @param {string} toolgroup
479 * A toolgroup name.
480 * @param {string} classes
481 * A string of space-delimited class names that will be applied to the
482 * wrapping element of the toolbar group.
483 */
484 addClass(toolgroup, classes) {
485 this._find(toolgroup).addClass(classes);
486 },
487
488 /**
489 * Removes classes from a toolgroup.
490 *
491 * @param {string} toolgroup
492 * A toolgroup name.
493 * @param {string} classes
494 * A string of space-delimited class names that will be removed from the
495 * wrapping element of the toolbar group.
496 */
497 removeClass(toolgroup, classes) {
498 this._find(toolgroup).removeClass(classes);
499 },
500
501 /**
502 * Finds a toolgroup.
503 *
504 * @param {string} toolgroup
505 * A toolgroup name.
506 *
507 * @return {jQuery}
508 * The toolgroup DOM element.
509 */
510 _find(toolgroup) {
511 return this.$el.find(`.quickedit-toolbar .quickedit-toolgroup.${toolgroup}`);
512 },
513
514 /**
515 * Shows a toolgroup.
516 *
517 * @param {string} toolgroup
518 * A toolgroup name.
519 */
520 show(toolgroup) {
521 this.$el.removeClass('quickedit-animate-invisible');
522 },
523
524 });
525 }(jQuery, _, Backbone, Drupal, Drupal.debounce));