instantsearch.js
Version:
InstantSearch.js is a JavaScript library for building performant and instant search experiences with Algolia.
289 lines (238 loc) • 9.89 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = historyRouter;
var _qs = _interopRequireDefault(require("qs"));
var _index = require("../utils/index.js");
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"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var setWindowTitle = function setWindowTitle(title) {
if (title) {
// This function is only executed on browsers so we can disable this check.
// eslint-disable-next-line no-restricted-globals
window.document.title = title;
}
};
var BrowserHistory = /*#__PURE__*/function () {
/**
* Initializes a new storage provider that syncs the search state to the URL
* using web APIs (`window.location.pushState` and `onpopstate` event).
*/
function BrowserHistory(_ref) {
var _this = this;
var windowTitle = _ref.windowTitle,
_ref$writeDelay = _ref.writeDelay,
writeDelay = _ref$writeDelay === void 0 ? 400 : _ref$writeDelay,
createURL = _ref.createURL,
parseURL = _ref.parseURL,
getLocation = _ref.getLocation;
_classCallCheck(this, BrowserHistory);
_defineProperty(this, "windowTitle", void 0);
_defineProperty(this, "writeDelay", void 0);
_defineProperty(this, "_createURL", void 0);
_defineProperty(this, "parseURL", void 0);
_defineProperty(this, "getLocation", void 0);
_defineProperty(this, "writeTimer", void 0);
_defineProperty(this, "inPopState", false);
_defineProperty(this, "isDisposed", false);
_defineProperty(this, "latestAcknowledgedHistory", 0);
this.windowTitle = windowTitle;
this.writeTimer = undefined;
this.writeDelay = writeDelay;
this._createURL = createURL;
this.parseURL = parseURL;
this.getLocation = getLocation;
(0, _index.safelyRunOnBrowser)(function (_ref2) {
var window = _ref2.window;
var title = _this.windowTitle && _this.windowTitle(_this.read());
setWindowTitle(title);
_this.latestAcknowledgedHistory = window.history.length;
});
}
/**
* Reads the URL and returns a syncable UI search state.
*/
_createClass(BrowserHistory, [{
key: "read",
value: function read() {
return this.parseURL({
qsModule: _qs.default,
location: this.getLocation()
});
}
/**
* Pushes a search state into the URL.
*/
}, {
key: "write",
value: function write(routeState) {
var _this2 = this;
(0, _index.safelyRunOnBrowser)(function (_ref3) {
var window = _ref3.window;
var url = _this2.createURL(routeState);
var title = _this2.windowTitle && _this2.windowTitle(routeState);
if (_this2.writeTimer) {
clearTimeout(_this2.writeTimer);
}
_this2.writeTimer = setTimeout(function () {
setWindowTitle(title);
if (_this2.shouldWrite(url)) {
window.history.pushState(routeState, title || '', url);
_this2.latestAcknowledgedHistory = window.history.length;
}
_this2.inPopState = false;
_this2.writeTimer = undefined;
}, _this2.writeDelay);
});
}
/**
* Sets a callback on the `onpopstate` event of the history API of the current page.
* It enables the URL sync to keep track of the changes.
*/
}, {
key: "onUpdate",
value: function onUpdate(callback) {
var _this3 = this;
this._onPopState = function (event) {
if (_this3.writeTimer) {
clearTimeout(_this3.writeTimer);
_this3.writeTimer = undefined;
}
_this3.inPopState = true;
var routeState = event.state; // At initial load, the state is read from the URL without update.
// Therefore the state object is not available.
// In this case, we fallback and read the URL.
if (!routeState) {
callback(_this3.read());
} else {
callback(routeState);
}
};
(0, _index.safelyRunOnBrowser)(function (_ref4) {
var window = _ref4.window;
window.addEventListener('popstate', _this3._onPopState);
});
}
/**
* Creates a complete URL from a given syncable UI state.
*
* It always generates the full URL, not a relative one.
* This allows to handle cases like using a <base href>.
* See: https://github.com/algolia/instantsearch.js/issues/790
*/
}, {
key: "createURL",
value: function createURL(routeState) {
return this._createURL({
qsModule: _qs.default,
routeState: routeState,
location: this.getLocation()
});
}
/**
* Removes the event listener and cleans up the URL.
*/
}, {
key: "dispose",
value: function dispose() {
var _this4 = this;
this.isDisposed = true;
(0, _index.safelyRunOnBrowser)(function (_ref5) {
var window = _ref5.window;
if (_this4._onPopState) {
window.removeEventListener('popstate', _this4._onPopState);
}
});
if (this.writeTimer) {
clearTimeout(this.writeTimer);
}
this.write({});
}
}, {
key: "shouldWrite",
value: function shouldWrite(url) {
var _this5 = this;
return (0, _index.safelyRunOnBrowser)(function (_ref6) {
var window = _ref6.window;
// We do want to `pushState` if:
// - the router is not disposed, IS.js needs to update the URL
// OR
// - the last write was from InstantSearch.js
// (unlike a SPA, where it would have last written)
var lastPushWasByISAfterDispose = !(_this5.isDisposed && _this5.latestAcknowledgedHistory !== window.history.length);
return (// When the last state change was through popstate, the IS.js state changes,
// but that should not write the URL.
!_this5.inPopState && // When the previous pushState after dispose was by IS.js, we want to write the URL.
lastPushWasByISAfterDispose && // When the URL is the same as the current one, we do not want to write it.
url !== window.location.href
);
});
}
}]);
return BrowserHistory;
}();
function historyRouter() {
var _ref7 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
_ref7$createURL = _ref7.createURL,
createURL = _ref7$createURL === void 0 ? function (_ref8) {
var qsModule = _ref8.qsModule,
routeState = _ref8.routeState,
location = _ref8.location;
var protocol = location.protocol,
hostname = location.hostname,
_location$port = location.port,
port = _location$port === void 0 ? '' : _location$port,
pathname = location.pathname,
hash = location.hash;
var queryString = qsModule.stringify(routeState);
var portWithPrefix = port === '' ? '' : ":".concat(port); // IE <= 11 has no proper `location.origin` so we cannot rely on it.
// IE <= 11 has no proper `location.origin` so we cannot rely on it.
if (!queryString) {
return "".concat(protocol, "//").concat(hostname).concat(portWithPrefix).concat(pathname).concat(hash);
}
return "".concat(protocol, "//").concat(hostname).concat(portWithPrefix).concat(pathname, "?").concat(queryString).concat(hash);
} : _ref7$createURL,
_ref7$parseURL = _ref7.parseURL,
parseURL = _ref7$parseURL === void 0 ? function (_ref9) {
var qsModule = _ref9.qsModule,
location = _ref9.location;
// `qs` by default converts arrays with more than 20 items to an object.
// We want to avoid this because the data structure manipulated can therefore vary.
// Setting the limit to `100` seems a good number because the engine's default is 100
// (it can go up to 1000 but it is very unlikely to select more than 100 items in the UI).
//
// Using an `arrayLimit` of `n` allows `n + 1` items.
//
// See:
// - https://github.com/ljharb/qs#parsing-arrays
// - https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/
return qsModule.parse(location.search.slice(1), {
arrayLimit: 99
});
} : _ref7$parseURL,
_ref7$writeDelay = _ref7.writeDelay,
writeDelay = _ref7$writeDelay === void 0 ? 400 : _ref7$writeDelay,
windowTitle = _ref7.windowTitle,
_ref7$getLocation = _ref7.getLocation,
getLocation = _ref7$getLocation === void 0 ? function () {
return (0, _index.safelyRunOnBrowser)(function (_ref10) {
var window = _ref10.window;
return window.location;
}, {
fallback: function fallback() {
throw new Error('You need to provide `getLocation` to the `history` router in environments where `window` does not exist.');
}
});
} : _ref7$getLocation;
return new BrowserHistory({
createURL: createURL,
parseURL: parseURL,
writeDelay: writeDelay,
windowTitle: windowTitle,
getLocation: getLocation
});
}
;