You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

scrollbar.js 13 KiB

3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. import { __assign, __decorate } from "tslib";
  2. import clamp from 'lodash-es/clamp';
  3. import { Options } from './options';
  4. import { setStyle, clearEventsOn, } from './utils/';
  5. import { debounce, } from './decorators/';
  6. import { TrackController, } from './track/';
  7. import { getSize, update, isVisible, } from './geometry/';
  8. import { scrollTo, setPosition, scrollIntoView, } from './scrolling/';
  9. import { initPlugins, } from './plugin';
  10. import * as eventHandlers from './events/';
  11. // DO NOT use WeakMap here
  12. // .getAll() methods requires `scrollbarMap.values()`
  13. export var scrollbarMap = new Map();
  14. var Scrollbar = /** @class */ (function () {
  15. function Scrollbar(containerEl, options) {
  16. var _this = this;
  17. /**
  18. * Current scrolling offsets
  19. */
  20. this.offset = {
  21. x: 0,
  22. y: 0,
  23. };
  24. /**
  25. * Max-allowed scrolling offsets
  26. */
  27. this.limit = {
  28. x: Infinity,
  29. y: Infinity,
  30. };
  31. /**
  32. * Container bounding rect
  33. */
  34. this.bounding = {
  35. top: 0,
  36. right: 0,
  37. bottom: 0,
  38. left: 0,
  39. };
  40. this._plugins = [];
  41. this._momentum = { x: 0, y: 0 };
  42. this._listeners = new Set();
  43. this.containerEl = containerEl;
  44. var contentEl = this.contentEl = document.createElement('div');
  45. this.options = new Options(options);
  46. // mark as a scroll element
  47. containerEl.setAttribute('data-scrollbar', 'true');
  48. // make container focusable
  49. containerEl.setAttribute('tabindex', '-1');
  50. setStyle(containerEl, {
  51. overflow: 'hidden',
  52. outline: 'none',
  53. });
  54. // enable touch event capturing in IE, see:
  55. // https://github.com/idiotWu/smooth-scrollbar/issues/39
  56. if (window.navigator.msPointerEnabled) {
  57. containerEl.style.msTouchAction = 'none';
  58. }
  59. // mount content
  60. contentEl.className = 'scroll-content';
  61. Array.from(containerEl.childNodes).forEach(function (node) {
  62. contentEl.appendChild(node);
  63. });
  64. containerEl.appendChild(contentEl);
  65. // attach track
  66. this.track = new TrackController(this);
  67. // initial measuring
  68. this.size = this.getSize();
  69. // init plugins
  70. this._plugins = initPlugins(this, this.options.plugins);
  71. // preserve scroll offset
  72. var scrollLeft = containerEl.scrollLeft, scrollTop = containerEl.scrollTop;
  73. containerEl.scrollLeft = containerEl.scrollTop = 0;
  74. this.setPosition(scrollLeft, scrollTop, {
  75. withoutCallbacks: true,
  76. });
  77. var global = window;
  78. var MutationObserver = global.MutationObserver || global.WebKitMutationObserver || global.MozMutationObserver;
  79. // observe
  80. if (typeof MutationObserver === 'function') {
  81. this._observer = new MutationObserver(function () {
  82. _this.update();
  83. });
  84. this._observer.observe(contentEl, {
  85. subtree: true,
  86. childList: true,
  87. });
  88. }
  89. scrollbarMap.set(containerEl, this);
  90. // wait for DOM ready
  91. requestAnimationFrame(function () {
  92. _this._init();
  93. });
  94. }
  95. Object.defineProperty(Scrollbar.prototype, "parent", {
  96. /**
  97. * Parent scrollbar
  98. */
  99. get: function () {
  100. var elem = this.containerEl.parentElement;
  101. while (elem) {
  102. var parentScrollbar = scrollbarMap.get(elem);
  103. if (parentScrollbar) {
  104. return parentScrollbar;
  105. }
  106. elem = elem.parentElement;
  107. }
  108. return null;
  109. },
  110. enumerable: true,
  111. configurable: true
  112. });
  113. Object.defineProperty(Scrollbar.prototype, "scrollTop", {
  114. /**
  115. * Gets or sets `scrollbar.offset.y`
  116. */
  117. get: function () {
  118. return this.offset.y;
  119. },
  120. set: function (y) {
  121. this.setPosition(this.scrollLeft, y);
  122. },
  123. enumerable: true,
  124. configurable: true
  125. });
  126. Object.defineProperty(Scrollbar.prototype, "scrollLeft", {
  127. /**
  128. * Gets or sets `scrollbar.offset.x`
  129. */
  130. get: function () {
  131. return this.offset.x;
  132. },
  133. set: function (x) {
  134. this.setPosition(x, this.scrollTop);
  135. },
  136. enumerable: true,
  137. configurable: true
  138. });
  139. /**
  140. * Returns the size of the scrollbar container element
  141. * and the content wrapper element
  142. */
  143. Scrollbar.prototype.getSize = function () {
  144. return getSize(this);
  145. };
  146. /**
  147. * Forces scrollbar to update geometry infomation.
  148. *
  149. * By default, scrollbars are automatically updated with `100ms` debounce (or `MutationObserver` fires).
  150. * You can call this method to force an update when you modified contents
  151. */
  152. Scrollbar.prototype.update = function () {
  153. update(this);
  154. this._plugins.forEach(function (plugin) {
  155. plugin.onUpdate();
  156. });
  157. };
  158. /**
  159. * Checks if an element is visible in the current view area
  160. */
  161. Scrollbar.prototype.isVisible = function (elem) {
  162. return isVisible(this, elem);
  163. };
  164. /**
  165. * Sets the scrollbar to the given offset without easing
  166. */
  167. Scrollbar.prototype.setPosition = function (x, y, options) {
  168. var _this = this;
  169. if (x === void 0) { x = this.offset.x; }
  170. if (y === void 0) { y = this.offset.y; }
  171. if (options === void 0) { options = {}; }
  172. var status = setPosition(this, x, y);
  173. if (!status || options.withoutCallbacks) {
  174. return;
  175. }
  176. this._listeners.forEach(function (fn) {
  177. fn.call(_this, status);
  178. });
  179. };
  180. /**
  181. * Scrolls to given position with easing function
  182. */
  183. Scrollbar.prototype.scrollTo = function (x, y, duration, options) {
  184. if (x === void 0) { x = this.offset.x; }
  185. if (y === void 0) { y = this.offset.y; }
  186. if (duration === void 0) { duration = 0; }
  187. if (options === void 0) { options = {}; }
  188. scrollTo(this, x, y, duration, options);
  189. };
  190. /**
  191. * Scrolls the target element into visible area of scrollbar,
  192. * likes the DOM method `element.scrollIntoView().
  193. */
  194. Scrollbar.prototype.scrollIntoView = function (elem, options) {
  195. if (options === void 0) { options = {}; }
  196. scrollIntoView(this, elem, options);
  197. };
  198. /**
  199. * Adds scrolling listener
  200. */
  201. Scrollbar.prototype.addListener = function (fn) {
  202. if (typeof fn !== 'function') {
  203. throw new TypeError('[smooth-scrollbar] scrolling listener should be a function');
  204. }
  205. this._listeners.add(fn);
  206. };
  207. /**
  208. * Removes listener previously registered with `scrollbar.addListener()`
  209. */
  210. Scrollbar.prototype.removeListener = function (fn) {
  211. this._listeners.delete(fn);
  212. };
  213. /**
  214. * Adds momentum and applys delta transformers.
  215. */
  216. Scrollbar.prototype.addTransformableMomentum = function (x, y, fromEvent, callback) {
  217. this._updateDebounced();
  218. var finalDelta = this._plugins.reduce(function (delta, plugin) {
  219. return plugin.transformDelta(delta, fromEvent) || delta;
  220. }, { x: x, y: y });
  221. var willScroll = !this._shouldPropagateMomentum(finalDelta.x, finalDelta.y);
  222. if (willScroll) {
  223. this.addMomentum(finalDelta.x, finalDelta.y);
  224. }
  225. if (callback) {
  226. callback.call(this, willScroll);
  227. }
  228. };
  229. /**
  230. * Increases scrollbar's momentum
  231. */
  232. Scrollbar.prototype.addMomentum = function (x, y) {
  233. this.setMomentum(this._momentum.x + x, this._momentum.y + y);
  234. };
  235. /**
  236. * Sets scrollbar's momentum to given value
  237. */
  238. Scrollbar.prototype.setMomentum = function (x, y) {
  239. if (this.limit.x === 0) {
  240. x = 0;
  241. }
  242. if (this.limit.y === 0) {
  243. y = 0;
  244. }
  245. if (this.options.renderByPixels) {
  246. x = Math.round(x);
  247. y = Math.round(y);
  248. }
  249. this._momentum.x = x;
  250. this._momentum.y = y;
  251. };
  252. /**
  253. * Update options for specific plugin
  254. *
  255. * @param pluginName Name of the plugin
  256. * @param [options] An object includes the properties that you want to update
  257. */
  258. Scrollbar.prototype.updatePluginOptions = function (pluginName, options) {
  259. this._plugins.forEach(function (plugin) {
  260. if (plugin.name === pluginName) {
  261. Object.assign(plugin.options, options);
  262. }
  263. });
  264. };
  265. Scrollbar.prototype.destroy = function () {
  266. var _a = this, containerEl = _a.containerEl, contentEl = _a.contentEl;
  267. clearEventsOn(this);
  268. this._listeners.clear();
  269. this.setMomentum(0, 0);
  270. cancelAnimationFrame(this._renderID);
  271. if (this._observer) {
  272. this._observer.disconnect();
  273. }
  274. scrollbarMap.delete(this.containerEl);
  275. // restore contents
  276. var childNodes = Array.from(contentEl.childNodes);
  277. while (containerEl.firstChild) {
  278. containerEl.removeChild(containerEl.firstChild);
  279. }
  280. childNodes.forEach(function (el) {
  281. containerEl.appendChild(el);
  282. });
  283. // reset scroll position
  284. setStyle(containerEl, {
  285. overflow: '',
  286. });
  287. containerEl.scrollTop = this.scrollTop;
  288. containerEl.scrollLeft = this.scrollLeft;
  289. // invoke plugin.onDestroy
  290. this._plugins.forEach(function (plugin) {
  291. plugin.onDestroy();
  292. });
  293. this._plugins.length = 0;
  294. };
  295. Scrollbar.prototype._init = function () {
  296. var _this = this;
  297. this.update();
  298. // init evet handlers
  299. Object.keys(eventHandlers).forEach(function (prop) {
  300. eventHandlers[prop](_this);
  301. });
  302. // invoke `plugin.onInit`
  303. this._plugins.forEach(function (plugin) {
  304. plugin.onInit();
  305. });
  306. this._render();
  307. };
  308. Scrollbar.prototype._updateDebounced = function () {
  309. this.update();
  310. };
  311. // check whether to propagate monmentum to parent scrollbar
  312. // the following situations are considered as `true`:
  313. // 1. continuous scrolling is enabled (automatically disabled when overscroll is enabled)
  314. // 2. scrollbar reaches one side and is not about to scroll on the other direction
  315. Scrollbar.prototype._shouldPropagateMomentum = function (deltaX, deltaY) {
  316. if (deltaX === void 0) { deltaX = 0; }
  317. if (deltaY === void 0) { deltaY = 0; }
  318. var _a = this, options = _a.options, offset = _a.offset, limit = _a.limit;
  319. if (!options.continuousScrolling)
  320. return false;
  321. // force an update when scrollbar is "unscrollable", see #106
  322. if (limit.x === 0 && limit.y === 0) {
  323. this._updateDebounced();
  324. }
  325. var destX = clamp(deltaX + offset.x, 0, limit.x);
  326. var destY = clamp(deltaY + offset.y, 0, limit.y);
  327. var res = true;
  328. // offsets are not about to change
  329. // `&=` operator is not allowed for boolean types
  330. res = res && (destX === offset.x);
  331. res = res && (destY === offset.y);
  332. // current offsets are on the edge
  333. res = res && (offset.x === limit.x || offset.x === 0 || offset.y === limit.y || offset.y === 0);
  334. return res;
  335. };
  336. Scrollbar.prototype._render = function () {
  337. var _momentum = this._momentum;
  338. if (_momentum.x || _momentum.y) {
  339. var nextX = this._nextTick('x');
  340. var nextY = this._nextTick('y');
  341. _momentum.x = nextX.momentum;
  342. _momentum.y = nextY.momentum;
  343. this.setPosition(nextX.position, nextY.position);
  344. }
  345. var remain = __assign({}, this._momentum);
  346. this._plugins.forEach(function (plugin) {
  347. plugin.onRender(remain);
  348. });
  349. this._renderID = requestAnimationFrame(this._render.bind(this));
  350. };
  351. Scrollbar.prototype._nextTick = function (direction) {
  352. var _a = this, options = _a.options, offset = _a.offset, _momentum = _a._momentum;
  353. var current = offset[direction];
  354. var remain = _momentum[direction];
  355. if (Math.abs(remain) <= 0.1) {
  356. return {
  357. momentum: 0,
  358. position: current + remain,
  359. };
  360. }
  361. var nextMomentum = remain * (1 - options.damping);
  362. if (options.renderByPixels) {
  363. nextMomentum |= 0;
  364. }
  365. return {
  366. momentum: nextMomentum,
  367. position: current + remain - nextMomentum,
  368. };
  369. };
  370. __decorate([
  371. debounce(100, { leading: true })
  372. ], Scrollbar.prototype, "_updateDebounced", null);
  373. return Scrollbar;
  374. }());
  375. export { Scrollbar };
  376. //# sourceMappingURL=scrollbar.js.map