@@ 1952-2543 (lines=592) @@ | ||
1949 | * relation to another element (this is the case for tooltips, popovers, |
|
1950 | * typeahead suggestions etc.). |
|
1951 | */ |
|
1952 | .factory('$uibPosition', ['$document', '$window', function($document, $window) { |
|
1953 | /** |
|
1954 | * Used by scrollbarWidth() function to cache scrollbar's width. |
|
1955 | * Do not access this variable directly, use scrollbarWidth() instead. |
|
1956 | */ |
|
1957 | var SCROLLBAR_WIDTH; |
|
1958 | /** |
|
1959 | * scrollbar on body and html element in IE and Edge overlay |
|
1960 | * content and should be considered 0 width. |
|
1961 | */ |
|
1962 | var BODY_SCROLLBAR_WIDTH; |
|
1963 | var OVERFLOW_REGEX = { |
|
1964 | normal: /(auto|scroll)/, |
|
1965 | hidden: /(auto|scroll|hidden)/ |
|
1966 | }; |
|
1967 | var PLACEMENT_REGEX = { |
|
1968 | auto: /\s?auto?\s?/i, |
|
1969 | primary: /^(top|bottom|left|right)$/, |
|
1970 | secondary: /^(top|bottom|left|right|center)$/, |
|
1971 | vertical: /^(top|bottom)$/ |
|
1972 | }; |
|
1973 | var BODY_REGEX = /(HTML|BODY)/; |
|
1974 | ||
1975 | return { |
|
1976 | ||
1977 | /** |
|
1978 | * Provides a raw DOM element from a jQuery/jQLite element. |
|
1979 | * |
|
1980 | * @param {element} elem - The element to convert. |
|
1981 | * |
|
1982 | * @returns {element} A HTML element. |
|
1983 | */ |
|
1984 | getRawNode: function(elem) { |
|
1985 | return elem.nodeName ? elem : elem[0] || elem; |
|
1986 | }, |
|
1987 | ||
1988 | /** |
|
1989 | * Provides a parsed number for a style property. Strips |
|
1990 | * units and casts invalid numbers to 0. |
|
1991 | * |
|
1992 | * @param {string} value - The style value to parse. |
|
1993 | * |
|
1994 | * @returns {number} A valid number. |
|
1995 | */ |
|
1996 | parseStyle: function(value) { |
|
1997 | value = parseFloat(value); |
|
1998 | return isFinite(value) ? value : 0; |
|
1999 | }, |
|
2000 | ||
2001 | /** |
|
2002 | * Provides the closest positioned ancestor. |
|
2003 | * |
|
2004 | * @param {element} element - The element to get the offest parent for. |
|
2005 | * |
|
2006 | * @returns {element} The closest positioned ancestor. |
|
2007 | */ |
|
2008 | offsetParent: function(elem) { |
|
2009 | elem = this.getRawNode(elem); |
|
2010 | ||
2011 | var offsetParent = elem.offsetParent || $document[0].documentElement; |
|
2012 | ||
2013 | function isStaticPositioned(el) { |
|
2014 | return ($window.getComputedStyle(el).position || 'static') === 'static'; |
|
2015 | } |
|
2016 | ||
2017 | while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { |
|
2018 | offsetParent = offsetParent.offsetParent; |
|
2019 | } |
|
2020 | ||
2021 | return offsetParent || $document[0].documentElement; |
|
2022 | }, |
|
2023 | ||
2024 | /** |
|
2025 | * Provides the scrollbar width, concept from TWBS measureScrollbar() |
|
2026 | * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js |
|
2027 | * In IE and Edge, scollbar on body and html element overlay and should |
|
2028 | * return a width of 0. |
|
2029 | * |
|
2030 | * @returns {number} The width of the browser scollbar. |
|
2031 | */ |
|
2032 | scrollbarWidth: function(isBody) { |
|
2033 | if (isBody) { |
|
2034 | if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { |
|
2035 | var bodyElem = $document.find('body'); |
|
2036 | bodyElem.addClass('uib-position-body-scrollbar-measure'); |
|
2037 | BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; |
|
2038 | BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; |
|
2039 | bodyElem.removeClass('uib-position-body-scrollbar-measure'); |
|
2040 | } |
|
2041 | return BODY_SCROLLBAR_WIDTH; |
|
2042 | } |
|
2043 | ||
2044 | if (angular.isUndefined(SCROLLBAR_WIDTH)) { |
|
2045 | var scrollElem = angular.element('<div class="uib-position-scrollbar-measure"></div>'); |
|
2046 | $document.find('body').append(scrollElem); |
|
2047 | SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; |
|
2048 | SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; |
|
2049 | scrollElem.remove(); |
|
2050 | } |
|
2051 | ||
2052 | return SCROLLBAR_WIDTH; |
|
2053 | }, |
|
2054 | ||
2055 | /** |
|
2056 | * Provides the padding required on an element to replace the scrollbar. |
|
2057 | * |
|
2058 | * @returns {object} An object with the following properties: |
|
2059 | * <ul> |
|
2060 | * <li>**scrollbarWidth**: the width of the scrollbar</li> |
|
2061 | * <li>**widthOverflow**: whether the the width is overflowing</li> |
|
2062 | * <li>**right**: the amount of right padding on the element needed to replace the scrollbar</li> |
|
2063 | * <li>**rightOriginal**: the amount of right padding currently on the element</li> |
|
2064 | * <li>**heightOverflow**: whether the the height is overflowing</li> |
|
2065 | * <li>**bottom**: the amount of bottom padding on the element needed to replace the scrollbar</li> |
|
2066 | * <li>**bottomOriginal**: the amount of bottom padding currently on the element</li> |
|
2067 | * </ul> |
|
2068 | */ |
|
2069 | scrollbarPadding: function(elem) { |
|
2070 | elem = this.getRawNode(elem); |
|
2071 | ||
2072 | var elemStyle = $window.getComputedStyle(elem); |
|
2073 | var paddingRight = this.parseStyle(elemStyle.paddingRight); |
|
2074 | var paddingBottom = this.parseStyle(elemStyle.paddingBottom); |
|
2075 | var scrollParent = this.scrollParent(elem, false, true); |
|
2076 | var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); |
|
2077 | ||
2078 | return { |
|
2079 | scrollbarWidth: scrollbarWidth, |
|
2080 | widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, |
|
2081 | right: paddingRight + scrollbarWidth, |
|
2082 | originalRight: paddingRight, |
|
2083 | heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, |
|
2084 | bottom: paddingBottom + scrollbarWidth, |
|
2085 | originalBottom: paddingBottom |
|
2086 | }; |
|
2087 | }, |
|
2088 | ||
2089 | /** |
|
2090 | * Checks to see if the element is scrollable. |
|
2091 | * |
|
2092 | * @param {element} elem - The element to check. |
|
2093 | * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, |
|
2094 | * default is false. |
|
2095 | * |
|
2096 | * @returns {boolean} Whether the element is scrollable. |
|
2097 | */ |
|
2098 | isScrollable: function(elem, includeHidden) { |
|
2099 | elem = this.getRawNode(elem); |
|
2100 | ||
2101 | var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; |
|
2102 | var elemStyle = $window.getComputedStyle(elem); |
|
2103 | return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX); |
|
2104 | }, |
|
2105 | ||
2106 | /** |
|
2107 | * Provides the closest scrollable ancestor. |
|
2108 | * A port of the jQuery UI scrollParent method: |
|
2109 | * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js |
|
2110 | * |
|
2111 | * @param {element} elem - The element to find the scroll parent of. |
|
2112 | * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, |
|
2113 | * default is false. |
|
2114 | * @param {boolean=} [includeSelf=false] - Should the element being passed be |
|
2115 | * included in the scrollable llokup. |
|
2116 | * |
|
2117 | * @returns {element} A HTML element. |
|
2118 | */ |
|
2119 | scrollParent: function(elem, includeHidden, includeSelf) { |
|
2120 | elem = this.getRawNode(elem); |
|
2121 | ||
2122 | var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; |
|
2123 | var documentEl = $document[0].documentElement; |
|
2124 | var elemStyle = $window.getComputedStyle(elem); |
|
2125 | if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { |
|
2126 | return elem; |
|
2127 | } |
|
2128 | var excludeStatic = elemStyle.position === 'absolute'; |
|
2129 | var scrollParent = elem.parentElement || documentEl; |
|
2130 | ||
2131 | if (scrollParent === documentEl || elemStyle.position === 'fixed') { |
|
2132 | return documentEl; |
|
2133 | } |
|
2134 | ||
2135 | while (scrollParent.parentElement && scrollParent !== documentEl) { |
|
2136 | var spStyle = $window.getComputedStyle(scrollParent); |
|
2137 | if (excludeStatic && spStyle.position !== 'static') { |
|
2138 | excludeStatic = false; |
|
2139 | } |
|
2140 | ||
2141 | if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { |
|
2142 | break; |
|
2143 | } |
|
2144 | scrollParent = scrollParent.parentElement; |
|
2145 | } |
|
2146 | ||
2147 | return scrollParent; |
|
2148 | }, |
|
2149 | ||
2150 | /** |
|
2151 | * Provides read-only equivalent of jQuery's position function: |
|
2152 | * http://api.jquery.com/position/ - distance to closest positioned |
|
2153 | * ancestor. Does not account for margins by default like jQuery position. |
|
2154 | * |
|
2155 | * @param {element} elem - The element to caclulate the position on. |
|
2156 | * @param {boolean=} [includeMargins=false] - Should margins be accounted |
|
2157 | * for, default is false. |
|
2158 | * |
|
2159 | * @returns {object} An object with the following properties: |
|
2160 | * <ul> |
|
2161 | * <li>**width**: the width of the element</li> |
|
2162 | * <li>**height**: the height of the element</li> |
|
2163 | * <li>**top**: distance to top edge of offset parent</li> |
|
2164 | * <li>**left**: distance to left edge of offset parent</li> |
|
2165 | * </ul> |
|
2166 | */ |
|
2167 | position: function(elem, includeMagins) { |
|
2168 | elem = this.getRawNode(elem); |
|
2169 | ||
2170 | var elemOffset = this.offset(elem); |
|
2171 | if (includeMagins) { |
|
2172 | var elemStyle = $window.getComputedStyle(elem); |
|
2173 | elemOffset.top -= this.parseStyle(elemStyle.marginTop); |
|
2174 | elemOffset.left -= this.parseStyle(elemStyle.marginLeft); |
|
2175 | } |
|
2176 | var parent = this.offsetParent(elem); |
|
2177 | var parentOffset = {top: 0, left: 0}; |
|
2178 | ||
2179 | if (parent !== $document[0].documentElement) { |
|
2180 | parentOffset = this.offset(parent); |
|
2181 | parentOffset.top += parent.clientTop - parent.scrollTop; |
|
2182 | parentOffset.left += parent.clientLeft - parent.scrollLeft; |
|
2183 | } |
|
2184 | ||
2185 | return { |
|
2186 | width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), |
|
2187 | height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), |
|
2188 | top: Math.round(elemOffset.top - parentOffset.top), |
|
2189 | left: Math.round(elemOffset.left - parentOffset.left) |
|
2190 | }; |
|
2191 | }, |
|
2192 | ||
2193 | /** |
|
2194 | * Provides read-only equivalent of jQuery's offset function: |
|
2195 | * http://api.jquery.com/offset/ - distance to viewport. Does |
|
2196 | * not account for borders, margins, or padding on the body |
|
2197 | * element. |
|
2198 | * |
|
2199 | * @param {element} elem - The element to calculate the offset on. |
|
2200 | * |
|
2201 | * @returns {object} An object with the following properties: |
|
2202 | * <ul> |
|
2203 | * <li>**width**: the width of the element</li> |
|
2204 | * <li>**height**: the height of the element</li> |
|
2205 | * <li>**top**: distance to top edge of viewport</li> |
|
2206 | * <li>**right**: distance to bottom edge of viewport</li> |
|
2207 | * </ul> |
|
2208 | */ |
|
2209 | offset: function(elem) { |
|
2210 | elem = this.getRawNode(elem); |
|
2211 | ||
2212 | var elemBCR = elem.getBoundingClientRect(); |
|
2213 | return { |
|
2214 | width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), |
|
2215 | height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), |
|
2216 | top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), |
|
2217 | left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) |
|
2218 | }; |
|
2219 | }, |
|
2220 | ||
2221 | /** |
|
2222 | * Provides offset distance to the closest scrollable ancestor |
|
2223 | * or viewport. Accounts for border and scrollbar width. |
|
2224 | * |
|
2225 | * Right and bottom dimensions represent the distance to the |
|
2226 | * respective edge of the viewport element. If the element |
|
2227 | * edge extends beyond the viewport, a negative value will be |
|
2228 | * reported. |
|
2229 | * |
|
2230 | * @param {element} elem - The element to get the viewport offset for. |
|
2231 | * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead |
|
2232 | * of the first scrollable element, default is false. |
|
2233 | * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element |
|
2234 | * be accounted for, default is true. |
|
2235 | * |
|
2236 | * @returns {object} An object with the following properties: |
|
2237 | * <ul> |
|
2238 | * <li>**top**: distance to the top content edge of viewport element</li> |
|
2239 | * <li>**bottom**: distance to the bottom content edge of viewport element</li> |
|
2240 | * <li>**left**: distance to the left content edge of viewport element</li> |
|
2241 | * <li>**right**: distance to the right content edge of viewport element</li> |
|
2242 | * </ul> |
|
2243 | */ |
|
2244 | viewportOffset: function(elem, useDocument, includePadding) { |
|
2245 | elem = this.getRawNode(elem); |
|
2246 | includePadding = includePadding !== false ? true : false; |
|
2247 | ||
2248 | var elemBCR = elem.getBoundingClientRect(); |
|
2249 | var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; |
|
2250 | ||
2251 | var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); |
|
2252 | var offsetParentBCR = offsetParent.getBoundingClientRect(); |
|
2253 | ||
2254 | offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; |
|
2255 | offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; |
|
2256 | if (offsetParent === $document[0].documentElement) { |
|
2257 | offsetBCR.top += $window.pageYOffset; |
|
2258 | offsetBCR.left += $window.pageXOffset; |
|
2259 | } |
|
2260 | offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; |
|
2261 | offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; |
|
2262 | ||
2263 | if (includePadding) { |
|
2264 | var offsetParentStyle = $window.getComputedStyle(offsetParent); |
|
2265 | offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); |
|
2266 | offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); |
|
2267 | offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); |
|
2268 | offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); |
|
2269 | } |
|
2270 | ||
2271 | return { |
|
2272 | top: Math.round(elemBCR.top - offsetBCR.top), |
|
2273 | bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), |
|
2274 | left: Math.round(elemBCR.left - offsetBCR.left), |
|
2275 | right: Math.round(offsetBCR.right - elemBCR.right) |
|
2276 | }; |
|
2277 | }, |
|
2278 | ||
2279 | /** |
|
2280 | * Provides an array of placement values parsed from a placement string. |
|
2281 | * Along with the 'auto' indicator, supported placement strings are: |
|
2282 | * <ul> |
|
2283 | * <li>top: element on top, horizontally centered on host element.</li> |
|
2284 | * <li>top-left: element on top, left edge aligned with host element left edge.</li> |
|
2285 | * <li>top-right: element on top, lerightft edge aligned with host element right edge.</li> |
|
2286 | * <li>bottom: element on bottom, horizontally centered on host element.</li> |
|
2287 | * <li>bottom-left: element on bottom, left edge aligned with host element left edge.</li> |
|
2288 | * <li>bottom-right: element on bottom, right edge aligned with host element right edge.</li> |
|
2289 | * <li>left: element on left, vertically centered on host element.</li> |
|
2290 | * <li>left-top: element on left, top edge aligned with host element top edge.</li> |
|
2291 | * <li>left-bottom: element on left, bottom edge aligned with host element bottom edge.</li> |
|
2292 | * <li>right: element on right, vertically centered on host element.</li> |
|
2293 | * <li>right-top: element on right, top edge aligned with host element top edge.</li> |
|
2294 | * <li>right-bottom: element on right, bottom edge aligned with host element bottom edge.</li> |
|
2295 | * </ul> |
|
2296 | * A placement string with an 'auto' indicator is expected to be |
|
2297 | * space separated from the placement, i.e: 'auto bottom-left' If |
|
2298 | * the primary and secondary placement values do not match 'top, |
|
2299 | * bottom, left, right' then 'top' will be the primary placement and |
|
2300 | * 'center' will be the secondary placement. If 'auto' is passed, true |
|
2301 | * will be returned as the 3rd value of the array. |
|
2302 | * |
|
2303 | * @param {string} placement - The placement string to parse. |
|
2304 | * |
|
2305 | * @returns {array} An array with the following values |
|
2306 | * <ul> |
|
2307 | * <li>**[0]**: The primary placement.</li> |
|
2308 | * <li>**[1]**: The secondary placement.</li> |
|
2309 | * <li>**[2]**: If auto is passed: true, else undefined.</li> |
|
2310 | * </ul> |
|
2311 | */ |
|
2312 | parsePlacement: function(placement) { |
|
2313 | var autoPlace = PLACEMENT_REGEX.auto.test(placement); |
|
2314 | if (autoPlace) { |
|
2315 | placement = placement.replace(PLACEMENT_REGEX.auto, ''); |
|
2316 | } |
|
2317 | ||
2318 | placement = placement.split('-'); |
|
2319 | ||
2320 | placement[0] = placement[0] || 'top'; |
|
2321 | if (!PLACEMENT_REGEX.primary.test(placement[0])) { |
|
2322 | placement[0] = 'top'; |
|
2323 | } |
|
2324 | ||
2325 | placement[1] = placement[1] || 'center'; |
|
2326 | if (!PLACEMENT_REGEX.secondary.test(placement[1])) { |
|
2327 | placement[1] = 'center'; |
|
2328 | } |
|
2329 | ||
2330 | if (autoPlace) { |
|
2331 | placement[2] = true; |
|
2332 | } else { |
|
2333 | placement[2] = false; |
|
2334 | } |
|
2335 | ||
2336 | return placement; |
|
2337 | }, |
|
2338 | ||
2339 | /** |
|
2340 | * Provides coordinates for an element to be positioned relative to |
|
2341 | * another element. Passing 'auto' as part of the placement parameter |
|
2342 | * will enable smart placement - where the element fits. i.e: |
|
2343 | * 'auto left-top' will check to see if there is enough space to the left |
|
2344 | * of the hostElem to fit the targetElem, if not place right (same for secondary |
|
2345 | * top placement). Available space is calculated using the viewportOffset |
|
2346 | * function. |
|
2347 | * |
|
2348 | * @param {element} hostElem - The element to position against. |
|
2349 | * @param {element} targetElem - The element to position. |
|
2350 | * @param {string=} [placement=top] - The placement for the targetElem, |
|
2351 | * default is 'top'. 'center' is assumed as secondary placement for |
|
2352 | * 'top', 'left', 'right', and 'bottom' placements. Available placements are: |
|
2353 | * <ul> |
|
2354 | * <li>top</li> |
|
2355 | * <li>top-right</li> |
|
2356 | * <li>top-left</li> |
|
2357 | * <li>bottom</li> |
|
2358 | * <li>bottom-left</li> |
|
2359 | * <li>bottom-right</li> |
|
2360 | * <li>left</li> |
|
2361 | * <li>left-top</li> |
|
2362 | * <li>left-bottom</li> |
|
2363 | * <li>right</li> |
|
2364 | * <li>right-top</li> |
|
2365 | * <li>right-bottom</li> |
|
2366 | * </ul> |
|
2367 | * @param {boolean=} [appendToBody=false] - Should the top and left values returned |
|
2368 | * be calculated from the body element, default is false. |
|
2369 | * |
|
2370 | * @returns {object} An object with the following properties: |
|
2371 | * <ul> |
|
2372 | * <li>**top**: Value for targetElem top.</li> |
|
2373 | * <li>**left**: Value for targetElem left.</li> |
|
2374 | * <li>**placement**: The resolved placement.</li> |
|
2375 | * </ul> |
|
2376 | */ |
|
2377 | positionElements: function(hostElem, targetElem, placement, appendToBody) { |
|
2378 | hostElem = this.getRawNode(hostElem); |
|
2379 | targetElem = this.getRawNode(targetElem); |
|
2380 | ||
2381 | // need to read from prop to support tests. |
|
2382 | var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); |
|
2383 | var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); |
|
2384 | ||
2385 | placement = this.parsePlacement(placement); |
|
2386 | ||
2387 | var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); |
|
2388 | var targetElemPos = {top: 0, left: 0, placement: ''}; |
|
2389 | ||
2390 | if (placement[2]) { |
|
2391 | var viewportOffset = this.viewportOffset(hostElem, appendToBody); |
|
2392 | ||
2393 | var targetElemStyle = $window.getComputedStyle(targetElem); |
|
2394 | var adjustedSize = { |
|
2395 | width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), |
|
2396 | height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) |
|
2397 | }; |
|
2398 | ||
2399 | placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : |
|
2400 | placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : |
|
2401 | placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : |
|
2402 | placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : |
|
2403 | placement[0]; |
|
2404 | ||
2405 | placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : |
|
2406 | placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : |
|
2407 | placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : |
|
2408 | placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : |
|
2409 | placement[1]; |
|
2410 | ||
2411 | if (placement[1] === 'center') { |
|
2412 | if (PLACEMENT_REGEX.vertical.test(placement[0])) { |
|
2413 | var xOverflow = hostElemPos.width / 2 - targetWidth / 2; |
|
2414 | if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { |
|
2415 | placement[1] = 'left'; |
|
2416 | } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { |
|
2417 | placement[1] = 'right'; |
|
2418 | } |
|
2419 | } else { |
|
2420 | var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; |
|
2421 | if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { |
|
2422 | placement[1] = 'top'; |
|
2423 | } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { |
|
2424 | placement[1] = 'bottom'; |
|
2425 | } |
|
2426 | } |
|
2427 | } |
|
2428 | } |
|
2429 | ||
2430 | switch (placement[0]) { |
|
2431 | case 'top': |
|
2432 | targetElemPos.top = hostElemPos.top - targetHeight; |
|
2433 | break; |
|
2434 | case 'bottom': |
|
2435 | targetElemPos.top = hostElemPos.top + hostElemPos.height; |
|
2436 | break; |
|
2437 | case 'left': |
|
2438 | targetElemPos.left = hostElemPos.left - targetWidth; |
|
2439 | break; |
|
2440 | case 'right': |
|
2441 | targetElemPos.left = hostElemPos.left + hostElemPos.width; |
|
2442 | break; |
|
2443 | } |
|
2444 | ||
2445 | switch (placement[1]) { |
|
2446 | case 'top': |
|
2447 | targetElemPos.top = hostElemPos.top; |
|
2448 | break; |
|
2449 | case 'bottom': |
|
2450 | targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; |
|
2451 | break; |
|
2452 | case 'left': |
|
2453 | targetElemPos.left = hostElemPos.left; |
|
2454 | break; |
|
2455 | case 'right': |
|
2456 | targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; |
|
2457 | break; |
|
2458 | case 'center': |
|
2459 | if (PLACEMENT_REGEX.vertical.test(placement[0])) { |
|
2460 | targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; |
|
2461 | } else { |
|
2462 | targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; |
|
2463 | } |
|
2464 | break; |
|
2465 | } |
|
2466 | ||
2467 | targetElemPos.top = Math.round(targetElemPos.top); |
|
2468 | targetElemPos.left = Math.round(targetElemPos.left); |
|
2469 | targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; |
|
2470 | ||
2471 | return targetElemPos; |
|
2472 | }, |
|
2473 | ||
2474 | /** |
|
2475 | * Provides a way for positioning tooltip & dropdown |
|
2476 | * arrows when using placement options beyond the standard |
|
2477 | * left, right, top, or bottom. |
|
2478 | * |
|
2479 | * @param {element} elem - The tooltip/dropdown element. |
|
2480 | * @param {string} placement - The placement for the elem. |
|
2481 | */ |
|
2482 | positionArrow: function(elem, placement) { |
|
2483 | elem = this.getRawNode(elem); |
|
2484 | ||
2485 | var innerElem = elem.querySelector('.tooltip-inner, .popover-inner'); |
|
2486 | if (!innerElem) { |
|
2487 | return; |
|
2488 | } |
|
2489 | ||
2490 | var isTooltip = angular.element(innerElem).hasClass('tooltip-inner'); |
|
2491 | ||
2492 | var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); |
|
2493 | if (!arrowElem) { |
|
2494 | return; |
|
2495 | } |
|
2496 | ||
2497 | var arrowCss = { |
|
2498 | top: '', |
|
2499 | bottom: '', |
|
2500 | left: '', |
|
2501 | right: '' |
|
2502 | }; |
|
2503 | ||
2504 | placement = this.parsePlacement(placement); |
|
2505 | if (placement[1] === 'center') { |
|
2506 | // no adjustment necessary - just reset styles |
|
2507 | angular.element(arrowElem).css(arrowCss); |
|
2508 | return; |
|
2509 | } |
|
2510 | ||
2511 | var borderProp = 'border-' + placement[0] + '-width'; |
|
2512 | var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; |
|
2513 | ||
2514 | var borderRadiusProp = 'border-'; |
|
2515 | if (PLACEMENT_REGEX.vertical.test(placement[0])) { |
|
2516 | borderRadiusProp += placement[0] + '-' + placement[1]; |
|
2517 | } else { |
|
2518 | borderRadiusProp += placement[1] + '-' + placement[0]; |
|
2519 | } |
|
2520 | borderRadiusProp += '-radius'; |
|
2521 | var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; |
|
2522 | ||
2523 | switch (placement[0]) { |
|
2524 | case 'top': |
|
2525 | arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; |
|
2526 | break; |
|
2527 | case 'bottom': |
|
2528 | arrowCss.top = isTooltip ? '0' : '-' + borderWidth; |
|
2529 | break; |
|
2530 | case 'left': |
|
2531 | arrowCss.right = isTooltip ? '0' : '-' + borderWidth; |
|
2532 | break; |
|
2533 | case 'right': |
|
2534 | arrowCss.left = isTooltip ? '0' : '-' + borderWidth; |
|
2535 | break; |
|
2536 | } |
|
2537 | ||
2538 | arrowCss[placement[1]] = borderRadius; |
|
2539 | ||
2540 | angular.element(arrowElem).css(arrowCss); |
|
2541 | } |
|
2542 | }; |
|
2543 | }]); |
|
2544 | ||
2545 | angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) |
|
2546 |
@@ 1951-2542 (lines=592) @@ | ||
1948 | * relation to another element (this is the case for tooltips, popovers, |
|
1949 | * typeahead suggestions etc.). |
|
1950 | */ |
|
1951 | .factory('$uibPosition', ['$document', '$window', function($document, $window) { |
|
1952 | /** |
|
1953 | * Used by scrollbarWidth() function to cache scrollbar's width. |
|
1954 | * Do not access this variable directly, use scrollbarWidth() instead. |
|
1955 | */ |
|
1956 | var SCROLLBAR_WIDTH; |
|
1957 | /** |
|
1958 | * scrollbar on body and html element in IE and Edge overlay |
|
1959 | * content and should be considered 0 width. |
|
1960 | */ |
|
1961 | var BODY_SCROLLBAR_WIDTH; |
|
1962 | var OVERFLOW_REGEX = { |
|
1963 | normal: /(auto|scroll)/, |
|
1964 | hidden: /(auto|scroll|hidden)/ |
|
1965 | }; |
|
1966 | var PLACEMENT_REGEX = { |
|
1967 | auto: /\s?auto?\s?/i, |
|
1968 | primary: /^(top|bottom|left|right)$/, |
|
1969 | secondary: /^(top|bottom|left|right|center)$/, |
|
1970 | vertical: /^(top|bottom)$/ |
|
1971 | }; |
|
1972 | var BODY_REGEX = /(HTML|BODY)/; |
|
1973 | ||
1974 | return { |
|
1975 | ||
1976 | /** |
|
1977 | * Provides a raw DOM element from a jQuery/jQLite element. |
|
1978 | * |
|
1979 | * @param {element} elem - The element to convert. |
|
1980 | * |
|
1981 | * @returns {element} A HTML element. |
|
1982 | */ |
|
1983 | getRawNode: function(elem) { |
|
1984 | return elem.nodeName ? elem : elem[0] || elem; |
|
1985 | }, |
|
1986 | ||
1987 | /** |
|
1988 | * Provides a parsed number for a style property. Strips |
|
1989 | * units and casts invalid numbers to 0. |
|
1990 | * |
|
1991 | * @param {string} value - The style value to parse. |
|
1992 | * |
|
1993 | * @returns {number} A valid number. |
|
1994 | */ |
|
1995 | parseStyle: function(value) { |
|
1996 | value = parseFloat(value); |
|
1997 | return isFinite(value) ? value : 0; |
|
1998 | }, |
|
1999 | ||
2000 | /** |
|
2001 | * Provides the closest positioned ancestor. |
|
2002 | * |
|
2003 | * @param {element} element - The element to get the offest parent for. |
|
2004 | * |
|
2005 | * @returns {element} The closest positioned ancestor. |
|
2006 | */ |
|
2007 | offsetParent: function(elem) { |
|
2008 | elem = this.getRawNode(elem); |
|
2009 | ||
2010 | var offsetParent = elem.offsetParent || $document[0].documentElement; |
|
2011 | ||
2012 | function isStaticPositioned(el) { |
|
2013 | return ($window.getComputedStyle(el).position || 'static') === 'static'; |
|
2014 | } |
|
2015 | ||
2016 | while (offsetParent && offsetParent !== $document[0].documentElement && isStaticPositioned(offsetParent)) { |
|
2017 | offsetParent = offsetParent.offsetParent; |
|
2018 | } |
|
2019 | ||
2020 | return offsetParent || $document[0].documentElement; |
|
2021 | }, |
|
2022 | ||
2023 | /** |
|
2024 | * Provides the scrollbar width, concept from TWBS measureScrollbar() |
|
2025 | * function in https://github.com/twbs/bootstrap/blob/master/js/modal.js |
|
2026 | * In IE and Edge, scollbar on body and html element overlay and should |
|
2027 | * return a width of 0. |
|
2028 | * |
|
2029 | * @returns {number} The width of the browser scollbar. |
|
2030 | */ |
|
2031 | scrollbarWidth: function(isBody) { |
|
2032 | if (isBody) { |
|
2033 | if (angular.isUndefined(BODY_SCROLLBAR_WIDTH)) { |
|
2034 | var bodyElem = $document.find('body'); |
|
2035 | bodyElem.addClass('uib-position-body-scrollbar-measure'); |
|
2036 | BODY_SCROLLBAR_WIDTH = $window.innerWidth - bodyElem[0].clientWidth; |
|
2037 | BODY_SCROLLBAR_WIDTH = isFinite(BODY_SCROLLBAR_WIDTH) ? BODY_SCROLLBAR_WIDTH : 0; |
|
2038 | bodyElem.removeClass('uib-position-body-scrollbar-measure'); |
|
2039 | } |
|
2040 | return BODY_SCROLLBAR_WIDTH; |
|
2041 | } |
|
2042 | ||
2043 | if (angular.isUndefined(SCROLLBAR_WIDTH)) { |
|
2044 | var scrollElem = angular.element('<div class="uib-position-scrollbar-measure"></div>'); |
|
2045 | $document.find('body').append(scrollElem); |
|
2046 | SCROLLBAR_WIDTH = scrollElem[0].offsetWidth - scrollElem[0].clientWidth; |
|
2047 | SCROLLBAR_WIDTH = isFinite(SCROLLBAR_WIDTH) ? SCROLLBAR_WIDTH : 0; |
|
2048 | scrollElem.remove(); |
|
2049 | } |
|
2050 | ||
2051 | return SCROLLBAR_WIDTH; |
|
2052 | }, |
|
2053 | ||
2054 | /** |
|
2055 | * Provides the padding required on an element to replace the scrollbar. |
|
2056 | * |
|
2057 | * @returns {object} An object with the following properties: |
|
2058 | * <ul> |
|
2059 | * <li>**scrollbarWidth**: the width of the scrollbar</li> |
|
2060 | * <li>**widthOverflow**: whether the the width is overflowing</li> |
|
2061 | * <li>**right**: the amount of right padding on the element needed to replace the scrollbar</li> |
|
2062 | * <li>**rightOriginal**: the amount of right padding currently on the element</li> |
|
2063 | * <li>**heightOverflow**: whether the the height is overflowing</li> |
|
2064 | * <li>**bottom**: the amount of bottom padding on the element needed to replace the scrollbar</li> |
|
2065 | * <li>**bottomOriginal**: the amount of bottom padding currently on the element</li> |
|
2066 | * </ul> |
|
2067 | */ |
|
2068 | scrollbarPadding: function(elem) { |
|
2069 | elem = this.getRawNode(elem); |
|
2070 | ||
2071 | var elemStyle = $window.getComputedStyle(elem); |
|
2072 | var paddingRight = this.parseStyle(elemStyle.paddingRight); |
|
2073 | var paddingBottom = this.parseStyle(elemStyle.paddingBottom); |
|
2074 | var scrollParent = this.scrollParent(elem, false, true); |
|
2075 | var scrollbarWidth = this.scrollbarWidth(scrollParent, BODY_REGEX.test(scrollParent.tagName)); |
|
2076 | ||
2077 | return { |
|
2078 | scrollbarWidth: scrollbarWidth, |
|
2079 | widthOverflow: scrollParent.scrollWidth > scrollParent.clientWidth, |
|
2080 | right: paddingRight + scrollbarWidth, |
|
2081 | originalRight: paddingRight, |
|
2082 | heightOverflow: scrollParent.scrollHeight > scrollParent.clientHeight, |
|
2083 | bottom: paddingBottom + scrollbarWidth, |
|
2084 | originalBottom: paddingBottom |
|
2085 | }; |
|
2086 | }, |
|
2087 | ||
2088 | /** |
|
2089 | * Checks to see if the element is scrollable. |
|
2090 | * |
|
2091 | * @param {element} elem - The element to check. |
|
2092 | * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, |
|
2093 | * default is false. |
|
2094 | * |
|
2095 | * @returns {boolean} Whether the element is scrollable. |
|
2096 | */ |
|
2097 | isScrollable: function(elem, includeHidden) { |
|
2098 | elem = this.getRawNode(elem); |
|
2099 | ||
2100 | var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; |
|
2101 | var elemStyle = $window.getComputedStyle(elem); |
|
2102 | return overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX); |
|
2103 | }, |
|
2104 | ||
2105 | /** |
|
2106 | * Provides the closest scrollable ancestor. |
|
2107 | * A port of the jQuery UI scrollParent method: |
|
2108 | * https://github.com/jquery/jquery-ui/blob/master/ui/scroll-parent.js |
|
2109 | * |
|
2110 | * @param {element} elem - The element to find the scroll parent of. |
|
2111 | * @param {boolean=} [includeHidden=false] - Should scroll style of 'hidden' be considered, |
|
2112 | * default is false. |
|
2113 | * @param {boolean=} [includeSelf=false] - Should the element being passed be |
|
2114 | * included in the scrollable llokup. |
|
2115 | * |
|
2116 | * @returns {element} A HTML element. |
|
2117 | */ |
|
2118 | scrollParent: function(elem, includeHidden, includeSelf) { |
|
2119 | elem = this.getRawNode(elem); |
|
2120 | ||
2121 | var overflowRegex = includeHidden ? OVERFLOW_REGEX.hidden : OVERFLOW_REGEX.normal; |
|
2122 | var documentEl = $document[0].documentElement; |
|
2123 | var elemStyle = $window.getComputedStyle(elem); |
|
2124 | if (includeSelf && overflowRegex.test(elemStyle.overflow + elemStyle.overflowY + elemStyle.overflowX)) { |
|
2125 | return elem; |
|
2126 | } |
|
2127 | var excludeStatic = elemStyle.position === 'absolute'; |
|
2128 | var scrollParent = elem.parentElement || documentEl; |
|
2129 | ||
2130 | if (scrollParent === documentEl || elemStyle.position === 'fixed') { |
|
2131 | return documentEl; |
|
2132 | } |
|
2133 | ||
2134 | while (scrollParent.parentElement && scrollParent !== documentEl) { |
|
2135 | var spStyle = $window.getComputedStyle(scrollParent); |
|
2136 | if (excludeStatic && spStyle.position !== 'static') { |
|
2137 | excludeStatic = false; |
|
2138 | } |
|
2139 | ||
2140 | if (!excludeStatic && overflowRegex.test(spStyle.overflow + spStyle.overflowY + spStyle.overflowX)) { |
|
2141 | break; |
|
2142 | } |
|
2143 | scrollParent = scrollParent.parentElement; |
|
2144 | } |
|
2145 | ||
2146 | return scrollParent; |
|
2147 | }, |
|
2148 | ||
2149 | /** |
|
2150 | * Provides read-only equivalent of jQuery's position function: |
|
2151 | * http://api.jquery.com/position/ - distance to closest positioned |
|
2152 | * ancestor. Does not account for margins by default like jQuery position. |
|
2153 | * |
|
2154 | * @param {element} elem - The element to caclulate the position on. |
|
2155 | * @param {boolean=} [includeMargins=false] - Should margins be accounted |
|
2156 | * for, default is false. |
|
2157 | * |
|
2158 | * @returns {object} An object with the following properties: |
|
2159 | * <ul> |
|
2160 | * <li>**width**: the width of the element</li> |
|
2161 | * <li>**height**: the height of the element</li> |
|
2162 | * <li>**top**: distance to top edge of offset parent</li> |
|
2163 | * <li>**left**: distance to left edge of offset parent</li> |
|
2164 | * </ul> |
|
2165 | */ |
|
2166 | position: function(elem, includeMagins) { |
|
2167 | elem = this.getRawNode(elem); |
|
2168 | ||
2169 | var elemOffset = this.offset(elem); |
|
2170 | if (includeMagins) { |
|
2171 | var elemStyle = $window.getComputedStyle(elem); |
|
2172 | elemOffset.top -= this.parseStyle(elemStyle.marginTop); |
|
2173 | elemOffset.left -= this.parseStyle(elemStyle.marginLeft); |
|
2174 | } |
|
2175 | var parent = this.offsetParent(elem); |
|
2176 | var parentOffset = {top: 0, left: 0}; |
|
2177 | ||
2178 | if (parent !== $document[0].documentElement) { |
|
2179 | parentOffset = this.offset(parent); |
|
2180 | parentOffset.top += parent.clientTop - parent.scrollTop; |
|
2181 | parentOffset.left += parent.clientLeft - parent.scrollLeft; |
|
2182 | } |
|
2183 | ||
2184 | return { |
|
2185 | width: Math.round(angular.isNumber(elemOffset.width) ? elemOffset.width : elem.offsetWidth), |
|
2186 | height: Math.round(angular.isNumber(elemOffset.height) ? elemOffset.height : elem.offsetHeight), |
|
2187 | top: Math.round(elemOffset.top - parentOffset.top), |
|
2188 | left: Math.round(elemOffset.left - parentOffset.left) |
|
2189 | }; |
|
2190 | }, |
|
2191 | ||
2192 | /** |
|
2193 | * Provides read-only equivalent of jQuery's offset function: |
|
2194 | * http://api.jquery.com/offset/ - distance to viewport. Does |
|
2195 | * not account for borders, margins, or padding on the body |
|
2196 | * element. |
|
2197 | * |
|
2198 | * @param {element} elem - The element to calculate the offset on. |
|
2199 | * |
|
2200 | * @returns {object} An object with the following properties: |
|
2201 | * <ul> |
|
2202 | * <li>**width**: the width of the element</li> |
|
2203 | * <li>**height**: the height of the element</li> |
|
2204 | * <li>**top**: distance to top edge of viewport</li> |
|
2205 | * <li>**right**: distance to bottom edge of viewport</li> |
|
2206 | * </ul> |
|
2207 | */ |
|
2208 | offset: function(elem) { |
|
2209 | elem = this.getRawNode(elem); |
|
2210 | ||
2211 | var elemBCR = elem.getBoundingClientRect(); |
|
2212 | return { |
|
2213 | width: Math.round(angular.isNumber(elemBCR.width) ? elemBCR.width : elem.offsetWidth), |
|
2214 | height: Math.round(angular.isNumber(elemBCR.height) ? elemBCR.height : elem.offsetHeight), |
|
2215 | top: Math.round(elemBCR.top + ($window.pageYOffset || $document[0].documentElement.scrollTop)), |
|
2216 | left: Math.round(elemBCR.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)) |
|
2217 | }; |
|
2218 | }, |
|
2219 | ||
2220 | /** |
|
2221 | * Provides offset distance to the closest scrollable ancestor |
|
2222 | * or viewport. Accounts for border and scrollbar width. |
|
2223 | * |
|
2224 | * Right and bottom dimensions represent the distance to the |
|
2225 | * respective edge of the viewport element. If the element |
|
2226 | * edge extends beyond the viewport, a negative value will be |
|
2227 | * reported. |
|
2228 | * |
|
2229 | * @param {element} elem - The element to get the viewport offset for. |
|
2230 | * @param {boolean=} [useDocument=false] - Should the viewport be the document element instead |
|
2231 | * of the first scrollable element, default is false. |
|
2232 | * @param {boolean=} [includePadding=true] - Should the padding on the offset parent element |
|
2233 | * be accounted for, default is true. |
|
2234 | * |
|
2235 | * @returns {object} An object with the following properties: |
|
2236 | * <ul> |
|
2237 | * <li>**top**: distance to the top content edge of viewport element</li> |
|
2238 | * <li>**bottom**: distance to the bottom content edge of viewport element</li> |
|
2239 | * <li>**left**: distance to the left content edge of viewport element</li> |
|
2240 | * <li>**right**: distance to the right content edge of viewport element</li> |
|
2241 | * </ul> |
|
2242 | */ |
|
2243 | viewportOffset: function(elem, useDocument, includePadding) { |
|
2244 | elem = this.getRawNode(elem); |
|
2245 | includePadding = includePadding !== false ? true : false; |
|
2246 | ||
2247 | var elemBCR = elem.getBoundingClientRect(); |
|
2248 | var offsetBCR = {top: 0, left: 0, bottom: 0, right: 0}; |
|
2249 | ||
2250 | var offsetParent = useDocument ? $document[0].documentElement : this.scrollParent(elem); |
|
2251 | var offsetParentBCR = offsetParent.getBoundingClientRect(); |
|
2252 | ||
2253 | offsetBCR.top = offsetParentBCR.top + offsetParent.clientTop; |
|
2254 | offsetBCR.left = offsetParentBCR.left + offsetParent.clientLeft; |
|
2255 | if (offsetParent === $document[0].documentElement) { |
|
2256 | offsetBCR.top += $window.pageYOffset; |
|
2257 | offsetBCR.left += $window.pageXOffset; |
|
2258 | } |
|
2259 | offsetBCR.bottom = offsetBCR.top + offsetParent.clientHeight; |
|
2260 | offsetBCR.right = offsetBCR.left + offsetParent.clientWidth; |
|
2261 | ||
2262 | if (includePadding) { |
|
2263 | var offsetParentStyle = $window.getComputedStyle(offsetParent); |
|
2264 | offsetBCR.top += this.parseStyle(offsetParentStyle.paddingTop); |
|
2265 | offsetBCR.bottom -= this.parseStyle(offsetParentStyle.paddingBottom); |
|
2266 | offsetBCR.left += this.parseStyle(offsetParentStyle.paddingLeft); |
|
2267 | offsetBCR.right -= this.parseStyle(offsetParentStyle.paddingRight); |
|
2268 | } |
|
2269 | ||
2270 | return { |
|
2271 | top: Math.round(elemBCR.top - offsetBCR.top), |
|
2272 | bottom: Math.round(offsetBCR.bottom - elemBCR.bottom), |
|
2273 | left: Math.round(elemBCR.left - offsetBCR.left), |
|
2274 | right: Math.round(offsetBCR.right - elemBCR.right) |
|
2275 | }; |
|
2276 | }, |
|
2277 | ||
2278 | /** |
|
2279 | * Provides an array of placement values parsed from a placement string. |
|
2280 | * Along with the 'auto' indicator, supported placement strings are: |
|
2281 | * <ul> |
|
2282 | * <li>top: element on top, horizontally centered on host element.</li> |
|
2283 | * <li>top-left: element on top, left edge aligned with host element left edge.</li> |
|
2284 | * <li>top-right: element on top, lerightft edge aligned with host element right edge.</li> |
|
2285 | * <li>bottom: element on bottom, horizontally centered on host element.</li> |
|
2286 | * <li>bottom-left: element on bottom, left edge aligned with host element left edge.</li> |
|
2287 | * <li>bottom-right: element on bottom, right edge aligned with host element right edge.</li> |
|
2288 | * <li>left: element on left, vertically centered on host element.</li> |
|
2289 | * <li>left-top: element on left, top edge aligned with host element top edge.</li> |
|
2290 | * <li>left-bottom: element on left, bottom edge aligned with host element bottom edge.</li> |
|
2291 | * <li>right: element on right, vertically centered on host element.</li> |
|
2292 | * <li>right-top: element on right, top edge aligned with host element top edge.</li> |
|
2293 | * <li>right-bottom: element on right, bottom edge aligned with host element bottom edge.</li> |
|
2294 | * </ul> |
|
2295 | * A placement string with an 'auto' indicator is expected to be |
|
2296 | * space separated from the placement, i.e: 'auto bottom-left' If |
|
2297 | * the primary and secondary placement values do not match 'top, |
|
2298 | * bottom, left, right' then 'top' will be the primary placement and |
|
2299 | * 'center' will be the secondary placement. If 'auto' is passed, true |
|
2300 | * will be returned as the 3rd value of the array. |
|
2301 | * |
|
2302 | * @param {string} placement - The placement string to parse. |
|
2303 | * |
|
2304 | * @returns {array} An array with the following values |
|
2305 | * <ul> |
|
2306 | * <li>**[0]**: The primary placement.</li> |
|
2307 | * <li>**[1]**: The secondary placement.</li> |
|
2308 | * <li>**[2]**: If auto is passed: true, else undefined.</li> |
|
2309 | * </ul> |
|
2310 | */ |
|
2311 | parsePlacement: function(placement) { |
|
2312 | var autoPlace = PLACEMENT_REGEX.auto.test(placement); |
|
2313 | if (autoPlace) { |
|
2314 | placement = placement.replace(PLACEMENT_REGEX.auto, ''); |
|
2315 | } |
|
2316 | ||
2317 | placement = placement.split('-'); |
|
2318 | ||
2319 | placement[0] = placement[0] || 'top'; |
|
2320 | if (!PLACEMENT_REGEX.primary.test(placement[0])) { |
|
2321 | placement[0] = 'top'; |
|
2322 | } |
|
2323 | ||
2324 | placement[1] = placement[1] || 'center'; |
|
2325 | if (!PLACEMENT_REGEX.secondary.test(placement[1])) { |
|
2326 | placement[1] = 'center'; |
|
2327 | } |
|
2328 | ||
2329 | if (autoPlace) { |
|
2330 | placement[2] = true; |
|
2331 | } else { |
|
2332 | placement[2] = false; |
|
2333 | } |
|
2334 | ||
2335 | return placement; |
|
2336 | }, |
|
2337 | ||
2338 | /** |
|
2339 | * Provides coordinates for an element to be positioned relative to |
|
2340 | * another element. Passing 'auto' as part of the placement parameter |
|
2341 | * will enable smart placement - where the element fits. i.e: |
|
2342 | * 'auto left-top' will check to see if there is enough space to the left |
|
2343 | * of the hostElem to fit the targetElem, if not place right (same for secondary |
|
2344 | * top placement). Available space is calculated using the viewportOffset |
|
2345 | * function. |
|
2346 | * |
|
2347 | * @param {element} hostElem - The element to position against. |
|
2348 | * @param {element} targetElem - The element to position. |
|
2349 | * @param {string=} [placement=top] - The placement for the targetElem, |
|
2350 | * default is 'top'. 'center' is assumed as secondary placement for |
|
2351 | * 'top', 'left', 'right', and 'bottom' placements. Available placements are: |
|
2352 | * <ul> |
|
2353 | * <li>top</li> |
|
2354 | * <li>top-right</li> |
|
2355 | * <li>top-left</li> |
|
2356 | * <li>bottom</li> |
|
2357 | * <li>bottom-left</li> |
|
2358 | * <li>bottom-right</li> |
|
2359 | * <li>left</li> |
|
2360 | * <li>left-top</li> |
|
2361 | * <li>left-bottom</li> |
|
2362 | * <li>right</li> |
|
2363 | * <li>right-top</li> |
|
2364 | * <li>right-bottom</li> |
|
2365 | * </ul> |
|
2366 | * @param {boolean=} [appendToBody=false] - Should the top and left values returned |
|
2367 | * be calculated from the body element, default is false. |
|
2368 | * |
|
2369 | * @returns {object} An object with the following properties: |
|
2370 | * <ul> |
|
2371 | * <li>**top**: Value for targetElem top.</li> |
|
2372 | * <li>**left**: Value for targetElem left.</li> |
|
2373 | * <li>**placement**: The resolved placement.</li> |
|
2374 | * </ul> |
|
2375 | */ |
|
2376 | positionElements: function(hostElem, targetElem, placement, appendToBody) { |
|
2377 | hostElem = this.getRawNode(hostElem); |
|
2378 | targetElem = this.getRawNode(targetElem); |
|
2379 | ||
2380 | // need to read from prop to support tests. |
|
2381 | var targetWidth = angular.isDefined(targetElem.offsetWidth) ? targetElem.offsetWidth : targetElem.prop('offsetWidth'); |
|
2382 | var targetHeight = angular.isDefined(targetElem.offsetHeight) ? targetElem.offsetHeight : targetElem.prop('offsetHeight'); |
|
2383 | ||
2384 | placement = this.parsePlacement(placement); |
|
2385 | ||
2386 | var hostElemPos = appendToBody ? this.offset(hostElem) : this.position(hostElem); |
|
2387 | var targetElemPos = {top: 0, left: 0, placement: ''}; |
|
2388 | ||
2389 | if (placement[2]) { |
|
2390 | var viewportOffset = this.viewportOffset(hostElem, appendToBody); |
|
2391 | ||
2392 | var targetElemStyle = $window.getComputedStyle(targetElem); |
|
2393 | var adjustedSize = { |
|
2394 | width: targetWidth + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginLeft) + this.parseStyle(targetElemStyle.marginRight))), |
|
2395 | height: targetHeight + Math.round(Math.abs(this.parseStyle(targetElemStyle.marginTop) + this.parseStyle(targetElemStyle.marginBottom))) |
|
2396 | }; |
|
2397 | ||
2398 | placement[0] = placement[0] === 'top' && adjustedSize.height > viewportOffset.top && adjustedSize.height <= viewportOffset.bottom ? 'bottom' : |
|
2399 | placement[0] === 'bottom' && adjustedSize.height > viewportOffset.bottom && adjustedSize.height <= viewportOffset.top ? 'top' : |
|
2400 | placement[0] === 'left' && adjustedSize.width > viewportOffset.left && adjustedSize.width <= viewportOffset.right ? 'right' : |
|
2401 | placement[0] === 'right' && adjustedSize.width > viewportOffset.right && adjustedSize.width <= viewportOffset.left ? 'left' : |
|
2402 | placement[0]; |
|
2403 | ||
2404 | placement[1] = placement[1] === 'top' && adjustedSize.height - hostElemPos.height > viewportOffset.bottom && adjustedSize.height - hostElemPos.height <= viewportOffset.top ? 'bottom' : |
|
2405 | placement[1] === 'bottom' && adjustedSize.height - hostElemPos.height > viewportOffset.top && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom ? 'top' : |
|
2406 | placement[1] === 'left' && adjustedSize.width - hostElemPos.width > viewportOffset.right && adjustedSize.width - hostElemPos.width <= viewportOffset.left ? 'right' : |
|
2407 | placement[1] === 'right' && adjustedSize.width - hostElemPos.width > viewportOffset.left && adjustedSize.width - hostElemPos.width <= viewportOffset.right ? 'left' : |
|
2408 | placement[1]; |
|
2409 | ||
2410 | if (placement[1] === 'center') { |
|
2411 | if (PLACEMENT_REGEX.vertical.test(placement[0])) { |
|
2412 | var xOverflow = hostElemPos.width / 2 - targetWidth / 2; |
|
2413 | if (viewportOffset.left + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.right) { |
|
2414 | placement[1] = 'left'; |
|
2415 | } else if (viewportOffset.right + xOverflow < 0 && adjustedSize.width - hostElemPos.width <= viewportOffset.left) { |
|
2416 | placement[1] = 'right'; |
|
2417 | } |
|
2418 | } else { |
|
2419 | var yOverflow = hostElemPos.height / 2 - adjustedSize.height / 2; |
|
2420 | if (viewportOffset.top + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.bottom) { |
|
2421 | placement[1] = 'top'; |
|
2422 | } else if (viewportOffset.bottom + yOverflow < 0 && adjustedSize.height - hostElemPos.height <= viewportOffset.top) { |
|
2423 | placement[1] = 'bottom'; |
|
2424 | } |
|
2425 | } |
|
2426 | } |
|
2427 | } |
|
2428 | ||
2429 | switch (placement[0]) { |
|
2430 | case 'top': |
|
2431 | targetElemPos.top = hostElemPos.top - targetHeight; |
|
2432 | break; |
|
2433 | case 'bottom': |
|
2434 | targetElemPos.top = hostElemPos.top + hostElemPos.height; |
|
2435 | break; |
|
2436 | case 'left': |
|
2437 | targetElemPos.left = hostElemPos.left - targetWidth; |
|
2438 | break; |
|
2439 | case 'right': |
|
2440 | targetElemPos.left = hostElemPos.left + hostElemPos.width; |
|
2441 | break; |
|
2442 | } |
|
2443 | ||
2444 | switch (placement[1]) { |
|
2445 | case 'top': |
|
2446 | targetElemPos.top = hostElemPos.top; |
|
2447 | break; |
|
2448 | case 'bottom': |
|
2449 | targetElemPos.top = hostElemPos.top + hostElemPos.height - targetHeight; |
|
2450 | break; |
|
2451 | case 'left': |
|
2452 | targetElemPos.left = hostElemPos.left; |
|
2453 | break; |
|
2454 | case 'right': |
|
2455 | targetElemPos.left = hostElemPos.left + hostElemPos.width - targetWidth; |
|
2456 | break; |
|
2457 | case 'center': |
|
2458 | if (PLACEMENT_REGEX.vertical.test(placement[0])) { |
|
2459 | targetElemPos.left = hostElemPos.left + hostElemPos.width / 2 - targetWidth / 2; |
|
2460 | } else { |
|
2461 | targetElemPos.top = hostElemPos.top + hostElemPos.height / 2 - targetHeight / 2; |
|
2462 | } |
|
2463 | break; |
|
2464 | } |
|
2465 | ||
2466 | targetElemPos.top = Math.round(targetElemPos.top); |
|
2467 | targetElemPos.left = Math.round(targetElemPos.left); |
|
2468 | targetElemPos.placement = placement[1] === 'center' ? placement[0] : placement[0] + '-' + placement[1]; |
|
2469 | ||
2470 | return targetElemPos; |
|
2471 | }, |
|
2472 | ||
2473 | /** |
|
2474 | * Provides a way for positioning tooltip & dropdown |
|
2475 | * arrows when using placement options beyond the standard |
|
2476 | * left, right, top, or bottom. |
|
2477 | * |
|
2478 | * @param {element} elem - The tooltip/dropdown element. |
|
2479 | * @param {string} placement - The placement for the elem. |
|
2480 | */ |
|
2481 | positionArrow: function(elem, placement) { |
|
2482 | elem = this.getRawNode(elem); |
|
2483 | ||
2484 | var innerElem = elem.querySelector('.tooltip-inner, .popover-inner'); |
|
2485 | if (!innerElem) { |
|
2486 | return; |
|
2487 | } |
|
2488 | ||
2489 | var isTooltip = angular.element(innerElem).hasClass('tooltip-inner'); |
|
2490 | ||
2491 | var arrowElem = isTooltip ? elem.querySelector('.tooltip-arrow') : elem.querySelector('.arrow'); |
|
2492 | if (!arrowElem) { |
|
2493 | return; |
|
2494 | } |
|
2495 | ||
2496 | var arrowCss = { |
|
2497 | top: '', |
|
2498 | bottom: '', |
|
2499 | left: '', |
|
2500 | right: '' |
|
2501 | }; |
|
2502 | ||
2503 | placement = this.parsePlacement(placement); |
|
2504 | if (placement[1] === 'center') { |
|
2505 | // no adjustment necessary - just reset styles |
|
2506 | angular.element(arrowElem).css(arrowCss); |
|
2507 | return; |
|
2508 | } |
|
2509 | ||
2510 | var borderProp = 'border-' + placement[0] + '-width'; |
|
2511 | var borderWidth = $window.getComputedStyle(arrowElem)[borderProp]; |
|
2512 | ||
2513 | var borderRadiusProp = 'border-'; |
|
2514 | if (PLACEMENT_REGEX.vertical.test(placement[0])) { |
|
2515 | borderRadiusProp += placement[0] + '-' + placement[1]; |
|
2516 | } else { |
|
2517 | borderRadiusProp += placement[1] + '-' + placement[0]; |
|
2518 | } |
|
2519 | borderRadiusProp += '-radius'; |
|
2520 | var borderRadius = $window.getComputedStyle(isTooltip ? innerElem : elem)[borderRadiusProp]; |
|
2521 | ||
2522 | switch (placement[0]) { |
|
2523 | case 'top': |
|
2524 | arrowCss.bottom = isTooltip ? '0' : '-' + borderWidth; |
|
2525 | break; |
|
2526 | case 'bottom': |
|
2527 | arrowCss.top = isTooltip ? '0' : '-' + borderWidth; |
|
2528 | break; |
|
2529 | case 'left': |
|
2530 | arrowCss.right = isTooltip ? '0' : '-' + borderWidth; |
|
2531 | break; |
|
2532 | case 'right': |
|
2533 | arrowCss.left = isTooltip ? '0' : '-' + borderWidth; |
|
2534 | break; |
|
2535 | } |
|
2536 | ||
2537 | arrowCss[placement[1]] = borderRadius; |
|
2538 | ||
2539 | angular.element(arrowElem).css(arrowCss); |
|
2540 | } |
|
2541 | }; |
|
2542 | }]); |
|
2543 | ||
2544 | angular.module('ui.bootstrap.datepickerPopup', ['ui.bootstrap.datepicker', 'ui.bootstrap.position']) |
|
2545 |