index.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. /**
  2. * gemini-scrollbar
  3. * @version 1.5.3
  4. * @link http://noeldelgado.github.io/gemini-scrollbar/
  5. * @license MIT
  6. */
  7. (function() {
  8. var SCROLLBAR_WIDTH, DONT_CREATE_GEMINI, CLASSNAMES;
  9. CLASSNAMES = {
  10. element: 'gm-scrollbar-container',
  11. verticalScrollbar: 'gm-scrollbar -vertical',
  12. horizontalScrollbar: 'gm-scrollbar -horizontal',
  13. thumb: 'thumb',
  14. view: 'gm-scroll-view',
  15. autoshow: 'gm-autoshow',
  16. disable: 'gm-scrollbar-disable-selection',
  17. prevented: 'gm-prevented',
  18. resizeTrigger: 'gm-resize-trigger',
  19. };
  20. function getScrollbarWidth() {
  21. var e = document.createElement('div'), sw;
  22. e.style.position = 'absolute';
  23. e.style.top = '-9999px';
  24. e.style.width = '100px';
  25. e.style.height = '100px';
  26. e.style.overflow = 'scroll';
  27. e.style.msOverflowStyle = 'scrollbar';
  28. document.body.appendChild(e);
  29. sw = (e.offsetWidth - e.clientWidth);
  30. document.body.removeChild(e);
  31. return sw;
  32. }
  33. function addClass(el, classNames) {
  34. if (el.classList) {
  35. return classNames.forEach(function(cl) {
  36. el.classList.add(cl);
  37. });
  38. }
  39. el.className += ' ' + classNames.join(' ');
  40. }
  41. function removeClass(el, classNames) {
  42. if (el.classList) {
  43. return classNames.forEach(function(cl) {
  44. el.classList.remove(cl);
  45. });
  46. }
  47. el.className = el.className.replace(new RegExp('(^|\\b)' + classNames.join('|') + '(\\b|$)', 'gi'), ' ');
  48. }
  49. /* Copyright (c) 2015 Lucas Wiener
  50. * https://github.com/wnr/element-resize-detector
  51. */
  52. function isIE() {
  53. var agent = navigator.userAgent.toLowerCase();
  54. return agent.indexOf("msie") !== -1 || agent.indexOf("trident") !== -1 || agent.indexOf(" edge/") !== -1;
  55. }
  56. function GeminiScrollbar(config) {
  57. this.element = null;
  58. this.autoshow = false;
  59. this.createElements = true;
  60. this.forceGemini = false;
  61. this.onResize = null;
  62. this.minThumbSize = 20;
  63. Object.keys(config || {}).forEach(function (propertyName) {
  64. this[propertyName] = config[propertyName];
  65. }, this);
  66. SCROLLBAR_WIDTH = getScrollbarWidth();
  67. DONT_CREATE_GEMINI = ((SCROLLBAR_WIDTH === 0) && (this.forceGemini === false));
  68. this._cache = {events: {}};
  69. this._created = false;
  70. this._cursorDown = false;
  71. this._prevPageX = 0;
  72. this._prevPageY = 0;
  73. this._document = null;
  74. this._viewElement = this.element;
  75. this._scrollbarVerticalElement = null;
  76. this._thumbVerticalElement = null;
  77. this._scrollbarHorizontalElement = null;
  78. this._scrollbarHorizontalElement = null;
  79. }
  80. GeminiScrollbar.prototype.create = function create() {
  81. if (DONT_CREATE_GEMINI) {
  82. addClass(this.element, [CLASSNAMES.prevented]);
  83. if (this.onResize) {
  84. // still need a resize trigger if we have an onResize callback, which
  85. // also means we need a separate _viewElement to do the scrolling.
  86. if (this.createElements === true) {
  87. this._viewElement = document.createElement('div');
  88. while(this.element.childNodes.length > 0) {
  89. this._viewElement.appendChild(this.element.childNodes[0]);
  90. }
  91. this.element.appendChild(this._viewElement);
  92. } else {
  93. this._viewElement = this.element.querySelector('.' + CLASSNAMES.view);
  94. }
  95. addClass(this.element, [CLASSNAMES.element]);
  96. addClass(this._viewElement, [CLASSNAMES.view]);
  97. this._createResizeTrigger();
  98. }
  99. return this;
  100. }
  101. if (this._created === true) {
  102. console.warn('calling on a already-created object');
  103. return this;
  104. }
  105. if (this.autoshow) {
  106. addClass(this.element, [CLASSNAMES.autoshow]);
  107. }
  108. this._document = document;
  109. if (this.createElements === true) {
  110. this._viewElement = document.createElement('div');
  111. this._scrollbarVerticalElement = document.createElement('div');
  112. this._thumbVerticalElement = document.createElement('div');
  113. this._scrollbarHorizontalElement = document.createElement('div');
  114. this._thumbHorizontalElement = document.createElement('div');
  115. while(this.element.childNodes.length > 0) {
  116. this._viewElement.appendChild(this.element.childNodes[0]);
  117. }
  118. this._scrollbarVerticalElement.appendChild(this._thumbVerticalElement);
  119. this._scrollbarHorizontalElement.appendChild(this._thumbHorizontalElement);
  120. this.element.appendChild(this._scrollbarVerticalElement);
  121. this.element.appendChild(this._scrollbarHorizontalElement);
  122. this.element.appendChild(this._viewElement);
  123. } else {
  124. this._viewElement = this.element.querySelector('.' + CLASSNAMES.view);
  125. this._scrollbarVerticalElement = this.element.querySelector('.' + CLASSNAMES.verticalScrollbar.split(' ').join('.'));
  126. this._thumbVerticalElement = this._scrollbarVerticalElement.querySelector('.' + CLASSNAMES.thumb);
  127. this._scrollbarHorizontalElement = this.element.querySelector('.' + CLASSNAMES.horizontalScrollbar.split(' ').join('.'));
  128. this._thumbHorizontalElement = this._scrollbarHorizontalElement.querySelector('.' + CLASSNAMES.thumb);
  129. }
  130. addClass(this.element, [CLASSNAMES.element]);
  131. addClass(this._viewElement, [CLASSNAMES.view]);
  132. addClass(this._scrollbarVerticalElement, CLASSNAMES.verticalScrollbar.split(/\s/));
  133. addClass(this._scrollbarHorizontalElement, CLASSNAMES.horizontalScrollbar.split(/\s/));
  134. addClass(this._thumbVerticalElement, [CLASSNAMES.thumb]);
  135. addClass(this._thumbHorizontalElement, [CLASSNAMES.thumb]);
  136. this._scrollbarVerticalElement.style.display = '';
  137. this._scrollbarHorizontalElement.style.display = '';
  138. this._createResizeTrigger();
  139. this._created = true;
  140. return this._bindEvents().update();
  141. };
  142. GeminiScrollbar.prototype._createResizeTrigger = function createResizeTrigger() {
  143. // We need to arrange for self.scrollbar.update to be called whenever
  144. // the DOM is changed resulting in a size-change for our div. To make
  145. // this happen, we use a technique described here:
  146. // http://www.backalleycoder.com/2013/03/18/cross-browser-event-based-element-resize-detection/.
  147. //
  148. // The idea is that we create an <object> element in our div, which we
  149. // arrange to have the same size as that div. The <object> element
  150. // contains a Window object, to which we can attach an onresize
  151. // handler.
  152. //
  153. // (React appears to get very confused by the object (we end up with
  154. // Chrome windows which only show half of the text they are supposed
  155. // to), so we always do this manually.)
  156. var obj = document.createElement('object');
  157. addClass(obj, [CLASSNAMES.resizeTrigger]);
  158. obj.type = 'text/html';
  159. obj.setAttribute('tabindex', '-1');
  160. var resizeHandler = this._resizeHandler.bind(this);
  161. obj.onload = function () {
  162. var win = obj.contentDocument.defaultView;
  163. win.addEventListener('resize', resizeHandler);
  164. };
  165. //IE: Does not like that this happens before, even if it is also added after.
  166. if (!isIE()) {
  167. obj.data = 'about:blank';
  168. }
  169. this.element.appendChild(obj);
  170. //IE: This must occur after adding the object to the DOM.
  171. if (isIE()) {
  172. obj.data = 'about:blank';
  173. }
  174. this._resizeTriggerElement = obj;
  175. };
  176. GeminiScrollbar.prototype.update = function update() {
  177. if (DONT_CREATE_GEMINI) {
  178. return this;
  179. }
  180. if (this._created === false) {
  181. console.warn('calling on a not-yet-created object');
  182. return this;
  183. }
  184. this._viewElement.style.width = ((this.element.offsetWidth + SCROLLBAR_WIDTH).toString() + 'px');
  185. this._viewElement.style.height = ((this.element.offsetHeight + SCROLLBAR_WIDTH).toString() + 'px');
  186. this._naturalThumbSizeX = this._scrollbarHorizontalElement.clientWidth / this._viewElement.scrollWidth * this._scrollbarHorizontalElement.clientWidth;
  187. this._naturalThumbSizeY = this._scrollbarVerticalElement.clientHeight / this._viewElement.scrollHeight * this._scrollbarVerticalElement.clientHeight;
  188. this._scrollTopMax = this._viewElement.scrollHeight - this._viewElement.clientHeight;
  189. this._scrollLeftMax = this._viewElement.scrollWidth - this._viewElement.clientWidth;
  190. if (this._naturalThumbSizeY < this.minThumbSize) {
  191. this._thumbVerticalElement.style.height = this.minThumbSize + 'px';
  192. } else if (this._scrollTopMax) {
  193. this._thumbVerticalElement.style.height = this._naturalThumbSizeY + 'px';
  194. } else {
  195. this._thumbVerticalElement.style.height = '0px';
  196. }
  197. if (this._naturalThumbSizeX < this.minThumbSize) {
  198. this._thumbHorizontalElement.style.width = this.minThumbSize + 'px';
  199. } else if (this._scrollLeftMax) {
  200. this._thumbHorizontalElement.style.width = this._naturalThumbSizeX + 'px';
  201. } else {
  202. this._thumbHorizontalElement.style.width = '0px';
  203. }
  204. this._thumbSizeY = this._thumbVerticalElement.clientHeight;
  205. this._thumbSizeX = this._thumbHorizontalElement.clientWidth;
  206. this._trackTopMax = this._scrollbarVerticalElement.clientHeight - this._thumbSizeY;
  207. this._trackLeftMax = this._scrollbarHorizontalElement.clientWidth - this._thumbSizeX;
  208. this._scrollHandler();
  209. return this;
  210. };
  211. GeminiScrollbar.prototype.destroy = function destroy() {
  212. if (this._resizeTriggerElement) {
  213. this.element.removeChild(this._resizeTriggerElement);
  214. this._resizeTriggerElement = null;
  215. }
  216. if (DONT_CREATE_GEMINI) {
  217. return this;
  218. }
  219. if (this._created === false) {
  220. console.warn('calling on a not-yet-created object');
  221. return this;
  222. }
  223. this._unbinEvents();
  224. removeClass(this.element, [CLASSNAMES.element, CLASSNAMES.autoshow]);
  225. if (this.createElements === true) {
  226. this.element.removeChild(this._scrollbarVerticalElement);
  227. this.element.removeChild(this._scrollbarHorizontalElement);
  228. while(this._viewElement.childNodes.length > 0) {
  229. this.element.appendChild(this._viewElement.childNodes[0]);
  230. }
  231. this.element.removeChild(this._viewElement);
  232. } else {
  233. this._viewElement.style.width = '';
  234. this._viewElement.style.height = '';
  235. this._scrollbarVerticalElement.style.display = 'none';
  236. this._scrollbarHorizontalElement.style.display = 'none';
  237. }
  238. this._created = false;
  239. this._document = null;
  240. return null;
  241. };
  242. GeminiScrollbar.prototype.getViewElement = function getViewElement() {
  243. return this._viewElement;
  244. };
  245. GeminiScrollbar.prototype._bindEvents = function _bindEvents() {
  246. this._cache.events.scrollHandler = this._scrollHandler.bind(this);
  247. this._cache.events.clickVerticalTrackHandler = this._clickVerticalTrackHandler.bind(this);
  248. this._cache.events.clickHorizontalTrackHandler = this._clickHorizontalTrackHandler.bind(this);
  249. this._cache.events.clickVerticalThumbHandler = this._clickVerticalThumbHandler.bind(this);
  250. this._cache.events.clickHorizontalThumbHandler = this._clickHorizontalThumbHandler.bind(this);
  251. this._cache.events.mouseUpDocumentHandler = this._mouseUpDocumentHandler.bind(this);
  252. this._cache.events.mouseMoveDocumentHandler = this._mouseMoveDocumentHandler.bind(this);
  253. this._viewElement.addEventListener('scroll', this._cache.events.scrollHandler);
  254. this._scrollbarVerticalElement.addEventListener('mousedown', this._cache.events.clickVerticalTrackHandler);
  255. this._scrollbarHorizontalElement.addEventListener('mousedown', this._cache.events.clickHorizontalTrackHandler);
  256. this._thumbVerticalElement.addEventListener('mousedown', this._cache.events.clickVerticalThumbHandler);
  257. this._thumbHorizontalElement.addEventListener('mousedown', this._cache.events.clickHorizontalThumbHandler);
  258. this._document.addEventListener('mouseup', this._cache.events.mouseUpDocumentHandler);
  259. return this;
  260. };
  261. GeminiScrollbar.prototype._unbinEvents = function _unbinEvents() {
  262. this._viewElement.removeEventListener('scroll', this._cache.events.scrollHandler);
  263. this._scrollbarVerticalElement.removeEventListener('mousedown', this._cache.events.clickVerticalTrackHandler);
  264. this._scrollbarHorizontalElement.removeEventListener('mousedown', this._cache.events.clickHorizontalTrackHandler);
  265. this._thumbVerticalElement.removeEventListener('mousedown', this._cache.events.clickVerticalThumbHandler);
  266. this._thumbHorizontalElement.removeEventListener('mousedown', this._cache.events.clickHorizontalThumbHandler);
  267. this._document.removeEventListener('mouseup', this._cache.events.mouseUpDocumentHandler);
  268. this._document.removeEventListener('mousemove', this._cache.events.mouseMoveDocumentHandler);
  269. return this;
  270. };
  271. GeminiScrollbar.prototype._scrollHandler = function _scrollHandler() {
  272. var x = (this._viewElement.scrollLeft * this._trackLeftMax / this._scrollLeftMax) || 0;
  273. var y = (this._viewElement.scrollTop * this._trackTopMax / this._scrollTopMax) || 0;
  274. this._thumbHorizontalElement.style.msTransform = 'translateX(' + x + 'px)';
  275. this._thumbHorizontalElement.style.webkitTransform = 'translate3d(' + x + 'px, 0, 0)';
  276. this._thumbHorizontalElement.style.transform = 'translate3d(' + x + 'px, 0, 0)';
  277. this._thumbVerticalElement.style.msTransform = 'translateY(' + y + 'px)';
  278. this._thumbVerticalElement.style.webkitTransform = 'translate3d(0, ' + y + 'px, 0)';
  279. this._thumbVerticalElement.style.transform = 'translate3d(0, ' + y + 'px, 0)';
  280. };
  281. GeminiScrollbar.prototype._resizeHandler = function _resizeHandler() {
  282. this.update();
  283. if (this.onResize) {
  284. this.onResize();
  285. }
  286. };
  287. GeminiScrollbar.prototype._clickVerticalTrackHandler = function _clickVerticalTrackHandler(e) {
  288. if(e.target !== e.currentTarget) {
  289. return;
  290. }
  291. var offset = e.offsetY - this._naturalThumbSizeY * .5
  292. , thumbPositionPercentage = offset * 100 / this._scrollbarVerticalElement.clientHeight;
  293. this._viewElement.scrollTop = thumbPositionPercentage * this._viewElement.scrollHeight / 100;
  294. };
  295. GeminiScrollbar.prototype._clickHorizontalTrackHandler = function _clickHorizontalTrackHandler(e) {
  296. if(e.target !== e.currentTarget) {
  297. return;
  298. }
  299. var offset = e.offsetX - this._naturalThumbSizeX * .5
  300. , thumbPositionPercentage = offset * 100 / this._scrollbarHorizontalElement.clientWidth;
  301. this._viewElement.scrollLeft = thumbPositionPercentage * this._viewElement.scrollWidth / 100;
  302. };
  303. GeminiScrollbar.prototype._clickVerticalThumbHandler = function _clickVerticalThumbHandler(e) {
  304. this._startDrag(e);
  305. this._prevPageY = this._thumbSizeY - e.offsetY;
  306. };
  307. GeminiScrollbar.prototype._clickHorizontalThumbHandler = function _clickHorizontalThumbHandler(e) {
  308. this._startDrag(e);
  309. this._prevPageX = this._thumbSizeX - e.offsetX;
  310. };
  311. GeminiScrollbar.prototype._startDrag = function _startDrag(e) {
  312. this._cursorDown = true;
  313. addClass(document.body, [CLASSNAMES.disable]);
  314. this._document.addEventListener('mousemove', this._cache.events.mouseMoveDocumentHandler);
  315. this._document.onselectstart = function() {return false;};
  316. };
  317. GeminiScrollbar.prototype._mouseUpDocumentHandler = function _mouseUpDocumentHandler() {
  318. this._cursorDown = false;
  319. this._prevPageX = this._prevPageY = 0;
  320. removeClass(document.body, [CLASSNAMES.disable]);
  321. this._document.removeEventListener('mousemove', this._cache.events.mouseMoveDocumentHandler);
  322. this._document.onselectstart = null;
  323. };
  324. GeminiScrollbar.prototype._mouseMoveDocumentHandler = function _mouseMoveDocumentHandler(e) {
  325. if (this._cursorDown === false) {return;}
  326. var offset, thumbClickPosition;
  327. if (this._prevPageY) {
  328. offset = e.clientY - this._scrollbarVerticalElement.getBoundingClientRect().top;
  329. thumbClickPosition = this._thumbSizeY - this._prevPageY;
  330. this._viewElement.scrollTop = this._scrollTopMax * (offset - thumbClickPosition) / this._trackTopMax;
  331. return void 0;
  332. }
  333. if (this._prevPageX) {
  334. offset = e.clientX - this._scrollbarHorizontalElement.getBoundingClientRect().left;
  335. thumbClickPosition = this._thumbSizeX - this._prevPageX;
  336. this._viewElement.scrollLeft = this._scrollLeftMax * (offset - thumbClickPosition) / this._trackLeftMax;
  337. }
  338. };
  339. if (typeof exports === 'object') {
  340. module.exports = GeminiScrollbar;
  341. } else {
  342. window.GeminiScrollbar = GeminiScrollbar;
  343. }
  344. })();