Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / toolbar / js / views / ToolbarVisualView.es6.js
1 /**
2  * @file
3  * A Backbone view for the toolbar element. Listens to mouse & touch.
4  */
5
6 (function ($, Drupal, drupalSettings, Backbone) {
7   Drupal.toolbar.ToolbarVisualView = Backbone.View.extend(/** @lends Drupal.toolbar.ToolbarVisualView# */{
8
9     /**
10      * Event map for the `ToolbarVisualView`.
11      *
12      * @return {object}
13      *   A map of events.
14      */
15     events() {
16       // Prevents delay and simulated mouse events.
17       const touchEndToClick = function (event) {
18         event.preventDefault();
19         event.target.click();
20       };
21
22       return {
23         'click .toolbar-bar .toolbar-tab .trigger': 'onTabClick',
24         'click .toolbar-toggle-orientation button': 'onOrientationToggleClick',
25         'touchend .toolbar-bar .toolbar-tab .trigger': touchEndToClick,
26         'touchend .toolbar-toggle-orientation button': touchEndToClick,
27       };
28     },
29
30     /**
31      * Backbone view for the toolbar element. Listens to mouse & touch.
32      *
33      * @constructs
34      *
35      * @augments Backbone.View
36      *
37      * @param {object} options
38      *   Options for the view object.
39      * @param {object} options.strings
40      *   Various strings to use in the view.
41      */
42     initialize(options) {
43       this.strings = options.strings;
44
45       this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented change:isTrayToggleVisible', this.render);
46       this.listenTo(this.model, 'change:mqMatches', this.onMediaQueryChange);
47       this.listenTo(this.model, 'change:offsets', this.adjustPlacement);
48       this.listenTo(this.model, 'change:activeTab change:orientation change:isOriented', this.updateToolbarHeight);
49
50       // Add the tray orientation toggles.
51       this.$el
52         .find('.toolbar-tray .toolbar-lining')
53         .append(Drupal.theme('toolbarOrientationToggle'));
54
55       // Trigger an activeTab change so that listening scripts can respond on
56       // page load. This will call render.
57       this.model.trigger('change:activeTab');
58     },
59
60     /**
61      * Update the toolbar element height.
62      *
63      * @constructs
64      *
65      * @augments Backbone.View
66      */
67     updateToolbarHeight() {
68       const toolbarTabOuterHeight = $('#toolbar-bar').find('.toolbar-tab').outerHeight() || 0;
69       const toolbarTrayHorizontalOuterHeight = $('.is-active.toolbar-tray-horizontal').outerHeight() || 0;
70       this.model.set('height', toolbarTabOuterHeight + toolbarTrayHorizontalOuterHeight);
71
72       $('body').css({
73         'padding-top': this.model.get('height'),
74       });
75
76       this.triggerDisplace();
77     },
78
79     // Trigger a recalculation of viewport displacing elements. Use setTimeout
80     // to ensure this recalculation happens after changes to visual elements
81     // have processed.
82     triggerDisplace() {
83       _.defer(() => {
84         Drupal.displace(true);
85       });
86     },
87
88     /**
89      * @inheritdoc
90      *
91      * @return {Drupal.toolbar.ToolbarVisualView}
92      *   The `ToolbarVisualView` instance.
93      */
94     render() {
95       this.updateTabs();
96       this.updateTrayOrientation();
97       this.updateBarAttributes();
98
99       $('body').removeClass('toolbar-loading');
100
101       // Load the subtrees if the orientation of the toolbar is changed to
102       // vertical. This condition responds to the case that the toolbar switches
103       // from horizontal to vertical orientation. The toolbar starts in a
104       // vertical orientation by default and then switches to horizontal during
105       // initialization if the media query conditions are met. Simply checking
106       // that the orientation is vertical here would result in the subtrees
107       // always being loaded, even when the toolbar initialization ultimately
108       // results in a horizontal orientation.
109       //
110       // @see Drupal.behaviors.toolbar.attach() where admin menu subtrees
111       // loading is invoked during initialization after media query conditions
112       // have been processed.
113       if (this.model.changed.orientation === 'vertical' || this.model.changed.activeTab) {
114         this.loadSubtrees();
115       }
116
117       return this;
118     },
119
120     /**
121      * Responds to a toolbar tab click.
122      *
123      * @param {jQuery.Event} event
124      *   The event triggered.
125      */
126     onTabClick(event) {
127       // If this tab has a tray associated with it, it is considered an
128       // activatable tab.
129       if (event.target.hasAttribute('data-toolbar-tray')) {
130         const activeTab = this.model.get('activeTab');
131         const clickedTab = event.target;
132
133         // Set the event target as the active item if it is not already.
134         this.model.set('activeTab', (!activeTab || clickedTab !== activeTab) ? clickedTab : null);
135
136         event.preventDefault();
137         event.stopPropagation();
138       }
139     },
140
141     /**
142      * Toggles the orientation of a toolbar tray.
143      *
144      * @param {jQuery.Event} event
145      *   The event triggered.
146      */
147     onOrientationToggleClick(event) {
148       const orientation = this.model.get('orientation');
149       // Determine the toggle-to orientation.
150       const antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
151       const locked = antiOrientation === 'vertical';
152       // Remember the locked state.
153       if (locked) {
154         localStorage.setItem('Drupal.toolbar.trayVerticalLocked', 'true');
155       }
156       else {
157         localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
158       }
159       // Update the model.
160       this.model.set({
161         locked,
162         orientation: antiOrientation,
163       }, {
164         validate: true,
165         override: true,
166       });
167
168       event.preventDefault();
169       event.stopPropagation();
170     },
171
172     /**
173      * Updates the display of the tabs: toggles a tab and the associated tray.
174      */
175     updateTabs() {
176       const $tab = $(this.model.get('activeTab'));
177       // Deactivate the previous tab.
178       $(this.model.previous('activeTab'))
179         .removeClass('is-active')
180         .prop('aria-pressed', false);
181       // Deactivate the previous tray.
182       $(this.model.previous('activeTray'))
183         .removeClass('is-active');
184
185       // Activate the selected tab.
186       if ($tab.length > 0) {
187         $tab
188           .addClass('is-active')
189           // Mark the tab as pressed.
190           .prop('aria-pressed', true);
191         const name = $tab.attr('data-toolbar-tray');
192         // Store the active tab name or remove the setting.
193         const id = $tab.get(0).id;
194         if (id) {
195           localStorage.setItem('Drupal.toolbar.activeTabID', JSON.stringify(id));
196         }
197         // Activate the associated tray.
198         const $tray = this.$el.find(`[data-toolbar-tray="${name}"].toolbar-tray`);
199         if ($tray.length) {
200           $tray.addClass('is-active');
201           this.model.set('activeTray', $tray.get(0));
202         }
203         else {
204           // There is no active tray.
205           this.model.set('activeTray', null);
206         }
207       }
208       else {
209         // There is no active tray.
210         this.model.set('activeTray', null);
211         localStorage.removeItem('Drupal.toolbar.activeTabID');
212       }
213     },
214
215     /**
216      * Update the attributes of the toolbar bar element.
217      */
218     updateBarAttributes() {
219       const isOriented = this.model.get('isOriented');
220       if (isOriented) {
221         this.$el.find('.toolbar-bar').attr('data-offset-top', '');
222       }
223       else {
224         this.$el.find('.toolbar-bar').removeAttr('data-offset-top');
225       }
226       // Toggle between a basic vertical view and a more sophisticated
227       // horizontal and vertical display of the toolbar bar and trays.
228       this.$el.toggleClass('toolbar-oriented', isOriented);
229     },
230
231     /**
232      * Updates the orientation of the active tray if necessary.
233      */
234     updateTrayOrientation() {
235       const orientation = this.model.get('orientation');
236
237       // The antiOrientation is used to render the view of action buttons like
238       // the tray orientation toggle.
239       const antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
240
241       // Toggle toolbar's parent classes before other toolbar classes to avoid
242       // potential flicker and re-rendering.
243       $('body')
244         .toggleClass('toolbar-vertical', (orientation === 'vertical'))
245         .toggleClass('toolbar-horizontal', (orientation === 'horizontal'));
246
247       const removeClass = (antiOrientation === 'horizontal') ? 'toolbar-tray-horizontal' : 'toolbar-tray-vertical';
248       const $trays = this.$el.find('.toolbar-tray')
249         .removeClass(removeClass)
250         .addClass(`toolbar-tray-${orientation}`);
251
252       // Update the tray orientation toggle button.
253       const iconClass = `toolbar-icon-toggle-${orientation}`;
254       const iconAntiClass = `toolbar-icon-toggle-${antiOrientation}`;
255       const $orientationToggle = this.$el.find('.toolbar-toggle-orientation')
256         .toggle(this.model.get('isTrayToggleVisible'));
257       $orientationToggle.find('button')
258         .val(antiOrientation)
259         .attr('title', this.strings[antiOrientation])
260         .text(this.strings[antiOrientation])
261         .removeClass(iconClass)
262         .addClass(iconAntiClass);
263
264       // Update data offset attributes for the trays.
265       const dir = document.documentElement.dir;
266       const edge = (dir === 'rtl') ? 'right' : 'left';
267       // Remove data-offset attributes from the trays so they can be refreshed.
268       $trays.removeAttr('data-offset-left data-offset-right data-offset-top');
269       // If an active vertical tray exists, mark it as an offset element.
270       $trays.filter('.toolbar-tray-vertical.is-active').attr(`data-offset-${edge}`, '');
271       // If an active horizontal tray exists, mark it as an offset element.
272       $trays.filter('.toolbar-tray-horizontal.is-active').attr('data-offset-top', '');
273     },
274
275     /**
276      * Sets the tops of the trays so that they align with the bottom of the bar.
277      */
278     adjustPlacement() {
279       const $trays = this.$el.find('.toolbar-tray');
280       if (!this.model.get('isOriented')) {
281         $trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical');
282       }
283     },
284
285     /**
286      * Calls the endpoint URI that builds an AJAX command with the rendered
287      * subtrees.
288      *
289      * The rendered admin menu subtrees HTML is cached on the client in
290      * localStorage until the cache of the admin menu subtrees on the server-
291      * side is invalidated. The subtreesHash is stored in localStorage as well
292      * and compared to the subtreesHash in drupalSettings to determine when the
293      * admin menu subtrees cache has been invalidated.
294      */
295     loadSubtrees() {
296       const $activeTab = $(this.model.get('activeTab'));
297       const orientation = this.model.get('orientation');
298       // Only load and render the admin menu subtrees if:
299       //   (1) They have not been loaded yet.
300       //   (2) The active tab is the administration menu tab, indicated by the
301       //       presence of the data-drupal-subtrees attribute.
302       //   (3) The orientation of the tray is vertical.
303       if (!this.model.get('areSubtreesLoaded') && typeof $activeTab.data('drupal-subtrees') !== 'undefined' && orientation === 'vertical') {
304         const subtreesHash = drupalSettings.toolbar.subtreesHash;
305         const theme = drupalSettings.ajaxPageState.theme;
306         const endpoint = Drupal.url(`toolbar/subtrees/${subtreesHash}`);
307         const cachedSubtreesHash = localStorage.getItem(`Drupal.toolbar.subtreesHash.${theme}`);
308         const cachedSubtrees = JSON.parse(localStorage.getItem(`Drupal.toolbar.subtrees.${theme}`));
309         const isVertical = this.model.get('orientation') === 'vertical';
310         // If we have the subtrees in localStorage and the subtree hash has not
311         // changed, then use the cached data.
312         if (isVertical && subtreesHash === cachedSubtreesHash && cachedSubtrees) {
313           Drupal.toolbar.setSubtrees.resolve(cachedSubtrees);
314         }
315         // Only make the call to get the subtrees if the orientation of the
316         // toolbar is vertical.
317         else if (isVertical) {
318           // Remove the cached menu information.
319           localStorage.removeItem(`Drupal.toolbar.subtreesHash.${theme}`);
320           localStorage.removeItem(`Drupal.toolbar.subtrees.${theme}`);
321           // The AJAX response's command will trigger the resolve method of the
322           // Drupal.toolbar.setSubtrees Promise.
323           Drupal.ajax({ url: endpoint }).execute();
324           // Cache the hash for the subtrees locally.
325           localStorage.setItem(`Drupal.toolbar.subtreesHash.${theme}`, subtreesHash);
326         }
327       }
328     },
329   });
330 }(jQuery, Drupal, drupalSettings, Backbone));