diff --git a/src/modal/index.js b/src/modal/index.js index 73d393b8a3..088be95dac 100644 --- a/src/modal/index.js +++ b/src/modal/index.js @@ -2,6 +2,7 @@ require('../stackedMap'); require('../../template/modal/backdrop.html.js'); require('../../template/modal/window.html.js'); require('./modal'); +require('../position/position.css'); var MODULE_NAME = 'ui.bootstrap.module.modal'; diff --git a/src/modal/modal.js b/src/modal/modal.js index a95621ff9e..e28d39187a 100644 --- a/src/modal/modal.js +++ b/src/modal/modal.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) +angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap', 'ui.bootstrap.position']) /** * A helper, internal data structure that stores all references attached to key */ @@ -247,8 +247,8 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) }) .factory('$uibModalStack', ['$animate', '$animateCss', '$document', - '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', - function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap) { + '$compile', '$rootScope', '$q', '$$multiMap', '$$stackedMap', '$uibPosition', + function($animate, $animateCss, $document, $compile, $rootScope, $q, $$multiMap, $$stackedMap, $uibPosition) { var OPENED_MODAL_CLASS = 'modal-open'; var backdropDomEl, backdropScope; @@ -262,6 +262,7 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) var tabableSelector = 'a[href], area[href], input:not([disabled]), ' + 'button:not([disabled]),select:not([disabled]), textarea:not([disabled]), ' + 'iframe, object, embed, *[tabindex], *[contenteditable=true]'; + var scrollbarPadding; function isVisible(element) { return !!(element.offsetWidth || @@ -297,6 +298,14 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) var modalBodyClass = modalWindow.openedClass || OPENED_MODAL_CLASS; openedClasses.remove(modalBodyClass, modalInstance); appendToElement.toggleClass(modalBodyClass, openedClasses.hasKey(modalBodyClass)); + if (scrollbarPadding && scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + if (scrollbarPadding.originalRight) { + appendToElement.css({paddingRight: scrollbarPadding.originalRight + 'px'}); + } else { + appendToElement.css({paddingRight: ''}); + } + scrollbarPadding = null; + } toggleTopWindowClass(true); }, modalWindow.closedDeferred); checkRemoveBackdrop(); @@ -472,12 +481,12 @@ angular.module('ui.bootstrap.modal', ['ui.bootstrap.stackedMap']) angularDomEl.attr('modal-animation', 'true'); } - $animate.enter($compile(angularDomEl)(modal.scope), appendToElement) - .then(function() { - if (!modal.scope.$$uibDestructionScheduled) { - $animate.addClass(appendToElement, modalBodyClass); - } - }); + scrollbarPadding = $uibPosition.scrollbarPadding(appendToElement); + if (scrollbarPadding.heightOverflow && scrollbarPadding.scrollbarWidth) { + appendToElement.css({paddingRight: scrollbarPadding.right + 'px'}); + } + appendToElement.addClass(modalBodyClass); + $animate.enter($compile(angularDomEl)(modal.scope), appendToElement); openedWindows.top().value.modalDomEl = angularDomEl; openedWindows.top().value.modalOpener = modalOpener; diff --git a/src/position/docs/readme.md b/src/position/docs/readme.md index 58f8d9dad1..1dad79aca0 100644 --- a/src/position/docs/readme.md +++ b/src/position/docs/readme.md @@ -45,15 +45,60 @@ Gets the closest positioned ancestor. * _(Type: `element`)_ - The closest positioned ancestor. -#### scrollbarWidth() +#### scrollbarWidth(isBody) Calculates the browser scrollbar width and caches the result for future calls. Concept from the TWBS measureScrollbar() function in [modal.js](https://github.com/twbs/bootstrap/blob/master/js/modal.js). +##### parameters + +* `isBody` + _(Type: `boolean`, Default: `false`, optional)_ - Is the requested scrollbar width for the body/html element. IE and Edge overlay the scrollbar on the body/html element and should be considered 0. + ##### returns * _(Type: `number`)_ - The width of the browser scrollbar. +#### scrollbarPadding(element) + +Calculates the padding required to replace the scrollbar on an element. + +##### parameters + +* 'element' _(Type: `element`)_ - The element to calculate the padding on (should be a scrollable element). + +##### returns + +An object with the following properties: + +* `scrollbarWidth` + _(Type: `number`)_ - + The width of the scrollbar. + +* `widthOverflow` + _(Type: `boolean`)_ - + Whether the width is overflowing. + +* `right` + _(Type: `number`)_ - + The total right padding required to replace the scrollbar. + +* `originalRight` + _(Type: `number`)_ - + The oringal right padding on the element. + +* `heightOverflow` + _(Type: `boolean`)_ - + Whether the height is overflowing. + +* `bottom` + _(Type: `number`)_ - + The total bottom padding required to replace the scrollbar. + +* `originalBottom` + _(Type: `number`)_ - + The oringal bottom padding on the element. + #### isScrollable(element, includeHidden) Determines if an element is scrollable. @@ -72,7 +117,7 @@ Determines if an element is scrollable. * _(Type: `boolean`)_ - Whether the element is scrollable. -#### scrollParent(element, includeHidden) +#### scrollParent(element, includeHidden, includeSelf) Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.js](https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js). @@ -85,6 +130,9 @@ Gets the closest scrollable ancestor. Concept from the jQueryUI [scrollParent.j * `includeHidden` _(Type: `boolean`, Default: `false`, optional)_ - Should scroll style of 'hidden' be considered. +* `includeSelf` + _(Type: `boolean`, Default: `false`, optional)_ - Should the element passed in be included in the scrollable lookup. + ##### returns * _(Type: `element`)_ - diff --git a/src/position/position.css b/src/position/position.css index caf5ea17a4..41fb4a2d87 100644 --- a/src/position/position.css +++ b/src/position/position.css @@ -7,9 +7,13 @@ } .uib-position-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; + position: absolute !important; + top: -9999px !important; + width: 50px !important; + height: 50px !important; + overflow: scroll !important; +} + +.uib-position-body-scrollbar-measure { + overflow: scroll !important; } \ No newline at end of file diff --git a/src/position/position.js b/src/position/position.js index 9f6c1f446e..341b0a8209 100644 --- a/src/position/position.js +++ b/src/position/position.js @@ -12,6 +12,11 @@ angular.module('ui.bootstrap.position', []) * Do not access this variable directly, use scrollbarWidth() instead. */ var SCROLLBAR_WIDTH; + /** + * scrollbar on body and html element in IE and Edge overlay + * content and should be considered 0 width. + */ + var BODY_SCROLLBAR_WIDTH; var OVERFLOW_REGEX = { normal: /(auto|scroll)/, hidden: /(auto|scroll|hidden)/ @@ -22,6 +27,7 @@ angular.module('ui.bootstrap.position', []) secondary: /^(top|bottom|left|right|center)$/, vertical: /^(top|bottom)$/ }; + var BODY_REGEX = /(HTML|BODY)/; return { @@ -75,10 +81,23 @@ angular.module('ui.bootstrap.position', []) /** * Provides the scrollbar width, concept from TWBS measureScrollbar() * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js + * In IE and Edge, scollbar on body and html element overlay and should + * return a width of 0. * * @returns {number} The width of the browser scollbar. */ - scrollbarWidth: function() { + scrollbarWidth: function(isBody) { + if (isBody) { + if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { + var bodyElem = $document.find('body'); + bodyElem.addClass('uib-position-body-scrollbar-measure'); + BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; + BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; + bodyElem.removeClass('uib-position-body-scrollbar-measure'); + } + return BODY_SCROLLBAR_WIDTH; + } + if (angular.isUndefined(SCROLLBAR_WIDTH)) { var scrollElem = angular.element('
'); $document.find('body').append(scrollElem); @@ -90,6 +109,40 @@ angular.module('ui.bootstrap.position', []) return SCROLLBAR_WIDTH; }, + /** + * Provides the padding required on an element to replace the scrollbar. + * + * @returns {object} An object with the following properties: + * + */ + scrollbarPadding: function(elem) { + elem = this.getRawNode(elem); + + var elemStyle = $window.getComputedStyle(elem); + var paddingRight = this.parseStyle(elemStyle.paddingRight); + var paddingBottom = this.parseStyle(elemStyle.paddingBottom); + var scrollParent = this.scrollParent(elem, false, true); + var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); + + return { + scrollbarWidth: scrollbarWidth, + widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, + right: paddingRight + scrollbarWidth, + originalRight: paddingRight, + heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, + bottom: paddingBottom + scrollbarWidth, + originalBottom: paddingBottom + }; + }, + /** * Checks to see if the element is scrollable. * @@ -115,15 +168,20 @@ angular.module('ui.bootstrap.position', []) * @param {element} elem - The element to find the scroll parent of. * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, * default is false. + * @param {boolean=} [includeSelf=false] - Should the element being passed be + * included in the scrollable llokup. * * @returns {element} A HTML element. */ - scrollParent: function(elem, includeHidden) { + scrollParent: function(elem, includeHidden, includeSelf) { elem = this.getRawNode(elem); var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; var documentEl = $document[0].documentElement; var elemStyle = $window.getComputedStyle(elem); + if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { + return elem; + } var excludeStatic = elemStyle.position === 'absolute'; var scrollParent = elem.parentElement || documentEl;