| @@ 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 | ||