utils.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. /* Native Javascript for Bootstrap 3 | Internal Utility Functions
  2. ----------------------------------------------------------------*/
  3. "use strict";
  4. // globals
  5. var globalObject = typeof global !== 'undefined' ? global : this||window,
  6. DOC = document, HTML = DOC.documentElement, body = 'body', // allow the library to be used in <head>
  7. // Native Javascript for Bootstrap Global Object
  8. BSN = globalObject.BSN = {},
  9. supports = BSN.supports = [],
  10. // function toggle attributes
  11. dataToggle = 'data-toggle',
  12. dataDismiss = 'data-dismiss',
  13. dataSpy = 'data-spy',
  14. dataRide = 'data-ride',
  15. // components
  16. stringAffix = 'Affix',
  17. stringAlert = 'Alert',
  18. stringButton = 'Button',
  19. stringCarousel = 'Carousel',
  20. stringCollapse = 'Collapse',
  21. stringDropdown = 'Dropdown',
  22. stringModal = 'Modal',
  23. stringPopover = 'Popover',
  24. stringScrollSpy = 'ScrollSpy',
  25. stringTab = 'Tab',
  26. stringTooltip = 'Tooltip',
  27. // options DATA API
  28. databackdrop = 'data-backdrop',
  29. dataKeyboard = 'data-keyboard',
  30. dataTarget = 'data-target',
  31. dataInterval = 'data-interval',
  32. dataHeight = 'data-height',
  33. dataPause = 'data-pause',
  34. dataTitle = 'data-title',
  35. dataOriginalTitle = 'data-original-title',
  36. dataOriginalText = 'data-original-text',
  37. dataDismissible = 'data-dismissible',
  38. dataTrigger = 'data-trigger',
  39. dataAnimation = 'data-animation',
  40. dataContainer = 'data-container',
  41. dataPlacement = 'data-placement',
  42. dataDelay = 'data-delay',
  43. dataOffsetTop = 'data-offset-top',
  44. dataOffsetBottom = 'data-offset-bottom',
  45. // option keys
  46. backdrop = 'backdrop', keyboard = 'keyboard', delay = 'delay',
  47. content = 'content', target = 'target', currentTarget = 'currentTarget',
  48. interval = 'interval', pause = 'pause', animation = 'animation',
  49. placement = 'placement', container = 'container',
  50. // box model
  51. offsetTop = 'offsetTop', offsetBottom = 'offsetBottom',
  52. offsetLeft = 'offsetLeft',
  53. scrollTop = 'scrollTop', scrollLeft = 'scrollLeft',
  54. clientWidth = 'clientWidth', clientHeight = 'clientHeight',
  55. offsetWidth = 'offsetWidth', offsetHeight = 'offsetHeight',
  56. innerWidth = 'innerWidth', innerHeight = 'innerHeight',
  57. scrollHeight = 'scrollHeight', height = 'height',
  58. // aria
  59. ariaExpanded = 'aria-expanded',
  60. ariaHidden = 'aria-hidden',
  61. // event names
  62. clickEvent = 'click',
  63. focusEvent = 'focus',
  64. hoverEvent = 'hover',
  65. keydownEvent = 'keydown',
  66. keyupEvent = 'keyup',
  67. resizeEvent = 'resize', // passive
  68. scrollEvent = 'scroll', // passive
  69. mouseHover = ('onmouseleave' in DOC) ? [ 'mouseenter', 'mouseleave'] : [ 'mouseover', 'mouseout' ],
  70. // touch since 2.0.26
  71. touchEvents = { start: 'touchstart', end: 'touchend', move:'touchmove' }, // passive
  72. // originalEvents
  73. showEvent = 'show',
  74. shownEvent = 'shown',
  75. hideEvent = 'hide',
  76. hiddenEvent = 'hidden',
  77. closeEvent = 'close',
  78. closedEvent = 'closed',
  79. slidEvent = 'slid',
  80. slideEvent = 'slide',
  81. changeEvent = 'change',
  82. // other
  83. getAttribute = 'getAttribute',
  84. setAttribute = 'setAttribute',
  85. hasAttribute = 'hasAttribute',
  86. createElement = 'createElement',
  87. appendChild = 'appendChild',
  88. innerHTML = 'innerHTML',
  89. getElementsByTagName = 'getElementsByTagName',
  90. preventDefault = 'preventDefault',
  91. getBoundingClientRect = 'getBoundingClientRect',
  92. querySelectorAll = 'querySelectorAll',
  93. getElementsByCLASSNAME = 'getElementsByClassName',
  94. getComputedStyle = 'getComputedStyle',
  95. indexOf = 'indexOf',
  96. parentNode = 'parentNode',
  97. length = 'length',
  98. toLowerCase = 'toLowerCase',
  99. Transition = 'Transition',
  100. Duration = 'Duration',
  101. Webkit = 'Webkit',
  102. style = 'style',
  103. push = 'push',
  104. tabindex = 'tabindex',
  105. contains = 'contains',
  106. active = 'active',
  107. inClass = 'in',
  108. collapsing = 'collapsing',
  109. disabled = 'disabled',
  110. loading = 'loading',
  111. left = 'left',
  112. right = 'right',
  113. top = 'top',
  114. bottom = 'bottom',
  115. // IE8 browser detect
  116. isIE8 = !('opacity' in HTML[style]),
  117. // tooltip / popover
  118. tipPositions = /\b(top|bottom|left|right)+/,
  119. // modal
  120. modalOverlay = 0,
  121. fixedTop = 'navbar-fixed-top',
  122. fixedBottom = 'navbar-fixed-bottom',
  123. // transitionEnd since 2.0.4
  124. supportTransitions = Webkit+Transition in HTML[style] || Transition[toLowerCase]() in HTML[style],
  125. transitionEndEvent = Webkit+Transition in HTML[style] ? Webkit[toLowerCase]()+Transition+'End' : Transition[toLowerCase]()+'end',
  126. transitionDuration = Webkit+Duration in HTML[style] ? Webkit[toLowerCase]()+Transition+Duration : Transition[toLowerCase]()+Duration,
  127. // set new focus element since 2.0.3
  128. setFocus = function(element){
  129. element.focus ? element.focus() : element.setActive();
  130. },
  131. // class manipulation, since 2.0.0 requires polyfill.js
  132. addClass = function(element,classNAME) {
  133. element.classList.add(classNAME);
  134. },
  135. removeClass = function(element,classNAME) {
  136. element.classList.remove(classNAME);
  137. },
  138. hasClass = function(element,classNAME){ // since 2.0.0
  139. return element.classList[contains](classNAME);
  140. },
  141. // selection methods
  142. nodeListToArray = function(nodeList){
  143. var childItems = []; for (var i = 0, nll = nodeList[length]; i<nll; i++) { childItems[push]( nodeList[i] ) }
  144. return childItems;
  145. },
  146. getElementsByClassName = function(element,classNAME) { // getElementsByClassName IE8+
  147. var selectionMethod = isIE8 ? querySelectorAll : getElementsByCLASSNAME;
  148. return nodeListToArray(element[selectionMethod]( isIE8 ? '.' + classNAME.replace(/\s(?=[a-z])/g,'.') : classNAME ));
  149. },
  150. queryElement = function (selector, parent) {
  151. var lookUp = parent ? parent : DOC;
  152. return typeof selector === 'object' ? selector : lookUp.querySelector(selector);
  153. },
  154. getClosest = function (element, selector) { //element is the element and selector is for the closest parent element to find
  155. // source http://gomakethings.com/climbing-up-and-down-the-dom-tree-with-vanilla-javascript/
  156. var firstChar = selector.charAt(0), selectorSubstring = selector.substr(1);
  157. if ( firstChar === '.' ) {// If selector is a class
  158. for ( ; element && element !== DOC; element = element[parentNode] ) { // Get closest match
  159. if ( queryElement(selector,element[parentNode]) !== null && hasClass(element,selectorSubstring) ) { return element; }
  160. }
  161. } else if ( firstChar === '#' ) { // If selector is an ID
  162. for ( ; element && element !== DOC; element = element[parentNode] ) { // Get closest match
  163. if ( element.id === selectorSubstring ) { return element; }
  164. }
  165. }
  166. return false;
  167. },
  168. // event attach jQuery style / trigger since 1.2.0
  169. on = function (element, event, handler, options) {
  170. options = options || false;
  171. element.addEventListener(event, handler, options);
  172. },
  173. off = function(element, event, handler, options) {
  174. options = options || false;
  175. element.removeEventListener(event, handler, options);
  176. },
  177. one = function (element, event, handler, options) { // one since 2.0.4
  178. on(element, event, function handlerWrapper(e){
  179. handler(e);
  180. off(element, event, handlerWrapper, options);
  181. }, options);
  182. },
  183. // determine support for passive events
  184. supportPassive = (function(){
  185. // Test via a getter in the options object to see if the passive property is accessed
  186. var result = false;
  187. try {
  188. var opts = Object.defineProperty({}, 'passive', {
  189. get: function() {
  190. result = true;
  191. }
  192. });
  193. one(globalObject, 'testPassive', null, opts);
  194. } catch (e) {}
  195. return result;
  196. }()),
  197. // event options
  198. // https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md#feature-detection
  199. passiveHandler = supportPassive ? { passive: true } : false,
  200. // transitions
  201. getTransitionDurationFromElement = function(element) {
  202. var duration = supportTransitions ? globalObject[getComputedStyle](element)[transitionDuration] : 0;
  203. duration = parseFloat(duration);
  204. duration = typeof duration === 'number' && !isNaN(duration) ? duration * 1000 : 0;
  205. return duration; // we take a short offset to make sure we fire on the next frame after animation
  206. },
  207. emulateTransitionEnd = function(element,handler){ // emulateTransitionEnd since 2.0.4
  208. var called = 0, duration = getTransitionDurationFromElement(element);
  209. duration ? one(element, transitionEndEvent, function(e){ !called && handler(e), called = 1; })
  210. : setTimeout(function() { !called && handler(), called = 1; }, 17);
  211. },
  212. bootstrapCustomEvent = function (eventName, componentName, related) {
  213. var OriginalCustomEvent = new CustomEvent( eventName + '.bs.' + componentName);
  214. OriginalCustomEvent.relatedTarget = related;
  215. this.dispatchEvent(OriginalCustomEvent);
  216. },
  217. // tooltip / popover stuff
  218. getScroll = function() { // also Affix and ScrollSpy uses it
  219. return {
  220. y : globalObject.pageYOffset || HTML[scrollTop],
  221. x : globalObject.pageXOffset || HTML[scrollLeft]
  222. }
  223. },
  224. styleTip = function(link,element,position,parent) { // both popovers and tooltips (target,tooltip/popover,placement,elementToAppendTo)
  225. var elementDimensions = { w : element[offsetWidth], h: element[offsetHeight] },
  226. windowWidth = (HTML[clientWidth] || DOC[body][clientWidth]),
  227. windowHeight = (HTML[clientHeight] || DOC[body][clientHeight]),
  228. rect = link[getBoundingClientRect](),
  229. scroll = parent === DOC[body] ? getScroll() : { x: parent[offsetLeft] + parent[scrollLeft], y: parent[offsetTop] + parent[scrollTop] },
  230. linkDimensions = { w: rect[right] - rect[left], h: rect[bottom] - rect[top] },
  231. arrow = queryElement('[class*="arrow"]',element),
  232. topPosition, leftPosition, arrowTop, arrowLeft,
  233. halfTopExceed = rect[top] + linkDimensions.h/2 - elementDimensions.h/2 < 0,
  234. halfLeftExceed = rect[left] + linkDimensions.w/2 - elementDimensions.w/2 < 0,
  235. halfRightExceed = rect[left] + elementDimensions.w/2 + linkDimensions.w/2 >= windowWidth,
  236. halfBottomExceed = rect[top] + elementDimensions.h/2 + linkDimensions.h/2 >= windowHeight,
  237. topExceed = rect[top] - elementDimensions.h < 0,
  238. leftExceed = rect[left] - elementDimensions.w < 0,
  239. bottomExceed = rect[top] + elementDimensions.h + linkDimensions.h >= windowHeight,
  240. rightExceed = rect[left] + elementDimensions.w + linkDimensions.w >= windowWidth;
  241. // recompute position
  242. position = (position === left || position === right) && leftExceed && rightExceed ? top : position; // first, when both left and right limits are exceeded, we fall back to top|bottom
  243. position = position === top && topExceed ? bottom : position;
  244. position = position === bottom && bottomExceed ? top : position;
  245. position = position === left && leftExceed ? right : position;
  246. position = position === right && rightExceed ? left : position;
  247. // apply styling to tooltip or popover
  248. if ( position === left || position === right ) { // secondary|side positions
  249. if ( position === left ) { // LEFT
  250. leftPosition = rect[left] + scroll.x - elementDimensions.w;
  251. } else { // RIGHT
  252. leftPosition = rect[left] + scroll.x + linkDimensions.w;
  253. }
  254. // adjust top and arrow
  255. if (halfTopExceed) {
  256. topPosition = rect[top] + scroll.y;
  257. arrowTop = linkDimensions.h/2;
  258. } else if (halfBottomExceed) {
  259. topPosition = rect[top] + scroll.y - elementDimensions.h + linkDimensions.h;
  260. arrowTop = elementDimensions.h - linkDimensions.h/2;
  261. } else {
  262. topPosition = rect[top] + scroll.y - elementDimensions.h/2 + linkDimensions.h/2;
  263. }
  264. } else if ( position === top || position === bottom ) { // primary|vertical positions
  265. if ( position === top) { // TOP
  266. topPosition = rect[top] + scroll.y - elementDimensions.h;
  267. } else { // BOTTOM
  268. topPosition = rect[top] + scroll.y + linkDimensions.h;
  269. }
  270. // adjust left | right and also the arrow
  271. if (halfLeftExceed) {
  272. leftPosition = 0;
  273. arrowLeft = rect[left] + linkDimensions.w/2;
  274. } else if (halfRightExceed) {
  275. leftPosition = windowWidth - elementDimensions.w*1.01;
  276. arrowLeft = elementDimensions.w - ( windowWidth - rect[left] ) + linkDimensions.w/2;
  277. } else {
  278. leftPosition = rect[left] + scroll.x - elementDimensions.w/2 + linkDimensions.w/2;
  279. }
  280. }
  281. // apply style to tooltip/popover and it's arrow
  282. element[style][top] = topPosition + 'px';
  283. element[style][left] = leftPosition + 'px';
  284. arrowTop && (arrow[style][top] = arrowTop + 'px');
  285. arrowLeft && (arrow[style][left] = arrowLeft + 'px');
  286. element.className[indexOf](position) === -1 && (element.className = element.className.replace(tipPositions,position));
  287. };