'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'];