Files
30-seconds-of-code/node_modules/scroll-behavior/lib/index.js
2019-08-20 15:52:05 +02:00

328 lines
12 KiB
JavaScript

'use strict';
exports.__esModule = true;
var _off = require('dom-helpers/events/off');
var _off2 = _interopRequireDefault(_off);
var _on = require('dom-helpers/events/on');
var _on2 = _interopRequireDefault(_on);
var _scrollLeft = require('dom-helpers/query/scrollLeft');
var _scrollLeft2 = _interopRequireDefault(_scrollLeft);
var _scrollTop = require('dom-helpers/query/scrollTop');
var _scrollTop2 = _interopRequireDefault(_scrollTop);
var _requestAnimationFrame = require('dom-helpers/util/requestAnimationFrame');
var _requestAnimationFrame2 = _interopRequireDefault(_requestAnimationFrame);
var _invariant = require('invariant');
var _invariant2 = _interopRequireDefault(_invariant);
var _utils = require('./utils');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /* eslint-disable no-underscore-dangle */
// Try at most this many times to scroll, to avoid getting stuck.
var MAX_SCROLL_ATTEMPTS = 2;
var ScrollBehavior = function () {
function ScrollBehavior(_ref) {
var _this = this;
var addTransitionHook = _ref.addTransitionHook,
stateStorage = _ref.stateStorage,
getCurrentLocation = _ref.getCurrentLocation,
shouldUpdateScroll = _ref.shouldUpdateScroll;
_classCallCheck(this, ScrollBehavior);
this._restoreScrollRestoration = function () {
/* istanbul ignore if: not supported by any browsers on Travis */
if (_this._oldScrollRestoration) {
try {
window.history.scrollRestoration = _this._oldScrollRestoration;
} catch (e) {
/* silence */
}
}
};
this._onWindowScroll = function () {
// It's possible that this scroll operation was triggered by what will be a
// `POP` transition. Instead of updating the saved location immediately, we
// have to enqueue the update, then potentially cancel it if we observe a
// location update.
if (!_this._saveWindowPositionHandle) {
_this._saveWindowPositionHandle = (0, _requestAnimationFrame2.default)(_this._saveWindowPosition);
}
if (_this._windowScrollTarget) {
var _windowScrollTarget = _this._windowScrollTarget,
xTarget = _windowScrollTarget[0],
yTarget = _windowScrollTarget[1];
var x = (0, _scrollLeft2.default)(window);
var y = (0, _scrollTop2.default)(window);
if (x === xTarget && y === yTarget) {
_this._windowScrollTarget = null;
_this._cancelCheckWindowScroll();
}
}
};
this._saveWindowPosition = function () {
_this._saveWindowPositionHandle = null;
_this._savePosition(null, window);
};
this._checkWindowScrollPosition = function () {
_this._checkWindowScrollHandle = null;
// We can only get here if scrollTarget is set. Every code path that unsets
// scroll target also cancels the handle to avoid calling this handler.
// Still, check anyway just in case.
/* istanbul ignore if: paranoid guard */
if (!_this._windowScrollTarget) {
return;
}
_this.scrollToTarget(window, _this._windowScrollTarget);
++_this._numWindowScrollAttempts;
/* istanbul ignore if: paranoid guard */
if (_this._numWindowScrollAttempts >= MAX_SCROLL_ATTEMPTS) {
_this._windowScrollTarget = null;
return;
}
_this._checkWindowScrollHandle = (0, _requestAnimationFrame2.default)(_this._checkWindowScrollPosition);
};
this._stateStorage = stateStorage;
this._getCurrentLocation = getCurrentLocation;
this._shouldUpdateScroll = shouldUpdateScroll;
// This helps avoid some jankiness in fighting against the browser's
// default scroll behavior on `POP` transitions.
/* istanbul ignore else: Travis browsers all support this */
if ('scrollRestoration' in window.history &&
// Unfortunately, Safari on iOS freezes for 2-6s after the user swipes to
// navigate through history with scrollRestoration being 'manual', so we
// need to detect this browser and exclude it from the following code
// until this bug is fixed by Apple.
!(0, _utils.isMobileSafari)()) {
this._oldScrollRestoration = window.history.scrollRestoration;
try {
window.history.scrollRestoration = 'manual';
// Scroll restoration persists across page reloads. We want to reset
// this to the original value, so that we can let the browser handle
// restoring the initial scroll position on server-rendered pages.
(0, _on2.default)(window, 'beforeunload', this._restoreScrollRestoration);
} catch (e) {
this._oldScrollRestoration = null;
}
} else {
this._oldScrollRestoration = null;
}
this._saveWindowPositionHandle = null;
this._checkWindowScrollHandle = null;
this._windowScrollTarget = null;
this._numWindowScrollAttempts = 0;
this._scrollElements = {};
// We have to listen to each window scroll update rather than to just
// location updates, because some browsers will update scroll position
// before emitting the location change.
(0, _on2.default)(window, 'scroll', this._onWindowScroll);
this._removeTransitionHook = addTransitionHook(function () {
_requestAnimationFrame2.default.cancel(_this._saveWindowPositionHandle);
_this._saveWindowPositionHandle = null;
Object.keys(_this._scrollElements).forEach(function (key) {
var scrollElement = _this._scrollElements[key];
_requestAnimationFrame2.default.cancel(scrollElement.savePositionHandle);
scrollElement.savePositionHandle = null;
// It's fine to save element scroll positions here, though; the browser
// won't modify them.
_this._saveElementPosition(key);
});
});
}
ScrollBehavior.prototype.registerElement = function registerElement(key, element, shouldUpdateScroll, context) {
var _this2 = this;
!!this._scrollElements[key] ? process.env.NODE_ENV !== 'production' ? (0, _invariant2.default)(false, 'ScrollBehavior: There is already an element registered for `%s`.', key) : (0, _invariant2.default)(false) : void 0;
var saveElementPosition = function saveElementPosition() {
_this2._saveElementPosition(key);
};
var scrollElement = {
element: element,
shouldUpdateScroll: shouldUpdateScroll,
savePositionHandle: null,
onScroll: function onScroll() {
if (!scrollElement.savePositionHandle) {
scrollElement.savePositionHandle = (0, _requestAnimationFrame2.default)(saveElementPosition);
}
}
};
this._scrollElements[key] = scrollElement;
(0, _on2.default)(element, 'scroll', scrollElement.onScroll);
this._updateElementScroll(key, null, context);
};
ScrollBehavior.prototype.unregisterElement = function unregisterElement(key) {
!this._scrollElements[key] ? process.env.NODE_ENV !== 'production' ? (0, _invariant2.default)(false, 'ScrollBehavior: There is no element registered for `%s`.', key) : (0, _invariant2.default)(false) : void 0;
var _scrollElements$key = this._scrollElements[key],
element = _scrollElements$key.element,
onScroll = _scrollElements$key.onScroll,
savePositionHandle = _scrollElements$key.savePositionHandle;
(0, _off2.default)(element, 'scroll', onScroll);
_requestAnimationFrame2.default.cancel(savePositionHandle);
delete this._scrollElements[key];
};
ScrollBehavior.prototype.updateScroll = function updateScroll(prevContext, context) {
var _this3 = this;
this._updateWindowScroll(prevContext, context);
Object.keys(this._scrollElements).forEach(function (key) {
_this3._updateElementScroll(key, prevContext, context);
});
};
ScrollBehavior.prototype.stop = function stop() {
this._restoreScrollRestoration();
(0, _off2.default)(window, 'scroll', this._onWindowScroll);
this._cancelCheckWindowScroll();
this._removeTransitionHook();
};
ScrollBehavior.prototype._cancelCheckWindowScroll = function _cancelCheckWindowScroll() {
_requestAnimationFrame2.default.cancel(this._checkWindowScrollHandle);
this._checkWindowScrollHandle = null;
};
ScrollBehavior.prototype._saveElementPosition = function _saveElementPosition(key) {
var scrollElement = this._scrollElements[key];
scrollElement.savePositionHandle = null;
this._savePosition(key, scrollElement.element);
};
ScrollBehavior.prototype._savePosition = function _savePosition(key, element) {
this._stateStorage.save(this._getCurrentLocation(), key, [(0, _scrollLeft2.default)(element), (0, _scrollTop2.default)(element)]);
};
ScrollBehavior.prototype._updateWindowScroll = function _updateWindowScroll(prevContext, context) {
// Whatever we were doing before isn't relevant any more.
this._cancelCheckWindowScroll();
this._windowScrollTarget = this._getScrollTarget(null, this._shouldUpdateScroll, prevContext, context);
// Updating the window scroll position is really flaky. Just trying to
// scroll it isn't enough. Instead, try to scroll a few times until it
// works.
this._numWindowScrollAttempts = 0;
this._checkWindowScrollPosition();
};
ScrollBehavior.prototype._updateElementScroll = function _updateElementScroll(key, prevContext, context) {
var _scrollElements$key2 = this._scrollElements[key],
element = _scrollElements$key2.element,
shouldUpdateScroll = _scrollElements$key2.shouldUpdateScroll;
var scrollTarget = this._getScrollTarget(key, shouldUpdateScroll, prevContext, context);
if (!scrollTarget) {
return;
}
// Unlike with the window, there shouldn't be any flakiness to deal with
// here.
this.scrollToTarget(element, scrollTarget);
};
ScrollBehavior.prototype._getDefaultScrollTarget = function _getDefaultScrollTarget(location) {
var hash = location.hash;
if (hash && hash !== '#') {
return hash.charAt(0) === '#' ? hash.slice(1) : hash;
}
return [0, 0];
};
ScrollBehavior.prototype._getScrollTarget = function _getScrollTarget(key, shouldUpdateScroll, prevContext, context) {
var scrollTarget = shouldUpdateScroll ? shouldUpdateScroll.call(this, prevContext, context) : true;
if (!scrollTarget || Array.isArray(scrollTarget) || typeof scrollTarget === 'string') {
return scrollTarget;
}
var location = this._getCurrentLocation();
return this._getSavedScrollTarget(key, location) || this._getDefaultScrollTarget(location);
};
ScrollBehavior.prototype._getSavedScrollTarget = function _getSavedScrollTarget(key, location) {
if (location.action === 'PUSH') {
return null;
}
return this._stateStorage.read(location, key);
};
ScrollBehavior.prototype.scrollToTarget = function scrollToTarget(element, target) {
if (typeof target === 'string') {
var targetElement = document.getElementById(target) || document.getElementsByName(target)[0];
if (targetElement) {
targetElement.scrollIntoView();
return;
}
// Fallback to scrolling to top when target fragment doesn't exist.
target = [0, 0]; // eslint-disable-line no-param-reassign
}
var _target = target,
left = _target[0],
top = _target[1];
(0, _scrollLeft2.default)(element, left);
(0, _scrollTop2.default)(element, top);
};
return ScrollBehavior;
}();
exports.default = ScrollBehavior;
module.exports = exports['default'];