smooth-scrollbar
Version:
Customize scrollbar in modern browsers with smooth scrolling experience.
374 lines • 13 kB
JavaScript
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