Skip to content

Commit 6143e6d

Browse files
authored
Refactor hash methods to accept callbacks (#12188)
* refactor hash methods to accept callbacks * streamline hash updates
2 parents c4021d0 + 69de647 commit 6143e6d

12 files changed

Lines changed: 110 additions & 189 deletions

File tree

modules/behavior/hash.js

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,71 @@ import { select as d3_select } from 'd3-selection';
55
import { geoSphericalDistance } from '../geo';
66
import { modeBrowse } from '../modes/browse';
77
import { modeSelect, modeSelectNote } from '../modes';
8-
import { utilObjectOmit, utilQsString, utilStringQs } from '../util';
8+
import { utilQsString, utilStringQs } from '../util';
99
import { utilArrayIdentical } from '../util/array';
1010
import { utilDisplayLabel } from '../util/utilDisplayLabel';
1111
import { localizer, t } from '../core/localizer';
1212
import { prefs } from '../core/preferences';
1313

14+
function getNewHash(arg) {
15+
const original = utilStringQs(window.location.hash);
16+
const update = typeof arg === 'function' ? arg(original) : arg;
17+
if (!update || typeof update !== 'object') return;
18+
19+
const updated = { ...original, ...update };
20+
Object.keys(update)
21+
.filter(key => update[key] === null || update[key] === undefined)
22+
.forEach(key => delete updated[key]);
23+
24+
return '#' + utilQsString(updated, true);
25+
}
26+
27+
/**
28+
* Updates the URL hash by applying a partial patch.
29+
*
30+
* Keys with nullish values will be removed from the hash.
31+
*
32+
* @param {(?Object<string, any>|function (Object<string, any>): Object<string, any>)} updater Either
33+
* - a plain object of key/value pairs to merge into the hash, or
34+
* - a function `(currentHash) => patchObject` that returns such an object.
35+
* @returns {boolean} Whether the hash was updated.
36+
*/
37+
export function patchHash(updater) {
38+
if (!updater || !['function', 'object'].includes(typeof updater)) return false;
39+
40+
const latestHash = getNewHash(updater);
41+
if (!latestHash || window.location.hash === latestHash) return false;
42+
43+
// Update the URL hash without affecting the browser navigation stack,
44+
// though unavoidably creating a browser history entry
45+
window.history.replaceState(null, '', latestHash);
46+
47+
// save last used map location for future
48+
const { map } = utilStringQs(latestHash);
49+
if (map) prefs('map-location', map);
50+
return true;
51+
}
1452

1553
export function behaviorHash(context) {
1654

17-
// cached window.location.hash
18-
var _cachedHash = null;
1955
// allowable latitude range
2056
var _latitudeLimit = 90 - 1e-8;
2157

22-
function computedHashParameters() {
58+
function computeHashUpdate() {
59+
if (context.inIntro()) return null;
60+
2361
var map = context.map();
2462
var center = map.center();
2563
var zoom = map.zoom();
2664
var precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
27-
var oldParams = utilObjectOmit(utilStringQs(window.location.hash),
28-
['comment', 'source', 'hashtags', 'walkthrough']
29-
);
30-
var newParams = {};
65+
const newParams = {
66+
comment: null,
67+
source: null,
68+
hashtags: null,
69+
walkthrough: null,
70+
id: null
71+
};
3172

32-
delete oldParams.id;
3373
var selected = context.selectedIDs().filter(function(id) {
3474
return context.hasEntity(id);
3575
});
@@ -43,11 +83,7 @@ export function behaviorHash(context) {
4383
'/' + center[1].toFixed(precision) +
4484
'/' + center[0].toFixed(precision);
4585

46-
return Object.assign(oldParams, newParams);
47-
}
48-
49-
function computedHash() {
50-
return '#' + utilQsString(computedHashParameters(), true);
86+
return newParams;
5187
}
5288

5389
function computedTitle(includeChangeCount) {
@@ -91,7 +127,7 @@ export function behaviorHash(context) {
91127
return baseTitle;
92128
}
93129

94-
function updateTitle(includeChangeCount) {
130+
function updateTitle(includeChangeCount = true) {
95131
if (!context.setsDocumentTitle()) return;
96132

97133
var newTitle = computedTitle(includeChangeCount);
@@ -100,40 +136,14 @@ export function behaviorHash(context) {
100136
}
101137
}
102138

103-
function updateHashIfNeeded() {
104-
if (context.inIntro()) return;
105-
106-
var latestHash = computedHash();
107-
if (_cachedHash !== latestHash) {
108-
_cachedHash = latestHash;
109-
110-
// Update the URL hash without affecting the browser navigation stack,
111-
// though unavoidably creating a browser history entry
112-
window.history.replaceState(null, '', latestHash);
113-
114-
// set the title we want displayed for the browser tab/window
115-
updateTitle(true /* includeChangeCount */);
116-
117-
// save last used map location for future
118-
const q = utilStringQs(latestHash);
119-
if (q.map) {
120-
prefs('map-location', q.map);
121-
}
122-
}
123-
}
124-
125-
var _throttledUpdate = throttle(updateHashIfNeeded, 500);
126-
var _throttledUpdateTitle = throttle(function() {
127-
updateTitle(true /* includeChangeCount */);
139+
var _throttledUpdate = throttle(() => {
140+
patchHash(computeHashUpdate);
141+
updateTitle();
128142
}, 500);
143+
var _throttledUpdateTitle = throttle(updateTitle, 500);
129144

130145
function hashchange() {
131-
// ignore spurious hashchange events
132-
if (window.location.hash === _cachedHash) return;
133-
134-
_cachedHash = window.location.hash;
135-
136-
var q = utilStringQs(_cachedHash);
146+
var q = utilStringQs(window.location.hash);
137147

138148
if (q.theme) {
139149
context.theme(q.theme);
@@ -147,11 +157,11 @@ export function behaviorHash(context) {
147157
var mapArgs = (q.map || '').split('/').map(Number);
148158
if (mapArgs.length < 3 || mapArgs.some(isNaN)) {
149159
// replace bogus hash
150-
updateHashIfNeeded();
151-
160+
patchHash(computeHashUpdate);
161+
updateTitle();
152162
} else {
153163
// don't update if the new hash already reflects the state of iD
154-
if (_cachedHash === computedHash()) return;
164+
if (window.location.hash === getNewHash(computeHashUpdate)) return;
155165

156166
var mode = context.mode();
157167

@@ -224,14 +234,14 @@ export function behaviorHash(context) {
224234
const mapArgs = prefs('map-location').split('/').map(Number);
225235
context.map().centerZoom([mapArgs[2], Math.min(_latitudeLimit, Math.max(-_latitudeLimit, mapArgs[1]))], mapArgs[0]);
226236

227-
updateHashIfNeeded();
237+
patchHash(computeHashUpdate);
228238

229239
behavior.hadLocation = true;
230240
}
231241

232242
hashchange();
233243

234-
updateTitle(false);
244+
updateTitle(false /* includeChangeCount */);
235245
}
236246

237247
behavior.off = function() {

modules/behavior/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export { behaviorDrag } from './drag';
44
export { behaviorDrawWay } from './draw_way';
55
export { behaviorDraw } from './draw';
66
export { behaviorEdit } from './edit';
7-
export { behaviorHash } from './hash';
7+
export { behaviorHash, patchHash } from './hash';
88
export { behaviorHover } from './hover';
99
export { behaviorLasso } from './lasso';
1010
export { behaviorOperation } from './operation';

modules/renderer/background.js

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import { fileFetcher } from '../core/file_fetcher';
1111
import { geoMetersToOffset, geoOffsetToMeters, geoExtent } from '../geo';
1212
import { rendererBackgroundSource } from './background_source';
1313
import { rendererTileLayer } from './tile_layer';
14-
import { utilQsString, utilStringQs } from '../util';
14+
import { utilStringQs } from '../util';
1515
import { utilRebind } from '../util/rebind';
16+
import { patchHash } from '../behavior';
1617

1718

1819
let _imageryIndex = null;
@@ -200,32 +201,18 @@ export function rendererBackground(context) {
200201
const EPSILON = 0.01;
201202
const x = +meters[0].toFixed(2);
202203
const y = +meters[1].toFixed(2);
203-
let hash = utilStringQs(window.location.hash);
204+
const notableOffset = Math.abs(x) > EPSILON || Math.abs(y) > EPSILON;
204205

205206
let id = currSource.id;
206207
if (id === 'custom') {
207208
id = `custom:${currSource.template()}`;
208209
}
209210

210-
if (id) {
211-
hash.background = id;
212-
} else {
213-
delete hash.background;
214-
}
215-
216-
if (o) {
217-
hash.overlays = o;
218-
} else {
219-
delete hash.overlays;
220-
}
221-
222-
if (Math.abs(x) > EPSILON || Math.abs(y) > EPSILON) {
223-
hash.offset = `${x},${y}`;
224-
} else {
225-
delete hash.offset;
226-
}
227-
228-
window.history.replaceState(null, '', '#' + utilQsString(hash, true));
211+
patchHash({
212+
background: id || null,
213+
overlays: o || null,
214+
offset: notableOffset ? `${x},${y}` : null
215+
});
229216

230217
let imageryUsed = [];
231218
let photoOverlaysUsed = [];

modules/renderer/features.js

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { prefs } from '../core/preferences';
44
import { osmEntity } from '../osm';
55
import { osmLanduseTags, osmLifecyclePrefixes } from '../osm/tags.js';
66
import { utilRebind } from '../util/rebind';
7-
import { utilArrayGroupBy, utilArrayUnion, utilQsString, utilStringQs } from '../util';
7+
import { utilArrayGroupBy, utilArrayUnion, utilStringQs } from '../util';
88
import { isAddressPoint } from '../svg/labels';
9+
import { patchHash } from '../behavior';
910

1011

1112
export function rendererFeatures(context) {
@@ -56,15 +57,9 @@ export function rendererFeatures(context) {
5657

5758

5859
function update() {
59-
const hash = utilStringQs(window.location.hash);
60-
const disabled = features.disabled();
61-
if (disabled.length) {
62-
hash.disable_features = disabled.join(',');
63-
} else {
64-
delete hash.disable_features;
65-
}
66-
window.history.replaceState(null, '', '#' + utilQsString(hash, true));
67-
prefs('disabled-features', disabled.join(','));
60+
const disabled = features.disabled().join(',');
61+
patchHash({ disable_features: disabled || null });
62+
prefs('disabled-features', disabled);
6863
_hidden = features.hidden();
6964
dispatch.call('change');
7065
dispatch.call('redraw');

modules/renderer/photos.js

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { dispatch as d3_dispatch } from 'd3-dispatch';
22

33
import { services } from '../services';
44
import { utilRebind } from '../util/rebind';
5-
import { utilQsString, utilStringQs } from '../util';
5+
import { utilStringQs } from '../util';
6+
import { patchHash } from '../behavior';
67

78

89
export function rendererPhotos(context) {
@@ -18,18 +19,12 @@ export function rendererPhotos(context) {
1819
function photos() {}
1920

2021
function updateStorage() {
21-
var hash = utilStringQs(window.location.hash);
2222
var enabled = context.layers().all().filter(function(d) {
2323
return _layerIDs.indexOf(d.id) !== -1 && d.layer && d.layer.supported() && d.layer.enabled();
2424
}).map(function(d) {
2525
return d.id;
2626
});
27-
if (enabled.length) {
28-
hash.photo_overlay = enabled.join(',');
29-
} else {
30-
delete hash.photo_overlay;
31-
}
32-
window.history.replaceState(null, '', '#' + utilQsString(hash, true));
27+
patchHash({ photo_overlay: enabled.join(',') || null });
3328
}
3429

3530
/**
@@ -148,15 +143,7 @@ export function rendererPhotos(context) {
148143
* @param {string} property Name of the value
149144
*/
150145
function setUrlFilterValue(property, val) {
151-
const hash = utilStringQs(window.location.hash);
152-
if (val) {
153-
if (hash[property] === val) return;
154-
hash[property] = val;
155-
} else {
156-
if (!(property in hash)) return;
157-
delete hash[property];
158-
}
159-
window.history.replaceState(null, '', '#' + utilQsString(hash, true));
146+
patchHash({ [property]: val || null });
160147
}
161148

162149
function showsLayer(id) {

modules/services/kartaview.js

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { zoom as d3_zoom, zoomIdentity as d3_zoomIdentity } from 'd3-zoom';
55
import RBush from 'rbush';
66

77
import { geoExtent, geoScaleToZoom } from '../geo';
8-
import { utilQsString, utilRebind, utilSetTransform, utilStringQs, utilTiler } from '../util';
8+
import { utilQsString, utilRebind, utilSetTransform, utilTiler } from '../util';
99
import { services } from './';
1010
import { searchLimited } from '../util/partition';
1111
import { localeDateString } from '../util/date';
12+
import { patchHash } from '../behavior';
1213

1314

1415
var apibase = 'https://kartaview.org';
@@ -363,7 +364,7 @@ export default {
363364
hideViewer: function(context) {
364365
_oscSelectedImage = null;
365366

366-
this.updateUrlImage(null);
367+
patchHash({ photo: null });
367368

368369
var viewer = context.container().select('.photoviewer');
369370
if (!viewer.empty()) viewer.datum(null);
@@ -386,7 +387,7 @@ export default {
386387

387388
_oscSelectedImage = d;
388389

389-
this.updateUrlImage(imageKey);
390+
patchHash({ photo: 'kartaview/' + imageKey });
390391

391392
var viewer = context.container().select('.photoviewer');
392393
if (!viewer.empty()) viewer.datum(d);
@@ -517,17 +518,6 @@ export default {
517518
},
518519

519520

520-
updateUrlImage: function(imageKey) {
521-
const hash = utilStringQs(window.location.hash);
522-
if (imageKey) {
523-
hash.photo = 'kartaview/' + imageKey;
524-
} else {
525-
delete hash.photo;
526-
}
527-
window.history.replaceState(null, '', '#' + utilQsString(hash, true));
528-
},
529-
530-
531521
cache: function() {
532522
return _oscCache;
533523
}

0 commit comments

Comments
 (0)