From 89def315cc0a7bbe2413bff2e791f0a58bce7cc1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Mon, 15 Apr 2013 14:44:50 -0400
Subject: [PATCH] Issue #1860434-29
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.module          |    2 +-
 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/css/toolbar.base.css          |   17 +-
 core/modules/toolbar/js/toolbar.js                 |  766 ++++++++++++--------
 core/modules/toolbar/js/toolbar.menu.js            |   10 +-
 core/modules/toolbar/toolbar.module                |   34 +-
 core/modules/user/user.module                      |    2 +-
 10 files changed, 491 insertions(+), 351 deletions(-)

diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index 76e39ba..5fd3786 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -14,7 +14,6 @@ function contextual_toolbar() {
   }
 
   $tab['contextual'] = array(
-    '#type' => 'toolbar_item',
     'tab' => array(
       '#type' => 'html_tag',
       '#tag' => 'button',
@@ -27,6 +26,7 @@ function contextual_toolbar() {
       // @todo remove this once http://drupal.org/node/1908906 lands.
       '#options' => array('attributes' => array()),
     ),
+    '#theme_wrappers' => array('toolbar_tab_wrapper'),
     '#wrapper_attributes' => array(
       'class' => array('element-hidden', 'contextual-toolbar-tab'),
     ),
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/css/toolbar.base.css b/core/modules/toolbar/css/toolbar.base.css
index 64e6468..5d7b343 100644
--- a/core/modules/toolbar/css/toolbar.base.css
+++ b/core/modules/toolbar/css/toolbar.base.css
@@ -84,10 +84,10 @@ html.js .toolbar {
  */
 .js .toolbar .tray {
   display: none;
-  position: fixed;
 }
-/* Make vertical toolbar tray scroll with page for touch devices. */
-.touch .toolbar .tray {
+
+.toolbar .tray {
+  bottom: auto;
   position: absolute;
 }
 .toolbar .tray {
@@ -147,7 +147,6 @@ html.js .toolbar {
 }
 .toolbar .vertical.active,
 .toolbar .vertical.active > .lining {
-  bottom: 0;
   left: 0; /* LTR */
   top: 0;
 }
@@ -163,15 +162,11 @@ html.js .toolbar {
 @media only screen {
   .toolbar .vertical,
   .toolbar .vertical > .lining:before {
-    bottom: auto;
     width: 100%;
   }
 }
 
 @media only screen and (min-width: 16.5em) {
-  .toolbar .vertical {
-    bottom: 0;
-  }
   .toolbar .vertical,
   .toolbar .vertical > .lining:before {
     width: 240px;
@@ -183,9 +178,13 @@ html.js .toolbar {
   }
 }
 @media only screen and (min-width: 28.125em) {
-  .toolbar .tray.horizontal {
+  .toolbar .tray.horizontal,
+  .toolbar .tray.vertical {
     position: fixed;
   }
+  .toolbar .tray.vertical {
+    bottom: 0;
+  }
 }
 /**
  * At larger screen sizes, the tray pushes the page content.
diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index 50c6cdd..c3b35d6 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -1,333 +1,501 @@
 /**
- * @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';
+(function ($, Backbone, Drupal, drupalSettings) {
 
-/**
- * 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
-};
+"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.
  *
- * 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.
-      var subtrees = Drupal.toolbar.subtrees;
-      if (subtrees) {
-        for (var id in subtrees) {
-          if (subtrees.hasOwnProperty(id)) {
-            $('#toolbar-link-' + id).after(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']);
+  * Modules register tabs with hook_toolbar().
+  */
+ Drupal.behaviors.toolbar = {
+  attach: function (context) {
+    var that = this;
+    // Merge the defaults and toolbar settings into the full list of options.
+    var options = $.extend({}, this.defaults, (drupalSettings.toolbar || {}));
+    // Process the administration toolbar element.
+    $(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();
-      // Attach behaviors to the document.
+      var mql = window.matchMedia(options.breakpoints['module.toolbar.wide']);
+      var model = that.models.toolbarModel = new Drupal.toolbar.ToolbarModel({
+        locked: JSON.parse(localStorage.getItem('Drupal.toolbar.trayVerticalLocked')) || false,
+        activeTab: document.getElementById(JSON.parse(localStorage.getItem('Drupal.toolbar.activeTabID'))),
+        mqMatches: mql.matches
+      });
+
+      // Update the model when matchMedia fires.
+      mql.addListener(function (mql) {
+        model.set('mqMatches', mql.matches);
+      });
+
+      that.views.toolbarView = new Drupal.toolbar.ToolbarView(
+        $.extend({
+          el: this,
+          model: model
+        }, options)
+      );
+
+      that.views.bodyView = new Drupal.toolbar.BodyView(
+        $.extend({
+          el: this,
+          model: model
+        }, options)
+      );
+
+      // Render collapsible menus.
+      var menuModel = that.models.menuModel = new Drupal.toolbar.MenuModel();
+      that.views.menuView = new Drupal.toolbar.MenuView(
+        $.extend({
+          el: $(this).find('.toolbar-menu-administration').get(0),
+          model: menuModel
+        }, options)
+      );
+
+      // 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);
+      });
+
+      // Respond to viewport offset dimension changes.
       $(document)
-        .on('drupalViewportOffsetChange.toolbar', Drupal.toolbar.adjustPlacement);
-      // 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('drupalViewportOffsetChange.toolbar', function (event, offsets) {
+          // Update the model with the new offset values.
+          model.set('offsets', offsets);
+        });
+
+      // Broadcast model changes to other modules.
+      model
+        .on('change:orientation', function (model, orientation) {
+          $(document).trigger('drupalToolbarOrientationChange', orientation);
+        })
+        .on('change:activeTab', function (model, tab) {
+          $(document).trigger('drupalToolbarTabChange', tab);
+        })
+        .on('change:activeTray', function (model, tray) {
+          $(document).trigger('drupalToolbarTrayChange', tray);
+        });
+
       // Call displace to get the initial placement of offset elements.
       Drupal.displace();
-    }
+    });
   },
+
+  // A map of View instances.
+  views: {},
+
+  // A map of Model instances.
+  models: {},
+
   // 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')
     }
   }
 };
 
 /**
- * Set subtrees.
- *
- * JSONP callback.
- * @see toolbar_subtrees_jsonp().
+ * Toolbar methods of Backbone objects.
  */
-Drupal.toolbar.setSubtrees = function(subtrees) {
-  Drupal.toolbar.subtrees = subtrees;
-};
+Drupal.toolbar = Drupal.toolbar || {};
 
-/**
- * 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.prop('aria-pressed');
-    $tab.prop('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));
-    }
-    else {
-      localStorage.removeItem('Drupal.toolbar.activeTab');
-    }
-    // Disable non-selected tabs and trays.
-    $toolbar.find('.bar .trigger')
-      .not($tab)
-      .removeClass('active')
-      // Set aria-pressed to false.
-      .prop('aria-pressed', false);
-    $toolbar.find('.tray').not($activateTray).removeClass('active');
-  }
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+$.extend(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(),
 
-/**
- * Repositions trays and sets body padding according to the height of the bar.
- *
- * @param {Event} event
- *   - jQuery Event object.
- *
- * @param {Object} offsets
- *   - Contains for keys -- top, right, bottom and left -- that indicate the
- *   viewport offset distances calculated by Drupal.displace().
- */
-Drupal.toolbar.adjustPlacement = function (event, offsets) {
-  // 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;
+  /**
+   * Backbone model for the toolbar.
+   */
+  ToolbarModel: 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,
+      // The current viewport offsets determined by Drupal.displace(). The offsets
+      // suggest how a module might position is components relative to the
+      // viewport.
+      offsets: {
+        top: 0,
+        right: 0,
+        bottom: 0,
+        left: 0
+      }
     }
-  }
-  // Alter the padding on the top of the body element.
-  $('body').css('padding-top', offsets.top);
-};
+  }),
 
-/**
- * Sets the width of a vertical tray in a data attribute.
- *
- * If the width of the tray changed, Drupal.displace is called so that elements
- * can adjust to the placement of the tray.
- */
-Drupal.toolbar.setTrayWidth = function () {
-  var dir = document.documentElement.dir;
-  var edge = (dir === 'rtl') ? 'right' : 'left';
-  // Remove the left offset from the trays.
-  $toolbar.find('.tray').removeAttr('data-offset-' + edge + ' data-offset-top');
-  // If an active vertical tray exists, mark it as an offset element.
-  $toolbar.find('.tray.vertical.active').attr('data-offset-' + edge, '');
-  // If an active horizontal tray exists, mark it as an offset element.
-  $toolbar.find('.tray.horizontal.active').attr('data-offset-top', '');
-  // Trigger a recalculation of viewport displacing elements.
-  Drupal.displace();
-};
+  /**
+   * Backbone view for the toolbar element.
+   */
+  ToolbarView: Backbone.View.extend({
+    events: {
+      'click .bar .toolbar-tab': 'onTabClick',
+      'click .toggle-orientation button': 'onOrientationToggleClick'
+    },
 
-/**
- * Respond to configured media query applicability changes.
- */
-Drupal.toolbar.mediaQueryChangeHandler = function (mql) {
-  var orientation = (mql.matches) ? 'horizontal' : 'vertical';
-  changeOrientation(orientation);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+    /**
+     * Implements Backbone.View.prototype.initialize().
+     */
+    initialize: function () {
+      this.strings = this.options.strings;
 
-/**
- * Respond to the toggling of the tray orientation.
- */
-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();
-};
+      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:offsets', this.adjustPlacement, this);
 
-/**
- * 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));
+      // Add the tray orientation toggles.
+      this.$el.find('.tray')
+        .find('.lining')
+        .append(Drupal.theme('toolbarOrientationToggle'));
+
+      // Update the tray orientation.
+      var orientation = this._checkOrientationLock(this._getTrayOrientation(this.model.get('mqMatches')));
+      // This model change is silent because it will be triggered below when
+      // change:activeTab is triggered.
+      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');
+
+      // Invoke Drupal.displace() to get the current viewport offset values.
+      Drupal.displace();
+    },
+
+    /**
+     * Implements Backbone.View.prototype.render().
+     *
+     * @return Backbone.View
+     */
+    render: function () {
+      // Update the display of the tabs.
+      this.refreshTabs();
+      // Adjust the orientation of the active tray.
+      this.setOrientation();
+      // Adjust the height of the toolbar.
+      this.model.set('dimensionsAreValid', false);
+
+      return this;
+    },
+
+    /**
+     * Responds to a toolbar tab click.
+     *
+     * @param jQuery.Event event
+     */
+    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();
+    },
+
+    /**
+     * Toggles the orientation of a toolbar tray.
+     *
+     * @param jQuery.Event event
+     */
+    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.trayVerticalLocked');
+      }
+      // Update the model.
+      this.model.set({
+        locked: locked,
+        orientation: antiOrientation
+      });
+
+      event.preventDefault();
+      event.stopPropagation();
+    },
+
+    /**
+     * Responds to change:orientation event when window.matchMedia fires.
+     *
+     * @param Backbone.Model model
+     * @param Boolean mqMatches
+     *   The matches property of a MediaQueryList object.
+     */
+    onMediaQueryChange: function (model, mqMatches) {
+      this.model.set('orientation', this._checkOrientationLock(this._getTrayOrientation(mqMatches)));
+    },
+
+    /**
+     * Gets the tray orientation depending on whether the media query matches.
+     *
+     * @param Boolean mqMatches
+     *   The matches property of a MediaQueryList object.
+     * @return String
+     *   The orientation, either 'horizontal' or 'vertical'.
+     */
+    _getTrayOrientation: function (mqMatches) {
+      return mqMatches ? 'horizontal' : 'vertical';
+    },
+
+    /**
+     * Toggles a toolbar tab and the associated tray.
+     *
+     * @param jQuery.Event event
+     */
+    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.prop('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')
+        .prop('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.
+     */
+    setOrientation: function () {
+      var orientation = this._checkOrientationLock(this.model.get('orientation'));
+      // The antiOrientation is used to render the view of action buttons like
+      // the tray orientation toggle.
+      var antiOrientation = (orientation === 'vertical') ? 'horizontal' : 'vertical';
+      // 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);
+
+      var dir = document.documentElement.dir;
+      var edge = (dir === 'rtl') ? 'right' : 'left';
+      // Remove data-offset attributes from the trays so they can be refreshed.
+      $trays.removeAttr('data-offset-left data-offset-right data-offset-top');
+      // If an active vertical tray exists, mark it as an offset element.
+      $trays.filter('.vertical.active').attr('data-offset-' + edge, '');
+      // If an active horizontal tray exists, mark it as an offset element.
+      $trays.filter('.horizontal.active').attr('data-offset-top', '');
+      // Trigger a recalculation of viewport displacing elements.
+      Drupal.displace();
+
+      // Append a message that the tray orientation has been changed.
+      Drupal.announce(Drupal.t('Tray orientation changed to @orientation.', {
+        '@orientation': orientation
+      }));
+    },
+
+    /**
+     * Returns an orientation based on the orientation lock.
+     *
+     * Orientation is locked to the vertical position when a user changes a
+     * horizontal tray to a vertical try using the tray orientation toggle button.
+     *
+     * @param String orientation
+     *   The value can be either 'horizontal' or 'vertical'.
+     * @return String
+     *   The orientation, either 'horizontal' or 'vertical'.
+     */
+    _checkOrientationLock: function (orientation) {
+      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.
+      return (locked) ? 'vertical' : orientation;
+    },
+
+    /**
+     * Sets the tops of the trays so that they align with the bottom of the bar.
+     */
+    adjustPlacement: function () {
+      // 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;
+        }
+      }
     }
-    else {
-      localStorage.removeItem('Drupal.toolbar.trayVerticalLocked');
+  }),
+
+  /**
+   * Backbone Model for collapsible menus.
+   */
+  MenuModel: Backbone.Model.extend({
+    defaults: {
+      subtrees: {}
     }
-  }
-  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'));
-}
+  /**
+   * Backbone View for collapsible menus.
+   */
+  MenuView: Backbone.View.extend({
 
-/**
- * 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);
-}
+    /**
+     * Implements Backbone.View.prototype.initialize().
+     */
+    initialize: function () {
+      this.model.on('change:subtrees', this.render, this);
+    },
 
-/**
- * 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 tray width for vertical trays.
-  Drupal.toolbar.setTrayWidth();
-}
+    /**
+     * Implements Backbone.View.prototype.render().
+     */
+    render: function () {
+      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 ('drupalToolbarMenu' in $.fn) {
+        this.$el
+          .find('> .menu')
+          .drupalToolbarMenu();
+      }
+    }
+  }),
 
-/**
- * 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));
-}
+  /**
+   * Adjusts the body element with the toolbar position and dimension changes.
+   */
+  BodyView: Backbone.View.extend({
+
+    /**
+     * Implements Backbone.View.prototype.initialize().
+     */
+    initialize: function () {
+      this.model.on('change:orientation change:offsets change:activeTray', this.render, this);
+    },
+
+    /**
+     * Implements Backbone.View.prototype.render().
+     */
+    render: function () {
+      var orientation = this.model.get('orientation');
+      var tray = this.model.get('activeTray');
+      var offsets = this.model.get('offsets');
+
+      $('body')
+        // Apply classes to the body element that reflect the current
+        // orientation of the active toolbar.
+        .toggleClass('toolbar-vertical', orientation === 'vertical')
+        .toggleClass('toolbar-horizontal', orientation === 'horizontal')
+        // Toggle the toolbar-tray-open class on the body elment. The class is
+        // applied when a toolbar tray is active.
+        .toggleClass('toolbar-tray-open', !!tray)
+        // Apply padding to the top of the body to offset the placement of the
+        // toolbar bar element.
+        .css('padding-top', offsets.top);
+    }
+  })
+});
 
 /**
- * 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">' +
@@ -335,24 +503,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));
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 fe4f615..a25e1d8 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -91,8 +91,6 @@ function toolbar_element_info() {
       // The id cannot be simply "toolbar" or it will clash with the simpletest
       // tests listing which produces a checkbox with attribute id="toolbar"
       'id' => 'toolbar-administration',
-      // The 'overlay-displace-top' class pushes the overlay down, so it appears
-      // below the toolbar.
       'class' => array('toolbar',),
       'role' => 'navigation',
     ),
@@ -142,7 +140,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;
 }
 
@@ -284,21 +282,25 @@ function theme_toolbar(&$variables) {
  */
 function toolbar_pre_render_item($element) {
 
+  // Assign each item an unique ID.
+  $id = drupal_html_id('toolbar-item');
+
+  // Provide attributes for a toolbar item.
+  $attributes = array(
+    'id' => $id,
+  );
+
   // If tray content is present, markup the tray and its associated trigger.
   if (!empty($element['tray'])) {
 
-    // Trays are associated with their trigger by a unique ID.
-    $id = drupal_html_id('toolbar-tray');
-    $element['tab']['#tray_id'] = $id;
-
-    // Provide attributes for a tray trigger.
-    $attributes = array(
-      'id' => 'toolbar-tab-' . $id,
-      'data-toolbar-tray' => $id,
+    // Provide attributes necessary for trays.
+    $attributes += array(
+      'data-toolbar-tray' => $id . '-tray',
       'aria-owns' => $id,
       'role' => 'button',
       'aria-pressed' => 'false',
     );
+
     // Merge in module-provided attributes.
     $element['tab'] += array('#attributes' => array());
     $element['tab']['#attributes'] += $attributes;
@@ -306,9 +308,9 @@ function toolbar_pre_render_item($element) {
 
     // Provide attributes for the tray theme wrapper.
     $attributes = array(
-      'id' => $id,
-      'data-toolbar-tray' => $id,
-      'aria-owned-by' => 'toolbar-tab-' . $id,
+      'id' => $id . '-tray',
+      'data-toolbar-tray' => $id . '-tray',
+      'aria-owned-by' => $id,
     );
     // Merge in module-provided attributes.
     if (!isset($element['tray']['#wrapper_attributes'])) {
@@ -485,7 +487,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,9 +630,9 @@ 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'),
       array('system', 'drupal.displace'),
       array('toolbar', 'toolbar.menu'),
     ),
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index d41206f..7f49635 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -2713,7 +2713,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

