-
Notifications
You must be signed in to change notification settings - Fork 516
/
url-sync.js
255 lines (225 loc) · 7.73 KB
/
url-sync.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
import algoliasearchHelper from 'algoliasearch-helper';
import urlHelper from 'algoliasearch-helper/src/url';
import isEqual from 'lodash/isEqual';
const AlgoliaSearchHelper = algoliasearchHelper.AlgoliaSearchHelper;
/**
* @typedef {object} UrlUtil
* @property {string} character the character used in the url
* @property {function} onpopstate add an event listener for the URL change
* @property {function} pushState creates a new entry in the browser history
* @property {function} readUrl reads the query string of the parameters
*/
/**
* Handles the legacy browsers
* @type {UrlUtil}
*/
const hashUrlUtils = {
ignoreNextPopState: false,
character: '#',
onpopstate(cb) {
this._onHashChange = hash => {
if (this.ignoreNextPopState) {
this.ignoreNextPopState = false;
return;
}
cb(hash);
};
window.addEventListener('hashchange', this._onHashChange);
},
pushState(qs) {
// hash change or location assign does trigger an hashchange event
// so every time we change it manually, we inform the code
// to ignore the next hashchange event
// see https://github.com/algolia/instantsearch.js/issues/2012
this.ignoreNextPopState = true;
window.location.assign(getFullURL(this.createURL(qs)));
},
createURL(qs) {
return window.location.search + this.character + qs;
},
readUrl() {
return window.location.hash.slice(1);
},
dispose() {
window.removeEventListener('hashchange', this._onHashChange);
window.location.assign(getFullURL(''));
},
};
/**
* Handles the modern API
* @type {UrlUtil}
*/
const modernUrlUtils = {
character: '?',
onpopstate(cb) {
this._onPopState = (...args) => cb(...args);
window.addEventListener('popstate', this._onPopState);
},
pushState(qs, { getHistoryState }) {
window.history.pushState(
getHistoryState(),
'',
getFullURL(this.createURL(qs))
);
},
createURL(qs) {
return this.character + qs + document.location.hash;
},
readUrl() {
return window.location.search.slice(1);
},
dispose() {
window.removeEventListener('popstate', this._onPopState);
window.history.pushState(null, null, getFullURL(''));
},
};
// we always push the full url to the url bar. Not a relative one.
// So that we handle cases like using a <base href>, see
// https://github.com/algolia/instantsearch.js/issues/790 for the original issue
function getFullURL(relative) {
return getLocationOrigin() + window.location.pathname + relative;
}
// IE <= 11 has no location.origin or buggy
function getLocationOrigin() {
// eslint-disable-next-line max-len
return `${window.location.protocol}//${window.location.hostname}${
window.location.port ? `:${window.location.port}` : ''
}`;
}
// see InstantSearch.js file for urlSync options
class URLSync {
constructor(urlUtils, options) {
this.urlUtils = urlUtils;
this.originalConfig = null;
this.mapping = options.mapping || {};
this.getHistoryState = options.getHistoryState || (() => null);
this.threshold = options.threshold || 700;
this.trackedParameters = options.trackedParameters || [
'query',
'attribute:*',
'index',
'page',
'hitsPerPage',
];
this.firstRender = true;
this.searchParametersFromUrl = AlgoliaSearchHelper.getConfigurationFromQueryString(
this.urlUtils.readUrl(),
{ mapping: this.mapping }
);
}
init({ state }) {
this.initState = state;
}
getConfiguration(currentConfiguration) {
// we need to create a REAL helper to then get its state. Because some parameters
// like hierarchicalFacet.rootPath are then triggering a default refinement that would
// be not present if it was not going trough the SearchParameters constructor
this.originalConfig = algoliasearchHelper(
{},
currentConfiguration.index,
currentConfiguration
).state;
return this.searchParametersFromUrl;
}
render({ helper, state }) {
if (this.firstRender) {
this.firstRender = false;
this.onHistoryChange(this.onPopState.bind(this, helper));
helper.on('change', s => this.renderURLFromState(s));
const initStateQs = this.getQueryString(this.initState);
const stateQs = this.getQueryString(state);
if (initStateQs !== stateQs) {
// force update the URL, if the state has changed since the initial URL read
// We do this in order to make a URL update when there is search function
// that prevent the search of the initial rendering
// See: https://github.com/algolia/instantsearch.js/issues/2523#issuecomment-339356157
this.renderURLFromState(state);
}
}
}
dispose({ helper }) {
helper.removeListener('change', this.renderURLFromState);
this.urlUtils.dispose();
}
onPopState(helper, fullState) {
clearTimeout(this.urlUpdateTimeout);
// compare with helper.state
const partialHelperState = helper.getState(this.trackedParameters);
const fullHelperState = {
...this.originalConfig,
...partialHelperState,
};
if (isEqual(fullHelperState, fullState)) return;
helper.overrideStateWithoutTriggeringChangeEvent(fullState).search();
}
renderURLFromState(state) {
const qs = this.getQueryString(state);
clearTimeout(this.urlUpdateTimeout);
this.urlUpdateTimeout = setTimeout(() => {
this.urlUtils.pushState(qs, { getHistoryState: this.getHistoryState });
}, this.threshold);
}
getQueryString(state) {
const currentQueryString = this.urlUtils.readUrl();
const foreignConfig = AlgoliaSearchHelper.getForeignConfigurationInQueryString(
currentQueryString,
{ mapping: this.mapping }
);
return urlHelper.getQueryStringFromState(
state.filter(this.trackedParameters),
{
moreAttributes: foreignConfig,
mapping: this.mapping,
safe: true,
}
);
}
// External APIs
createURL(state, { absolute }) {
const filteredState = state.filter(this.trackedParameters);
const relative = this.urlUtils.createURL(
algoliasearchHelper.url.getQueryStringFromState(filteredState, {
mapping: this.mapping,
})
);
return absolute ? getFullURL(relative) : relative;
}
onHistoryChange(fn) {
this.urlUtils.onpopstate(() => {
const qs = this.urlUtils.readUrl();
const partialState = AlgoliaSearchHelper.getConfigurationFromQueryString(
qs,
{ mapping: this.mapping }
);
const fullState = {
...this.originalConfig,
...partialState,
};
fn(fullState);
});
}
}
/**
* Instantiate a url sync widget. This widget let you synchronize the search
* parameters with the URL. It can operate with legacy API and hash or it can use
* the modern history API. By default, it will use the modern API, but if you are
* looking for compatibility with IE8 and IE9, then you should set 'useHash' to
* true.
* @param {object} options all the parameters to configure the URL synchronization. It
* may contain the following keys :
* - threshold:number time in ms after which a new state is created in the browser
* history. The default value is 700.
* - trackedParameters:string[] parameters that will be synchronized in the
* URL. By default, it will track the query, all the refinable attributes (facets and numeric
* filters), the index and the page.
* - useHash:boolean if set to true, the url will be hash based. Otherwise,
* it'll use the query parameters using the modern history API.
* @return {object} the widget instance
*/
function urlSync(options = {}) {
const useHash = options.useHash || false;
const customUrlUtils = options.urlUtils;
const urlUtils = customUrlUtils || (useHash ? hashUrlUtils : modernUrlUtils);
return new URLSync(urlUtils, options);
}
export default urlSync;