diff --git a/API.md b/API.md
index c228b08aa..22044dcc9 100644
--- a/API.md
+++ b/API.md
@@ -15,7 +15,7 @@ of iD (e.g. `https://ideditor-release.netlify.app`), the following parameters ar
`{z}`/`{zoom}`, `{ty}` for flipped TMS-style Y coordinates, and `{switch:a,b,c}` for
DNS multiplexing.
_Example:_ `background=custom:https://tile.openstreetmap.org/{zoom}/{x}/{y}.png`
-* __`comment`__ - Prefills the changeset comment. Pass a url encoded string.
+* __`comment`__ - Prefills the changeset comment.
_Example:_ `comment=CAR%20crisis%2C%20refugee%20areas%20in%20Cameroon`
* __`disable_features`__ - Disables features in the list.
_Example:_ `disable_features=water,service_roads,points,paths,boundaries`
@@ -24,7 +24,7 @@ of iD (e.g. `https://ideditor-release.netlify.app`), the following parameters ar
* __`gpx`__ - A custom URL for loading a gpx track. Specifying a `gpx` parameter will
automatically enable the gpx layer for display.
_Example:_ `gpx=https://gist.githubusercontent.com/answerquest/9445352b60ca5b44714675eae00f243a/raw/56a6343a29223318f4a697bfd16cbb2c3b8155ad/sample_boundary.gpx`
-* __`hashtags`__ - Prefills the changeset hashtags. Pass a url encoded list of event
+* __`hashtags`__ - Prefills the changeset hashtags. Pass a list of event
hashtags separated by commas, semicolons, or spaces. Leading '#' symbols are
optional and will be added automatically. (Note that hashtag-like strings are
automatically detected in the `comment`).
@@ -55,16 +55,19 @@ of iD (e.g. `https://ideditor-release.netlify.app`), the following parameters ar
* __`presets`__ - A comma-separated list of preset IDs. These will be the only presets the user may select.
_Example:_ `presets=building,highway/residential,highway/unclassified`
* __`rtl=true`__ - Force iD into right-to-left mode (useful for testing).
-* __`source`__ - Prefills the changeset source. Pass a url encoded string.
+* __`source`__ - Prefills the changeset source.
_Example:_ `source=Bing%3BMapillary`
-* __`validationDisable`__ - The issues identified by these types/subtypes will be disabled (i.e. Issues will not be shown at all). Each parameter value should contain a urlencoded, comma-separated list of type/subtype match rules. An asterisk `*` may be used as a wildcard.
+* __`validationDisable`__ - The issues identified by these types/subtypes will be disabled (i.e. Issues will not be shown at all). Each parameter value should contain a comma-separated list of type/subtype match rules. An asterisk `*` may be used as a wildcard.
_Example:_ `validationDisable=crossing_ways/highway*,crossing_ways/tunnel*`
-* __`validationWarning`__ - The issues identified by these types/subtypes will be treated as warnings (i.e. Issues will be surfaced to the user but not block changeset upload). Each parameter value should contain a urlencoded, comma-separated list of type/subtype match rules. An asterisk `*` may be used as a wildcard.
+* __`validationWarning`__ - The issues identified by these types/subtypes will be treated as warnings (i.e. Issues will be surfaced to the user but not block changeset upload). Each parameter value should contain a comma-separated list of type/subtype match rules. An asterisk `*` may be used as a wildcard.
_Example:_ `validationWarning=crossing_ways/highway*,crossing_ways/tunnel*`
-* __`validationError`__ - The issues identified by these types/subtypes will be treated as errors (i.e. Issues will be surfaced to the user but will block changeset upload). Each parameter value should contain a urlencoded, comma-separated list of type/subtype match rules. An asterisk `*` may be used as a wildcard.
+* __`validationError`__ - The issues identified by these types/subtypes will be treated as errors (i.e. Issues will be surfaced to the user but will block changeset upload). Each parameter value should contain a comma-separated list of type/subtype match rules. An asterisk `*` may be used as a wildcard.
_Example:_ `validationError=crossing_ways/highway*,crossing_ways/tunnel*`
* __`walkthrough=true`__ - Start the walkthrough automatically
+Pass these parameters as a `x-www-form-urlencoded` string in the _hash_ portion of the URL, similarly to how you how a _query_ string of an URL is typically constructed. Input strings have to be [percent encoded](https://en.wikipedia.org/wiki/Percent-encoding) (spaces can be represented either as `+` or `%20`), for example using [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/toString) in Javascript.
+
+
##### iD on openstreetmap.org (Rails Port)
When constructing a URL to an instance of iD embedded on the [OpenStreetMap website](github.com/openstreetmap/openstreetmap-website/) (e.g. `https://www.openstreetmap.org/edit?editor=id`), the following parameters
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 212e247e3..64d7f7c89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
* Preserve imagery offset during tile layer switching transition ([#10748])
* Fix over-saturated map tiles near the border of the tile service's coverage area ([#10747], thanks [@hlfan])
* Fix too dim markers of selected/hovered photo of some street level imagery layers ([#10755], thanks [@draunger])
+* Fix `+` symbol appearing in changeset comments from external tools ([#10766], thanks [@k-yle])
#### :earth_asia: Localization
#### :hourglass: Performance
#### :mortar_board: Walkthrough / Help
@@ -63,6 +64,7 @@ _Breaking developer changes, which may affect downstream projects or sites that
[#10747]: https://github.com/openstreetmap/iD/issues/10747
[#10748]: https://github.com/openstreetmap/iD/issues/10748
[#10755]: https://github.com/openstreetmap/iD/issues/10755
+[#10766]: https://github.com/openstreetmap/iD/pull/10766
[@hlfan]: https://github.com/hlfan
[@Deeptanshu-sankhwar]: https://github.com/Deeptanshu-sankhwar
[@draunger]: https://github.com/draunger
diff --git a/modules/util/util.js b/modules/util/util.js
index 8002c0c2c..0039ef152 100644
--- a/modules/util/util.js
+++ b/modules/util/util.js
@@ -358,31 +358,21 @@ export function utilCombinedTags(entityIDs, graph) {
export function utilStringQs(str) {
- var i = 0; // advance past any leading '?' or '#' characters
- while (i < str.length && (str[i] === '?' || str[i] === '#')) i++;
- str = str.slice(i);
-
- return str.split('&').reduce(function(obj, pair){
- var parts = pair.split('=');
- if (parts.length === 2) {
- obj[parts[0]] = (null === parts[1]) ? '' : decodeURIComponent(parts[1]);
- }
- return obj;
- }, {});
+ str = str.replace(/^[#?]{0,2}/, ''); // advance past any leading '?' or '#' characters
+ return Object.fromEntries(new URLSearchParams(str));
}
-export function utilQsString(obj, noencode) {
- // encode everything except special characters used in certain hash parameters:
- // "/" in map states, ":", ",", {" and "}" in background
- function softEncode(s) {
- return encodeURIComponent(s).replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
+export function utilQsString(obj, softEncode) {
+ let str = new URLSearchParams(obj).toString();
+ if (softEncode) {
+ // for better readability of URL hashes: optionally
+ // leave some special characters unescaped
+ // "/" used in map state
+ // ":", ",", {" and "}" used in background param
+ str = str.replace(/(%2F|%3A|%2C|%7B|%7D)/g, decodeURIComponent);
}
-
- return Object.keys(obj).sort().map(function(key) {
- return encodeURIComponent(key) + '=' + (
- noencode ? softEncode(obj[key]) : encodeURIComponent(obj[key]));
- }).join('&');
+ return str;
}
diff --git a/test/spec/behavior/hash.js b/test/spec/behavior/hash.js
index d2d388a29..226a11161 100644
--- a/test/spec/behavior/hash.js
+++ b/test/spec/behavior/hash.js
@@ -79,4 +79,13 @@ describe('iD.behaviorHash', function () {
done();
}, 600);
});
+
+ it('accepts default changeset comment as hash parameter', function () {
+ window.location.hash = '#comment=foo+bar%20%2B1';
+ var container = d3.select(document.createElement('div'));
+ context = iD.coreContext().assetPath('../dist/').init().container(container);
+ iD.behaviorHash(context);
+ expect(context.defaultChangesetComment()).to.eql('foo bar +1');
+ hash.off();
+ });
});
diff --git a/test/spec/util/util.js b/test/spec/util/util.js
index c6e702b17..1ec9fdca1 100644
--- a/test/spec/util/util.js
+++ b/test/spec/util/util.js
@@ -80,31 +80,35 @@ describe('iD.util', function() {
describe('utilStringQs', function() {
it('splits a parameter string into k=v pairs', function() {
+ expect(iD.utilStringQs('')).to.eql({});
expect(iD.utilStringQs('foo=bar')).to.eql({foo: 'bar'});
expect(iD.utilStringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' });
- expect(iD.utilStringQs('')).to.eql({});
+ expect(iD.utilStringQs('foo=bar baz')).to.eql({foo: 'bar baz'});
+ expect(iD.utilStringQs('foo=bar+baz')).to.eql({foo: 'bar baz'});
+ expect(iD.utilStringQs('foo=bar%20baz')).to.eql({foo: 'bar baz'});
});
it('trims leading # if present', function() {
expect(iD.utilStringQs('#foo=bar')).to.eql({foo: 'bar'});
- expect(iD.utilStringQs('#foo=bar&one=2')).to.eql({foo: 'bar', one: '2' });
- expect(iD.utilStringQs('#')).to.eql({});
});
it('trims leading ? if present', function() {
expect(iD.utilStringQs('?foo=bar')).to.eql({foo: 'bar'});
- expect(iD.utilStringQs('?foo=bar&one=2')).to.eql({foo: 'bar', one: '2' });
- expect(iD.utilStringQs('?')).to.eql({});
});
it('trims leading #? if present', function() {
expect(iD.utilStringQs('#?foo=bar')).to.eql({foo: 'bar'});
- expect(iD.utilStringQs('#?foo=bar&one=2')).to.eql({foo: 'bar', one: '2' });
+ });
+ it('supports both + and %20 for escaping spaces', function() {
+ expect(iD.utilStringQs('#?foo=a+b%20c')).to.eql({foo: 'a b c'});
expect(iD.utilStringQs('#?')).to.eql({});
});
});
it('utilQsString', function() {
+ expect(iD.utilQsString({})).to.eql('');
expect(iD.utilQsString({ foo: 'bar' })).to.eql('foo=bar');
expect(iD.utilQsString({ foo: 'bar', one: 2 })).to.eql('foo=bar&one=2');
- expect(iD.utilQsString({})).to.eql('');
+ expect(iD.utilQsString({ foo: 'bar baz' })).to.be.oneOf(['foo=bar%20baz', 'foo=bar+baz']);
+ expect(iD.utilQsString({ foo: 'bar/baz' })).to.eql('foo=bar%2Fbaz');
+ expect(iD.utilQsString({ foo: 'bar/baz' }, true)).to.eql('foo=bar/baz');
});
describe('utilEditDistance', function() {