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