From 8afbe55cf61feee3552f4ee7c2013cc2139a747f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Fri, 22 Feb 2013 19:10:36 -0500
Subject: [PATCH] Issue #1860434 by jessebeach: Provide a JavaScript API to
 control the state of various Toolbar components and
 triggered events to respond to state changes.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: J. Renée Beach <splendidnoise@gmail.com>
---
 core/modules/contextual/contextual.toolbar.js      |   11 +-
 core/modules/shortcut/shortcut.module              |    2 +-
 .../modules/toolbar/config/toolbar.breakpoints.yml |    2 -
 core/modules/toolbar/config/toolbar.config.yml     |    1 -
 core/modules/toolbar/js/toolbar.js                 |  665 ++++++++++++--------
 core/modules/toolbar/js/toolbar.menu.js            |    2 +-
 core/modules/toolbar/toolbar.module                |    7 +-
 core/modules/tour/js/tour.js                       |    3 +-
 core/modules/user/user.module                      |    2 +-
 9 files changed, 417 insertions(+), 278 deletions(-)

diff --git a/core/modules/contextual/contextual.toolbar.js b/core/modules/contextual/contextual.toolbar.js
index 45f9757..f1e6f9f 100644
--- a/core/modules/contextual/contextual.toolbar.js
+++ b/core/modules/contextual/contextual.toolbar.js
@@ -34,7 +34,8 @@ Drupal.behaviors.contextualToolbar = {
         })
         .on('drupalOverlayClose.contextualToolbar', function () {
           model.set('isVisible', true);
-        });
+        })
+        .on('drupalEditModeChanged', Drupal.contextualToolbar.editModeChangedHandler);
 
       // Update the model to show the edit tab if there's >=1 contextual link.
       if ($contextuals.length > 0) {
@@ -56,7 +57,13 @@ Drupal.behaviors.contextualToolbar = {
   }
 };
 
-Drupal.contextualToolbar = Drupal.contextualToolbar || { models: {}, views: {}};
+Drupal.contextualToolbar = Drupal.contextualToolbar || {
+  models: {},
+  views: {},
+  editModeChangedHandler: function (event, data) {
+    Drupal.toolbar.setActiveItem($('.js .toolbar .bar .contextual-toolbar-tab'));
+  }
+};
 
 /**
  * Backbone Model for the edit toggle.
diff --git a/core/modules/shortcut/shortcut.module b/core/modules/shortcut/shortcut.module
index 18bff02..835e4ff 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -568,7 +568,7 @@ function shortcut_toolbar() {
         '#options' => array(
           'attributes' => array(
             'title' => t('Shortcuts'),
-            'class' => array('icon', 'icon-shortcut'),
+            'class' => array('icon', 'icon-shortcut', 'toolbar-tab'),
           ),
         ),
       ),
diff --git a/core/modules/toolbar/config/toolbar.breakpoints.yml b/core/modules/toolbar/config/toolbar.breakpoints.yml
index c623c0e..94e977d 100644
--- a/core/modules/toolbar/config/toolbar.breakpoints.yml
+++ b/core/modules/toolbar/config/toolbar.breakpoints.yml
@@ -1,3 +1 @@
-narrow: 'only screen and (min-width: 16.5em)'
-standard: 'only screen and (min-width: 38.125em)'
 wide: 'only screen and (min-width: 50em)'
diff --git a/core/modules/toolbar/config/toolbar.config.yml b/core/modules/toolbar/config/toolbar.config.yml
index 8a98715..79522f0 100644
--- a/core/modules/toolbar/config/toolbar.config.yml
+++ b/core/modules/toolbar/config/toolbar.config.yml
@@ -1,3 +1,2 @@
 breakpoints:
-  - 'module.toolbar.narrow'
   - 'module.toolbar.wide'
diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index 45e91ad..196f6d4 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -7,33 +7,9 @@
 
 "use strict";
 
-Drupal.toolbar = Drupal.toolbar || {};
-
-/**
- * Store the state of the active tab so it will remain active across page loads.
- */
-var activeTab = JSON.parse(localStorage.getItem('Drupal.toolbar.activeTab'));
-
-/**
- * Store the state of the trays to maintain them across page loads.
- */
-var locked = JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) || false;
-var orientation = (locked) ? 'vertical' : 'horizontal';
-
-/**
- * Holds the jQuery objects of the toolbar DOM element, the trays and messages.
- */
-var $toolbar;
-var $trays;
-var $messages;
-
-/**
- * Holds the mediaQueryList object.
- */
-var mql = {
-  standard: null,
-  wide: null
-};
+// Closure variables.
+var model; // The state model for the toolbar.
+var view; // The view of the toolbar.
 
 /**
  * Register tabs with the toolbar.
@@ -46,269 +22,446 @@ var mql = {
 Drupal.behaviors.toolbar = {
   attach: function(context) {
     var options = $.extend(this.options, drupalSettings.toolbar);
-    var $toolbarOnce = $(context).find('#toolbar-administration').once('toolbar');
-    if ($toolbarOnce.length) {
-      // Assign the $toolbar variable in the closure.
-      $toolbar = $toolbarOnce;
-      // Add subtrees.
-      // @todo Optimize this to delay adding each subtree to the DOM until it is
-      //   needed; however, take into account screen readers for determining
-      //   when the DOM elements are needed.
-      if (Drupal.toolbar.subtrees) {
-        for (var id in Drupal.toolbar.subtrees) {
-          $('#toolbar-link-' + id).after(Drupal.toolbar.subtrees[id]);
-        }
-      }
-      // Append a messages element for appending interaction updates for screen
-      // readers.
-      $messages = $(Drupal.theme('toolbarMessageBox')).appendTo($toolbar);
-      // Store the trays in a scoped variable.
-      $trays = $toolbar.find('.tray');
-      $trays
-        // Add the tray orientation toggles.
-        .find('.lining')
-        .append(Drupal.theme('toolbarOrientationToggle'));
-      // Store media queries.
-      mql.standard = window.matchMedia(options.breakpoints['module.toolbar.standard']);
+    $(context).find('#toolbar-administration').once('toolbar', function () {
       // Set up switching between the vertical and horizontal presentation
       // of the toolbar trays based on a breakpoint.
-      mql.wide = window.matchMedia(options.breakpoints['module.toolbar.wide']);
-      mql.wide.addListener(Drupal.toolbar.mediaQueryChangeHandler);
-      // Set the orientation of the tray.
-      // If the tray is set to vertical in localStorage, persist the vertical
-      // presentation. If the tray is not locked to vertical, let the media
-      // query application decide the orientation.
-      changeOrientation((locked) ? 'vertical' : ((mql.wide.matches) ? 'horizontal' : 'vertical'), locked);
-      // Render the main menu as a nested, collapsible accordion.
-      $toolbar.find('.toolbar-menu-administration > .menu').toolbarMenu();
-      // Call setHeight on screen resize. Wrap it in debounce to prevent
-      // setHeight from being called too frequently.
-      var setHeight = Drupal.debounce(Drupal.toolbar.setHeight, 200);
-      // Attach behavior to the window.
+      var mql = window.matchMedia(options.breakpoints['module.toolbar.wide']);
+      // Build the model and assign it to the closure variable reference.
+      model = new StateModel({
+        locked: JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) || false,
+        activeTab: document.getElementById(JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID'))),
+        orientation: JSON.parse(localStorage.getItem('Drupal.toolbar.trayOrientation')) || 'vertical',
+        mql: mql
+      });
+      // Respond to external events.
       $(window)
-        .on('resize.toolbar', setHeight);
-      // Attach behaviors to the toolbar.
-      $toolbar
-        .on('click.toolbar', '.bar a', Drupal.toolbar.toggleTray)
-        .on('click.toolbar', '.toggle-orientation button', Drupal.toolbar.orientationChangeHandler);
-      // Restore the open tab. Only open the tab on wide screens.
-      if (activeTab && window.matchMedia(options.breakpoints['module.toolbar.standard']).matches) {
-        $toolbar.find('[data-toolbar-tray="' + activeTab + '"]').trigger('click.toolbar');
-      }
-      else {
-        // Update the page and toolbar dimension indicators.
-        updatePeripherals();
-      }
-    }
+        .on('resize.toolbar', Drupal.debounce(function (event) {
+          model.set('dimensionsAreValid', false);
+        }, 150));
+      // Update the model when matchMedia fires.
+      mql.addListener(function (mql) {
+        model.set('mql', mql);
+        // Setting the mql won't trigger a change in the model. It needs to be
+        // triggered manually.
+        model.trigger('change:mql');
+      });
+      // Respond to toolbar events.
+      $(document)
+        .on('drupalToolbarDimensionsChanged.toolbar', Drupal.toolbar.dimensionsChangeHandler)
+        .on('drupalToolbarOrientationChanged.toolbar', Drupal.toolbar.orientationChangeHandler)
+        .on('drupalToolbarTrayChanged.toolbar', Drupal.toolbar.trayChangeHandler);
+      // Broadcast model changes to other modules.
+      model
+        .on('change:height', function (model, height) {
+          $(document).trigger('drupalToolbarDimensionsChanged', height);
+        })
+        .on('change:orientation', function (model, orientation) {
+          $(document).trigger('drupalToolbarOrientationChange', orientation);
+        })
+        .on('change:activeTab', function (model, tab) {
+          $(document).trigger('drupalToolbarTabChanged', tab);
+        })
+        .on('change:activeTray', function (model, tray) {
+          $(document).trigger('drupalToolbarTrayChanged', tray);
+        });
+      // Build the toolbar view and assign it to the closure variable reference.
+      view = new ToolbarView({
+        el: this,
+        model: model,
+        strings: options.strings
+      });
+      // Render collapsible menus.
+      var menuModel = new MenuStateModel();
+      var menuView = new MenuView({
+        el: $(this).find('.toolbar-menu-administration').get(0),
+        model: menuModel
+      });
+      // Handle the resolution of Drupal.toolbar.setSubtrees().
+      // This is handled with a deferred so that the function may be invoked
+      // asynchronously.
+      Drupal.toolbar.setSubtrees.done(function (subtrees) {
+        menuModel.set('subtrees', subtrees);
+      });
+      // Expose a set of methods for
+    });
   },
   // Default options.
   options: {
     breakpoints: {
-      'module.toolbar.standard': '',
       'module.toolbar.wide': ''
+    },
+    strings: {
+      opened: Drupal.t('opened'),
+      horizontal: Drupal.t('Horizontal orientation'),
+      vertical: Drupal.t('Vertical orientation')
     }
   }
 };
 
-/**
- * Set subtrees.
- *
- * JSONP callback.
- * @see toolbar_subtrees_jsonp().
- */
-Drupal.toolbar.setSubtrees = function(subtrees) {
-  Drupal.toolbar.subtrees = subtrees;
-};
+// Expose a limited API to outside modules.
+Drupal.toolbar = Drupal.toolbar || {
+  /**
+   * Accpets a list of subtree menu elements.
+   *
+   * A deferred object that is resolved by an inlined JavaScript callback.
+   *
+   * JSONP callback.
+   * @see toolbar_subtrees_jsonp().
+   */
+  setSubtrees: new $.Deferred(),
 
-/**
- * Toggle a toolbar tab and the associated tray.
- */
-Drupal.toolbar.toggleTray = function (event) {
-  var strings = {
-    opened: Drupal.t('opened'),
-    closed: Drupal.t('closed')
-  };
-  var $tab = $(event.target);
-  var name = $tab.attr('data-toolbar-tray');
-  // Activate the selected tab and associated tray.
-  var $activateTray = $toolbar.find('[data-toolbar-tray="' + name + '"].tray').toggleClass('active');
-  if ($activateTray.length) {
-    event.preventDefault();
-    event.stopPropagation();
-    $tab.toggleClass('active');
-    // Toggle aria-pressed.
-    var value = $tab.attr('aria-pressed');
-    $tab.attr('aria-pressed', (value === 'false') ? 'true' : 'false');
-    // Append a message that a tray has been opened.
-    setMessage(Drupal.t('@tray tray @state.', {
-      '@tray': name,
-      '@state': (value === 'true') ? strings.closed : strings.opened
-    }));
-    // Store the active tab name or remove the setting.
-    if ($tab.hasClass('active')) {
-      localStorage.setItem('Drupal.toolbar.activeTab', JSON.stringify(name));
+  /**
+   *
+   */
+  setActiveItem: function (element) {
+    var item;
+    if (typeof element === 'object') {
+      // Check if the element is a DOM element.
+      if ('tagName' in element) {
+        item = element;
+      }
+      // Check if the element is a jQuery set.
+      if ('jquery' in element) {
+        item = element.get(0);
+      }
     }
-    else {
-      localStorage.removeItem('Drupal.toolbar.activeTab');
+    else if (!element) {
+      item = element;
     }
-    // Disable non-selected tabs and trays.
-    $toolbar.find('.bar .trigger')
-      .not($tab)
-      .removeClass('active')
-      // Set aria-pressed to false.
-      .attr('aria-pressed', 'false');
-    $toolbar.find('.tray').not($activateTray).removeClass('active');
-    // Update the page and toolbar dimension indicators.
-    updatePeripherals();
-  }
-};
-
-/**
- * The height of the toolbar offsets the top of the page content.
- *
- * Page components can register with the offsettopchange event to know when
- * the height of the toolbar changes.
- */
-Drupal.toolbar.setHeight = function () {
-  // Set the top of the all the trays to the height of the bar.
-  var barHeight = $toolbar.find('.bar').outerHeight();
-  var height = barHeight;
-  var bhpx =  barHeight + 'px';
-  var tray;
-  for (var i = 0, il = $trays.length; i < il; i++) {
-    tray = $trays[i];
-    if (!tray.style.top.length || (tray.style.top !== bhpx)) {
-      tray.style.top = bhpx;
+    if (model && item) {
+      model.set('activeTab', item);
     }
-  }
+  },
   /**
-   * Get the height of the active tray and include it in the total
-   * height of the toolbar.
+   *
    */
-  height += $trays.filter('.active.horizontal').outerHeight() || 0;
-  // Indicate the height of the toolbar in the attribute data-offset-top.
-  var offset = parseInt($toolbar.attr('data-offset-top'), 10);
-  if (offset !== height) {
-    $toolbar.attr('data-offset-top', height);
+  dimensionsChangeHandler: function (event, height) {
     // Alter the padding on the top of the body element.
-    $('body').css('padding-top', height);
-    $(document).trigger('offsettopchange', height);
-    $(window).trigger('resize');
+    $('body')
+      .css('padding-top', height);
+    $(document)
+      .trigger('offsettopchange', height);
+  },
+
+  /**
+   *
+   */
+  orientationChangeHandler: function (event, orientation) {
+    $('body')
+      .toggleClass('toolbar-vertical', orientation === 'vertical')
+      .toggleClass('toolbar-horizontal', orientation === 'horizontal');
+  },
+
+  /**
+   *
+   */
+  trayChangeHandler: function (event, tray) {
+    $('body')
+      .toggleClass('toolbar-tray-open', !!tray);
   }
 };
 
 /**
- * Respond to configured media query applicability changes.
+ * Backbone model for the toolbar.
  */
-Drupal.toolbar.mediaQueryChangeHandler = function (mql) {
-  var orientation = (mql.matches) ? 'horizontal' : 'vertical';
-  changeOrientation(orientation);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+var StateModel = Backbone.Model.extend({
+  defaults: {
+    // The active toolbar item. All other items should be inactive under
+    // normal circumstances. It will remain active across page loads. The active
+    // item is stored as a DOM element, not a jQuery set.
+    activeTab: null,
+    // Represents whether a tray is open or not.
+    activeTray: null,
+    // The orientation of the active tray.
+    orientation: 'horizontal',
+    // A tray is locked if a user toggled it to vertical. Otherwise a tray
+    // will switch between vertical and horizontal orientation based on the
+    // configured breakpoints. The locked state will be maintained across page
+    // loads.
+    locked: false,
+    // Holds the mediaQueryList object.
+    mql: null,
+    // The height of the toolbar.
+    height: null,
+    // Whether the current dimensions are valid. Dimensions can be invalidated
+    // by a screen resize.
+    dimensionsAreValid: true
+  }
+});
 
 /**
- * Respond to the toggling of the tray orientation.
+ * Backbone view for the toolbar element.
  */
-Drupal.toolbar.orientationChangeHandler = function (event) {
-  event.preventDefault();
-  event.stopPropagation();
-  var $button = $(event.target);
-  var orientation = event.target.value;
-  var $tray = $button.closest('.tray');
-  changeOrientation(orientation, true);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+var ToolbarView = Backbone.View.extend({
+  events: {
+    'click .bar .toolbar-tab': 'onTabClick',
+    'click .toggle-orientation button': 'onOrientationToggleClick'
+  },
 
-/**
- * Change the orientation of the tray between vertical and horizontal.
- *
- * @param {String} newOrientation
- *   Either 'vertical' or 'horizontal'. The orientation to change the tray to.
- *
- * @param {Boolean} isLock
- *   Whether the orientation of the tray should be locked if it is being toggled
- *   to vertical.
- */
-function changeOrientation (newOrientation, isLock) {
-  var oldOrientation = orientation;
-  if (isLock) {
-    locked = (newOrientation === 'vertical');
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function() {
+    this.strings = this.options.strings;
+
+    this.model.on('change:activeTab', this.render, this);
+    this.model.on('change:orientation', this.render, this);
+    this.model.on('change:mql', this.onMediaQueryChange, this);
+    this.model.on('change:dimensionsAreValid', this.setHeight, this);
+
+    // Add the tray orientation toggles.
+    this.$el.find('.tray')
+      .find('.lining')
+      .append(Drupal.theme('toolbarOrientationToggle'));
+
+    // Update the orientation according to the mql value.
+    // This model change is silent. The change will be reflect when render()
+    // is called.
+    this.model.set({
+      orientation: this.mqlHandler()
+    }, {
+      silent: true
+    });
+
+    // Trigger an activeTab change so that listening scripts can respond on
+    // page load. This will call render.
+    this.model.trigger('change:activeTab');
+  },
+
+  /**
+   * Implements Backbone.View.prototype.render().
+   */
+  render: function (model, property) {
+    // Update the display of the tabs.
+    this.refreshTabs();
+    // Adjust the orientation of the active tray.
+    this.changeOrientation();
+    // Adjust the height of the toolbar.
+    this.model.set('dimensionsAreValid', false);
+
+    return this;
+  },
+
+  /**
+   *
+   */
+  onTabClick: function (event) {
+    var tab = this.model.get('activeTab');
+
+    // Set the event target as the active item if it is not already.
+    this.model.set('activeTab', (!tab || event.target !== tab) ? event.target : null);
+
+    event.preventDefault();
+    event.stopPropagation();
+  },
+
+  /**
+   *
+   */
+  onOrientationToggleClick: function (event) {
+    var orientation = this.model.get('orientation');
+    // Determine the toggle-to orientation.
+    var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
+    var locked = (antiOrientation === 'vertical') ? true : false;
+    // Remember the locked state.
     if (locked) {
       localStorage.setItem('Drupal.toolbar.trayVerticalLocked', JSON.stringify(locked));
     }
     else {
       localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
     }
-  }
-  if ((!locked && newOrientation === 'horizontal') || newOrientation === 'vertical') {
-    $trays
+    // Update the model.
+    this.model.set({
+      locked: locked,
+      orientation: antiOrientation
+    });
+
+    event.preventDefault();
+    event.stopPropagation();
+  },
+
+  /**
+   * Respond to configured media query applicability changes.
+   */
+  onMediaQueryChange: function (model, mql) {
+    this.model.set('orientation', this.mqlHandler());
+  },
+
+  /**
+   *
+   */
+  mqlHandler: function () {
+    var mql = this.model.get('mql');
+    return (mql.matches) ? 'horizontal' : 'vertical';
+  },
+
+  /**
+   * Toggle a toolbar tab and the associated tray.
+   */
+  refreshTabs: function (event) {
+    var $tab = $(this.model.get('activeTab'));
+    var $tray = $();
+    // Activate the selected tab.
+    if ($tab.length > 0) {
+      $tab.addClass('active');
+      var name = $tab.attr('data-toolbar-tray');
+      // Mark the tab as pressed.
+      $tab.attr('aria-pressed', 'true');
+      // Store the active tab name or remove the setting.
+      var id = $tab.get(0).id;
+      if (id) {
+        localStorage.setItem('Drupal.toolbar.activeTabID', JSON.stringify(id));
+      }
+      // Activate the associated tray.
+      var $tray = this.$el.find('[data-toolbar-tray="' + name + '"].tray');
+      if ($tray.length) {
+        $tray.addClass('active');
+        this.model.set('activeTray', $tray.get(0));
+        // Announce that a tray has been opened.
+        Drupal.announce(Drupal.t('@tray tray @state', {
+          '@tray': name,
+          '@state': this.strings.opened
+        }));
+      }
+      else {
+        // There is no active tray.
+        this.model.set('activeTray', null);
+      }
+    }
+    else {
+      // There is no active tray.
+      this.model.set('activeTray', null);
+      localStorage.removeItem('Drupal.toolbar.activeTabID');
+    }
+    // Disable non-selected tabs.
+    this.$el.find('.bar .toolbar-tab')
+      .not($tab)
+      .removeClass('active')
+      // Set aria-pressed to false.
+      .attr('aria-pressed', 'false');
+    // Disable non-selected trays.
+    this.$el.find('.tray')
+      .not($tray)
+      .removeClass('active');
+  },
+
+  /**
+   * Change the orientation of the tray between vertical and horizontal.
+   */
+  changeOrientation: function () {
+    var orientation = this.model.get('orientation');
+    var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
+    var locked = this.model.get('locked');
+    // Set the orientation of the tray.
+    // If the tray is locked to vertical in localStorage, persist the vertical
+    // presentation. If the tray is not locked to vertical, let the media
+    // query application decide the orientation.
+    orientation = (locked) ? 'vertical' : orientation;
+    // Update the orientation of the trays.
+    var $trays = this.$el.find('.tray')
       .removeClass('horizontal vertical')
-      .addClass(newOrientation);
-    orientation = newOrientation;
-    toggleOrientationToggle((newOrientation === 'vertical') ? 'horizontal' : 'vertical');
+      .addClass(orientation);
+
+    // Update the tray orientation toggle button.
+    var iconClass = 'icon-toggle-' + orientation;
+    var iconAntiClass = 'icon-toggle-' + antiOrientation;
+    this.$el.find('.toggle-orientation button')
+      .val(antiOrientation)
+      .text(this.strings[antiOrientation])
+      .removeClass(iconClass)
+      .addClass(iconAntiClass);
+
+    // Append a message that the tray orientation has been changed.
+    Drupal.announce(Drupal.t('Tray orientation changed to @orientation.', {
+      '@orientation': orientation
+    }));
+  },
+
+  /**
+   * The height of the toolbar offsets the top of the page content.
+   *
+   * Page components can register with the offsettopchange event to know when
+   * the height of the toolbar changes.
+   */
+  setHeight: function (model, dimensionsAreValid) {
+    // Don't reprocess unless the current dimensions have been invalidated.
+    if (dimensionsAreValid) {
+      return;
+    }
+    var originalHeight = this.model.get('height');
+    // Set the top of the all the trays to the height of the bar.
+    var barHeight = this.$el.find('.bar').outerHeight();
+    var height = barHeight;
+    var bhpx =  barHeight + 'px';
+    var $trays = this.$el.find('.tray');
+    var tray;
+    for (var i = 0, il = $trays.length; i < il; i++) {
+      tray = $trays[i];
+      if (!tray.style.top.length || (tray.style.top !== bhpx)) {
+        tray.style.top = bhpx;
+      }
+    }
+    /**
+     * Get the height of the active tray and include it in the total
+     * height of the toolbar.
+     */
+    height += $trays.filter('.active.horizontal').outerHeight() || 0;
+    // Indicate the height of the toolbar in the attribute data-offset-top.
+    var offset = parseInt(this.$el.attr('data-offset-top'), 10);
+    if (originalHeight !== height) {
+      this.$el.attr('data-offset-top', height);
+      this.model.set('height', height);
+    }
+    this.model.set('dimensionsAreValid', true);
   }
-}
+});
 
 /**
- * Mark up the body tag to reflect the current state of the toolbar.
+ * Backbone Model for collapsible menus.
  */
-function setBodyState () {
-  var $activeTray = $trays.filter('.active');
-  $('body')
-    .toggleClass('toolbar-tray-open', !!$activeTray.length)
-    .toggleClass('toolbar-vertical', (!!$activeTray.length && orientation === 'vertical'))
-    .toggleClass('toolbar-horizontal', (!!$activeTray.length && orientation === 'horizontal'));
-}
+var MenuStateModel = Backbone.Model.extend({
+  defaults: {
+    subtrees: {}
+  }
+});
 
 /**
- * Change the orientation toggle active state.
+ * Backbone View for collapsible menus.
  */
-function toggleOrientationToggle (orientation) {
-  var strings = {
-    horizontal: Drupal.t('Horizontal orientation'),
-    vertical: Drupal.t('Vertical orientation')
-  };
-  var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
-  var iconClass = 'icon-toggle-' + orientation;
-  var iconAntiClass = 'icon-toggle-' + antiOrientation;
-  // Append a message that the tray orientation has been changed.
-  setMessage(Drupal.t('Tray orientation changed to @orientation.', {
-    '@orientation': antiOrientation
-  }));
-  // Change the tray orientation.
-  $trays.find('.toggle-orientation button')
-    .val(orientation)
-    .text(strings[orientation])
-    .removeClass(iconAntiClass)
-    .addClass(iconClass);
-}
+var MenuView = Backbone.View.extend({
 
-/**
- * Updates elements peripheral to the toolbar.
- *
- * When the dimensions and orientation of the toolbar change, elements on the
- * page must either be changed or informed of the changes.
- */
-function updatePeripherals () {
-  // Adjust the body to accommodate trays.
-  setBodyState();
-  // Adjust the height of the toolbar.
-  Drupal.toolbar.setHeight();
-}
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function () {
+    this.model.on('change:subtrees', this.render, this);
+  },
 
-/**
- * Places the message in the toolbar's ARIA live message area.
- *
- * The message will be read by speaking User Agents.
- *
- * @param {String} message
- *   A string to be inserted into the message area.
- */
-function setMessage (message) {
-  $messages.html(Drupal.theme('toolbarTrayMessage', message));
-}
+  /**
+   * Implements Backbone.View.prototype.render().
+   */
+  render: function (model, property) {
+    var subtrees = this.model.get('subtrees');
+    // Add subtrees.
+    // @todo Optimize this to delay adding each subtree to the DOM until it is
+    //   needed; however, take into account screen readers for determining
+    //   when the DOM elements are needed.
+    for (var id in subtrees) {
+      if (subtrees.hasOwnProperty(id)) {
+        this.$el
+          .find('#toolbar-link-' + id)
+          .once('toolbar-subtrees')
+          .after(subtrees[id]);
+      }
+    }
+    // Render the main menu as a nested, collapsible accordion.
+    if ('toolbarMenu' in $.fn) {
+      this.$el
+        .find('> .menu')
+        .toolbarMenu();
+    }
+  }
+});
 
 /**
  * A toggle is an interactive element often bound to a click handler.
@@ -322,24 +475,4 @@ Drupal.theme.toolbarOrientationToggle = function () {
     '</div></div>';
 };
 
-/**
- * A region to post messages that a screen reading UA will announce.
- *
- * @return {String}
- *   A string representing a DOM fragment.
- */
-Drupal.theme.toolbarMessageBox = function () {
-  return '<div id="toolbar-messages" class="element-invisible" role="region" aria-live="polite"></div>';
-};
-
-/**
- * Wrap a message string in a p tag.
- *
- * @return {String}
- *   A string representing a DOM fragment.
- */
-Drupal.theme.toolbarTrayMessage = function (message) {
-  return '<p>' + message + '</p>';
-};
-
 }(jQuery, Drupal, drupalSettings));
diff --git a/core/modules/toolbar/js/toolbar.menu.js b/core/modules/toolbar/js/toolbar.menu.js
index f05bcb0..9b8473a 100644
--- a/core/modules/toolbar/js/toolbar.menu.js
+++ b/core/modules/toolbar/js/toolbar.menu.js
@@ -12,7 +12,7 @@
 /**
  * Store the open menu tray.
  */
-var activeItem = drupalSettings.basePath + drupalSettings.currentPath;
+var activeItem = drupalSettings.basePath + Drupal.encodePath(drupalSettings.currentPath);
 
   $.fn.toolbarMenu = function () {
 
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index e4996bc..47cf740 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -141,7 +141,7 @@ function toolbar_subtrees_jsonp($hash) {
   _toolbar_initialize_page_cache();
   $subtrees = toolbar_get_rendered_subtrees();
   $response = new JsonResponse($subtrees);
-  $response->setCallback('Drupal.toolbar.setSubtrees');
+  $response->setCallback('Drupal.toolbar.setSubtrees.resolve');
   return $response;
 }
 
@@ -443,7 +443,7 @@ function toolbar_toolbar() {
       '#options' => array(
         'attributes' => array(
           'title' => t('Home page'),
-          'class' => array('icon', 'icon-home'),
+          'class' => array('icon', 'icon-home',),
         ),
       ),
     ),
@@ -485,7 +485,7 @@ function toolbar_toolbar() {
       '#options' => array(
         'attributes' => array(
           'title' => t('Admin menu'),
-          'class' => array('icon', 'icon-menu'),
+          'class' => array('icon', 'icon-menu', 'toolbar-tab'),
         ),
       ),
     ),
@@ -628,6 +628,7 @@ function toolbar_library_info() {
       array('system', 'jquery'),
       array('system', 'drupal'),
       array('system', 'drupalSettings'),
+      array('system', 'backbone'),
       array('system', 'matchmedia'),
       array('system', 'jquery.once'),
       array('system', 'drupal.debounce'),
diff --git a/core/modules/tour/js/tour.js b/core/modules/tour/js/tour.js
index d8885be..b6e8a58 100644
--- a/core/modules/tour/js/tour.js
+++ b/core/modules/tour/js/tour.js
@@ -130,6 +130,7 @@ Drupal.tour.views.ToggleTourView = Backbone.View.extend({
    */
   onClick: function (event) {
     this.model.set('isActive', !this.model.get('isActive'));
+    Drupal.toolbar.setActiveItem(event.target);
     event.preventDefault();
     event.stopPropagation();
   },
@@ -201,4 +202,4 @@ Drupal.tour.views.ToggleTourView = Backbone.View.extend({
 
 });
 
-})(jQuery, Backbone, Drupal, document);
\ No newline at end of file
+})(jQuery, Backbone, Drupal, document);
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 5635e16..1c583a7 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2741,7 +2741,7 @@ function user_toolbar() {
       '#options' => array(
         'attributes' => array(
           'title' => t('My account'),
-          'class' => array('icon', 'icon-user'),
+          'class' => array('icon', 'icon-user', 'toolbar-tab'),
         ),
       ),
     ),
-- 
1.7.10.4

