Mercurial > hg > cmmr2012-drupal-site
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)); |