-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
refactor!: UrlSearchParams based Hash
#7073
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
18ab1a8
bd37fe2
53977e2
9a4aa80
661f343
80f0973
578887c
2cc15ba
be00422
a3fa7b6
615b59c
919d058
38a0c92
f59809a
85dcd78
9ce289c
9e859e6
f1e70d8
26839d7
cbdca58
8ba995c
411fd7a
ae6ae6e
97cebf2
a794bd0
3e04b9f
068d16b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -383,10 +383,10 @@ describe('hash', () => { | |
| expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy(); | ||
| }); | ||
|
|
||
| test('invalidate hash with slashes encoded as %2F', () => { | ||
| test('validate hash with slashes encoded as %2F', () => { | ||
| window.location.hash = '#10%2F3.00%2F-1.00'; | ||
|
|
||
| expect(hash._isValidHash(hash._getCurrentHash())).toBeFalsy(); | ||
| expect(hash._isValidHash(hash._getCurrentHash())).toBeTruthy(); | ||
| }); | ||
|
|
||
| test('invalidate hash with string values', () => { | ||
|
|
@@ -524,7 +524,7 @@ describe('hash', () => { | |
|
|
||
| }); | ||
|
|
||
| test('hash with URL in other parameter does not change', () => { | ||
| test('hash with URL in other parameter does not change except normalization', () => { | ||
| const hash = createHash('map') | ||
| .addTo(map); | ||
|
|
||
|
|
@@ -533,7 +533,7 @@ describe('hash', () => { | |
| map.setZoom(5); | ||
| map.setCenter([1.0, 2.0]); | ||
|
|
||
| expect(window.location.hash).toBe('#map=5/2/1&returnUrl=https://example.com&filter=a&b='); | ||
| expect(window.location.hash).toBe('#map=5/2/1&returnUrl=https://example.com&filter=a&b'); | ||
|
|
||
| window.location.hash = '#search=foo&map=7/4/2&redirect=/path?query=value'; | ||
| hash._onHashChange(); | ||
|
|
@@ -542,7 +542,7 @@ describe('hash', () => { | |
| expect(map.getCenter().lng).toBe(2); | ||
| }); | ||
|
|
||
| test('hash with URL+path in other parameter does not change', () => { | ||
| test('hash with URL+path in other parameter does not change except for normalization', () => { | ||
| const hash = createHash('map') | ||
| .addTo(map); | ||
|
|
||
|
|
@@ -551,7 +551,7 @@ describe('hash', () => { | |
| map.setZoom(5); | ||
| map.setCenter([1.0, 2.0]); | ||
|
|
||
| expect(window.location.hash).toBe('#map=5/2/1&returnUrl=https://example.com/abcd/ef&filter=a&b='); | ||
| expect(window.location.hash).toBe('#map=5/2/1&returnUrl=https://example.com/abcd/ef&filter=a&b'); | ||
|
|
||
| window.location.hash = '#search=foo&map=7/4/2&redirect=/path?query=value'; | ||
| hash._onHashChange(); | ||
|
|
@@ -601,7 +601,7 @@ describe('hash', () => { | |
|
|
||
| }); | ||
|
|
||
| test('update to hash with empty parameter values is kept as-is', () => { | ||
| test('update to hash with empty parameter are de-normalized', () => { | ||
| const hash = createHash('map') | ||
| .addTo(map); | ||
|
|
||
|
|
@@ -610,7 +610,7 @@ describe('hash', () => { | |
| expect(map.getZoom()).toBe(10); | ||
|
|
||
| map.setZoom(5); | ||
| expect(window.location.hash).toBe('#map=5/3/-1&empty='); | ||
| expect(window.location.hash).toBe('#map=5/3/-1&empty'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. whatwg/url#427 led me down quite a rabbit hole of discussions… As I mentioned earlier, I think we could have leeway to make this change. The fact that our hashes generally conform to the query string syntax is just a convention on our part. I also don’t think we necessarily need to hold the hash behavior to the same standard as a formal public API, since That said, if this is a source of concern nonetheless, anything calling const hash = window.location.hash.slice(1);
const params = new URLSearchParams(hash);
const flags = [...hash.matchAll(/(?<=^|&)(\w+)(?=&|$)/g).map(p => p[1])];
// …
const result = [...params.entries().map(([k, v]) => flags.includes(k) ? k : `${k}=${v}`)].join('&');
return `#${result}`;Kind of cryptic…
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The question is who is relaying on this functionality to work this way, is somewhat a concern I have. The methods in the hash my not be public, setting it as part on map initialization is public and reading the address bar value is public as well. |
||
| }); | ||
|
|
||
| describe('geographic boundary values', () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,40 +65,25 @@ export class Hash { | |
| if (pitch) hash += (`/${Math.round(pitch)}`); | ||
|
|
||
| if (this._hashName) { | ||
| const hashName = this._hashName; | ||
| let found = false; | ||
| const parts = window.location.hash.slice(1).split('&').map(part => { | ||
| const key = part.split('=')[0]; | ||
| if (key === hashName) { | ||
| found = true; | ||
| return `${key}=${hash}`; | ||
| } | ||
| return part; | ||
| }).filter(a => a); | ||
| if (!found) { | ||
| parts.push(`${hashName}=${hash}`); | ||
| } | ||
| return `#${parts.join('&')}`; | ||
| const params = this._getHashParams(); | ||
| params.set(this._hashName, hash); | ||
| return `#${decodeURIComponent(params.toString()).replace(/=&/g, '&').replace(/=$/g, '')}`; | ||
| } | ||
|
|
||
| return `#${hash}`; | ||
| } | ||
|
|
||
| _getHashParams = () => { | ||
| return new URLSearchParams(window.location.hash.replace('#', '')); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If |
||
| }; | ||
|
|
||
| _getCurrentHash = () => { | ||
| // Get the current hash from location, stripped from its number sign | ||
| const hash = window.location.hash.replace('#', ''); | ||
| const params = this._getHashParams(); | ||
| if (this._hashName) { | ||
| // Split the parameter-styled hash into parts and find the value we need | ||
| let keyval; | ||
| hash.split('&').map( | ||
| part => part.split('=') | ||
| ).forEach(part => { | ||
| if (part[0] === this._hashName) { | ||
| keyval = part; | ||
| } | ||
| }); | ||
| return (keyval ? keyval[1] || '' : '').split('/'); | ||
| return (params.get(this._hashName) || '').split('/'); | ||
| } | ||
| // For unnamed hashes, get the first key | ||
| const hash = [...params.keys()][0] ?? ''; | ||
| return hash.split('/'); | ||
| }; | ||
|
|
||
|
|
@@ -121,30 +106,25 @@ export class Hash { | |
| }; | ||
|
|
||
| _updateHashUnthrottled = () => { | ||
| // Replace if already present, else append the updated hash string | ||
| const location = window.location.href.replace(/(#.*)?$/, this.getHashString()); | ||
| window.history.replaceState(window.history.state, null, location); | ||
| }; | ||
|
|
||
| _removeHash = () => { | ||
| const currentHash = this._getCurrentHash(); | ||
| if (currentHash.length === 0) { | ||
| return; | ||
| } | ||
| const baseHash = currentHash.join('/'); | ||
| let targetHash = baseHash; | ||
| if (targetHash.split('&').length > 0) { | ||
| targetHash = targetHash.split('&')[0]; // #3/1/2&foo=bar -> #3/1/2 | ||
| } | ||
| const params = this._getHashParams(); | ||
|
|
||
| if (this._hashName) { | ||
| targetHash = `${this._hashName}=${baseHash}`; | ||
| } | ||
| let replaceString = window.location.hash.replace(targetHash, ''); | ||
| if (replaceString.startsWith('#&')) { | ||
| replaceString = replaceString.slice(0, 1) + replaceString.slice(2); | ||
| } else if (replaceString === '#') { | ||
| replaceString = ''; | ||
| params.delete(this._hashName); | ||
| } else { | ||
| // For unnamed hash (#zoom/lat/lng&other=params), remove first entry | ||
| const keys = Array.from(params.keys()); | ||
| if (keys.length > 0) { | ||
| params.delete(keys[0]); | ||
| } | ||
| } | ||
|
|
||
| const newHash = decodeURIComponent(params.toString()).replace(/=&/g, '&').replace(/=$/g, ''); | ||
| const replaceString = newHash ? `#${newHash}` : ''; | ||
| let location = window.location.href.replace(/(#.+)?$/, replaceString); | ||
| location = location.replace('&&', '&'); | ||
| window.history.replaceState(window.history.state, null, location); | ||
|
|
@@ -155,8 +135,8 @@ export class Hash { | |
| */ | ||
| _updateHash: () => ReturnType<typeof setTimeout> = throttle(this._updateHashUnthrottled, 30 * 1000 / 100); | ||
|
|
||
| _isValidHash(hash: number[]) { | ||
| if (hash.length < 3 || hash.some(isNaN)) { | ||
| _isValidHash(hash: string[]) { | ||
| if (hash.length < 3 || hash.some(h => isNaN(+h))) { | ||
| return false; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d characterize this as a routine bug fix rather than a backwards-incompatible change. I don’t think anyone would be expecting the
%2Fto persist, and we continue to recognize it anyways.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A case where this is needed for example is if you have a link to a file or location as part of the query parameters, then you can't use "/" but you have to use the encoded way. I'm not sure this answers any question, but I wanted to give a use case.
If this is kept the same and doesn't break, that's great.