UNPKG

smooth-scrollbar

Version:

Customize scrollbar in modern browsers with smooth scrolling experience.

374 lines 13 kB
import { __assign, __decorate } from "tslib"; import { clamp } from './utils'; import { Options } from './options'; import { setStyle, clearEventsOn, } from './utils/'; import { debounce, } from './decorators/'; import { TrackController, } from './track/'; import { getSize, update, isVisible, } from './geometry/'; import { scrollTo, setPosition, scrollIntoView, } from './scrolling/'; import { initPlugins, } from './plugin'; import * as eventHandlers from './events/'; // DO NOT use WeakMap here // .getAll() methods requires `scrollbarMap.values()` export var scrollbarMap = new Map(); var Scrollbar = /** @class */ (function () { function Scrollbar(containerEl, options) { var _this = this; /** * Current scrolling offsets */ this.offset = { x: 0, y: 0, }; /** * Max-allowed scrolling offsets */ this.limit = { x: Infinity, y: Infinity, }; /** * Container bounding rect */ this.bounding = { top: 0, right: 0, bottom: 0, left: 0, }; // private _observer: ResizeObserver; this._plugins = []; this._momentum = { x: 0, y: 0 }; this._listeners = new Set(); this.containerEl = containerEl; var contentEl = this.contentEl = document.createElement('div'); this.options = new Options(options); // mark as a scroll element containerEl.setAttribute('data-scrollbar', 'true'); // make container focusable containerEl.setAttribute('tabindex', '-1'); setStyle(containerEl, { overflow: 'hidden', outline: 'none', }); // enable touch event capturing in IE, see: // https://github.com/idiotWu/smooth-scrollbar/issues/39 if (window.navigator.msPointerEnabled) { containerEl.style.msTouchAction = 'none'; } // mount content contentEl.className = 'scroll-content'; Array.from(containerEl.childNodes).forEach(function (node) { contentEl.appendChild(node); }); containerEl.appendChild(contentEl); // attach track this.track = new TrackController(this); // initial measuring this.size = this.getSize(); // init plugins this._plugins = initPlugins(this, this.options.plugins); // preserve scroll offset var scrollLeft = containerEl.scrollLeft, scrollTop = containerEl.scrollTop; containerEl.scrollLeft = containerEl.scrollTop = 0; this.setPosition(scrollLeft, scrollTop, { withoutCallbacks: true, }); // FIXME: update typescript var ResizeObserver = window.ResizeObserver; // observe if (typeof ResizeObserver === 'function') { this._observer = new ResizeObserver(function () { _this.update(); }); this._observer.observe(contentEl); } scrollbarMap.set(containerEl, this); // wait for DOM ready requestAnimationFrame(function () { _this._init(); }); } Object.defineProperty(Scrollbar.prototype, "parent", { /** * Parent scrollbar */ get: function () { var elem = this.containerEl.parentElement; while (elem) { var parentScrollbar = scrollbarMap.get(elem); if (parentScrollbar) { return parentScrollbar; } elem = elem.parentElement; } return null; }, enumerable: true, configurable: true }); Object.defineProperty(Scrollbar.prototype, "scrollTop", { /** * Gets or sets `scrollbar.offset.y` */ get: function () { return this.offset.y; }, set: function (y) { this.setPosition(this.scrollLeft, y); }, enumerable: true, configurable: true }); Object.defineProperty(Scrollbar.prototype, "scrollLeft", { /** * Gets or sets `scrollbar.offset.x` */ get: function () { return this.offset.x; }, set: function (x) { this.setPosition(x, this.scrollTop); }, enumerable: true, configurable: true }); /** * Returns the size of the scrollbar container element * and the content wrapper element */ Scrollbar.prototype.getSize = function () { return getSize(this); }; /** * Forces scrollbar to update geometry infomation. * * By default, scrollbars are automatically updated with `100ms` debounce (or `MutationObserver` fires). * You can call this method to force an update when you modified contents */ Scrollbar.prototype.update = function () { update(this); this._plugins.forEach(function (plugin) { plugin.onUpdate(); }); }; /** * Checks if an element is visible in the current view area */ Scrollbar.prototype.isVisible = function (elem) { return isVisible(this, elem); }; /** * Sets the scrollbar to the given offset without easing */ Scrollbar.prototype.setPosition = function (x, y, options) { var _this = this; if (x === void 0) { x = this.offset.x; } if (y === void 0) { y = this.offset.y; } if (options === void 0) { options = {}; } var status = setPosition(this, x, y); if (!status || options.withoutCallbacks) { return; } this._listeners.forEach(function (fn) { fn.call(_this, status); }); }; /** * Scrolls to given position with easing function */ Scrollbar.prototype.scrollTo = function (x, y, duration, options) { if (x === void 0) { x = this.offset.x; } if (y === void 0) { y = this.offset.y; } if (duration === void 0) { duration = 0; } if (options === void 0) { options = {}; } scrollTo(this, x, y, duration, options); }; /** * Scrolls the target element into visible area of scrollbar, * likes the DOM method `element.scrollIntoView(). */ Scrollbar.prototype.scrollIntoView = function (elem, options) { if (options === void 0) { options = {}; } scrollIntoView(this, elem, options); }; /** * Adds scrolling listener */ Scrollbar.prototype.addListener = function (fn) { if (typeof fn !== 'function') { throw new TypeError('[smooth-scrollbar] scrolling listener should be a function'); } this._listeners.add(fn); }; /** * Removes listener previously registered with `scrollbar.addListener()` */ Scrollbar.prototype.removeListener = function (fn) { this._listeners.delete(fn); }; /** * Adds momentum and applys delta transformers. */ Scrollbar.prototype.addTransformableMomentum = function (x, y, fromEvent, callback) { this._updateDebounced(); var finalDelta = this._plugins.reduce(function (delta, plugin) { return plugin.transformDelta(delta, fromEvent) || delta; }, { x: x, y: y }); var willScroll = !this._shouldPropagateMomentum(finalDelta.x, finalDelta.y); if (willScroll) { this.addMomentum(finalDelta.x, finalDelta.y); } if (callback) { callback.call(this, willScroll); } }; /** * Increases scrollbar's momentum */ Scrollbar.prototype.addMomentum = function (x, y) { this.setMomentum(this._momentum.x + x, this._momentum.y + y); }; /** * Sets scrollbar's momentum to given value */ Scrollbar.prototype.setMomentum = function (x, y) { if (this.limit.x === 0) { x = 0; } if (this.limit.y === 0) { y = 0; } if (this.options.renderByPixels) { x = Math.round(x); y = Math.round(y); } this._momentum.x = x; this._momentum.y = y; }; /** * Update options for specific plugin * * @param pluginName Name of the plugin * @param [options] An object includes the properties that you want to update */ Scrollbar.prototype.updatePluginOptions = function (pluginName, options) { this._plugins.forEach(function (plugin) { if (plugin.name === pluginName) { Object.assign(plugin.options, options); } }); }; Scrollbar.prototype.destroy = function () { var _a = this, containerEl = _a.containerEl, contentEl = _a.contentEl; clearEventsOn(this); this._listeners.clear(); this.setMomentum(0, 0); cancelAnimationFrame(this._renderID); if (this._observer) { this._observer.disconnect(); } scrollbarMap.delete(this.containerEl); // restore contents var childNodes = Array.from(contentEl.childNodes); while (containerEl.firstChild) { containerEl.removeChild(containerEl.firstChild); } childNodes.forEach(function (el) { containerEl.appendChild(el); }); // reset scroll position setStyle(containerEl, { overflow: '', }); containerEl.scrollTop = this.scrollTop; containerEl.scrollLeft = this.scrollLeft; // invoke plugin.onDestroy this._plugins.forEach(function (plugin) { plugin.onDestroy(); }); this._plugins.length = 0; }; Scrollbar.prototype._init = function () { var _this = this; this.update(); // init evet handlers Object.keys(eventHandlers).forEach(function (prop) { eventHandlers[prop](_this); }); // invoke `plugin.onInit` this._plugins.forEach(function (plugin) { plugin.onInit(); }); this._render(); }; Scrollbar.prototype._updateDebounced = function () { this.update(); }; // check whether to propagate monmentum to parent scrollbar // the following situations are considered as `true`: // 1. continuous scrolling is enabled (automatically disabled when overscroll is enabled) // 2. scrollbar reaches one side and is not about to scroll on the other direction Scrollbar.prototype._shouldPropagateMomentum = function (deltaX, deltaY) { if (deltaX === void 0) { deltaX = 0; } if (deltaY === void 0) { deltaY = 0; } var _a = this, options = _a.options, offset = _a.offset, limit = _a.limit; if (!options.continuousScrolling) return false; // force an update when scrollbar is "unscrollable", see #106 if (limit.x === 0 && limit.y === 0) { this._updateDebounced(); } var destX = clamp(deltaX + offset.x, 0, limit.x); var destY = clamp(deltaY + offset.y, 0, limit.y); var res = true; // offsets are not about to change // `&=` operator is not allowed for boolean types res = res && (destX === offset.x); res = res && (destY === offset.y); // current offsets are on the edge res = res && (offset.x === limit.x || offset.x === 0 || offset.y === limit.y || offset.y === 0); return res; }; Scrollbar.prototype._render = function () { var _momentum = this._momentum; if (_momentum.x || _momentum.y) { var nextX = this._nextTick('x'); var nextY = this._nextTick('y'); _momentum.x = nextX.momentum; _momentum.y = nextY.momentum; this.setPosition(nextX.position, nextY.position); } var remain = __assign({}, this._momentum); this._plugins.forEach(function (plugin) { plugin.onRender(remain); }); this._renderID = requestAnimationFrame(this._render.bind(this)); }; Scrollbar.prototype._nextTick = function (direction) { var _a = this, options = _a.options, offset = _a.offset, _momentum = _a._momentum; var current = offset[direction]; var remain = _momentum[direction]; if (Math.abs(remain) <= 0.1) { return { momentum: 0, position: current + remain, }; } var nextMomentum = remain * (1 - options.damping); if (options.renderByPixels) { nextMomentum |= 0; } return { momentum: nextMomentum, position: current + remain - nextMomentum, }; }; __decorate([ debounce(100, true) ], Scrollbar.prototype, "_updateDebounced", null); return Scrollbar; }()); export { Scrollbar }; //# sourceMappingURL=scrollbar.js.map