Mercurial > hg > isophonics-drupal-site
comparison core/modules/quickedit/js/views/EntityToolbarView.es6.js @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 1fec387a4317 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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 var fieldModels = this.model.get('fields').models; | |
235 var topMostPosition = 1000000; | |
236 var 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 // Prepare to check the next possible element to position against. | |
250 check++; | |
251 } while (!of); | |
252 | |
253 /** | |
254 * Refines the positioning algorithm of jquery.ui.position(). | |
255 * | |
256 * Invoked as the 'using' callback of jquery.ui.position() in | |
257 * positionToolbar(). | |
258 * | |
259 * @param {*} view | |
260 * The view the positions will be calculated from. | |
261 * @param {object} suggested | |
262 * A hash of top and left values for the position that should be set. It | |
263 * can be forwarded to .css() or .animate(). | |
264 * @param {object} info | |
265 * The position and dimensions of both the 'my' element and the 'of' | |
266 * elements, as well as calculations to their relative position. This | |
267 * object contains the following properties: | |
268 * @param {object} info.element | |
269 * A hash that contains information about the HTML element that will be | |
270 * positioned. Also known as the 'my' element. | |
271 * @param {object} info.target | |
272 * A hash that contains information about the HTML element that the | |
273 * 'my' element will be positioned against. Also known as the 'of' | |
274 * element. | |
275 */ | |
276 function refinePosition(view, suggested, info) { | |
277 // Determine if the pointer should be on the top or bottom. | |
278 const isBelow = suggested.top > info.target.top; | |
279 info.element.element.toggleClass('quickedit-toolbar-pointer-top', isBelow); | |
280 // Don't position the toolbar past the first or last editable field if | |
281 // the entity is the target. | |
282 if (view.$entity[0] === info.target.element[0]) { | |
283 // Get the first or last field according to whether the toolbar is | |
284 // above or below the entity. | |
285 const $field = view.$entity.find('.quickedit-editable').eq((isBelow) ? -1 : 0); | |
286 if ($field.length > 0) { | |
287 suggested.top = (isBelow) ? ($field.offset().top + $field.outerHeight(true)) : $field.offset().top - info.element.element.outerHeight(true); | |
288 } | |
289 } | |
290 // Don't let the toolbar go outside the fence. | |
291 const fenceTop = view.$fence.offset().top; | |
292 const fenceHeight = view.$fence.height(); | |
293 const toolbarHeight = info.element.element.outerHeight(true); | |
294 if (suggested.top < fenceTop) { | |
295 suggested.top = fenceTop; | |
296 } | |
297 else if ((suggested.top + toolbarHeight) > (fenceTop + fenceHeight)) { | |
298 suggested.top = fenceTop + fenceHeight - toolbarHeight; | |
299 } | |
300 // Position the toolbar. | |
301 info.element.element.css({ | |
302 left: Math.floor(suggested.left), | |
303 top: Math.floor(suggested.top), | |
304 }); | |
305 } | |
306 | |
307 /** | |
308 * Calls the jquery.ui.position() method on the $el of this view. | |
309 */ | |
310 function positionToolbar() { | |
311 that.$el | |
312 .position({ | |
313 my: `${edge} bottom`, | |
314 // Move the toolbar 1px towards the start edge of the 'of' element, | |
315 // plus any horizontal padding that may have been added to the | |
316 // element that is being added, to prevent unwanted horizontal | |
317 // movement. | |
318 at: `${edge}+${1 + horizontalPadding} top`, | |
319 of, | |
320 collision: 'flipfit', | |
321 using: refinePosition.bind(null, that), | |
322 within: that.$fence, | |
323 }) | |
324 // Resize the toolbar to match the dimensions of the field, up to a | |
325 // maximum width that is equal to 90% of the field's width. | |
326 .css({ | |
327 'max-width': (document.documentElement.clientWidth < 450) ? document.documentElement.clientWidth : 450, | |
328 // Set a minimum width of 240px for the entity toolbar, or the width | |
329 // of the client if it is less than 240px, so that the toolbar | |
330 // never folds up into a squashed and jumbled mess. | |
331 'min-width': (document.documentElement.clientWidth < 240) ? document.documentElement.clientWidth : 240, | |
332 width: '100%', | |
333 }); | |
334 } | |
335 | |
336 // Uses the jQuery.ui.position() method. Use a timeout to move the toolbar | |
337 // only after the user has focused on an editable for 250ms. This prevents | |
338 // the toolbar from jumping around the screen. | |
339 this.timer = setTimeout(() => { | |
340 // Render the position in the next execution cycle, so that animations | |
341 // on the field have time to process. This is not strictly speaking, a | |
342 // guarantee that all animations will be finished, but it's a simple | |
343 // way to get better positioning without too much additional code. | |
344 _.defer(positionToolbar); | |
345 }, delay); | |
346 }, | |
347 | |
348 /** | |
349 * Set the model state to 'saving' when the save button is clicked. | |
350 * | |
351 * @param {jQuery.Event} event | |
352 * The click event. | |
353 */ | |
354 onClickSave(event) { | |
355 event.stopPropagation(); | |
356 event.preventDefault(); | |
357 // Save the model. | |
358 this.model.set('state', 'committing'); | |
359 }, | |
360 | |
361 /** | |
362 * Sets the model state to candidate when the cancel button is clicked. | |
363 * | |
364 * @param {jQuery.Event} event | |
365 * The click event. | |
366 */ | |
367 onClickCancel(event) { | |
368 event.preventDefault(); | |
369 this.model.set('state', 'deactivating'); | |
370 }, | |
371 | |
372 /** | |
373 * Clears the timeout that will eventually reposition the entity toolbar. | |
374 * | |
375 * Without this, it may reposition itself, away from the user's cursor! | |
376 * | |
377 * @param {jQuery.Event} event | |
378 * The mouse event. | |
379 */ | |
380 onMouseenter(event) { | |
381 clearTimeout(this.timer); | |
382 }, | |
383 | |
384 /** | |
385 * Builds the entity toolbar HTML; attaches to DOM; sets starting position. | |
386 * | |
387 * @return {jQuery} | |
388 * The toolbar element. | |
389 */ | |
390 buildToolbarEl() { | |
391 const $toolbar = $(Drupal.theme('quickeditEntityToolbar', { | |
392 id: 'quickedit-entity-toolbar', | |
393 })); | |
394 | |
395 $toolbar | |
396 .find('.quickedit-toolbar-entity') | |
397 // Append the "ops" toolgroup into the toolbar. | |
398 .prepend(Drupal.theme('quickeditToolgroup', { | |
399 classes: ['ops'], | |
400 buttons: [ | |
401 { | |
402 label: Drupal.t('Save'), | |
403 type: 'submit', | |
404 classes: 'action-save quickedit-button icon', | |
405 attributes: { | |
406 'aria-hidden': true, | |
407 }, | |
408 }, | |
409 { | |
410 label: Drupal.t('Close'), | |
411 classes: 'action-cancel quickedit-button icon icon-close icon-only', | |
412 }, | |
413 ], | |
414 })); | |
415 | |
416 // Give the toolbar a sensible starting position so that it doesn't | |
417 // animate on to the screen from a far off corner. | |
418 $toolbar | |
419 .css({ | |
420 left: this.$entity.offset().left, | |
421 top: this.$entity.offset().top, | |
422 }); | |
423 | |
424 return $toolbar; | |
425 }, | |
426 | |
427 /** | |
428 * Returns the DOM element that fields will attach their toolbars to. | |
429 * | |
430 * @return {jQuery} | |
431 * The DOM element that fields will attach their toolbars to. | |
432 */ | |
433 getToolbarRoot() { | |
434 return this._fieldToolbarRoot; | |
435 }, | |
436 | |
437 /** | |
438 * Generates a state-dependent label for the entity toolbar. | |
439 */ | |
440 label() { | |
441 // The entity label. | |
442 let label = ''; | |
443 const entityLabel = this.model.get('label'); | |
444 | |
445 // Label of an active field, if it exists. | |
446 const activeField = Drupal.quickedit.app.model.get('activeField'); | |
447 const activeFieldLabel = activeField && activeField.get('metadata').label; | |
448 // Label of a highlighted field, if it exists. | |
449 const highlightedField = Drupal.quickedit.app.model.get('highlightedField'); | |
450 const highlightedFieldLabel = highlightedField && highlightedField.get('metadata').label; | |
451 // The label is constructed in a priority order. | |
452 if (activeFieldLabel) { | |
453 label = Drupal.theme('quickeditEntityToolbarLabel', { | |
454 entityLabel, | |
455 fieldLabel: activeFieldLabel, | |
456 }); | |
457 } | |
458 else if (highlightedFieldLabel) { | |
459 label = Drupal.theme('quickeditEntityToolbarLabel', { | |
460 entityLabel, | |
461 fieldLabel: highlightedFieldLabel, | |
462 }); | |
463 } | |
464 else { | |
465 // @todo Add XSS regression test coverage in https://www.drupal.org/node/2547437 | |
466 label = Drupal.checkPlain(entityLabel); | |
467 } | |
468 | |
469 this.$el | |
470 .find('.quickedit-toolbar-label') | |
471 .html(label); | |
472 }, | |
473 | |
474 /** | |
475 * Adds classes to a toolgroup. | |
476 * | |
477 * @param {string} toolgroup | |
478 * A toolgroup name. | |
479 * @param {string} classes | |
480 * A string of space-delimited class names that will be applied to the | |
481 * wrapping element of the toolbar group. | |
482 */ | |
483 addClass(toolgroup, classes) { | |
484 this._find(toolgroup).addClass(classes); | |
485 }, | |
486 | |
487 /** | |
488 * Removes classes from a toolgroup. | |
489 * | |
490 * @param {string} toolgroup | |
491 * A toolgroup name. | |
492 * @param {string} classes | |
493 * A string of space-delimited class names that will be removed from the | |
494 * wrapping element of the toolbar group. | |
495 */ | |
496 removeClass(toolgroup, classes) { | |
497 this._find(toolgroup).removeClass(classes); | |
498 }, | |
499 | |
500 /** | |
501 * Finds a toolgroup. | |
502 * | |
503 * @param {string} toolgroup | |
504 * A toolgroup name. | |
505 * | |
506 * @return {jQuery} | |
507 * The toolgroup DOM element. | |
508 */ | |
509 _find(toolgroup) { | |
510 return this.$el.find(`.quickedit-toolbar .quickedit-toolgroup.${toolgroup}`); | |
511 }, | |
512 | |
513 /** | |
514 * Shows a toolgroup. | |
515 * | |
516 * @param {string} toolgroup | |
517 * A toolgroup name. | |
518 */ | |
519 show(toolgroup) { | |
520 this.$el.removeClass('quickedit-animate-invisible'); | |
521 }, | |
522 | |
523 }); | |
524 }(jQuery, _, Backbone, Drupal, Drupal.debounce)); |