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