328 lines
12 KiB
JavaScript
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']; |