From 8dc9bdcf73d8e8aa7c5ebbfddb511b88c8f036ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= <splendidnoise@gmail.com>
Date: Wed, 20 Mar 2013 17:27:51 -0400
Subject: [PATCH] Issue #1847084 by trawekp, tarekdj, jessebeach: Vertical
 toolbar doesn't work with overlay
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/misc/displace.js                     |  126 +++++++++++++++++++++++++++++
 core/misc/tableheader.js                  |   50 +++++-------
 core/modules/overlay/overlay-child.css    |   12 +--
 core/modules/overlay/overlay-child.js     |   11 +--
 core/modules/overlay/overlay-parent.js    |   46 +++++++----
 core/modules/overlay/overlay.module       |    1 +
 core/modules/system/system.module         |   16 ++++
 core/modules/toolbar/css/toolbar.base.css |    2 +-
 core/modules/toolbar/js/toolbar.js        |   28 ++++++-
 core/modules/toolbar/toolbar.module       |    6 +-
 core/themes/bartik/css/style.css          |    4 +-
 core/themes/seven/style.css               |    4 +-
 12 files changed, 236 insertions(+), 70 deletions(-)
 create mode 100644 core/misc/displace.js

diff --git a/core/misc/displace.js b/core/misc/displace.js
new file mode 100644
index 0000000..f2638f5
--- /dev/null
+++ b/core/misc/displace.js
@@ -0,0 +1,126 @@
+/**
+ * Manages elements that can offset the size of the viewport.
+ */
+(function (window, Drupal, $) {
+
+  var offsets = {
+    top: 0,
+    right: 0,
+    bottom: 0,
+    left: 0
+  };
+
+  /**
+   * Informs listeners of the current offset dimensions.
+   *
+   * @param Boolean refresh
+   *   - When true or undefined, causes the current offsets values to be
+   *   recalculated.
+   */
+  function displace (refresh) {
+    if (refresh === undefined || refresh) {
+      calculateOffsets();
+      $(document).trigger('drupalViewportOffsetChange', offsets);
+    }
+    return offsets;
+  }
+
+  /**
+   * Determines the viewport offsets.
+   *
+   * Any element with the attribute data-offset-{edge} e.g. data-offset-top will
+   * be consider in the viewport offset calculations. If the attribute has a
+   * numeric value, that value will be used. If no value is provided, one will
+   * be calculated using the element's dimensions and placement.
+   */
+  function calculateOffsets () {
+    // Perform the calculations on a local varialbe
+    var offset = {
+      top: 0,
+      right: 0,
+      bottom: 0,
+      left: 0
+    }
+    // Go through each edge and find the largest displacement.
+    for (var edge in offset) {
+      if (offset.hasOwnProperty(edge)) {
+        var $displacingElements = $('[data-offset-' + edge + ']');
+        for (var i = 0, n = $displacingElements.length; i < n; i++) {
+          var $el = $displacingElements.eq(i);
+          // If the element is not visble, do consider its dimensions.
+          if (!$el.is(':visible')) {
+            continue;
+          }
+          // If the offset data attribute contains a displacing value, use it.
+          var displacement = parseInt($el.attr('data-offset-' + edge), 10);
+          // If the element's offset data attribute exits
+          // but is not a valid number then get the displacement
+          // dimensions directly from the element.
+          if (typeof displacement !== 'number' || isNaN(displacement)) {
+            displacement = getRawOffset($el, edge);
+          }
+          // If the displacement value is larger than the current value for this
+          // edge, use the displacement value.
+          offset[edge] = Math.max(offset[edge], displacement);
+        }
+      }
+    }
+    // Store the calculated offset
+    offsets = offset;
+  }
+
+  /**
+   * Calculates displacement for element based on its dimensions and placement.
+   *
+   * @param jQuery $el
+   *   - The jQuery element whose dimensions and placement will be measured.
+   *
+   * @param string edge
+   *   - The name of the edge of the viewport that the element is associated
+   *   with.
+   */
+  function getRawOffset ($el, edge) {
+    var size = 0;
+    var docSize = 0;
+    var displacement = 0;
+    // Get the offset of the element itself.
+    var placement = $el.offset()[(edge === 'left' || edge === 'right') ? 'left' : 'top'];
+    // Subtract scroll distance from placement to get the distance
+    // to the edge of the viewport.
+    placement = placement - window['scroll' + ((edge === 'right' || edge === 'left') ? 'X' : 'Y')];
+    // Find the displacement value according to the edge.
+    switch (edge) {
+      // Left and top elements displace as a sum of their own offset value
+      // plus their size.
+      case 'top':
+        size = $el.outerHeight();
+        // Total displacment is the sum of the elements placement and size.
+        displacement = placement + size;
+        break;
+      case 'left':
+        size = $el.outerWidth();
+        // Total displacment is the sum of the elements placement and size.
+        displacement = placement + size;
+        break;
+      // Right and bottom elements displace according to their left and
+      // top offset. Their size isn't important.
+      case 'bottom':
+        docSize = document.documentElement.clientHeight;
+        displacement = docSize - placement;
+        break;
+      case 'right':
+        docSize = document.documentElement.clientWidth;
+        displacement = docSize - placement;
+        break;
+      default:
+        displacement = 0;
+    }
+    return displacement;
+  }
+
+  /**
+   * Assign the displace function to a property of the Drupal global object.
+   */
+  Drupal.displace = displace;
+
+}(window, Drupal, jQuery));
diff --git a/core/misc/tableheader.js b/core/misc/tableheader.js
index 0d4f7cd..633dc6e 100644
--- a/core/misc/tableheader.js
+++ b/core/misc/tableheader.js
@@ -1,4 +1,4 @@
-(function ($, Drupal) {
+(function ($, Drupal, window, document) {
 
 "use strict";
 
@@ -17,10 +17,13 @@ function scrollValue(position) {
 
 // Select and initilize sticky table headers.
 function tableHeaderInitHandler(e) {
+  TableHeader.displacements = window.parent.Drupal.displace.getOffsets(true);
   var $tables = $(e.data.context).find('table.sticky-enabled').once('tableheader');
   for (var i = 0, il = $tables.length; i < il; i++) {
     TableHeader.tables.push(new TableHeader($tables[i]));
   }
+  // Trigger a scroll event to prime sticky headers that may be applicable.
+  $(window).trigger('scroll.TableHeader');
 }
 
 // Helper method to loop through tables and execute a method.
@@ -39,16 +42,16 @@ function tableHeaderOnScrollHandler(e) {
   forTables('onScroll');
 }
 
-function tableHeaderOffsetChangeHandler(e) {
+function tableHeaderOffsetChangeHandler(e, offsets) {
   // Compute the new offset value.
-  TableHeader.computeOffsetTop();
-  forTables('stickyPosition', TableHeader.offsetTop);
+  TableHeader.displacements = offsets;
+  forTables('stickyPosition', offsets.top);
 }
 
 // Bind event that need to change all tables.
 $(window).on({
   /**
-   * When resizing table width and offset top can change, recalculate everything.
+   * When resizing table width can change, recalculate everything.
    */
   'resize.TableHeader': tableHeaderResizeHandler,
 
@@ -66,9 +69,9 @@ $(document).on({
   'columnschange.TableHeader': tableHeaderResizeHandler,
 
   /**
-   * Offset value vas changed by a third party script.
+   * Recalculate TableHeader.topOffset when viewport is resized
    */
-  'offsettopchange.TableHeader': tableHeaderOffsetChangeHandler
+  'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler
 });
 
 /**
@@ -77,9 +80,6 @@ $(document).on({
  * TableHeader will make the current table header stick to the top of the page
  * if the table is very long.
  *
- * Fire a custom "topoffsetchange" event to make TableHeader compute the
- * new offset value from the "data-offset-top" attributes of relevant elements.
- *
  * @param table
  *   DOM object for the table to add a sticky header to.
  *
@@ -122,24 +122,15 @@ $.extend(TableHeader, {
   tables: [],
 
   /**
-   * Cache of computed offset value.
+   * Cache of computed viewport displacement values.
    *
    * @type {Number}
    */
-  offsetTop: 0,
-
-  /**
-   * Sum all [data-offset-top] values and cache it.
-   */
-  computeOffsetTop: function () {
-    var $offsets = $('[data-offset-top]');
-    var value, sum = 0;
-    for (var i = 0, il = $offsets.length; i < il; i++) {
-      value = parseInt($offsets[i].getAttribute('data-offset-top'), 10);
-      sum += !isNaN(value) ? value : 0;
-    }
-    this.offsetTop = sum;
-    return sum;
+  displacements: {
+    top: 0,
+    right: 0,
+    bottom: 0,
+    left: 0
   }
 });
 
@@ -211,7 +202,7 @@ $.extend(TableHeader.prototype, {
    */
   checkStickyVisible: function () {
     var scrollTop = scrollValue('scrollTop');
-    var tableTop = this.tableOffset.top - TableHeader.offsetTop;
+    var tableTop = this.tableOffset.top - TableHeader.displacements.top;
     var tableBottom = tableTop + this.tableHeight;
     var visible = false;
 
@@ -230,7 +221,7 @@ $.extend(TableHeader.prototype, {
    *
    * @param event
    */
-  onScroll: function (e) {
+  onScroll: function (event) {
     this.checkStickyVisible();
     // Track horizontal positioning relative to the viewport.
     this.stickyPosition(null, scrollValue('scrollLeft'));
@@ -248,9 +239,8 @@ $.extend(TableHeader.prototype, {
     this.tableHeight = this.$originalTable[0].clientHeight;
 
     // Update offset top.
-    TableHeader.computeOffsetTop();
     this.tableOffset = this.$originalTable.offset();
-    this.stickyPosition(TableHeader.offsetTop);
+    this.stickyPosition(TableHeader.displacements.top, scrollValue('scrollLeft'));
 
     // Update columns width.
     var $that = null;
@@ -277,4 +267,4 @@ $.extend(TableHeader.prototype, {
 // Expose constructor in the public space.
 Drupal.TableHeader = TableHeader;
 
-}(jQuery, Drupal));
+}(jQuery, Drupal, window, window.document));
diff --git a/core/modules/overlay/overlay-child.css b/core/modules/overlay/overlay-child.css
index ecfa4cb..66f07bc 100644
--- a/core/modules/overlay/overlay-child.css
+++ b/core/modules/overlay/overlay-child.css
@@ -16,15 +16,17 @@
 }
 
 #overlay {
-  display: table;
+  box-sizing: border-box;
+  display: block;
   margin: 0 auto;
+  max-width: 80em;
+  max-width: 80rem;
   min-height: 100px;
-  min-width: 700px;
   position: relative;
-  padding: .2em;
   padding-bottom: 2em;
-  padding-right: 26px; /* LTR */
-  width: 88%;
+  padding-left: 40px;
+  padding-right: 40px;
+  width: 100%;
 }
 #overlay-titlebar {
   padding: 0 20px;
diff --git a/core/modules/overlay/overlay-child.js b/core/modules/overlay/overlay-child.js
index 80a8a2f..60964c8 100644
--- a/core/modules/overlay/overlay-child.js
+++ b/core/modules/overlay/overlay-child.js
@@ -3,7 +3,7 @@
  * Attaches the behaviors for the Overlay child pages.
  */
 
-(function ($) {
+(function ($, window, document) {
 
 "use strict";
 
@@ -179,11 +179,4 @@ Drupal.overlayChild.behaviors.shortcutAddLink = function (context, settings) {
   });
 };
 
-// Workaround because of the way jQuery events works.
-// jQuery from the parent frame needs to be used to catch this event.
-parent.jQuery(document).bind('offsettopchange', function () {
-  // Fires an event that the child iframe can listen to.
-  $(document).trigger('offsettopchange');
-});
-
-})(jQuery);
+})(jQuery, window, window.document);
diff --git a/core/modules/overlay/overlay-parent.js b/core/modules/overlay/overlay-parent.js
index caf9336..0c47f2c 100644
--- a/core/modules/overlay/overlay-parent.js
+++ b/core/modules/overlay/overlay-parent.js
@@ -106,6 +106,11 @@ Drupal.overlay.open = function (url) {
  * Create the underlying markup and behaviors for the overlay.
  */
 Drupal.overlay.create = function () {
+  // The overlay's positioning can be affected by other elements on the page.
+  // Get the current viewpor offset values.
+  this.viewportOffsets = Drupal.displace();
+
+  // Build the overlay container.
   this.$container = $(Drupal.theme('overlayContainer'))
     .appendTo(document.body);
 
@@ -131,6 +136,7 @@ Drupal.overlay.create = function () {
   $(window)
     .bind('resize' + eventClass, $.proxy(this, 'eventhandlerOuterResize'));
   $(document)
+    .on('drupalViewportOffsetChange' + eventClass, $.proxy(this, 'eventhandlerViewportOffsetChange'))
     .bind('drupalOverlayLoad' + eventClass, $.proxy(this, 'eventhandlerOuterResize'))
     .bind('drupalOverlayReady' + eventClass +
           ' drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerSyncURLFragment'))
@@ -139,11 +145,9 @@ Drupal.overlay.create = function () {
           ' drupalOverlayBeforeLoad' + eventClass +
           ' drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerDispatchEvent'));
 
-  if ($('.overlay-displace-top, .overlay-displace-bottom').length) {
-    $(document)
-      .bind('drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerAlterDisplacedElements'))
-      .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRestoreDisplacedElements'));
-  }
+  $(document)
+    .bind('drupalOverlayResize' + eventClass, $.proxy(this, 'eventhandlerAlterDisplacedElements'))
+    .bind('drupalOverlayClose' + eventClass, $.proxy(this, 'eventhandlerRestoreDisplacedElements'));
 };
 
 /**
@@ -398,6 +402,14 @@ Drupal.overlay.isExternalLink = function (url) {
 };
 
 /**
+ *
+ */
+Drupal.overlay.eventhandlerViewportOffsetChange = function (event, offsets) {
+  Drupal.overlay.viewportOffsets = offsets;
+  Drupal.overlay.eventhandlerOuterResize();
+}
+
+/**
  * Event handler: resizes overlay according to the size of the parent window.
  *
  * @param event
@@ -435,22 +447,28 @@ Drupal.overlay.eventhandlerAlterDisplacedElements = function (event) {
     return;
   }
 
+  var offsets = Drupal.overlay.viewportOffsets;
+
   $(this.iframeWindow.document.body).css({
-    marginTop: Drupal.overlay.getDisplacement('top'),
-    marginBottom: Drupal.overlay.getDisplacement('bottom')
-  }).attr('data-offset-top', Drupal.overlay.getDisplacement('top'));
-
-  $(document).bind('offsettopchange', function () {
-    var iframeDocument = Drupal.overlay.iframeWindow.document;
-    $(iframeDocument.body).attr('data-offset-top', Drupal.overlay.getDisplacement('top'));
-    $(iframeDocument).trigger('offsettopchange');
+    'margin-top': offsets.top,
+    'margin-right': offsets.right,
+    'margin-bottom': offsets.bottom,
+    'margin-left': offsets.left
   });
 
+  $(Drupal.overlay.iframeWindow.document).trigger('drupalViewportOffsetChange', Drupal.overlay.viewportOffsets);
+
   var documentHeight = this.iframeWindow.document.body.clientHeight;
   var documentWidth = this.iframeWindow.document.body.clientWidth;
   // IE6 doesn't support maxWidth, use width instead.
   var maxWidthName = 'maxWidth';
 
+  // If either the documentWidth or documentHeight are a zero dimension, abort
+  // the effort to alter presentation elements.
+  if (documentWidth === 0 || documentHeight === 0) {
+    return;
+  }
+
   if (Drupal.overlay.leftSidedScrollbarOffset === undefined && $(document.documentElement).attr('dir') === 'rtl') {
     // We can't use element.clientLeft to detect whether scrollbars are placed
     // on the left side of the element when direction is set to "rtl" as most
@@ -474,7 +492,7 @@ Drupal.overlay.eventhandlerAlterDisplacedElements = function (event) {
 
   // Consider any element that should be visible above the overlay (such as
   // a toolbar).
-  $('.overlay-displace-top, .overlay-displace-bottom').each(function () {
+  $('[data-offset-top]').add('[data-offset-bottom]').each(function () {
     var data = $(this).data();
     var maxWidth = documentWidth;
     // In IE, Shadow filter makes element to overlap the scrollbar with 1px.
diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module
index cc29255..197d006 100644
--- a/core/modules/overlay/overlay.module
+++ b/core/modules/overlay/overlay.module
@@ -225,6 +225,7 @@ function overlay_library_info() {
       array('system', 'jquery'),
       array('system', 'drupal'),
       array('system', 'drupalSettings'),
+      array('system', 'drupal.displace'),
       array('system', 'jquery.ui.core'),
       array('system', 'jquery.bbq'),
     ),
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 13469b0..2712bc7 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1397,6 +1397,21 @@ function system_library_info() {
     ),
   );
 
+  // A utility that measures elements like the toolbar that can potentially
+  // displace the positioning of display elements like the overlay.
+  $libraries['drupal.displace'] = array(
+    'title' => 'Drupal displace',
+    'version' => VERSION,
+    'js' => array(
+      'core/misc/displace.js' => array('group' => JS_LIBRARY),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'drupal.debounce'),
+    ),
+  );
+
   // A utility function to limit calls to a function with a given time.
   $libraries['drupal.debounce'] = array(
     'title' => 'Drupal debounce',
@@ -2093,6 +2108,7 @@ function system_library_info() {
       array('system', 'drupal'),
       array('system', 'drupalSettings'),
       array('system', 'jquery.once'),
+      array('system', 'drupal.displace'),
     ),
   );
   $libraries['drupal.timezone'] = array(
diff --git a/core/modules/toolbar/css/toolbar.base.css b/core/modules/toolbar/css/toolbar.base.css
index 58a2c2b..64e6468 100644
--- a/core/modules/toolbar/css/toolbar.base.css
+++ b/core/modules/toolbar/css/toolbar.base.css
@@ -91,7 +91,7 @@ html.js .toolbar {
   position: absolute;
 }
 .toolbar .tray {
-  z-index: 250;
+  z-index: 1200;
 }
 .toolbar .horizontal {
   width: 100%;
diff --git a/core/modules/toolbar/js/toolbar.js b/core/modules/toolbar/js/toolbar.js
index 45e91ad..ce9970c 100644
--- a/core/modules/toolbar/js/toolbar.js
+++ b/core/modules/toolbar/js/toolbar.js
@@ -158,9 +158,9 @@ Drupal.toolbar.toggleTray = function (event) {
       // Set aria-pressed to false.
       .attr('aria-pressed', 'false');
     $toolbar.find('.tray').not($activateTray).removeClass('active');
-    // Update the page and toolbar dimension indicators.
-    updatePeripherals();
   }
+  // Update the page and toolbar dimension indicators.
+  updatePeripherals();
 };
 
 /**
@@ -184,6 +184,9 @@ Drupal.toolbar.setHeight = function () {
   /**
    * Get the height of the active tray and include it in the total
    * height of the toolbar.
+   *
+   * If the height of the bar changed, Drupal.displace is called so that
+   * elements can adjust to the placement of the bar.
    */
   height += $trays.filter('.active.horizontal').outerHeight() || 0;
   // Indicate the height of the toolbar in the attribute data-offset-top.
@@ -192,12 +195,27 @@ Drupal.toolbar.setHeight = function () {
     $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');
+    // Trigger a recalculation of viewport displacing elements.
+    Drupal.displace();
   }
 };
 
 /**
+ * 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 () {
+  // Remove the left offset from the trays.
+  $toolbar.find('.tray').removeAttr('data-offset-left');
+  // If an active vertical tray exists, mark it as an offset element.
+  $toolbar.find('.tray.vertical.active').attr('data-offset-left', '');
+  // Trigger a recalculation of viewport displacing elements.
+  Drupal.displace();
+};
+
+/**
  * Respond to configured media query applicability changes.
  */
 Drupal.toolbar.mediaQueryChangeHandler = function (mql) {
@@ -296,6 +314,8 @@ function updatePeripherals () {
   setBodyState();
   // Adjust the height of the toolbar.
   Drupal.toolbar.setHeight();
+  // Adjust the tray width for vertical trays.
+  Drupal.toolbar.setTrayWidth();
 }
 
 /**
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index e4996bc..5cd1f5b 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -93,7 +93,7 @@ function toolbar_element_info() {
       'id' => 'toolbar-administration',
       // The 'overlay-displace-top' class pushes the overlay down, so it appears
       // below the toolbar.
-      'class' => array('toolbar', 'overlay-displace-top'),
+      'class' => array('toolbar',),
       'role' => 'navigation',
     ),
     // Metadata for the administration bar.
@@ -101,7 +101,7 @@ function toolbar_element_info() {
       '#heading' => t('Toolbar items'),
       '#attributes' => array(
         'id' => 'toolbar-bar',
-        'class' => array('bar', 'overlay-displace-top', 'clearfix'),
+        'class' => array('bar', 'clearfix'),
       ),
     ),
   );
@@ -315,7 +315,6 @@ function toolbar_pre_render_item($element) {
     }
     $element['tray']['#wrapper_attributes'] += $attributes;
     $element['tray']['#wrapper_attributes']['class'][] = 'tray';
-    $element['tray']['#wrapper_attributes']['class'][] = 'overlay-displace-top';
 
     if (!isset($element['tray']['#theme_wrappers'])) {
       $element['tray']['#theme_wrappers'] = array();
@@ -631,6 +630,7 @@ function toolbar_library_info() {
       array('system', 'matchmedia'),
       array('system', 'jquery.once'),
       array('system', 'drupal.debounce'),
+      array('system', 'drupal.displace'),
       array('toolbar', 'toolbar.menu'),
     ),
   );
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index 26534c7..27e2e0a 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -1622,7 +1622,7 @@ div.admin-panel .description {
 /**
  * Responsive tables.
  */
-@media screen and (max-width:28.125em) { /* 450px */
+@media screen and (max-width: 37.5em) { /* 600px */
   th.priority-low,
   td.priority-low,
   th.priority-medium,
@@ -1630,7 +1630,7 @@ div.admin-panel .description {
     display: none;
   }
 }
-@media screen and (max-width:45em) { /* 720px */
+@media screen and (max-width: 60em) { /* 920px */
   th.priority-low,
   td.priority-low {
     display: none;
diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css
index 72ce5e6..6440294 100644
--- a/core/themes/seven/style.css
+++ b/core/themes/seven/style.css
@@ -522,7 +522,7 @@ table.system-status-report tr.error {
 /**
  * Responsive tables.
  */
-@media screen and (max-width:28.125em) { /* 450px */
+@media screen and (max-width: 37.5em) { /* 600px */
   th.priority-low,
   td.priority-low,
   th.priority-medium,
@@ -530,7 +530,7 @@ table.system-status-report tr.error {
     display: none;
   }
 }
-@media screen and (max-width:45em) { /* 720px */
+@media screen and (max-width: 60em) { /* 920px */
   th.priority-low,
   td.priority-low {
     display: none;
-- 
1.7.10.4

