Mercurial > hg > cmmr2012-drupal-site
comparison core/modules/quickedit/js/views/EntityToolbarView.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 provides an entity level toolbar. | |
4 */ | |
5 | |
6 (function ($, _, Backbone, Drupal, debounce) { | |
7 Drupal.quickedit.EntityToolbarView = Backbone.View.extend(/** @lends Drupal.quickedit.EntityToolbarView# */{ | |
8 | |
9 /** | |
10 * @type {jQuery} | |
11 */ | |
12 _fieldToolbarRoot: null, | |
13 | |
14 /** | |
15 * @return {object} | |
16 * A map of events. | |
17 */ | |
18 events() { | |
19 const map = { | |
20 'click button.action-save': 'onClickSave', | |
21 'click button.action-cancel': 'onClickCancel', | |
22 mouseenter: 'onMouseenter', | |
23 }; | |
24 return map; | |
25 }, | |
26 | |
27 /** | |
28 * @constructs | |
29 * | |
30 * @augments Backbone.View | |
31 * | |
32 * @param {object} options | |
33 * Options to construct the view. | |
34 * @param {Drupal.quickedit.AppModel} options.appModel | |
35 * A quickedit `AppModel` to use in the view. | |
36 */ | |
37 initialize(options) { | |
38 const that = this; | |
39 this.appModel = options.appModel; | |
40 this.$entity = $(this.model.get('el')); | |
41 | |
42 // Rerender whenever the entity state changes. | |
43 this.listenTo(this.model, 'change:isActive change:isDirty change:state', this.render); | |
44 // Also rerender whenever a different field is highlighted or activated. | |
45 this.listenTo(this.appModel, 'change:highlightedField change:activeField', this.render); | |
46 // Rerender when a field of the entity changes state. | |
47 this.listenTo(this.model.get('fields'), 'change:state', this.fieldStateChange); | |
48 | |
49 // Reposition the entity toolbar as the viewport and the position within | |
50 // the viewport changes. | |
51 $(window).on('resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit', debounce($.proxy(this.windowChangeHandler, this), 150)); | |
52 | |
53 // Adjust the fence placement within which the entity toolbar may be | |
54 // positioned. | |
55 $(document).on('drupalViewportOffsetChange.quickedit', (event, offsets) => { | |
56 if (that.$fence) { | |
57 that.$fence.css(offsets); | |
58 } | |
59 }); | |
60 | |
61 // Set the entity toolbar DOM element as the el for this view. | |
62 const $toolbar = this.buildToolbarEl(); | |
63 this.setElement($toolbar); | |
64 this._fieldToolbarRoot = $toolbar.find('.quickedit-toolbar-field').get(0); | |
65 | |
66 // Initial render. | |
67 this.render(); | |
68 }, | |
69 | |
70 /** | |
71 * @inheritdoc | |
72 * | |
73 * @return {Drupal.quickedit.EntityToolbarView} | |
74 * The entity toolbar view. | |
75 */ | |
76 render() { | |
77 if (this.model.get('isActive')) { | |
78 // If the toolbar container doesn't exist, create it. | |
79 const $body = $('body'); | |
80 if ($body.children('#quickedit-entity-toolbar').length === 0) { | |
81 $body.append(this.$el); | |
82 } | |
83 // The fence will define a area on the screen that the entity toolbar | |
84 // will be position within. | |
85 if ($body.children('#quickedit-toolbar-fence').length === 0) { | |
86 this.$fence = $(Drupal.theme('quickeditEntityToolbarFence')) | |
87 .css(Drupal.displace()) | |
88 .appendTo($body); | |
89 } | |
90 // Adds the entity title to the toolbar. | |
91 this.label(); | |
92 | |
93 // Show the save and cancel buttons. | |
94 this.show('ops'); | |
95 // If render is being called and the toolbar is already visible, just | |
96 // reposition it. | |
97 this.position(); | |
98 } | |
99 | |
100 // The save button text and state varies with the state of the entity | |
101 // model. | |
102 const $button = this.$el.find('.quickedit-button.action-save'); | |
103 const isDirty = this.model.get('isDirty'); | |
104 // Adjust the save button according to the state of the model. | |
105 switch (this.model.get('state')) { | |
106 // Quick editing is active, but no field is being edited. | |
107 case 'opened': | |
108 // The saving throbber is not managed by AJAX system. The | |
109 // EntityToolbarView manages this visual element. | |
110 $button | |
111 .removeClass('action-saving icon-throbber icon-end') | |
112 .text(Drupal.t('Save')) | |
113 .removeAttr('disabled') | |
114 .attr('aria-hidden', !isDirty); | |
115 break; | |
116 | |
117 // The changes to the fields of the entity are being committed. | |
118 case 'committing': | |
119 $button | |
120 .addClass('action-saving icon-throbber icon-end') | |
121 .text(Drupal.t('Saving')) | |
122 .attr('disabled', 'disabled'); | |
123 break; | |
124 | |
125 default: | |
126 $button.attr('aria-hidden', true); | |
127 break; | |
128 } | |
129 | |
130 return this; | |
131 }, | |
132 | |
133 /** | |
134 * @inheritdoc | |
135 */ | |
136 remove() { | |
137 // Remove additional DOM elements controlled by this View. | |
138 this.$fence.remove(); | |
139 | |
140 // Stop listening to additional events. | |
141 $(window).off('resize.quickedit scroll.quickedit drupalViewportOffsetChange.quickedit'); | |
142 $(document).off('drupalViewportOffsetChange.quickedit'); | |
143 | |
144 Backbone.View.prototype.remove.call(this); | |
145 }, | |
146 | |
147 /** | |
148 * Repositions the entity toolbar on window scroll and resize. | |
149 * | |
150 * @param {jQuery.Event} event | |
151 * The scroll or resize event. | |
152 */ | |
153 windowChangeHandler(event) { | |
154 this.position(); | |
155 }, | |
156 | |
157 /** | |
158 * Determines the actions to take given a change of state. | |
159 * | |
160 * @param {Drupal.quickedit.FieldModel} model | |
161 * The `FieldModel` model. | |
162 * @param {string} state | |
163 * The state of the associated field. One of | |
164 * {@link Drupal.quickedit.FieldModel.states}. | |
165 */ | |
166 fieldStateChange(model, state) { | |
167 switch (state) { | |
168 case 'active': | |
169 this.render(); | |
170 break; | |
171 | |
172 case 'invalid': | |
173 this.render(); | |
174 break; | |
175 } | |
176 }, | |
177 | |
178 /** | |
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 } | |
224 break; | |
225 | |
226 case 3: | |
227 // Position against a highlighted field. | |
228 highlightedField = Drupal.quickedit.app.model.get('highlightedField'); | |
229 of = highlightedField && highlightedField.editorView && highlightedField.editorView.getEditedElement(); | |
230 delay = 250; | |
231 break; | |
232 | |
233 default: { | |
234 const fieldModels = this.model.get('fields').models; | |
235 let topMostPosition = 1000000; | |
236 let topMostField = null; | |
237 // Position against the topmost field. | |
238 for (let i = 0; i < fieldModels.length; i++) { | |
239 const pos = fieldModels[i].get('el').getBoundingClientRect().top; | |
240 if (pos < topMostPosition) { | |
241 topMostPosition = pos; | |
242 topMostField = fieldModels[i]; | |
243 } | |
244 } | |
245 of = topMostField.get('el'); | |
246 delay = 50; | |
247 break; | |
248 } | |
249 } | |
250 // Prepare to check the next possible element to position against. | |
251 check++; | |
252 } while (!of); | |
253 | |
254 /** | |
255 * Refines the positioning algorithm of jquery.ui.position(). | |
256 * | |
257 * Invoked as the 'using' callback of jquery.ui.position() in | |
258 * positionToolbar(). | |
259 * | |
260 * @param {*} view | |
261 * The view the positions will be calculated from. | |
262 * @param {object} suggested | |
263 * A hash of top and left values for the position that should be set. It | |
264 * can be forwarded to .css() or .animate(). | |
265 * @param {object} info | |
266 * The position and dimensions of both the 'my' element and the 'of' | |
267 * elements, as well as calculations to their relative position. This | |
268 * object contains the following properties: | |
269 * @param {object} info.element | |
270 * A hash that contains information about the HTML element that will be | |
271 * positioned. Also known as the 'my' element. | |
272 * @param {object} info.target | |
273 * A hash that contains information about the HTML element that the | |
274 * 'my' element will be positioned against. Also known as the 'of' | |
275 * element. | |
276 */ | |
277 function refinePosition(view, suggested, info) { | |
278 // Determine if the pointer should be on the top or bottom. | |
279 const isBelow = suggested.top > info.target.top; | |
280 info.element.element.toggleClass('quickedit-toolbar-pointer-top', isBelow); | |
281 // Don't position the toolbar past the first or last editable field if | |
282 // the entity is the target. | |
283 if (view.$entity[0] === info.target.element[0]) { | |
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 } | |
291 // Don't let the toolbar go outside the fence. | |
292 const fenceTop = view.$fence.offset().top; | |
293 const fenceHeight = view.$fence.height(); | |
294 const toolbarHeight = info.element.element.outerHeight(true); | |
295 if (suggested.top < fenceTop) { | |
296 suggested.top = fenceTop; | |
297 } | |
298 else if ((suggested.top + toolbarHeight) > (fenceTop + fenceHeight)) { | |
299 suggested.top = (fenceTop + fenceHeight) - toolbarHeight; | |
300 } | |
301 // Position the toolbar. | |
302 info.element.element.css({ | |
303 left: Math.floor(suggested.left), | |
304 top: Math.floor(suggested.top), | |
305 }); | |
306 } | |
307 | |
308 /** | |
309 * Calls the jquery.ui.position() method on the $el of this view. | |
310 */ | |
311 function positionToolbar() { | |
312 that.$el | |
313 .position({ | |
314 my: `${edge} bottom`, | |
315 // Move the toolbar 1px towards the start edge of the 'of' element, | |
316 // plus any horizontal padding that may have been added to the | |
317 // element that is being added, to prevent unwanted horizontal | |
318 // movement. | |
319 at: `${edge}+${1 + horizontalPadding} top`, | |
320 of, | |
321 collision: 'flipfit', | |
322 using: refinePosition.bind(null, that), | |
323 within: that.$fence, | |
324 }) | |
325 // Resize the toolbar to match the dimensions of the field, up to a | |
326 // maximum width that is equal to 90% of the field's width. | |
327 .css({ | |
328 'max-width': (document.documentElement.clientWidth < 450) ? document.documentElement.clientWidth : 450, | |
329 // Set a minimum width of 240px for the entity toolbar, or the width | |
330 // of the client if it is less than 240px, so that the toolbar | |
331 // never folds up into a squashed and jumbled mess. | |
332 'min-width': (document.documentElement.clientWidth < 240) ? document.documentElement.clientWidth : 240, | |
333 width: '100%', | |
334 }); | |
335 } | |
336 | |
337 // Uses the jQuery.ui.position() method. Use a timeout to move the toolbar | |
338 // only after the user has focused on an editable for 250ms. This prevents | |
339 // the toolbar from jumping around the screen. | |
340 this.timer = setTimeout(() => { | |
341 // Render the position in the next execution cycle, so that animations | |
342 // on the field have time to process. This is not strictly speaking, a | |
343 // guarantee that all animations will be finished, but it's a simple | |
344 // way to get better positioning without too much additional code. | |
345 _.defer(positionToolbar); | |
346 }, delay); | |
347 }, | |
348 | |
349 /** | |
350 * Set the model state to 'saving' when the save button is clicked. | |
351 * | |
352 * @param {jQuery.Event} event | |
353 * The click event. | |
354 */ | |
355 onClickSave(event) { | |
356 event.stopPropagation(); | |
357 event.preventDefault(); | |
358 // Save the model. | |
359 this.model.set('state', 'committing'); | |
360 }, | |
361 | |
362 /** | |
363 * Sets the model state to candidate when the cancel button is clicked. | |
364 * | |
365 * @param {jQuery.Event} event | |
366 * The click event. | |
367 */ | |
368 onClickCancel(event) { | |
369 event.preventDefault(); | |
370 this.model.set('state', 'deactivating'); | |
371 }, | |
372 | |
373 /** | |
374 * Clears the timeout that will eventually reposition the entity toolbar. | |
375 * | |
376 * Without this, it may reposition itself, away from the user's cursor! | |
377 * | |
378 * @param {jQuery.Event} event | |
379 * The mouse event. | |
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, | |
422 top: this.$entity.offset().top, | |
423 }); | |
424 | |
425 return $toolbar; | |
426 }, | |
427 | |
428 /** | |
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)); |