From d70a3235fbfd4d79eba0e8876659a6a017eedd32 Mon Sep 17 00:00:00 2001 From: Sebastien Corbin Date: Tue, 6 Aug 2019 12:21:47 +0200 Subject: [PATCH] Configurable hash (#8596) --- src/ui/hash.js | 41 ++++++++++++-- src/ui/map.js | 6 ++- test/unit/ui/hash.test.js | 109 +++++++++++++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/ui/hash.js b/src/ui/hash.js index 938accae4fa..086b0286bcd 100644 --- a/src/ui/hash.js +++ b/src/ui/hash.js @@ -15,9 +15,12 @@ import type Map from './map'; class Hash { _map: Map; _updateHash: () => ?TimeoutID; + _hashName: ?string; - constructor() { + constructor(hashName: string | boolean) { + this._hashName = (typeof hashName === 'string' && hashName) || null; bindAll([ + '_getCurrentHash', '_onHashChange', '_updateHash' ], this); @@ -67,18 +70,46 @@ class Hash { if (mapFeedback) { // new map feedback site has some constraints that don't allow // us to use the same hash format as we do for the Map hash option. - hash += `#/${lng}/${lat}/${zoom}`; + hash += `/${lng}/${lat}/${zoom}`; } else { - hash += `#${zoom}/${lat}/${lng}`; + hash += `${zoom}/${lat}/${lng}`; } if (bearing || pitch) hash += (`/${Math.round(bearing * 10) / 10}`); if (pitch) hash += (`/${Math.round(pitch)}`); - return hash; + + if (this._hashName && !mapFeedback) { + let found = false; + const parts = window.location.hash.slice(1).split('&').map(part => { + const key = part.split('=')[0]; + if (key === this._hashName) { + found = true; + return `${key}=${hash}`; + } + return part; + }).filter(a => a); + if (!found) { + parts.push(`${this._hashName || ''}=${hash}`); + } + return `#${parts.join('&')}`; + } + + return `#${hash}`; + } + + _getCurrentHash() { + const hash = window.location.hash.replace('#', ''); + if (this._hashName) { + const keyval = hash.split('&').map( + part => part.split('=') + ).find(part => part[0] === this._hashName); + return (keyval ? keyval[1] || '' : '').split('/'); + } + return hash.split('/'); } _onHashChange() { - const loc = window.location.hash.replace('#', '').split('/'); + const loc = this._getCurrentHash(); if (loc.length >= 3) { this._map.jumpTo({ center: [+loc[2], +loc[1]], diff --git a/src/ui/map.js b/src/ui/map.js index fc0f4e176dc..5fd65adbe3f 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -64,7 +64,7 @@ type IControl = { /* eslint-enable no-use-before-define */ type MapOptions = { - hash?: boolean, + hash?: boolean | string, interactive?: boolean, container: HTMLElement | string, bearingSnap?: number, @@ -173,6 +173,8 @@ const defaultOptions = { * * @param {boolean} [options.hash=false] If `true`, the map's position (zoom, center latitude, center longitude, bearing, and pitch) will be synced with the hash fragment of the page's URL. * For example, `http://path/to/my/page.html#2.59/39.26/53.07/-24.1/60`. + * If a string is provided, it will used as the name for a parameter-styled hash, for example: + * `http://path/to/my/page.html#map=2.59/39.26/53.07/-24.1/60&foo=bar` * @param {boolean} [options.interactive=true] If `false`, no mouse, touch, or keyboard listeners will be attached to the map, so it will not respond to interaction. * @param {number} [options.bearingSnap=7] The threshold, measured in degrees, that determines when the map's * bearing will snap to north. For example, with a `bearingSnap` of 7, if the user rotates @@ -378,7 +380,7 @@ class Map extends Camera { bindHandlers(this, options); - this._hash = options.hash && (new Hash()).addTo(this); + this._hash = options.hash && (new Hash(options.hash)).addTo(this); // don't set position from options if set through hash if (!this._hash || !this._hash._onHashChange()) { this.jumpTo({ diff --git a/test/unit/ui/hash.test.js b/test/unit/ui/hash.test.js index d3cf7306e58..59baa4d20f2 100644 --- a/test/unit/ui/hash.test.js +++ b/test/unit/ui/hash.test.js @@ -4,8 +4,8 @@ import window from '../../../src/util/window'; import { createMap as globalCreateMap } from '../../util'; test('hash', (t) => { - function createHash() { - const hash = new Hash(); + function createHash(name) { + const hash = new Hash(name); hash._updateHash = hash._updateHashUnthrottled.bind(hash); return hash; } @@ -100,6 +100,70 @@ test('hash', (t) => { t.end(); }); + t.test('#_onHashChange named', (t) => { + const map = createMap(t); + const hash = createHash('map') + .addTo(map); + + window.location.hash = '#map=10/3.00/-1.00&foo=bar'; + + hash._onHashChange(); + + t.equal(map.getCenter().lng, -1); + t.equal(map.getCenter().lat, 3); + t.equal(map.getZoom(), 10); + t.equal(map.getBearing(), 0); + t.equal(map.getPitch(), 0); + + window.location.hash = ''; + + t.end(); + }); + + t.test('#_getCurrentHash', (t) => { + const map = createMap(t); + const hash = createHash() + .addTo(map); + + window.location.hash = '#10/3.00/-1.00'; + + const currentHash = hash._getCurrentHash(); + + t.equal(currentHash[0], '10'); + t.equal(currentHash[1], '3.00'); + t.equal(currentHash[2], '-1.00'); + + window.location.hash = ''; + + t.end(); + }); + + t.test('#_getCurrentHash named', (t) => { + const map = createMap(t); + const hash = createHash('map') + .addTo(map); + + window.location.hash = '#map=10/3.00/-1.00&foo=bar'; + + let currentHash = hash._getCurrentHash(); + + t.equal(currentHash[0], '10'); + t.equal(currentHash[1], '3.00'); + t.equal(currentHash[2], '-1.00'); + + window.location.hash = '#baz&map=10/3.00/-1.00'; + + currentHash = hash._getCurrentHash(); + + t.equal(currentHash[0], '10'); + t.equal(currentHash[1], '3.00'); + t.equal(currentHash[2], '-1.00'); + + window.location.hash = ''; + + t.end(); + }); + t.test('#_updateHash', (t) => { function getHash() { return window.location.hash.split('/'); @@ -145,6 +209,47 @@ test('hash', (t) => { t.equal(newHash[3], '135'); t.equal(newHash[4], '60'); + window.location.hash = ''; + + t.end(); + }); + + t.test('#_updateHash named', (t) => { + const map = createMap(t); + createHash('map') + .addTo(map); + + t.notok(window.location.hash); + + map.setZoom(3); + map.setCenter([1.0, 2.0]); + + t.ok(window.location.hash); + + t.equal(window.location.hash, '#map=3/2/1'); + + map.setPitch(60); + + t.equal(window.location.hash, '#map=3/2/1/0/60'); + + map.setBearing(135); + + t.equal(window.location.hash, '#map=3/2/1/135/60'); + + window.location.hash += '&foo=bar'; + + map.setZoom(7); + + t.equal(window.location.hash, '#map=7/2/1/135/60&foo=bar'); + + window.location.hash = '#baz&map=7/2/1/135/60&foo=bar'; + + map.setCenter([2.0, 1.0]); + + t.equal(window.location.hash, '#baz&map=7/1/2/135/60&foo=bar'); + + window.location.hash = ''; + t.end(); });