From c4495a63abbfe4225d17a57cebfc5b65f48403da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Wed, 13 Mar 2013 15:05:46 -0400
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 +-
 .../toolbar/config/schema/toolbar.schema.yml       |    6 -
 .../modules/toolbar/config/toolbar.breakpoints.yml |    2 -
 core/modules/toolbar/config/toolbar.settings.yml   |    1 -
 core/modules/toolbar/js/toolbar.js                 |  674 ++++++++++++--------
 core/modules/toolbar/js/toolbar.menu.js            |   10 +-
 core/modules/toolbar/toolbar.module                |    5 +-
 core/modules/tour/js/tour.js                       |    3 +-
 core/modules/user/user.module                      |    2 +-
 10 files changed, 428 insertions(+), 288 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 25a04c7..3a75378 100644
--- a/core/modules/shortcut/shortcut.module
+++ b/core/modules/shortcut/shortcut.module
@@ -580,7 +580,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/schema/toolbar.schema.yml b/core/modules/toolbar/config/schema/toolbar.schema.yml
index c0949f2..5ffe3fb 100644
--- a/core/modules/toolbar/config/schema/toolbar.schema.yml
+++ b/core/modules/toolbar/config/schema/toolbar.schema.yml
@@ -14,12 +14,6 @@ toolbar.breakpoints:
   type: mapping
   label: 'Toolbar breakpoint settings'
   mapping:
-    narrow:
-      type: string
-      label: 'Narrow'
-    standard:
-      type: string
-      label: 'Standard'
     wide:
       type: string
       label: 'Wide'
diff --git a/core/modules/toolbar/config/toolbar.breakpoints.yml b/core/modules/toolbar/config/toolbar.breakpoints.yml
index 74b318e..68c66dd 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: 52em)'
diff --git a/core/modules/toolbar/config/toolbar.settings.yml b/core/modules/toolbar/config/toolbar.settings.yml
index 8a98715..79522f0 100644
--- a/core/modules/toolbar/config/toolbar.settings.yml
+++ b/core/modules/toolbar/config/toolbar.settings.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..0f2a274 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -1,42 +1,14 @@
 /**
- * @file toolbar.js
- *
+ * @file
  * Defines the behavior of the Drupal administration toolbar.
  */
-(function ($, Drupal, drupalSettings) {
-
-"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;
+(function ($, Backbone, Drupal, drupalSettings, document, window) {
 
-/**
- * Holds the mediaQueryList object.
- */
-var mql = {
-  standard: null,
-  wide: null
-};
+"use strict";
 
 /**
- * Register tabs with the toolbar.
+ * Registers tabs with the toolbar.
  *
  * The Drupal toolbar allows modules to register top-level tabs. These may point
  * directly to a resource or toggle the visibility of a tray.
@@ -44,277 +16,465 @@ var mql = {
  * Modules register tabs with hook_toolbar().
  */
 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']);
+  attach: function (context) {
+    var defaults = this.defaults;
+    $(context).find('#toolbar-administration').once('toolbar', function () {
+      // Create a reference to the defaults in this function scope.
+      var options = $.extend(defaults, drupalSettings.toolbar);
       // 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']);
+      var 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',
+        mqMatches: mql.matches
+      });
+
+      // 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('mqMatches', mql.matches);
+      });
+
+      // Respond to toolbar events.
+      $(document)
+        .on('drupalToolbarDimensionsChanged.toolbar', dimensionsChangeHandler)
+        .on('drupalToolbarOrientationChanged.toolbar', orientationChangeHandler)
+        .on('drupalToolbarTrayChanged.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.
+      var 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);
+      });
+    });
   },
   // Default options.
-  options: {
+  defaults: {
     breakpoints: {
-      'module.toolbar.standard': '',
       'module.toolbar.wide': ''
+    },
+    strings: {
+      opened: Drupal.t('opened'),
+      horizontal: Drupal.t('Horizontal orientation'),
+      vertical: Drupal.t('Vertical orientation')
     }
   }
 };
 
+// Expose a limited public API.
+  Drupal.toolbar = Drupal.toolbar || {
+    /**
+     * Accepts 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(),
+
+    /**
+     *
+     */
+    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 if (!element) {
+        item = element;
+      }
+      if (this.model && item) {
+        this.model.set('activeTab', item);
+      }
+    }
+  };
+
 /**
- * Set subtrees.
- *
- * JSONP callback.
- * @see toolbar_subtrees_jsonp().
+ * Backbone model for the toolbar.
  */
-Drupal.toolbar.setSubtrees = function(subtrees) {
-  Drupal.toolbar.subtrees = subtrees;
-};
+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. Stored as a DOM element, not a
+    // jQuery set.
+    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,
+    // Indicates whether the media query matches or not.
+    mqMatches: null,
+    // The height of the toolbar.
+    height: null,
+    // Whether the current dimensions are valid. Dimensions can be invalidated
+    // by a screen resize.
+    dimensionsAreValid: true
+  }
+});
 
 /**
- * Toggle a toolbar tab and the associated tray.
+ * Backbone view for the toolbar element.
  */
-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) {
+var ToolbarView = Backbone.View.extend({
+  events: {
+    'click .bar .toolbar-tab': 'onTabClick',
+    'click .toggle-orientation button': 'onOrientationToggleClick'
+  },
+
+  /**
+   * 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:mqMatches', 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 tray orientation.
+    if (!this.model.get('locked')) {
+      var orientation = this._getTrayOrientation(this.model.get('mqMatches'));
+      // This model change is silent because this is the initialization phase.
+      this.model.set('orientation', orientation, { 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;
+  },
+
+  /**
+   * Toolbar tab click event handler; sets activeTab.
+   */
+  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();
-    $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));
+  },
+
+  /**
+   * Orientation toggle click event handler; toggles orientation.
+   */
+  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', 'true');
     }
     else {
-      localStorage.removeItem('Drupal.toolbar.activeTab');
+      localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
     }
-    // Disable non-selected tabs and trays.
-    $toolbar.find('.bar .trigger')
+    // Update the model.
+    this.model.set({
+      locked: locked,
+      orientation: antiOrientation
+    });
+
+    event.preventDefault();
+    event.stopPropagation();
+  },
+
+  /**
+   * Model change handler; updates tray orientation.
+   */
+  onMediaQueryChange: function (model, mqMatches) {
+    this.model.set('orientation', this._getTrayOrientation(mqMatches));
+  },
+
+  /**
+   * Gets the tray orientation depending on whether the media query matches.
+   *
+   * @param Boolean mqMatches
+   *   Indicates whether the media query matches.
+   *
+   * @return String
+   *   The orientation, either 'horizontal' or 'vertical'.
+   */
+  _getTrayOrientation: function (mqMatches) {
+    return mqMatches ? 'horizontal' : 'vertical';
+  },
+
+  /**
+   * Toggles 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.
+      $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');
-    $toolbar.find('.tray').not($activateTray).removeClass('active');
-    // Update the page and toolbar dimension indicators.
-    updatePeripherals();
-  }
-};
+    // 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(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.
- */
-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;
-    }
-  }
   /**
-   * Get the height of the active tray and include it in the total
-   * height of the toolbar.
+   * 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.
    */
-  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);
-    // Alter the padding on the top of the body element.
-    $('body').css('padding-top', height);
-    $(document).trigger('offsettopchange', height);
-    $(window).trigger('resize');
+  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);
   }
-};
+});
 
 /**
- * Respond to configured media query applicability changes.
+ * Backbone Model for collapsible menus.
  */
-Drupal.toolbar.mediaQueryChangeHandler = function (mql) {
-  var orientation = (mql.matches) ? 'horizontal' : 'vertical';
-  changeOrientation(orientation);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+var MenuStateModel = Backbone.Model.extend({
+  defaults: {
+    subtrees: {}
+  }
+});
 
 /**
- * Respond to the toggling of the tray orientation.
+ * Backbone View for collapsible menus.
  */
-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 MenuView = Backbone.View.extend({
 
-/**
- * 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');
-    if (locked) {
-      localStorage.setItem('Drupal.toolbar.trayVerticalLocked', JSON.stringify(locked));
+  /**
+   * Implements Backbone.View.prototype.initialize().
+   */
+  initialize: function () {
+    this.model.on('change:subtrees', this.render, this);
+  },
+
+  /**
+   * 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]);
+      }
     }
-    else {
-      localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
+    // Render the main menu as a nested, collapsible accordion.
+    if ('drupalToolbarMenu' in $.fn) {
+      this.$el
+        .find('> .menu')
+        .drupalToolbarMenu();
     }
   }
-  if ((!locked && newOrientation === 'horizontal') || newOrientation === 'vertical') {
-    $trays
-      .removeClass('horizontal vertical')
-      .addClass(newOrientation);
-    orientation = newOrientation;
-    toggleOrientationToggle((newOrientation === 'vertical') ? 'horizontal' : 'vertical');
-  }
-}
 
-/**
- * Mark up the body tag to reflect the current state of the toolbar.
- */
-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'));
-}
+});
+
+
 
 /**
- * Change the orientation toggle active state.
+ *
  */
-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);
+function dimensionsChangeHandler (event, height) {
+  // Alter the padding on the top of the body element.
+  $('body')
+    .css('padding-top', height);
+  $(document)
+    .trigger('offsettopchange', height);
 }
 
 /**
- * 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();
+function orientationChangeHandler (event, orientation) {
+  $('body')
+    .toggleClass('toolbar-vertical', orientation === 'vertical')
+    .toggleClass('toolbar-horizontal', orientation === 'horizontal');
 }
 
 /**
- * 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));
+function trayChangeHandler (event, tray) {
+  $('body')
+    .toggleClass('toolbar-tray-open', !!tray);
 }
 
 /**
- * A toggle is an interactive element often bound to a click handler.
+ * Theme function for the toolbar orientation toggle.
  *
- * @return {String}
- *   A string representing a DOM fragment.
+ * @return
+ *   The corresponding HTML.
  */
 Drupal.theme.toolbarOrientationToggle = function () {
   return '<div class="toggle-orientation"><div class="lining">' +
@@ -322,24 +482,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));
+}(jQuery, Backbone, Drupal, drupalSettings, document, window));
diff --git a/core/modules/toolbar/js/toolbar.menu.js b/core/modules/toolbar/js/toolbar.menu.js
index f05bcb0..46c2143 100644
--- a/core/modules/toolbar/js/toolbar.menu.js
+++ b/core/modules/toolbar/js/toolbar.menu.js
@@ -2,19 +2,19 @@
  * Builds a nested accordion widget.
  *
  * Invoke on an HTML list element with the jQuery plugin pattern.
- * - For example, $('.menu').toolbarMenu();
+ * - For example, $('.menu').drupalToolbarMenu();
  */
 
-(function ($, Drupal) {
+(function ($, Drupal, drupalSettings) {
 
 "use strict";
 
 /**
  * Store the open menu tray.
  */
-var activeItem = drupalSettings.basePath + drupalSettings.currentPath;
+var activeItem = drupalSettings.basePath + Drupal.encodePath(drupalSettings.currentPath);
 
-  $.fn.toolbarMenu = function () {
+  $.fn.drupalToolbarMenu = function () {
 
     var ui = {
       'handleOpen': Drupal.t('Extend'),
@@ -156,4 +156,4 @@ var activeItem = drupalSettings.basePath + drupalSettings.currentPath;
     return '<button class="' + options['class'] + '"><span class="action">' + options.action + '</span><span class="label">' + options.text + '</span></button>';
   };
 
-}(jQuery, Drupal));
+}(jQuery, Drupal, drupalSettings));
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index e4996bc..63a061e 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;
 }
 
@@ -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 bdf5862..7761440 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2710,7 +2710,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

