From af55d1aeb01555e05663e7bdf1628db7d47e6273 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Tue, 16 Jul 2013 15:45:01 -0400
Subject: [PATCH] Issue #1860434: Refactor the Toolbar JavaScript to use
 Backbone; fix several poorly functioning behaviors
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/toolbar/config/toolbar.settings.yml   |    1 +
 core/modules/toolbar/css/toolbar.module.css        |   14 +-
 core/modules/toolbar/js/toolbar.js                 |  890 ++++++++++++--------
 core/modules/toolbar/js/toolbar.menu.js            |   10 +-
 .../Drupal/toolbar/Routing/ToolbarController.php   |    2 +-
 core/modules/toolbar/toolbar.module                |   39 +-
 7 files changed, 577 insertions(+), 381 deletions(-)

diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module
index 03cd17e..10d57b1 100644
--- a/core/modules/contextual/contextual.module
+++ b/core/modules/contextual/contextual.module
@@ -28,7 +28,6 @@ function contextual_toolbar() {
   }
 
   $tab['contextual'] = array(
-    '#type' => 'toolbar_item',
     'tab' => array(
       '#type' => 'html_tag',
       '#tag' => 'button',
@@ -39,6 +38,7 @@ function contextual_toolbar() {
         'aria-pressed' => 'false',
       ),
     ),
+    '#theme_wrappers' => array('toolbar_tab_wrapper'),
     '#wrapper_attributes' => array(
       'class' => array('hidden', 'contextual-toolbar-tab'),
     ),
diff --git a/core/modules/toolbar/config/toolbar.settings.yml b/core/modules/toolbar/config/toolbar.settings.yml
index 4df2f2a..1f2de55 100644
--- a/core/modules/toolbar/config/toolbar.settings.yml
+++ b/core/modules/toolbar/config/toolbar.settings.yml
@@ -1,3 +1,4 @@
 breakpoints:
   - module.toolbar.narrow
+  - module.toolbar.standard
   - module.toolbar.wide
diff --git a/core/modules/toolbar/css/toolbar.module.css b/core/modules/toolbar/css/toolbar.module.css
index d3e3f9e..279d57b 100644
--- a/core/modules/toolbar/css/toolbar.module.css
+++ b/core/modules/toolbar/css/toolbar.module.css
@@ -55,7 +55,7 @@
  * is active. The toolbar container, that contains the bar and the trays, is
  * position absolutely so that it scrolls with the page. Otherwise, on smaller
  * screens, the components of the admin toolbar are positioned statically. */
-body.toolbar-paneled .toolbar-oriented,
+body.toolbar-fixed .toolbar-oriented,
 .toolbar-oriented .toolbar-bar,
 .toolbar-oriented .toolbar-tray {
   left: 0;
@@ -69,13 +69,13 @@ body.toolbar-paneled .toolbar-oriented,
 }
 /* Position the admin toolbar fixed when the configured standard breakpoint is
  * active. */
-body.toolbar-paneled .toolbar-oriented .toolbar-bar {
+body.toolbar-fixed .toolbar-oriented .toolbar-bar {
   position: fixed;
 }
 /* When the configured narrow breakpoint is active, the toolbar is sized to wrap
  * around the trays in order to provide a context for scrolling tray content
  * that is taller than the viewport. */
-body.toolbar-tray-open.toolbar-paneled.toolbar-vertical .toolbar-oriented {
+body.toolbar-tray-open.toolbar-fixed.toolbar-vertical .toolbar-oriented {
   bottom: 0;
   width: 240px;
   width: 15rem;
@@ -175,13 +175,13 @@ body.toolbar-tray-open.toolbar-paneled.toolbar-vertical .toolbar-oriented {
 /* When the configured standard breakpoint is active and the tray is in a
  * horizontal position, the tray is fixed to the top of the viewport and does
  * not scroll with the page contents. */
-body.toolbar-paneled .toolbar .toolbar-tray-horizontal {
+body.toolbar-fixed .toolbar .toolbar-tray-horizontal {
   position: fixed;
 }
 /* When the configured standard breakpoint is active and the tray is in a
  * vertical position, the tray does not scroll with the page. The contents of
  * the tray scroll within the confines of the viewport. */
-body.toolbar-paneled .toolbar .toolbar-tray-vertical {
+body.toolbar-fixed .toolbar .toolbar-tray-vertical {
   height: 100%;
   overflow-x: hidden;
   overflow-y: auto;
@@ -200,11 +200,11 @@ body.toolbar-paneled .toolbar .toolbar-tray-vertical {
 }
 /* When the configured standard breakpoint is active, the tray appears to push
  * the page content away from the edge of the viewport. */
-body.toolbar-vertical.toolbar-paneled {
+body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
   margin-left: 240px; /* LTR */
   margin-left: 15rem; /* LTR */
 }
-[dir="rtl"] body.toolbar-vertical.toolbar-paneled {
+[dir="rtl"] body.toolbar-tray-open.toolbar-vertical.toolbar-fixed {
   margin-left: auto;
   margin-left: auto;
   margin-right: 240px;
diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index dea38b5..449ca94 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -3,41 +3,27 @@
  *
  * Defines the behavior of the Drupal administration toolbar.
  */
-(function ($, Drupal, drupalSettings) {
+(function ($, Drupal, drupalSettings, Backbone) {
 
 "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 = {
-  narrow: null,
-  standard: null,
-  wide: null
-};
+// Merge run-time settings with the defaults.
+var options = $.extend({
+  breakpoints: {
+    'module.toolbar.narrow': '',
+    'module.toolbar.standard': '',
+    'module.toolbar.wide': ''
+  },
+  strings: {
+    opened: Drupal.t('opened'),
+    closed: Drupal.t('closed'),
+    horizontal: Drupal.t('Horizontal orientation'),
+    vertical: Drupal.t('Vertical orientation')
+  }
+}, drupalSettings.toolbar);
 
 /**
- * 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.
@@ -45,329 +31,561 @@ var mql = {
  * Modules register tabs with hook_toolbar().
  */
 Drupal.behaviors.toolbar = {
-  attach: function(context) {
-    // Verify that the sser agent understands media queries. Complex admin
+
+  attach: function (context) {
+    // Verify that the user agent understands media queries. Complex admin
     // toolbar layouts require media query support.
     if (!window.matchMedia('only screen').matches) {
       return;
-    };
-    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]);
-          }
+    }
+    // Process the administrative toolbar.
+    $(context).find('#toolbar-administration').once('toolbar', function () {
+
+      // Establish the toolbar models and views.
+      var model = Drupal.toolbar.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')))
+      });
+      Drupal.toolbar.views.toolbarVisualView = new Drupal.toolbar.ToolbarVisualView({
+        el: this,
+        model: model,
+        strings: options.strings
+      });
+      Drupal.toolbar.views.toolbarAuralView = new Drupal.toolbar.ToolbarAuralView({
+        el: this,
+        model: model,
+        strings: options.strings
+      });
+      Drupal.toolbar.views.bodyVisualView = new Drupal.toolbar.BodyVisualView({
+        el: this,
+        model: model
+      });
+
+      // Render collapsible menus.
+      var menuModel = Drupal.toolbar.models.menuModel = new Drupal.toolbar.MenuModel();
+      Drupal.toolbar.views.menuVisualView = new Drupal.toolbar.MenuVisualView({
+        el: $(this).find('.toolbar-menu-administration').get(0),
+        model: menuModel,
+        strings: options.strings
+      });
+
+      // 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);
+      });
+
+      // Attach a listener to the configured media query breakpoints.
+      for (var label in options.breakpoints) {
+        if (options.breakpoints.hasOwnProperty(label)) {
+          var mq = options.breakpoints[label];
+          var mql = Drupal.toolbar.mql[label] = window.matchMedia(mq);
+          // Curry the model and the label of the media query breakpoint to the
+          // mediaQueryChangeHandler function.
+          mql.addListener(Drupal.toolbar.mediaQueryChangeHandler.bind(null, model, label));
+          // Fire the mediaQueryChangeHandler for each configured breakpoint
+          // so that they process once.
+          Drupal.toolbar.mediaQueryChangeHandler.call(null, model, label, mql);
         }
       }
-      // 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('.toolbar-tray');
-      $trays
-        // Add the tray orientation toggles.
-        .find('.toolbar-lining')
-        .append(Drupal.theme('toolbarOrientationToggle'));
-      // Store media queries and attach a handler.
-      mql.narrow = window.matchMedia(options.breakpoints['module.toolbar.narrow']);
-      mql.narrow.addListener(Drupal.toolbar.narrowMediaQueryChangeHandler);
-      mql.standard = window.matchMedia(options.breakpoints['module.toolbar.standard']);
-      mql.standard.addListener(Drupal.toolbar.standardMediaQueryChangeHandler);
-      mql.wide = window.matchMedia(options.breakpoints['module.toolbar.wide']);
-      mql.wide.addListener(Drupal.toolbar.wideMediaQueryChangeHandler);
-      // Fire each MediaQuery change handler so they process once.
-      Drupal.toolbar.narrowMediaQueryChangeHandler.call(this, mql.narrow);
-      Drupal.toolbar.standardMediaQueryChangeHandler.call(this, mql.standard);
-      Drupal.toolbar.wideMediaQueryChangeHandler.call(this, mql.wide);
-      // 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.
+
       $(document)
-        .on('drupalViewportOffsetChange.toolbar', Drupal.toolbar.adjustPlacement);
-      // Attach behaviors to the toolbar.
-      $toolbar
-        .on('click.toolbar', '.toolbar-bar a', Drupal.toolbar.toggleTray)
-        .on('click.toolbar', '.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();
-      }
-      // Call displace to get the initial placement of offset elements.
+        // Update the model when the viewport offset changes.
+        .on('drupalViewportOffsetChange.toolbar', function (event, offsets) {
+          model.set('offsets', offsets);
+        })
+        // The overlay will hide viewport overflow, potentially stranding tray
+        // items that are offscreen. The toolbar will adjust tray presentation
+        // to prevent this when viewport overflow is hidden.
+        .on('drupalOverlayOpen.toolbar', function () {
+          model.set('isViewportOverflowConstrained', true);
+        })
+        .on('drupalOverlayClose.toolbar', function () {
+          model.set('isViewportOverflowConstrained', false);
+        });
+
+      // 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();
-    }
-  },
-  // Default options.
-  options: {
-    breakpoints: {
-      'module.toolbar.narrow': '',
-      'module.toolbar.standard': '',
-      'module.toolbar.wide': ''
-    }
+    });
   }
-};
 
-/**
- * Set subtrees.
- *
- * JSONP callback.
- * @see toolbar_subtrees_jsonp().
- */
-Drupal.toolbar.setSubtrees = function(subtrees) {
-  Drupal.toolbar.subtrees = subtrees;
 };
 
 /**
- * Toggle a toolbar tab and the associated tray.
+ * Toolbar methods of Backbone objects.
  */
-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 + '"].toolbar-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));
+Drupal.toolbar = {
+
+  // A hash of View instances.
+  views: {},
+
+  // A hash of Model instances.
+  models: {},
+
+  // A hash of MediaQueryList objects tracked by the toolbar.
+  mql: {},
+
+  /**
+   * 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(),
+
+  /**
+   * Respond to configured narrow media query changes.
+   */
+  mediaQueryChangeHandler: function (model, label, mql) {
+    switch (label) {
+      case 'module.toolbar.narrow':
+        model.set('isOriented', mql.matches);
+        // If the toolbar doesn't have an explicit orientation yet, or if the
+        // narrow media query doesn't match then set the orientation to
+        // vertical.
+        if (!mql.matches || !model.get('orientation')) {
+          model.set('orientation', 'vertical');
+        }
+        break;
+      case 'module.toolbar.standard':
+        model.set('isFixed', mql.matches);
+        break;
+      case 'module.toolbar.wide':
+        model.set('orientation', ((mql.matches) ? 'horizontal' : 'vertical'));
+        break;
+      default:
+        break;
     }
-    else {
-      localStorage.removeItem('Drupal.toolbar.activeTab');
+  },
+
+  /**
+   * Backbone model for the toolbar.
+   */
+  ToolbarModel: Backbone.Model.extend({
+    defaults: {
+      // The active toolbar tab. All other tabs 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,
+      // Indicates whether the toolbar is displayed in an oriented fashion,
+      // either horizontal or vertical.
+      isOriented: false,
+      // Indicated whether the toolbar is positioned absolute (false) or fixed
+      // (true).
+      isFixed: false,
+      // If the viewport overflow becomes constrained, such as when the overlay
+      // is open, isFixed must be true so that elements in the trays aren't
+      // lost offscreen and impossible to get to.
+      isViewportOverflowConstrained: false,
+      // The orientation of the active tray.
+      orientation: 'vertical',
+      // 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,
+      // 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
+      }
+    },
+
+    /**
+     * {@inheritdoc}
+     *
+     * Call to set() are not automatically validated in Backbone 1.0. The option
+     * validate: true must be supplied. So we override set() and pass that option
+     * with all calls to set().
+     */
+    set: function () {
+      var args = Array.prototype.slice.call(arguments);
+      // If the changed attribute is passed as a string, then the second arg
+      // will be the new value. Pack these into an option in arg[0];
+      if (typeof args[0] === 'string') {
+        var tmp = {};
+        tmp[args[0]] = args[1];
+        args[0] = tmp;
+        // If options were passed, make them the second argument.
+        args[1] = args[2] || {};
+      }
+      // Get the options argument.
+      var options = args[1] || {};
+      // Validate all calls to set. Prefer the options provided to set().
+      args[1] = $.extend({
+        validate: true
+      }, options);
+      Backbone.Model.prototype.set.apply(this, args);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    validate: function (attributes, options) {
+      // Prevent the orientation being set to horizontal if it is locked, unless
+      // override has not been passed as an option.
+      if (attributes.orientation === 'horizontal' && this.get('locked') && !options.override) {
+        return Drupal.t('The toolbar cannot be set to a horizontal orientation when it is locked.');
+      }
     }
-    // Disable non-selected tabs and trays.
-    $toolbar.find('.toolbar-bar .trigger')
-      .not($tab)
-      .removeClass('active')
-      // Set aria-pressed to false.
-      .prop('aria-pressed', false);
-    $toolbar.find('.toolbar-tray').not($activateTray).removeClass('active');
-  }
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+  }),
 
-/**
- * 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) {
-  if (!mql.narrow.matches) {
-    var $body = $('body');
-    var $trays = $toolbar.find('.toolbar-tray');
-    // Alter the padding on the top of the body element.
-    $body.css('padding-top', 0);
-    $trays.css('padding-top', 0);
-    // Remove any orientation classes. Make vertical the default for trays.
-    $body.removeClass('toolbar-vertical toolbar-horizontal');
-    $trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical');
-  }
-  else {
-    // Alter the padding on the top of the body element.
-    $('body').css('padding-top', offsets.top);
-    // The navbar container is invisible. Its placement is used to determine the
-    // container for the trays.
-    $toolbar.find('.toolbar-tray').css('padding-top', $toolbar.find('.toolbar-bar').outerHeight());
-  }
-};
+  /**
+   * Backbone view for the aural feedback of the toolbar.
+   */
+  ToolbarAuralView: Backbone.View.extend({
 
-/**
- * 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 side offset from the trays.
-  $toolbar.find('.toolbar-tray').removeAttr('data-offset-' + edge + ' data-offset-top');
-  // If the page is wider than the narrow media query, apply offset attributes.
-  if (mql.narrow.matches) {
-    // If an active vertical tray exists, mark it as an offset element.
-    $toolbar.find('.toolbar-tray.toolbar-tray-vertical.active').attr('data-offset-' + edge, '');
-    // If an active horizontal tray exists, mark it as an offset element.
-    $toolbar.find('.toolbar-tray.toolbar-tray-horizontal.active').attr('data-offset-top', '');
-  }
-  // Trigger a recalculation of viewport displacing elements.
-  Drupal.displace();
-};
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function (options) {
+      this.strings = options.strings;
 
-/**
- * Respond to configured narrow media query changes.
- */
-Drupal.toolbar.narrowMediaQueryChangeHandler = function (mql) {
-  var $bar = $toolbar.find('.toolbar-bar');
-  if (mql.matches) {
-    $bar.attr('data-offset-top', '');
-  }
-  else {
-    $bar.removeAttr('data-offset-top');
-  }
-  // Toggle between a basic vertical view and a more sophisticated horizontal
-  // and vertical display of the toolbar bar and trays.
-  $toolbar.toggleClass('toolbar-oriented', mql.matches);
-  if (mql.matches) {
-    changeOrientation('vertical');
-  }
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+      this.model.on('change:orientation', this.onOrientationChange, this);
+      this.model.on('change:activeTray', this.onActiveTrayChange, this);
+    },
 
-/**
- * Respond to configured standard media query changes.
- */
-Drupal.toolbar.standardMediaQueryChangeHandler = function (mql) {
-  $('body').toggleClass('toolbar-paneled', mql.matches);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+    /**
+     * Announces an orientation change.
+     *
+     * @param Drupal.Toolbar.ToolbarModel model
+     * @param String orientation
+     *   The new value of the orientation attribute in the model.
+     */
+    onOrientationChange: function (model, orientation) {
+      Drupal.announce(Drupal.t('Tray orientation changed to @orientation.', {
+        '@orientation': orientation
+      }));
+    },
 
-/**
- * Respond to configured wide media query changes.
- */
-Drupal.toolbar.wideMediaQueryChangeHandler = function (mql) {
-  var orientation = (mql.matches) ? 'horizontal' : 'vertical';
-  changeOrientation(orientation);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+    /**
+     * Announces a changed active tray.
+     *
+     * @param Drupal.Toolbar.ToolbarModel model
+     * @param Element orientation
+     *   The new value of the tray attribute in the model.
+     */
+    onActiveTrayChange: function (model, tray) {
+      var relevantTray = (tray === null) ? model.previous('activeTray') : tray;
+      var state = (tray === null) ? this.strings.closed : this.strings.opened;
+      Drupal.announce(Drupal.t('"@tray" tray @state.', {
+        '@tray': relevantTray.querySelector('.toolbar-tray-name').textContent,
+        '@state': state
+      }));
+    }
+  }),
 
-/**
- * Respond to the toggling of the tray orientation.
- */
-Drupal.toolbar.orientationChangeHandler = function (event) {
-  event.preventDefault();
-  event.stopPropagation();
-  var orientation = event.target.value;
-  changeOrientation(orientation, true);
-  // Update the page and toolbar dimension indicators.
-  updatePeripherals();
-};
+  /**
+   * Backbone view for the toolbar element.
+   */
+  ToolbarVisualView: 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) {
-  if (isLock) {
-    locked = (newOrientation === 'vertical');
-    if (locked) {
-      localStorage.setItem('Drupal.toolbar.trayVerticalLocked', JSON.stringify(locked));
+    events: {
+      'click .toolbar-bar .toolbar-tab': 'onTabClick',
+      'click .toolbar-toggle-orientation button': 'onOrientationToggleClick'
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function (options) {
+      this.strings = options.strings;
+
+      this.model.on('change:activeTab change:orientation change:isOriented', this.render, this);
+      this.model.on('change:mqMatches', this.onMediaQueryChange, this);
+      this.model.on('change:offsets', this.adjustPlacement, this);
+
+      // Add the tray orientation toggles.
+      this.$el
+        .find('.toolbar-tray .toolbar-lining')
+        .append(Drupal.theme('toolbarOrientationToggle'));
+
+      // 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();
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function (model) {
+      this.updateTabs();
+      this.updateTrayOrientation();
+      this.updateBarAttributes();
+      return this;
+    },
+
+    /**
+     * Responds to a toolbar tab click.
+     *
+     * @param jQuery.Event event
+     */
+    onTabClick: function (event) {
+      // If this tab has a tray associated with it, it is considered an
+      // activatable tab.
+      if (event.target.hasAttribute('data-toolbar-tray')) {
+        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
+      }, {
+        override: true
+      });
+
+      event.preventDefault();
+      event.stopPropagation();
+    },
+
+    /**
+     * Updates the display of the tabs: toggles a tab and the associated tray.
+     */
+    updateTabs: function () {
+      var $tab = $(this.model.get('activeTab'));
+      var $tray = $();
+      // Deactivate the previous tab.
+      $(this.model.previous('activeTab'))
+        .removeClass('active')
+        .prop('aria-pressed', false);
+      // Deactivate the previous tray.
+      $(this.model.previous('activeTray'))
+        .removeClass('active');
+
+      // Activate the selected tab.
+      if ($tab.length > 0) {
+        $tab
+          .addClass('active')
+          // Mark the tab as pressed.
+          .prop('aria-pressed', true);
+        var name = $tab.attr('data-toolbar-tray');
+        // 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 + '"].toolbar-tray');
+        if ($tray.length) {
+          $tray.addClass('active');
+          this.model.set('activeTray', $tray.get(0));
+        }
+        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');
+      }
+    },
+
+    /**
+     * Update the attributes of the toolbar bar element.
+     */
+    updateBarAttributes: function () {
+      var isOriented = this.model.get('isOriented');
+      if (isOriented) {
+        this.$el.find('.toolbar-bar').attr('data-offset-top', '');
+      }
+      else {
+        this.$el.find('.toolbar-bar').removeAttr('data-offset-top');
+      }
+      // Toggle between a basic vertical view and a more sophisticated
+      // horizontal and vertical display of the toolbar bar and trays.
+      this.$el.toggleClass('toolbar-oriented', isOriented);
+    },
+
+    /**
+     * Updates the orientation of the active tray if necessary.
+     */
+    updateTrayOrientation: function () {
+      var orientation = 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('.toolbar-tray')
+        .removeClass('toolbar-tray-horizontal toolbar-tray-vertical')
+        .addClass('toolbar-tray-' + orientation);
+
+      // Update the tray orientation toggle button.
+      var iconClass = 'toolbar-icon-toggle-' + orientation;
+      var iconAntiClass = 'toolbar-icon-toggle-' + antiOrientation;
+      this.$el.find('.toolbar-toggle-orientation button')
+        .val(antiOrientation)
+        .text(this.strings[antiOrientation])
+        .removeClass(iconClass)
+        .addClass(iconAntiClass);
+
+      // Update data offset attributes for the trays.
+      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('.toolbar-tray-vertical.active').attr('data-offset-' + edge, '');
+      // If an active horizontal tray exists, mark it as an offset element.
+      $trays.filter('.toolbar-tray-horizontal.active').attr('data-offset-top', '');
+      // Trigger a recalculation of viewport displacing elements.
+      Drupal.displace();
+    },
+
+    /**
+     * Sets the tops of the trays so that they align with the bottom of the bar.
+     */
+    adjustPlacement: function () {
+      var $trays = this.$el.find('.toolbar-tray');
+      if (!this.model.get('isOriented')) {
+        $trays.css('padding-top', 0);
+        $trays.removeClass('toolbar-tray-horizontal').addClass('toolbar-tray-vertical');
+      }
+      else {
+        // The navbar container is invisible. Its placement is used to determine
+        // the container for the trays.
+        $trays.css('padding-top', this.$el.find('.toolbar-bar').outerHeight());
+      }
     }
-    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('toolbar-tray-horizontal toolbar-tray-vertical')
-      .addClass('toolbar-tray-' + 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.
+   */
+  MenuVisualView: Backbone.View.extend({
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.model.on('change:subtrees', this.render, this);
+    },
 
-/**
- * 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 = 'toolbar-icon-toggle-' + orientation;
-  var iconAntiClass = 'toolbar-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('.toolbar-toggle-orientation button')
-    .val(orientation)
-    .text(strings[orientation])
-    .removeClass(iconAntiClass)
-    .addClass(iconClass);
-}
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      var subtrees = this.model.get('subtrees');
+      // Add subtrees.
+      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
+          .children('.menu')
+          .drupalToolbarMenu();
+      }
+    }
+  }),
 
-/**
- * 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();
-}
+  /**
+   * Adjusts the body element with the toolbar position and dimension changes.
+   */
+  BodyVisualView: Backbone.View.extend({
 
-/**
- * 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));
-}
+    /**
+     * {@inheritdoc}
+     */
+    initialize: function () {
+      this.model.on('change:orientation change:offsets change:activeTray change:isOriented change:isFixed change:isViewportOverflowConstrained', this.render, this);
+    },
+
+    /**
+     * {@inheritdoc}
+     */
+    render: function () {
+      var $body = $('body');
+      var orientation = this.model.get('orientation');
+      var isOriented = this.model.get('isOriented');
+      var isViewportOverflowConstrained = this.model.get('isViewportOverflowConstrained');
+
+      $body
+        // Apply classes to the body element that reflect the current
+        // orientation of the active toolbar. The toolbar is not oriented by
+        // default. It displays within the page flow. The vertical orientation
+        // pulls the toolbar out of the page flow and positions its elements
+        // over or around the page's content. Viewport offsets or body styling
+        // may be necessary for the vertical and horizontal orientations.
+        .toggleClass('toolbar-vertical', (orientation === 'vertical'))
+        .toggleClass('toolbar-horizontal', (isOriented && orientation === 'horizontal'))
+        // When the toolbar is fixed, it will not scroll with page scrolling.
+        .toggleClass('toolbar-fixed', (isViewportOverflowConstrained || this.model.get('isFixed')))
+        // Toggle the toolbar-tray-open class on the body element. The class is
+        // applied when a toolbar tray is active. Padding might be applied to
+        // the body element to prevent the tray from overlapping content.
+        .toggleClass('toolbar-tray-open', !!this.model.get('activeTray'))
+        // Apply padding to the top of the body to offset the placement of the
+        // toolbar bar element.
+        .css('padding-top', this.model.get('offsets').top);
+    }
+  })
+};
 
 /**
  * A toggle is an interactive element often bound to a click handler.
@@ -381,24 +599,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="visually-hidden" 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, Drupal, drupalSettings, Backbone));
diff --git a/core/modules/toolbar/js/toolbar.menu.js b/core/modules/toolbar/js/toolbar.menu.js
index 4671fce..ce2f0ea 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'),
@@ -154,4 +154,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/lib/Drupal/toolbar/Routing/ToolbarController.php b/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php
index af1935f..7404555 100644
--- a/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Routing/ToolbarController.php
@@ -23,7 +23,7 @@ public function subtreesJsonp() {
     _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;
   }
 
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index bae1b07..7f132e2 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -80,8 +80,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',
     ),
@@ -251,22 +249,24 @@ function theme_toolbar(&$variables) {
  *   A renderable array.
  */
 function toolbar_pre_render_item($element) {
+  // Assign each item a 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;
@@ -274,9 +274,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'])) {
@@ -290,11 +290,8 @@ function toolbar_pre_render_item($element) {
     }
     // Add the standard theme_wrapper for trays.
     array_unshift($element['tray']['#theme_wrappers'], 'toolbar_tray_wrapper');
-    // If a #heading is provided for the tray, provided a #theme_wrapper
-    // function to append it.
-    if (!empty($element['tray']['#heading'])) {
-      array_unshift($element['tray']['#theme_wrappers'], 'toolbar_tray_heading_wrapper');
-    }
+    // Add the theme wrapper for the tray heading.
+    array_unshift($element['tray']['#theme_wrappers'], 'toolbar_tray_heading_wrapper');
   }
 
   return $element;
@@ -375,7 +372,7 @@ function theme_toolbar_tray_wrapper(&$variables) {
 function theme_toolbar_tray_heading_wrapper(&$variables) {
   if (!empty($variables['element']['#children'])) {
     $element = $variables['element'];
-    return '<h3 class="visually-hidden">' . $element['#heading'] . '</h3>' . $element['#children'];
+    return '<h3 class="toolbar-tray-name visually-hidden">' . $element['#heading'] . '</h3>' . $element['#children'];
   }
 }
 
@@ -597,9 +594,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'),
     ),
-- 
1.7.10.4

