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